Browse Source

SONAR-22148 Spotlight tour for cayc in branch overview

tags/10.6.0.92116
Mathieu Suen 1 month ago
parent
commit
af82ae0255
24 changed files with 620 additions and 74 deletions
  1. 21
    0
      server/sonar-web/design-system/config/jest/CSSStub.js
  2. 1
    1
      server/sonar-web/design-system/jest.config.js
  3. 2
    1
      server/sonar-web/design-system/package.json
  4. 26
    1
      server/sonar-web/design-system/src/components/SpotlightTour.tsx
  5. 1
    0
      server/sonar-web/design-system/src/theme/light.ts
  6. 2
    2
      server/sonar-web/package.json
  7. 2
    2
      server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx
  8. 4
    1
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx
  9. 25
    2
      server/sonar-web/src/main/js/apps/overview/branches/BranchMetaTopBar.tsx
  10. 76
    1
      server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx
  11. 134
    0
      server/sonar-web/src/main/js/apps/overview/branches/CaycPromotionGuide.tsx
  12. 96
    0
      server/sonar-web/src/main/js/apps/overview/branches/PromotedSection.tsx
  13. 1
    1
      server/sonar-web/src/main/js/apps/overview/branches/QualityGateStatusHeader.tsx
  14. 73
    0
      server/sonar-web/src/main/js/apps/overview/branches/ReplayTour.tsx
  15. 5
    1
      server/sonar-web/src/main/js/apps/overview/branches/TabsPanel.tsx
  16. 70
    11
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx
  17. 1
    1
      server/sonar-web/src/main/js/apps/overview/components/LastAnalysisLabel.tsx
  18. 1
    1
      server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx
  19. 1
    1
      server/sonar-web/src/main/js/components/tutorials/test-utils.ts
  20. 1
    0
      server/sonar-web/src/main/js/types/users.ts
  21. 47
    45
      server/sonar-web/yarn.lock
  22. 1
    1
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/DismissNoticeActionIT.java
  23. 3
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DismissNoticeAction.java
  24. 26
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 21
- 0
server/sonar-web/design-system/config/jest/CSSStub.js View File

/*
* SonarQube
* Copyright (C) 2009-2024 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.
*/
module.exports = {};


+ 1
- 1
server/sonar-web/design-system/jest.config.js View File

