From: Andrey Luiz Date: Fri, 1 Dec 2023 10:00:02 +0000 (+0100) Subject: SONAR-21012 Remove CaYC compliance check from front-end and rely only on the API... X-Git-Tag: 10.4.0.87286~382 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=415ee3d300898c403eac83e697d88ad0ba457568;p=sonarqube.git SONAR-21012 Remove CaYC compliance check from front-end and rely only on the API (#10041) --- 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(response: T): Promise { 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

( } }; } + +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

( } }; } + +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 { - 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 ( - - - -

- - - { + if (!name) { + openDefault(qualityGates); + } + }, [name, openDefault, qualityGates]); + + return ( + + + +
+ + + + + + + + + + {name !== undefined && ( +
- - - - - - - {name !== undefined && ( -
- -
- -
- )} -
- - - ); - } -} - -export default function AppWrapper() { - const params = useParams(); - const navigate = useNavigate(); - - return ; + +
+ +
+ )} +
+ + + ); } 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; showEdit?: boolean; isCaycModal?: boolean; } -interface State { - deleteFormOpen: boolean; - modal: boolean; -} - -export class ConditionComponent extends React.PureComponent { - 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) { + 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 ( - - - {getLocalizedMetricNameNoDiffMetric(metric, metrics)} - {metric.hidden && } - - - {this.renderOperator()} + }; - - - - - {!isCaycModal && canEdit && ( - <> - {(!isCaycCompliantAndOverCompliant || - !isConditionWithFixedValue(condition) || - (isCaycCompliantAndOverCompliant && showEdit)) && ( - <> - + + {getLocalizedMetricNameNoDiffMetric(metric, metrics)} + {metric.hidden && } + + + {renderOperator()} + + + + + + {!isCaycModal && canEdit && ( + <> + {(!isCaycCompliantAndOverCompliant || + !isConditionWithFixedValue(condition) || + (isCaycCompliantAndOverCompliant && showEdit)) && ( + <> + + {modal && ( + - {this.state.modal && ( - + )} + + )} + {(!isCaycCompliantAndOverCompliant || + !condition.isCaycCondition || + (isCaycCompliantAndOverCompliant && showEdit)) && ( + <> + - )} - {(!isCaycCompliantAndOverCompliant || - !condition.isCaycCondition || - (isCaycCompliantAndOverCompliant && showEdit)) && ( - <> - + {deleteFormOpen && ( + deleteCondition(condition)} + > + {translate('delete')} + + } + secondaryButtonLabel={translate('close')} /> - {this.state.deleteFormOpen && ( - this.removeCondition(condition)} - > - {translate('delete')} - - } - secondaryButtonLabel={translate('close')} - /> - )} - - )} - - )} - - - ); - } + )} + + )} + + )} + + + ); } - -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 { - 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) { + const [errorThreshold, setErrorThreshold] = React.useState(condition ? condition.error : ''); + const [scope, setScope] = React.useState<'new' | 'overall'>('new'); + const [selectedMetric, setSelectedMetric] = React.useState(metric); + const [selectedOperator, setSelectedOperator] = React.useState( + 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) => { + const handleFormSubmit = async (event: React.FormEvent) => { event.preventDefault(); - const { condition, qualityGate } = this.props; - const newCondition: Omit = { - 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 = { + 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 ( -
- {errorMessage && ( - - {errorMessage} - - )} - {this.props.metric === undefined && ( + + {metric === undefined && (
- + {translate('quality_gates.conditions.new_code')} @@ -139,22 +133,22 @@ export default class ConditionModal extends React.PureComponent { )} {metrics && ( scope === 'new' ? isDiffMetric(m.key) : !isDiffMetric(m.key), )} - onMetricChange={this.handleMetricChange} + onMetricChange={handleMetricChange} /> )} - {metric && ( + {selectedMetric && (
{ label={translate('quality_gates.conditions.operator')} > { label={translate('quality_gates.conditions.value')} >
@@ -184,29 +178,25 @@ export default class ConditionModal extends React.PureComponent { ); }; - render() { - const { header } = this.props; - const { metric } = this.state; - return ( - - {header} - - } - secondaryButtonLabel={translate('close')} - /> - ); - } + return ( + + {header} + + } + 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; - 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) { - 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) const getDocUrl = useDocUrl(); - const updateCaycQualityGate = React.useCallback(() => { - const promiseArr: Promise[] = []; - 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 = (
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; - 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) { +export default function Conditions({ qualityGate }: Readonly) { const [editing, setEditing] = React.useState( 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({ ); }, - [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({
); } - -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; - 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; qualityGate: QualityGate; - router: Router; -} - -interface State { - name: string; } const FORM_ID = 'rename-quality-gate'; -export class CopyQualityGateForm extends React.PureComponent { - constructor(props: Props) { - super(props); - this.state = { name: props.qualityGate.name }; - } +export default function CopyQualityGateForm({ qualityGate, onClose }: Readonly) { + const [name, setName] = React.useState(qualityGate.name); + const { mutateAsync: copyQualityGate } = useCopyQualityGateMutation(qualityGate.name); + const router = useRouter(); - handleNameChange = (event: React.ChangeEvent) => { - this.setState({ name: event.currentTarget.value }); + const handleNameChange = (event: React.ChangeEvent) => { + 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 ( - - - - - - - } - primaryButton={ - - {translate('copy')} - - } - secondaryButtonLabel={translate('cancel')} - /> - ); - } + return ( + + + + + + + } + primaryButton={ + + {translate('copy')} + + } + 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; - router: Router; } -interface State { - name: string; -} - -export class CreateQualityGateForm extends React.PureComponent { - state: State = { name: '' }; - - handleNameChange = (event: React.SyntheticEvent) => { - this.setState({ name: event.currentTarget.value }); - }; +export default function CreateQualityGateForm({ onClose }: Readonly) { + const [name, setName] = React.useState(''); + const { mutateAsync: createQualityGate } = useCreateQualityGateMutation(); + const router = useRouter(); - handleFormSubmit = (event: React.SyntheticEvent) => { - event.preventDefault(); - this.handleCreate(); + const handleNameChange = (event: React.SyntheticEvent) => { + 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) => { + event.preventDefault(); + handleCreate(); + }; - const body = ( -
- - - - - - ); + const body = ( +
+ + + + + + ); - return ( - - {translate('quality_gate.create')} - - } - secondaryButtonLabel={translate('cancel')} - /> - ); - } + return ( + + {translate('quality_gate.create')} + + } + 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; + onClose: () => void; qualityGate: QualityGate; - router: Router; } -export class DeleteQualityGateForm extends React.PureComponent { - 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) { + const { mutateAsync: deleteQualityGate } = useDeleteQualityGateMutation(qualityGate.name); + const router = useRouter(); - render() { - const { qualityGate } = this.props; + const onDelete = async () => { + await deleteQualityGate(); + router.push(getQualityGatesUrl()); + }; - return ( - - {translate('delete')} - - } - secondaryButtonLabel={translate('cancel')} - /> - ); - } + return ( + + {translate('delete')} + + } + 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; } -interface State { - loading: boolean; - qualityGate?: QualityGate; - updatedConditionId?: string; -} - -export default class Details extends React.PureComponent { - 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 ( -
- - {qualityGate && ( - <> - - - - - )} - -
- ); - } +export default function Details({ qualityGateName }: Readonly) { + const { data: qualityGate, isLoading } = useQualityGateQuery(qualityGateName); + + return ( +
+ + {qualityGate && ( + <> + + + + + )} + +
+ ); } 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) { )} - +
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; - refreshList: () => Promise; } const TOOLTIP_MOUSE_LEAVE_DELAY = 0.3; -export default function DetailsHeader({ - refreshItem, - refreshList, - onSetDefault, - qualityGate, -}: Readonly) { +export default function DetailsHeader({ qualityGate }: Readonly) { 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 && ( setIsRenameFormOpen(false)} - onRename={handleActionRefresh} qualityGate={qualityGate} /> )} {isCopyFormOpen && ( - setIsCopyFormOpen(false)} - onCopy={handleActionRefresh} - qualityGate={qualityGate} - /> + setIsCopyFormOpen(false)} qualityGate={qualityGate} /> )} {isRemoveFormOpen && ( 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; } -function CreateQualityGateModal({ - refreshQualityGates, -}: Readonly>) { +function CreateQualityGateModal() { const renderModal = React.useCallback( - ({ onClose }: ModalProps) => ( - - ), - [refreshQualityGates], + ({ onClose }: ModalProps) => , + [], ); return ( @@ -52,7 +47,7 @@ function CreateQualityGateModal({ ); } -export default function ListHeader({ canCreate, refreshQualityGates }: Readonly) { +export default function ListHeader({ canCreate }: Readonly) { return (
@@ -72,7 +67,7 @@ export default function ListHeader({ canCreate, refreshQualityGates }: Readonly<
- {canCreate && } + {canCreate && }
); } 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; qualityGate: QualityGate; } -interface State { - name: string; -} - const FORM_ID = 'rename-quality-gate'; -class RenameQualityGateForm extends React.PureComponent { - constructor(props: Props) { - super(props); - this.state = { name: props.qualityGate.name }; - } +export default function RenameQualityGateForm({ qualityGate, onClose }: Readonly) { + const [name, setName] = React.useState(qualityGate.name); + const { mutateAsync: renameQualityGate } = useRenameQualityGateMutation(qualityGate.name); + const router = useRouter(); - handleNameChange = (event: React.ChangeEvent) => { - this.setState({ name: event.currentTarget.value }); + const handleNameChange = (event: React.ChangeEvent) => { + 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 ( - - - - - - - } - primaryButton={ - - {translate('rename')} - - } - secondaryButtonLabel={translate('cancel')} - /> - ); - } + return ( + + + + + + + } + primaryButton={ + + {translate('rename')} + + } + 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) { 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) => { + 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')); + }, + }); +} diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index cab9fe1183e..c77e44ef4e5 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2164,6 +2164,7 @@ quality_gates.condition.delete=Delete condition on {0} quality_gates.condition_added=Successfully added condition. quality_gates.update_condition=Update Condition quality_gates.condition_updated=Successfully updated condition. +quality_gates.conditions_updated=Successfully updated conditions. quality_gates.no_conditions=No Conditions quality_gates.health_icons=Project health icons represent: quality_gates.projects_for_default=Every project not specifically associated to a quality gate will be associated to this one by default.