123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- /*
- * 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 {
- ButtonSecondary,
- FlagMessage,
- HeadingDark,
- HelperHintIcon,
- LightPrimary,
- Link,
- Note,
- SubHeading,
- } from 'design-system';
- 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 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 { groupAndSortByPriorityConditions, isQualityGateOptimized } from '../utils';
- import CaYCConditionsSimplificationGuide from './CaYCConditionsSimplificationGuide';
- import CaycCompliantBanner from './CaycCompliantBanner';
- import CaycConditionsTable from './CaycConditionsTable';
- import CaycFixOptimizeBanner from './CaycFixOptimizeBanner';
- import ConditionModal from './ConditionModal';
- import CaycReviewUpdateConditionsModal from './ConditionReviewAndUpdateModal';
- import ConditionsTable from './ConditionsTable';
-
- interface Props extends WithAvailableFeaturesProps {
- metrics: Dict<Metric>;
- onAddCondition: (condition: ConditionType) => void;
- onRemoveCondition: (Condition: ConditionType) => void;
- onSaveCondition: (newCondition: ConditionType, oldCondition: ConditionType) => void;
- qualityGate: QualityGate;
- updatedConditionId?: string;
- }
-
- const FORBIDDEN_METRIC_TYPES = ['DATA', 'DISTRIB', 'STRING', 'BOOL'];
- const FORBIDDEN_METRICS: string[] = [
- MetricKey.alert_status,
- MetricKey.releasability_rating,
- MetricKey.security_hotspots,
- MetricKey.new_security_hotspots,
- ];
-
- function Conditions({
- qualityGate,
- metrics,
- onRemoveCondition,
- onSaveCondition,
- onAddCondition,
- hasFeature,
- updatedConditionId,
- }: Readonly<Props>) {
- const [editing, setEditing] = React.useState<boolean>(
- qualityGate.caycStatus === CaycStatus.NonCompliant,
- );
- const { name } = qualityGate;
- const canEdit = Boolean(qualityGate.actions?.manageConditions);
- const { conditions = [] } = qualityGate;
- const existingConditions = conditions.filter((condition) => metrics[condition.metric]);
- const { overallCodeConditions, newCodeConditions, caycConditions } =
- groupAndSortByPriorityConditions(existingConditions, metrics, qualityGate.isBuiltIn);
-
- const duplicates: ConditionType[] = [];
- const savedConditions = existingConditions.filter((condition) => condition.id != null);
- savedConditions.forEach((condition) => {
- const sameCount = savedConditions.filter((sample) => sample.metric === condition.metric).length;
- if (sameCount > 1) {
- duplicates.push(condition);
- }
- });
-
- const uniqDuplicates = uniqBy(duplicates, (d) => d.metric).map((condition) => ({
- ...condition,
- metric: metrics[condition.metric],
- }));
-
- // set edit only when the name is change
- // i.e when user changes the quality gate
- React.useEffect(() => {
- setEditing(qualityGate.caycStatus === CaycStatus.NonCompliant);
- }, [name]); // eslint-disable-line react-hooks/exhaustive-deps
-
- const renderConditionModal = React.useCallback(
- ({ onClose }: ModalProps) => {
- const { conditions = [] } = qualityGate;
- const availableMetrics = differenceWith(
- map(metrics, (metric) => metric).filter(
- (metric) =>
- !metric.hidden &&
- !FORBIDDEN_METRIC_TYPES.includes(metric.type) &&
- !FORBIDDEN_METRICS.includes(metric.key),
- ),
- conditions,
- (metric, condition) => metric.key === condition.metric,
- );
- return (
- <ConditionModal
- header={translate('quality_gates.add_condition')}
- metrics={availableMetrics}
- onAddCondition={onAddCondition}
- onClose={onClose}
- qualityGate={qualityGate}
- />
- );
- },
- [metrics, qualityGate, onAddCondition],
- );
-
- const getDocUrl = useDocUrl();
- const isCompliantCustomQualityGate =
- qualityGate.caycStatus !== CaycStatus.NonCompliant && !qualityGate.isBuiltIn;
- const isOptimizing = isCompliantCustomQualityGate && !isQualityGateOptimized(qualityGate);
-
- const renderCaycModal = React.useCallback(
- ({ onClose }: ModalProps) => {
- const { conditions = [] } = qualityGate;
- const canEdit = Boolean(qualityGate.actions?.manageConditions);
- return (
- <CaycReviewUpdateConditionsModal
- qualityGate={qualityGate}
- metrics={metrics}
- canEdit={canEdit}
- onRemoveCondition={onRemoveCondition}
- onSaveCondition={onSaveCondition}
- onAddCondition={onAddCondition}
- lockEditing={() => setEditing(false)}
- updatedConditionId={updatedConditionId}
- conditions={conditions}
- scope="new-cayc"
- onClose={onClose}
- isOptimizing={isOptimizing}
- />
- );
- },
- [
- qualityGate,
- metrics,
- updatedConditionId,
- onAddCondition,
- onRemoveCondition,
- onSaveCondition,
- isOptimizing,
- ],
- );
-
- return (
- <div>
- <CaYCConditionsSimplificationGuide />
-
- {isCompliantCustomQualityGate && !isOptimizing && <CaycCompliantBanner />}
- {isCompliantCustomQualityGate && isOptimizing && canEdit && (
- <CaycFixOptimizeBanner renderCaycModal={renderCaycModal} isOptimizing />
- )}
- {qualityGate.caycStatus === CaycStatus.NonCompliant && canEdit && (
- <CaycFixOptimizeBanner renderCaycModal={renderCaycModal} />
- )}
-
- <header className="sw-flex sw-items-center sw-mb-4 sw-justify-between">
- <div className="sw-flex">
- <HeadingDark className="sw-body-md-highlight sw-m-0">
- {translate('quality_gates.conditions')}
- </HeadingDark>
- {!qualityGate.isBuiltIn && (
- <DocumentationTooltip
- className="sw-ml-2"
- content={translate('quality_gates.conditions.help')}
- links={[
- {
- href: '/user-guide/clean-as-you-code/',
- label: translate('quality_gates.conditions.help.link'),
- },
- ]}
- >
- <HelperHintIcon />
- </DocumentationTooltip>
- )}
- </div>
- <div>
- {(qualityGate.caycStatus === CaycStatus.NonCompliant || editing) && canEdit && (
- <ModalButton modal={renderConditionModal}>
- {({ onClick }) => (
- <ButtonSecondary data-test="quality-gates__add-condition" onClick={onClick}>
- {translate('quality_gates.add_condition')}
- </ButtonSecondary>
- )}
- </ModalButton>
- )}
- </div>
- </header>
-
- {uniqDuplicates.length > 0 && (
- <FlagMessage variant="warning" className="sw-flex sw-mb-4">
- <p>
- <p>{translate('quality_gates.duplicated_conditions')}</p>
- <ul className="sw-my-2 sw-list-disc sw-pl-10">
- {uniqDuplicates.map((d) => (
- <li key={d.metric.key}>{getLocalizedMetricName(d.metric)}</li>
- ))}
- </ul>
- </p>
- </FlagMessage>
- )}
-
- <div className="sw-flex sw-flex-col sw-gap-8">
- {caycConditions.length > 0 && (
- <div>
- <div className="sw-flex sw-items-center sw-gap-2 sw-mb-2">
- <HeadingDark as="h3">{translate('quality_gates.conditions.cayc')}</HeadingDark>
- <DocumentationTooltip
- content={translate('quality_gates.conditions.cayc.hint')}
- placement="right"
- >
- <HelperHintIcon />
- </DocumentationTooltip>
- </div>
-
- <CaycConditionsTable metrics={metrics} conditions={caycConditions} />
-
- {hasFeature(Feature.BranchSupport) && (
- <Note className="sw-mb-2 sw-body-sm">
- {translate('quality_gates.conditions.cayc', 'description')}
- </Note>
- )}
- </div>
- )}
-
- {newCodeConditions.length > 0 && (
- <div>
- <HeadingDark as="h3" className="sw-mb-2">
- {translate('quality_gates.conditions.new_code', 'long')}
- </HeadingDark>
-
- <ConditionsTable
- qualityGate={qualityGate}
- metrics={metrics}
- canEdit={canEdit}
- onRemoveCondition={onRemoveCondition}
- onSaveCondition={onSaveCondition}
- updatedConditionId={updatedConditionId}
- conditions={newCodeConditions}
- showEdit={editing}
- scope="new"
- />
-
- {hasFeature(Feature.BranchSupport) && (
- <Note className="sw-mb-2 sw-body-sm">
- {translate('quality_gates.conditions.new_code', 'description')}
- </Note>
- )}
- </div>
- )}
-
- {overallCodeConditions.length > 0 && (
- <div className="sw-mt-5">
- <HeadingDark as="h3" className="sw-mb-2">
- {translate('quality_gates.conditions.overall_code', 'long')}
- </HeadingDark>
-
- <ConditionsTable
- qualityGate={qualityGate}
- metrics={metrics}
- canEdit={canEdit}
- onRemoveCondition={onRemoveCondition}
- onSaveCondition={onSaveCondition}
- updatedConditionId={updatedConditionId}
- conditions={overallCodeConditions}
- scope="overall"
- />
-
- {hasFeature(Feature.BranchSupport) && (
- <Note className="sw-mb-2 sw-body-sm">
- {translate('quality_gates.conditions.overall_code', 'description')}
- </Note>
- )}
- </div>
- )}
- </div>
-
- {qualityGate.caycStatus !== CaycStatus.NonCompliant && !editing && canEdit && (
- <div className="sw-mt-4 it__qg-unfollow-cayc">
- <SubHeading as="p" className="sw-mb-2 sw-body-sm">
- <FormattedMessage
- id="quality_gates.cayc_unfollow.description"
- defaultMessage={translate('quality_gates.cayc_unfollow.description')}
- values={{
- cayc_link: (
- <Link to={getDocUrl('/user-guide/clean-as-you-code/')}>
- {translate('quality_gates.cayc')}
- </Link>
- ),
- }}
- />
- </SubHeading>
- <ButtonSecondary className="sw-mt-2" onClick={() => setEditing(true)}>
- {translate('quality_gates.cayc.unlock_edit')}
- </ButtonSecondary>
- </div>
- )}
-
- {existingConditions.length === 0 && (
- <div className="sw-mt-4 sw-body-sm">
- <LightPrimary as="p">{translate('quality_gates.no_conditions')}</LightPrimary>
- </div>
- )}
- </div>
- );
- }
-
- export default withMetricsContext(withAvailableFeatures(Conditions));
|