moduleNameMapper: { moduleNameMapper: {
'^.+\\.(md|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '^.+\\.(md|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/config/jest/FileStub.js', '<rootDir>/config/jest/FileStub.js',
// '^.+\\.css$': '<rootDir>/config/jest/CSSStub.js',
'^.+\\.css$': '<rootDir>/config/jest/CSSStub.js',
}, },
setupFiles: [ setupFiles: [
'<rootDir>/config/jest/SetupTestEnvironment.js', '<rootDir>/config/jest/SetupTestEnvironment.js',

+ 2
- 1
server/sonar-web/design-system/package.json View File

"@babel/preset-typescript": "7.23.3", "@babel/preset-typescript": "7.23.3",
"@emotion/babel-plugin": "11.11.0", "@emotion/babel-plugin": "11.11.0",
"@emotion/babel-plugin-jsx-pragmatic": "0.2.1", "@emotion/babel-plugin-jsx-pragmatic": "0.2.1",
"@sonarsource/echoes-react": "0.2.2",
"@testing-library/dom": "9.3.4", "@testing-library/dom": "9.3.4",
"@testing-library/jest-dom": "6.4.2", "@testing-library/jest-dom": "6.4.2",
"@testing-library/react": "14.2.1", "@testing-library/react": "14.2.1",
"react-helmet-async": "2.0.4", "react-helmet-async": "2.0.4",
"react-highlight-words": "0.20.0", "react-highlight-words": "0.20.0",
"react-intl": "6.6.2", "react-intl": "6.6.2",
"react-joyride": "2.7.2",
"react-joyride": "2.8.1",
"react-modal": "3.16.1", "react-modal": "3.16.1",
"react-router-dom": "6.22.0", "react-router-dom": "6.22.0",
"react-select": "5.7.7", "react-select": "5.7.7",

+ 26
- 1
server/sonar-web/design-system/src/components/SpotlightTour.tsx View File

*/ */
import { keyframes } from '@emotion/react'; import { keyframes } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { LinkStandalone } from '@sonarsource/echoes-react';
import React from 'react'; import React from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import ReactJoyride, { import ReactJoyride, {
Step as JoyrideStep, Step as JoyrideStep,
TooltipRenderProps, TooltipRenderProps,
} from 'react-joyride'; } from 'react-joyride';
import { LinkProps } from 'react-router-dom';
import tw from 'twin.macro'; import tw from 'twin.macro';
import { GLOBAL_POPUP_Z_INDEX, PopupZLevel, themeColor } from '../helpers'; import { GLOBAL_POPUP_Z_INDEX, PopupZLevel, themeColor } from '../helpers';
import { findAnchor } from '../helpers/dom'; import { findAnchor } from '../helpers/dom';
type Placement = 'left' | 'right' | 'top' | 'bottom' | 'center'; type Placement = 'left' | 'right' | 'top' | 'bottom' | 'center';


export interface SpotlightTourProps extends Omit<JoyrideProps, 'steps'> { export interface SpotlightTourProps extends Omit<JoyrideProps, 'steps'> {
actionLabel?: string;
actionPath?: LinkProps['to'];
backLabel?: string; backLabel?: string;
closeLabel?: string; closeLabel?: string;
nextLabel?: string; nextLabel?: string;
const defultRect = new DOMRect(0, 0, 0, 0); const defultRect = new DOMRect(0, 0, 0, 0);


function TooltipComponent({ function TooltipComponent({
actionLabel,
actionPath,
continuous, continuous,
index, index,
step, step,
tooltipProps, tooltipProps,
width = DEFAULT_WIDTH, width = DEFAULT_WIDTH,
}: TooltipRenderProps & { }: TooltipRenderProps & {
actionLabel?: string;
actionPath?: LinkProps['to'];
step: SpotlightTourStep; step: SpotlightTourStep;
stepXofYLabel: SpotlightTourProps['stepXofYLabel']; stepXofYLabel: SpotlightTourProps['stepXofYLabel'];
width?: number; width?: number;
</WrapperButton> </WrapperButton>
</div> </div>
<div>{step.content}</div> <div>{step.content}</div>

{actionLabel && actionPath && (
<div className="sw-pt-4">
<LinkStandalone to={actionPath}>{actionLabel}</LinkStandalone>
</div>
)}

<div className="sw-flex sw-justify-between sw-items-center sw-mt-4"> <div className="sw-flex sw-justify-between sw-items-center sw-mt-4">
{(stepXofYLabel || size > 1) && ( {(stepXofYLabel || size > 1) && (
<strong> <strong>


export function SpotlightTour(props: SpotlightTourProps) { export function SpotlightTour(props: SpotlightTourProps) {
const { const {
actionLabel,
actionPath,
steps, steps,
skipLabel, skipLabel,
backLabel, backLabel,
}))} }))}
tooltipComponent={( tooltipComponent={(
tooltipProps: React.PropsWithChildren<TooltipRenderProps & { step: SpotlightTourStep }>, tooltipProps: React.PropsWithChildren<TooltipRenderProps & { step: SpotlightTourStep }>,
) => <TooltipComponent stepXofYLabel={stepXofYLabel} width={width} {...tooltipProps} />}
) => (
<TooltipComponent
actionLabel={actionLabel}
actionPath={actionPath}
stepXofYLabel={stepXofYLabel}
width={width}
{...tooltipProps}
/>
)}
{...otherProps} {...otherProps}
/> />
); );

+ 1
- 0
server/sonar-web/design-system/src/theme/light.ts View File

projectCardInfo: COLORS.blueGrey[35], projectCardInfo: COLORS.blueGrey[35],


// overview // overview
backgroundPromotedSection: secondary.light,
overviewCardDefaultIcon: secondary.light, overviewCardDefaultIcon: secondary.light,
iconOverviewIssue: COLORS.blueGrey[400], iconOverviewIssue: COLORS.blueGrey[400],
overviewCardWarningIcon: COLORS.yellow[50], overviewCardWarningIcon: COLORS.yellow[50],

+ 2
- 2
server/sonar-web/package.json View File

"@primer/octicons-react": "19.8.0", "@primer/octicons-react": "19.8.0",
"@react-spring/rafz": "9.7.3", "@react-spring/rafz": "9.7.3",
"@react-spring/web": "9.7.3", "@react-spring/web": "9.7.3",
"@sonarsource/echoes-react": "0.2.1",
"@sonarsource/echoes-react": "0.2.2",
"@tanstack/react-query": "5.18.1", "@tanstack/react-query": "5.18.1",
"axios": "1.6.7", "axios": "1.6.7",
"classnames": "2.5.1", "classnames": "2.5.1",
"react-helmet-async": "2.0.4", "react-helmet-async": "2.0.4",
"react-highlight-words": "0.20.0", "react-highlight-words": "0.20.0",
"react-intl": "6.6.2", "react-intl": "6.6.2",
"react-joyride": "2.7.2",
"react-joyride": "2.8.1",
"react-modal": "3.16.1", "react-modal": "3.16.1",
"react-router-dom": "6.22.0", "react-router-dom": "6.22.0",
"react-select": "5.7.7", "react-select": "5.7.7",

+ 2
- 2
server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx View File

pluginsLink: byRole('link', { name: 'opens_in_new_window footer.plugins' }), pluginsLink: byRole('link', { name: 'opens_in_new_window footer.plugins' }),
apiLink: byRole('link', { name: 'footer.web_api' }), apiLink: byRole('link', { name: 'footer.web_api' }),
ltaDocumentationLinkActive: byRole('link', { ltaDocumentationLinkActive: byRole('link', {
name: `footer.version.status.active open_in_new_window`,
name: `footer.version.status.active open_in_new_tab`,
}), }),
ltaDocumentationLinkInactive: byRole('link', { ltaDocumentationLinkInactive: byRole('link', {
name: `footer.version.status.inactive open_in_new_window`,
name: `footer.version.status.inactive open_in_new_tab`,
}), }),
}; };

+ 4
- 1
server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx View File

return ( return (
<> <>
<SlashSeparator className=" sw-mx-2" /> <SlashSeparator className=" sw-mx-2" />
<div className="sw-flex sw-items-center it__branch-like-navigation-toggler-container">
<div
className="sw-flex sw-items-center it__branch-like-navigation-toggler-container"
data-spotlight-id="cayc-promotion-4"
>
<Popup <Popup
allowResizing allowResizing
overlay={ overlay={

+ 25
- 2
server/sonar-web/src/main/js/apps/overview/branches/BranchMetaTopBar.tsx View File

* along with this program; if not, write to the Free Software Foundation, * along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */
import { SeparatorCircleIcon } from 'design-system';
import { IconSlideshow } from '@sonarsource/echoes-react';
import { ButtonSecondary, SeparatorCircleIcon } from 'design-system';
import React from 'react'; import React from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { formatMeasure } from '~sonar-aligned/helpers/measures'; import { formatMeasure } from '~sonar-aligned/helpers/measures';
import { getCurrentPage } from '../../../app/components/nav/component/utils'; import { getCurrentPage } from '../../../app/components/nav/component/utils';
import ComponentReportActions from '../../../components/controls/ComponentReportActions'; import ComponentReportActions from '../../../components/controls/ComponentReportActions';
import HomePageSelect from '../../../components/controls/HomePageSelect'; import HomePageSelect from '../../../components/controls/HomePageSelect';
import Tooltip from '../../../components/controls/Tooltip';
import { translate } from '../../../helpers/l10n';
import { findMeasure } from '../../../helpers/measures'; import { findMeasure } from '../../../helpers/measures';
import { Branch } from '../../../types/branch-like'; import { Branch } from '../../../types/branch-like';
import { Component, MeasureEnhanced } from '../../../types/types'; import { Component, MeasureEnhanced } from '../../../types/types';
component: Component; component: Component;
branch: Branch; branch: Branch;
measures: MeasureEnhanced[]; measures: MeasureEnhanced[];
showTakeTheTourButton: boolean;
startTour?: () => void;
} }


export default function BranchMetaTopBar({ branch, measures, component }: Readonly<Props>) {
export default function BranchMetaTopBar({
branch,
measures,
component,
showTakeTheTourButton,
startTour,
}: Readonly<Props>) {
const intl = useIntl(); const intl = useIntl();


const currentPage = getCurrentPage(component, branch) as HomePage; const currentPage = getCurrentPage(component, branch) as HomePage;
)} )}
<HomePageSelect currentPage={currentPage} type="button" /> <HomePageSelect currentPage={currentPage} type="button" />
<ComponentReportActions component={component} branch={branch} /> <ComponentReportActions component={component} branch={branch} />
{showTakeTheTourButton && (
<Tooltip overlay={translate('overview.promoted_section.button_tooltip')}>
<ButtonSecondary
className="sw-pl-4 sw-shrink-0"
data-spotlight-id="take-tour-1"
onClick={startTour}
>
<IconSlideshow className="sw-mr-1" />
{translate('overview.promoted_section.button_primary')}
</ButtonSecondary>
</Tooltip>
)}
</div> </div>
); );



+ 76
- 1
server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx View File

PageContentFontWrapper, PageContentFontWrapper,
} from 'design-system'; } from 'design-system';
import * as React from 'react'; import * as React from 'react';
import { useState } from 'react';
import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget'; import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
import { useLocation, useRouter } from '~sonar-aligned/components/hoc/withRouter'; import { useLocation, useRouter } from '~sonar-aligned/components/hoc/withRouter';
import { isPortfolioLike } from '~sonar-aligned/helpers/component'; import { isPortfolioLike } from '~sonar-aligned/helpers/component';
import { ComponentQualifier } from '~sonar-aligned/types/component'; import { ComponentQualifier } from '~sonar-aligned/types/component';
import { CurrentUserContext } from '../../../app/components/current-user/CurrentUserContext';
import AnalysisMissingInfoMessage from '../../../components/shared/AnalysisMissingInfoMessage'; import AnalysisMissingInfoMessage from '../../../components/shared/AnalysisMissingInfoMessage';
import { parseDate } from '../../../helpers/dates'; import { parseDate } from '../../../helpers/dates';
import { translate } from '../../../helpers/l10n';
import { areCCTMeasuresComputed, isDiffMetric } from '../../../helpers/measures'; import { areCCTMeasuresComputed, isDiffMetric } from '../../../helpers/measures';
import { CodeScope } from '../../../helpers/urls'; import { CodeScope } from '../../../helpers/urls';
import { useDismissNoticeMutation } from '../../../queries/users';
import { ApplicationPeriod } from '../../../types/application'; import { ApplicationPeriod } from '../../../types/application';
import { Branch } from '../../../types/branch-like'; import { Branch } from '../../../types/branch-like';
import { Analysis, GraphType, MeasureHistory } from '../../../types/project-activity'; import { Analysis, GraphType, MeasureHistory } from '../../../types/project-activity';
import { QualityGateStatus } from '../../../types/quality-gates'; import { QualityGateStatus } from '../../../types/quality-gates';
import { Component, MeasureEnhanced, Metric, Period, QualityGate } from '../../../types/types'; import { Component, MeasureEnhanced, Metric, Period, QualityGate } from '../../../types/types';
import { NoticeType } from '../../../types/users';
import { AnalysisStatus } from '../components/AnalysisStatus'; import { AnalysisStatus } from '../components/AnalysisStatus';
import LastAnalysisLabel from '../components/LastAnalysisLabel'; import LastAnalysisLabel from '../components/LastAnalysisLabel';
import ActivityPanel from './ActivityPanel'; import ActivityPanel from './ActivityPanel';
import BranchMetaTopBar from './BranchMetaTopBar'; import BranchMetaTopBar from './BranchMetaTopBar';
import CaycPromotionGuide from './CaycPromotionGuide';
import FirstAnalysisNextStepsNotif from './FirstAnalysisNextStepsNotif'; import FirstAnalysisNextStepsNotif from './FirstAnalysisNextStepsNotif';
import MeasuresPanelNoNewCode from './MeasuresPanelNoNewCode'; import MeasuresPanelNoNewCode from './MeasuresPanelNoNewCode';
import NewCodeMeasuresPanel from './NewCodeMeasuresPanel'; import NewCodeMeasuresPanel from './NewCodeMeasuresPanel';
import NoCodeWarning from './NoCodeWarning'; import NoCodeWarning from './NoCodeWarning';
import OverallCodeMeasuresPanel from './OverallCodeMeasuresPanel'; import OverallCodeMeasuresPanel from './OverallCodeMeasuresPanel';
import PromotedSection from './PromotedSection';
import QualityGatePanel from './QualityGatePanel'; import QualityGatePanel from './QualityGatePanel';
import { QualityGateStatusTitle } from './QualityGateStatusTitle'; import { QualityGateStatusTitle } from './QualityGateStatusTitle';
import ReplayTourGuide from './ReplayTour';
import SonarLintPromotion from './SonarLintPromotion'; import SonarLintPromotion from './SonarLintPromotion';
import { TabsPanel } from './TabsPanel'; import { TabsPanel } from './TabsPanel';


const { query } = useLocation(); const { query } = useLocation();
const router = useRouter(); const router = useRouter();


const { currentUser } = React.useContext(CurrentUserContext);

const { mutateAsync: dismissNotice } = useDismissNoticeMutation();

const [startTour, setStartTour] = useState(false);
const [tourCompleted, setTourCompleted] = useState(false);
const [showReplay, setShowReplay] = useState(false);
const [dismissedTour, setDismissedTour] = useState(
currentUser.isLoggedIn &&
!!currentUser.dismissedNotices[NoticeType.ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE],
);

const tab = query.codeScope === CodeScope.Overall ? CodeScope.Overall : CodeScope.New; const tab = query.codeScope === CodeScope.Overall ? CodeScope.Overall : CodeScope.New;
const leakPeriod = component.qualifier === ComponentQualifier.Application ? appLeak : period; const leakPeriod = component.qualifier === ComponentQualifier.Application ? appLeak : period;
const isNewCodeTab = tab === CodeScope.New; const isNewCodeTab = tab === CodeScope.New;
/> />
); );


