Co-authored-by: Andrey Luiz <andrey.luiz@sonarsource.com> Co-authored-by: Nolwenn Cadic <98824442+Nolwenn-cadic-sonarsource@users.noreply.github.com>tags/10.3.0.82913
@@ -139,11 +139,14 @@ function TooltipComponent({ | |||
</div> | |||
<div>{step.content}</div> | |||
<div className="sw-flex sw-justify-between sw-items-center sw-mt-3"> | |||
<strong> | |||
{stepXofYLabel | |||
? stepXofYLabel(index + 1, size) | |||
: intl.formatMessage({ id: 'guiding.step_x_of_y' }, { '0': index + 1, '1': size })} | |||
</strong> | |||
{(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 && ( | |||
<ButtonLink className="sw-mr-4" {...backProps}> |
@@ -30,39 +30,40 @@ it('should display the spotlight tour', async () => { | |||
expect(await screen.findByRole('alertdialog')).toBeInTheDocument(); | |||
expect(screen.getByRole('alertdialog')).toHaveTextContent( | |||
'Trust The FooFoo bar is bazstep 1 of 5next' | |||
'Trust The FooFoo bar is bazstep 1 of 5next', | |||
); | |||
expect(screen.getByText('step 1 of 5')).toBeInTheDocument(); | |||
await user.click(screen.getByRole('button', { name: 'next' })); | |||
expect(screen.getByRole('alertdialog')).toHaveTextContent( | |||
'Trust The BazBaz foo is barstep 2 of 5go_backnext' | |||
'Trust The BazBaz foo is barstep 2 of 5go_backnext', | |||
); | |||
expect(callback).toHaveBeenCalled(); | |||
await user.click(screen.getByRole('button', { name: 'next' })); | |||
expect(screen.getByRole('alertdialog')).toHaveTextContent( | |||
'Trust The BarBar baz is foostep 3 of 5go_backnext' | |||
'Trust The BarBar baz is foostep 3 of 5go_backnext', | |||
); | |||
await user.click(screen.getByRole('button', { name: 'next' })); | |||
expect(screen.getByRole('alertdialog')).toHaveTextContent( | |||
'Trust The Foo 2Foo baz is barstep 4 of 5go_backnext' | |||
'Trust The Foo 2Foo baz is barstep 4 of 5go_backnext', | |||
); | |||
await user.click(screen.getByRole('button', { name: 'go_back' })); | |||
expect(screen.getByRole('alertdialog')).toHaveTextContent( | |||
'Trust The BarBar baz is foostep 3 of 5go_backnext' | |||
'Trust The BarBar baz is foostep 3 of 5go_backnext', | |||
); | |||
await user.click(screen.getByRole('button', { name: 'next' })); | |||
await user.click(screen.getByRole('button', { name: 'next' })); | |||
expect(screen.getByRole('alertdialog')).toHaveTextContent( | |||
'Trust The Baz 2Baz bar is foostep 5 of 5go_backclose' | |||
'Trust The Baz 2Baz bar is foostep 5 of 5go_backclose', | |||
); | |||
expect(screen.queryByRole('button', { name: 'next' })).not.toBeInTheDocument(); | |||
@@ -102,6 +103,23 @@ it('should allow the customization of button labels', async () => { | |||
expect(screen.getByRole('button', { name: 'close_me' })).toBeInTheDocument(); | |||
}); | |||
it('should not display steps counter when there is only one step and no render method', async () => { | |||
renderSpotlightTour({ | |||
steps: [ | |||
{ | |||
target: '#step1', | |||
content: 'Foo bar is baz', | |||
title: 'Trust The Foo', | |||
placement: 'top', | |||
}, | |||
], | |||
stepXofYLabel: undefined, | |||
}); | |||
expect(await screen.findByRole('alertdialog')).toBeInTheDocument(); | |||
expect(screen.queryByText('step 1 of 1')).not.toBeInTheDocument(); | |||
}); | |||
function renderSpotlightTour(props: Partial<SpotlightTourProps> = {}) { | |||
return renderWithContext( | |||
<div> | |||
@@ -149,6 +167,6 @@ function renderSpotlightTour(props: Partial<SpotlightTourProps> = {}) { | |||
]} | |||
{...props} | |||
/> | |||
</div> | |||
</div>, | |||
); | |||
} |
@@ -32,13 +32,22 @@ interface Props { | |||
value: string; | |||
failingConditionMetric: MetricKey; | |||
requireLabel: string; | |||
guidingKeyOnError?: string; | |||
} | |||
export default function MeasuresCardNumber( | |||
props: React.PropsWithChildren<Props & React.HTMLAttributes<HTMLDivElement>>, | |||
) { | |||
const { label, value, failedConditions, url, failingConditionMetric, requireLabel, ...rest } = | |||
props; | |||
const { | |||
label, | |||
value, | |||
failedConditions, | |||
url, | |||
failingConditionMetric, | |||
requireLabel, | |||
guidingKeyOnError, | |||
...rest | |||
} = props; | |||
const failed = Boolean( | |||
failedConditions.find((condition) => condition.metric === failingConditionMetric), | |||
@@ -51,6 +60,7 @@ export default function MeasuresCardNumber( | |||
metric={failingConditionMetric} | |||
label={label} | |||
failed={failed} | |||
data-guiding-id={failed ? guidingKeyOnError : undefined} | |||
{...rest} | |||
> | |||
{failed && <TextError className="sw-font-regular sw-mt-2" text={requireLabel} />} |
@@ -72,6 +72,7 @@ export default function MeasuresCardPanel(props: React.PropsWithChildren<Props>) | |||
count: newViolations, | |||
}, | |||
)} | |||
guidingKeyOnError="overviewZeroNewIssuesSimplification" | |||
/> | |||
<MeasuresCardNumber |
@@ -98,6 +98,7 @@ export function QualityGatePanel(props: QualityGatePanelProps) { | |||
isLastStatus={qgStatusIdx === failedQgStatuses.length - 1} | |||
key={qgStatus.key} | |||
qgStatus={qgStatus} | |||
qualityGate={qualityGate} | |||
/> | |||
))} | |||
</div> |
@@ -27,13 +27,16 @@ import { | |||
QualityGateStatus, | |||
QualityGateStatusConditionEnhanced, | |||
} from '../../../types/quality-gates'; | |||
import { QualityGate } from '../../../types/types'; | |||
import QualityGateConditions from '../components/QualityGateConditions'; | |||
import ZeroNewIssuesSimplificationGuide from '../components/ZeroNewIssuesSimplificationGuide'; | |||
export interface QualityGatePanelSectionProps { | |||
branchLike?: BranchLike; | |||
isApplication?: boolean; | |||
isLastStatus?: boolean; | |||
qgStatus: QualityGateStatus; | |||
qualityGate?: QualityGate; | |||
} | |||
function splitConditions( | |||
@@ -54,7 +57,7 @@ function splitConditions( | |||
} | |||
export function QualityGatePanelSection(props: QualityGatePanelSectionProps) { | |||
const { isApplication, isLastStatus, qgStatus } = props; | |||
const { isApplication, isLastStatus, qgStatus, qualityGate } = props; | |||
const [collapsed, setCollapsed] = React.useState(false); | |||
const toggle = React.useCallback(() => { | |||
@@ -101,10 +104,14 @@ export function QualityGatePanelSection(props: QualityGatePanelSectionProps) { | |||
</> | |||
)} | |||
{qualityGate?.isBuiltIn && ( | |||
<ZeroNewIssuesSimplificationGuide qualityGate={qualityGate} /> | |||
)} | |||
<QualityGateConditions | |||
component={qgStatus} | |||
branchLike={qgStatus.branchLike} | |||
failedConditions={newCodeFailedConditions} | |||
isBuiltInQualityGate={qualityGate?.isBuiltIn} | |||
/> | |||
</> | |||
)} |
@@ -20,9 +20,14 @@ | |||
import { screen } from '@testing-library/react'; | |||
import * as React from 'react'; | |||
import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider'; | |||
import { mockQualityGate, mockQualityGateStatus } from '../../../../helpers/mocks/quality-gates'; | |||
import { mockLoggedInUser } from '../../../../helpers/testMocks'; | |||
import { renderComponent } from '../../../../helpers/testReactTestingUtils'; | |||
import { byRole } from '../../../../helpers/testSelector'; | |||
import { MetricKey } from '../../../../types/metrics'; | |||
import { CaycStatus, Status } from '../../../../types/types'; | |||
import { CurrentUser, NoticeType } from '../../../../types/users'; | |||
import QualityGatePanelSection, { QualityGatePanelSectionProps } from '../QualityGatePanelSection'; | |||
const failedConditions = [ | |||
@@ -31,7 +36,7 @@ const failedConditions = [ | |||
measure: { | |||
metric: { | |||
id: 'metricId1', | |||
key: 'metricKey1', | |||
key: MetricKey.new_coverage, | |||
name: 'metricName1', | |||
type: 'metricType1', | |||
}, | |||
@@ -44,7 +49,7 @@ const failedConditions = [ | |||
measure: { | |||
metric: { | |||
id: 'metricId2', | |||
key: 'metricKey2', | |||
key: MetricKey.security_hotspots, | |||
name: 'metricName2', | |||
type: 'metricType2', | |||
}, | |||
@@ -52,20 +57,33 @@ const failedConditions = [ | |||
metric: MetricKey.security_hotspots, | |||
op: 'op2', | |||
}, | |||
{ | |||
level: 'ERROR' as Status, | |||
measure: { | |||
metric: { | |||
id: 'metricId2', | |||
key: MetricKey.new_violations, | |||
name: 'metricName2', | |||
type: 'metricType2', | |||
}, | |||
}, | |||
metric: MetricKey.new_violations, | |||
op: 'op2', | |||
}, | |||
]; | |||
const qgStatus = { | |||
const qgStatus = mockQualityGateStatus({ | |||
caycStatus: CaycStatus.Compliant, | |||
failedConditions, | |||
key: 'qgStatusKey', | |||
name: 'qgStatusName', | |||
status: 'ERROR' as Status, | |||
}; | |||
}); | |||
it('should render correctly for an application with 1 new code condition and 1 overall code condition', async () => { | |||
renderQualityGatePanelSection(); | |||
expect(await screen.findByText('quality_gates.conditions.new_code_1')).toBeInTheDocument(); | |||
expect(await screen.findByText('quality_gates.conditions.new_code_x.2')).toBeInTheDocument(); | |||
expect(await screen.findByText('quality_gates.conditions.overall_code_1')).toBeInTheDocument(); | |||
}); | |||
@@ -79,6 +97,40 @@ it('should render correctly for a project with 1 new code condition', () => { | |||
expect(screen.queryByText('quality_gates.conditions.overall_code_1')).not.toBeInTheDocument(); | |||
}); | |||
function renderQualityGatePanelSection(props: Partial<QualityGatePanelSectionProps> = {}) { | |||
return renderComponent(<QualityGatePanelSection isApplication qgStatus={qgStatus} {...props} />); | |||
it('should render correctly 0 New issues onboarding', async () => { | |||
renderQualityGatePanelSection({ | |||
isApplication: false, | |||
qgStatus: { ...qgStatus, failedConditions: [failedConditions[2]] }, | |||
qualityGate: mockQualityGate({ isBuiltIn: true }), | |||
}); | |||
expect(screen.queryByText('quality_gates.conditions.new_code_1')).not.toBeInTheDocument(); | |||
expect(await byRole('alertdialog').find()).toBeInTheDocument(); | |||
}); | |||
it('should not render 0 New issues onboarding for user who dismissed it', async () => { | |||
renderQualityGatePanelSection( | |||
{ | |||
isApplication: false, | |||
qgStatus: { ...qgStatus, failedConditions: [failedConditions[2]] }, | |||
qualityGate: mockQualityGate({ isBuiltIn: true }), | |||
}, | |||
mockLoggedInUser({ | |||
dismissedNotices: { [NoticeType.OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION]: true }, | |||
}), | |||
); | |||
expect(screen.queryByText('quality_gates.conditions.new_code_1')).not.toBeInTheDocument(); | |||
expect(await byRole('alertdialog').query()).not.toBeInTheDocument(); | |||
}); | |||
function renderQualityGatePanelSection( | |||
props: Partial<QualityGatePanelSectionProps> = {}, | |||
currentUser: CurrentUser = mockLoggedInUser(), | |||
) { | |||
return renderComponent( | |||
<CurrentUserContextProvider currentUser={currentUser}> | |||
<QualityGatePanelSection isApplication qgStatus={qgStatus} {...props} /> | |||
</CurrentUserContextProvider>, | |||
); | |||
} |
@@ -22,9 +22,11 @@ import { sortBy } from 'lodash'; | |||
import * as React from 'react'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { MetricKey } from '../../../types/metrics'; | |||
import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates'; | |||
import { Component } from '../../../types/types'; | |||
import QualityGateCondition from './QualityGateCondition'; | |||
import QualityGateSimplifiedCondition from './QualityGateSimplifiedCondition'; | |||
const LEVEL_ORDER = ['ERROR', 'WARN']; | |||
@@ -33,16 +35,25 @@ export interface QualityGateConditionsProps { | |||
component: Pick<Component, 'key'>; | |||
collapsible?: boolean; | |||
failedConditions: QualityGateStatusConditionEnhanced[]; | |||
isBuiltInQualityGate?: boolean; | |||
} | |||
const MAX_CONDITIONS = 5; | |||
export function QualityGateConditions(props: QualityGateConditionsProps) { | |||
const { branchLike, collapsible, component, failedConditions } = props; | |||
const { branchLike, collapsible, component, failedConditions, isBuiltInQualityGate } = props; | |||
const [collapsed, toggleCollapsed] = React.useState(Boolean(collapsible)); | |||
const handleToggleCollapsed = React.useCallback(() => toggleCollapsed(!collapsed), [collapsed]); | |||
const isSimplifiedCondition = React.useCallback( | |||
(condition: QualityGateStatusConditionEnhanced) => { | |||
const { metric } = condition.measure; | |||
return metric.key === MetricKey.new_violations && isBuiltInQualityGate; | |||
}, | |||
[isBuiltInQualityGate], | |||
); | |||
const sortedConditions = sortBy(failedConditions, (condition) => | |||
LEVEL_ORDER.indexOf(condition.level), | |||
); | |||
@@ -62,11 +73,19 @@ export function QualityGateConditions(props: QualityGateConditionsProps) { | |||
<ul id="overview-quality-gate-conditions-list" className="sw-mb-2"> | |||
{renderConditions.map((condition) => ( | |||
<div key={condition.measure.metric.key}> | |||
<QualityGateCondition | |||
branchLike={branchLike} | |||
component={component} | |||
condition={condition} | |||
/> | |||
{isSimplifiedCondition(condition) ? ( | |||
<QualityGateSimplifiedCondition | |||
branchLike={branchLike} | |||
component={component} | |||
condition={condition} | |||
/> | |||
) : ( | |||
<QualityGateCondition | |||
branchLike={branchLike} | |||
component={component} | |||
condition={condition} | |||
/> | |||
)} | |||
<BasicSeparator /> | |||
</div> | |||
))} |
@@ -0,0 +1,88 @@ | |||
/* | |||
* 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 { Highlight, LinkBox } from 'design-system'; | |||
import * as React from 'react'; | |||
import { propsToIssueParams } from '../../../components/shared/utils'; | |||
import { getBranchLikeQuery } from '../../../helpers/branch-like'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { formatMeasure, isDiffMetric, localizeMetric } from '../../../helpers/measures'; | |||
import { getComponentIssuesUrl } from '../../../helpers/urls'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { MetricKey, MetricType } from '../../../types/metrics'; | |||
import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates'; | |||
import { Component } from '../../../types/types'; | |||
interface Props { | |||
branchLike?: BranchLike; | |||
component: Pick<Component, 'key'>; | |||
condition: QualityGateStatusConditionEnhanced; | |||
} | |||
export default function QualityGateSimplifiedCondition({ | |||
branchLike, | |||
component, | |||
condition, | |||
}: Readonly<Props>) { | |||
const getPrimaryText = () => { | |||
const { measure } = condition; | |||
const { metric } = measure; | |||
const isDiff = isDiffMetric(metric.key); | |||
const subText = | |||
!isDiff && condition.period != null | |||
? `${localizeMetric(metric.key)} ${translate('quality_gates.conditions.new_code')}` | |||
: localizeMetric(metric.key); | |||
return subText; | |||
}; | |||
const { measure } = condition; | |||
const { metric } = measure; | |||
const value = (condition.period ? measure.period?.value : measure.value) as string; | |||
const formattedValue = formatMeasure(value, MetricType.ShortInteger, { | |||
decimals: 0, | |||
omitExtraDecimalZeros: metric.type === MetricType.Percent, | |||
}); | |||
return ( | |||
<LinkBox | |||
to={getComponentIssuesUrl(component.key, { | |||
...propsToIssueParams(condition.measure.metric.key, condition.period != null), | |||
...getBranchLikeQuery(branchLike), | |||
})} | |||
> | |||
<div className="sw-flex sw-p-2 sw-items-baseline"> | |||
<Highlight className="sw-mx-4 sw-w-6 sw-my-0 sw-text-right">{formattedValue}</Highlight> | |||
<Highlight | |||
className="sw-text-ellipsis sw-pr-4" | |||
data-guiding-id={ | |||
metric.key === MetricKey.new_violations | |||
? 'overviewZeroNewIssuesSimplification' | |||
: undefined | |||
} | |||
> | |||
{getPrimaryText()} | |||
</Highlight> | |||
</div> | |||
</LinkBox> | |||
); | |||
} |
@@ -0,0 +1,89 @@ | |||
/* | |||
* 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 { SpotlightTour, SpotlightTourStep } from 'design-system'; | |||
import React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { dismissNotice } from '../../../api/users'; | |||
import { CurrentUserContext } from '../../../app/components/current-user/CurrentUserContext'; | |||
import Link from '../../../components/common/Link'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { QualityGate } from '../../../types/types'; | |||
import { NoticeType } from '../../../types/users'; | |||
interface Props { | |||
qualityGate: QualityGate; | |||
} | |||
export default function ZeroNewIssuesSimplificationGuide({ qualityGate }: Readonly<Props>) { | |||
const { currentUser, updateDismissedNotices } = React.useContext(CurrentUserContext); | |||
const shouldRun = | |||
currentUser.isLoggedIn && | |||
!currentUser.dismissedNotices[NoticeType.OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION]; | |||
const steps: SpotlightTourStep[] = [ | |||
{ | |||
target: `[data-guiding-id="overviewZeroNewIssuesSimplification"]`, | |||
content: ( | |||
<> | |||
<p className="sw-mb-4"> | |||
<FormattedMessage | |||
id="overview.quality_gates.conditions.condition_simplification_tour.content1" | |||
defaultMessage={translate( | |||
'overview.quality_gates.conditions.condition_simplification_tour.content1', | |||
)} | |||
values={{ | |||
link: ( | |||
<Link to={`/quality_gates/show/${qualityGate.name}`}> | |||
{translateWithParameters( | |||
'overview.quality_gates.conditions.condition_simplification_tour.content1.link', | |||
qualityGate.name, | |||
)} | |||
</Link> | |||
), | |||
}} | |||
/> | |||
</p> | |||
<p> | |||
{translate('overview.quality_gates.conditions.condition_simplification_tour.content2')} | |||
</p> | |||
</> | |||
), | |||
title: translate('overview.quality_gates.conditions.condition_simplification_tour.title'), | |||
placement: 'right', | |||
}, | |||
]; | |||
const onCallback = async (props: { action: string; type: string }) => { | |||
if (props.action === 'close' && props.type === 'tour:end' && shouldRun) { | |||
await dismissNotice(NoticeType.OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION); | |||
updateDismissedNotices(NoticeType.OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION, true); | |||
} | |||
}; | |||
return ( | |||
<SpotlightTour | |||
run={shouldRun} | |||
closeLabel={translate('dismiss')} | |||
callback={onCallback} | |||
steps={steps} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,67 @@ | |||
/* | |||
* 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 React from 'react'; | |||
import { mockQualityGateStatusConditionEnhanced } from '../../../../helpers/mocks/quality-gates'; | |||
import { mockMetric } from '../../../../helpers/testMocks'; | |||
import { renderComponent } from '../../../../helpers/testReactTestingUtils'; | |||
import { MetricKey, MetricType } from '../../../../types/metrics'; | |||
import { QualityGateStatusConditionEnhanced } from '../../../../types/quality-gates'; | |||
import QualityGateCondition from '../QualityGateCondition'; | |||
import QualityGateSimplifiedCondition from '../QualityGateSimplifiedCondition'; | |||
it('should show simplified condition', async () => { | |||
renderQualityGateCondition({ | |||
condition: quickMock(MetricKey.new_violations, MetricType.Integer), | |||
}); | |||
expect(await screen.findByText('metric.new_violations.name')).toBeInTheDocument(); | |||
}); | |||
function renderQualityGateCondition(props: Partial<QualityGateCondition['props']>) { | |||
return renderComponent( | |||
<QualityGateSimplifiedCondition | |||
component={{ key: 'abcd-key' }} | |||
condition={mockQualityGateStatusConditionEnhanced()} | |||
{...props} | |||
/>, | |||
); | |||
} | |||
function quickMock( | |||
metric: MetricKey, | |||
type = MetricType.Rating, | |||
addPeriod = false, | |||
value = '3', | |||
): QualityGateStatusConditionEnhanced { | |||
return mockQualityGateStatusConditionEnhanced({ | |||
error: '1', | |||
measure: { | |||
metric: mockMetric({ | |||
key: metric, | |||
name: metric, | |||
type, | |||
}), | |||
value, | |||
...(addPeriod ? { period: { value, index: 1 } } : {}), | |||
}, | |||
metric, | |||
...(addPeriod ? { period: 1 } : {}), | |||
}); | |||
} |
@@ -22,16 +22,18 @@ import { uniq } from 'lodash'; | |||
import * as React from 'react'; | |||
import { useEffect, useState } from 'react'; | |||
import { getMeasuresWithMetrics } from '../../../api/measures'; | |||
import { fetchQualityGate, getGateForProject } from '../../../api/quality-gates'; | |||
import { getBranchLikeQuery } from '../../../helpers/branch-like'; | |||
import { enhanceConditionWithMeasure, enhanceMeasuresWithMetrics } from '../../../helpers/measures'; | |||
import { isDefined } from '../../../helpers/types'; | |||
import { useBranchStatusQuery } from '../../../queries/branch'; | |||
import { PullRequest } from '../../../types/branch-like'; | |||
import { Component, MeasureEnhanced } from '../../../types/types'; | |||
import { Component, MeasureEnhanced, QualityGate } from '../../../types/types'; | |||
import MeasuresCardPanel from '../branches/MeasuresCardPanel'; | |||
import BranchQualityGate from '../components/BranchQualityGate'; | |||
import IgnoredConditionWarning from '../components/IgnoredConditionWarning'; | |||
import MetaTopBar from '../components/MetaTopBar'; | |||
import ZeroNewIssuesSimplificationGuide from '../components/ZeroNewIssuesSimplificationGuide'; | |||
import '../styles.css'; | |||
import { PR_METRICS, Status } from '../utils'; | |||
import SonarLintAd from './SonarLintAd'; | |||
@@ -43,14 +45,18 @@ interface Props { | |||
export default function PullRequestOverview(props: Props) { | |||
const { component, branchLike } = props; | |||
const [loadingMeasure, setLoadingMeasure] = useState(false); | |||
const [isLoadingMeasures, setIsLoadingMeasures] = useState(false); | |||
const [measures, setMeasures] = useState<MeasureEnhanced[]>([]); | |||
const { data: { conditions, ignoredConditions, status } = {}, isLoading } = | |||
useBranchStatusQuery(component); | |||
const loading = isLoading || loadingMeasure; | |||
const { | |||
data: { conditions, ignoredConditions, status } = {}, | |||
isLoading: isLoadingBranchStatusesData, | |||
} = useBranchStatusQuery(component); | |||
const [isLoadingQualityGate, setIsLoadingQualityGate] = useState(false); | |||
const [qualityGate, setQualityGate] = useState<QualityGate>(); | |||
const isLoading = isLoadingBranchStatusesData || isLoadingMeasures || isLoadingQualityGate; | |||
useEffect(() => { | |||
setLoadingMeasure(true); | |||
setIsLoadingMeasures(true); | |||
const metricKeys = | |||
conditions !== undefined | |||
@@ -64,17 +70,31 @@ export default function PullRequestOverview(props: Props) { | |||
getMeasuresWithMetrics(component.key, metricKeys, getBranchLikeQuery(branchLike)).then( | |||
({ component, metrics }) => { | |||
if (component.measures) { | |||
setLoadingMeasure(false); | |||
setIsLoadingMeasures(false); | |||
setMeasures(enhanceMeasuresWithMetrics(component.measures || [], metrics)); | |||
} | |||
}, | |||
() => { | |||
setLoadingMeasure(false); | |||
setIsLoadingMeasures(false); | |||
}, | |||
); | |||
}, [branchLike, component.key, conditions]); | |||
if (loading) { | |||
useEffect(() => { | |||
async function fetchQualityGateDate() { | |||
setIsLoadingQualityGate(true); | |||
const qualityGate = await getGateForProject({ project: component.key }); | |||
const qgDetails = await fetchQualityGate({ name: qualityGate.name }); | |||
setQualityGate(qgDetails); | |||
setIsLoadingQualityGate(false); | |||
} | |||
fetchQualityGateDate(); | |||
}, [component.key]); | |||
if (isLoading) { | |||
return ( | |||
<CenteredLayout> | |||
<div className="sw-p-6"> | |||
@@ -119,6 +139,8 @@ export default function PullRequestOverview(props: Props) { | |||
measures={measures} | |||
/> | |||
{qualityGate?.isBuiltIn && <ZeroNewIssuesSimplificationGuide qualityGate={qualityGate} />} | |||
<SonarLintAd status={status} /> | |||
</div> | |||
</div> |
@@ -20,11 +20,14 @@ | |||
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 { fetchQualityGate, getQualityGateProjectStatus } from '../../../../api/quality-gates'; | |||
import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider'; | |||
import { mockPullRequest } from '../../../../helpers/mocks/branch-like'; | |||
import { mockComponent } from '../../../../helpers/mocks/component'; | |||
import { mockQualityGateProjectCondition } from '../../../../helpers/mocks/quality-gates'; | |||
import { | |||
mockQualityGate, | |||
mockQualityGateProjectCondition, | |||
} from '../../../../helpers/mocks/quality-gates'; | |||
import { mockLoggedInUser, mockMeasure, mockMetric } from '../../../../helpers/testMocks'; | |||
import { renderComponent } from '../../../../helpers/testReactTestingUtils'; | |||
import { byLabelText, byRole } from '../../../../helpers/testSelector'; | |||
@@ -32,6 +35,7 @@ import { ComponentPropsType } from '../../../../helpers/testUtils'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import { MetricKey, MetricType } from '../../../../types/metrics'; | |||
import { CaycStatus } from '../../../../types/types'; | |||
import { NoticeType } from '../../../../types/users'; | |||
import PullRequestOverview from '../PullRequestOverview'; | |||
jest.mock('../../../../api/measures', () => { | |||
@@ -55,27 +59,25 @@ jest.mock('../../../../api/measures', () => { | |||
mockMeasure({ | |||
metric: MetricKey.new_lines, | |||
}), | |||
mockMeasure({ | |||
metric: MetricKey.new_violations, | |||
}), | |||
], | |||
}, | |||
metrics: [ | |||
mockMetric({ key: MetricKey.new_coverage }), | |||
mockMetric({ | |||
key: MetricKey.duplicated_lines, | |||
}), | |||
mockMetric({ key: MetricKey.duplicated_lines }), | |||
mockMetric({ key: MetricKey.new_lines, type: MetricType.ShortInteger }), | |||
mockMetric({ | |||
key: MetricKey.new_bugs, | |||
type: MetricType.Integer, | |||
}), | |||
mockMetric({ key: MetricKey.new_bugs, type: MetricType.Integer }), | |||
mockMetric({ key: MetricKey.new_violations }), | |||
], | |||
}), | |||
}; | |||
}); | |||
jest.mock('../../../../api/quality-gates', () => { | |||
const { mockQualityGateProjectStatus, mockQualityGateApplicationStatus } = jest.requireActual( | |||
'../../../../helpers/mocks/quality-gates', | |||
); | |||
const { mockQualityGateProjectStatus, mockQualityGateApplicationStatus, mockQualityGate } = | |||
jest.requireActual('../../../../helpers/mocks/quality-gates'); | |||
const { MetricKey } = jest.requireActual('../../../../types/metrics'); | |||
return { | |||
getQualityGateProjectStatus: jest.fn().mockResolvedValue( | |||
@@ -110,6 +112,8 @@ jest.mock('../../../../api/quality-gates', () => { | |||
}), | |||
), | |||
getApplicationQualityGate: jest.fn().mockResolvedValue(mockQualityGateApplicationStatus()), | |||
getGateForProject: jest.fn().mockResolvedValue(mockQualityGate({ isBuiltIn: true })), | |||
fetchQualityGate: jest.fn().mockResolvedValue(mockQualityGate({ isBuiltIn: true })), | |||
}; | |||
}); | |||
@@ -216,11 +220,68 @@ it('renders SL promotion', async () => { | |||
).not.toBeInTheDocument(); | |||
}); | |||
it('should render correctly 0 New issues onboarding', async () => { | |||
jest.mocked(getQualityGateProjectStatus).mockResolvedValueOnce({ | |||
status: 'ERROR', | |||
conditions: [ | |||
mockQualityGateProjectCondition({ | |||
status: 'ERROR', | |||
errorThreshold: '0', | |||
metricKey: MetricKey.new_violations, | |||
actualValue: '1', | |||
}), | |||
], | |||
caycStatus: CaycStatus.Compliant, | |||
ignoredConditions: false, | |||
}); | |||
jest.mocked(fetchQualityGate).mockResolvedValueOnce(mockQualityGate({ isBuiltIn: true })); | |||
renderPullRequestOverview(); | |||
expect( | |||
await byLabelText('overview.quality_gate_x.overview.gate.ERROR').find(), | |||
).toBeInTheDocument(); | |||
expect(await byRole('alertdialog').find()).toBeInTheDocument(); | |||
}); | |||
it('should not render 0 New issues onboarding when user dismissed it', async () => { | |||
jest.mocked(getQualityGateProjectStatus).mockResolvedValueOnce({ | |||
status: 'ERROR', | |||
conditions: [ | |||
mockQualityGateProjectCondition({ | |||
status: 'ERROR', | |||
errorThreshold: '0', | |||
metricKey: MetricKey.new_violations, | |||
actualValue: '1', | |||
}), | |||
], | |||
caycStatus: CaycStatus.Compliant, | |||
ignoredConditions: false, | |||
}); | |||
jest.mocked(fetchQualityGate).mockResolvedValueOnce(mockQualityGate({ isBuiltIn: true })); | |||
renderPullRequestOverview( | |||
{}, | |||
mockLoggedInUser({ | |||
dismissedNotices: { [NoticeType.OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION]: true }, | |||
}), | |||
); | |||
await waitFor(async () => | |||
expect( | |||
await byLabelText('overview.quality_gate_x.overview.gate.ERROR').find(), | |||
).toBeInTheDocument(), | |||
); | |||
expect(await byRole('alertdialog').query()).not.toBeInTheDocument(); | |||
}); | |||
function renderPullRequestOverview( | |||
props: Partial<ComponentPropsType<typeof PullRequestOverview>> = {}, | |||
currentUser = mockLoggedInUser(), | |||
) { | |||
renderComponent( | |||
<CurrentUserContextProvider currentUser={mockLoggedInUser()}> | |||
<CurrentUserContextProvider currentUser={currentUser}> | |||
<PullRequestOverview | |||
branchLike={mockPullRequest()} | |||
component={mockComponent({ |
@@ -22,6 +22,7 @@ import { SpotlightTour, SpotlightTourStep } from 'design-system'; | |||
import React from 'react'; | |||
import { dismissNotice } from '../../../api/users'; | |||
import { CurrentUserContext } from '../../../app/components/current-user/CurrentUserContext'; | |||
import DocumentationLink from '../../../components/common/DocumentationLink'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { NoticeType } from '../../../types/users'; | |||
@@ -32,23 +33,46 @@ export default function CaYCConditionsSimplificationGuide() { | |||
!currentUser.dismissedNotices[NoticeType.QG_CAYC_CONDITIONS_SIMPLIFICATION]; | |||
const steps: SpotlightTourStep[] = [ | |||
{ | |||
target: '[data-guiding-id="caycConditionsSimplification"]', | |||
content: ( | |||
<p>{translate('quality_gates.cayc.condition_simplification_tour.page_1.content1')}</p> | |||
), | |||
title: translate('quality_gates.cayc.condition_simplification_tour.page_1.title'), | |||
placement: 'right', | |||
}, | |||
{ | |||
target: '[data-guiding-id="caycConditionsSimplification"]', | |||
content: ( | |||
<> | |||
<p className="sw-mb-4"> | |||
{translate('quality_gates.cayc.condition_simplification_tour.page_2.content1')} | |||
</p> | |||
<p>{translate('quality_gates.cayc.condition_simplification_tour.page_2.content2')}</p> | |||
</> | |||
), | |||
title: translate('quality_gates.cayc.condition_simplification_tour.page_2.title'), | |||
placement: 'right', | |||
}, | |||
{ | |||
target: '[data-guiding-id="caycConditionsSimplification"]', | |||
content: ( | |||
<> | |||
<p className="sw-mb-4"> | |||
{translate('quality_gates.cayc.condition_simplification_tour.content1')} | |||
{translate('quality_gates.cayc.condition_simplification_tour.page_3.content1')} | |||
</p> | |||
<p>{translate('quality_gates.cayc.condition_simplification_tour.content2')}</p> | |||
<DocumentationLink to="/user-guide/issues/#resolutions"> | |||
{translate('quality_gates.cayc.condition_simplification_tour.page_3.content2')} | |||
</DocumentationLink> | |||
</> | |||
), | |||
title: translate('quality_gates.cayc.condition_simplification_tour.title'), | |||
title: translate('quality_gates.cayc.condition_simplification_tour.page_3.title'), | |||
placement: 'right', | |||
}, | |||
]; | |||
const onCallback = async (props: { action: string }) => { | |||
if (props.action === 'close' && shouldRun) { | |||
const onCallback = async (props: { action: string; type: string }) => { | |||
if (props.action === 'close' && props.type === 'tour:end' && shouldRun) { | |||
await dismissNotice(NoticeType.QG_CAYC_CONDITIONS_SIMPLIFICATION); | |||
updateDismissedNotices(NoticeType.QG_CAYC_CONDITIONS_SIMPLIFICATION, true); | |||
} | |||
@@ -56,6 +80,7 @@ export default function CaYCConditionsSimplificationGuide() { | |||
return ( | |||
<SpotlightTour | |||
continuous | |||
run={shouldRun} | |||
closeLabel={translate('dismiss')} | |||
callback={onCallback} |
@@ -426,6 +426,32 @@ it('should display CaYC condition simplification tour for users who didnt dismis | |||
expect(await byRole('alertdialog').find()).toBeInTheDocument(); | |||
expect( | |||
byRole('alertdialog') | |||
.byText('quality_gates.cayc.condition_simplification_tour.page_1.title') | |||
.get(), | |||
).toBeInTheDocument(); | |||
await act(async () => { | |||
await user.click(byRole('alertdialog').byRole('button', { name: 'next' }).get()); | |||
}); | |||
expect( | |||
byRole('alertdialog') | |||
.byText('quality_gates.cayc.condition_simplification_tour.page_2.title') | |||
.get(), | |||
).toBeInTheDocument(); | |||
await act(async () => { | |||
await user.click(byRole('alertdialog').byRole('button', { name: 'next' }).get()); | |||
}); | |||
expect( | |||
byRole('alertdialog') | |||
.byText('quality_gates.cayc.condition_simplification_tour.page_3.title') | |||
.get(), | |||
).toBeInTheDocument(); | |||
await act(async () => { | |||
await user.click(byRole('alertdialog').byRole('button', { name: 'dismiss' }).get()); | |||
}); |
@@ -34,6 +34,7 @@ export enum NoticeType { | |||
SONARLINT_AD = 'sonarlintAd', | |||
ISSUE_GUIDE = 'issueCleanCodeGuide', | |||
QG_CAYC_CONDITIONS_SIMPLIFICATION = 'qualityGateCaYCConditionsSimplification', | |||
OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION = 'overviewZeroNewIssuesSimplification', | |||
} | |||
export interface LoggedInUser extends CurrentUser, UserActive { |
@@ -19,9 +19,13 @@ | |||
*/ | |||
package org.sonar.server.user.ws; | |||
import com.tngtech.java.junit.dataprovider.DataProvider; | |||
import com.tngtech.java.junit.dataprovider.DataProviderRunner; | |||
import com.tngtech.java.junit.dataprovider.UseDataProvider; | |||
import java.util.Optional; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.runner.RunWith; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.property.PropertyDto; | |||
@@ -33,7 +37,9 @@ import org.sonar.server.ws.WsActionTester; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | |||
import static org.sonar.server.user.ws.DismissNoticeAction.AVAILABLE_NOTICE_KEYS; | |||
@RunWith(DataProviderRunner.class) | |||
public class DismissNoticeActionIT { | |||
@Rule | |||
@@ -43,48 +49,6 @@ public class DismissNoticeActionIT { | |||
private final WsActionTester tester = new WsActionTester(new DismissNoticeAction(userSessionRule, db.getDbClient())); | |||
@Test | |||
public void dismiss_educationPrinciples() { | |||
userSessionRule.logIn(); | |||
TestResponse testResponse = tester.newRequest() | |||
.setParam("notice", "educationPrinciples") | |||
.execute(); | |||
assertThat(testResponse.getStatus()).isEqualTo(204); | |||
Optional<PropertyDto> propertyDto = db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices.educationPrinciples"); | |||
assertThat(propertyDto).isPresent(); | |||
} | |||
@Test | |||
public void dismiss_sonarlintAd() { | |||
userSessionRule.logIn(); | |||
TestResponse testResponse = tester.newRequest() | |||
.setParam("notice", "sonarlintAd") | |||
.execute(); | |||
assertThat(testResponse.getStatus()).isEqualTo(204); | |||
Optional<PropertyDto> propertyDto = db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices.sonarlintAd"); | |||
assertThat(propertyDto).isPresent(); | |||
} | |||
@Test | |||
public void execute_whenNoticeIsIssueCleanCodeGuide_shouldDismissCorrespondingNotice() { | |||
userSessionRule.logIn(); | |||
TestResponse testResponse = tester.newRequest() | |||
.setParam("notice", "issueCleanCodeGuide") | |||
.execute(); | |||
assertThat(testResponse.getStatus()).isEqualTo(204); | |||
Optional<PropertyDto> propertyDto = db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices.issueCleanCodeGuide"); | |||
assertThat(propertyDto).isPresent(); | |||
} | |||
@Test | |||
public void authentication_is_required() { | |||
TestRequest testRequest = tester.newRequest() | |||
@@ -114,7 +78,8 @@ public class DismissNoticeActionIT { | |||
assertThatThrownBy(testRequest::execute) | |||
.isInstanceOf(IllegalArgumentException.class) | |||
.hasMessage( | |||
"Value of parameter 'notice' (not_supported_value) must be one of: [educationPrinciples, sonarlintAd, issueCleanCodeGuide, qualityGateCaYCConditionsSimplification]"); | |||
"Value of parameter 'notice' (not_supported_value) must be one of: [educationPrinciples, sonarlintAd, issueCleanCodeGuide, qualityGateCaYCConditionsSimplification, " + | |||
"overviewZeroNewIssuesSimplification]"); | |||
} | |||
@Test | |||
@@ -125,11 +90,32 @@ public class DismissNoticeActionIT { | |||
assertThat(db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices.educationPrinciples")).isPresent(); | |||
TestResponse testResponse = tester.newRequest() | |||
.setParam("notice", "sonarlintAd") | |||
.setParam("notice", "educationPrinciples") | |||
.execute(); | |||
assertThat(testResponse.getStatus()).isEqualTo(204); | |||
assertThat(db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices.educationPrinciples")).isPresent(); | |||
} | |||
@Test | |||
@UseDataProvider("noticeKeys") | |||
public void dismiss_notice(String noticeKey) { | |||
userSessionRule.logIn(); | |||
TestResponse testResponse = tester.newRequest() | |||
.setParam("notice", noticeKey) | |||
.execute(); | |||
assertThat(testResponse.getStatus()).isEqualTo(204); | |||
Optional<PropertyDto> propertyDto = db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices." + noticeKey); | |||
assertThat(propertyDto).isPresent(); | |||
} | |||
@DataProvider | |||
public static Object[][] noticeKeys() { | |||
return AVAILABLE_NOTICE_KEYS.stream() | |||
.map(noticeKey -> new Object[] {noticeKey}) | |||
.toArray(Object[][]::new); | |||
} | |||
} |
@@ -38,8 +38,10 @@ public class DismissNoticeAction implements UsersWsAction { | |||
private static final String SONARLINT_AD = "sonarlintAd"; | |||
private static final String ISSUE_CLEAN_CODE_GUIDE = "issueCleanCodeGuide"; | |||
private static final String QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION = "qualityGateCaYCConditionsSimplification"; | |||
private static final String OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION = "overviewZeroNewIssuesSimplification"; | |||
protected static final List<String> AVAILABLE_NOTICE_KEYS = List.of(EDUCATION_PRINCIPLES, SONARLINT_AD, ISSUE_CLEAN_CODE_GUIDE, QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION); | |||
protected static final List<String> AVAILABLE_NOTICE_KEYS = List.of(EDUCATION_PRINCIPLES, SONARLINT_AD, ISSUE_CLEAN_CODE_GUIDE, QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION, | |||
OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION); | |||
public static final String USER_DISMISS_CONSTANT = "user.dismissedNotices."; | |||
private final UserSession userSession; | |||
@@ -86,14 +88,12 @@ public class DismissNoticeAction implements UsersWsAction { | |||
.setKey(paramKey) | |||
.build(); | |||
if (!dbClient.propertiesDao().selectByQuery(query, dbSession).isEmpty()) { | |||
// already dismissed | |||
response.noContent(); | |||
if (dbClient.propertiesDao().selectByQuery(query, dbSession).isEmpty()) { | |||
PropertyDto property = new PropertyDto().setUserUuid(currentUserUuid).setKey(paramKey); | |||
dbClient.propertiesDao().saveProperty(dbSession, property); | |||
dbSession.commit(); | |||
} | |||
PropertyDto property = new PropertyDto().setUserUuid(currentUserUuid).setKey(paramKey); | |||
dbClient.propertiesDao().saveProperty(dbSession, property); | |||
dbSession.commit(); | |||
response.noContent(); | |||
} | |||
} |
@@ -2127,7 +2127,7 @@ quality_gates.copy=Copy Quality Gate | |||
quality_gates.cannot_set_default_no_cayc=You must make this quality gate Clean as You Code compliant to make this the default quality gate. | |||
quality_gates.cannot_copy_no_cayc=You must make this quality gate Clean as You Code compliant to copy. | |||
quality_gates.is_default_no_conditions=This is the default quality gate, but it has no configured conditions. Please configure at least 1 condition for this quality gate. | |||
quality_gates.is_built_in.description=The only Quality Gate you need to {link} | |||
quality_gates.is_built_in.description=The only quality gate you need to practice {link} | |||
quality_gates.conditions=Conditions | |||
quality_gates.conditions.help=Your project will fail the Quality Gate if it crosses any metric thresholds set for New Code or Overall Code. | |||
quality_gates.conditions.help.link=See also: Clean as You Code | |||
@@ -2215,9 +2215,14 @@ quality_gates.cayc.review_update_modal.header=Fix "{0}" to comply with Clean as | |||
quality_gates.cayc.review_update_modal.confirm_text=Fix Quality Gate | |||
quality_gates.cayc.review_update_modal.description1=This quality gate will be updated to comply with {cayc_link}. Please review the changes below. | |||
quality_gates.cayc.review_update_modal.description2=All other conditions will be preserved | |||
quality_gates.cayc.condition_simplification_tour.title=One condition, zero issues | |||
quality_gates.cayc.condition_simplification_tour.content1=One single condition ensures that new code has 0 issues. | |||
quality_gates.cayc.condition_simplification_tour.content2=This condition replaces the three conditions on Security rating, Reliability rating and Maintainability rating. | |||
quality_gates.cayc.condition_simplification_tour.page_1.title='Clean as You Code' ready! | |||
quality_gates.cayc.condition_simplification_tour.page_1.content1=The conditions in this quality gate have been updated to ensure that any code added or changed is clean. | |||
quality_gates.cayc.condition_simplification_tour.page_2.title=One condition, zero issues | |||
quality_gates.cayc.condition_simplification_tour.page_2.content1=One single condition ensures that new code has no issues. | |||
quality_gates.cayc.condition_simplification_tour.page_2.content2=This condition replaces the three conditions on Security rating, Reliability rating and Maintainability rating. | |||
quality_gates.cayc.condition_simplification_tour.page_3.title=Resolve pending issues | |||
quality_gates.cayc.condition_simplification_tour.page_3.content1=Starting now, every issue in new code must be resolved for a project to pass this quality gate. | |||
quality_gates.cayc.condition_simplification_tour.page_3.content2=Learn more: Issue resolutions | |||
quality_gates.cayc.new_maintainability_rating.A=Technical debt ratio is less than {0} | |||
quality_gates.cayc.new_maintainability_rating=Technical debt ratio is greater than {1} | |||
quality_gates.cayc.new_reliability_rating.A=No bugs | |||
@@ -3764,6 +3769,10 @@ overview.quality_gate.conditions.cayc.details=Fixing this quality gate will help | |||
overview.quality_gate.conditions.cayc.details_with_link=Fixing {link} will help you achieve a Clean Code state. | |||
overview.quality_gate.conditions.non_cayc.warning.link=this quality gate | |||
overview.quality_gate.conditions.cayc.link=Learn why | |||
overview.quality_gates.conditions.condition_simplification_tour.title=One condition, zero issues | |||
overview.quality_gates.conditions.condition_simplification_tour.content1=A new condition was introduced in {link} to ensure that new code has no issues. | |||
overview.quality_gates.conditions.condition_simplification_tour.content1.link={0} quality gate | |||
overview.quality_gates.conditions.condition_simplification_tour.content2=Starting now, every issue in new code must be resolved for a project to pass this quality gate. | |||
overview.quality_gate.application.non_cayc.projects_x={0} project(s) in this application use a Quality Gate that does not comply with Clean as You Code | |||
overview.quality_gate.show_project_conditions_x=Show failed conditions for project {0} | |||
overview.quality_gate.hide_project_conditions_x=Hide failed conditions for project {0} |