import IndexationNotification from './indexation/IndexationNotification';
import LanguagesContextProvider from './languages/LanguagesContextProvider';
import MetricsContextProvider from './metrics/MetricsContextProvider';
+import ModeTour from './ModeTour';
import GlobalNav from './nav/global/GlobalNav';
import PromotionNotification from './promotion-notification/PromotionNotification';
import StartupModal from './StartupModal';
<NCDAutoUpdateMessage />
<UpdateNotification dismissable />
<GlobalNav location={location} />
+ <ModeTour />
<CalculationChangeMessage />
{/* The following is the portal anchor point for the component nav
* See ComponentContainer.tsx
--- /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 { Button, ButtonVariety, Modal, ModalSize } from '@sonarsource/echoes-react';
+import { useContext, useEffect, useState } from 'react';
+import { useIntl } from 'react-intl';
+import { CallBackProps } from 'react-joyride';
+import { SpotlightTour, SpotlightTourStep } from '~design-system';
+import { Image } from '~sonar-aligned/components/common/Image';
+import { dismissNotice } from '../../api/users';
+import DocumentationLink from '../../components/common/DocumentationLink';
+import { CustomEvents } from '../../helpers/constants';
+import { DocLink } from '../../helpers/doc-links';
+import { useStandardExperienceModeQuery } from '../../queries/mode';
+import { Permissions } from '../../types/permissions';
+import { NoticeType } from '../../types/users';
+import { useAppState } from './app-state/withAppStateContext';
+import { CurrentUserContext } from './current-user/CurrentUserContext';
+
+export default function ModeTour() {
+ const { currentUser, updateDismissedNotices } = useContext(CurrentUserContext);
+ const appState = useAppState();
+ const intl = useIntl();
+ const { data: isStandardMode } = useStandardExperienceModeQuery();
+ const [step, setStep] = useState(1);
+ const [runManually, setRunManually] = useState(false);
+
+ const steps: SpotlightTourStep[] = [
+ {
+ target: '[data-guiding-id="mode-tour-1"]',
+ content: intl.formatMessage(
+ { id: 'mode_tour.step4.description' },
+ {
+ mode: intl.formatMessage({
+ id: `settings.mode.${isStandardMode ? 'standard' : 'mqr'}.name`,
+ }),
+ p1: (text) => <p>{text}</p>,
+ p: (text) => <p className="sw-mt-2">{text}</p>,
+ b: (text) => <b>{text}</b>,
+ },
+ ),
+ title: intl.formatMessage({ id: 'mode_tour.step4.title' }),
+ placement: 'bottom',
+ },
+ {
+ target: '[data-guiding-id="mode-tour-2"]',
+ title: intl.formatMessage({ id: 'mode_tour.step5.title' }),
+ content: null,
+ placement: 'left',
+ hideFooter: true,
+ },
+ ];
+
+ const nextStep = () => {
+ if ((step === 3 && !isAdmin) || step === 4) {
+ document.dispatchEvent(new CustomEvent(CustomEvents.OpenHelpMenu));
+ setTimeout(() => setStep(5));
+ } else {
+ setStep(step + 1);
+ }
+ };
+
+ const dismissTour = () => {
+ document.dispatchEvent(new CustomEvent(CustomEvents.CloseHelpMenu));
+ setStep(6);
+ dismissNotice(NoticeType.MODE_TOUR)
+ .then(() => {
+ updateDismissedNotices(NoticeType.MODE_TOUR, true);
+ })
+ .catch(() => {
+ /* noop */
+ });
+ };
+
+ const onToggle = (props: CallBackProps) => {
+ switch (props.action) {
+ case 'close':
+ case 'skip':
+ case 'reset':
+ dismissTour();
+ break;
+ case 'next':
+ if (props.lifecycle === 'complete') {
+ nextStep();
+ }
+ break;
+ default:
+ break;
+ }
+ };
+
+ useEffect(() => {
+ const listener = () => {
+ setStep(1);
+ setRunManually(true);
+ };
+ document.addEventListener(CustomEvents.RunTourMode, listener);
+
+ return () => document.removeEventListener(CustomEvents.RunTourMode, listener);
+ }, []);
+
+ const isAdmin = currentUser.permissions?.global.includes(Permissions.Admin);
+ const isAdminOrQGAdmin =
+ isAdmin || currentUser.permissions?.global.includes(Permissions.QualityGateAdmin);
+
+ if (!runManually && (currentUser.dismissedNotices[NoticeType.MODE_TOUR] || !isAdminOrQGAdmin)) {
+ return null;
+ }
+
+ const maxSteps = isAdmin ? 4 : 3;
+
+ return (
+ <>
+ <Modal
+ size={ModalSize.Wide}
+ isOpen={step <= 3}
+ onOpenChange={(isOpen) => isOpen === false && dismissTour()}
+ title={
+ step < 4 &&
+ intl.formatMessage({ id: `mode_tour.step${step}.title` }, { version: appState.version })
+ }
+ content={
+ <>
+ {step < 4 && (
+ <>
+ <Image
+ alt={intl.formatMessage({ id: `mode_tour.step${step}.img_alt` })}
+ className="sw-w-full sw-mb-4"
+ src={`/images/mode-tour/step${step}.png`}
+ />
+ {intl.formatMessage(
+ { id: `mode_tour.step${step}.description` },
+ {
+ p1: (text) => <p>{text}</p>,
+ p: (text) => <p className="sw-mt-4">{text}</p>,
+ b: (text) => <b>{text}</b>,
+ },
+ )}
+ <div className="sw-mt-6">
+ <b>
+ {intl.formatMessage({ id: 'guiding.step_x_of_y' }, { 0: step, 1: maxSteps })}
+ </b>
+ </div>
+ </>
+ )}
+ </>
+ }
+ footerLink={
+ <DocumentationLink standalone to={DocLink.ModeMQR}>
+ {intl.formatMessage({ id: `mode_tour.link` })}
+ </DocumentationLink>
+ }
+ primaryButton={
+ <Button variety={ButtonVariety.Primary} onClick={nextStep}>
+ {intl.formatMessage({ id: step === 1 ? 'lets_go' : 'next' })}
+ </Button>
+ }
+ secondaryButton={
+ step === 1 && (
+ <Button variety={ButtonVariety.Default} onClick={dismissTour}>
+ {intl.formatMessage({ id: 'later' })}
+ </Button>
+ )
+ }
+ />
+ <SpotlightTour
+ callback={onToggle}
+ steps={steps}
+ run={step > 3}
+ continuous
+ showProgress={step !== 5}
+ stepIndex={step - 4}
+ nextLabel={intl.formatMessage({ id: 'next' })}
+ stepXofYLabel={(x: number) =>
+ intl.formatMessage({ id: 'guiding.step_x_of_y' }, { 0: x + 3, 1: maxSteps })
+ }
+ />
+ </>
+ );
+}
--- /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 userEvent from '@testing-library/user-event';
+import { ModeServiceMock } from '../../../api/mocks/ModeServiceMock';
+import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
+import UsersServiceMock from '../../../api/mocks/UsersServiceMock';
+import { mockAppState, mockCurrentUser, mockLocation } from '../../../helpers/testMocks';
+import { renderApp } from '../../../helpers/testReactTestingUtils';
+import { byRole } from '../../../sonar-aligned/helpers/testSelector';
+import { Permissions } from '../../../types/permissions';
+import { NoticeType } from '../../../types/users';
+import ModeTour from '../ModeTour';
+import GlobalNav from '../nav/global/GlobalNav';
+
+const ui = {
+ dialog: byRole('dialog'),
+ step1Dialog: byRole('dialog', { name: /mode_tour.step1.title/ }),
+ step2Dialog: byRole('dialog', { name: /mode_tour.step2.title/ }),
+ step3Dialog: byRole('dialog', { name: /mode_tour.step3.title/ }),
+ next: byRole('button', { name: 'next' }),
+ later: byRole('button', { name: 'later' }),
+ skip: byRole('button', { name: 'skip' }),
+ letsgo: byRole('button', { name: 'lets_go' }),
+ help: byRole('button', { name: 'help' }),
+ guidePopup: byRole('alertdialog'),
+ tourTrigger: byRole('menuitem', { name: 'mode_tour.name' }),
+};
+
+const settingsHandler = new SettingsServiceMock();
+const modeHandler = new ModeServiceMock();
+const usersHandler = new UsersServiceMock();
+
+beforeEach(() => {
+ settingsHandler.reset();
+ modeHandler.reset();
+ usersHandler.reset();
+});
+
+it('renders the tour for admin', async () => {
+ const user = userEvent.setup();
+ renderGlobalNav(mockCurrentUser({ permissions: { global: [Permissions.Admin] } }));
+ expect(ui.step1Dialog.get()).toBeInTheDocument();
+ expect(ui.later.get()).toBeInTheDocument();
+ expect(ui.next.query()).not.toBeInTheDocument();
+ expect(ui.letsgo.get()).toBeInTheDocument();
+ expect(ui.step1Dialog.get()).toHaveTextContent('guiding.step_x_of_y.1.4');
+ await user.click(ui.letsgo.get());
+
+ expect(ui.step2Dialog.get()).toBeInTheDocument();
+ expect(ui.step1Dialog.query()).not.toBeInTheDocument();
+ expect(ui.later.query()).not.toBeInTheDocument();
+ expect(ui.next.get()).toBeInTheDocument();
+ expect(ui.letsgo.query()).not.toBeInTheDocument();
+ expect(ui.step2Dialog.get()).toHaveTextContent('guiding.step_x_of_y.2.4');
+ await user.click(ui.next.get());
+
+ expect(ui.step3Dialog.get()).toBeInTheDocument();
+ expect(ui.step2Dialog.query()).not.toBeInTheDocument();
+ expect(ui.next.get()).toBeInTheDocument();
+ expect(ui.step3Dialog.get()).toHaveTextContent('guiding.step_x_of_y.3.4');
+ await user.click(ui.next.get());
+
+ expect(ui.dialog.query()).not.toBeInTheDocument();
+ expect(await ui.guidePopup.find()).toBeInTheDocument();
+ expect(ui.guidePopup.get()).toHaveTextContent('guiding.step_x_of_y.4.4');
+ expect(ui.guidePopup.by(ui.next).get()).toBeInTheDocument();
+ expect(ui.tourTrigger.query()).not.toBeInTheDocument();
+ await user.click(ui.next.get());
+
+ expect(ui.tourTrigger.get()).toBeInTheDocument();
+ expect(await ui.guidePopup.find()).toBeInTheDocument();
+ expect(ui.guidePopup.query()).not.toHaveTextContent('guiding.step_x_of_y');
+ expect(ui.next.query()).not.toBeInTheDocument();
+ expect(ui.skip.get()).toBeInTheDocument();
+ await user.click(ui.skip.get());
+
+ expect(ui.tourTrigger.query()).not.toBeInTheDocument();
+ expect(ui.dialog.query()).not.toBeInTheDocument();
+
+ // replay the tour
+ await user.click(ui.help.get());
+ await user.click(ui.tourTrigger.get());
+ expect(ui.step1Dialog.get()).toBeInTheDocument();
+ expect(ui.step1Dialog.get()).toHaveTextContent('guiding.step_x_of_y.1.4');
+});
+
+it('renders the tour for gateadmins', async () => {
+ const user = userEvent.setup();
+ renderGlobalNav(mockCurrentUser({ permissions: { global: [Permissions.QualityGateAdmin] } }));
+ expect(ui.step1Dialog.get()).toBeInTheDocument();
+ expect(ui.later.get()).toBeInTheDocument();
+ expect(ui.next.query()).not.toBeInTheDocument();
+ expect(ui.letsgo.get()).toBeInTheDocument();
+ expect(ui.step1Dialog.get()).toHaveTextContent('guiding.step_x_of_y.1.3');
+ await user.click(ui.letsgo.get());
+
+ expect(ui.step2Dialog.get()).toBeInTheDocument();
+ expect(ui.step1Dialog.query()).not.toBeInTheDocument();
+ expect(ui.later.query()).not.toBeInTheDocument();
+ expect(ui.next.get()).toBeInTheDocument();
+ expect(ui.letsgo.query()).not.toBeInTheDocument();
+ expect(ui.step2Dialog.get()).toHaveTextContent('guiding.step_x_of_y.2.3');
+ await user.click(ui.next.get());
+
+ expect(ui.step3Dialog.get()).toBeInTheDocument();
+ expect(ui.step2Dialog.query()).not.toBeInTheDocument();
+ expect(ui.next.get()).toBeInTheDocument();
+ expect(ui.step3Dialog.get()).toHaveTextContent('guiding.step_x_of_y.3.3');
+ await user.click(ui.next.get());
+
+ expect(ui.dialog.query()).not.toBeInTheDocument();
+ expect(ui.tourTrigger.get()).toBeInTheDocument();
+ expect(await ui.guidePopup.find()).toBeInTheDocument();
+ expect(ui.guidePopup.query()).not.toHaveTextContent('guiding.step_x_of_y');
+ expect(ui.next.query()).not.toBeInTheDocument();
+ expect(ui.skip.get()).toBeInTheDocument();
+ await user.click(ui.skip.get());
+
+ expect(ui.tourTrigger.query()).not.toBeInTheDocument();
+ expect(ui.dialog.query()).not.toBeInTheDocument();
+
+ // replay the tour
+ await user.click(ui.help.get());
+ await user.click(ui.tourTrigger.get());
+ expect(ui.step1Dialog.get()).toBeInTheDocument();
+ expect(ui.step1Dialog.get()).toHaveTextContent('guiding.step_x_of_y.1.3');
+});
+
+it('should not render the tour for regular users', async () => {
+ const user = userEvent.setup();
+ renderGlobalNav(mockCurrentUser({ permissions: { global: [] } }));
+ expect(ui.dialog.query()).not.toBeInTheDocument();
+ await user.click(ui.help.get());
+ expect(ui.tourTrigger.query()).not.toBeInTheDocument();
+});
+
+it('should not render the tour if it is already dismissed', async () => {
+ const user = userEvent.setup();
+ renderGlobalNav(
+ mockCurrentUser({
+ permissions: { global: [Permissions.Admin] },
+ dismissedNotices: { [NoticeType.MODE_TOUR]: true },
+ }),
+ );
+ expect(ui.dialog.query()).not.toBeInTheDocument();
+ await user.click(ui.help.get());
+ expect(ui.tourTrigger.get()).toBeInTheDocument();
+
+ await user.click(ui.tourTrigger.get());
+ expect(ui.step1Dialog.get()).toBeInTheDocument();
+ expect(ui.step1Dialog.get()).toHaveTextContent('guiding.step_x_of_y.1.4');
+});
+
+function renderGlobalNav(currentUser = mockCurrentUser()) {
+ renderApp(
+ '/',
+ <>
+ <GlobalNav location={mockLocation()} />
+ <ModeTour />
+ </>,
+ {
+ currentUser,
+ appState: mockAppState({ canAdmin: currentUser.permissions?.global.includes('admin') }),
+ },
+ );
+}
return (
<MainMenuItem>
<NavLink
+ data-guiding-id="mode-tour-1"
className={({ isActive }) => (isActive ? ACTIVE_CLASS_NAME : '')}
to="/admin/settings"
>
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { DropdownMenu } from '@sonarsource/echoes-react';
+import { DropdownMenu, IconSlideshow } from '@sonarsource/echoes-react';
import * as React from 'react';
+import { useCurrentUser } from '../../app/components/current-user/CurrentUserContext';
+import { CustomEvents } from '../../helpers/constants';
import { DocLink } from '../../helpers/doc-links';
import { translate } from '../../helpers/l10n';
+import { Permissions } from '../../types/permissions';
import { SuggestionLink } from '../../types/types';
import { DocItemLink } from './DocItemLink';
import { SuggestionsContext } from './SuggestionsContext';
export function EmbedDocsPopup() {
const firstItemRef = React.useRef<HTMLAnchorElement>(null);
+ const { currentUser } = useCurrentUser();
const { suggestions } = React.useContext(SuggestionsContext);
React.useEffect(() => {
firstItemRef.current?.focus();
}, []);
+ const runModeTour = () => {
+ document.dispatchEvent(new CustomEvent(CustomEvents.RunTourMode));
+ };
+
+ const isAdminOrQGAdmin =
+ currentUser.permissions?.global.includes(Permissions.Admin) ||
+ currentUser.permissions?.global.includes(Permissions.QualityGateAdmin);
+
return (
<>
{suggestions.length !== 0 && <Suggestions suggestions={suggestions} />}
</DropdownMenu.ItemLink>
<DropdownMenu.ItemLink to="https://twitter.com/SonarQube">X @SonarQube</DropdownMenu.ItemLink>
+
+ {isAdminOrQGAdmin && (
+ <>
+ <DropdownMenu.Separator />
+
+ <DropdownMenu.GroupLabel>{translate('tours')}</DropdownMenu.GroupLabel>
+
+ <DropdownMenu.ItemButton
+ prefix={<IconSlideshow />}
+ data-guiding-id="mode-tour-2"
+ onClick={runModeTour}
+ >
+ {translate('mode_tour.name')}
+ </DropdownMenu.ItemButton>
+ </>
+ )}
</>
);
}
DropdownMenuAlign,
IconQuestionMark,
} from '@sonarsource/echoes-react';
+import { useEffect, useState } from 'react';
+import { CustomEvents } from '../../helpers/constants';
import { translate } from '../../helpers/l10n';
import { EmbedDocsPopup } from './EmbedDocsPopup';
export default function EmbedDocsPopupHelper() {
+ const [open, setOpen] = useState(false);
+
+ useEffect(() => {
+ const openListener = () => setOpen(true);
+ const closeListener = () => setOpen(false);
+ document.addEventListener(CustomEvents.OpenHelpMenu, openListener);
+ document.addEventListener(CustomEvents.CloseHelpMenu, closeListener);
+ return () => {
+ document.removeEventListener(CustomEvents.OpenHelpMenu, openListener);
+ document.addEventListener(CustomEvents.CloseHelpMenu, closeListener);
+ };
+ }, []);
+
return (
<div className="dropdown">
<DropdownMenu.Root
align={DropdownMenuAlign.End}
id="help-menu-dropdown"
+ isOpen={open}
+ onOpen={() => setOpen(true)}
+ onClose={() => setOpen(false)}
items={<EmbedDocsPopup />}
>
<ButtonIcon
</div>
)}
- <div className="sw-flex sw-justify-between sw-items-center sw-mt-4">
- {(stepXofYLabel || size > 1) && (
- <strong>
- {stepXofYLabel
- ? stepXofYLabel(index + 1, size)
- : intl.formatMessage({ id: 'guiding.step_x_of_y' }, { '0': index + 1, '1': size })}
- </strong>
- )}
- <span />
- <div>
- {index > 0 && (
- <Button className="sw-mr-4" variety={ButtonVariety.DefaultGhost} {...backProps}>
- {backProps.title}
- </Button>
- )}
- {continuous && !isLastStep && (
- <Button variety={ButtonVariety.Primary} {...primaryProps}>
- {primaryProps.title}
- </Button>
- )}
- {(!continuous || isLastStep) && (
- <Button variety={ButtonVariety.Primary} {...closeProps}>
- {closeProps.title}
- </Button>
+ {!step.hideFooter && (
+ <div className="sw-flex sw-justify-between sw-items-center sw-mt-4">
+ {(stepXofYLabel || size > 1) && (
+ <strong>
+ {stepXofYLabel
+ ? stepXofYLabel(index + 1, size)
+ : intl.formatMessage({ id: 'guiding.step_x_of_y' }, { '0': index + 1, '1': size })}
+ </strong>
)}
+ <span />
+ <div>
+ {index > 0 && (
+ <Button className="sw-mr-4" variety={ButtonVariety.DefaultGhost} {...backProps}>
+ {backProps.title}
+ </Button>
+ )}
+ {continuous && !isLastStep && (
+ <Button variety={ButtonVariety.Primary} {...primaryProps}>
+ {primaryProps.title}
+ </Button>
+ )}
+ {(!continuous || isLastStep) && (
+ <Button variety={ButtonVariety.Primary} {...closeProps}>
+ {closeProps.title}
+ </Button>
+ )}
+ </div>
</div>
- </div>
+ )}
</StyledPopupWrapper>
);
}
export const GRADLE_SCANNER_VERSION = '6.0.0.5145';
export const ONE_SECOND = 1000;
+
+export enum CustomEvents {
+ OpenHelpMenu = 'open-help-menu',
+ CloseHelpMenu = 'close-help-menu',
+ RunTourMode = 'runTour-mode',
+}
OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION = 'overviewZeroNewIssuesSimplification',
ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE = 'onboardingDismissCaycBranchSummaryGuide',
MQR_MODE_ADVERTISEMENT_BANNER = 'showNewModesBanner',
+ MODE_TOUR = 'showNewModesTour',
}
export interface LoggedInUser extends CurrentUser, UserActive {
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(
"Value of parameter 'notice' (not_supported_value) must be one of: [educationPrinciples, sonarlintAd, issueCleanCodeGuide, qualityGateCaYCConditionsSimplification, " +
- "overviewZeroNewIssuesSimplification, issueNewIssueStatusAndTransitionGuide, onboardingDismissCaycBranchSummaryGuide]");
+ "overviewZeroNewIssuesSimplification, issueNewIssueStatusAndTransitionGuide, onboardingDismissCaycBranchSummaryGuide, showNewModesTour, showNewModesBanner]");
}
@Test
private static final String SHOW_NEW_MODES_BANNER = "showNewModesBanner";
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, ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE);
+ OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION, ISSUE_NEW_ISSUE_STATUS_AND_TRANSITION_GUIDE, ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE, SHOW_NEW_MODES_TOUR, SHOW_NEW_MODES_BANNER);
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.";
inheritance=Inheritance
internal=internal
key=Key
+later=Later
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
+lets_go=Let's go
library=Library
line_number=Line Number
links=Links
to=To
to_=to
total=Total
+tours=Tours
treemap=Treemap
true=True
type=Type
#
#------------------------------------------------------------------------------
guiding.step_x_of_y={0} of {1}
+
+#------------------------------------------------------------------------------
+#
+# MODE TOUR
+#
+#------------------------------------------------------------------------------
+mode_tour.name=Standard Experience and MQR mode tour
+mode_tour.link=Learn more about the modes in documentation
+mode_tour.step1.title=Welcome to SonarQube Server {version}!
+mode_tour.step1.img_alt=Visual presentation of the difference of the issue in the two new modes
+mode_tour.step1.description=<p1>We have introduced two modes - <b>Standard Experience</b> and <b>Multi-Quality Rule (MQR) Mode</b> - to empower you and your team to either continue using familiar workflows or explore new concepts.</p1><p>Take this tour to see what is the difference is between them.</p>
+mode_tour.step2.title=Standard Experience
+mode_tour.step2.img_alt=Visual presentation of the new look of Standard Experience badges: Vulnerability, Bug and Code Smell
+mode_tour.step2.description=<p1>The Standard Experience encompasses the use of rule types such as <b>Vulnerabilities</b>, <b>Bugs</b>, and <b>Code Smells</b>. Each rule has a single type and severity level, which you can customize with appropriate permissions.</p1><p>This approach focuses on assigning severity to a rule based on the single software quality (e.g., Security, Reliability, or Maintainability) on which it has the most significant impact.</p>
+mode_tour.step3.title=Multi-Quality Rule Mode
+mode_tour.step3.img_alt=Visual presentation of the new Software Quality badges: Security, Reliability and Maintainability and their connection to the corresponding Standard Experience badges
+mode_tour.step3.description=<p1>The new Multi-Quality Rule Mode aims to more accurately represent an issue's impact on all software qualities. </p1><p>It does this by assigning a separate severity to a rule for each software quality (Security, Reliability, and Maintainability), which replaces the types (Vulnerabilities, Bugs, and Code Smells). You can customize the severity level with appropriate permissions.</p><p>This approach focuses on ensuring the impact on all software qualities is clear, not just the one most severely impacted.</p>
+mode_tour.step4.title=Switch modes
+mode_tour.step4.description=<p1>You are currently in <b>{mode}</b>.</p1><p>To change it, go to Administration > Configuration > General Settings > Mode.</p><p>Your instance will start in the mode that most closely resembles the software version you are upgrading from.</p>
+mode_tour.step5.title=You can replay the tour from the help section
+
import org.sonarqube.ws.UserTokens;
import org.sonarqube.ws.Users;
import org.sonarqube.ws.Users.CreateWsResponse.User;
+import org.sonarqube.ws.client.HttpConnector;
import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.WsClientFactories;
import org.sonarqube.ws.client.WsResponse;
import org.sonarqube.ws.client.usergroups.AddUserRequest;
import org.sonarqube.ws.client.users.ChangePasswordRequest;
new org.sonarqube.ws.client.permissions.AddUserRequest()
.setLogin(u.getLogin())
.setPermission("admin"));
+ dismissModesTour(u);
return u;
}
User user = generate(populators);
session.wsClient().permissions().addUser(new org.sonarqube.ws.client.permissions.AddUserRequest().setLogin(user.getLogin()).setPermission("admin"));
session.wsClient().userGroups().addUser(new AddUserRequest().setLogin(user.getLogin()).setName("sonar-administrators"));
+ dismissModesTour(user);
return user;
}
+ private void dismissModesTour(User user) {
+ WsClientFactories.getDefault().newClient(HttpConnector.newBuilder()
+ .url(session.wsClient().wsConnector().baseUrl())
+ .credentials(user.getLogin(), user.getLogin())
+ .build()).users().dismissNotice("showNewModesTour");
+ }
+
public UsersService service() {
return session.wsClient().users();
}