id: 'AXJMbIUHPAOIsUIE3eOi',
metric: 'new_security_hotspots_reviewed',
op: 'LT',
- error: '85',
+ error: '100',
isCaycCondition: true,
},
{
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));
}
}
};
}
+
+export function useAvailableFeatures() {
+ const availableFeatures = React.useContext(AvailableFeaturesContext);
+
+ return {
+ hasFeature: (feature: Feature) => availableFeatures.includes(feature),
+ };
+}
}
};
}
+
+export function useMetrics() {
+ return React.useContext(MetricsContext);
+}
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';
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`
* 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,
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);
* 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';
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">
)}
<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"
label={translate('quality_gates.conditions.operator')}
>
<ConditionOperator
- metric={metric}
- onOperatorChange={this.handleOperatorChange}
- op={op}
+ metric={selectedMetric}
+ onOperatorChange={handleOperatorChange}
+ op={selectedOperator}
/>
</FormField>
<FormField
label={translate('quality_gates.conditions.value')}
>
<ThresholdInput
- metric={metric}
+ metric={selectedMetric}
name="error"
- onChange={this.handleErrorChange}
- value={error}
+ onChange={handleErrorChange}
+ value={errorThreshold}
/>
</FormField>
</div>
);
};
- 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')}
+ />
+ );
}
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(
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">
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';
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'];
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]);
duplicates.push(condition);
}
});
+ const { hasFeature } = useAvailableFeatures();
const uniqDuplicates = uniqBy(duplicates, (d) => d.metric).map((condition) => ({
...condition,
<ConditionModal
header={translate('quality_gates.add_condition')}
metrics={availableMetrics}
- onAddCondition={onAddCondition}
onClose={onClose}
qualityGate={qualityGate}
/>
);
},
- [metrics, qualityGate, onAddCondition],
+ [metrics, qualityGate],
);
const getDocUrl = useDocUrl();
qualityGate={qualityGate}
metrics={metrics}
canEdit={canEdit}
- onRemoveCondition={onRemoveCondition}
- onSaveCondition={onSaveCondition}
- onAddCondition={onAddCondition}
lockEditing={() => setEditing(false)}
- updatedConditionId={updatedConditionId}
conditions={conditions}
scope="new-cayc"
onClose={onClose}
/>
);
},
- [
- qualityGate,
- metrics,
- updatedConditionId,
- onAddCondition,
- onRemoveCondition,
- onSaveCondition,
- isOptimizing,
- ],
+ [qualityGate, metrics, isOptimizing],
);
return (
qualityGate={qualityGate}
metrics={metrics}
canEdit={canEdit}
- onRemoveCondition={onRemoveCondition}
- onSaveCondition={onSaveCondition}
- updatedConditionId={updatedConditionId}
conditions={newCodeConditions}
showEdit={editing}
scope="new"
qualityGate={qualityGate}
metrics={metrics}
canEdit={canEdit}
- onRemoveCondition={onRemoveCondition}
- onSaveCondition={onSaveCondition}
- updatedConditionId={updatedConditionId}
conditions={overallCodeConditions}
scope="overall"
/>
</div>
);
}
-
-export default withMetricsContext(withAvailableFeatures(Conditions));
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() {
qualityGate,
metrics,
canEdit,
- onRemoveCondition,
- onSaveCondition,
- updatedConditionId,
scope,
conditions,
isCaycModal,
condition={condition}
key={condition.id}
metric={metrics[condition.metric]}
- onRemoveCondition={onRemoveCondition}
- onSaveCondition={onSaveCondition}
qualityGate={qualityGate}
- updated={condition.id === updatedConditionId}
isCaycModal={isCaycModal}
showEdit={showEdit}
/>
*/
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);
*/
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);
*/
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);
*/
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>
+ );
}
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 (
</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">
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';
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);
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 });
}
};
{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}
/>
)}
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 (
);
}
-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">
<HelperHintIcon />
</DocumentationTooltip>
</div>
- {canCreate && <CreateQualityGateModal refreshQualityGates={refreshQualityGates} />}
+ {canCreate && <CreateQualityGateModal />}
</div>
);
}
*/
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);
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';
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);
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();
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();
});
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 () => {
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' }),
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' }),
);
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' }),
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' }));
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();
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}');
});
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: [],
});
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();
});
});
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';
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;
}
--- /dev/null
+/*
+ * 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'));
+ },
+ });
+}
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.