const dismissPromotedSection = () => {
dismissNotice(NoticeType.ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE);

setDismissedTour(true);
setShowReplay(true);
};

const closeTour = (action: string) => {
setStartTour(false);
if (action === 'skip' && !dismissedTour) {
dismissPromotedSection();
}

if (action === 'close' && !dismissedTour) {
dismissPromotedSection();
setTourCompleted(true);
}
};

const startTourGuide = () => {
if (!isNewCodeTab) {
selectTab(CodeScope.New);
}
setShowReplay(false);
setStartTour(true);
};

return ( return (
<> <>
<FirstAnalysisNextStepsNotif <FirstAnalysisNextStepsNotif
/> />
<LargeCenteredLayout> <LargeCenteredLayout>
<PageContentFontWrapper> <PageContentFontWrapper>
<CaycPromotionGuide closeTour={closeTour} run={startTour} />
{showReplay && (
<ReplayTourGuide
closeTour={() => setShowReplay(false)}
run={showReplay}
tourCompleted={tourCompleted}
/>
)}
<div className="overview sw-my-6 sw-body-sm"> <div className="overview sw-my-6 sw-body-sm">
<A11ySkipTarget anchor="overview_main" /> <A11ySkipTarget anchor="overview_main" />


<div> <div>
{branch && ( {branch && (
<> <>
<BranchMetaTopBar branch={branch} component={component} measures={measures} />
{currentUser.isLoggedIn && (
<PromotedSection
content={translate('overview.promoted_section.content')}
dismissed={dismissedTour ?? false}
onDismiss={dismissPromotedSection}
onPrimaryButtonClick={startTourGuide}
primaryButtonLabel={translate('overview.promoted_section.button_primary')}
secondaryButtonLabel={translate(
'overview.promoted_section.button_secondary',
)}
title={translate('overview.promoted_section.title')}
/>
)}

<BranchMetaTopBar
branch={branch}
component={component}
measures={measures}
showTakeTheTourButton={dismissedTour && currentUser.isLoggedIn}
startTour={startTourGuide}
/>
<BasicSeparator /> <BasicSeparator />
</> </>
)} )}

+ 134
- 0
server/sonar-web/src/main/js/apps/overview/branches/CaycPromotionGuide.tsx View File

/*
* SonarQube
* Copyright (C) 2009-2024 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 { SpotlightTour, SpotlightTourStep } from 'design-system';
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { translate, translateWithParameters } from '../../../helpers/l10n';

interface Props {
closeTour: (action: string) => void;
run: boolean;
}

function CaycPromotionGuide(props: Readonly<Props>) {
const { run } = props;
const onToggle = ({ action, type }: { action: string; type: string }) => {
if (type === 'tour:end' && (action === 'close' || action === 'skip')) {
props.closeTour(action);
}
};

const constructContent = (first: string) => <p className="sw-mt-2">{translate(first)}</p>;

const constructContentLastStep = (first: string, second: string, third: string) => (
<>
<p className="sw-mt-2">
<FormattedMessage
defaultMessage={translate(first)}
id={first}
values={{
value: <strong>{translate('ide')}</strong>,
}}
/>
</p>
<p className="sw-mt-2">
<FormattedMessage
defaultMessage={translate(second)}
id={second}
values={{
value: <strong>{translate('pull_request.small')}</strong>,
}}
/>
</p>
<p className="sw-mt-2">
<FormattedMessage
defaultMessage={translate(third)}
id={third}
values={{
value: <strong>{translate('branch.small')}</strong>,
}}
/>
</p>
</>
);

const steps: SpotlightTourStep[] = [
{
disableScrolling: false,
disableOverlayClose: true,
target: '[data-spotlight-id="cayc-promotion-1"]',
content: constructContent('guiding.cayc_promotion.1.content.1'),
title: translate('guiding.cayc_promotion.1.title'),
placement: 'left',
},
{
disableScrolling: true,
disableOverlayClose: true,
target: '[data-spotlight-id="cayc-promotion-2"]',
content: constructContent('guiding.cayc_promotion.2.content.1'),
title: translate('guiding.cayc_promotion.2.title'),
placement: 'left',
},
{
disableScrolling: true,
disableOverlayClose: true,
target: '[data-spotlight-id="cayc-promotion-3"]',
content: constructContent('guiding.cayc_promotion.3.content.1'),
title: translate('guiding.cayc_promotion.3.title'),
placement: 'right',
},
{
disableScrolling: true,
disableOverlayClose: true,
target: '[data-spotlight-id="cayc-promotion-4"]',
content: constructContentLastStep(
'guiding.cayc_promotion.4.content.1',
'guiding.cayc_promotion.4.content.2',
'guiding.cayc_promotion.4.content.3',
),
title: translate('guiding.cayc_promotion.4.title'),
placement: 'right',
spotlightPadding: 0,
},
];

return (
<SpotlightTour
disableOverlay={false}
disableScrolling
backLabel={translate('previous')}
callback={onToggle}
closeLabel={translate('complete')}
continuous
nextLabel={translate('next')}
run={run}
skipLabel={translate('skip')}
stepXofYLabel={(x: number, y: number) => translateWithParameters('guiding.step_x_of_y', x, y)}
steps={steps}
styles={{
options: {
zIndex: 1000,
},
}}
/>
);
}

export default CaycPromotionGuide;

+ 96
- 0
server/sonar-web/src/main/js/apps/overview/branches/PromotedSection.tsx View File

/*
* SonarQube
* Copyright (C) 2009-2024 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 styled from '@emotion/styled';
import { IconX } from '@sonarsource/echoes-react';
import {
ButtonPrimary,
ButtonSecondary,
InteractiveIcon,
themeBorder,
themeColor,
} from 'design-system';
import React, { useState } from 'react';
import { translate } from '../../../helpers/l10n';

interface Props {
content: string;
dismissed: boolean;
onDismiss: () => void;
onPrimaryButtonClick: () => void;
primaryButtonLabel: string;
secondaryButtonLabel: string;
title: string;
}

export default function PromotedSection({
content,
primaryButtonLabel,
secondaryButtonLabel,
title,
dismissed,
onDismiss,
onPrimaryButtonClick,
}: Readonly<Props>) {
const [display, setDisplay] = useState(!dismissed);

const handlePrimaryButtonClick = () => {
setDisplay(false);
onPrimaryButtonClick();
};

const handleDismiss = () => {
setDisplay(false);
onDismiss();
};

if (!display) {
return null;
}

return (
<StyledWrapper className="sw-p-4 sw-pl-6 sw-my-6 sw-rounded-2 sw-w-8/12">
<div className="sw-flex sw-justify-between sw-mb-2">
<StyledTitle className="sw-body-md-highlight">{title}</StyledTitle>
<InteractiveIcon
Icon={IconX}
aria-label={translate('dismiss')}
onClick={handleDismiss}
size="small"
/>
</div>
<p className="sw-body-sm sw-mb-4">{content}</p>
<div>
<ButtonPrimary className="sw-mr-2" onClick={handlePrimaryButtonClick}>
{primaryButtonLabel}
</ButtonPrimary>
<ButtonSecondary onClick={handleDismiss}>{secondaryButtonLabel}</ButtonSecondary>
</div>
</StyledWrapper>
);
}

const StyledWrapper = styled.div`
background-color: ${themeColor('backgroundPromotedSection')};
border: ${themeBorder('default')};
`;

const StyledTitle = styled.p`
color: ${themeColor('primary')};
`;

+ 1
- 1
server/sonar-web/src/main/js/apps/overview/branches/QualityGateStatusHeader.tsx View File

const intl = useIntl(); const intl = useIntl();


return ( return (
<div className="sw-flex sw-items-center sw-mb-4">
<div className="sw-flex sw-items-center sw-mb-4" data-spotlight-id="cayc-promotion-3">
<QualityGateIndicator status={status} className="sw-mr-2" size="xl" /> <QualityGateIndicator status={status} className="sw-mr-2" size="xl" />
<div className="sw-flex sw-flex-col"> <div className="sw-flex sw-flex-col">
<span className="sw-heading-lg">{translate('metric.level', status)}</span> <span className="sw-heading-lg">{translate('metric.level', status)}</span>

+ 73
- 0
server/sonar-web/src/main/js/apps/overview/branches/ReplayTour.tsx View File

/*
* SonarQube
* Copyright (C) 2009-2024 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 { SpotlightTour, SpotlightTourStep } from 'design-system';
import React from 'react';
import { useDocUrl } from '../../../helpers/docs';
import { translate } from '../../../helpers/l10n';

interface Props {
closeTour: () => void;
run: boolean;
tourCompleted: boolean;
}

export default function ReplayTourGuide({ run, closeTour, tourCompleted }: Readonly<Props>) {
const onToggle = ({ action }: { action: string }) => {
if (action === 'skip' || action === 'close') {
closeTour();
}
};

const constructContent = (first: string) => <p className="sw-mt-2">{translate(first)}</p>;

const docUrl = useDocUrl('improving/clean-as-you-code/');

const steps: SpotlightTourStep[] = [
{
disableOverlayClose: true,
target: '[data-spotlight-id="take-tour-1"]',
content: constructContent('guiding.replay_tour_button.1.content'),
title: tourCompleted
? translate('guiding.replay_tour_button.tour_completed.1.title')
: translate('guiding.replay_tour_button.1.title'),
placement: 'left',
},
];

return (
<div>
<SpotlightTour
actionLabel={tourCompleted ? translate('learn_more.clean_code') : undefined}
actionPath={tourCompleted ? docUrl : undefined}
backLabel={translate('go_back')}
callback={onToggle}
closeLabel={translate('got_it')}
continuous
disableOverlay
nextLabel={translate('next')}
run={run}
skipLabel={translate('skip')}
stepXofYLabel={() => ''}
steps={steps}
width={350}
/>
</div>
);
}

+ 5
- 1
server/sonar-web/src/main/js/apps/overview/branches/TabsPanel.tsx View File

]; ];


return ( return (
<div className="sw-mt-3" data-testid="overview__measures-panel">
<div
className="sw-mt-3"
data-testid="overview__measures-panel"
data-spotlight-id="cayc-promotion-1"
>
{loading ? ( {loading ? (
<div> <div>
<Spinner isLoading={loading} /> <Spinner isLoading={loading} />

+ 70
- 11
server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx View File

import { ProjectActivityServiceMock } from '../../../../api/mocks/ProjectActivityServiceMock'; import { ProjectActivityServiceMock } from '../../../../api/mocks/ProjectActivityServiceMock';
import { QualityGatesServiceMock } from '../../../../api/mocks/QualityGatesServiceMock'; import { QualityGatesServiceMock } from '../../../../api/mocks/QualityGatesServiceMock';
import { TimeMachineServiceMock } from '../../../../api/mocks/TimeMachineServiceMock'; import { TimeMachineServiceMock } from '../../../../api/mocks/TimeMachineServiceMock';
import UsersServiceMock from '../../../../api/mocks/UsersServiceMock';
import { PARENT_COMPONENT_KEY } from '../../../../api/mocks/data/ids'; import { PARENT_COMPONENT_KEY } from '../../../../api/mocks/data/ids';
import { getProjectActivity } from '../../../../api/projectActivity'; import { getProjectActivity } from '../../../../api/projectActivity';
import { getQualityGateProjectStatus } from '../../../../api/quality-gates'; import { getQualityGateProjectStatus } from '../../../../api/quality-gates';
import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider'; import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider';
import { Header } from '../../../../app/components/nav/component/Header';
import { parseDate } from '../../../../helpers/dates'; import { parseDate } from '../../../../helpers/dates';
import { mockMainBranch } from '../../../../helpers/mocks/branch-like'; import { mockMainBranch } from '../../../../helpers/mocks/branch-like';
import { mockComponent } from '../../../../helpers/mocks/component'; import { mockComponent } from '../../../../helpers/mocks/component';
let measuresHandler: MeasuresServiceMock; let measuresHandler: MeasuresServiceMock;
let applicationHandler: ApplicationServiceMock; let applicationHandler: ApplicationServiceMock;
let projectActivityHandler: ProjectActivityServiceMock; let projectActivityHandler: ProjectActivityServiceMock;
let usersHandler: UsersServiceMock;
let timeMarchineHandler: TimeMachineServiceMock; let timeMarchineHandler: TimeMachineServiceMock;
let qualityGatesHandler: QualityGatesServiceMock; let qualityGatesHandler: QualityGatesServiceMock;


measuresHandler = new MeasuresServiceMock(); measuresHandler = new MeasuresServiceMock();
applicationHandler = new ApplicationServiceMock(); applicationHandler = new ApplicationServiceMock();
projectActivityHandler = new ProjectActivityServiceMock(); projectActivityHandler = new ProjectActivityServiceMock();
usersHandler = new UsersServiceMock();
projectActivityHandler.setAnalysesList([ projectActivityHandler.setAnalysesList([
mockAnalysis({ key: 'a1', detectedCI: 'Cirrus CI' }), mockAnalysis({ key: 'a1', detectedCI: 'Cirrus CI' }),
mockAnalysis({ key: 'a2' }), mockAnalysis({ key: 'a2' }),
measuresHandler.reset(); measuresHandler.reset();
applicationHandler.reset(); applicationHandler.reset();
projectActivityHandler.reset(); projectActivityHandler.reset();
usersHandler.reset();
timeMarchineHandler.reset(); timeMarchineHandler.reset();
qualityGatesHandler.reset(); qualityGatesHandler.reset();
almHandler.reset(); almHandler.reset();
expect(await screen.findByText('overview.missing_project_dataTRK')).toBeInTheDocument(); expect(await screen.findByText('overview.missing_project_dataTRK')).toBeInTheDocument();
}, },
); );

it('should dismiss CaYC promoted section', async () => {
qualityGatesHandler.setQualityGateProjectStatus(
mockQualityGateProjectStatus({
status: 'OK',
}),
);
const { user } = getPageObjects();
renderBranchOverview();

// Meta info
expect(await byText('overview.promoted_section.title').find()).toBeInTheDocument();

await user.click(
byRole('button', { name: 'overview.promoted_section.button_secondary' }).get(),
);

expect(byText('overview.promoted_section.title').query()).not.toBeInTheDocument();

expect(byText('guiding.replay_tour_button.1.title').get()).toBeInTheDocument();
});

it('should show CaYC tour', async () => {
qualityGatesHandler.setQualityGateProjectStatus(
mockQualityGateProjectStatus({
status: 'OK',
}),
);
const { user } = getPageObjects();
renderBranchOverview();

expect(await byText('overview.promoted_section.title').find()).toBeInTheDocument();

await user.click(byRole('button', { name: 'overview.promoted_section.button_primary' }).get());

expect(byText('overview.promoted_section.title').query()).not.toBeInTheDocument();

expect(await byText('guiding.cayc_promotion.1.title').find()).toBeInTheDocument();

await user.click(byRole('button', { name: 'next' }).get());

expect(byText('guiding.cayc_promotion.2.title').get()).toBeInTheDocument();

await user.click(byRole('button', { name: 'next' }).get());

expect(byText('guiding.cayc_promotion.3.title').get()).toBeInTheDocument();

await user.click(await byRole('button', { name: 'next' }).find());

expect(byText('guiding.cayc_promotion.4.title').get()).toBeInTheDocument();

await user.click(byRole('button', { name: 'complete' }).get());

expect(byText('guiding.replay_tour_button.tour_completed.1.title').get()).toBeInTheDocument();
});
}); });


describe('application overview', () => { describe('application overview', () => {
); );


function renderBranchOverview(props: Partial<BranchOverview['props']> = {}) { function renderBranchOverview(props: Partial<BranchOverview['props']> = {}) {
const user = mockLoggedInUser();
const component = mockComponent({
breadcrumbs: [mockComponent({ key: 'foo' })],
key: 'foo',
name: 'Foo',
version: 'version-1.0',
});
return renderComponent( return renderComponent(
<CurrentUserContextProvider currentUser={mockLoggedInUser()}>
<BranchOverview
branch={mockMainBranch()}
component={mockComponent({
breadcrumbs: [mockComponent({ key: 'foo' })],
key: 'foo',
name: 'Foo',
version: 'version-1.0',
})}
{...props}
/>
<CurrentUserContextProvider currentUser={user}>
<Header component={component} currentUser={user} />
<BranchOverview branch={mockMainBranch()} component={component} {...props} />
</CurrentUserContextProvider>, </CurrentUserContextProvider>,
); );
} }

+ 1
- 1
server/sonar-web/src/main/js/apps/overview/components/LastAnalysisLabel.tsx View File

const intl = useIntl(); const intl = useIntl();


return analysisDate ? ( return analysisDate ? (
<span>
<span className="sw-pl-4" data-spotlight-id="cayc-promotion-2">
{intl.formatMessage( {intl.formatMessage(
{ {
id: 'overview.last_analysis_x', id: 'overview.last_analysis_x',

+ 1
- 1
server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx View File

versionLabel: (version?: string) => versionLabel: (version?: string) =>
version ? byText(/footer\.version\s*(\d.\d)/) : byText(/footer\.version/), version ? byText(/footer\.version\s*(\d.\d)/) : byText(/footer\.version/),
ltaDocumentationLinkActive: byRole('link', { ltaDocumentationLinkActive: byRole('link', {
name: `footer.version.status.active open_in_new_window`,
name: `footer.version.status.active open_in_new_tab`,
}), }),
}; };



+ 1
- 1
server/sonar-web/src/main/js/components/tutorials/test-utils.ts View File

linkToRepo: byRole('link', { linkToRepo: byRole('link', {
name: `onboarding.tutorial.with.${CI_TRANSLATE_MAP[ci]}.${ name: `onboarding.tutorial.with.${CI_TRANSLATE_MAP[ci]}.${
ci === TutorialModes.GitHubActions ? 'secret' : 'variables' ci === TutorialModes.GitHubActions ? 'secret' : 'variables'
}.intro.link open_in_new_window`,
}.intro.link open_in_new_tab`,
}), }),
allSetSentence: byText('onboarding.tutorial.ci_outro.done'), allSetSentence: byText('onboarding.tutorial.ci_outro.done'),
}; };

+ 1
- 0
server/sonar-web/src/main/js/types/users.ts View File

ISSUE_NEW_STATUS_AND_TRANSITION_GUIDE = 'issueNewIssueStatusAndTransitionGuide', ISSUE_NEW_STATUS_AND_TRANSITION_GUIDE = 'issueNewIssueStatusAndTransitionGuide',
QG_CAYC_CONDITIONS_SIMPLIFICATION = 'qualityGateCaYCConditionsSimplification', QG_CAYC_CONDITIONS_SIMPLIFICATION = 'qualityGateCaYCConditionsSimplification',
OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION = 'overviewZeroNewIssuesSimplification', OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION = 'overviewZeroNewIssuesSimplification',
ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE = 'onboardingDismissCaycBranchSummaryGuide',
} }


export interface LoggedInUser extends CurrentUser, UserActive { export interface LoggedInUser extends CurrentUser, UserActive {

+ 47
- 45
server/sonar-web/yarn.lock View File

languageName: node languageName: node
linkType: hard linkType: hard


"@gilbarbara/helpers@npm:^0.9.0":
version: 0.9.2
resolution: "@gilbarbara/helpers@npm:0.9.2"
dependencies:
"@gilbarbara/types": "npm:^0.2.2"
is-lite: "npm:^1.2.1"
checksum: 10/17a111aea44ce5368413042974d8d890f48244becb50bb51c9ba75f7c0dd6d3e918d2d2fbba445a9188993c2ab11a2ba72ed0a477219621a37a80b2998751bb9
languageName: node
linkType: hard

"@gilbarbara/types@npm:^0.2.2":
version: 0.2.2
resolution: "@gilbarbara/types@npm:0.2.2"
dependencies:
type-fest: "npm:^4.1.0"
checksum: 10/fb71d2e577a48b68b2205146c4cc6180d4e1d175df8b37de47e7581feaeb68ff32918dade9ddb94627755f0d8270727ef7209a9b238bc3af79dc83493b14da5a
languageName: node
linkType: hard

"@highlightjs/cdn-assets@npm:^11.9.0": "@highlightjs/cdn-assets@npm:^11.9.0":
version: 11.9.0 version: 11.9.0
resolution: "@highlightjs/cdn-assets@npm:11.9.0" resolution: "@highlightjs/cdn-assets@npm:11.9.0"
languageName: node languageName: node
linkType: hard linkType: hard


"@material-symbols/font-400@npm:0.17.2":
version: 0.17.2
resolution: "@material-symbols/font-400@npm:0.17.2"
checksum: 10/f3dae732c3d16a1dcc4310f045608cd422e32e1b158189a546c2aeca3af449d3cb6d952a096caf267a1646f4b4ab98a19348735e5b39b60841e04c5a8b96339b
languageName: node
linkType: hard

"@microsoft/api-extractor-model@npm:7.28.3": "@microsoft/api-extractor-model@npm:7.28.3":
version: 7.28.3 version: 7.28.3
resolution: "@microsoft/api-extractor-model@npm:7.28.3" resolution: "@microsoft/api-extractor-model@npm:7.28.3"
languageName: node languageName: node
linkType: hard linkType: hard


"@radix-ui/react-visually-hidden@npm:1.0.3":
version: 1.0.3
resolution: "@radix-ui/react-visually-hidden@npm:1.0.3"
dependencies:
"@babel/runtime": "npm:^7.13.10"
"@radix-ui/react-primitive": "npm:1.0.3"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 10/2e9d0c8253f97e7d6ffb2e52a5cfd40ba719f813b39c3e2e42c496d54408abd09ef66b5aec4af9b8ab0553215e32452a5d0934597a49c51dd90dc39181ed0d57
languageName: node
linkType: hard

"@react-spring/animated@npm:~9.7.3": "@react-spring/animated@npm:~9.7.3":
version: 9.7.3 version: 9.7.3
resolution: "@react-spring/animated@npm:9.7.3" resolution: "@react-spring/animated@npm:9.7.3"
languageName: node languageName: node
linkType: hard linkType: hard


"@sonarsource/echoes-react@npm:0.2.1":
version: 0.2.1
resolution: "@sonarsource/echoes-react@npm:0.2.1"
"@sonarsource/echoes-react@npm:0.2.2":
version: 0.2.2
resolution: "@sonarsource/echoes-react@npm:0.2.2"
dependencies: dependencies:
"@material-symbols/font-400": "npm:0.17.2"
"@radix-ui/react-checkbox": "npm:1.0.4" "@radix-ui/react-checkbox": "npm:1.0.4"
"@radix-ui/react-radio-group": "npm:1.1.3" "@radix-ui/react-radio-group": "npm:1.1.3"
material-symbols: "npm:0.17.0"
"@radix-ui/react-visually-hidden": "npm:1.0.3"
peerDependencies: peerDependencies:
"@emotion/react": ^11.0.0 "@emotion/react": ^11.0.0
"@emotion/styled": ^11.0.0 "@emotion/styled": ^11.0.0
react-dom: ^17.0.0 || ^18.0.0 react-dom: ^17.0.0 || ^18.0.0
react-intl: ^6.0.0 react-intl: ^6.0.0
react-router-dom: ^6.0.0 react-router-dom: ^6.0.0
checksum: 10/dccb03e27d0ff13d32923255b984807fce119a212728eb69cd16019a927e1014c07c7facef91e9306f270d0929b2b36ee03a452b99ca53d51ce5434c24fc6951
checksum: 10/ec5fed2ac473028ca489a8445190c5585ff7e41d9cad0221b315260647307c1c91d59efe0f7b708d099ca24d7a9005dcde49d91f1408d26d7c33ad6a8a80578a
languageName: node languageName: node
linkType: hard linkType: hard


"@primer/octicons-react": "npm:19.8.0" "@primer/octicons-react": "npm:19.8.0"
"@react-spring/rafz": "npm:9.7.3" "@react-spring/rafz": "npm:9.7.3"
"@react-spring/web": "npm:9.7.3" "@react-spring/web": "npm:9.7.3"
"@sonarsource/echoes-react": "npm:0.2.1"
"@sonarsource/echoes-react": "npm:0.2.2"
"@swc/core": "npm:1.4.0" "@swc/core": "npm:1.4.0"
"@swc/jest": "npm:0.2.36" "@swc/jest": "npm:0.2.36"
"@tanstack/react-query": "npm:5.18.1" "@tanstack/react-query": "npm:5.18.1"
react-helmet-async: "npm:2.0.4" react-helmet-async: "npm:2.0.4"
react-highlight-words: "npm:0.20.0" react-highlight-words: "npm:0.20.0"
react-intl: "npm:6.6.2" react-intl: "npm:6.6.2"
react-joyride: "npm:2.7.2"
react-joyride: "npm:2.8.1"
react-modal: "npm:3.16.1" react-modal: "npm:3.16.1"
react-router-dom: "npm:6.22.0" react-router-dom: "npm:6.22.0"
react-select: "npm:5.7.7" react-select: "npm:5.7.7"
"@babel/preset-typescript": "npm:7.23.3" "@babel/preset-typescript": "npm:7.23.3"
"@emotion/babel-plugin": "npm:11.11.0" "@emotion/babel-plugin": "npm:11.11.0"
"@emotion/babel-plugin-jsx-pragmatic": "npm:0.2.1" "@emotion/babel-plugin-jsx-pragmatic": "npm:0.2.1"
"@sonarsource/echoes-react": "npm:0.2.2"
"@testing-library/dom": "npm:9.3.4" "@testing-library/dom": "npm:9.3.4"
"@testing-library/jest-dom": "npm:6.4.2" "@testing-library/jest-dom": "npm:6.4.2"
"@testing-library/react": "npm:14.2.1" "@testing-library/react": "npm:14.2.1"
react-helmet-async: 2.0.4 react-helmet-async: 2.0.4
react-highlight-words: 0.20.0 react-highlight-words: 0.20.0
react-intl: 6.6.2 react-intl: 6.6.2
react-joyride: 2.7.2
react-joyride: 2.8.1
react-modal: 3.16.1 react-modal: 3.16.1
react-router-dom: 6.22.0 react-router-dom: 6.22.0
react-select: 5.7.7 react-select: 5.7.7
languageName: node languageName: node
linkType: hard linkType: hard


"material-symbols@npm:0.17.0":
version: 0.17.0
resolution: "material-symbols@npm:0.17.0"
checksum: 10/d432e18203d38b83a645783b03ac4b321d30cd6a8bec1aae5e47fccf4340387d4f7bc703d3aebb5a25639c316bb04f2027518e592869361d8bfbbd46ca8c9835
languageName: node
linkType: hard

"memoize-one@npm:^4.0.0": "memoize-one@npm:^4.0.0":
version: 4.0.3 version: 4.0.3
resolution: "memoize-one@npm:4.0.3" resolution: "memoize-one@npm:4.0.3"
languageName: node languageName: node
linkType: hard linkType: hard


"react-joyride@npm:2.7.2":
version: 2.7.2
resolution: "react-joyride@npm:2.7.2"
"react-joyride@npm:2.8.1":
version: 2.8.1
resolution: "react-joyride@npm:2.8.1"
dependencies: dependencies:
"@gilbarbara/deep-equal": "npm:^0.3.1" "@gilbarbara/deep-equal": "npm:^0.3.1"
"@gilbarbara/helpers": "npm:^0.9.0"
deep-diff: "npm:^1.0.2" deep-diff: "npm:^1.0.2"
deepmerge: "npm:^4.3.1" deepmerge: "npm:^4.3.1"
is-lite: "npm:^1.2.0"
is-lite: "npm:^1.2.1"
react-floater: "npm:^0.7.9" react-floater: "npm:^0.7.9"
react-innertext: "npm:^1.1.5" react-innertext: "npm:^1.1.5"
react-is: "npm:^16.13.1" react-is: "npm:^16.13.1"
scroll: "npm:^3.0.1" scroll: "npm:^3.0.1"
scrollparent: "npm:^2.1.0" scrollparent: "npm:^2.1.0"
tree-changes: "npm:^0.11.2" tree-changes: "npm:^0.11.2"
type-fest: "npm:^4.8.3"
type-fest: "npm:^4.15.0"
peerDependencies: peerDependencies:
react: 15 - 18 react: 15 - 18
react-dom: 15 - 18 react-dom: 15 - 18
checksum: 10/1eb93d7edcfd662bc9a942b62430b5e92bf396002586d2c807dab5b5d599805209ee854ea1adb48d9a7855aada8dba6872be76ff6f2448820a42ca4c0be1661a
checksum: 10/55ff023104f708c3d4c17e2dcc27e3b54a268872b0baf60bd57ee41971e6e8a9e1d4e4c74d4dec084ec4b69eb21edcc985ef4979611a775327e856e03d1335d3
languageName: node languageName: node
linkType: hard linkType: hard


languageName: node languageName: node
linkType: hard linkType: hard


"type-fest@npm:^4.1.0, type-fest@npm:^4.8.3":
version: 4.10.2
resolution: "type-fest@npm:4.10.2"
checksum: 10/2b1ad1270d9fabeeb506ba831d513caeb05bfc852e5e012511d785ce9dc68d773fe0a42bddf857a362c7f3406244809c5b8a698b743bb7617d4a8c470672087f
"type-fest@npm:^4.15.0":
version: 4.18.2
resolution: "type-fest@npm:4.18.2"
checksum: 10/2c176de28384a247fac1503165774e874c15ac39434a775f32ecda3aef5a0cefcfa2f5fb670c3da1f81cf773c355999154078c8d9657db19b65de78334b27933
languageName: node languageName: node
linkType: hard linkType: hard



+ 1
- 1
server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/DismissNoticeActionIT.java View File

.isInstanceOf(IllegalArgumentException.class) .isInstanceOf(IllegalArgumentException.class)
.hasMessage( .hasMessage(
"Value of parameter 'notice' (not_supported_value) must be one of: [educationPrinciples, sonarlintAd, issueCleanCodeGuide, qualityGateCaYCConditionsSimplification, " + "Value of parameter 'notice' (not_supported_value) must be one of: [educationPrinciples, sonarlintAd, issueCleanCodeGuide, qualityGateCaYCConditionsSimplification, " +
"overviewZeroNewIssuesSimplification, issueNewIssueStatusAndTransitionGuide]");
"overviewZeroNewIssuesSimplification, issueNewIssueStatusAndTransitionGuide, onboardingDismissCaycBranchSummaryGuide]");
} }


@Test @Test

+ 3
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DismissNoticeAction.java View File

private static final String QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION = "qualityGateCaYCConditionsSimplification"; private static final String QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION = "qualityGateCaYCConditionsSimplification";
private static final String OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION = "overviewZeroNewIssuesSimplification"; private static final String OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION = "overviewZeroNewIssuesSimplification";
private static final String ISSUE_NEW_ISSUE_STATUS_AND_TRANSITION_GUIDE = "issueNewIssueStatusAndTransitionGuide"; private static final String ISSUE_NEW_ISSUE_STATUS_AND_TRANSITION_GUIDE = "issueNewIssueStatusAndTransitionGuide";
private static final String ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE = "onboardingDismissCaycBranchSummaryGuide";


protected static final List<String> AVAILABLE_NOTICE_KEYS = List.of(EDUCATION_PRINCIPLES, SONARLINT_AD, ISSUE_CLEAN_CODE_GUIDE, QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION, protected static final List<String> AVAILABLE_NOTICE_KEYS = List.of(EDUCATION_PRINCIPLES, SONARLINT_AD, ISSUE_CLEAN_CODE_GUIDE, QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION,
OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION, ISSUE_NEW_ISSUE_STATUS_AND_TRANSITION_GUIDE);
OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION, ISSUE_NEW_ISSUE_STATUS_AND_TRANSITION_GUIDE, ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE);
public static final String USER_DISMISS_CONSTANT = "user.dismissedNotices."; public static final String USER_DISMISS_CONSTANT = "user.dismissedNotices.";
public static final String SUPPORT_FOR_NEW_NOTICE_MESSAGE = "Support for new notice '%s' was added."; public static final String SUPPORT_FOR_NEW_NOTICE_MESSAGE = "Support for new notice '%s' was added.";


public void define(WebService.NewController context) { public void define(WebService.NewController context) {
WebService.NewAction action = context.createAction("dismiss_notice") WebService.NewAction action = context.createAction("dismiss_notice")
.setDescription("Dismiss a notice for the current user. Silently ignore if the notice is already dismissed.") .setDescription("Dismiss a notice for the current user. Silently ignore if the notice is already dismissed.")
.setChangelog(new Change("10.6", SUPPORT_FOR_NEW_NOTICE_MESSAGE.formatted(ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE)))
.setChangelog(new Change("10.4", SUPPORT_FOR_NEW_NOTICE_MESSAGE.formatted(ISSUE_NEW_ISSUE_STATUS_AND_TRANSITION_GUIDE))) .setChangelog(new Change("10.4", SUPPORT_FOR_NEW_NOTICE_MESSAGE.formatted(ISSUE_NEW_ISSUE_STATUS_AND_TRANSITION_GUIDE)))
.setChangelog(new Change("10.3", SUPPORT_FOR_NEW_NOTICE_MESSAGE.formatted(QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION))) .setChangelog(new Change("10.3", SUPPORT_FOR_NEW_NOTICE_MESSAGE.formatted(QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION)))
.setChangelog(new Change("10.2", SUPPORT_FOR_NEW_NOTICE_MESSAGE.formatted(ISSUE_CLEAN_CODE_GUIDE))) .setChangelog(new Change("10.2", SUPPORT_FOR_NEW_NOTICE_MESSAGE.formatted(ISSUE_CLEAN_CODE_GUIDE)))

+ 26
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

blocker=Blocker blocker=Blocker
bold=Bold bold=Bold
branch=Branch branch=Branch
branch.small=branch
breadcrumbs=Breadcrumbs breadcrumbs=Breadcrumbs
expand_breadcrumbs=Expand breadcrumbs expand_breadcrumbs=Expand breadcrumbs
by_=by by_=by
color=Color color=Color
collapse_all=Collapse all collapse_all=Collapse all
compare=Compare compare=Compare
complete=Complete
component=Component component=Component
configure=Configure configure=Configure
confirm=Confirm confirm=Confirm
global=Global global=Global
github=GitHub github=GitHub
go_back=Go back go_back=Go back
got_it=Got it
help=Help help=Help
here=here here=here
hide=Hide hide=Hide
last_analysis=Last Analysis last_analysis=Last Analysis
learn_more=Learn More learn_more=Learn More
learn_more_x=Learn More: {link} learn_more_x=Learn More: {link}
learn_more.clean_code=Learn more: Clean as You Code
library=Library library=Library
line_number=Line Number line_number=Line Number
links=Links links=Links
path=Path path=Path
permalink=Permanent Link permalink=Permanent Link
plugin=Plugin plugin=Plugin
previous=Previous
previous_=previous previous_=previous
previous_month_x=previous month {month} previous_month_x=previous month {month}
project=Project project=Project
x_projects_={0} project(s) x_projects_={0} project(s)
project_plural=projects project_plural=projects
projects_management=Projects Management projects_management=Projects Management
pull_request.small=pull request
quality_profile=Quality Profile quality_profile=Quality Profile
raw=Raw raw=Raw
recent_history=Recent History recent_history=Recent History
overview.activity.variations.new_analysis=New analysis: overview.activity.variations.new_analysis=New analysis:
overview.activity.variations.first_analysis=First analysis: overview.activity.variations.first_analysis=First analysis:


overview.promoted_section.title=Struggling with too many issues? Discover ‘Clean as You Code’!
overview.promoted_section.content=Learn how to improve your code base by cleaning only new code.
overview.promoted_section.button_primary=Take the Tour
overview.promoted_section.button_secondary=Not now
overview.promoted_section.button_tooltip=Learn how to improve your code base by cleaning only new code.

guiding.cayc_promotion.1.title=The power of new code
guiding.cayc_promotion.1.content.1=Cleaning only new code is easy and guarantees no debt will be added. As you change old code, it also gets cleaner over time. We call this ‘Clean as You Code’.
guiding.cayc_promotion.2.title=Define your new code
guiding.cayc_promotion.2.content.1=Your team decides when a new code period for your project should start, for example, each time a project is released.
guiding.cayc_promotion.3.title=Green is clean
guiding.cayc_promotion.3.content.1=Quality Gate Status tells you if your new code is clean or not. Keep it green as often as possible, and your project will always be production-ready.
guiding.cayc_promotion.4.title=Clean at all levels
guiding.cayc_promotion.4.content.1=With SonarLint, clean code as you write it in your {value}.
guiding.cayc_promotion.4.content.2=When a feature is ready, analyze your {value} and make sure no issue is missed.
guiding.cayc_promotion.4.content.3=Finally, rely on a thorough {value} analysis to ensure the new code is clean.
guiding.replay_tour_button.1.title=Replay tour
guiding.replay_tour_button.tour_completed.1.title=Tour complete!
guiding.replay_tour_button.1.content=You can replay this product tour any time here.



#------------------------------------------------------------------------------ #------------------------------------------------------------------------------
# #

Loading…
Cancel
Save