diff options
author | Andrey Luiz <andrey.luiz@sonarsource.com> | 2023-12-01 11:00:02 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-12-01 20:02:43 +0000 |
commit | 415ee3d300898c403eac83e697d88ad0ba457568 (patch) | |
tree | 327c921e263f39fb1bb34866fe4c123f7e7c8806 /server/sonar-web | |
parent | 47b21c6d7986ff06ad8dbbb0848753f72aeb316b (diff) | |
download | sonarqube-415ee3d300898c403eac83e697d88ad0ba457568.tar.gz sonarqube-415ee3d300898c403eac83e697d88ad0ba457568.zip |
SONAR-21012 Remove CaYC compliance check from front-end and rely only on the API (#10041)
Diffstat (limited to 'server/sonar-web')
20 files changed, 749 insertions, 989 deletions
diff --git a/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts index 5b4ce44f7e2..cfb2574674c 100644 --- a/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts @@ -129,7 +129,7 @@ export class QualityGatesServiceMock { id: 'AXJMbIUHPAOIsUIE3eOi', metric: 'new_security_hotspots_reviewed', op: 'LT', - error: '85', + error: '100', isCaycCondition: true, }, { @@ -568,6 +568,13 @@ export class QualityGatesServiceMock { this.qualityGateProjectStatus = mockQualityGateProjectStatus(status); }; + setCaycStatusForQualityGate = (name: string, caycStatus: CaycStatus) => { + const qg = this.list.find((q) => q.name === name); + if (qg) { + qg.caycStatus = caycStatus; + } + }; + reply<T>(response: T): Promise<T> { return Promise.resolve(cloneDeep(response)); } diff --git a/server/sonar-web/src/main/js/app/components/available-features/withAvailableFeatures.tsx b/server/sonar-web/src/main/js/app/components/available-features/withAvailableFeatures.tsx index bc6fdc07173..5a25ac62230 100644 --- a/server/sonar-web/src/main/js/app/components/available-features/withAvailableFeatures.tsx +++ b/server/sonar-web/src/main/js/app/components/available-features/withAvailableFeatures.tsx @@ -50,3 +50,11 @@ export default function withAvailableFeatures<P>( } }; } + +export function useAvailableFeatures() { + const availableFeatures = React.useContext(AvailableFeaturesContext); + + return { + hasFeature: (feature: Feature) => availableFeatures.includes(feature), + }; +} diff --git a/server/sonar-web/src/main/js/app/components/metrics/withMetricsContext.tsx b/server/sonar-web/src/main/js/app/components/metrics/withMetricsContext.tsx index ac6caa0a929..7c069e4ac36 100644 --- a/server/sonar-web/src/main/js/app/components/metrics/withMetricsContext.tsx +++ b/server/sonar-web/src/main/js/app/components/metrics/withMetricsContext.tsx @@ -45,3 +45,7 @@ export default function withMetricsContext<P>( } }; } + +export function useMetrics() { + return React.useContext(MetricsContext); +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/App.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/App.tsx index 526675bf4b5..b4292d9d236 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/App.tsx @@ -29,9 +29,9 @@ import { themeColor, } from 'design-system'; import * as React from 'react'; +import { useCallback, useEffect } from 'react'; import { Helmet } from 'react-helmet-async'; -import { NavigateFunction, useNavigate, useParams } from 'react-router-dom'; -import { fetchQualityGates } from '../../../api/quality-gates'; +import { useNavigate, useParams } from 'react-router-dom'; import Suggestions from '../../../components/embed-docs-modal/Suggestions'; import '../../../components/search-navigator.css'; import { translate, translateWithParameters } from '../../../helpers/l10n'; @@ -42,140 +42,93 @@ import { removeWhitePageClass, } from '../../../helpers/pages'; import { getQualityGateUrl } from '../../../helpers/urls'; +import { useQualityGatesQuery } from '../../../queries/quality-gates'; import { QualityGate } from '../../../types/types'; import '../styles.css'; import Details from './Details'; import List from './List'; import ListHeader from './ListHeader'; -interface Props { - name?: string; - navigate: NavigateFunction; -} - -interface State { - canCreate: boolean; - loading: boolean; - qualityGates: QualityGate[]; -} - -class App extends React.PureComponent<Props, State> { - mounted = false; - state: State = { canCreate: false, loading: true, qualityGates: [] }; - - componentDidMount() { - this.mounted = true; - this.fetchQualityGates(); +export default function App() { + const { data, isLoading } = useQualityGatesQuery(); + const { name } = useParams(); + const navigate = useNavigate(); + const { + qualitygates: qualityGates, + actions: { create: canCreate }, + } = data ?? { qualitygates: [], actions: { create: false } }; + + const openDefault = useCallback( + (qualityGates?: QualityGate[]) => { + if (!qualityGates || qualityGates.length === 0) { + return; + } + const defaultQualityGate = qualityGates.find((gate) => Boolean(gate.isDefault)); + if (!defaultQualityGate) { + return; + } + navigate(getQualityGateUrl(defaultQualityGate.name), { replace: true }); + }, + [navigate], + ); + + useEffect(() => { addWhitePageClass(); addSideBarClass(); - } - - componentDidUpdate(prevProps: Props) { - if (prevProps.name !== undefined && this.props.name === undefined) { - this.openDefault(this.state.qualityGates); - } - } - - componentWillUnmount() { - this.mounted = false; - removeWhitePageClass(); - removeSideBarClass(); - } - - fetchQualityGates = () => { - return fetchQualityGates().then( - ({ actions, qualitygates: qualityGates }) => { - if (this.mounted) { - this.setState({ canCreate: actions.create, loading: false, qualityGates }); - if (!this.props.name) { - this.openDefault(qualityGates); - } - } - }, - () => { - if (this.mounted) { - this.setState({ loading: false }); - } - }, - ); - }; + return () => { + removeWhitePageClass(); + removeSideBarClass(); + }; + }, []); - openDefault(qualityGates: QualityGate[]) { - const defaultQualityGate = qualityGates.find((gate) => Boolean(gate.isDefault))!; - this.props.navigate(getQualityGateUrl(defaultQualityGate.name), { replace: true }); - } - - handleSetDefault = (qualityGate: QualityGate) => { - this.setState(({ qualityGates }) => { - return { - qualityGates: qualityGates.map((candidate) => { - if (candidate.isDefault || candidate.name === qualityGate.name) { - return { ...candidate, isDefault: candidate.name === qualityGate.name }; - } - return candidate; - }), - }; - }); - }; - - render() { - const { name } = this.props; - const { canCreate, qualityGates } = this.state; - - return ( - <LargeCenteredLayout id="quality-gates-page"> - <PageContentFontWrapper className="sw-body-sm"> - <Helmet - defer={false} - titleTemplate={translateWithParameters( - 'page_title.template.with_category', - translate('quality_gates.page'), - )} - /> - <div className="sw-grid sw-gap-x-12 sw-gap-y-6 sw-grid-cols-12 sw-w-full"> - <Suggestions suggestions="quality_gates" /> - - <StyledContentWrapper - className="sw-col-span-3 sw-px-4 sw-py-6 sw-border-y-0 sw-rounded-0" + useEffect(() => { + if (!name) { + openDefault(qualityGates); + } + }, [name, openDefault, qualityGates]); + + return ( + <LargeCenteredLayout id="quality-gates-page"> + <PageContentFontWrapper className="sw-body-sm"> + <Helmet + defer={false} + titleTemplate={translateWithParameters( + 'page_title.template.with_category', + translate('quality_gates.page'), + )} + /> + <div className="sw-grid sw-gap-x-12 sw-gap-y-6 sw-grid-cols-12 sw-w-full"> + <Suggestions suggestions="quality_gates" /> + + <StyledContentWrapper + className="sw-col-span-3 sw-px-4 sw-py-6 sw-border-y-0 sw-rounded-0" + style={{ + height: `calc(100vh - ${LAYOUT_GLOBAL_NAV_HEIGHT + LAYOUT_FOOTER_HEIGHT}px)`, + }} + > + <ListHeader canCreate={canCreate} /> + <Spinner loading={isLoading}> + <List qualityGates={qualityGates} currentQualityGate={name} /> + </Spinner> + </StyledContentWrapper> + + {name !== undefined && ( + <div + className="sw-col-span-9 sw-overflow-y-auto" style={{ height: `calc(100vh - ${LAYOUT_GLOBAL_NAV_HEIGHT + LAYOUT_FOOTER_HEIGHT}px)`, }} > - <ListHeader canCreate={canCreate} refreshQualityGates={this.fetchQualityGates} /> - <Spinner loading={this.state.loading}> - <List qualityGates={qualityGates} currentQualityGate={name} /> - </Spinner> - </StyledContentWrapper> - - {name !== undefined && ( - <div - className="sw-col-span-9 sw-overflow-y-auto" - style={{ - height: `calc(100vh - ${LAYOUT_GLOBAL_NAV_HEIGHT + LAYOUT_FOOTER_HEIGHT}px)`, - }} - > - <StyledContentWrapper className="sw-my-12"> - <Details - qualityGateName={name} - onSetDefault={this.handleSetDefault} - refreshQualityGates={this.fetchQualityGates} - /> - </StyledContentWrapper> - </div> - )} - </div> - </PageContentFontWrapper> - </LargeCenteredLayout> - ); - } -} - -export default function AppWrapper() { - const params = useParams(); - const navigate = useNavigate(); - - return <App name={params['name']} navigate={navigate} />; + <StyledContentWrapper className="sw-my-12"> + <Details qualityGateName={name} /> + </StyledContentWrapper> + </div> + )} + </div> + </PageContentFontWrapper> + </LargeCenteredLayout> + ); } const StyledContentWrapper = withTheme(styled.div` diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx index 41bcdfc6558..2d9f5ce3ee6 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Condition.tsx @@ -17,7 +17,6 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import classNames from 'classnames'; import { ActionCell, ContentCell, @@ -32,185 +31,149 @@ import { TrashIcon, } from 'design-system'; import * as React from 'react'; -import { deleteCondition } from '../../../api/quality-gates'; -import withMetricsContext from '../../../app/components/metrics/withMetricsContext'; +import { useMetrics } from '../../../app/components/metrics/withMetricsContext'; import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n'; -import { - CaycStatus, - Condition as ConditionType, - Dict, - Metric, - QualityGate, -} from '../../../types/types'; +import { useDeleteConditionMutation } from '../../../queries/quality-gates'; +import { MetricType } from '../../../types/metrics'; +import { CaycStatus, Condition as ConditionType, Metric, QualityGate } from '../../../types/types'; import { getLocalizedMetricNameNoDiffMetric, isConditionWithFixedValue } from '../utils'; import ConditionModal from './ConditionModal'; import ConditionValue from './ConditionValue'; +export enum ConditionChange { + Added = 'added', + Updated = 'updated', + Deleted = 'deleted', +} + interface Props { condition: ConditionType; canEdit: boolean; metric: Metric; - onRemoveCondition: (Condition: ConditionType) => void; - onSaveCondition: (newCondition: ConditionType, oldCondition: ConditionType) => void; qualityGate: QualityGate; - updated?: boolean; - metrics: Dict<Metric>; showEdit?: boolean; isCaycModal?: boolean; } -interface State { - deleteFormOpen: boolean; - modal: boolean; -} - -export class ConditionComponent extends React.PureComponent<Props, State> { - constructor(props: Props) { - super(props); - this.state = { - deleteFormOpen: false, - modal: false, - }; - } - - handleUpdateCondition = (newCondition: ConditionType) => { - this.props.onSaveCondition(newCondition, this.props.condition); - }; - - handleOpenUpdate = () => { - this.setState({ modal: true }); - }; - - handleUpdateClose = () => { - this.setState({ modal: false }); +export default function ConditionComponent({ + condition, + canEdit, + metric, + qualityGate, + showEdit, + isCaycModal, +}: Readonly<Props>) { + const [deleteFormOpen, setDeleteFormOpen] = React.useState(false); + const [modal, setModal] = React.useState(false); + const { mutateAsync: deleteCondition } = useDeleteConditionMutation(qualityGate.name); + const metrics = useMetrics(); + + const handleOpenUpdate = () => { + setModal(true); }; - handleDeleteClick = () => { - this.setState({ deleteFormOpen: true }); + const handleUpdateClose = () => { + setModal(false); }; - closeDeleteForm = () => { - this.setState({ deleteFormOpen: false }); + const handleDeleteClick = () => { + setDeleteFormOpen(true); }; - removeCondition = (condition: ConditionType) => { - deleteCondition({ id: condition.id }).then( - () => this.props.onRemoveCondition(condition), - () => {}, - ); + const closeDeleteForm = () => { + setDeleteFormOpen(false); }; - renderOperator() { - // TODO can operator be missing? - const { op = 'GT' } = this.props.condition; - return this.props.metric.type === 'RATING' + const renderOperator = () => { + const { op = 'GT' } = condition; + return metric.type === MetricType.Rating ? translate('quality_gates.operator', op, 'rating') : translate('quality_gates.operator', op); - } - - render() { - const { - condition, - canEdit, - metric, - qualityGate, - updated, - metrics, - showEdit = true, - isCaycModal = false, - } = this.props; - - const isCaycCompliantAndOverCompliant = qualityGate.caycStatus !== CaycStatus.NonCompliant; - - return ( - <TableRow className={classNames({ highlighted: updated })}> - <ContentCell> - {getLocalizedMetricNameNoDiffMetric(metric, metrics)} - {metric.hidden && <TextError className="sw-ml-1" text={translate('deprecated')} />} - </ContentCell> - - <ContentCell className="sw-whitespace-nowrap">{this.renderOperator()}</ContentCell> + }; - <NumericalCell className="sw-whitespace-nowrap"> - <ConditionValue - metric={metric} - isCaycModal={isCaycModal} - condition={condition} - isCaycCompliantAndOverCompliant={isCaycCompliantAndOverCompliant} - /> - </NumericalCell> - <ActionCell> - {!isCaycModal && canEdit && ( - <> - {(!isCaycCompliantAndOverCompliant || - !isConditionWithFixedValue(condition) || - (isCaycCompliantAndOverCompliant && showEdit)) && ( - <> - <InteractiveIcon - Icon={PencilIcon} - aria-label={translateWithParameters( - 'quality_gates.condition.edit', - metric.name, - )} - data-test="quality-gates__condition-update" - onClick={this.handleOpenUpdate} - className="sw-mr-4" - size="small" + const isCaycCompliantAndOverCompliant = qualityGate.caycStatus !== CaycStatus.NonCompliant; + + return ( + <TableRow> + <ContentCell> + {getLocalizedMetricNameNoDiffMetric(metric, metrics)} + {metric.hidden && <TextError className="sw-ml-1" text={translate('deprecated')} />} + </ContentCell> + + <ContentCell className="sw-whitespace-nowrap">{renderOperator()}</ContentCell> + + <NumericalCell className="sw-whitespace-nowrap"> + <ConditionValue + metric={metric} + isCaycModal={isCaycModal} + condition={condition} + isCaycCompliantAndOverCompliant={isCaycCompliantAndOverCompliant} + /> + </NumericalCell> + <ActionCell> + {!isCaycModal && canEdit && ( + <> + {(!isCaycCompliantAndOverCompliant || + !isConditionWithFixedValue(condition) || + (isCaycCompliantAndOverCompliant && showEdit)) && ( + <> + <InteractiveIcon + Icon={PencilIcon} + aria-label={translateWithParameters('quality_gates.condition.edit', metric.name)} + data-test="quality-gates__condition-update" + onClick={handleOpenUpdate} + className="sw-mr-4" + size="small" + /> + {modal && ( + <ConditionModal + condition={condition} + header={translate('quality_gates.update_condition')} + metric={metric} + onClose={handleUpdateClose} + qualityGate={qualityGate} /> - {this.state.modal && ( - <ConditionModal - condition={condition} - header={translate('quality_gates.update_condition')} - metric={metric} - onAddCondition={this.handleUpdateCondition} - onClose={this.handleUpdateClose} - qualityGate={qualityGate} - /> + )} + </> + )} + {(!isCaycCompliantAndOverCompliant || + !condition.isCaycCondition || + (isCaycCompliantAndOverCompliant && showEdit)) && ( + <> + <DestructiveIcon + Icon={TrashIcon} + aria-label={translateWithParameters( + 'quality_gates.condition.delete', + metric.name, )} - </> - )} - {(!isCaycCompliantAndOverCompliant || - !condition.isCaycCondition || - (isCaycCompliantAndOverCompliant && showEdit)) && ( - <> - <DestructiveIcon - Icon={TrashIcon} - aria-label={translateWithParameters( - 'quality_gates.condition.delete', - metric.name, + onClick={handleDeleteClick} + size="small" + /> + {deleteFormOpen && ( + <Modal + headerTitle={translate('quality_gates.delete_condition')} + onClose={closeDeleteForm} + body={translateWithParameters( + 'quality_gates.delete_condition.confirm.message', + getLocalizedMetricName(metric), )} - data-test="quality-gates__condition-delete" - onClick={this.handleDeleteClick} - size="small" + primaryButton={ + <DangerButtonPrimary + autoFocus + type="submit" + onClick={() => deleteCondition(condition)} + > + {translate('delete')} + </DangerButtonPrimary> + } + secondaryButtonLabel={translate('close')} /> - {this.state.deleteFormOpen && ( - <Modal - headerTitle={translate('quality_gates.delete_condition')} - onClose={this.closeDeleteForm} - body={translateWithParameters( - 'quality_gates.delete_condition.confirm.message', - getLocalizedMetricName(this.props.metric), - )} - primaryButton={ - <DangerButtonPrimary - autoFocus - type="submit" - onClick={() => this.removeCondition(condition)} - > - {translate('delete')} - </DangerButtonPrimary> - } - secondaryButtonLabel={translate('close')} - /> - )} - </> - )} - </> - )} - </ActionCell> - </TableRow> - ); - } + )} + </> + )} + </> + )} + </ActionCell> + </TableRow> + ); } - -export default withMetricsContext(ConditionComponent); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionModal.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionModal.tsx index e2bfb237257..91e110d823e 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionModal.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionModal.tsx @@ -17,11 +17,14 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { ButtonPrimary, FlagMessage, FormField, Modal, RadioButton } from 'design-system'; +import { ButtonPrimary, FormField, Modal, RadioButton } from 'design-system'; import * as React from 'react'; -import { createCondition, updateCondition } from '../../../api/quality-gates'; import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; import { isDiffMetric } from '../../../helpers/measures'; +import { + useCreateConditionMutation, + useUpdateConditionMutation, +} from '../../../queries/quality-gates'; import { Condition, Metric, QualityGate } from '../../../types/types'; import { getPossibleOperators } from '../utils'; import ConditionOperator from './ConditionOperator'; @@ -33,101 +36,92 @@ interface Props { metric?: Metric; metrics?: Metric[]; header: string; - onAddCondition: (condition: Condition) => void; onClose: () => void; qualityGate: QualityGate; } -interface State { - error: string; - errorMessage?: string; - metric?: Metric; - op?: string; - scope: 'new' | 'overall'; -} - const ADD_CONDITION_MODAL_ID = 'add-condition-modal'; -export default class ConditionModal extends React.PureComponent<Props, State> { - constructor(props: Props) { - super(props); - this.state = { - error: props.condition ? props.condition.error : '', - scope: 'new', - metric: props.metric ? props.metric : undefined, - op: props.condition ? props.condition.op : undefined, - }; - } - - getSinglePossibleOperator(metric: Metric) { +export default function ConditionModal({ + condition, + metric, + metrics, + header, + onClose, + qualityGate, +}: Readonly<Props>) { + const [errorThreshold, setErrorThreshold] = React.useState(condition ? condition.error : ''); + const [scope, setScope] = React.useState<'new' | 'overall'>('new'); + const [selectedMetric, setSelectedMetric] = React.useState<Metric | undefined>(metric); + const [selectedOperator, setSelectedOperator] = React.useState<string | undefined>( + condition ? condition.op : undefined, + ); + const { mutateAsync: createCondition } = useCreateConditionMutation(qualityGate.name); + const { mutateAsync: updateCondition } = useUpdateConditionMutation(qualityGate.name); + + const getSinglePossibleOperator = (metric: Metric) => { const operators = getPossibleOperators(metric); return Array.isArray(operators) ? undefined : operators; - } + }; - handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => { + const handleFormSubmit = async (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); - const { condition, qualityGate } = this.props; - const newCondition: Omit<Condition, 'id'> = { - metric: this.state.metric!.key, - op: this.getSinglePossibleOperator(this.state.metric!) || this.state.op, - error: this.state.error, - }; - const submitPromise = condition - ? updateCondition({ id: condition.id, ...newCondition }) - : createCondition({ gateName: qualityGate.name, ...newCondition }); - return submitPromise.then(this.props.onAddCondition).then(this.props.onClose); + if (selectedMetric) { + const newCondition: Omit<Condition, 'id'> = { + metric: selectedMetric.key, + op: getSinglePossibleOperator(selectedMetric) ?? selectedOperator, + error: errorThreshold, + }; + const submitPromise = condition + ? updateCondition({ id: condition.id, ...newCondition }) + : createCondition(newCondition); + await submitPromise; + onClose(); + } }; - handleScopeChange = (scope: 'new' | 'overall') => { - this.setState(({ metric }) => { - const { metrics } = this.props; - let correspondingMetric; + const handleScopeChange = (scope: 'new' | 'overall') => { + let correspondingMetric; - if (metric && metrics) { - const correspondingMetricKey = - scope === 'new' ? `new_${metric.key}` : metric.key.replace(/^new_/, ''); - correspondingMetric = metrics.find((m) => m.key === correspondingMetricKey); - } + if (selectedMetric && metrics) { + const correspondingMetricKey = + scope === 'new' ? `new_${selectedMetric.key}` : selectedMetric.key.replace(/^new_/, ''); + correspondingMetric = metrics.find((m) => m.key === correspondingMetricKey); + } - return { scope, metric: correspondingMetric }; - }); + setScope(scope); + setSelectedMetric(correspondingMetric); }; - handleMetricChange = (metric: Metric) => { - this.setState({ metric, op: undefined, error: '' }); + const handleMetricChange = (metric: Metric) => { + setSelectedMetric(metric); + setSelectedOperator(undefined); + setErrorThreshold(''); }; - handleOperatorChange = (op: string) => { - this.setState({ op }); + const handleOperatorChange = (op: string) => { + setSelectedOperator(op); }; - handleErrorChange = (error: string) => { - this.setState({ error }); + const handleErrorChange = (error: string) => { + setErrorThreshold(error); }; - renderBody = () => { - const { metrics } = this.props; - const { op, error, scope, metric, errorMessage } = this.state; - + const renderBody = () => { return ( - <form id={ADD_CONDITION_MODAL_ID} onSubmit={this.handleFormSubmit}> - {errorMessage && ( - <FlagMessage className="sw-mb-2" variant="error"> - {errorMessage} - </FlagMessage> - )} - {this.props.metric === undefined && ( + <form id={ADD_CONDITION_MODAL_ID} onSubmit={handleFormSubmit}> + {metric === undefined && ( <FormField label={translate('quality_gates.conditions.where')}> <div className="sw-flex sw-gap-4"> - <RadioButton checked={scope === 'new'} onCheck={this.handleScopeChange} value="new"> + <RadioButton checked={scope === 'new'} onCheck={handleScopeChange} value="new"> <span data-test="quality-gates__condition-scope-new"> {translate('quality_gates.conditions.new_code')} </span> </RadioButton> <RadioButton checked={scope === 'overall'} - onCheck={this.handleScopeChange} + onCheck={handleScopeChange} value="overall" > <span data-test="quality-gates__condition-scope-overall"> @@ -139,22 +133,22 @@ export default class ConditionModal extends React.PureComponent<Props, State> { )} <FormField - description={this.props.metric && getLocalizedMetricName(this.props.metric)} + description={metric && getLocalizedMetricName(metric)} htmlFor="condition-metric" label={translate('quality_gates.conditions.fails_when')} > {metrics && ( <MetricSelect - metric={metric} + metric={selectedMetric} metricsArray={metrics.filter((m) => scope === 'new' ? isDiffMetric(m.key) : !isDiffMetric(m.key), )} - onMetricChange={this.handleMetricChange} + onMetricChange={handleMetricChange} /> )} </FormField> - {metric && ( + {selectedMetric && ( <div className="sw-flex sw-gap-2"> <FormField className="sw-mb-0" @@ -162,9 +156,9 @@ export default class ConditionModal extends React.PureComponent<Props, State> { label={translate('quality_gates.conditions.operator')} > <ConditionOperator - metric={metric} - onOperatorChange={this.handleOperatorChange} - op={op} + metric={selectedMetric} + onOperatorChange={handleOperatorChange} + op={selectedOperator} /> </FormField> <FormField @@ -172,10 +166,10 @@ export default class ConditionModal extends React.PureComponent<Props, State> { label={translate('quality_gates.conditions.value')} > <ThresholdInput - metric={metric} + metric={selectedMetric} name="error" - onChange={this.handleErrorChange} - value={error} + onChange={handleErrorChange} + value={errorThreshold} /> </FormField> </div> @@ -184,29 +178,25 @@ export default class ConditionModal extends React.PureComponent<Props, State> { ); }; - render() { - const { header } = this.props; - const { metric } = this.state; - return ( - <Modal - isScrollable={false} - isOverflowVisible - headerTitle={header} - onClose={this.props.onClose} - body={this.renderBody()} - primaryButton={ - <ButtonPrimary - autoFocus - disabled={metric === undefined} - id="add-condition-button" - form={ADD_CONDITION_MODAL_ID} - type="submit" - > - {header} - </ButtonPrimary> - } - secondaryButtonLabel={translate('close')} - /> - ); - } + return ( + <Modal + isScrollable={false} + isOverflowVisible + headerTitle={header} + onClose={onClose} + body={renderBody()} + primaryButton={ + <ButtonPrimary + autoFocus + disabled={selectedMetric === undefined} + id="add-condition-button" + form={ADD_CONDITION_MODAL_ID} + type="submit" + > + {header} + </ButtonPrimary> + } + secondaryButtonLabel={translate('close')} + /> + ); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionReviewAndUpdateModal.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionReviewAndUpdateModal.tsx index 63b9d78ca14..879d26deb32 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionReviewAndUpdateModal.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionReviewAndUpdateModal.tsx @@ -21,39 +21,27 @@ import { ButtonPrimary, Link, Modal, SubHeading, Title } from 'design-system'; import { sortBy } from 'lodash'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { createCondition, updateCondition } from '../../../api/quality-gates'; import { useDocUrl } from '../../../helpers/docs'; import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { useFixQualityGateMutation } from '../../../queries/quality-gates'; import { Condition, Dict, Metric, QualityGate } from '../../../types/types'; -import { getCorrectCaycCondition, getWeakMissingAndNonCaycConditions } from '../utils'; +import { getWeakMissingAndNonCaycConditions } from '../utils'; import ConditionsTable from './ConditionsTable'; interface Props { canEdit: boolean; metrics: Dict<Metric>; - updatedConditionId?: string; conditions: Condition[]; scope: 'new' | 'overall' | 'new-cayc'; onClose: () => void; - onAddCondition: (condition: Condition) => void; - onRemoveCondition: (condition: Condition) => void; - onSaveCondition: (newCondition: Condition, oldCondition: Condition) => void; lockEditing: () => void; qualityGate: QualityGate; isOptimizing?: boolean; } export default function CaycReviewUpdateConditionsModal(props: Readonly<Props>) { - const { - conditions, - qualityGate, - metrics, - onSaveCondition, - onAddCondition, - lockEditing, - onClose, - isOptimizing, - } = props; + const { conditions, qualityGate, metrics, lockEditing, onClose, isOptimizing } = props; + const { mutateAsync: fixQualityGate } = useFixQualityGateMutation(qualityGate.name); const { weakConditions, missingConditions } = getWeakMissingAndNonCaycConditions(conditions); const sortedWeakConditions = sortBy( @@ -68,43 +56,10 @@ export default function CaycReviewUpdateConditionsModal(props: Readonly<Props>) const getDocUrl = useDocUrl(); - const updateCaycQualityGate = React.useCallback(() => { - const promiseArr: Promise<Condition | undefined | void>[] = []; - const { weakConditions, missingConditions } = getWeakMissingAndNonCaycConditions(conditions); - - weakConditions.forEach((condition) => { - promiseArr.push( - updateCondition({ - ...getCorrectCaycCondition(condition), - id: condition.id, - }) - .then((resultCondition) => { - const currentCondition = conditions.find((con) => con.metric === condition.metric); - if (currentCondition) { - onSaveCondition({ ...resultCondition, isCaycCondition: true }, currentCondition); - } - }) - .catch(() => undefined), - ); - }); - - missingConditions.forEach((condition) => { - promiseArr.push( - createCondition({ - ...getCorrectCaycCondition(condition), - gateName: qualityGate.name, - }) - .then((resultCondition) => { - onAddCondition({ ...resultCondition, isCaycCondition: true }); - }) - .catch(() => undefined), - ); - }); - - return Promise.all(promiseArr).then(() => { - lockEditing(); - }); - }, [conditions, qualityGate, lockEditing, onAddCondition, onSaveCondition]); + const updateCaycQualityGate = React.useCallback(async () => { + await fixQualityGate({ weakConditions, missingConditions }); + lockEditing(); + }, [fixQualityGate, weakConditions, missingConditions, lockEditing]); const body = ( <div className="sw-mb-10"> diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx index c3b9ec5c052..32782668497 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx @@ -31,23 +31,15 @@ import { import { differenceWith, map, uniqBy } from 'lodash'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import withAvailableFeatures, { - WithAvailableFeaturesProps, -} from '../../../app/components/available-features/withAvailableFeatures'; -import withMetricsContext from '../../../app/components/metrics/withMetricsContext'; +import { useAvailableFeatures } from '../../../app/components/available-features/withAvailableFeatures'; +import { useMetrics } from '../../../app/components/metrics/withMetricsContext'; import DocumentationTooltip from '../../../components/common/DocumentationTooltip'; import ModalButton, { ModalProps } from '../../../components/controls/ModalButton'; import { useDocUrl } from '../../../helpers/docs'; import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; import { Feature } from '../../../types/features'; import { MetricKey } from '../../../types/metrics'; -import { - CaycStatus, - Condition as ConditionType, - Dict, - Metric, - QualityGate, -} from '../../../types/types'; +import { CaycStatus, Condition as ConditionType, QualityGate } from '../../../types/types'; import { groupAndSortByPriorityConditions, isQualityGateOptimized } from '../utils'; import CaYCConditionsSimplificationGuide from './CaYCConditionsSimplificationGuide'; import CaycCompliantBanner from './CaycCompliantBanner'; @@ -57,13 +49,8 @@ import ConditionModal from './ConditionModal'; import CaycReviewUpdateConditionsModal from './ConditionReviewAndUpdateModal'; import ConditionsTable from './ConditionsTable'; -interface Props extends WithAvailableFeaturesProps { - metrics: Dict<Metric>; - onAddCondition: (condition: ConditionType) => void; - onRemoveCondition: (Condition: ConditionType) => void; - onSaveCondition: (newCondition: ConditionType, oldCondition: ConditionType) => void; +interface Props { qualityGate: QualityGate; - updatedConditionId?: string; } const FORBIDDEN_METRIC_TYPES = ['DATA', 'DISTRIB', 'STRING', 'BOOL']; @@ -74,19 +61,12 @@ const FORBIDDEN_METRICS: string[] = [ MetricKey.new_security_hotspots, ]; -function Conditions({ - qualityGate, - metrics, - onRemoveCondition, - onSaveCondition, - onAddCondition, - hasFeature, - updatedConditionId, -}: Readonly<Props>) { +export default function Conditions({ qualityGate }: Readonly<Props>) { const [editing, setEditing] = React.useState<boolean>( qualityGate.caycStatus === CaycStatus.NonCompliant, ); const { name } = qualityGate; + const metrics = useMetrics(); const canEdit = Boolean(qualityGate.actions?.manageConditions); const { conditions = [] } = qualityGate; const existingConditions = conditions.filter((condition) => metrics[condition.metric]); @@ -101,6 +81,7 @@ function Conditions({ duplicates.push(condition); } }); + const { hasFeature } = useAvailableFeatures(); const uniqDuplicates = uniqBy(duplicates, (d) => d.metric).map((condition) => ({ ...condition, @@ -130,13 +111,12 @@ function Conditions({ <ConditionModal header={translate('quality_gates.add_condition')} metrics={availableMetrics} - onAddCondition={onAddCondition} onClose={onClose} qualityGate={qualityGate} /> ); }, - [metrics, qualityGate, onAddCondition], + [metrics, qualityGate], ); const getDocUrl = useDocUrl(); @@ -153,11 +133,7 @@ function Conditions({ qualityGate={qualityGate} metrics={metrics} canEdit={canEdit} - onRemoveCondition={onRemoveCondition} - onSaveCondition={onSaveCondition} - onAddCondition={onAddCondition} lockEditing={() => setEditing(false)} - updatedConditionId={updatedConditionId} conditions={conditions} scope="new-cayc" onClose={onClose} @@ -165,15 +141,7 @@ function Conditions({ /> ); }, - [ - qualityGate, - metrics, - updatedConditionId, - onAddCondition, - onRemoveCondition, - onSaveCondition, - isOptimizing, - ], + [qualityGate, metrics, isOptimizing], ); return ( @@ -267,9 +235,6 @@ function Conditions({ qualityGate={qualityGate} metrics={metrics} canEdit={canEdit} - onRemoveCondition={onRemoveCondition} - onSaveCondition={onSaveCondition} - updatedConditionId={updatedConditionId} conditions={newCodeConditions} showEdit={editing} scope="new" @@ -293,9 +258,6 @@ function Conditions({ qualityGate={qualityGate} metrics={metrics} canEdit={canEdit} - onRemoveCondition={onRemoveCondition} - onSaveCondition={onSaveCondition} - updatedConditionId={updatedConditionId} conditions={overallCodeConditions} scope="overall" /> @@ -338,5 +300,3 @@ function Conditions({ </div> ); } - -export default withMetricsContext(withAvailableFeatures(Conditions)); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionsTable.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionsTable.tsx index b062ed10504..63e7aaee9ae 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionsTable.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/ConditionsTable.tsx @@ -26,14 +26,11 @@ import Condition from './Condition'; interface Props { canEdit: boolean; metrics: Dict<Metric>; - onRemoveCondition: (Condition: ConditionType) => void; - onSaveCondition: (newCondition: ConditionType, oldCondition: ConditionType) => void; qualityGate: QualityGate; - updatedConditionId?: string; conditions: ConditionType[]; scope: 'new' | 'overall' | 'new-cayc'; - isCaycModal?: boolean; showEdit?: boolean; + isCaycModal?: boolean; } function Header() { @@ -63,9 +60,6 @@ export default function ConditionsTable({ qualityGate, metrics, canEdit, - onRemoveCondition, - onSaveCondition, - updatedConditionId, scope, conditions, isCaycModal, @@ -86,10 +80,7 @@ export default function ConditionsTable({ condition={condition} key={condition.id} metric={metrics[condition.metric]} - onRemoveCondition={onRemoveCondition} - onSaveCondition={onSaveCondition} qualityGate={qualityGate} - updated={condition.id === updatedConditionId} isCaycModal={isCaycModal} showEdit={showEdit} /> diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx index 13193152b68..d7317466b4d 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx @@ -19,87 +19,69 @@ */ import { ButtonPrimary, FormField, InputField, Modal } from 'design-system'; import * as React from 'react'; -import { copyQualityGate } from '../../../api/quality-gates'; -import { Router, withRouter } from '../../../components/hoc/withRouter'; +import { useRouter } from '../../../components/hoc/withRouter'; import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation'; import { translate } from '../../../helpers/l10n'; import { getQualityGateUrl } from '../../../helpers/urls'; +import { useCopyQualityGateMutation } from '../../../queries/quality-gates'; import { QualityGate } from '../../../types/types'; interface Props { onClose: () => void; - onCopy: () => Promise<void>; qualityGate: QualityGate; - router: Router; -} - -interface State { - name: string; } const FORM_ID = 'rename-quality-gate'; -export class CopyQualityGateForm extends React.PureComponent<Props, State> { - constructor(props: Props) { - super(props); - this.state = { name: props.qualityGate.name }; - } +export default function CopyQualityGateForm({ qualityGate, onClose }: Readonly<Props>) { + const [name, setName] = React.useState(qualityGate.name); + const { mutateAsync: copyQualityGate } = useCopyQualityGateMutation(qualityGate.name); + const router = useRouter(); - handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => { - this.setState({ name: event.currentTarget.value }); + const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => { + setName(event.currentTarget.value); }; - handleCopy = (event: React.FormEvent) => { + const handleCopy = async (event: React.FormEvent) => { event.preventDefault(); - const { qualityGate } = this.props; - const { name } = this.state; - - return copyQualityGate({ sourceName: qualityGate.name, name }).then((newQualityGate) => { - this.props.onCopy(); - this.props.router.push(getQualityGateUrl(newQualityGate.name)); - }); + const newQualityGate = await copyQualityGate(name); + router.push(getQualityGateUrl(newQualityGate.name)); }; - render() { - const { qualityGate } = this.props; - const { name } = this.state; - const buttonDisabled = !name || (qualityGate && qualityGate.name === name); + const buttonDisabled = !name || (qualityGate && qualityGate.name === name); - return ( - <Modal - headerTitle={translate('quality_gates.copy')} - onClose={this.props.onClose} - body={ - <form id={FORM_ID} onSubmit={this.handleCopy}> - <MandatoryFieldsExplanation /> - <FormField - label={translate('name')} - htmlFor="quality-gate-form-name" - required - className="sw-my-2" - > - <InputField - autoFocus - id="quality-gate-form-name" - maxLength={100} - onChange={this.handleNameChange} - size="auto" - type="text" - value={name} - /> - </FormField> - </form> - } - primaryButton={ - <ButtonPrimary autoFocus type="submit" disabled={buttonDisabled} form={FORM_ID}> - {translate('copy')} - </ButtonPrimary> - } - secondaryButtonLabel={translate('cancel')} - /> - ); - } + return ( + <Modal + headerTitle={translate('quality_gates.copy')} + onClose={onClose} + body={ + <form id={FORM_ID} onSubmit={handleCopy}> + <MandatoryFieldsExplanation /> + <FormField + label={translate('name')} + htmlFor="quality-gate-form-name" + required + className="sw-my-2" + > + <InputField + autoFocus + id="quality-gate-form-name" + maxLength={100} + onChange={handleNameChange} + size="auto" + type="text" + value={name} + /> + </FormField> + </form> + } + primaryButton={ + <ButtonPrimary autoFocus type="submit" disabled={buttonDisabled} form={FORM_ID}> + {translate('copy')} + </ButtonPrimary> + } + secondaryButtonLabel={translate('cancel')} + /> + ); } - -export default withRouter(CopyQualityGateForm); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx index 92b6c630ade..176f5689fee 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx @@ -19,92 +19,79 @@ */ import { ButtonSecondary, FormField, InputField, Modal } from 'design-system'; import * as React from 'react'; -import { createQualityGate } from '../../../api/quality-gates'; -import { Router, withRouter } from '../../../components/hoc/withRouter'; +import { useRouter } from '../../../components/hoc/withRouter'; import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation'; import { translate } from '../../../helpers/l10n'; import { getQualityGateUrl } from '../../../helpers/urls'; +import { useCreateQualityGateMutation } from '../../../queries/quality-gates'; interface Props { onClose: () => void; - onCreate: () => Promise<void>; - router: Router; } -interface State { - name: string; -} - -export class CreateQualityGateForm extends React.PureComponent<Props, State> { - state: State = { name: '' }; - - handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => { - this.setState({ name: event.currentTarget.value }); - }; +export default function CreateQualityGateForm({ onClose }: Readonly<Props>) { + const [name, setName] = React.useState(''); + const { mutateAsync: createQualityGate } = useCreateQualityGateMutation(); + const router = useRouter(); - handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { - event.preventDefault(); - this.handleCreate(); + const handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => { + setName(event.currentTarget.value); }; - handleCreate = async () => { - const { name } = this.state; - + const handleCreate = async () => { if (name !== undefined) { - const qualityGate = await createQualityGate({ name }); - await this.props.onCreate(); - this.props.onClose(); - this.props.router.push(getQualityGateUrl(qualityGate.name)); + const qualityGate = await createQualityGate(name); + onClose(); + router.push(getQualityGateUrl(qualityGate.name)); } }; - render() { - const { name } = this.state; + const handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { + event.preventDefault(); + handleCreate(); + }; - const body = ( - <form onSubmit={this.handleFormSubmit}> - <MandatoryFieldsExplanation className="modal-field" /> - <FormField - htmlFor="quality-gate-form-name" - label={translate('name')} - required - requiredAriaLabel={translate('field_required')} - > - <InputField - className="sw-mb-1" - autoComplete="off" - id="quality-gate-form-name" - maxLength={256} - name="key" - onChange={this.handleNameChange} - type="text" - size="full" - value={name} - /> - </FormField> - </form> - ); + const body = ( + <form onSubmit={handleFormSubmit}> + <MandatoryFieldsExplanation className="modal-field" /> + <FormField + htmlFor="quality-gate-form-name" + label={translate('name')} + required + requiredAriaLabel={translate('field_required')} + > + <InputField + className="sw-mb-1" + autoComplete="off" + id="quality-gate-form-name" + maxLength={256} + name="key" + onChange={handleNameChange} + type="text" + size="full" + value={name} + /> + </FormField> + </form> + ); - return ( - <Modal - onClose={this.props.onClose} - headerTitle={translate('quality_gates.create')} - isScrollable - body={body} - primaryButton={ - <ButtonSecondary - disabled={name === null || name === ''} - form="create-application-form" - type="submit" - onClick={this.handleCreate} - > - {translate('quality_gate.create')} - </ButtonSecondary> - } - secondaryButtonLabel={translate('cancel')} - /> - ); - } + return ( + <Modal + onClose={onClose} + headerTitle={translate('quality_gates.create')} + isScrollable + body={body} + primaryButton={ + <ButtonSecondary + disabled={name === null || name === ''} + form="create-application-form" + type="submit" + onClick={handleCreate} + > + {translate('quality_gate.create')} + </ButtonSecondary> + } + secondaryButtonLabel={translate('cancel')} + /> + ); } - -export default withRouter(CreateQualityGateForm); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx index a022e111554..13ed58276b1 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DeleteQualityGateForm.tsx @@ -19,46 +19,37 @@ */ import { DangerButtonPrimary, Modal } from 'design-system'; import * as React from 'react'; -import { deleteQualityGate } from '../../../api/quality-gates'; -import { Router, withRouter } from '../../../components/hoc/withRouter'; +import { useRouter } from '../../../components/hoc/withRouter'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { getQualityGatesUrl } from '../../../helpers/urls'; +import { useDeleteQualityGateMutation } from '../../../queries/quality-gates'; import { QualityGate } from '../../../types/types'; interface Props { - readonly onClose: () => void; - onDelete: () => Promise<void>; + onClose: () => void; qualityGate: QualityGate; - router: Router; } -export class DeleteQualityGateForm extends React.PureComponent<Props> { - onDelete = () => { - const { qualityGate } = this.props; - return deleteQualityGate({ name: qualityGate.name }) - .then(this.props.onDelete) - .then(() => { - this.props.router.push(getQualityGatesUrl()); - }); - }; +export default function DeleteQualityGateForm({ qualityGate, onClose }: Readonly<Props>) { + const { mutateAsync: deleteQualityGate } = useDeleteQualityGateMutation(qualityGate.name); + const router = useRouter(); - render() { - const { qualityGate } = this.props; + const onDelete = async () => { + await deleteQualityGate(); + router.push(getQualityGatesUrl()); + }; - return ( - <Modal - headerTitle={translate('quality_gates.delete')} - onClose={this.props.onClose} - body={translateWithParameters('quality_gates.delete.confirm.message', qualityGate.name)} - primaryButton={ - <DangerButtonPrimary autoFocus type="submit" onClick={this.onDelete}> - {translate('delete')} - </DangerButtonPrimary> - } - secondaryButtonLabel={translate('cancel')} - /> - ); - } + return ( + <Modal + headerTitle={translate('quality_gates.delete')} + onClose={onClose} + body={translateWithParameters('quality_gates.delete.confirm.message', qualityGate.name)} + primaryButton={ + <DangerButtonPrimary autoFocus type="submit" onClick={onDelete}> + {translate('delete')} + </DangerButtonPrimary> + } + secondaryButtonLabel={translate('cancel')} + /> + ); } - -export default withRouter(DeleteQualityGateForm); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx index e54437d6ad0..e34dc06d2fc 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Details.tsx @@ -19,160 +19,30 @@ */ import { Spinner } from 'design-system'; -import { clone } from 'lodash'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; -import { fetchQualityGate } from '../../../api/quality-gates'; -import { addGlobalSuccessMessage } from '../../../helpers/globalMessages'; -import { translate } from '../../../helpers/l10n'; -import { Condition, QualityGate } from '../../../types/types'; -import { addCondition, deleteCondition, replaceCondition } from '../utils'; +import { useQualityGateQuery } from '../../../queries/quality-gates'; import DetailsContent from './DetailsContent'; import DetailsHeader from './DetailsHeader'; interface Props { qualityGateName: string; - onSetDefault: (qualityGate: QualityGate) => void; - refreshQualityGates: () => Promise<void>; } -interface State { - loading: boolean; - qualityGate?: QualityGate; - updatedConditionId?: string; -} - -export default class Details extends React.PureComponent<Props, State> { - mounted = false; - state: State = { loading: true }; - - componentDidMount() { - this.mounted = true; - this.fetchDetails(); - } - - componentDidUpdate(prevProps: Props) { - if (prevProps.qualityGateName !== this.props.qualityGateName) { - this.fetchDetails(); - } - } - - componentWillUnmount() { - this.mounted = false; - } - - fetchDetails = () => { - const { qualityGateName } = this.props; - - this.setState({ loading: true }); - return fetchQualityGate({ name: qualityGateName }).then( - (qualityGate) => { - if (this.mounted) { - this.setState({ loading: false, qualityGate, updatedConditionId: undefined }); - } - }, - () => { - if (this.mounted) { - this.setState({ loading: false }); - } - }, - ); - }; - - handleAddCondition = (condition: Condition) => { - this.setState(({ qualityGate }) => { - if (!qualityGate) { - return null; - } - addGlobalSuccessMessage(translate('quality_gates.condition_added')); - - const updatedQualityGate = addCondition(clone(qualityGate), condition); - if (qualityGate.caycStatus !== updatedQualityGate.caycStatus) { - this.props.refreshQualityGates(); - } - - return { - qualityGate: updatedQualityGate, - updatedConditionId: condition.id, - }; - }); - }; - - handleSaveCondition = (newCondition: Condition, oldCondition: Condition) => { - this.setState(({ qualityGate }) => { - if (!qualityGate) { - return null; - } - addGlobalSuccessMessage(translate('quality_gates.condition_updated')); - const updatedQualityGate = replaceCondition(clone(qualityGate), newCondition, oldCondition); - if (qualityGate.caycStatus !== updatedQualityGate.caycStatus) { - this.props.refreshQualityGates(); - } - return { - qualityGate: updatedQualityGate, - updatedConditionId: newCondition.id, - }; - }); - }; - - handleRemoveCondition = (condition: Condition) => { - this.setState(({ qualityGate }) => { - if (!qualityGate) { - return null; - } - addGlobalSuccessMessage(translate('quality_gates.condition_deleted')); - const updatedQualityGate = deleteCondition(clone(qualityGate), condition); - if (qualityGate.caycStatus !== updatedQualityGate.caycStatus) { - this.props.refreshQualityGates(); - } - return { - qualityGate: updatedQualityGate, - updatedConditionId: undefined, - }; - }); - }; - - handleSetDefault = () => { - this.setState(({ qualityGate }) => { - if (!qualityGate) { - return null; - } - this.props.onSetDefault(qualityGate); - const newQualityGate: QualityGate = { - ...qualityGate, - actions: { ...qualityGate.actions, delete: false, setAsDefault: false }, - }; - return { qualityGate: newQualityGate }; - }); - }; - - render() { - const { refreshQualityGates } = this.props; - const { loading, qualityGate, updatedConditionId } = this.state; - - return ( - <main className="layout-page-main"> - <Spinner loading={loading}> - {qualityGate && ( - <> - <Helmet defer={false} title={qualityGate.name} /> - <DetailsHeader - onSetDefault={this.handleSetDefault} - qualityGate={qualityGate} - refreshItem={this.fetchDetails} - refreshList={refreshQualityGates} - /> - <DetailsContent - onAddCondition={this.handleAddCondition} - onRemoveCondition={this.handleRemoveCondition} - onSaveCondition={this.handleSaveCondition} - qualityGate={qualityGate} - updatedConditionId={updatedConditionId} - /> - </> - )} - </Spinner> - </main> - ); - } +export default function Details({ qualityGateName }: Readonly<Props>) { + const { data: qualityGate, isLoading } = useQualityGateQuery(qualityGateName); + + return ( + <main className="layout-page-main"> + <Spinner loading={isLoading}> + {qualityGate && ( + <> + <Helmet defer={false} title={qualityGate.name} /> + <DetailsHeader qualityGate={qualityGate} /> + <DetailsContent qualityGate={qualityGate} /> + </> + )} + </Spinner> + </main> + ); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx index b65ddc0e4d4..ec87de8cf81 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsContent.tsx @@ -21,21 +21,17 @@ import { FlagMessage, HelperHintIcon, SubTitle } from 'design-system'; import * as React from 'react'; import DocumentationTooltip from '../../../components/common/DocumentationTooltip'; import { translate } from '../../../helpers/l10n'; -import { Condition, QualityGate } from '../../../types/types'; +import { QualityGate } from '../../../types/types'; import Conditions from './Conditions'; import Projects from './Projects'; import QualityGatePermissions from './QualityGatePermissions'; export interface DetailsContentProps { - onAddCondition: (condition: Condition) => void; - onRemoveCondition: (Condition: Condition) => void; - onSaveCondition: (newCondition: Condition, oldCondition: Condition) => void; qualityGate: QualityGate; - updatedConditionId?: string; } export function DetailsContent(props: DetailsContentProps) { - const { qualityGate, updatedConditionId } = props; + const { qualityGate } = props; const actions = qualityGate.actions || {}; return ( @@ -47,13 +43,7 @@ export function DetailsContent(props: DetailsContentProps) { </FlagMessage> )} - <Conditions - onAddCondition={props.onAddCondition} - onRemoveCondition={props.onRemoveCondition} - onSaveCondition={props.onSaveCondition} - qualityGate={qualityGate} - updatedConditionId={updatedConditionId} - /> + <Conditions qualityGate={qualityGate} /> <div className="sw-mt-10"> <div className="sw-flex sw-flex-col"> diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx index 8a80d6826c7..d07397a9d70 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx @@ -33,10 +33,10 @@ import { import { countBy } from 'lodash'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { setQualityGateAsDefault } from '../../../api/quality-gates'; import DocumentationLink from '../../../components/common/DocumentationLink'; import Tooltip from '../../../components/controls/Tooltip'; import { translate } from '../../../helpers/l10n'; +import { useSetQualityGateAsDefaultMutation } from '../../../queries/quality-gates'; import { CaycStatus, QualityGate } from '../../../types/types'; import BuiltInQualityGateBadge from './BuiltInQualityGateBadge'; import CaycBadgeTooltip from './CaycBadgeTooltip'; @@ -45,20 +45,12 @@ import DeleteQualityGateForm from './DeleteQualityGateForm'; import RenameQualityGateForm from './RenameQualityGateForm'; interface Props { - onSetDefault: () => void; qualityGate: QualityGate; - refreshItem: () => Promise<void>; - refreshList: () => Promise<void>; } const TOOLTIP_MOUSE_LEAVE_DELAY = 0.3; -export default function DetailsHeader({ - refreshItem, - refreshList, - onSetDefault, - qualityGate, -}: Readonly<Props>) { +export default function DetailsHeader({ qualityGate }: Readonly<Props>) { const [isRenameFormOpen, setIsRenameFormOpen] = React.useState(false); const [isCopyFormOpen, setIsCopyFormOpen] = React.useState(false); const [isRemoveFormOpen, setIsRemoveFormOpen] = React.useState(false); @@ -70,22 +62,13 @@ export default function DetailsHeader({ actions.setAsDefault, ])['true']; const canEdit = Boolean(actions?.manageConditions); - - const handleActionRefresh = () => { - return Promise.all([refreshItem(), refreshList()]).then( - () => {}, - () => {}, - ); - }; + const { mutateAsync: setQualityGateAsDefault } = useSetQualityGateAsDefaultMutation( + qualityGate.name, + ); const handleSetAsDefaultClick = () => { if (!qualityGate.isDefault) { - // Optimistic update - onSetDefault(); - setQualityGateAsDefault({ name: qualityGate.name }).then( - handleActionRefresh, - handleActionRefresh, - ); + setQualityGateAsDefault({ name: qualityGate.name }); } }; @@ -237,23 +220,17 @@ export default function DetailsHeader({ {isRenameFormOpen && ( <RenameQualityGateForm onClose={() => setIsRenameFormOpen(false)} - onRename={handleActionRefresh} qualityGate={qualityGate} /> )} {isCopyFormOpen && ( - <CopyQualityGateForm - onClose={() => setIsCopyFormOpen(false)} - onCopy={handleActionRefresh} - qualityGate={qualityGate} - /> + <CopyQualityGateForm onClose={() => setIsCopyFormOpen(false)} qualityGate={qualityGate} /> )} {isRemoveFormOpen && ( <DeleteQualityGateForm onClose={() => setIsRemoveFormOpen(false)} - onDelete={refreshList} qualityGate={qualityGate} /> )} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx index 609eb5a2c21..c0548308dd1 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx @@ -26,17 +26,12 @@ import CreateQualityGateForm from './CreateQualityGateForm'; interface Props { canCreate: boolean; - refreshQualityGates: () => Promise<void>; } -function CreateQualityGateModal({ - refreshQualityGates, -}: Readonly<Pick<Props, 'refreshQualityGates'>>) { +function CreateQualityGateModal() { const renderModal = React.useCallback( - ({ onClose }: ModalProps) => ( - <CreateQualityGateForm onClose={onClose} onCreate={refreshQualityGates} /> - ), - [refreshQualityGates], + ({ onClose }: ModalProps) => <CreateQualityGateForm onClose={onClose} />, + [], ); return ( @@ -52,7 +47,7 @@ function CreateQualityGateModal({ ); } -export default function ListHeader({ canCreate, refreshQualityGates }: Readonly<Props>) { +export default function ListHeader({ canCreate }: Readonly<Props>) { return ( <div className="sw-flex sw-justify-between sw-pb-4"> <div className="sw-flex sw-justify-between"> @@ -72,7 +67,7 @@ export default function ListHeader({ canCreate, refreshQualityGates }: Readonly< <HelperHintIcon /> </DocumentationTooltip> </div> - {canCreate && <CreateQualityGateModal refreshQualityGates={refreshQualityGates} />} + {canCreate && <CreateQualityGateModal />} </div> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/RenameQualityGateForm.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/RenameQualityGateForm.tsx index 9d2c26bf662..6fc406d4a9b 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/RenameQualityGateForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/RenameQualityGateForm.tsx @@ -19,86 +19,69 @@ */ import { ButtonPrimary, FormField, InputField, Modal } from 'design-system/lib'; import * as React from 'react'; -import { renameQualityGate } from '../../../api/quality-gates'; -import { WithRouterProps, withRouter } from '../../../components/hoc/withRouter'; +import { useRouter } from '../../../components/hoc/withRouter'; import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation'; import { translate } from '../../../helpers/l10n'; import { getQualityGateUrl } from '../../../helpers/urls'; +import { useRenameQualityGateMutation } from '../../../queries/quality-gates'; import { QualityGate } from '../../../types/types'; -interface Props extends WithRouterProps { +interface Props { onClose: () => void; - onRename: () => Promise<void>; qualityGate: QualityGate; } -interface State { - name: string; -} - const FORM_ID = 'rename-quality-gate'; -class RenameQualityGateForm extends React.PureComponent<Props, State> { - constructor(props: Props) { - super(props); - this.state = { name: props.qualityGate.name }; - } +export default function RenameQualityGateForm({ qualityGate, onClose }: Readonly<Props>) { + const [name, setName] = React.useState(qualityGate.name); + const { mutateAsync: renameQualityGate } = useRenameQualityGateMutation(qualityGate.name); + const router = useRouter(); - handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => { - this.setState({ name: event.currentTarget.value }); + const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => { + setName(event.currentTarget.value); }; - handleRename = (event: React.FormEvent) => { + const handleRename = async (event: React.FormEvent) => { event.preventDefault(); - const { qualityGate, router } = this.props; - const { name } = this.state; - - return renameQualityGate({ currentName: qualityGate.name, name }).then(() => { - router.push(getQualityGateUrl(name)); - this.props.onRename(); - }); + await renameQualityGate(name); + router.push(getQualityGateUrl(name)); }; - render() { - const { qualityGate } = this.props; - const { name } = this.state; - const confirmDisable = !name || (qualityGate && qualityGate.name === name); + const confirmDisable = !name || (qualityGate && qualityGate.name === name); - return ( - <Modal - headerTitle={translate('quality_gates.rename')} - onClose={this.props.onClose} - body={ - <form id={FORM_ID} onSubmit={this.handleRename}> - <MandatoryFieldsExplanation /> - <FormField - label={translate('name')} - htmlFor="quality-gate-form-name" - required - className="sw-my-2" - > - <InputField - autoFocus - id="quality-gate-form-name" - maxLength={100} - onChange={this.handleNameChange} - size="auto" - type="text" - value={name} - /> - </FormField> - </form> - } - primaryButton={ - <ButtonPrimary autoFocus type="submit" disabled={confirmDisable} form={FORM_ID}> - {translate('rename')} - </ButtonPrimary> - } - secondaryButtonLabel={translate('cancel')} - /> - ); - } + return ( + <Modal + headerTitle={translate('quality_gates.rename')} + onClose={onClose} + body={ + <form id={FORM_ID} onSubmit={handleRename}> + <MandatoryFieldsExplanation /> + <FormField + label={translate('name')} + htmlFor="quality-gate-form-name" + required + className="sw-my-2" + > + <InputField + autoFocus + id="quality-gate-form-name" + maxLength={100} + onChange={handleNameChange} + size="auto" + type="text" + value={name} + /> + </FormField> + </form> + } + primaryButton={ + <ButtonPrimary autoFocus type="submit" disabled={confirmDisable} form={FORM_ID}> + {translate('rename')} + </ButtonPrimary> + } + secondaryButtonLabel={translate('cancel')} + /> + ); } - -export default withRouter(RenameQualityGateForm); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx index f6e563cc422..90602910f36 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx @@ -28,6 +28,7 @@ import { mockLoggedInUser } from '../../../../helpers/testMocks'; import { RenderContext, renderAppRoutes } from '../../../../helpers/testReactTestingUtils'; import { byRole } from '../../../../helpers/testSelector'; import { Feature } from '../../../../types/features'; +import { CaycStatus } from '../../../../types/types'; import { NoticeType } from '../../../../types/users'; import routes from '../../routes'; @@ -140,7 +141,7 @@ it('should be able to copy a quality gate which is CAYC compliant', async () => const notDefaultQualityGate = await screen.findByText('Sonar way'); await user.click(notDefaultQualityGate); - await user.click(screen.getByLabelText('menu')); + await user.click(await screen.findByLabelText('menu')); const copyButton = screen.getByRole('menuitem', { name: 'copy' }); await user.click(copyButton); @@ -160,7 +161,7 @@ it('should not be able to copy a quality gate which is not CAYC compliant', asyn const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily'); await user.click(notDefaultQualityGate); - await user.click(screen.getByLabelText('menu')); + await user.click(await screen.findByLabelText('menu')); const copyButton = screen.getByRole('menuitem', { name: 'copy' }); expect(copyButton).toBeDisabled(); @@ -189,7 +190,7 @@ it('should not be able to set as default a quality gate which is not CAYC compli const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily'); await user.click(notDefaultQualityGate); - await user.click(screen.getByLabelText('menu')); + await user.click(await screen.findByLabelText('menu')); const setAsDefaultButton = screen.getByRole('menuitem', { name: 'set_as_default' }); expect(setAsDefaultButton).toBeDisabled(); }); @@ -201,10 +202,10 @@ it('should be able to set as default a quality gate which is CAYC compliant', as const notDefaultQualityGate = await screen.findByRole('button', { name: /Sonar way/ }); await user.click(notDefaultQualityGate); - await user.click(screen.getByLabelText('menu')); + await user.click(await screen.findByLabelText('menu')); const setAsDefaultButton = screen.getByRole('menuitem', { name: 'set_as_default' }); await user.click(setAsDefaultButton); - expect(screen.getByRole('button', { name: /Sonar way default/ })).toBeInTheDocument(); + expect(await screen.findByRole('button', { name: /Sonar way default/ })).toBeInTheDocument(); }); it('should be able to add a condition', async () => { @@ -343,7 +344,7 @@ it('should show warning banner when CAYC condition is not properly set and shoul await user.click(qualityGate); - expect(screen.getByText('quality_gates.cayc_missing.banner.title')).toBeInTheDocument(); + expect(await screen.findByText('quality_gates.cayc_missing.banner.title')).toBeInTheDocument(); expect(screen.getByText('quality_gates.cayc_missing.banner.description')).toBeInTheDocument(); expect( screen.getByRole('button', { name: 'quality_gates.cayc_condition.review_update' }), @@ -367,6 +368,8 @@ it('should show warning banner when CAYC condition is not properly set and shoul screen.getByRole('button', { name: 'quality_gates.cayc.review_update_modal.confirm_text' }), ).toBeInTheDocument(); + qualityGateHandler.setCaycStatusForQualityGate('SonarSource way - CFamily', CaycStatus.Compliant); + await user.click( screen.getByRole('button', { name: 'quality_gates.cayc.review_update_modal.confirm_text' }), ); @@ -388,7 +391,7 @@ it('should show optimize banner when CAYC condition is not properly set and QG i await user.click(qualityGate); - expect(screen.getByText('quality_gates.cayc_optimize.banner.title')).toBeInTheDocument(); + expect(await screen.findByText('quality_gates.cayc_optimize.banner.title')).toBeInTheDocument(); expect(screen.getByText('quality_gates.cayc_optimize.banner.description')).toBeInTheDocument(); expect( screen.getByRole('button', { name: 'quality_gates.cayc_condition.review_optimize' }), @@ -559,7 +562,7 @@ describe('The Project section', () => { await user.click(notDefaultQualityGate); // by default it shows "selected" values - expect(screen.getAllByRole('checkbox')).toHaveLength(2); + expect(await screen.findAllByRole('checkbox')).toHaveLength(2); // change tabs to show deselected projects await user.click(screen.getByRole('radio', { name: 'quality_gates.projects.without' })); @@ -579,8 +582,8 @@ describe('The Project section', () => { await user.click(notDefaultQualityGate); + expect(await screen.findAllByRole('checkbox')).toHaveLength(2); const checkedProjects = screen.getAllByRole('checkbox')[0]; - expect(screen.getAllByRole('checkbox')).toHaveLength(2); await user.click(checkedProjects); const reloadButton = screen.getByRole('button', { name: 'reload' }); expect(reloadButton).toBeInTheDocument(); @@ -610,7 +613,7 @@ describe('The Project section', () => { await user.click(notDefaultQualityGate); - const searchInput = screen.getByRole('searchbox', { name: 'search_verb' }); + const searchInput = await screen.findByRole('searchbox', { name: 'search_verb' }); expect(searchInput).toBeInTheDocument(); await user.click(searchInput); await user.keyboard('test2{Enter}'); @@ -621,7 +624,7 @@ describe('The Project section', () => { }); it('should display show more button if there are multiple pages of data', async () => { - (searchProjects as jest.Mock).mockResolvedValueOnce({ + jest.mocked(searchProjects).mockResolvedValueOnce({ paging: { pageIndex: 2, pageSize: 3, total: 55 }, results: [], }); @@ -633,7 +636,7 @@ describe('The Project section', () => { const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily'); await user.click(notDefaultQualityGate); - expect(screen.getByRole('button', { name: 'show_more' })).toBeInTheDocument(); + expect(await screen.findByRole('button', { name: 'show_more' })).toBeInTheDocument(); }); }); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/utils.ts b/server/sonar-web/src/main/js/apps/quality-gates/utils.ts index 3376cbb2267..a986757f42a 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/utils.ts +++ b/server/sonar-web/src/main/js/apps/quality-gates/utils.ts @@ -253,64 +253,6 @@ export function getCorrectCaycCondition(condition: Condition) { return OPTIMIZED_CAYC_CONDITIONS[conditionMetric]; } -export function addCondition(qualityGate: QualityGate, condition: Condition): QualityGate { - const oldConditions = qualityGate.conditions || []; - const conditions = [...oldConditions, condition]; - if (conditions) { - qualityGate.caycStatus = updateCaycCompliantStatus(conditions); - } - return { ...qualityGate, conditions }; -} - -export function deleteCondition(qualityGate: QualityGate, condition: Condition): QualityGate { - const conditions = - qualityGate.conditions && qualityGate.conditions.filter((candidate) => candidate !== condition); - if (conditions) { - qualityGate.caycStatus = updateCaycCompliantStatus(conditions); - } - return { ...qualityGate, conditions }; -} - -export function replaceCondition( - qualityGate: QualityGate, - newCondition: Condition, - oldCondition: Condition, -): QualityGate { - const conditions = - qualityGate.conditions && - qualityGate.conditions.map((candidate) => { - return candidate === oldCondition ? newCondition : candidate; - }); - if (conditions) { - qualityGate.caycStatus = updateCaycCompliantStatus(conditions); - } - - return { ...qualityGate, conditions }; -} - -function updateCaycCompliantStatus(conditions: Condition[]) { - const isCompliantOptimized = Object.values(OPTIMIZED_CAYC_CONDITIONS).every((condition) => { - const foundCondition = conditions.find((c) => c.metric === condition.metric); - return ( - foundCondition && - !isWeakCondition(condition.metric as OptimizedCaycMetricKeys, foundCondition) - ); - }); - const isCompliantUnoptimized = Object.values(UNOPTIMIZED_CAYC_CONDITIONS).every((condition) => { - const foundCondition = conditions.find((c) => c.metric === condition.metric); - return ( - foundCondition && - !isWeakCondition(condition.metric as UnoptimizedCaycMetricKeys, foundCondition) - ); - }); - - if (isCompliantOptimized || isCompliantUnoptimized) { - return CaycStatus.Compliant; - } - - return CaycStatus.NonCompliant; -} - export function getPossibleOperators(metric: Metric) { if (metric.direction === 1) { return 'LT'; @@ -328,8 +270,8 @@ function getNoDiffMetric(metric: Metric, metrics: Dict<Metric>) { const regularMetricKey = metric.key.replace(/^new_/, ''); if (isDiffMetric(metric.key) && metricKeyExists(regularMetricKey, metrics)) { return metrics[regularMetricKey]; - } else if (metric.key === 'new_maintainability_rating') { - return metrics['sqale_rating'] || metric; + } else if (metric.key === MetricKey.new_maintainability_rating) { + return metrics[MetricKey.sqale_rating] || metric; } return metric; } diff --git a/server/sonar-web/src/main/js/queries/quality-gates.ts b/server/sonar-web/src/main/js/queries/quality-gates.ts new file mode 100644 index 00000000000..79fc0968b02 --- /dev/null +++ b/server/sonar-web/src/main/js/queries/quality-gates.ts @@ -0,0 +1,209 @@ +/* + * 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 { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { + copyQualityGate, + createCondition, + createQualityGate, + deleteCondition, + deleteQualityGate, + fetchQualityGate, + fetchQualityGates, + renameQualityGate, + setQualityGateAsDefault, + updateCondition, +} from '../api/quality-gates'; +import { getCorrectCaycCondition } from '../apps/quality-gates/utils'; +import { addGlobalSuccessMessage } from '../helpers/globalMessages'; +import { translate } from '../helpers/l10n'; +import { Condition, QualityGate } from '../types/types'; + +const QUALITY_GATE_KEY = 'quality-gate'; +const QUALITY_GATES_KEY = 'quality-gates'; + +export function useQualityGateQuery(name: string) { + return useQuery({ + queryKey: [QUALITY_GATE_KEY, name] as const, + queryFn: ({ queryKey: [_, name] }) => { + return fetchQualityGate({ name }); + }, + }); +} + +export function useQualityGatesQuery() { + return useQuery({ + queryKey: [QUALITY_GATES_KEY] as const, + queryFn: () => { + return fetchQualityGates(); + }, + staleTime: 2 * 60 * 1000, + }); +} + +export function useCreateQualityGateMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (name: string) => { + return createQualityGate({ name }); + }, + onSuccess: () => { + queryClient.invalidateQueries([QUALITY_GATES_KEY]); + }, + }); +} + +export function useSetQualityGateAsDefaultMutation(gateName: string) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (qualityGate: QualityGate) => { + return setQualityGateAsDefault({ name: qualityGate.name }); + }, + onSuccess: () => { + queryClient.invalidateQueries([QUALITY_GATES_KEY]); + queryClient.invalidateQueries([QUALITY_GATE_KEY, gateName]); + }, + }); +} + +export function useRenameQualityGateMutation(currentName: string) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (newName: string) => { + return renameQualityGate({ currentName, name: newName }); + }, + onSuccess: (_, newName: string) => { + queryClient.invalidateQueries([QUALITY_GATES_KEY]); + queryClient.invalidateQueries([QUALITY_GATE_KEY, newName]); + }, + }); +} + +export function useCopyQualityGateMutation(sourceName: string) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (newName: string) => { + return copyQualityGate({ sourceName, name: newName }); + }, + onSuccess: () => { + queryClient.invalidateQueries([QUALITY_GATES_KEY]); + }, + }); +} + +export function useDeleteQualityGateMutation(name: string) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: () => { + return deleteQualityGate({ name }); + }, + onSuccess: () => { + queryClient.invalidateQueries([QUALITY_GATES_KEY]); + }, + }); +} + +export function useFixQualityGateMutation(gateName: string) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ + weakConditions, + missingConditions, + }: { + weakConditions: Condition[]; + missingConditions: Condition[]; + }) => { + const promiseArr = weakConditions + .map((condition) => { + return updateCondition({ + ...getCorrectCaycCondition(condition), + id: condition.id, + }); + }) + .concat( + missingConditions.map((condition) => { + return createCondition({ + ...getCorrectCaycCondition(condition), + gateName, + }); + }), + ); + + return Promise.all(promiseArr); + }, + onSuccess: () => { + queryClient.invalidateQueries([QUALITY_GATES_KEY]); + queryClient.invalidateQueries([QUALITY_GATE_KEY, gateName]); + addGlobalSuccessMessage(translate('quality_gates.conditions_updated')); + }, + }); +} + +export function useCreateConditionMutation(gateName: string) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (condition: Omit<Condition, 'id'>) => { + return createCondition({ ...condition, gateName }); + }, + onSuccess: () => { + queryClient.invalidateQueries([QUALITY_GATES_KEY]); + queryClient.invalidateQueries([QUALITY_GATE_KEY, gateName]); + addGlobalSuccessMessage(translate('quality_gates.condition_added')); + }, + }); +} + +export function useUpdateConditionMutation(gateName: string) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (condition: Condition) => { + return updateCondition(condition); + }, + onSuccess: () => { + queryClient.invalidateQueries([QUALITY_GATES_KEY]); + queryClient.invalidateQueries([QUALITY_GATE_KEY, gateName]); + addGlobalSuccessMessage(translate('quality_gates.condition_updated')); + }, + }); +} + +export function useDeleteConditionMutation(gateName: string) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (condition: Condition) => { + return deleteCondition({ + id: condition.id, + }); + }, + onSuccess: () => { + queryClient.invalidateQueries([QUALITY_GATES_KEY]); + queryClient.invalidateQueries([QUALITY_GATE_KEY, gateName]); + addGlobalSuccessMessage(translate('quality_gates.condition_deleted')); + }, + }); +} |