--- /dev/null
+/*
+ * 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 = {};
+
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',
"@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",
"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",
*/
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, {
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';
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;
const defultRect = new DOMRect(0, 0, 0, 0);
function TooltipComponent({
+ actionLabel,
+ actionPath,
continuous,
index,
step,
tooltipProps,
width = DEFAULT_WIDTH,
}: TooltipRenderProps & {
+ actionLabel?: string;
+ actionPath?: LinkProps['to'];
step: SpotlightTourStep;
stepXofYLabel: SpotlightTourProps['stepXofYLabel'];
width?: number;
</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>
export function SpotlightTour(props: SpotlightTourProps) {
const {
+ actionLabel,
+ actionPath,
steps,
skipLabel,
backLabel,
}))}
tooltipComponent={(
tooltipProps: React.PropsWithChildren<TooltipRenderProps & { step: SpotlightTourStep }>,
- ) => <TooltipComponent stepXofYLabel={stepXofYLabel} width={width} {...tooltipProps} />}
+ ) => (
+ <TooltipComponent
+ actionLabel={actionLabel}
+ actionPath={actionPath}
+ stepXofYLabel={stepXofYLabel}
+ width={width}
+ {...tooltipProps}
+ />
+ )}
{...otherProps}
/>
);
projectCardInfo: COLORS.blueGrey[35],
// overview
+ backgroundPromotedSection: secondary.light,
overviewCardDefaultIcon: secondary.light,
iconOverviewIssue: COLORS.blueGrey[400],
overviewCardWarningIcon: COLORS.yellow[50],
"@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",
"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",
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`,
}),
};
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={
* 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';
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';
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;
)}
<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>
);
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';
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;
/>
);
+ 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
/>
<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" />
<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 />
</>
)}
--- /dev/null
+/*
+ * 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;
--- /dev/null
+/*
+ * 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')};
+`;
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>
--- /dev/null
+/*
+ * 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>
+ );
+}
];
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} />
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';
let measuresHandler: MeasuresServiceMock;
let applicationHandler: ApplicationServiceMock;
let projectActivityHandler: ProjectActivityServiceMock;
+let usersHandler: UsersServiceMock;
let timeMarchineHandler: TimeMachineServiceMock;
let qualityGatesHandler: QualityGatesServiceMock;
measuresHandler = new MeasuresServiceMock();
applicationHandler = new ApplicationServiceMock();
projectActivityHandler = new ProjectActivityServiceMock();
+ usersHandler = new UsersServiceMock();
projectActivityHandler.setAnalysesList([
mockAnalysis({ key: 'a1', detectedCI: 'Cirrus CI' }),
mockAnalysis({ key: 'a2' }),
measuresHandler.reset();
applicationHandler.reset();
projectActivityHandler.reset();
+ usersHandler.reset();
timeMarchineHandler.reset();
qualityGatesHandler.reset();
almHandler.reset();
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', () => {
);
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>,
);
}
const intl = useIntl();
return analysisDate ? (
- <span>
+ <span className="sw-pl-4" data-spotlight-id="cayc-promotion-2">
{intl.formatMessage(
{
id: 'overview.last_analysis_x',
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`,
}),
};
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'),
};
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 {
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"
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"
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"
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
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
"@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"
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"
"@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"
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
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"
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
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
.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
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.";
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)))
blocker=Blocker
bold=Bold
branch=Branch
+branch.small=branch
breadcrumbs=Breadcrumbs
expand_breadcrumbs=Expand breadcrumbs
by_=by
color=Color
collapse_all=Collapse all
compare=Compare
+complete=Complete
component=Component
configure=Configure
confirm=Confirm
global=Global
github=GitHub
go_back=Go back
+got_it=Got it
help=Help
here=here
hide=Hide
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
path=Path
permalink=Permanent Link
plugin=Plugin
+previous=Previous
previous_=previous
previous_month_x=previous month {month}
project=Project
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
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.
+
#------------------------------------------------------------------------------
#