Browse Source

SONAR-20742 Implement new SL promotion

tags/10.3.0.82913
stanislavh 8 months ago
parent
commit
68f1de76d0

+ 8
- 1
server/sonar-web/src/main/js/app/components/current-user/CurrentUserContext.ts View File

@@ -38,8 +38,15 @@ export const CurrentUserContext = React.createContext<CurrentUserContextInterfac
updateDismissedNotices: noop,
});

export function useCurrentLoginUser() {
export function useCurrentUser() {
const { currentUser } = useContext(CurrentUserContext);

return currentUser;
}

export function useCurrentLoginUser() {
const currentUser = useCurrentUser();

if (!currentUser.isLoggedIn) {
handleRequiredAuthentication();
}

+ 13
- 5
server/sonar-web/src/main/js/apps/overview/components/BranchQualityGateConditions.tsx View File

@@ -21,6 +21,7 @@
import { ChevronRightIcon, DangerButtonSecondary } from 'design-system';
import React from 'react';
import { useIntl } from 'react-intl';
import { isIssueMeasure, propsToIssueParams } from '../../../components/shared/utils';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { getLocalizedMetricName } from '../../../helpers/l10n';
import { formatMeasure, getShortType, isDiffMetric } from '../../../helpers/measures';
@@ -168,10 +169,10 @@ function getQGConditionUrl(
) {
const { metric } = condition;
const sinceLeakPeriod = isDiffMetric(metric);
const issueType = RATING_METRICS_MAPPING[metric];
const ratingIssueType = RATING_METRICS_MAPPING[metric];

if (issueType) {
if (issueType === IssueType.SecurityHotspot) {
if (ratingIssueType) {
if (ratingIssueType === IssueType.SecurityHotspot) {
return getComponentSecurityHotspotsUrl(componentKey, {
...getBranchLikeQuery(branchLike),
...(sinceLeakPeriod ? { sinceLeakPeriod: 'true' } : {}),
@@ -179,15 +180,22 @@ function getQGConditionUrl(
}
return getComponentIssuesUrl(componentKey, {
resolved: 'false',
types: issueType,
types: ratingIssueType,
...getBranchLikeQuery(branchLike),
...(sinceLeakPeriod ? { sinceLeakPeriod: 'true' } : {}),
...(issueType !== IssueType.CodeSmell
...(ratingIssueType !== IssueType.CodeSmell
? { severities: RATING_TO_SEVERITIES_MAPPING[Number(condition.error) - 1] }
: {}),
});
}

if (isIssueMeasure(condition.measure.metric.key)) {
return getComponentIssuesUrl(componentKey, {
...propsToIssueParams(condition.measure.metric.key, condition.period != null),
...getBranchLikeQuery(branchLike),
});
}

return getComponentDrilldownUrl({
componentKey,
metric,

+ 32
- 20
server/sonar-web/src/main/js/apps/overview/components/__tests__/BranchQualityGate-it.tsx View File

@@ -33,32 +33,44 @@ it('renders failed QG', () => {
renderBranchQualityGate();

// Maintainability rating condition
expect(
byRole('link', {
name: 'overview.failed_condition.x_requiredmetric_domain.Maintainability metric.type.rating A',
}).get(),
).toBeInTheDocument();
const maintainabilityRatingLink = byRole('link', {
name: 'overview.failed_condition.x_requiredmetric_domain.Maintainability metric.type.rating A',
}).get();
expect(maintainabilityRatingLink).toBeInTheDocument();
expect(maintainabilityRatingLink).toHaveAttribute(
'href',
'/project/issues?resolved=false&types=CODE_SMELL&pullRequest=1001&sinceLeakPeriod=true&id=my-project',
);

// Security Hotspots rating condition
expect(
byRole('link', {
name: 'overview.failed_condition.x_requiredmetric_domain.Security Review metric.type.rating A',
}).get(),
).toBeInTheDocument();
const securityHotspotsRatingLink = byRole('link', {
name: 'overview.failed_condition.x_requiredmetric_domain.Security Review metric.type.rating A',
}).get();
expect(securityHotspotsRatingLink).toBeInTheDocument();
expect(securityHotspotsRatingLink).toHaveAttribute(
'href',
'/security_hotspots?id=my-project&pullRequest=1001',
);

// New code smells
expect(
byRole('link', {
name: 'overview.failed_condition.x_required 5 Code Smells ≤ 1',
}).get(),
).toBeInTheDocument();
const codeSmellsLink = byRole('link', {
name: 'overview.failed_condition.x_required 5 Code Smells ≤ 1',
}).get();
expect(codeSmellsLink).toBeInTheDocument();
expect(codeSmellsLink).toHaveAttribute(
'href',
'/project/issues?resolved=false&types=CODE_SMELL&pullRequest=1001&id=my-project',
);

// Conditions to cover
expect(
byRole('link', {
name: 'overview.failed_condition.x_required 5 Conditions to cover ≥ 10',
}).get(),
).toBeInTheDocument();
const conditionToCoverLink = byRole('link', {
name: 'overview.failed_condition.x_required 5 Conditions to cover ≥ 10',
}).get();
expect(conditionToCoverLink).toBeInTheDocument();
expect(conditionToCoverLink).toHaveAttribute(
'href',
'/component_measures?id=my-project&metric=conditions_to_cover&pullRequest=1001&view=list',
);

expect(byLabelText('overview.quality_gate_x.overview.gate.ERROR').get()).toBeInTheDocument();
});

+ 4
- 4
server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx View File

@@ -32,9 +32,9 @@ import MeasuresCardPanel from '../branches/MeasuresCardPanel';
import BranchQualityGate from '../components/BranchQualityGate';
import IgnoredConditionWarning from '../components/IgnoredConditionWarning';
import MetaTopBar from '../components/MetaTopBar';
import SonarLintPromotion from '../components/SonarLintPromotion';
import '../styles.css';
import { PR_METRICS, Status } from '../utils';
import SonarLintAd from './SonarLintAd';

interface Props {
branchLike: PullRequest;
@@ -89,13 +89,13 @@ export default function PullRequestOverview(props: Props) {
}

const failedConditions = conditions
.filter((condition) => condition.level === 'ERROR')
.filter((condition) => condition.level === Status.ERROR)
.map((c) => enhanceConditionWithMeasure(c, measures))
.filter(isDefined);

return (
<CenteredLayout>
<div className="it__pr-overview sw-mt-12 sw-grid sw-grid-cols-12">
<div className="it__pr-overview sw-mt-12 sw-mb-8 sw-grid sw-grid-cols-12">
<div className="sw-col-start-2 sw-col-span-10">
<MetaTopBar branchLike={branchLike} measures={measures} />
<BasicSeparator className="sw-my-4" />
@@ -119,7 +119,7 @@ export default function PullRequestOverview(props: Props) {
measures={measures}
/>

<SonarLintPromotion qgConditions={conditions} />
<SonarLintAd status={status} />
</div>
</div>
</CenteredLayout>

+ 111
- 0
server/sonar-web/src/main/js/apps/overview/pullRequests/SonarLintAd.tsx View File

@@ -0,0 +1,111 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import styled from '@emotion/styled';
import {
Card,
CheckIcon,
CloseIcon,
DiscreetInteractiveIcon,
LightLabel,
ListItem,
StandoutLink,
SubTitle,
SubnavigationFlowSeparator,
} from 'design-system';
import React from 'react';
import { useIntl } from 'react-intl';
import { useCurrentUser } from '../../../app/components/current-user/CurrentUserContext';
import useLocalStorage from '../../../hooks/useLocalStorage';
import { Status } from '../../../types/types';
import { isLoggedIn } from '../../../types/users';
import { Status as QGStatus } from '../utils';

interface Props {
status?: Status;
}

const SONARLINT_PR_LS_KEY = 'sonarqube.pr_overview.show_sonarlint_promotion';

export default function SonarLintAd({ status }: Readonly<Props>) {
const intl = useIntl();
const user = useCurrentUser();
const [showSLPromotion, setSLPromotion] = useLocalStorage(SONARLINT_PR_LS_KEY, true);

const onDismiss = React.useCallback(() => {
setSLPromotion(false);
}, [setSLPromotion]);

if (
!isLoggedIn(user) ||
user.usingSonarLintConnectedMode ||
status !== QGStatus.ERROR ||
!showSLPromotion
) {
return null;
}

return (
<StyledSummaryCard className="it__overview__sonarlint-promotion sw-flex sw-flex-col sw-mt-4">
<div className="sw-flex sw-justify-between">
<SubTitle as="h2" className="sw-body-md-highlight">
{intl.formatMessage({ id: 'overview.sonarlint_ad.header' })}
</SubTitle>
<DiscreetInteractiveIcon
Icon={CloseIcon}
aria-label={intl.formatMessage({ id: 'overview.sonarlint_ad.close_promotion' })}
onClick={onDismiss}
size="medium"
/>
</div>
<ul>
<TickLink message={intl.formatMessage({ id: 'overview.sonarlint_ad.details_1' })} />
<TickLink message={intl.formatMessage({ id: 'overview.sonarlint_ad.details_2' })} />
<TickLink message={intl.formatMessage({ id: 'overview.sonarlint_ad.details_3' })} />
<TickLink message={intl.formatMessage({ id: 'overview.sonarlint_ad.details_4' })} />
<TickLink
className="sw-body-sm-highlight"
message={intl.formatMessage({ id: 'overview.sonarlint_ad.details_5' })}
/>
</ul>
<SubnavigationFlowSeparator className="sw-mb-4" />
<div>
<StandoutLink
className="sw-text-left sw-body-sm-highlight"
to="https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=sonarqube"
>
{intl.formatMessage({ id: 'overview.sonarlint_ad.learn_more' })}
</StandoutLink>
</div>
</StyledSummaryCard>
);
}

function TickLink({ className, message }: Readonly<{ className?: string; message: string }>) {
return (
<ListItem className={`sw-body-sm ${className}`}>
<CheckIcon fill="iconTrendPositive" />
<LightLabel className="sw-pl-1">{message}</LightLabel>
</ListItem>
);
}

const StyledSummaryCard = styled(Card)`
background-color: transparent;
`;

+ 31
- 0
server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/PullRequestOverview-it.tsx View File

@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import { getQualityGateProjectStatus } from '../../../../api/quality-gates';
import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider';
@@ -185,6 +186,36 @@ it('should render correctly for a failed QG', async () => {
).toBeInTheDocument();
});

it('renders SL promotion', async () => {
const user = userEvent.setup();
jest.mocked(getQualityGateProjectStatus).mockResolvedValueOnce({
status: 'ERROR',
conditions: [
mockQualityGateProjectCondition({
errorThreshold: '2.0',
metricKey: MetricKey.new_coverage,
periodIndex: 1,
}),
],
caycStatus: CaycStatus.Compliant,
ignoredConditions: true,
});
renderPullRequestOverview();

await waitFor(async () =>
expect(
await byRole('heading', { name: 'overview.sonarlint_ad.header' }).find(),
).toBeInTheDocument(),
);

// Close promotion
await user.click(byRole('button', { name: 'overview.sonarlint_ad.close_promotion' }).get());

expect(
byRole('heading', { name: 'overview.sonarlint_ad.header' }).query(),
).not.toBeInTheDocument();
});

function renderPullRequestOverview(
props: Partial<ComponentPropsType<typeof PullRequestOverview>> = {},
) {

+ 79
- 0
server/sonar-web/src/main/js/hooks/__tests__/useLocalStorage-test.tsx View File

@@ -0,0 +1,79 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { renderComponent } from '../../helpers/testReactTestingUtils';
import { FCProps } from '../../types/misc';
import useLocalStorage from '../useLocalStorage';

describe('useLocalStorage hook', () => {
it('gets/sets boolean value', async () => {
const user = userEvent.setup();
renderLSComponent();

expect(screen.getByRole('button', { name: 'show' })).toBeInTheDocument();
user.click(screen.getByRole('button', { name: 'show' }));
expect(await screen.findByText('text')).toBeInTheDocument();
});

it('gets/sets string value', async () => {
const user = userEvent.setup();
const props = { condition: (value: string) => value === 'ok', valueToSet: 'wow' };
const { rerender } = renderLSComponent(props);

expect(screen.getByRole('button', { name: 'show' })).toBeInTheDocument();
user.click(screen.getByRole('button', { name: 'show' }));
expect(screen.queryByText('text')).not.toBeInTheDocument();

rerender(<LSComponent lsKey="test_ls" {...props} valueToSet="ok" />);
user.click(screen.getByRole('button', { name: 'show' }));
expect(await screen.findByText('text')).toBeInTheDocument();
});
});

function renderLSComponent(props: Partial<FCProps<typeof LSComponent>> = {}) {
return renderComponent(
<LSComponent lsKey="test_ls" valueToSet condition={(value) => Boolean(value)} {...props} />,
);
}

function LSComponent({
lsKey,
condition,
initialValue,
valueToSet,
}: Readonly<{
lsKey: string;
condition: (value: boolean | string) => boolean;
valueToSet: boolean | string;
initialValue?: boolean | string;
}>) {
const [value, setValue] = useLocalStorage(lsKey, initialValue);

return (
<div>
<button type="button" onClick={() => setValue(valueToSet)}>
show
</button>
{condition(value) && <span>text</span>}
</div>
);
}

+ 48
- 0
server/sonar-web/src/main/js/hooks/useLocalStorage.ts View File

@@ -0,0 +1,48 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import { get, save } from '../helpers/storage';

export default function useLocalStorage<T>(key: string, initialValue?: T) {
const lsValue = React.useCallback(() => {
const v = get(key);
try {
return JSON.parse(v as string);
} catch {
return v;
}
}, [key]);

const [storedValue, setStoredValue] = React.useState(lsValue() ?? initialValue);

const changeValue = React.useCallback(
(value: T) => {
save(key, JSON.stringify(value));
setStoredValue(lsValue());
},
[key, lsValue],
);

React.useEffect(() => {
setStoredValue(lsValue() ?? initialValue);
}, [lsValue, initialValue]);

return [storedValue, changeValue];
}

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

@@ -3865,6 +3865,15 @@ overview.deprecated_profile=This Quality Profile uses {0} deprecated rules and s
overview.deleted_profile={0} has been deleted since the last analysis.
overview.link_to_x_profile_y=Go to {0} profile "{1}"

overview.sonarlint_ad.header=Catch issues before they fail your Quality Gate with our IDE extension, SonarLint
overview.sonarlint_ad.details_1=The power of Sonar analyzers directly as you type
overview.sonarlint_ad.details_2=No need to wait for your PR to pass all checks
overview.sonarlint_ad.details_3=Repair flagged issues in real-time with quick fixes
overview.sonarlint_ad.details_4=12 major IDE's supported (including key JetBrains and Microsoft IDE's
overview.sonarlint_ad.details_5=Free forever
overview.sonarlint_ad.learn_more=Learn more about SonarLint
overview.sonarlint_ad.close_promotion=Close SonarLint romotion

overview.badges.get_badge=Badges
overview.badges.title=Get project badges
overview.badges.description.TRK=Show the status of your project metrics on your README or website. Pick your style:

Loading…
Cancel
Save