diff options
author | Revanshu Paliwal <revanshu.paliwal@sonarsource.com> | 2023-07-14 16:50:17 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-07-21 20:03:16 +0000 |
commit | a7cd1d6bf47e1e2966e3029bd28badf74c2af46b (patch) | |
tree | 5fbb424fbced3458ef540b1545a0b5cd91929dc0 /server | |
parent | 106e5141332a4d11dd055941394d4a8b4bc83f10 (diff) | |
download | sonarqube-a7cd1d6bf47e1e2966e3029bd28badf74c2af46b.tar.gz sonarqube-a7cd1d6bf47e1e2966e3029bd28badf74c2af46b.zip |
SONAR-19906 Migrating github action tutorial page to MIUI
Diffstat (limited to 'server')
27 files changed, 410 insertions, 297 deletions
diff --git a/server/sonar-web/config/jest/SetupTestEnvironment.ts b/server/sonar-web/config/jest/SetupTestEnvironment.ts index 54350855106..a10eb5dd7e3 100644 --- a/server/sonar-web/config/jest/SetupTestEnvironment.ts +++ b/server/sonar-web/config/jest/SetupTestEnvironment.ts @@ -21,6 +21,12 @@ import React from 'react'; (window as any).React = React; +const MockObserver = { + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +}; + const content = document.createElement('div'); content.id = 'content'; document.documentElement.appendChild(content); @@ -37,3 +43,10 @@ jest.mock('../../src/main/js/helpers/l10n', () => ({ translateWithParameters: (messageKey: string, ...parameters: Array<string | number>) => [messageKey, ...parameters].join('.'), })); + +const MockIntersectionObserverEntries = [{ isIntersecting: true }]; + +(window as any).IntersectionObserver = jest.fn().mockImplementation((callback) => { + callback(MockIntersectionObserverEntries, MockObserver); + return MockObserver; +}); diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index 6d42f3de7bb..dfdd3cf7092 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -11,6 +11,8 @@ "@emotion/react": "11.11.1", "@emotion/styled": "11.11.0", "@primer/octicons-react": "19.3.0", + "@react-spring/rafz": "9.7.3", + "@react-spring/web": "9.7.3", "@tanstack/react-query": "4.29.14", "classnames": "2.3.2", "clipboard": "2.0.11", diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/AzurePipelinesTutorial-it.tsx b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/AzurePipelinesTutorial-it.tsx index 6080a73c7be..541a20afae0 100644 --- a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/AzurePipelinesTutorial-it.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/AzurePipelinesTutorial-it.tsx @@ -216,7 +216,7 @@ function assertOtherStepIsCorrectlyRendered() { function assertFinishStepIsCorrectlyRendered() { expect( screen.getByRole('heading', { - name: 'onboarding.tutorial.ci_outro.all_set.title', + name: 'onboarding.tutorial.ci_outro.done', }) ).toBeInTheDocument(); } diff --git a/server/sonar-web/src/main/js/components/tutorials/bitbucket-pipelines/__tests__/BitbucketPipelinesTutorial-it.tsx b/server/sonar-web/src/main/js/components/tutorials/bitbucket-pipelines/__tests__/BitbucketPipelinesTutorial-it.tsx index 0a1285123a2..07691d586b8 100644 --- a/server/sonar-web/src/main/js/components/tutorials/bitbucket-pipelines/__tests__/BitbucketPipelinesTutorial-it.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/bitbucket-pipelines/__tests__/BitbucketPipelinesTutorial-it.tsx @@ -28,7 +28,7 @@ import { } from '../../../../helpers/mocks/alm-settings'; import { mockComponent } from '../../../../helpers/mocks/component'; import { mockLanguage, mockLoggedInUser } from '../../../../helpers/testMocks'; -import { renderApp, RenderContext } from '../../../../helpers/testReactTestingUtils'; +import { RenderContext, renderApp } from '../../../../helpers/testReactTestingUtils'; import { AlmKeys } from '../../../../types/alm-settings'; import { Feature } from '../../../../types/features'; import { @@ -75,28 +75,28 @@ it('should follow and complete all steps', async () => { // Create/update configuration file step // Maven await user.click(ui.mavenBuildButton.get()); - expect(getCopyToClipboardValue(1)).toMatchSnapshot('Maven: bitbucket-pipelines.yml'); + expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('Maven: bitbucket-pipelines.yml'); // Gradle await user.click(ui.gradleBuildButton.get()); - expect(getCopyToClipboardValue(2)).toMatchSnapshot('Groovy: build.gradle'); + expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('Groovy: build.gradle'); await user.click(ui.gradleDSLButton(GradleBuildDSL.Kotlin).get()); - expect(getCopyToClipboardValue(2)).toMatchSnapshot('Kotlin: build.gradle.kts'); - expect(getCopyToClipboardValue(4)).toMatchSnapshot('Gradle: bitbucket-pipelines.yml'); + expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('Kotlin: build.gradle.kts'); + expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot('Gradle: bitbucket-pipelines.yml'); // .NET await user.click(ui.dotnetBuildButton.get()); - expect(getCopyToClipboardValue(1)).toMatchSnapshot('.NET: bitbucket-pipelines.yml'); + expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('.NET: bitbucket-pipelines.yml'); // CFamily await user.click(ui.cFamilyBuildButton.get()); - expect(getCopyToClipboardValue()).toMatchSnapshot('CFamily: sonar-project.properties'); - expect(getCopyToClipboardValue(2)).toMatchSnapshot('CFamily: bitbucket-pipelines.yml'); + expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('CFamily: sonar-project.properties'); + expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot('CFamily: bitbucket-pipelines.yml'); // Other await user.click(ui.otherBuildButton.get()); - expect(getCopyToClipboardValue()).toMatchSnapshot('Other: sonar-project.properties'); - expect(getCopyToClipboardValue(2)).toMatchSnapshot('Other: .github/workflows/build.yml'); + expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('Other: sonar-project.properties'); + expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot('Other: .github/workflows/build.yml'); await user.click(ui.finishTutorialButton.get()); expect(ui.allSetSentence.get()).toBeInTheDocument(); diff --git a/server/sonar-web/src/main/js/components/tutorials/components/AllSet.tsx b/server/sonar-web/src/main/js/components/tutorials/components/AllSet.tsx index afe2f20d884..b3a6b0e2ece 100644 --- a/server/sonar-web/src/main/js/components/tutorials/components/AllSet.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/components/AllSet.tsx @@ -17,15 +17,17 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import styled from '@emotion/styled'; +import { animated, config, useSpring } from '@react-spring/web'; +import { CheckIcon, FlagVisual, SubTitle } from 'design-system'; import * as React from 'react'; import withAvailableFeatures, { WithAvailableFeaturesProps, } from '../../../app/components/available-features/withAvailableFeatures'; import { translate } from '../../../helpers/l10n'; -import { getBaseUrl } from '../../../helpers/system'; +import useIntersectionObserver from '../../../hooks/useIntersectionObserver'; import { AlmKeys } from '../../../types/alm-settings'; import { Feature } from '../../../types/features'; -import SentenceWithHighlights from './SentenceWithHighlights'; export interface AllSetProps extends WithAvailableFeaturesProps { alm: AlmKeys; @@ -33,65 +35,53 @@ export interface AllSetProps extends WithAvailableFeaturesProps { } export function AllSet(props: AllSetProps) { + const outroRef = React.useRef<HTMLDivElement>(null); const { alm, willRefreshAutomatically } = props; const branchSupportEnabled = props.hasFeature(Feature.BranchSupport); + const intersectionEntry = useIntersectionObserver(outroRef, { freezeOnceVisible: true }); + + const outroAnimation = useSpring({ + from: { top: '200px' }, + to: intersectionEntry?.isIntersecting ? { top: '0px' } : { top: '200px' }, + config: config.wobbly, + }); + return ( - <> - <div className="abs-width-600"> - <p className="big-spacer-bottom"> - <SentenceWithHighlights - highlightKeys={['all_set']} - translationKey="onboarding.tutorial.ci_outro.all_set" - /> + <animated.div + className="sw-flex sw-flex-col sw-items-center sw-relative" + ref={outroRef} + style={outroAnimation} + > + <FlagVisual /> + <SubTitle className="sw-mt-3 sw-mb-12"> + {translate('onboarding.tutorial.ci_outro.done')} + </SubTitle> + <MessageContainer> + <p className="sw-body-sm sw-mb-4"> + {translate('onboarding.tutorial.ci_outro.refresh_text')} </p> - <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.ci_outro.commit')}</strong> - </p> - <p> - {branchSupportEnabled - ? translate('onboarding.tutorial.ci_outro.commit.why', alm) - : translate('onboarding.tutorial.ci_outro.commit.why.no_branches')} - </p> - </div> - </div> - {willRefreshAutomatically && ( - <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.ci_outro.refresh')}</strong> - </p> - <p>{translate('onboarding.tutorial.ci_outro.refresh.why')}</p> - </div> - </div> - )} - </div> - {willRefreshAutomatically && ( - <div className="huge-spacer-bottom huge-spacer-top big-padded-top text-muted display-flex-center display-flex-justify-center"> - <i className="spinner spacer-right" /> - {translate('onboarding.tutorial.ci_outro.waiting_for_fist_analysis')} - </div> - )} - </> + <ul className="sw-mb-6"> + <li className="sw-mb-4 sw-flex sw-items-center"> + <CheckIcon className="sw-mr-2" /> + {branchSupportEnabled + ? translate('onboarding.tutorial.ci_outro.commit.why', alm) + : translate('onboarding.tutorial.ci_outro.commit.why.no_branches')} + </li> + {willRefreshAutomatically && ( + <li className="sw-mb-4 sw-flex sw-items-center"> + <CheckIcon className="sw-mr-2" /> + {translate('onboarding.tutorial.ci_outro.refresh.why')} + </li> + )} + </ul> + </MessageContainer> + </animated.div> ); } +const MessageContainer = styled.div` + width: 840px; +`; + export default withAvailableFeatures(AllSet); diff --git a/server/sonar-web/src/main/js/components/tutorials/components/CreateYmlFile.tsx b/server/sonar-web/src/main/js/components/tutorials/components/CreateYmlFile.tsx index 26a1368bac7..cad8d25b570 100644 --- a/server/sonar-web/src/main/js/components/tutorials/components/CreateYmlFile.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/components/CreateYmlFile.tsx @@ -17,11 +17,11 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { ClipboardIconButton, CodeSnippet, NumberedListItem } from 'design-system'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { ClipboardIconButton } from '../../../components/controls/clipboard'; import { translate } from '../../../helpers/l10n'; -import CodeSnippet from '../../common/CodeSnippet'; +import { InlineSnippet } from './InlineSnippet'; export interface CreateYmlFileProps { yamlFileName: string; @@ -31,20 +31,20 @@ export interface CreateYmlFileProps { export default function CreateYmlFile(props: CreateYmlFileProps) { const { yamlTemplate, yamlFileName } = props; return ( - <li className="abs-width-800"> + <NumberedListItem> <FormattedMessage defaultMessage={translate('onboarding.tutorial.with.github_action.yaml.create_yml')} id="onboarding.tutorial.with.github_action.yaml.create_yml" values={{ file: ( <> - <code className="rule">{yamlFileName}</code> + <InlineSnippet snippet={yamlFileName} /> <ClipboardIconButton copyValue={yamlFileName} /> </> ), }} /> - <CodeSnippet snippet={yamlTemplate} /> - </li> + <CodeSnippet className="sw-p-6 sw-overflow-auto" snippet={yamlTemplate} language="yml" /> + </NumberedListItem> ); } diff --git a/server/sonar-web/src/main/js/components/tutorials/components/DefaultProjectKey.tsx b/server/sonar-web/src/main/js/components/tutorials/components/DefaultProjectKey.tsx index 5112acc8ced..5833fdfd374 100644 --- a/server/sonar-web/src/main/js/components/tutorials/components/DefaultProjectKey.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/components/DefaultProjectKey.tsx @@ -17,9 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { CodeSnippet, NumberedListItem } from 'design-system'; import * as React from 'react'; import { Component } from '../../../types/types'; -import CodeSnippet from '../../common/CodeSnippet'; import SentenceWithFilename from './SentenceWithFilename'; export interface DefaultProjectKeyProps { @@ -31,12 +31,15 @@ const sonarProjectSnippet = (key: string) => `sonar.projectKey=${key}`; export default function DefaultProjectKey(props: DefaultProjectKeyProps) { const { component } = props; return ( - <li className="abs-width-600"> + <NumberedListItem> <SentenceWithFilename filename="sonar-project.properties" translationKey="onboarding.tutorial.other.project_key" /> - <CodeSnippet snippet={sonarProjectSnippet(component.key)} /> - </li> + <CodeSnippet + snippet={sonarProjectSnippet(component.key)} + className="sw-p-8 sw-overflow-auto" + /> + </NumberedListItem> ); } diff --git a/server/sonar-web/src/main/js/components/tutorials/components/GithubCFamilyExampleRepositories.tsx b/server/sonar-web/src/main/js/components/tutorials/components/GithubCFamilyExampleRepositories.tsx index 99d924eb5a3..332ba0ff603 100644 --- a/server/sonar-web/src/main/js/components/tutorials/components/GithubCFamilyExampleRepositories.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/components/GithubCFamilyExampleRepositories.tsx @@ -18,10 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import classNames from 'classnames'; +import { Card, LightLabel, StandoutLink } from 'design-system'; import React from 'react'; import { translate } from '../../../helpers/l10n'; import { getBaseUrl } from '../../../helpers/system'; -import Link from '../../common/Link'; import { OSs, TutorialModes } from '../types'; import './GithubCFamilyExampleRepositories.css'; @@ -57,26 +57,21 @@ export default function GithubCFamilyExampleRepositories( const link = `https://github.com/orgs/sonarsource-cfamily-examples/repositories?q=${queryParams}`; return ( - <div - className={classNames( - 'github-cfamily-example-repositories-box big-padded boxed-group', - className - )} - > - <div className="display-flex-center"> + <Card className={classNames('sw-p-4', className)}> + <div> <img alt="" // Should be ignored by screen readers className="spacer-right" height={20} src={`${getBaseUrl()}/images/alm/github.svg`} /> - <Link className="spacer-left big" target="_blank" to={link}> + <StandoutLink target="_blank" to={link}> sonarsource-cfamily-examples - </Link> + </StandoutLink> </div> - <p className="spacer-top"> + <LightLabel as="p" className="sw-mt-4"> {translate('onboarding.tutorial.cfamily.examples_repositories_description')} - </p> - </div> + </LightLabel> + </Card> ); } diff --git a/server/sonar-web/src/main/js/components/tutorials/components/GradleBuild.tsx b/server/sonar-web/src/main/js/components/tutorials/components/GradleBuild.tsx index 895aadb09b1..8087bdb5b04 100644 --- a/server/sonar-web/src/main/js/components/tutorials/components/GradleBuild.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/components/GradleBuild.tsx @@ -17,15 +17,15 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { ClipboardIconButton, CodeSnippet, NumberedListItem } from 'design-system/lib'; import React from 'react'; import { FormattedMessage } from 'react-intl'; import { translate } from '../../../helpers/l10n'; import { Component } from '../../../types/types'; -import CodeSnippet from '../../common/CodeSnippet'; -import { ClipboardIconButton } from '../../controls/clipboard'; import { GradleBuildDSL } from '../types'; import { buildGradleSnippet } from '../utils'; import GradleBuildSelection from './GradleBuildSelection'; +import { InlineSnippet } from './InlineSnippet'; interface Props { component: Component; @@ -33,31 +33,35 @@ interface Props { export default function GradleBuild({ component }: Props) { return ( - <li className="abs-width-600"> + <NumberedListItem> <FormattedMessage defaultMessage={translate('onboarding.tutorial.with.yaml.gradle')} id="onboarding.tutorial.with.yaml.gradle" values={{ groovy: ( <> - <code className="rule">{GradleBuildDSL.Groovy}</code> + <InlineSnippet snippet={GradleBuildDSL.Groovy} /> <ClipboardIconButton copyValue={GradleBuildDSL.Groovy} /> </> ), kotlin: ( <> - <code className="rule">{GradleBuildDSL.Kotlin}</code> + <InlineSnippet snippet={GradleBuildDSL.Kotlin} /> <ClipboardIconButton copyValue={GradleBuildDSL.Kotlin} /> </> ), - sq: <code className="rule">org.sonarqube</code>, + sq: <InlineSnippet snippet="org.sonarqube" />, }} /> - <GradleBuildSelection className="big-spacer-top big-spacer-bottom"> + <GradleBuildSelection className="sw-my-4"> {(build) => ( - <CodeSnippet snippet={buildGradleSnippet(component.key, component.name, build)} /> + <CodeSnippet + language="gradle" + className="sw-p-6" + snippet={buildGradleSnippet(component.key, component.name, build)} + /> )} </GradleBuildSelection> - </li> + </NumberedListItem> ); } diff --git a/server/sonar-web/src/main/js/components/tutorials/components/InlineSnippet.tsx b/server/sonar-web/src/main/js/components/tutorials/components/InlineSnippet.tsx index d84392f84cc..18f56f690d8 100644 --- a/server/sonar-web/src/main/js/components/tutorials/components/InlineSnippet.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/components/InlineSnippet.tsx @@ -17,12 +17,21 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import classNames from 'classnames'; import { CodeSnippet } from 'design-system'; import * as React from 'react'; import { FCProps } from '../../../types/misc'; -export function InlineSnippet({ snippet }: Pick<FCProps<typeof CodeSnippet>, 'snippet'>) { +export function InlineSnippet({ + snippet, + className, +}: Pick<FCProps<typeof CodeSnippet>, 'snippet'> & { className?: string }) { return ( - <CodeSnippet className="sw-code sw-inline-block sw-px-1" noCopy isOneLine snippet={snippet} /> + <CodeSnippet + className={classNames('sw-code sw-inline-block sw-px-1', className)} + noCopy + isOneLine + snippet={snippet} + /> ); } diff --git a/server/sonar-web/src/main/js/components/tutorials/components/RenderOptions.tsx b/server/sonar-web/src/main/js/components/tutorials/components/RenderOptions.tsx index 301008e4cbb..4a138015375 100644 --- a/server/sonar-web/src/main/js/components/tutorials/components/RenderOptions.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/components/RenderOptions.tsx @@ -28,23 +28,32 @@ export interface RenderOptionsProps { optionLabelKey: string; options: string[]; titleLabelKey?: string; + setDone?: (doneStatus: boolean) => void; } export default function RenderOptions({ checked, label, onCheck, + setDone, optionLabelKey, options, titleLabelKey, }: RenderOptionsProps) { + const onChange = (checked: string) => { + if (setDone) { + setDone(true); + } + onCheck(checked); + }; + return ( <div className="sw-mt-4"> {titleLabelKey && <label className="sw-block sw-mb-1">{translate(titleLabelKey)}</label>} <ToggleButton label={label} - onChange={onCheck} + onChange={onChange} options={options.map((build) => ({ label: translate(optionLabelKey, build), value: build, diff --git a/server/sonar-web/src/main/js/components/tutorials/components/SentenceWithFilename.tsx b/server/sonar-web/src/main/js/components/tutorials/components/SentenceWithFilename.tsx index eacfc511ef2..94aa6f25a07 100644 --- a/server/sonar-web/src/main/js/components/tutorials/components/SentenceWithFilename.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/components/SentenceWithFilename.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { translate } from '../../../helpers/l10n'; +import { InlineSnippet } from './InlineSnippet'; export interface SentenceWithFilenameProps { filename: string; @@ -36,7 +37,7 @@ export default function SentenceWithFilename({ defaultMessage={translate(translationKey, 'sentence')} id={`${translationKey}.sentence`} values={{ - file: <code>{filename}</code>, + file: <InlineSnippet snippet={filename} />, }} /> </span> diff --git a/server/sonar-web/src/main/js/components/tutorials/components/TokenStepGenerator.tsx b/server/sonar-web/src/main/js/components/tutorials/components/TokenStepGenerator.tsx index d44606562cd..e43cb39b1c9 100644 --- a/server/sonar-web/src/main/js/components/tutorials/components/TokenStepGenerator.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/components/TokenStepGenerator.tsx @@ -17,9 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { ButtonSecondary, NumberedListItem } from 'design-system'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { Button } from '../../../components/controls/buttons'; import { translate } from '../../../helpers/l10n'; import { Component } from '../../../types/types'; import { LoggedInUser } from '../../../types/users'; @@ -39,21 +39,25 @@ export default function TokenStepGenerator(props: TokenStepGeneratorProps) { return ( <> - <li className="big-spacer-bottom"> + <NumberedListItem> <FormattedMessage defaultMessage={translate('onboarding.tutorial.env_variables')} id="onboarding.tutorial.env_variables" values={{ extra: ( - <Button className="spacer-left" onClick={toggleTokenModal}> + <ButtonSecondary className="sw-ml-2" onClick={toggleTokenModal}> {translate('onboarding.token.generate.long')} - </Button> + </ButtonSecondary> + ), + field: ( + <span className="sw-body-sm-highlight"> + {translate('onboarding.tutorial.env_variables.field')} + </span> ), - field: <strong>{translate('onboarding.tutorial.env_variables.field')}</strong>, value: translate('onboarding.tutorial.env_variables.token_generator.value'), }} /> - </li> + </NumberedListItem> {isModalVisible && ( <EditTokenModal component={component} currentUser={currentUser} onClose={closeTokenModal} /> )} diff --git a/server/sonar-web/src/main/js/components/tutorials/components/YamlFileStep.tsx b/server/sonar-web/src/main/js/components/tutorials/components/YamlFileStep.tsx index e451af41491..b968dd2d3f6 100644 --- a/server/sonar-web/src/main/js/components/tutorials/components/YamlFileStep.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/components/YamlFileStep.tsx @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { NumberedList, NumberedListItem } from 'design-system'; import * as React from 'react'; import { translate } from '../../../helpers/l10n'; import { withCLanguageFeature } from '../../hoc/withCLanguageFeature'; @@ -26,6 +27,7 @@ import { BuildTools } from '../types'; export interface YamlFileStepProps { children?: (buildTool: BuildTools) => React.ReactElement<{}>; hasCLanguageFeature: boolean; + setDone?: (doneStatus: boolean) => void; } export function YamlFileStep(props: YamlFileStepProps) { @@ -40,8 +42,8 @@ export function YamlFileStep(props: YamlFileStepProps) { const [buildToolSelected, setBuildToolSelected] = React.useState<BuildTools>(); return ( - <ol className="list-styled big-spacer-top big-spacer-bottom"> - <li className="abs-width-600"> + <NumberedList> + <NumberedListItem> {translate('onboarding.build')} <RenderOptions label={translate('onboarding.build')} @@ -49,10 +51,11 @@ export function YamlFileStep(props: YamlFileStepProps) { onCheck={(value) => setBuildToolSelected(value as BuildTools)} options={buildTools} optionLabelKey="onboarding.build" + setDone={props.setDone} /> - </li> + </NumberedListItem> {children && buildToolSelected && children(buildToolSelected)} - </ol> + </NumberedList> ); } diff --git a/server/sonar-web/src/main/js/components/tutorials/github-action/AnalysisCommand.tsx b/server/sonar-web/src/main/js/components/tutorials/github-action/AnalysisCommand.tsx index 6204a994d68..89ac0a07d6c 100644 --- a/server/sonar-web/src/main/js/components/tutorials/github-action/AnalysisCommand.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/github-action/AnalysisCommand.tsx @@ -34,7 +34,6 @@ export interface AnalysisCommandProps extends WithAvailableFeaturesProps { buildTool: BuildTools; mainBranchName: string; component: Component; - onDone: () => void; } export function AnalysisCommand(props: AnalysisCommandProps) { @@ -48,7 +47,6 @@ export function AnalysisCommand(props: AnalysisCommandProps) { branchesEnabled={branchSupportEnabled} mainBranchName={mainBranchName} component={component} - onDone={props.onDone} /> ); case BuildTools.Gradle: @@ -57,7 +55,6 @@ export function AnalysisCommand(props: AnalysisCommandProps) { branchesEnabled={branchSupportEnabled} mainBranchName={mainBranchName} component={component} - onDone={props.onDone} /> ); case BuildTools.DotNet: @@ -66,7 +63,6 @@ export function AnalysisCommand(props: AnalysisCommandProps) { branchesEnabled={branchSupportEnabled} mainBranchName={mainBranchName} component={component} - onDone={props.onDone} /> ); case BuildTools.CFamily: @@ -75,7 +71,6 @@ export function AnalysisCommand(props: AnalysisCommandProps) { branchesEnabled={branchSupportEnabled} mainBranchName={mainBranchName} component={component} - onDone={props.onDone} /> ); case BuildTools.Other: @@ -84,7 +79,6 @@ export function AnalysisCommand(props: AnalysisCommandProps) { branchesEnabled={branchSupportEnabled} mainBranchName={mainBranchName} component={component} - onDone={props.onDone} /> ); } diff --git a/server/sonar-web/src/main/js/components/tutorials/github-action/GitHubActionTutorial.tsx b/server/sonar-web/src/main/js/components/tutorials/github-action/GitHubActionTutorial.tsx index 1315f062729..0f0b134674e 100644 --- a/server/sonar-web/src/main/js/components/tutorials/github-action/GitHubActionTutorial.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/github-action/GitHubActionTutorial.tsx @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { BasicSeparator, TutorialStep, TutorialStepList } from 'design-system'; import * as React from 'react'; import { translate } from '../../../helpers/l10n'; import { @@ -26,18 +27,11 @@ import { } from '../../../types/alm-settings'; import { Component } from '../../../types/types'; import { LoggedInUser } from '../../../types/users'; -import AllSetStep from '../components/AllSetStep'; -import Step from '../components/Step'; +import AllSet from '../components/AllSet'; import YamlFileStep from '../components/YamlFileStep'; import AnalysisCommand from './AnalysisCommand'; import SecretStep from './SecretStep'; -export enum Steps { - CREATE_SECRET = 1, - YAML = 2, - ALL_SET = 3, -} - export interface GitHubActionTutorialProps { almBinding?: AlmSettingsInstance; baseUrl: string; @@ -49,6 +43,7 @@ export interface GitHubActionTutorialProps { } export default function GitHubActionTutorial(props: GitHubActionTutorialProps) { + const [done, setDone] = React.useState<boolean>(false); const { almBinding, baseUrl, @@ -58,52 +53,37 @@ export default function GitHubActionTutorial(props: GitHubActionTutorialProps) { mainBranchName, willRefreshAutomatically, } = props; - - const [step, setStep] = React.useState<Steps>(Steps.CREATE_SECRET); return ( - <> - <Step - finished={step > Steps.CREATE_SECRET} - onOpen={() => setStep(Steps.CREATE_SECRET)} - open={step === Steps.CREATE_SECRET} - renderForm={() => ( - <SecretStep - almBinding={almBinding} - baseUrl={baseUrl} - component={component} - currentUser={currentUser} - projectBinding={projectBinding} - onDone={() => setStep(Steps.YAML)} + <TutorialStepList className="sw-mb-8"> + <TutorialStep title={translate('onboarding.tutorial.with.github_action.create_secret.title')}> + <SecretStep + almBinding={almBinding} + baseUrl={baseUrl} + component={component} + currentUser={currentUser} + projectBinding={projectBinding} + /> + </TutorialStep> + <TutorialStep title={translate('onboarding.tutorial.with.github_action.yaml.title')}> + <YamlFileStep setDone={setDone}> + {(buildTool) => ( + <AnalysisCommand + buildTool={buildTool} + mainBranchName={mainBranchName} + component={component} + /> + )} + </YamlFileStep> + </TutorialStep> + {done && ( + <> + <BasicSeparator className="sw-my-10" /> + <AllSet + alm={almBinding?.alm || AlmKeys.GitHub} + willRefreshAutomatically={willRefreshAutomatically} /> - )} - stepNumber={Steps.CREATE_SECRET} - stepTitle={translate('onboarding.tutorial.with.github_action.create_secret.title')} - /> - <Step - finished={step > Steps.YAML} - onOpen={() => setStep(Steps.YAML)} - open={step === Steps.YAML} - renderForm={() => ( - <YamlFileStep> - {(buildTool) => ( - <AnalysisCommand - buildTool={buildTool} - mainBranchName={mainBranchName} - component={component} - onDone={() => setStep(Steps.ALL_SET)} - /> - )} - </YamlFileStep> - )} - stepNumber={Steps.YAML} - stepTitle={translate('onboarding.tutorial.with.github_action.yaml.title')} - /> - <AllSetStep - alm={almBinding?.alm || AlmKeys.GitHub} - open={step === Steps.ALL_SET} - stepNumber={Steps.ALL_SET} - willRefreshAutomatically={willRefreshAutomatically} - /> - </> + </> + )} + </TutorialStepList> ); } diff --git a/server/sonar-web/src/main/js/components/tutorials/github-action/SecretStep.tsx b/server/sonar-web/src/main/js/components/tutorials/github-action/SecretStep.tsx index 2e137e2353b..a86d562f45e 100644 --- a/server/sonar-web/src/main/js/components/tutorials/github-action/SecretStep.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/github-action/SecretStep.tsx @@ -17,14 +17,20 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { + BasicSeparator, + ClipboardIconButton, + NumberedList, + NumberedListItem, + StandoutLink, +} from 'design-system'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { Button } from '../../../components/controls/buttons'; -import { ClipboardIconButton } from '../../../components/controls/clipboard'; import { translate } from '../../../helpers/l10n'; import { AlmSettingsInstance, ProjectAlmBindingResponse } from '../../../types/alm-settings'; import { Component } from '../../../types/types'; import { LoggedInUser } from '../../../types/users'; +import { InlineSnippet } from '../components/InlineSnippet'; import SentenceWithHighlights from '../components/SentenceWithHighlights'; import TokenStepGenerator from '../components/TokenStepGenerator'; import { buildGithubLink } from '../utils'; @@ -35,97 +41,94 @@ export interface SecretStepProps { component: Component; currentUser: LoggedInUser; projectBinding?: ProjectAlmBindingResponse; - onDone: () => void; } export default function SecretStep(props: SecretStepProps) { const { almBinding, baseUrl, component, currentUser, projectBinding } = props; return ( - <div className="boxed-group-inner"> - <p className="big-spacer-bottom"> - <FormattedMessage - defaultMessage={translate('onboarding.tutorial.with.github_action.secret.intro')} - id="onboarding.tutorial.with.github_action.secret.intro" - values={{ - settings_secret: - almBinding && projectBinding ? ( - <a - href={`${buildGithubLink(almBinding, projectBinding)}/settings/secrets`} - target="_blank" - rel="noopener noreferrer" - > - {translate('onboarding.tutorial.with.github_action.secret.intro.link')} - </a> - ) : ( - <strong> - {translate('onboarding.tutorial.with.github_action.secret.intro.link')} - </strong> - ), - }} - /> - </p> - <ol className="list-styled"> - <li> + <> + <FormattedMessage + defaultMessage={translate('onboarding.tutorial.with.github_action.secret.intro')} + id="onboarding.tutorial.with.github_action.secret.intro" + values={{ + settings_secret: + almBinding && projectBinding ? ( + <StandoutLink + to={`${buildGithubLink(almBinding, projectBinding)}/settings/secrets`} + target="_blank" + rel="noopener noreferrer" + > + {translate('onboarding.tutorial.with.github_action.secret.intro.link')} + </StandoutLink> + ) : ( + <span className="sw-body-sm-highlight"> + {translate('onboarding.tutorial.with.github_action.secret.intro.link')} + </span> + ), + }} + /> + <NumberedList> + <NumberedListItem> <SentenceWithHighlights translationKey="onboarding.tutorial.with.github_action.secret.new" highlightKeys={['new_secret']} /> - </li> - <li> + </NumberedListItem> + <NumberedListItem> <SentenceWithHighlights translationKey="onboarding.tutorial.with.github_action.secret.name" highlightKeys={['name']} /> - <code className="rule little-spacer-left">SONAR_TOKEN</code> + <InlineSnippet snippet="SONAR_TOKEN" className="sw-ml-1" /> <ClipboardIconButton copyValue="SONAR_TOKEN" /> - </li> + </NumberedListItem> <TokenStepGenerator component={component} currentUser={currentUser} /> - <li> + <NumberedListItem> <SentenceWithHighlights translationKey="onboarding.tutorial.with.github_action.secret.add" highlightKeys={['add_secret']} /> - </li> - </ol> - - <hr className="no-horizontal-margins" /> - - <ol className="list-styled big-spacer-top big-spacer-bottom"> - <li> + </NumberedListItem> + </NumberedList> + <BasicSeparator className="sw-my-6" /> + <NumberedList> + <NumberedListItem> <SentenceWithHighlights translationKey="onboarding.tutorial.with.github_action.secret.new" highlightKeys={['new_secret']} /> - </li> - <li> + </NumberedListItem> + <NumberedListItem> <SentenceWithHighlights translationKey="onboarding.tutorial.with.github_action.secret.name" highlightKeys={['name']} /> - - <code className="rule little-spacer-left">SONAR_HOST_URL</code> + <InlineSnippet snippet="SONAR_HOST_URL" className="sw-ml-1" /> <ClipboardIconButton copyValue="SONAR_HOST_URL" /> - </li> - <li className="big-spacer-bottom"> + </NumberedListItem> + <NumberedListItem> <FormattedMessage defaultMessage={translate('onboarding.tutorial.env_variables')} id="onboarding.tutorial.env_variables" values={{ extra: <ClipboardIconButton copyValue={baseUrl} />, - field: <strong>{translate('onboarding.tutorial.env_variables.field')}</strong>, - value: <code className="rule">{baseUrl}</code>, + field: ( + <span className="sw-body-sm-highlight"> + {translate('onboarding.tutorial.env_variables.field')} + </span> + ), + value: <InlineSnippet snippet={baseUrl} className="sw-ml-1" />, }} /> - </li> - <li> + </NumberedListItem> + <NumberedListItem> <SentenceWithHighlights translationKey="onboarding.tutorial.with.github_action.secret.add" highlightKeys={['add_secret']} /> - </li> - </ol> - <Button onClick={props.onDone}>{translate('continue')}</Button> - </div> + </NumberedListItem> + </NumberedList> + </> ); } diff --git a/server/sonar-web/src/main/js/components/tutorials/github-action/__tests__/GithubActionTutorial-it.tsx b/server/sonar-web/src/main/js/components/tutorials/github-action/__tests__/GithubActionTutorial-it.tsx index 06ab3f48ec5..16c3e2e9c87 100644 --- a/server/sonar-web/src/main/js/components/tutorials/github-action/__tests__/GithubActionTutorial-it.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/github-action/__tests__/GithubActionTutorial-it.tsx @@ -28,7 +28,7 @@ import { } from '../../../../helpers/mocks/alm-settings'; import { mockComponent } from '../../../../helpers/mocks/component'; import { mockLanguage, mockLoggedInUser } from '../../../../helpers/testMocks'; -import { renderApp, RenderContext } from '../../../../helpers/testReactTestingUtils'; +import { RenderContext, renderApp } from '../../../../helpers/testReactTestingUtils'; import { AlmKeys } from '../../../../types/alm-settings'; import { Feature } from '../../../../types/features'; import { @@ -65,45 +65,49 @@ it('should follow and complete all steps', async () => { expect(await ui.secretsStepTitle.find()).toBeInTheDocument(); // Env variables step - expect(getCopyToClipboardValue()).toMatchSnapshot('sonar token key'); - expect(getCopyToClipboardValue(1)).toMatchSnapshot('sonarqube host url key'); - expect(getCopyToClipboardValue(2)).toMatchSnapshot('sonarqube host url value'); - await user.click(ui.continueButton.get()); + expect(getCopyToClipboardValue(0, 'Copy to clipboard')).toMatchSnapshot('sonar token key'); + expect(getCopyToClipboardValue(1, 'Copy to clipboard')).toMatchSnapshot('sonarqube host url key'); + expect(getCopyToClipboardValue(2, 'Copy to clipboard')).toMatchSnapshot( + 'sonarqube host url value' + ); // Create/update configuration file step // Maven await user.click(ui.mavenBuildButton.get()); - expect(getCopyToClipboardValue(1)).toMatchSnapshot('Maven: .github/workflows/build.yml'); + expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('Maven: .github/workflows/build.yml'); // Gradle await user.click(ui.gradleBuildButton.get()); - expect(getCopyToClipboardValue(2)).toMatchSnapshot('Groovy: build.gradle'); + expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('Groovy: build.gradle'); await user.click(ui.gradleDSLButton(GradleBuildDSL.Kotlin).get()); - expect(getCopyToClipboardValue(2)).toMatchSnapshot('Kotlin: build.gradle.kts'); - expect(getCopyToClipboardValue(4)).toMatchSnapshot('Gradle: .github/workflows/build.yml'); + expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('Kotlin: build.gradle.kts'); + expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot('Gradle: .github/workflows/build.yml'); // .NET await user.click(ui.dotnetBuildButton.get()); - expect(getCopyToClipboardValue(1)).toMatchSnapshot('.NET: .github/workflows/build.yml'); + expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('.NET: .github/workflows/build.yml'); // CFamily await user.click(ui.cFamilyBuildButton.get()); - expect(getCopyToClipboardValue()).toMatchSnapshot('CFamily: sonar-project.properties'); + expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('CFamily: sonar-project.properties'); await user.click(ui.linuxButton.get()); - expect(getCopyToClipboardValue(2)).toMatchSnapshot('CFamily Linux: .github/workflows/build.yml'); + expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot( + 'CFamily Linux: .github/workflows/build.yml' + ); await user.click(ui.windowsButton.get()); - expect(getCopyToClipboardValue(2)).toMatchSnapshot( + expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot( 'CFamily Windows: .github/workflows/build.yml' ); await user.click(ui.macosButton.get()); - expect(getCopyToClipboardValue(2)).toMatchSnapshot('CFamily MacOS: .github/workflows/build.yml'); + expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot( + 'CFamily MacOS: .github/workflows/build.yml' + ); // Other await user.click(ui.otherBuildButton.get()); - expect(getCopyToClipboardValue()).toMatchSnapshot('Other: sonar-project.properties'); - expect(getCopyToClipboardValue(2)).toMatchSnapshot('Other: .github/workflows/build.yml'); + expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('Other: sonar-project.properties'); + expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot('Other: .github/workflows/build.yml'); - await user.click(ui.finishTutorialButton.get()); expect(ui.allSetSentence.get()).toBeInTheDocument(); }); @@ -116,7 +120,7 @@ it('should generate/delete a new token or use existing one', async () => { // Generate token await user.click(ui.genTokenDialogButton.get()); await user.click(ui.generateTokenButton.get()); - expect(getCopyToClipboardValue(3)).toEqual('generatedtoken2'); + expect(getCopyToClipboardValue()).toEqual('generatedtoken2'); // Revoke current token and create new one await user.click(ui.deleteTokenButton.get()); @@ -124,7 +128,7 @@ it('should generate/delete a new token or use existing one', async () => { await selectEvent.select(ui.expiresInSelect.get(), 'users.tokens.expiration.365'); await user.click(ui.generateTokenButton.get()); expect(ui.tokenValue.get()).toBeInTheDocument(); - await user.click(ui.continueButton.getAll()[1]); + await user.click(ui.continueButton.getAll()[0]); expect(ui.tokenValue.query()).not.toBeInTheDocument(); }); @@ -141,9 +145,7 @@ it('navigates between steps', async () => { // If project is bound, link to repo is visible expect(await ui.linkToRepo.find()).toBeInTheDocument(); - await user.click(await ui.continueButton.find()); await user.click(ui.mavenBuildButton.get()); - await user.click(ui.finishTutorialButton.get()); expect(ui.allSetSentence.get()).toBeInTheDocument(); await user.click(ui.ymlFileStepTitle.get()); diff --git a/server/sonar-web/src/main/js/components/tutorials/github-action/commands/CFamily.tsx b/server/sonar-web/src/main/js/components/tutorials/github-action/commands/CFamily.tsx index 34c99c81052..7aeaf12410a 100644 --- a/server/sonar-web/src/main/js/components/tutorials/github-action/commands/CFamily.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/github-action/commands/CFamily.tsx @@ -17,13 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { NumberedListItem } from 'design-system'; import * as React from 'react'; import { translate } from '../../../../helpers/l10n'; import { Component } from '../../../../types/types'; import { CompilationInfo } from '../../components/CompilationInfo'; import CreateYmlFile from '../../components/CreateYmlFile'; import DefaultProjectKey from '../../components/DefaultProjectKey'; -import FinishButton from '../../components/FinishButton'; import GithubCFamilyExampleRepositories from '../../components/GithubCFamilyExampleRepositories'; import RenderOptions from '../../components/RenderOptions'; import { OSs, TutorialModes } from '../../types'; @@ -33,7 +33,6 @@ export interface CFamilyProps { branchesEnabled?: boolean; mainBranchName: string; component: Component; - onDone: () => void; } const STEPS = { @@ -118,12 +117,12 @@ const STEPS = { export default function CFamily(props: CFamilyProps) { const { component, branchesEnabled, mainBranchName } = props; - const [os, setOs] = React.useState<undefined | OSs>(); + const [os, setOs] = React.useState<undefined | OSs>(OSs.Linux); return ( <> <DefaultProjectKey component={component} /> - <li className="abs-width-600"> + <NumberedListItem> <span>{translate('onboarding.build.other.os')}</span> <RenderOptions label={translate('onboarding.build.other.os')} @@ -134,12 +133,12 @@ export default function CFamily(props: CFamilyProps) { /> {os && ( <GithubCFamilyExampleRepositories - className="big-spacer-top" + className="sw-mt-4" os={os} ci={TutorialModes.GitHubActions} /> )} - </li> + </NumberedListItem> {os && ( <> <CreateYmlFile @@ -151,8 +150,7 @@ export default function CFamily(props: CFamilyProps) { STEPS[os] )} /> - <CompilationInfo className="abs-width-800" /> - <FinishButton onClick={props.onDone} /> + <CompilationInfo /> </> )} </> diff --git a/server/sonar-web/src/main/js/components/tutorials/github-action/commands/DotNet.tsx b/server/sonar-web/src/main/js/components/tutorials/github-action/commands/DotNet.tsx index 26580bbfc34..ad306727393 100644 --- a/server/sonar-web/src/main/js/components/tutorials/github-action/commands/DotNet.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/github-action/commands/DotNet.tsx @@ -20,7 +20,6 @@ import * as React from 'react'; import { Component } from '../../../../types/types'; import CreateYmlFile from '../../components/CreateYmlFile'; -import FinishButton from '../../components/FinishButton'; import { GITHUB_ACTIONS_RUNS_ON_WINDOWS } from '../constants'; import { generateGitHubActionsYaml } from '../utils'; @@ -28,7 +27,6 @@ export interface DotNetProps { branchesEnabled?: boolean; mainBranchName: string; component: Component; - onDone: () => void; } function dotnetYamlSteps(projectKey: string) { @@ -69,17 +67,14 @@ function dotnetYamlSteps(projectKey: string) { export default function DotNet(props: DotNetProps) { const { component, branchesEnabled, mainBranchName } = props; return ( - <> - <CreateYmlFile - yamlFileName=".github/workflows/build.yml" - yamlTemplate={generateGitHubActionsYaml( - mainBranchName, - !!branchesEnabled, - GITHUB_ACTIONS_RUNS_ON_WINDOWS, - dotnetYamlSteps(component.key) - )} - /> - <FinishButton onClick={props.onDone} /> - </> + <CreateYmlFile + yamlFileName=".github/workflows/build.yml" + yamlTemplate={generateGitHubActionsYaml( + mainBranchName, + !!branchesEnabled, + GITHUB_ACTIONS_RUNS_ON_WINDOWS, + dotnetYamlSteps(component.key) + )} + /> ); } diff --git a/server/sonar-web/src/main/js/components/tutorials/github-action/commands/Gradle.tsx b/server/sonar-web/src/main/js/components/tutorials/github-action/commands/Gradle.tsx index c0e84c06cd8..f5ab48b5b1d 100644 --- a/server/sonar-web/src/main/js/components/tutorials/github-action/commands/Gradle.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/github-action/commands/Gradle.tsx @@ -20,7 +20,6 @@ import * as React from 'react'; import { Component } from '../../../../types/types'; import CreateYmlFile from '../../components/CreateYmlFile'; -import FinishButton from '../../components/FinishButton'; import GradleBuild from '../../components/GradleBuild'; import { GITHUB_ACTIONS_RUNS_ON_LINUX } from '../constants'; import { generateGitHubActionsYaml } from '../utils'; @@ -29,7 +28,6 @@ export interface GradleProps { branchesEnabled?: boolean; mainBranchName: string; component: Component; - onDone: () => void; } const GRADLE_YAML_STEPS = ` @@ -71,7 +69,6 @@ export default function Gradle(props: GradleProps) { GRADLE_YAML_STEPS )} /> - <FinishButton onClick={props.onDone} /> </> ); } diff --git a/server/sonar-web/src/main/js/components/tutorials/github-action/commands/JavaMaven.tsx b/server/sonar-web/src/main/js/components/tutorials/github-action/commands/JavaMaven.tsx index c9b253dd3e8..a619a5eeb87 100644 --- a/server/sonar-web/src/main/js/components/tutorials/github-action/commands/JavaMaven.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/github-action/commands/JavaMaven.tsx @@ -20,7 +20,6 @@ import * as React from 'react'; import { Component } from '../../../../types/types'; import CreateYmlFile from '../../components/CreateYmlFile'; -import FinishButton from '../../components/FinishButton'; import { GITHUB_ACTIONS_RUNS_ON_LINUX } from '../constants'; import { generateGitHubActionsYaml } from '../utils'; @@ -28,7 +27,6 @@ export interface JavaMavenProps { branchesEnabled?: boolean; mainBranchName: string; component: Component; - onDone: () => void; } function mavenYamlSteps(projectKey: string, projectName: string) { @@ -60,17 +58,14 @@ function mavenYamlSteps(projectKey: string, projectName: string) { export default function JavaMaven(props: JavaMavenProps) { const { component, branchesEnabled, mainBranchName } = props; return ( - <> - <CreateYmlFile - yamlFileName=".github/workflows/build.yml" - yamlTemplate={generateGitHubActionsYaml( - mainBranchName, - !!branchesEnabled, - GITHUB_ACTIONS_RUNS_ON_LINUX, - mavenYamlSteps(component.key, component.name) - )} - /> - <FinishButton onClick={props.onDone} /> - </> + <CreateYmlFile + yamlFileName=".github/workflows/build.yml" + yamlTemplate={generateGitHubActionsYaml( + mainBranchName, + !!branchesEnabled, + GITHUB_ACTIONS_RUNS_ON_LINUX, + mavenYamlSteps(component.key, component.name) + )} + /> ); } diff --git a/server/sonar-web/src/main/js/components/tutorials/github-action/commands/Others.tsx b/server/sonar-web/src/main/js/components/tutorials/github-action/commands/Others.tsx index b2a067f0b7c..d3633fce33c 100644 --- a/server/sonar-web/src/main/js/components/tutorials/github-action/commands/Others.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/github-action/commands/Others.tsx @@ -21,7 +21,6 @@ import * as React from 'react'; import { Component } from '../../../../types/types'; import CreateYmlFile from '../../components/CreateYmlFile'; import DefaultProjectKey from '../../components/DefaultProjectKey'; -import FinishButton from '../../components/FinishButton'; import { GITHUB_ACTIONS_RUNS_ON_LINUX } from '../constants'; import { generateGitHubActionsYaml } from '../utils'; @@ -29,7 +28,6 @@ export interface OthersProps { branchesEnabled?: boolean; mainBranchName: string; component: Component; - onDone: () => void; } function otherYamlSteps(branchesEnabled: boolean) { @@ -70,7 +68,6 @@ export default function Others(props: OthersProps) { otherYamlSteps(!!branchesEnabled) )} /> - <FinishButton onClick={props.onDone} /> </> ); } diff --git a/server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/JenkinsTutorial-it.tsx b/server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/JenkinsTutorial-it.tsx index 0031a56f1dd..fd1378270cf 100644 --- a/server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/JenkinsTutorial-it.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/JenkinsTutorial-it.tsx @@ -17,7 +17,6 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ - import userEvent from '@testing-library/user-event'; import React from 'react'; import UserTokensMock from '../../../../api/mocks/UserTokensMock'; @@ -80,7 +79,7 @@ const ui = { jenkinsStepTitle: byRole('heading', { name: 'onboarding.tutorial.with.jenkins.jenkinsfile.title', }), - allSetSentence: byText('onboarding.tutorial.ci_outro.all_set.sentence'), + allSetSentence: byText('onboarding.tutorial.ci_outro.done'), ...getTutorialActionButtons(), ...getTutorialBuildButtons(), }; @@ -139,23 +138,23 @@ it.each([AlmKeys.BitbucketCloud, AlmKeys.BitbucketServer, AlmKeys.GitHub, AlmKey // CFamilly await user.click(ui.cFamilyBuildButton.get()); - expect(getCopyToClipboardValue()).toMatchSnapshot(`sonar-project.properties code`); + expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(`sonar-project.properties code`); await user.click(ui.linuxButton.get()); - expect(getCopyToClipboardValue(1)).toMatchSnapshot(`cfamily linux jenkinsfile`); + expect(getCopyToClipboardValue()).toMatchSnapshot(`cfamily linux jenkinsfile`); await user.click(ui.windowsButton.get()); - expect(getCopyToClipboardValue(1)).toMatchSnapshot(`cfamily windows jenkinsfile`); + expect(getCopyToClipboardValue()).toMatchSnapshot(`cfamily windows jenkinsfile`); await user.click(ui.macosButton.get()); - expect(getCopyToClipboardValue(1)).toMatchSnapshot(`cfamily macos jenkinsfile`); + expect(getCopyToClipboardValue()).toMatchSnapshot(`cfamily macos jenkinsfile`); // Other await user.click(ui.otherBuildButton.get()); - expect(getCopyToClipboardValue()).toMatchSnapshot( + expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot( `other build tools sonar-project.properties code` ); - expect(getCopyToClipboardValue(1)).toMatchSnapshot(`other build tools jenkinsfile`); + expect(getCopyToClipboardValue()).toMatchSnapshot(`other build tools jenkinsfile`); await user.click(ui.finishTutorialButton.get()); expect(ui.allSetSentence.get()).toBeInTheDocument(); diff --git a/server/sonar-web/src/main/js/components/tutorials/test-utils.ts b/server/sonar-web/src/main/js/components/tutorials/test-utils.ts index 760ee7d2d95..58be0817983 100644 --- a/server/sonar-web/src/main/js/components/tutorials/test-utils.ts +++ b/server/sonar-web/src/main/js/components/tutorials/test-utils.ts @@ -52,7 +52,7 @@ export function getCommonNodes(ci: TutorialModes) { ci === TutorialModes.GitHubActions ? 'secret' : 'variables' }.intro.link`, }), - allSetSentence: byText('onboarding.tutorial.ci_outro.all_set.sentence'), + allSetSentence: byText('onboarding.tutorial.ci_outro.done'), }; } diff --git a/server/sonar-web/src/main/js/hooks/useIntersectionObserver.ts b/server/sonar-web/src/main/js/hooks/useIntersectionObserver.ts new file mode 100644 index 00000000000..6da44bf380f --- /dev/null +++ b/server/sonar-web/src/main/js/hooks/useIntersectionObserver.ts @@ -0,0 +1,53 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 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 { RefObject, useEffect, useState } from 'react'; +import { isDefined } from '../helpers/types'; + +interface Options extends IntersectionObserverInit { + freezeOnceVisible?: boolean; +} + +export default function useIntersectionObserver<T extends Element>( + ref: RefObject<T>, + options: Options = {} +) { + const { root = null, rootMargin = '0px', threshold = 0, freezeOnceVisible = false } = options; + const [entry, setEntry] = useState<IntersectionObserverEntry>(); + + const frozen = (entry?.isIntersecting || false) && freezeOnceVisible; + + useEffect(() => { + if (!isDefined(IntersectionObserver) || !isDefined(ref.current) || frozen) { + return; + } + + const observer = new IntersectionObserver( + ([entry]) => setEntry(entry), + + { root, rootMargin, threshold } + ); + + observer.observe(ref.current); + return () => observer.disconnect(); + }, [ref, frozen, root, rootMargin, threshold]); + + return entry; +} diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock index 17a4c72095d..909ed7d284f 100644 --- a/server/sonar-web/yarn.lock +++ b/server/sonar-web/yarn.lock @@ -3606,6 +3606,71 @@ __metadata: languageName: node linkType: hard +"@react-spring/animated@npm:~9.7.3": + version: 9.7.3 + resolution: "@react-spring/animated@npm:9.7.3" + dependencies: + "@react-spring/shared": ~9.7.3 + "@react-spring/types": ~9.7.3 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 468942ca3a11c02c3e56def26b2da9dd10ddbed548004245c4ac309cce00b58d971e781abed67db0d652f72737eaa73766ea9a43b8ef3b08a7ed2eddc04d4c39 + languageName: node + linkType: hard + +"@react-spring/core@npm:~9.7.3": + version: 9.7.3 + resolution: "@react-spring/core@npm:9.7.3" + dependencies: + "@react-spring/animated": ~9.7.3 + "@react-spring/shared": ~9.7.3 + "@react-spring/types": ~9.7.3 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 8a80a07276458fd14099320eda824e58a11ce3a9b03a5c9cd3f4252adb4d26da04ee5caf5cbc961199f55c2d58a99638d5ea292cdb6aa029208dbab741b5c531 + languageName: node + linkType: hard + +"@react-spring/rafz@npm:9.7.3": + version: 9.7.3 + resolution: "@react-spring/rafz@npm:9.7.3" + checksum: c88b3c6306eab4a93a9f4ea38ad2e0ea3287a8b79a9a092cb1fd934a97ce092e41c32373b2b2e3de4b63027de7ae7414e0d442936dd8d94b51c2ff7d51e8f643 + languageName: node + linkType: hard + +"@react-spring/shared@npm:~9.7.3": + version: 9.7.3 + resolution: "@react-spring/shared@npm:9.7.3" + dependencies: + "@react-spring/types": ~9.7.3 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 912b5e567eb5345c9a6c8e8c0c2d69b1f411af72a0685b95831809c267c89846a31341ca071f284ace98b3cb5de647054dc76f6ace81d6379513eaf96b52f195 + languageName: node + linkType: hard + +"@react-spring/types@npm:~9.7.3": + version: 9.7.3 + resolution: "@react-spring/types@npm:9.7.3" + checksum: f47b81fe556464aa54a78603311cb584d6a0f03088522229afb058265bbe2ade2095a55ec7f4e960c3b9cceaa5d47865bc41fc6643c0f5f4bd3d8650203d8389 + languageName: node + linkType: hard + +"@react-spring/web@npm:9.7.3": + version: 9.7.3 + resolution: "@react-spring/web@npm:9.7.3" + dependencies: + "@react-spring/animated": ~9.7.3 + "@react-spring/core": ~9.7.3 + "@react-spring/shared": ~9.7.3 + "@react-spring/types": ~9.7.3 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 7f5cd05b2314b7f2f715e1926abcf9aa0a539399b222ab34e989144f48350adfcd2edab65d41425570f72c57f602fc6994d6730fbeed902171ac527b630a8a9b + languageName: node + linkType: hard + "@remix-run/router@npm:1.6.2": version: 1.6.2 resolution: "@remix-run/router@npm:1.6.2" @@ -4844,6 +4909,8 @@ __metadata: "@emotion/react": 11.11.1 "@emotion/styled": 11.11.0 "@primer/octicons-react": 19.3.0 + "@react-spring/rafz": 9.7.3 + "@react-spring/web": 9.7.3 "@swc/core": 1.3.65 "@swc/jest": 0.2.26 "@tanstack/react-query": 4.29.14 |