Browse Source

SONAR-22148 Spotlight tour for cayc in branch overview

master
Mathieu Suen 2 weeks 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

@@ -0,0 +1,21 @@
/*
* 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

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

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

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

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

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

export interface SpotlightTourProps extends Omit<JoyrideProps, 'steps'> {
actionLabel?: string;
actionPath?: LinkProps['to'];
backLabel?: string;
closeLabel?: string;
nextLabel?: string;
@@ -60,6 +64,8 @@ const DEFAULT_WIDTH = 315;
const defultRect = new DOMRect(0, 0, 0, 0);

function TooltipComponent({
actionLabel,
actionPath,
continuous,
index,
step,
@@ -73,6 +79,8 @@ function TooltipComponent({
tooltipProps,
width = DEFAULT_WIDTH,
}: TooltipRenderProps & {
actionLabel?: string;
actionPath?: LinkProps['to'];
step: SpotlightTourStep;
stepXofYLabel: SpotlightTourProps['stepXofYLabel'];
width?: number;
@@ -154,6 +162,13 @@ function TooltipComponent({
</WrapperButton>
</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">
{(stepXofYLabel || size > 1) && (
<strong>
@@ -183,6 +198,8 @@ function TooltipComponent({

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

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

@@ -525,6 +525,7 @@ export const lightTheme = {
projectCardInfo: COLORS.blueGrey[35],

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

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

@@ -13,7 +13,7 @@
"@primer/octicons-react": "19.8.0",
"@react-spring/rafz": "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",
"axios": "1.6.7",
"classnames": "2.5.1",
@@ -38,7 +38,7 @@
"react-helmet-async": "2.0.4",
"react-highlight-words": "0.20.0",
"react-intl": "6.6.2",
"react-joyride": "2.7.2",
"react-joyride": "2.8.1",
"react-modal": "3.16.1",
"react-router-dom": "6.22.0",
"react-select": "5.7.7",

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

@@ -132,9 +132,9 @@ const ui = {
pluginsLink: byRole('link', { name: 'opens_in_new_window footer.plugins' }),
apiLink: byRole('link', { name: 'footer.web_api' }),
ltaDocumentationLinkActive: byRole('link', {
name: `footer.version.status.active open_in_new_window`,
name: `footer.version.status.active open_in_new_tab`,
}),
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

@@ -69,7 +69,10 @@ export function BranchLikeNavigation(props: BranchLikeNavigationProps) {
return (
<>
<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
allowResizing
overlay={

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

@@ -17,7 +17,8 @@
* along with this program; if not, write to the Free Software Foundation,
* 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 { useIntl } from 'react-intl';
import { formatMeasure } from '~sonar-aligned/helpers/measures';
@@ -25,6 +26,8 @@ import { MetricKey, MetricType } from '~sonar-aligned/types/metrics';
import { getCurrentPage } from '../../../app/components/nav/component/utils';
import ComponentReportActions from '../../../components/controls/ComponentReportActions';
import HomePageSelect from '../../../components/controls/HomePageSelect';
import Tooltip from '../../../components/controls/Tooltip';
import { translate } from '../../../helpers/l10n';
import { findMeasure } from '../../../helpers/measures';
import { Branch } from '../../../types/branch-like';
import { Component, MeasureEnhanced } from '../../../types/types';
@@ -34,9 +37,17 @@ interface Props {
component: Component;
branch: Branch;
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 currentPage = getCurrentPage(component, branch) as HomePage;
@@ -66,6 +77,18 @@ export default function BranchMetaTopBar({ branch, measures, component }: Readon
)}
<HomePageSelect currentPage={currentPage} type="button" />
<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>
);


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

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

@@ -96,6 +104,18 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
const { query } = useLocation();
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 leakPeriod = component.qualifier === ComponentQualifier.Application ? appLeak : period;
const isNewCodeTab = tab === CodeScope.New;
@@ -126,6 +146,33 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
/>
);

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 (
<>
<FirstAnalysisNextStepsNotif
@@ -135,6 +182,14 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
/>
<LargeCenteredLayout>
<PageContentFontWrapper>
<CaycPromotionGuide closeTour={closeTour} run={startTour} />
{showReplay && (
<ReplayTourGuide
closeTour={() => setShowReplay(false)}
run={showReplay}
tourCompleted={tourCompleted}
/>
)}
<div className="overview sw-my-6 sw-body-sm">
<A11ySkipTarget anchor="overview_main" />

@@ -144,7 +199,27 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
<div>
{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 />
</>
)}

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

@@ -0,0 +1,134 @@
/*
* 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

@@ -0,0 +1,96 @@
/*
* 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

@@ -33,7 +33,7 @@ export default function QualityGateStatusHeader(props: Props) {
const intl = useIntl();

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" />
<div className="sw-flex sw-flex-col">
<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

@@ -0,0 +1,73 @@
/*
* 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

@@ -119,7 +119,11 @@ export function TabsPanel(props: React.PropsWithChildren<MeasuresPanelProps>) {
];

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 ? (
<div>
<Spinner isLoading={loading} />

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

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

@@ -59,6 +62,7 @@ beforeAll(() => {
measuresHandler = new MeasuresServiceMock();
applicationHandler = new ApplicationServiceMock();
projectActivityHandler = new ProjectActivityServiceMock();
usersHandler = new UsersServiceMock();
projectActivityHandler.setAnalysesList([
mockAnalysis({ key: 'a1', detectedCI: 'Cirrus CI' }),
mockAnalysis({ key: 'a2' }),
@@ -116,6 +120,7 @@ afterEach(() => {
measuresHandler.reset();
applicationHandler.reset();
projectActivityHandler.reset();
usersHandler.reset();
timeMarchineHandler.reset();
qualityGatesHandler.reset();
almHandler.reset();
@@ -453,6 +458,61 @@ describe('project overview', () => {
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', () => {
@@ -716,18 +776,17 @@ it.each([
);

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(
<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>,
);
}

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

@@ -30,7 +30,7 @@ export default function LastAnalysisLabel({ analysisDate }: Readonly<Props>) {
const intl = useIntl();

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

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

@@ -151,7 +151,7 @@ function getPageObjects() {
versionLabel: (version?: string) =>
version ? byText(/footer\.version\s*(\d.\d)/) : byText(/footer\.version/),
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

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

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

@@ -36,6 +36,7 @@ export enum NoticeType {
ISSUE_NEW_STATUS_AND_TRANSITION_GUIDE = 'issueNewIssueStatusAndTransitionGuide',
QG_CAYC_CONDITIONS_SIMPLIFICATION = 'qualityGateCaYCConditionsSimplification',
OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION = 'overviewZeroNewIssuesSimplification',
ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE = 'onboardingDismissCaycBranchSummaryGuide',
}

export interface LoggedInUser extends CurrentUser, UserActive {

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

@@ -3377,25 +3377,6 @@ __metadata:
languageName: node
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":
version: 11.9.0
resolution: "@highlightjs/cdn-assets@npm:11.9.0"
@@ -3820,6 +3801,13 @@ __metadata:
languageName: node
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":
version: 7.28.3
resolution: "@microsoft/api-extractor-model@npm:7.28.3"
@@ -4364,6 +4352,26 @@ __metadata:
languageName: node
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":
version: 9.7.3
resolution: "@react-spring/animated@npm:9.7.3"
@@ -4533,13 +4541,14 @@ __metadata:
languageName: node
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:
"@material-symbols/font-400": "npm:0.17.2"
"@radix-ui/react-checkbox": "npm:1.0.4"
"@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:
"@emotion/react": ^11.0.0
"@emotion/styled": ^11.0.0
@@ -4547,7 +4556,7 @@ __metadata:
react-dom: ^17.0.0 || ^18.0.0
react-intl: ^6.0.0
react-router-dom: ^6.0.0
checksum: 10/dccb03e27d0ff13d32923255b984807fce119a212728eb69cd16019a927e1014c07c7facef91e9306f270d0929b2b36ee03a452b99ca53d51ce5434c24fc6951
checksum: 10/ec5fed2ac473028ca489a8445190c5585ff7e41d9cad0221b315260647307c1c91d59efe0f7b708d099ca24d7a9005dcde49d91f1408d26d7c33ad6a8a80578a
languageName: node
linkType: hard

@@ -5749,7 +5758,7 @@ __metadata:
"@primer/octicons-react": "npm:19.8.0"
"@react-spring/rafz": "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/jest": "npm:0.2.36"
"@tanstack/react-query": "npm:5.18.1"
@@ -5836,7 +5845,7 @@ __metadata:
react-helmet-async: "npm:2.0.4"
react-highlight-words: "npm:0.20.0"
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-router-dom: "npm:6.22.0"
react-select: "npm:5.7.7"
@@ -7676,6 +7685,7 @@ __metadata:
"@babel/preset-typescript": "npm:7.23.3"
"@emotion/babel-plugin": "npm:11.11.0"
"@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/jest-dom": "npm:6.4.2"
"@testing-library/react": "npm:14.2.1"
@@ -7733,7 +7743,7 @@ __metadata:
react-helmet-async: 2.0.4
react-highlight-words: 0.20.0
react-intl: 6.6.2
react-joyride: 2.7.2
react-joyride: 2.8.1
react-modal: 3.16.1
react-router-dom: 6.22.0
react-select: 5.7.7
@@ -11701,13 +11711,6 @@ __metadata:
languageName: node
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":
version: 4.0.3
resolution: "memoize-one@npm:4.0.3"
@@ -13112,26 +13115,25 @@ __metadata:
languageName: node
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:
"@gilbarbara/deep-equal": "npm:^0.3.1"
"@gilbarbara/helpers": "npm:^0.9.0"
deep-diff: "npm:^1.0.2"
deepmerge: "npm:^4.3.1"
is-lite: "npm:^1.2.0"
is-lite: "npm:^1.2.1"
react-floater: "npm:^0.7.9"
react-innertext: "npm:^1.1.5"
react-is: "npm:^16.13.1"
scroll: "npm:^3.0.1"
scrollparent: "npm:^2.1.0"
tree-changes: "npm:^0.11.2"
type-fest: "npm:^4.8.3"
type-fest: "npm:^4.15.0"
peerDependencies:
react: 15 - 18
react-dom: 15 - 18
checksum: 10/1eb93d7edcfd662bc9a942b62430b5e92bf396002586d2c807dab5b5d599805209ee854ea1adb48d9a7855aada8dba6872be76ff6f2448820a42ca4c0be1661a
checksum: 10/55ff023104f708c3d4c17e2dcc27e3b54a268872b0baf60bd57ee41971e6e8a9e1d4e4c74d4dec084ec4b69eb21edcc985ef4979611a775327e856e03d1335d3
languageName: node
linkType: hard

@@ -14800,10 +14802,10 @@ __metadata:
languageName: node
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
linkType: hard


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

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

@Test

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

@@ -40,9 +40,10 @@ public class DismissNoticeAction implements UsersWsAction {
private static final String QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION = "qualityGateCaYCConditionsSimplification";
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 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,
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 SUPPORT_FOR_NEW_NOTICE_MESSAGE = "Support for new notice '%s' was added.";

@@ -58,6 +59,7 @@ public class DismissNoticeAction implements UsersWsAction {
public void define(WebService.NewController context) {
WebService.NewAction action = context.createAction("dismiss_notice")
.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.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)))

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

@@ -30,6 +30,7 @@ beta=BETA
blocker=Blocker
bold=Bold
branch=Branch
branch.small=branch
breadcrumbs=Breadcrumbs
expand_breadcrumbs=Expand breadcrumbs
by_=by
@@ -52,6 +53,7 @@ code=Code
color=Color
collapse_all=Collapse all
compare=Compare
complete=Complete
component=Component
configure=Configure
confirm=Confirm
@@ -104,6 +106,7 @@ from=From
global=Global
github=GitHub
go_back=Go back
got_it=Got it
help=Help
here=here
hide=Hide
@@ -118,6 +121,7 @@ language=Language
last_analysis=Last Analysis
learn_more=Learn More
learn_more_x=Learn More: {link}
learn_more.clean_code=Learn more: Clean as You Code
library=Library
line_number=Line Number
links=Links
@@ -170,6 +174,7 @@ password=Password
path=Path
permalink=Permanent Link
plugin=Plugin
previous=Previous
previous_=previous
previous_month_x=previous month {month}
project=Project
@@ -179,6 +184,7 @@ projects_=project(s)
x_projects_={0} project(s)
project_plural=projects
projects_management=Projects Management
pull_request.small=pull request
quality_profile=Quality Profile
raw=Raw
recent_history=Recent History
@@ -4071,6 +4077,26 @@ overview.quality_profiles_update_after_sq_upgrade.link=See more details
overview.activity.variations.new_analysis=New 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