aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorPhilippe Perrin <philippe.perrin@sonarsource.com>2020-11-18 13:23:52 +0100
committersonartech <sonartech@sonarsource.com>2020-12-02 20:06:58 +0000
commit9dea56b68988cf5c77a51a9fb6a3b076c546cb93 (patch)
tree0596673d8f33f4c1065de030679eb9685518b52a /server/sonar-web
parent346d2c41b28c15d99cc092ac792f36b6d3ffd9da (diff)
downloadsonarqube-9dea56b68988cf5c77a51a9fb6a3b076c546cb93.tar.gz
sonarqube-9dea56b68988cf5c77a51a9fb6a3b076c546cb93.zip
SONAR-14079 Add Azure Pipelines tutorial for Azure DevOps Server
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/TutorialSelection.tsx8
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx25
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelection-test.tsx9
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelectionRenderer-test.tsx18
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/__tests__/__snapshots__/TutorialSelectionRenderer-test.tsx.snap48
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/azure-pipelines/AzurePipelinesTutorial.tsx130
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/azure-pipelines/BranchAnalysisStepContent.tsx208
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/azure-pipelines/ExtensionInstallationStepContent.tsx55
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/azure-pipelines/SaveAndRunStepContent.tsx66
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/azure-pipelines/ServiceEndpointStepContent.tsx92
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/AzurePipelinesTutorial-test.tsx111
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/BranchAnalysisStepContent-test.tsx58
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/ExtensionInstallationStepContent-test.tsx32
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/SaveAndRunStepContent-test.tsx32
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/ServiceEndpointStepContent-test.tsx52
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/__snapshots__/AzurePipelinesTutorial-test.tsx.snap128
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/__snapshots__/BranchAnalysisStepContent-test.tsx.snap613
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/__snapshots__/ExtensionInstallationStepContent-test.tsx.snap24
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/__snapshots__/SaveAndRunStepContent-test.tsx.snap54
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/__snapshots__/ServiceEndpointStepContent-test.tsx.snap66
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/gitlabci/GitLabCITutorial.tsx6
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/jenkins/JenkinsTutorial.tsx11
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/jenkins/MultiBranchPipelineStep.tsx6
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/jenkins/WebhookStep.tsx6
-rw-r--r--server/sonar-web/src/main/js/components/tutorials/types.ts3
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/alm-settings-test.ts67
-rw-r--r--server/sonar-web/src/main/js/helpers/alm-settings.ts66
-rw-r--r--server/sonar-web/src/main/js/helpers/mocks/alm-settings.ts14
-rw-r--r--server/sonar-web/src/main/js/types/alm-settings.ts50
29 files changed, 1895 insertions, 163 deletions
diff --git a/server/sonar-web/src/main/js/components/tutorials/TutorialSelection.tsx b/server/sonar-web/src/main/js/components/tutorials/TutorialSelection.tsx
index a077d399d1a..24095a0d008 100644
--- a/server/sonar-web/src/main/js/components/tutorials/TutorialSelection.tsx
+++ b/server/sonar-web/src/main/js/components/tutorials/TutorialSelection.tsx
@@ -20,7 +20,7 @@
import * as React from 'react';
import { WithRouterProps } from 'react-router';
import { getAlmDefinitionsNoCatch, getProjectAlmBinding } from '../../api/alm-settings';
-import { AlmBindingDefinition, AlmKeys, ProjectAlmBindingResponse } from '../../types/alm-settings';
+import { AlmBindingDefinition, ProjectAlmBindingResponse } from '../../types/alm-settings';
import { withRouter } from '../hoc/withRouter';
import TutorialSelectionRenderer from './TutorialSelectionRenderer';
import { TutorialModes } from './types';
@@ -58,11 +58,7 @@ export class TutorialSelection extends React.PureComponent<Props, State> {
]);
if (this.mounted) {
- // We only support Bitbucket, GitHub & Gitlab for now.
- if (
- projectBinding === undefined ||
- ![AlmKeys.Bitbucket, AlmKeys.GitHub, AlmKeys.GitLab].includes(projectBinding.alm)
- ) {
+ if (projectBinding === undefined) {
this.setState({ loading: false, forceManual: true });
} else {
let almBinding;
diff --git a/server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx b/server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx
index 1fc339d8dcf..1cf814c2646 100644
--- a/server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx
+++ b/server/sonar-web/src/main/js/components/tutorials/TutorialSelectionRenderer.tsx
@@ -22,6 +22,7 @@ import * as React from 'react';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { getBaseUrl } from 'sonar-ui-common/helpers/urls';
import { AlmBindingDefinition, AlmKeys, ProjectAlmBindingResponse } from '../../types/alm-settings';
+import AzurePipelinesTutorial from './azure-pipelines/AzurePipelinesTutorial';
import GitLabCITutorial from './gitlabci/GitLabCITutorial';
import JenkinsTutorial from './jenkins/JenkinsTutorial';
import ManualTutorial from './manual/ManualTutorial';
@@ -74,6 +75,22 @@ export default function TutorialSelectionRenderer(props: TutorialSelectionRender
</button>
)}
+ {projectBinding?.alm === AlmKeys.Azure && (
+ <button
+ className="button button-huge display-flex-column spacer-left spacer-right azure-pipelines"
+ onClick={() => props.onSelectTutorial(TutorialModes.AzurePipelines)}
+ type="button">
+ <img
+ alt="" // Should be ignored by screen readers
+ height={80}
+ src={`${getBaseUrl()}/images/alm/azure.svg`}
+ />
+ <div className="medium big-spacer-top">
+ {translate('onboarding.tutorial.choose_method.azure_pipelines')}
+ </div>
+ </button>
+ )}
+
{jenkinsAvailable && (
<button
className="button button-huge display-flex-column spacer-left spacer-right tutorial-mode-jenkins"
@@ -126,6 +143,14 @@ export default function TutorialSelectionRenderer(props: TutorialSelectionRender
projectBinding={projectBinding}
/>
)}
+
+ {selectedTutorial === TutorialModes.AzurePipelines && projectBinding !== undefined && (
+ <AzurePipelinesTutorial
+ component={component}
+ currentUser={currentUser}
+ projectBinding={projectBinding}
+ />
+ )}
</>
);
}
diff --git a/server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelection-test.tsx b/server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelection-test.tsx
index 06ba0070978..9585564dcc5 100644
--- a/server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelection-test.tsx
+++ b/server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelection-test.tsx
@@ -50,20 +50,13 @@ it('should select manual if project is not bound', async () => {
expect(wrapper.state().forceManual).toBe(true);
});
-it('should not select anything if project is bound to Bitbucket', async () => {
+it('should not select anything if project is bound', async () => {
(getProjectAlmBinding as jest.Mock).mockResolvedValueOnce({ alm: AlmKeys.Bitbucket });
const wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(wrapper.state().forceManual).toBe(false);
});
-it('should select manual if project is bound to unsupported ALM', async () => {
- (getProjectAlmBinding as jest.Mock).mockResolvedValueOnce({ alm: AlmKeys.Azure });
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- expect(wrapper.state().forceManual).toBe(true);
-});
-
it('should correctly find the global ALM binding definition', async () => {
const key = 'foo';
const almBinding = mockBitbucketBindingDefinition({ key });
diff --git a/server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelectionRenderer-test.tsx b/server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelectionRenderer-test.tsx
index bd45cbac9c1..9231d66da02 100644
--- a/server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelectionRenderer-test.tsx
+++ b/server/sonar-web/src/main/js/components/tutorials/__tests__/TutorialSelectionRenderer-test.tsx
@@ -23,6 +23,7 @@ import * as React from 'react';
import { click } from 'sonar-ui-common/helpers/testUtils';
import {
mockBitbucketBindingDefinition,
+ mockProjectAzureBindingResponse,
mockProjectBitbucketBindingResponse,
mockProjectGitLabBindingResponse
} from '../../../helpers/mocks/alm-settings';
@@ -53,6 +54,12 @@ it('should render correctly', () => {
projectBinding: mockProjectGitLabBindingResponse()
})
).toMatchSnapshot('gitlab tutorial');
+ expect(
+ shallowRender({
+ selectedTutorial: TutorialModes.AzurePipelines,
+ projectBinding: mockProjectAzureBindingResponse()
+ })
+ ).toMatchSnapshot('azure pipelines tutorial');
});
it('should allow mode selection', () => {
@@ -80,6 +87,17 @@ it('should allow gitlab selection', () => {
expect(onSelectTutorial).toHaveBeenLastCalledWith(TutorialModes.GitLabCI);
});
+it('should allow azure pipelines selection', () => {
+ const onSelectTutorial = jest.fn();
+ const wrapper = shallowRender({
+ onSelectTutorial,
+ projectBinding: mockProjectAzureBindingResponse()
+ });
+
+ click(wrapper.find('button.azure-pipelines'));
+ expect(onSelectTutorial).toHaveBeenLastCalledWith(TutorialModes.AzurePipelines);
+});
+
function shallowRender(props: Partial<TutorialSelectionRendererProps> = {}) {
return shallow<TutorialSelectionRendererProps>(
<TutorialSelectionRenderer
diff --git a/server/sonar-web/src/main/js/components/tutorials/__tests__/__snapshots__/TutorialSelectionRenderer-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/__tests__/__snapshots__/TutorialSelectionRenderer-test.tsx.snap
index ef26e810291..10796cbddc1 100644
--- a/server/sonar-web/src/main/js/components/tutorials/__tests__/__snapshots__/TutorialSelectionRenderer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/tutorials/__tests__/__snapshots__/TutorialSelectionRenderer-test.tsx.snap
@@ -1,5 +1,53 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`should render correctly: azure pipelines tutorial 1`] = `
+<Fragment>
+ <AzurePipelinesTutorial
+ component={
+ Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "deleted": false,
+ "key": "my-qp",
+ "language": "ts",
+ "name": "Sonar way",
+ },
+ ],
+ "tags": Array [],
+ }
+ }
+ currentUser={
+ Object {
+ "groups": Array [],
+ "isLoggedIn": true,
+ "login": "luke",
+ "name": "Skywalker",
+ "scmAccounts": Array [],
+ }
+ }
+ projectBinding={
+ Object {
+ "alm": "azure",
+ "key": "foo",
+ "repository": "REPOSITORY_NAME",
+ "slug": "PROJECT_NAME",
+ "url": "https://ado.my_company.com/mycollection",
+ }
+ }
+ />
+</Fragment>
+`;
+
exports[`should render correctly: gitlab tutorial 1`] = `
<Fragment>
<GitLabCITutorial
diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/AzurePipelinesTutorial.tsx b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/AzurePipelinesTutorial.tsx
new file mode 100644
index 00000000000..8cde6833184
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/AzurePipelinesTutorial.tsx
@@ -0,0 +1,130 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import * as React from 'react';
+import { Button } from 'sonar-ui-common/components/controls/buttons';
+import { Alert } from 'sonar-ui-common/components/ui/Alert';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import {
+ isProjectAzureBindingResponse,
+ ProjectAlmBindingResponse
+} from '../../../types/alm-settings';
+import Step from '../components/Step';
+import BranchAnalysisStepContent from './BranchAnalysisStepContent';
+import ExtensionInstallationStepContent from './ExtensionInstallationStepContent';
+import SaveAndRunStepContent from './SaveAndRunStepContent';
+import ServiceEndpointStepContent from './ServiceEndpointStepContent';
+
+export interface AzurePipelinesTutorialProps {
+ component: T.Component;
+ currentUser: T.LoggedInUser;
+ projectBinding: ProjectAlmBindingResponse;
+}
+
+export enum Steps {
+ ExtensionInstallation,
+ ServiceEndpoint,
+ BranchAnalysis,
+ SaveAndRun
+}
+
+interface Step {
+ step: Steps;
+ content: JSX.Element;
+ checkValidity?: boolean;
+}
+
+export default function AzurePipelinesTutorial(props: AzurePipelinesTutorialProps) {
+ const { component, currentUser, projectBinding } = props;
+
+ const [currentStep, setCurrentStep] = React.useState(Steps.ExtensionInstallation);
+ const [isCurrentStepValid, setIsCurrentStepValid] = React.useState(false);
+
+ // Failsafe; should never happen.
+ if (!isProjectAzureBindingResponse(projectBinding)) {
+ return (
+ <Alert variant="error">
+ {translate('onboarding.tutorial.with.azure_pipelines.unsupported')}
+ </Alert>
+ );
+ }
+
+ const steps: Array<Step> = [
+ { step: Steps.ExtensionInstallation, content: <ExtensionInstallationStepContent /> },
+ {
+ step: Steps.ServiceEndpoint,
+ content: <ServiceEndpointStepContent component={component} currentUser={currentUser} />
+ },
+ {
+ step: Steps.BranchAnalysis,
+ content: (
+ <BranchAnalysisStepContent
+ component={component}
+ onStepValidationChange={isValid => setIsCurrentStepValid(isValid)}
+ />
+ ),
+ checkValidity: true
+ },
+ { step: Steps.SaveAndRun, content: <SaveAndRunStepContent /> }
+ ];
+
+ const switchCurrentStep = (step: Steps) => {
+ setCurrentStep(step);
+ setIsCurrentStepValid(false);
+ };
+
+ const canContinue = (step: Step, i: number) =>
+ i < steps.length - 1 && (!step.checkValidity || isCurrentStepValid);
+
+ return (
+ <>
+ <div className="page-header big-spacer-bottom">
+ <h1 className="page-title">
+ {translate('onboarding.tutorial.with.azure_pipelines.title')}
+ </h1>
+ </div>
+
+ {steps.map((step, i) => (
+ <Step
+ key={step.step}
+ stepNumber={i + 1}
+ stepTitle={translate(
+ `onboarding.tutorial.with.azure_pipelines.${Steps[step.step]}.title`
+ )}
+ open={step.step === currentStep}
+ finished={step.step < currentStep}
+ onOpen={() => switchCurrentStep(step.step)}
+ renderForm={() => (
+ <div className="boxed-group-inner">
+ <div>{step.content}</div>
+ {canContinue(step, i) && (
+ <Button
+ className="big-spacer-top spacer-bottom"
+ onClick={() => switchCurrentStep(step.step + 1)}>
+ {translate('continue')}
+ </Button>
+ )}
+ </div>
+ )}
+ />
+ ))}
+ </>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/BranchAnalysisStepContent.tsx b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/BranchAnalysisStepContent.tsx
new file mode 100644
index 00000000000..f091e226786
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/BranchAnalysisStepContent.tsx
@@ -0,0 +1,208 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { Link } from 'react-router';
+import { ClipboardIconButton } from 'sonar-ui-common/components/controls/clipboard';
+import { Alert } from 'sonar-ui-common/components/ui/Alert';
+import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
+import CodeSnippet from '../../common/CodeSnippet';
+import RenderOptions from '../components/RenderOptions';
+import SentenceWithHighlights from '../components/SentenceWithHighlights';
+
+export interface BranchesAnalysisStepProps {
+ component: T.Component;
+ onStepValidationChange: (isValid: boolean) => void;
+}
+
+export enum BuildTechnology {
+ DotNet = 'dotnet',
+ Maven = 'maven',
+ Gradle = 'gradle',
+ Other = 'other'
+}
+
+export default function BranchAnalysisStepContent(props: BranchesAnalysisStepProps) {
+ const { component, onStepValidationChange } = props;
+
+ const [buildTechnology, setBuildTechnology] = React.useState<BuildTechnology | undefined>();
+
+ React.useEffect(() => {
+ if (buildTechnology) {
+ onStepValidationChange(true);
+ }
+ }, [buildTechnology, onStepValidationChange]);
+
+ const MAVEN_GRADLE_PROPS_SNIPPET = `# Additional properties that will be passed to the scanner,
+# Put one key=value per line, example:
+# sonar.exclusions=**/*.bin
+sonar.projectKey=${component.key}`;
+
+ return (
+ <>
+ <span>{translate('onboarding.build')}</span>
+ <RenderOptions
+ checked={buildTechnology}
+ name="buildTechnology"
+ onCheck={value => setBuildTechnology(value as BuildTechnology)}
+ optionLabelKey="onboarding.build"
+ options={Object.values(BuildTechnology)}
+ />
+ <ol className="list-styled big-spacer-top">
+ {buildTechnology && (
+ <>
+ <li>
+ <SentenceWithHighlights
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare"
+ highlightKeys={['pipeline', 'task', 'before']}
+ />
+ </li>
+ <ul className="list-styled">
+ <li>
+ <SentenceWithHighlights
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.endpoint"
+ highlightKeys={['endpoint']}
+ />
+ </li>
+ <li>
+ <FormattedMessage
+ id="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.run_analysis"
+ defaultMessage={translate(
+ 'onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.run_analysis'
+ )}
+ values={{
+ section: (
+ <strong>
+ {translate(
+ 'onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.run_analysis.section'
+ )}
+ </strong>
+ ),
+ run_analysis_value: (
+ <strong>
+ {translate(
+ 'onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.run_analysis.values',
+ buildTechnology
+ )}
+ </strong>
+ )
+ }}
+ />
+ </li>
+ {buildTechnology === BuildTechnology.Other && (
+ <li>
+ <SentenceWithHighlights
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.manual"
+ highlightKeys={['mode']}
+ />
+ </li>
+ )}
+ {[BuildTechnology.DotNet, BuildTechnology.Other].includes(buildTechnology) && (
+ <li>
+ <FormattedMessage
+ id="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.run.key.sentence"
+ defaultMessage={translate(
+ 'onboarding.tutorial.with.azure_pipelines.BranchAnalysis.run.key.sentence'
+ )}
+ values={{
+ key: <code className="rule">{component.key}</code>,
+ button: <ClipboardIconButton copyValue={component.key} />
+ }}
+ />
+ </li>
+ )}
+ {[BuildTechnology.Maven, BuildTechnology.Gradle].includes(buildTechnology) && (
+ <li>
+ <SentenceWithHighlights
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.advanced_properties"
+ highlightKeys={['section', 'properties']}
+ />
+ :
+ <CodeSnippet snippet={MAVEN_GRADLE_PROPS_SNIPPET} />
+ </li>
+ )}
+ </ul>
+ {[BuildTechnology.DotNet, BuildTechnology.Other].includes(buildTechnology) && (
+ <li>
+ <SentenceWithHighlights
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.run"
+ highlightKeys={['task', 'after']}
+ />
+ </li>
+ )}
+ {[BuildTechnology.Maven, BuildTechnology.Gradle].includes(buildTechnology) && (
+ <>
+ <li>
+ {translateWithParameters(
+ 'onboarding.tutorial.with.azure_pipelines.BranchAnalysis.java',
+ translate('onboarding.build', buildTechnology)
+ )}
+ </li>
+ <ul className="list-styled big-spacer-bottom">
+ <li>
+ <SentenceWithHighlights
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.java.settings"
+ highlightKeys={['section', 'option']}
+ />
+ </li>
+ </ul>
+ </>
+ )}
+
+ <li>
+ <SentenceWithHighlights
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.publish_qg"
+ highlightKeys={['task']}
+ />
+ <Alert variant="info" className="spacer-top">
+ {translate(
+ 'onboarding.tutorial.with.azure_pipelines.BranchAnalysis.publish_qg.info.sentence1'
+ )}
+ </Alert>
+ </li>
+ <li>
+ <SentenceWithHighlights
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.continous_integration"
+ highlightKeys={['tab', 'continuous_integration']}
+ />
+ </li>
+ <hr />
+ <FormattedMessage
+ id="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.branch_protection"
+ defaultMessage={translate(
+ 'onboarding.tutorial.with.azure_pipelines.BranchAnalysis.branch_protection'
+ )}
+ values={{
+ link: (
+ <Link to="/documentation/analysis/azuredevops-integration/" target="_blank">
+ {translate(
+ 'onboarding.tutorial.with.azure_pipelines.BranchAnalysis.branch_protection.link'
+ )}
+ </Link>
+ )
+ }}
+ />
+ </>
+ )}
+ </ol>
+ </>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/ExtensionInstallationStepContent.tsx b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/ExtensionInstallationStepContent.tsx
new file mode 100644
index 00000000000..4e00f962e9c
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/ExtensionInstallationStepContent.tsx
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+
+export default function ExtensionInstallationStepContent() {
+ return (
+ <span>
+ <FormattedMessage
+ defaultMessage={translate(
+ 'onboarding.tutorial.with.azure_pipelines.ExtensionInstallation.sentence'
+ )}
+ id="onboarding.tutorial.with.azure_pipelines.ExtensionInstallation.sentence"
+ values={{
+ link: (
+ <a
+ href="https://marketplace.visualstudio.com/items?itemName=SonarSource.sonarqube"
+ rel="noopener noreferrer"
+ target="_blank">
+ {translate(
+ 'onboarding.tutorial.with.azure_pipelines.ExtensionInstallation.sentence.link'
+ )}
+ </a>
+ ),
+ button: (
+ <strong>
+ {translate(
+ 'onboarding.tutorial.with.azure_pipelines.ExtensionInstallation.sentence.button'
+ )}
+ </strong>
+ )
+ }}
+ />
+ </span>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/SaveAndRunStepContent.tsx b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/SaveAndRunStepContent.tsx
new file mode 100644
index 00000000000..ed70ae4ccc8
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/SaveAndRunStepContent.tsx
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import * as React from 'react';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { getBaseUrl } from '../../../helpers/system';
+
+export default function SaveAndRunStepContent() {
+ return (
+ <>
+ <div className="display-flex-row big-spacer-bottom">
+ <div>
+ <img
+ alt="" // Should be ignored by screen readers
+ className="big-spacer-right"
+ width={30}
+ src={`${getBaseUrl()}/images/tutorials/commit.svg`}
+ />
+ </div>
+ <div>
+ <p className="little-spacer-bottom">
+ <strong>
+ {translate('onboarding.tutorial.with.azure_pipelines.SaveAndRun.commit')}
+ </strong>
+ </p>
+ <p>{translate('onboarding.tutorial.with.azure_pipelines.SaveAndRun.commit.why')}</p>
+ </div>
+ </div>
+ <div className="display-flex-row">
+ <div>
+ <img
+ alt="" // Should be ignored by screen readers
+ className="big-spacer-right"
+ width={30}
+ src={`${getBaseUrl()}/images/tutorials/refresh.svg`}
+ />
+ </div>
+ <div>
+ <p className="little-spacer-bottom">
+ <strong>
+ {translate('onboarding.tutorial.with.azure_pipelines.SaveAndRun.refresh')}
+ </strong>
+ </p>
+ <p>{translate('onboarding.tutorial.with.azure_pipelines.SaveAndRun.refresh.why')}</p>
+ </div>
+ </div>
+ </>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/ServiceEndpointStepContent.tsx b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/ServiceEndpointStepContent.tsx
new file mode 100644
index 00000000000..69a907af0c4
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/ServiceEndpointStepContent.tsx
@@ -0,0 +1,92 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { Button } from 'sonar-ui-common/components/controls/buttons';
+import { ClipboardIconButton } from 'sonar-ui-common/components/controls/clipboard';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { getHostUrl } from 'sonar-ui-common/helpers/urls';
+import EditTokenModal from '../components/EditTokenModal';
+import SentenceWithHighlights from '../components/SentenceWithHighlights';
+
+export interface ServiceEndpointStepProps {
+ component: T.Component;
+ currentUser: T.LoggedInUser;
+}
+
+export default function ServiceEndpointStepContent(props: ServiceEndpointStepProps) {
+ const { component, currentUser } = props;
+
+ const [isModalVisible, toggleModal] = React.useState(false);
+
+ return (
+ <>
+ <ol className="list-styled">
+ <li>
+ <SentenceWithHighlights
+ translationKey="onboarding.tutorial.with.azure_pipelines.ServiceEndpoint.step1"
+ highlightKeys={['menu']}
+ />
+ </li>
+ <li>
+ <SentenceWithHighlights
+ translationKey="onboarding.tutorial.with.azure_pipelines.ServiceEndpoint.step2"
+ highlightKeys={['type']}
+ />
+ </li>
+ <li>
+ {translate('onboarding.tutorial.with.azure_pipelines.ServiceEndpoint.step3.sentence')}
+ </li>
+ <li>
+ <FormattedMessage
+ defaultMessage={translate(
+ 'onboarding.tutorial.with.azure_pipelines.ServiceEndpoint.step4.sentence'
+ )}
+ id="onboarding.tutorial.with.azure_pipelines.ServiceEndpoint.step4.sentence"
+ values={{
+ url: <code className="rule">{getHostUrl()}</code>,
+ button: <ClipboardIconButton copyValue={getHostUrl()} />
+ }}
+ />
+ </li>
+ <li>
+ <span>
+ {translate('onboarding.tutorial.with.azure_pipelines.ServiceEndpoint.step5.sentence')}:
+ </span>
+ <Button className="spacer-left" onClick={() => toggleModal(true)}>
+ {translate('onboarding.token.generate_token')}
+ </Button>
+ </li>
+ <li>
+ {translate('onboarding.tutorial.with.azure_pipelines.ServiceEndpoint.step6.sentence')}
+ </li>
+ </ol>
+
+ {isModalVisible && (
+ <EditTokenModal
+ component={component}
+ currentUser={currentUser}
+ onClose={() => toggleModal(false)}
+ />
+ )}
+ </>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/AzurePipelinesTutorial-test.tsx b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/AzurePipelinesTutorial-test.tsx
new file mode 100644
index 00000000000..d7ad84a21c2
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/AzurePipelinesTutorial-test.tsx
@@ -0,0 +1,111 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { Button } from 'sonar-ui-common/components/controls/buttons';
+import { click } from 'sonar-ui-common/helpers/testUtils';
+import {
+ mockProjectAzureBindingResponse,
+ mockProjectGithubBindingResponse
+} from '../../../../helpers/mocks/alm-settings';
+import { mockComponent, mockLoggedInUser } from '../../../../helpers/testMocks';
+import Step from '../../components/Step';
+import AzurePipelinesTutorial, { AzurePipelinesTutorialProps } from '../AzurePipelinesTutorial';
+
+it('should render correctly', () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
+ expect(
+ wrapper
+ .find(Step)
+ .first()
+ .dive()
+ ).toMatchSnapshot('first-step-wrapper');
+ expect(
+ wrapper
+ .find(Step)
+ .last()
+ .dive()
+ ).toMatchSnapshot('last-step-wrapper');
+ expect(shallowRender({ projectBinding: mockProjectGithubBindingResponse() })).toMatchSnapshot(
+ 'wrong alm'
+ );
+});
+
+it('should display the next step when one is finished', () => {
+ const wrapper = shallowRender();
+ expect(
+ wrapper
+ .find(Step)
+ .filterWhere(elt => elt.props().open === true)
+ .props().stepNumber
+ ).toBe(1);
+
+ click(
+ wrapper
+ .find(Step)
+ .first()
+ .dive()
+ .find(Button)
+ );
+
+ expect(
+ wrapper
+ .find(Step)
+ .filterWhere(elt => elt.props().open === true)
+ .props().stepNumber
+ ).toBe(2);
+});
+
+it('should open a step when user click on it', () => {
+ const wrapper = shallowRender();
+ expect(
+ wrapper
+ .find(Step)
+ .filterWhere(elt => elt.props().open === true)
+ .props().stepNumber
+ ).toBe(1);
+
+ (
+ wrapper
+ .find(Step)
+ .filterWhere(elt => elt.props().stepNumber === 3)
+ .props().onOpen ?? (() => {})
+ )();
+
+ expect(
+ wrapper
+ .find(Step)
+ .filterWhere(elt => elt.props().open === true)
+ .props().stepNumber
+ ).toBe(3);
+});
+
+function shallowRender(props: Partial<AzurePipelinesTutorialProps> = {}) {
+ return shallow<AzurePipelinesTutorialProps>(
+ <AzurePipelinesTutorial
+ component={mockComponent()}
+ currentUser={mockLoggedInUser()}
+ projectBinding={mockProjectAzureBindingResponse()}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/BranchAnalysisStepContent-test.tsx b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/BranchAnalysisStepContent-test.tsx
new file mode 100644
index 00000000000..f6f45aed90b
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/BranchAnalysisStepContent-test.tsx
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { mockComponent } from '../../../../helpers/testMocks';
+import RenderOptions from '../../components/RenderOptions';
+import BranchAnalysisStepContent, {
+ BranchesAnalysisStepProps,
+ BuildTechnology
+} from '../BranchAnalysisStepContent';
+
+it('should render correctly', () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
+});
+
+it.each([
+ BuildTechnology.DotNet,
+ BuildTechnology.Gradle,
+ BuildTechnology.Maven,
+ BuildTechnology.Other
+])('should render correctly', (buildTech: BuildTechnology) => {
+ const wrapper = shallowRender();
+ wrapper
+ .find(RenderOptions)
+ .props()
+ .onCheck(buildTech);
+
+ expect(wrapper).toMatchSnapshot(buildTech);
+});
+
+function shallowRender(props: Partial<BranchesAnalysisStepProps> = {}) {
+ return shallow(
+ <BranchAnalysisStepContent
+ component={mockComponent()}
+ onStepValidationChange={jest.fn()}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/ExtensionInstallationStepContent-test.tsx b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/ExtensionInstallationStepContent-test.tsx
new file mode 100644
index 00000000000..da361004576
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/ExtensionInstallationStepContent-test.tsx
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import ExtensionInstallationStepContent from '../ExtensionInstallationStepContent';
+
+it('should render correctly', () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
+});
+
+function shallowRender() {
+ return shallow(<ExtensionInstallationStepContent />);
+}
diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/SaveAndRunStepContent-test.tsx b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/SaveAndRunStepContent-test.tsx
new file mode 100644
index 00000000000..0e0af022fe5
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/SaveAndRunStepContent-test.tsx
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import SaveAndRunStepContent from '../SaveAndRunStepContent';
+
+it('should render correctly', () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
+});
+
+function shallowRender() {
+ return shallow(<SaveAndRunStepContent />);
+}
diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/ServiceEndpointStepContent-test.tsx b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/ServiceEndpointStepContent-test.tsx
new file mode 100644
index 00000000000..683e9321561
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/ServiceEndpointStepContent-test.tsx
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { Button } from 'sonar-ui-common/components/controls/buttons';
+import { click } from 'sonar-ui-common/helpers/testUtils';
+import { mockComponent, mockLoggedInUser } from '../../../../helpers/testMocks';
+import EditTokenModal from '../../components/EditTokenModal';
+import ServiceEndpointStepContent from '../ServiceEndpointStepContent';
+
+it('should render correctly', () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should render open/close the token modal when asked to', () => {
+ const wrapper = shallowRender();
+ expect(wrapper.exists(EditTokenModal)).toBe(false);
+
+ click(wrapper.find(Button));
+ expect(wrapper.exists(EditTokenModal)).toBe(true);
+
+ wrapper
+ .find(EditTokenModal)
+ .props()
+ .onClose();
+ expect(wrapper.exists(EditTokenModal)).toBe(false);
+});
+
+function shallowRender() {
+ return shallow(
+ <ServiceEndpointStepContent component={mockComponent()} currentUser={mockLoggedInUser()} />
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/__snapshots__/AzurePipelinesTutorial-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/__snapshots__/AzurePipelinesTutorial-test.tsx.snap
new file mode 100644
index 00000000000..84e32877db7
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/__snapshots__/AzurePipelinesTutorial-test.tsx.snap
@@ -0,0 +1,128 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Fragment>
+ <div
+ className="page-header big-spacer-bottom"
+ >
+ <h1
+ className="page-title"
+ >
+ onboarding.tutorial.with.azure_pipelines.title
+ </h1>
+ </div>
+ <Step
+ finished={false}
+ key="0"
+ onOpen={[Function]}
+ open={true}
+ renderForm={[Function]}
+ stepNumber={1}
+ stepTitle="onboarding.tutorial.with.azure_pipelines.ExtensionInstallation.title"
+ />
+ <Step
+ finished={false}
+ key="1"
+ onOpen={[Function]}
+ open={false}
+ renderForm={[Function]}
+ stepNumber={2}
+ stepTitle="onboarding.tutorial.with.azure_pipelines.ServiceEndpoint.title"
+ />
+ <Step
+ finished={false}
+ key="2"
+ onOpen={[Function]}
+ open={false}
+ renderForm={[Function]}
+ stepNumber={3}
+ stepTitle="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.title"
+ />
+ <Step
+ finished={false}
+ key="3"
+ onOpen={[Function]}
+ open={false}
+ renderForm={[Function]}
+ stepNumber={4}
+ stepTitle="onboarding.tutorial.with.azure_pipelines.SaveAndRun.title"
+ />
+</Fragment>
+`;
+
+exports[`should render correctly: first-step-wrapper 1`] = `
+<div
+ className="boxed-group onboarding-step is-open"
+>
+ <div
+ className="onboarding-step-number"
+ >
+ 1
+ </div>
+ <div
+ className="boxed-group-header"
+ >
+ <h2>
+ onboarding.tutorial.with.azure_pipelines.ExtensionInstallation.title
+ </h2>
+ </div>
+ <div
+ className=""
+ >
+ <div
+ className="boxed-group-inner"
+ >
+ <div>
+ <ExtensionInstallationStepContent />
+ </div>
+ <Button
+ className="big-spacer-top spacer-bottom"
+ onClick={[Function]}
+ >
+ continue
+ </Button>
+ </div>
+ </div>
+</div>
+`;
+
+exports[`should render correctly: last-step-wrapper 1`] = `
+<div
+ className="boxed-group onboarding-step"
+>
+ <div
+ className="onboarding-step-number"
+ >
+ 4
+ </div>
+ <div
+ className="boxed-group-header"
+ >
+ <h2>
+ onboarding.tutorial.with.azure_pipelines.SaveAndRun.title
+ </h2>
+ </div>
+ <div
+ className="boxed-group-inner"
+ />
+ <div
+ className="hidden"
+ >
+ <div
+ className="boxed-group-inner"
+ >
+ <div>
+ <SaveAndRunStepContent />
+ </div>
+ </div>
+ </div>
+</div>
+`;
+
+exports[`should render correctly: wrong alm 1`] = `
+<Alert
+ variant="error"
+>
+ onboarding.tutorial.with.azure_pipelines.unsupported
+</Alert>
+`;
diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/__snapshots__/BranchAnalysisStepContent-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/__snapshots__/BranchAnalysisStepContent-test.tsx.snap
new file mode 100644
index 00000000000..07602310f0f
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/__snapshots__/BranchAnalysisStepContent-test.tsx.snap
@@ -0,0 +1,613 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Fragment>
+ <span>
+ onboarding.build
+ </span>
+ <RenderOptions
+ name="buildTechnology"
+ onCheck={[Function]}
+ optionLabelKey="onboarding.build"
+ options={
+ Array [
+ "dotnet",
+ "maven",
+ "gradle",
+ "other",
+ ]
+ }
+ />
+ <ol
+ className="list-styled big-spacer-top"
+ />
+</Fragment>
+`;
+
+exports[`should render correctly: dotnet 1`] = `
+<Fragment>
+ <span>
+ onboarding.build
+ </span>
+ <RenderOptions
+ checked="dotnet"
+ name="buildTechnology"
+ onCheck={[Function]}
+ optionLabelKey="onboarding.build"
+ options={
+ Array [
+ "dotnet",
+ "maven",
+ "gradle",
+ "other",
+ ]
+ }
+ />
+ <ol
+ className="list-styled big-spacer-top"
+ >
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "pipeline",
+ "task",
+ "before",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare"
+ />
+ </li>
+ <ul
+ className="list-styled"
+ >
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "endpoint",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.endpoint"
+ />
+ </li>
+ <li>
+ <FormattedMessage
+ defaultMessage="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.run_analysis"
+ id="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.run_analysis"
+ values={
+ Object {
+ "run_analysis_value": <strong>
+ onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.run_analysis.values.dotnet
+ </strong>,
+ "section": <strong>
+ onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.run_analysis.section
+ </strong>,
+ }
+ }
+ />
+ </li>
+ <li>
+ <FormattedMessage
+ defaultMessage="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.run.key.sentence"
+ id="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.run.key.sentence"
+ values={
+ Object {
+ "button": <ClipboardIconButton
+ copyValue="my-project"
+ />,
+ "key": <code
+ className="rule"
+ >
+ my-project
+ </code>,
+ }
+ }
+ />
+ </li>
+ </ul>
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "task",
+ "after",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.run"
+ />
+ </li>
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "task",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.publish_qg"
+ />
+ <Alert
+ className="spacer-top"
+ variant="info"
+ >
+ onboarding.tutorial.with.azure_pipelines.BranchAnalysis.publish_qg.info.sentence1
+ </Alert>
+ </li>
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "tab",
+ "continuous_integration",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.continous_integration"
+ />
+ </li>
+ <hr />
+ <FormattedMessage
+ defaultMessage="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.branch_protection"
+ id="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.branch_protection"
+ values={
+ Object {
+ "link": <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to="/documentation/analysis/azuredevops-integration/"
+ >
+ onboarding.tutorial.with.azure_pipelines.BranchAnalysis.branch_protection.link
+ </Link>,
+ }
+ }
+ />
+ </ol>
+</Fragment>
+`;
+
+exports[`should render correctly: gradle 1`] = `
+<Fragment>
+ <span>
+ onboarding.build
+ </span>
+ <RenderOptions
+ checked="gradle"
+ name="buildTechnology"
+ onCheck={[Function]}
+ optionLabelKey="onboarding.build"
+ options={
+ Array [
+ "dotnet",
+ "maven",
+ "gradle",
+ "other",
+ ]
+ }
+ />
+ <ol
+ className="list-styled big-spacer-top"
+ >
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "pipeline",
+ "task",
+ "before",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare"
+ />
+ </li>
+ <ul
+ className="list-styled"
+ >
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "endpoint",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.endpoint"
+ />
+ </li>
+ <li>
+ <FormattedMessage
+ defaultMessage="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.run_analysis"
+ id="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.run_analysis"
+ values={
+ Object {
+ "run_analysis_value": <strong>
+ onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.run_analysis.values.gradle
+ </strong>,
+ "section": <strong>
+ onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.run_analysis.section
+ </strong>,
+ }
+ }
+ />
+ </li>
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "section",
+ "properties",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.advanced_properties"
+ />
+ :
+ <CodeSnippet
+ snippet="# Additional properties that will be passed to the scanner,
+# Put one key=value per line, example:
+# sonar.exclusions=**/*.bin
+sonar.projectKey=my-project"
+ />
+ </li>
+ </ul>
+ <li>
+ onboarding.tutorial.with.azure_pipelines.BranchAnalysis.java.onboarding.build.gradle
+ </li>
+ <ul
+ className="list-styled big-spacer-bottom"
+ >
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "section",
+ "option",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.java.settings"
+ />
+ </li>
+ </ul>
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "task",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.publish_qg"
+ />
+ <Alert
+ className="spacer-top"
+ variant="info"
+ >
+ onboarding.tutorial.with.azure_pipelines.BranchAnalysis.publish_qg.info.sentence1
+ </Alert>
+ </li>
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "tab",
+ "continuous_integration",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.continous_integration"
+ />
+ </li>
+ <hr />
+ <FormattedMessage
+ defaultMessage="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.branch_protection"
+ id="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.branch_protection"
+ values={
+ Object {
+ "link": <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to="/documentation/analysis/azuredevops-integration/"
+ >
+ onboarding.tutorial.with.azure_pipelines.BranchAnalysis.branch_protection.link
+ </Link>,
+ }
+ }
+ />
+ </ol>
+</Fragment>
+`;
+
+exports[`should render correctly: maven 1`] = `
+<Fragment>
+ <span>
+ onboarding.build
+ </span>
+ <RenderOptions
+ checked="maven"
+ name="buildTechnology"
+ onCheck={[Function]}
+ optionLabelKey="onboarding.build"
+ options={
+ Array [
+ "dotnet",
+ "maven",
+ "gradle",
+ "other",
+ ]
+ }
+ />
+ <ol
+ className="list-styled big-spacer-top"
+ >
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "pipeline",
+ "task",
+ "before",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare"
+ />
+ </li>
+ <ul
+ className="list-styled"
+ >
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "endpoint",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.endpoint"
+ />
+ </li>
+ <li>
+ <FormattedMessage
+ defaultMessage="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.run_analysis"
+ id="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.run_analysis"
+ values={
+ Object {
+ "run_analysis_value": <strong>
+ onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.run_analysis.values.maven
+ </strong>,
+ "section": <strong>
+ onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.run_analysis.section
+ </strong>,
+ }
+ }
+ />
+ </li>
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "section",
+ "properties",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.advanced_properties"
+ />
+ :
+ <CodeSnippet
+ snippet="# Additional properties that will be passed to the scanner,
+# Put one key=value per line, example:
+# sonar.exclusions=**/*.bin
+sonar.projectKey=my-project"
+ />
+ </li>
+ </ul>
+ <li>
+ onboarding.tutorial.with.azure_pipelines.BranchAnalysis.java.onboarding.build.maven
+ </li>
+ <ul
+ className="list-styled big-spacer-bottom"
+ >
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "section",
+ "option",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.java.settings"
+ />
+ </li>
+ </ul>
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "task",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.publish_qg"
+ />
+ <Alert
+ className="spacer-top"
+ variant="info"
+ >
+ onboarding.tutorial.with.azure_pipelines.BranchAnalysis.publish_qg.info.sentence1
+ </Alert>
+ </li>
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "tab",
+ "continuous_integration",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.continous_integration"
+ />
+ </li>
+ <hr />
+ <FormattedMessage
+ defaultMessage="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.branch_protection"
+ id="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.branch_protection"
+ values={
+ Object {
+ "link": <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to="/documentation/analysis/azuredevops-integration/"
+ >
+ onboarding.tutorial.with.azure_pipelines.BranchAnalysis.branch_protection.link
+ </Link>,
+ }
+ }
+ />
+ </ol>
+</Fragment>
+`;
+
+exports[`should render correctly: other 1`] = `
+<Fragment>
+ <span>
+ onboarding.build
+ </span>
+ <RenderOptions
+ checked="other"
+ name="buildTechnology"
+ onCheck={[Function]}
+ optionLabelKey="onboarding.build"
+ options={
+ Array [
+ "dotnet",
+ "maven",
+ "gradle",
+ "other",
+ ]
+ }
+ />
+ <ol
+ className="list-styled big-spacer-top"
+ >
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "pipeline",
+ "task",
+ "before",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare"
+ />
+ </li>
+ <ul
+ className="list-styled"
+ >
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "endpoint",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.endpoint"
+ />
+ </li>
+ <li>
+ <FormattedMessage
+ defaultMessage="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.run_analysis"
+ id="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.run_analysis"
+ values={
+ Object {
+ "run_analysis_value": <strong>
+ onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.run_analysis.values.other
+ </strong>,
+ "section": <strong>
+ onboarding.tutorial.with.azure_pipelines.BranchAnalysis.prepare.run_analysis.section
+ </strong>,
+ }
+ }
+ />
+ </li>
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "mode",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.manual"
+ />
+ </li>
+ <li>
+ <FormattedMessage
+ defaultMessage="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.run.key.sentence"
+ id="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.run.key.sentence"
+ values={
+ Object {
+ "button": <ClipboardIconButton
+ copyValue="my-project"
+ />,
+ "key": <code
+ className="rule"
+ >
+ my-project
+ </code>,
+ }
+ }
+ />
+ </li>
+ </ul>
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "task",
+ "after",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.run"
+ />
+ </li>
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "task",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.publish_qg"
+ />
+ <Alert
+ className="spacer-top"
+ variant="info"
+ >
+ onboarding.tutorial.with.azure_pipelines.BranchAnalysis.publish_qg.info.sentence1
+ </Alert>
+ </li>
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "tab",
+ "continuous_integration",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.continous_integration"
+ />
+ </li>
+ <hr />
+ <FormattedMessage
+ defaultMessage="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.branch_protection"
+ id="onboarding.tutorial.with.azure_pipelines.BranchAnalysis.branch_protection"
+ values={
+ Object {
+ "link": <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to="/documentation/analysis/azuredevops-integration/"
+ >
+ onboarding.tutorial.with.azure_pipelines.BranchAnalysis.branch_protection.link
+ </Link>,
+ }
+ }
+ />
+ </ol>
+</Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/__snapshots__/ExtensionInstallationStepContent-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/__snapshots__/ExtensionInstallationStepContent-test.tsx.snap
new file mode 100644
index 00000000000..9a8c93a3e5f
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/__snapshots__/ExtensionInstallationStepContent-test.tsx.snap
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<span>
+ <FormattedMessage
+ defaultMessage="onboarding.tutorial.with.azure_pipelines.ExtensionInstallation.sentence"
+ id="onboarding.tutorial.with.azure_pipelines.ExtensionInstallation.sentence"
+ values={
+ Object {
+ "button": <strong>
+ onboarding.tutorial.with.azure_pipelines.ExtensionInstallation.sentence.button
+ </strong>,
+ "link": <a
+ href="https://marketplace.visualstudio.com/items?itemName=SonarSource.sonarqube"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ onboarding.tutorial.with.azure_pipelines.ExtensionInstallation.sentence.link
+ </a>,
+ }
+ }
+ />
+</span>
+`;
diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/__snapshots__/SaveAndRunStepContent-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/__snapshots__/SaveAndRunStepContent-test.tsx.snap
new file mode 100644
index 00000000000..9f4dd3417bd
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/__snapshots__/SaveAndRunStepContent-test.tsx.snap
@@ -0,0 +1,54 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Fragment>
+ <div
+ className="display-flex-row big-spacer-bottom"
+ >
+ <div>
+ <img
+ alt=""
+ className="big-spacer-right"
+ src="/images/tutorials/commit.svg"
+ width={30}
+ />
+ </div>
+ <div>
+ <p
+ className="little-spacer-bottom"
+ >
+ <strong>
+ onboarding.tutorial.with.azure_pipelines.SaveAndRun.commit
+ </strong>
+ </p>
+ <p>
+ onboarding.tutorial.with.azure_pipelines.SaveAndRun.commit.why
+ </p>
+ </div>
+ </div>
+ <div
+ className="display-flex-row"
+ >
+ <div>
+ <img
+ alt=""
+ className="big-spacer-right"
+ src="/images/tutorials/refresh.svg"
+ width={30}
+ />
+ </div>
+ <div>
+ <p
+ className="little-spacer-bottom"
+ >
+ <strong>
+ onboarding.tutorial.with.azure_pipelines.SaveAndRun.refresh
+ </strong>
+ </p>
+ <p>
+ onboarding.tutorial.with.azure_pipelines.SaveAndRun.refresh.why
+ </p>
+ </div>
+ </div>
+</Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/__snapshots__/ServiceEndpointStepContent-test.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/__snapshots__/ServiceEndpointStepContent-test.tsx.snap
new file mode 100644
index 00000000000..1596e44fa1c
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/__snapshots__/ServiceEndpointStepContent-test.tsx.snap
@@ -0,0 +1,66 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Fragment>
+ <ol
+ className="list-styled"
+ >
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "menu",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.ServiceEndpoint.step1"
+ />
+ </li>
+ <li>
+ <SentenceWithHighlights
+ highlightKeys={
+ Array [
+ "type",
+ ]
+ }
+ translationKey="onboarding.tutorial.with.azure_pipelines.ServiceEndpoint.step2"
+ />
+ </li>
+ <li>
+ onboarding.tutorial.with.azure_pipelines.ServiceEndpoint.step3.sentence
+ </li>
+ <li>
+ <FormattedMessage
+ defaultMessage="onboarding.tutorial.with.azure_pipelines.ServiceEndpoint.step4.sentence"
+ id="onboarding.tutorial.with.azure_pipelines.ServiceEndpoint.step4.sentence"
+ values={
+ Object {
+ "button": <ClipboardIconButton
+ copyValue="http://localhost"
+ />,
+ "url": <code
+ className="rule"
+ >
+ http://localhost
+ </code>,
+ }
+ }
+ />
+ </li>
+ <li>
+ <span>
+ onboarding.tutorial.with.azure_pipelines.ServiceEndpoint.step5.sentence
+ :
+ </span>
+ <Button
+ className="spacer-left"
+ onClick={[Function]}
+ >
+ onboarding.token.generate_token
+ </Button>
+ </li>
+ <li>
+ onboarding.tutorial.with.azure_pipelines.ServiceEndpoint.step6.sentence
+ </li>
+ </ol>
+</Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/components/tutorials/gitlabci/GitLabCITutorial.tsx b/server/sonar-web/src/main/js/components/tutorials/gitlabci/GitLabCITutorial.tsx
index 8d4dee3d6bc..d66ac7b3771 100644
--- a/server/sonar-web/src/main/js/components/tutorials/gitlabci/GitLabCITutorial.tsx
+++ b/server/sonar-web/src/main/js/components/tutorials/gitlabci/GitLabCITutorial.tsx
@@ -20,8 +20,10 @@
import * as React from 'react';
import { Alert } from 'sonar-ui-common/components/ui/Alert';
import { translate } from 'sonar-ui-common/helpers/l10n';
-import { isProjectGitLabBindingResponse } from '../../../helpers/alm-settings';
-import { ProjectAlmBindingResponse } from '../../../types/alm-settings';
+import {
+ isProjectGitLabBindingResponse,
+ ProjectAlmBindingResponse
+} from '../../../types/alm-settings';
import EnvironmentVariablesStep from './EnvironmentVariablesStep';
import ProjectKeyStep from './ProjectKeyStep';
import { BuildTools } from './types';
diff --git a/server/sonar-web/src/main/js/components/tutorials/jenkins/JenkinsTutorial.tsx b/server/sonar-web/src/main/js/components/tutorials/jenkins/JenkinsTutorial.tsx
index a516260891f..cdc00fbf235 100644
--- a/server/sonar-web/src/main/js/components/tutorials/jenkins/JenkinsTutorial.tsx
+++ b/server/sonar-web/src/main/js/components/tutorials/jenkins/JenkinsTutorial.tsx
@@ -21,13 +21,14 @@ import * as React from 'react';
import { connect } from 'react-redux';
import { Alert } from 'sonar-ui-common/components/ui/Alert';
import { translate } from 'sonar-ui-common/helpers/l10n';
-import {
- isProjectBitbucketBindingResponse,
- isProjectGitHubBindingResponse
-} from '../../../helpers/alm-settings';
import { getCurrentUserSetting, Store } from '../../../store/rootReducer';
import { setCurrentUserSetting } from '../../../store/users';
-import { AlmBindingDefinition, ProjectAlmBindingResponse } from '../../../types/alm-settings';
+import {
+ AlmBindingDefinition,
+ isProjectBitbucketBindingResponse,
+ isProjectGitHubBindingResponse,
+ ProjectAlmBindingResponse
+} from '../../../types/alm-settings';
import JenkinsfileStep from './JenkinsfileStep';
import MultiBranchPipelineStep from './MultiBranchPipelineStep';
import PreRequisitesStep from './PreRequisitesStep';
diff --git a/server/sonar-web/src/main/js/components/tutorials/jenkins/MultiBranchPipelineStep.tsx b/server/sonar-web/src/main/js/components/tutorials/jenkins/MultiBranchPipelineStep.tsx
index d0feb3133bc..26279e2c0c7 100644
--- a/server/sonar-web/src/main/js/components/tutorials/jenkins/MultiBranchPipelineStep.tsx
+++ b/server/sonar-web/src/main/js/components/tutorials/jenkins/MultiBranchPipelineStep.tsx
@@ -21,12 +21,10 @@ import * as React from 'react';
import { Button } from 'sonar-ui-common/components/controls/buttons';
import { translate } from 'sonar-ui-common/helpers/l10n';
import {
+ AlmBindingDefinition,
isGithubBindingDefinition,
isProjectBitbucketBindingResponse,
- isProjectGitHubBindingResponse
-} from '../../../helpers/alm-settings';
-import {
- AlmBindingDefinition,
+ isProjectGitHubBindingResponse,
ProjectBitbucketBindingResponse,
ProjectGitHubBindingResponse
} from '../../../types/alm-settings';
diff --git a/server/sonar-web/src/main/js/components/tutorials/jenkins/WebhookStep.tsx b/server/sonar-web/src/main/js/components/tutorials/jenkins/WebhookStep.tsx
index 60dfc618314..4243c27cc2e 100644
--- a/server/sonar-web/src/main/js/components/tutorials/jenkins/WebhookStep.tsx
+++ b/server/sonar-web/src/main/js/components/tutorials/jenkins/WebhookStep.tsx
@@ -22,12 +22,10 @@ import { FormattedMessage } from 'react-intl';
import { Button, ButtonLink } from 'sonar-ui-common/components/controls/buttons';
import { translate } from 'sonar-ui-common/helpers/l10n';
import {
- isBitbucketBindingDefinition,
- isGithubBindingDefinition
-} from '../../../helpers/alm-settings';
-import {
AlmBindingDefinition,
AlmKeys,
+ isBitbucketBindingDefinition,
+ isGithubBindingDefinition,
ProjectAlmBindingResponse
} from '../../../types/alm-settings';
import Step from '../components/Step';
diff --git a/server/sonar-web/src/main/js/components/tutorials/types.ts b/server/sonar-web/src/main/js/components/tutorials/types.ts
index 80e22751eda..46edfc4f552 100644
--- a/server/sonar-web/src/main/js/components/tutorials/types.ts
+++ b/server/sonar-web/src/main/js/components/tutorials/types.ts
@@ -20,7 +20,8 @@
export enum TutorialModes {
Manual = 'manual',
Jenkins = 'jenkins',
- GitLabCI = 'gitlab-ci'
+ GitLabCI = 'gitlab-ci',
+ AzurePipelines = 'azure-pipelines'
}
export enum BuildTools {
diff --git a/server/sonar-web/src/main/js/helpers/__tests__/alm-settings-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/alm-settings-test.ts
deleted file mode 100644
index 2f8c1c4a189..00000000000
--- a/server/sonar-web/src/main/js/helpers/__tests__/alm-settings-test.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2020 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { AlmKeys } from '../../types/alm-settings';
-import {
- isBitbucketBindingDefinition,
- isGithubBindingDefinition,
- isProjectBitbucketBindingResponse,
- isProjectGitHubBindingResponse
-} from '../alm-settings';
-import {
- mockBitbucketBindingDefinition,
- mockGithubBindingDefinition,
- mockProjectAlmBindingResponse,
- mockProjectBitbucketBindingResponse,
- mockProjectGithubBindingResponse
-} from '../mocks/alm-settings';
-
-/* eslint-disable sonarjs/no-duplicate-string */
-
-describe('isProjectBitbucketBindingResponse', () => {
- it('works as expected', () => {
- expect(isProjectBitbucketBindingResponse(mockProjectAlmBindingResponse())).toBe(false);
- expect(isProjectBitbucketBindingResponse(mockProjectBitbucketBindingResponse())).toBe(true);
- });
-});
-
-describe('isBitbucketBindingDefinition', () => {
- it('works as expected', () => {
- expect(isBitbucketBindingDefinition(undefined)).toBe(false);
- expect(isBitbucketBindingDefinition(mockGithubBindingDefinition())).toBe(false);
- expect(isBitbucketBindingDefinition(mockBitbucketBindingDefinition())).toBe(true);
- });
-});
-
-describe('isProjectGithubBindingResponse', () => {
- it('works as expected', () => {
- expect(
- isProjectGitHubBindingResponse(mockProjectAlmBindingResponse({ alm: AlmKeys.Azure }))
- ).toBe(false);
- expect(isProjectGitHubBindingResponse(mockProjectGithubBindingResponse())).toBe(true);
- });
-});
-
-describe('isGithubBindingDefinition', () => {
- it('works as expected', () => {
- expect(isGithubBindingDefinition(undefined)).toBe(false);
- expect(isGithubBindingDefinition(mockBitbucketBindingDefinition())).toBe(false);
- expect(isGithubBindingDefinition(mockGithubBindingDefinition())).toBe(true);
- });
-});
diff --git a/server/sonar-web/src/main/js/helpers/alm-settings.ts b/server/sonar-web/src/main/js/helpers/alm-settings.ts
deleted file mode 100644
index bd0013775fc..00000000000
--- a/server/sonar-web/src/main/js/helpers/alm-settings.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2020 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import {
- AlmBindingDefinition,
- AlmKeys,
- BitbucketBindingDefinition,
- GithubBindingDefinition,
- ProjectAlmBindingResponse,
- ProjectBitbucketBindingResponse,
- ProjectGitHubBindingResponse,
- ProjectGitLabBindingResponse
-} from '../types/alm-settings';
-
-export function isProjectBitbucketBindingResponse(
- binding: ProjectAlmBindingResponse
-): binding is ProjectBitbucketBindingResponse {
- return binding.alm === AlmKeys.Bitbucket;
-}
-
-export function isProjectGitHubBindingResponse(
- binding: ProjectAlmBindingResponse
-): binding is ProjectGitHubBindingResponse {
- return binding.alm === AlmKeys.GitHub;
-}
-
-export function isProjectGitLabBindingResponse(
- binding: ProjectAlmBindingResponse
-): binding is ProjectGitLabBindingResponse {
- return binding.alm === AlmKeys.GitLab;
-}
-
-export function isBitbucketBindingDefinition(
- binding?: AlmBindingDefinition & { url?: string; personalAccessToken?: string }
-): binding is BitbucketBindingDefinition {
- return (
- binding !== undefined && binding.url !== undefined && binding.personalAccessToken !== undefined
- );
-}
-
-export function isGithubBindingDefinition(
- binding?: AlmBindingDefinition & { appId?: string; privateKey?: string; url?: string }
-): binding is GithubBindingDefinition {
- return (
- binding !== undefined &&
- binding.appId !== undefined &&
- binding.privateKey !== undefined &&
- binding.url !== undefined
- );
-}
diff --git a/server/sonar-web/src/main/js/helpers/mocks/alm-settings.ts b/server/sonar-web/src/main/js/helpers/mocks/alm-settings.ts
index b4a67f72610..b8de0c0aa21 100644
--- a/server/sonar-web/src/main/js/helpers/mocks/alm-settings.ts
+++ b/server/sonar-web/src/main/js/helpers/mocks/alm-settings.ts
@@ -27,6 +27,7 @@ import {
GithubBindingDefinition,
GitlabBindingDefinition,
ProjectAlmBindingResponse,
+ ProjectAzureBindingResponse,
ProjectBitbucketBindingResponse,
ProjectGitHubBindingResponse,
ProjectGitLabBindingResponse
@@ -132,6 +133,19 @@ export function mockProjectGitLabBindingResponse(
};
}
+export function mockProjectAzureBindingResponse(
+ overrides: Partial<ProjectAzureBindingResponse> = {}
+): ProjectAzureBindingResponse {
+ return {
+ alm: AlmKeys.Azure,
+ key: 'foo',
+ slug: 'PROJECT_NAME',
+ repository: 'REPOSITORY_NAME',
+ url: 'https://ado.my_company.com/mycollection',
+ ...overrides
+ };
+}
+
export function mockAlmSettingsBindingStatus(
overrides: Partial<AlmSettingsBindingStatus>
): AlmSettingsBindingStatus {
diff --git a/server/sonar-web/src/main/js/types/alm-settings.ts b/server/sonar-web/src/main/js/types/alm-settings.ts
index c27018afb25..c70a371b878 100644
--- a/server/sonar-web/src/main/js/types/alm-settings.ts
+++ b/server/sonar-web/src/main/js/types/alm-settings.ts
@@ -60,6 +60,13 @@ export interface ProjectAlmBindingResponse {
summaryCommentEnabled?: boolean;
}
+export interface ProjectAzureBindingResponse extends ProjectAlmBindingResponse {
+ alm: AlmKeys.Azure;
+ repository: string;
+ slug: string;
+ url: string;
+}
+
export interface ProjectBitbucketBindingResponse extends ProjectAlmBindingResponse {
alm: AlmKeys.Bitbucket;
repository: string;
@@ -126,3 +133,46 @@ export enum AlmSettingsBindingStatusType {
Failure,
Warning
}
+
+export function isProjectBitbucketBindingResponse(
+ binding: ProjectAlmBindingResponse
+): binding is ProjectBitbucketBindingResponse {
+ return binding.alm === AlmKeys.Bitbucket;
+}
+
+export function isProjectGitHubBindingResponse(
+ binding: ProjectAlmBindingResponse
+): binding is ProjectGitHubBindingResponse {
+ return binding.alm === AlmKeys.GitHub;
+}
+
+export function isProjectGitLabBindingResponse(
+ binding: ProjectAlmBindingResponse
+): binding is ProjectGitLabBindingResponse {
+ return binding.alm === AlmKeys.GitLab;
+}
+
+export function isProjectAzureBindingResponse(
+ binding: ProjectAlmBindingResponse
+): binding is ProjectAzureBindingResponse {
+ return binding.alm === AlmKeys.Azure;
+}
+
+export function isBitbucketBindingDefinition(
+ binding?: AlmBindingDefinition & { url?: string; personalAccessToken?: string }
+): binding is BitbucketBindingDefinition {
+ return (
+ binding !== undefined && binding.url !== undefined && binding.personalAccessToken !== undefined
+ );
+}
+
+export function isGithubBindingDefinition(
+ binding?: AlmBindingDefinition & { appId?: string; privateKey?: string; url?: string }
+): binding is GithubBindingDefinition {
+ return (
+ binding !== undefined &&
+ binding.appId !== undefined &&
+ binding.privateKey !== undefined &&
+ binding.url !== undefined
+ );
+}