<Global styles={globalStyles({ theme })} />
<ReactModal
- aria={{ labelledby: '#modal_header_title' }}
+ aria={{ labelledby: 'modal_header_title' }}
className={classNames('design-system-modal-contents modal', { large: isLarge })}
isOpen={isOpen}
onRequestClose={onClose}
&.large {
max-width: 1280px;
min-width: 1040px;
+ transform: translateX(-50%);
+ margin-left: 0px;
}
}
import {
ActionCell,
ContentCell,
+ DangerButtonPrimary,
DestructiveIcon,
InteractiveIcon,
+ Modal,
NumericalCell,
PencilIcon,
TableRow,
import * as React from 'react';
import { deleteCondition } from '../../../api/quality-gates';
import withMetricsContext from '../../../app/components/metrics/withMetricsContext';
-import ConfirmModal from '../../../components/controls/ConfirmModal';
import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n';
import {
CaycStatus,
size="small"
/>
{this.state.deleteFormOpen && (
- <ConfirmModal
- confirmButtonText={translate('delete')}
- confirmData={condition}
- header={translate('quality_gates.delete_condition')}
- isDestructive
+ <Modal
+ headerTitle={translate('quality_gates.delete_condition')}
onClose={this.closeDeleteForm}
- onConfirm={this.removeCondition}
- >
- {translateWithParameters(
+ body={translateWithParameters(
'quality_gates.delete_condition.confirm.message',
getLocalizedMetricName(this.props.metric),
)}
- </ConfirmModal>
+ primaryButton={
+ <DangerButtonPrimary
+ autoFocus
+ type="submit"
+ onClick={() => this.removeCondition(condition)}
+ >
+ {translate('delete')}
+ </DangerButtonPrimary>
+ }
+ secondaryButtonLabel={translate('close')}
+ />
)}
</>
)}
* 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 * as React from 'react';
import { createCondition, updateCondition } from '../../../api/quality-gates';
-import ConfirmModal from '../../../components/controls/ConfirmModal';
-import Radio from '../../../components/controls/Radio';
-import { Alert } from '../../../components/ui/Alert';
import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
import { isDiffMetric } from '../../../helpers/measures';
import { Condition, Metric, QualityGate } from '../../../types/types';
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);
return Array.isArray(operators) ? undefined : operators;
}
- handleFormSubmit = () => {
+ handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
+ event.preventDefault();
+
const { condition, qualityGate } = this.props;
const newCondition: Omit<Condition, 'id'> = {
metric: this.state.metric!.key,
const submitPromise = condition
? updateCondition({ id: condition.id, ...newCondition })
: createCondition({ gateName: qualityGate.name, ...newCondition });
- return submitPromise.then(this.props.onAddCondition);
+ return submitPromise.then(this.props.onAddCondition).then(this.props.onClose);
};
handleScopeChange = (scope: 'new' | 'overall') => {
this.setState({ error });
};
- render() {
- const { header, metrics, onClose } = this.props;
- const { op, error, scope, metric } = this.state;
- return (
- <ConfirmModal
- confirmButtonText={header}
- confirmDisable={metric === undefined}
- header={header}
- onClose={onClose}
- onConfirm={this.handleFormSubmit}
- size="small"
- >
- {this.state.errorMessage && <Alert variant="error">{this.state.errorMessage}</Alert>}
+ renderBody = () => {
+ const { metrics } = this.props;
+ const { op, error, scope, metric, errorMessage } = this.state;
+ return (
+ <form id={ADD_CONDITION_MODAL_ID} onSubmit={this.handleFormSubmit}>
+ {errorMessage && (
+ <FlagMessage className="sw-mb-2" variant="error">
+ {errorMessage}
+ </FlagMessage>
+ )}
{this.props.metric === undefined && (
- <div className="modal-field display-flex-center">
- <Radio checked={scope === 'new'} onCheck={this.handleScopeChange} value="new">
- <span data-test="quality-gates__condition-scope-new">
- {translate('quality_gates.conditions.new_code')}
- </span>
- </Radio>
- <Radio
- checked={scope === 'overall'}
- className="big-spacer-left"
- onCheck={this.handleScopeChange}
- value="overall"
- >
- <span data-test="quality-gates__condition-scope-overall">
- {translate('quality_gates.conditions.overall_code')}
- </span>
- </Radio>
- </div>
+ <FormField label={translate('quality_gates.conditions.where')}>
+ <div className="sw-flex sw-gap-4">
+ <RadioButton checked={scope === 'new'} onCheck={this.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}
+ value="overall"
+ >
+ <span data-test="quality-gates__condition-scope-overall">
+ {translate('quality_gates.conditions.overall_code')}
+ </span>
+ </RadioButton>
+ </div>
+ </FormField>
)}
- <div className="modal-field">
- <label htmlFor="condition-metric">
- {translate('quality_gates.conditions.fails_when')}
- </label>
+ <FormField
+ description={this.props.metric && getLocalizedMetricName(this.props.metric)}
+ htmlFor="condition-metric"
+ label={translate('quality_gates.conditions.fails_when')}
+ >
{metrics && (
<MetricSelect
metric={metric}
onMetricChange={this.handleMetricChange}
/>
)}
- {this.props.metric && (
- <span className="note">{getLocalizedMetricName(this.props.metric)}</span>
- )}
- </div>
+ </FormField>
{metric && (
- <>
- <div className="modal-field display-inline-block">
- <label id="condition-operator-label">
- {translate('quality_gates.conditions.operator')}
- </label>
+ <div className="sw-flex sw-gap-2">
+ <FormField
+ className="sw-mb-0"
+ htmlFor="condition-operator"
+ label={translate('quality_gates.conditions.operator')}
+ >
<ConditionOperator
metric={metric}
onOperatorChange={this.handleOperatorChange}
op={op}
/>
- </div>
- <div className="modal-field display-inline-block spacer-left">
- <label id="condition-threshold-label">
- {translate('quality_gates.conditions.value')}
- </label>
+ </FormField>
+ <FormField
+ htmlFor="condition-threshold"
+ label={translate('quality_gates.conditions.value')}
+ >
<ThresholdInput
metric={metric}
name="error"
onChange={this.handleErrorChange}
value={error}
/>
- </div>
- </>
+ </FormField>
+ </div>
)}
- </ConfirmModal>
+ </form>
+ );
+ };
+
+ 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')}
+ />
);
}
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
+import { InputSelect, Note } from 'design-system';
import * as React from 'react';
-import Select from '../../../components/controls/Select';
import { translate } from '../../../helpers/l10n';
import { Metric } from '../../../types/types';
import { getPossibleOperators } from '../utils';
});
return (
- <Select
+ <InputSelect
autoFocus
- aria-labelledby="condition-operator-label"
- className="input-medium"
+ size="small"
isClearable={false}
- id="condition-operator"
+ inputId="condition-operator"
name="operator"
onChange={this.handleChange}
options={operatorOptions}
value={operatorOptions.filter((o) => o.value === this.props.op)}
/>
);
- } else {
- return (
- <span className="display-inline-block note abs-width-150">
- {this.getLabel(operators, this.props.metric)}
- </span>
- );
}
+
+ return <Note className="sw-w-abs-150">{this.getLabel(operators, this.props.metric)}</Note>;
}
}
* 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, 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 DocLink from '../../../components/common/DocLink';
-import ConfirmModal from '../../../components/controls/ConfirmModal';
+import { useDocUrl } from '../../../helpers/docs';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Condition, Dict, Metric, QualityGate } from '../../../types/types';
import { getCorrectCaycCondition, getWeakMissingAndNonCaycConditions } from '../utils';
qualityGate: QualityGate;
}
-export default class CaycReviewUpdateConditionsModal extends React.PureComponent<Props> {
- updateCaycQualityGate = () => {
- const { conditions, qualityGate } = this.props;
+export default function CaycReviewUpdateConditionsModal(props: Props) {
+ const {
+ conditions,
+ qualityGate,
+ metrics,
+ onSaveCondition,
+ onAddCondition,
+ lockEditing,
+ onClose,
+ } = props;
+
+ const { weakConditions, missingConditions } = getWeakMissingAndNonCaycConditions(conditions);
+ const sortedWeakConditions = sortBy(
+ weakConditions,
+ (condition) => metrics[condition.metric] && metrics[condition.metric].name
+ );
+
+ const sortedMissingConditions = sortBy(
+ missingConditions,
+ (condition) => metrics[condition.metric] && metrics[condition.metric].name
+ );
+
+ const getDocUrl = useDocUrl();
+
+ const updateCaycQualityGate = React.useCallback(() => {
const promiseArr: Promise<Condition | undefined | void>[] = [];
const { weakConditions, missingConditions } = getWeakMissingAndNonCaycConditions(conditions);
.then((resultCondition) => {
const currentCondition = conditions.find((con) => con.metric === condition.metric);
if (currentCondition) {
- this.props.onSaveCondition(resultCondition, currentCondition);
+ onSaveCondition(resultCondition, currentCondition);
}
})
- .catch(() => undefined),
+ .catch(() => undefined)
);
});
...getCorrectCaycCondition(condition),
gateName: qualityGate.name,
})
- .then((resultCondition) => this.props.onAddCondition(resultCondition))
- .catch(() => undefined),
+ .then((resultCondition) => onAddCondition(resultCondition))
+ .catch(() => undefined)
);
});
return Promise.all(promiseArr).then(() => {
- this.props.lockEditing();
+ lockEditing();
});
- };
-
- render() {
- const { conditions, qualityGate, metrics } = this.props;
-
- const { weakConditions, missingConditions } = getWeakMissingAndNonCaycConditions(conditions);
- const sortedWeakConditions = sortBy(
- weakConditions,
- (condition) => metrics[condition.metric]?.name,
- );
+ }, [conditions, qualityGate, lockEditing, onAddCondition, onSaveCondition]);
- const sortedMissingConditions = sortBy(
- missingConditions,
- (condition) => metrics[condition.metric]?.name,
- );
+ const body = (
+ <div className="sw-mb-10">
+ <SubHeading as="p" className="sw-body-sm">
+ <FormattedMessage
+ id="quality_gates.cayc.review_update_modal.description1"
+ defaultMessage={translate('quality_gates.cayc.review_update_modal.description1')}
+ values={{
+ cayc_link: (
+ <Link to={getDocUrl('/user-guide/clean-as-you-code/')}>
+ {translate('quality_gates.cayc')}
+ </Link>
+ ),
+ }}
+ />
+ </SubHeading>
- return (
- <ConfirmModal
- header={translateWithParameters(
- 'quality_gates.cayc.review_update_modal.header',
- qualityGate.name,
- )}
- confirmButtonText={translate('quality_gates.cayc.review_update_modal.confirm_text')}
- onClose={this.props.onClose}
- onConfirm={this.updateCaycQualityGate}
- size="medium"
- >
- <div className="quality-gate-section huge-spacer-bottom">
- <p>
- <FormattedMessage
- id="quality_gates.cayc.review_update_modal.description1"
- defaultMessage={translate('quality_gates.cayc.review_update_modal.description1')}
- values={{
- cayc_link: (
- <DocLink to="/user-guide/clean-as-you-code/">
- {translate('quality_gates.cayc')}
- </DocLink>
- ),
- }}
- />
- </p>
+ {sortedMissingConditions.length > 0 && (
+ <>
+ <Title as="h4" className="sw-mb-2 sw-mt-4 sw-body-sm-highlight">
+ {translateWithParameters(
+ 'quality_gates.cayc.review_update_modal.add_condition.header',
+ sortedMissingConditions.length
+ )}
+ </Title>
+ <ConditionsTable
+ {...props}
+ conditions={sortedMissingConditions}
+ showEdit={false}
+ isCaycModal
+ />
+ </>
+ )}
- {sortedMissingConditions.length > 0 && (
- <>
- <h4 className="big-spacer-top spacer-bottom">
- {translateWithParameters(
- 'quality_gates.cayc.review_update_modal.add_condition.header',
- sortedMissingConditions.length,
- )}
- </h4>
- <ConditionsTable
- {...this.props}
- conditions={sortedMissingConditions}
- showEdit={false}
- isCaycModal
- />
- </>
- )}
+ {sortedWeakConditions.length > 0 && (
+ <>
+ <Title as="h4" className="sw-mb-2 sw-mt-4 sw-body-sm-highlight">
+ {translateWithParameters(
+ 'quality_gates.cayc.review_update_modal.modify_condition.header',
+ sortedWeakConditions.length
+ )}
+ </Title>
+ <ConditionsTable
+ {...props}
+ conditions={sortedWeakConditions}
+ showEdit={false}
+ isCaycModal
+ />
+ </>
+ )}
- {sortedWeakConditions.length > 0 && (
- <>
- <h4 className="big-spacer-top spacer-bottom">
- {translateWithParameters(
- 'quality_gates.cayc.review_update_modal.modify_condition.header',
- sortedWeakConditions.length,
- )}
- </h4>
- <ConditionsTable
- {...this.props}
- conditions={sortedWeakConditions}
- showEdit={false}
- isCaycModal
- />
- </>
- )}
+ <Title as="h4" className="sw-mb-2 sw-mt-4 sw-body-sm-highlight">
+ {translate('quality_gates.cayc.review_update_modal.description2')}
+ </Title>
+ </div>
+ );
- <h4 className="big-spacer-top spacer-bottom">
- {translate('quality_gates.cayc.review_update_modal.description2')}
- </h4>
- </div>
- </ConfirmModal>
- );
- }
+ return (
+ <Modal
+ isLarge
+ headerTitle={translateWithParameters(
+ 'quality_gates.cayc.review_update_modal.header',
+ qualityGate.name
+ )}
+ onClose={onClose}
+ body={body}
+ primaryButton={
+ <ButtonPrimary
+ autoFocus
+ id="fix-quality-gate"
+ type="submit"
+ onClick={updateCaycQualityGate}
+ >
+ {translate('quality_gates.cayc.review_update_modal.confirm_text')}
+ </ButtonPrimary>
+ }
+ secondaryButtonLabel={translate('close')}
+ />
+ );
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { LabelValueSelectOption, SearchSelectDropdown } from 'design-system';
import { sortBy } from 'lodash';
import * as React from 'react';
+import { Options } from 'react-select';
import withMetricsContext from '../../../app/components/metrics/withMetricsContext';
-import Select from '../../../components/controls/Select';
import { getLocalizedMetricDomain, translate } from '../../../helpers/l10n';
import { Dict, Metric } from '../../../types/types';
import { getLocalizedMetricNameNoDiffMetric } from '../utils';
value: string;
}
-export class MetricSelect extends React.PureComponent<Props> {
- handleChange = (option: Option | null) => {
+export function MetricSelect({ metric, metricsArray, metrics, onMetricChange }: Props) {
+ const handleChange = (option: Option | null) => {
if (option) {
- const { metricsArray: metrics } = this.props;
- const selectedMetric = metrics.find((metric) => metric.key === option.value);
+ const selectedMetric = metricsArray.find((metric) => metric.key === option.value);
if (selectedMetric) {
- this.props.onMetricChange(selectedMetric);
+ onMetricChange(selectedMetric);
}
}
};
- render() {
- const { metric, metricsArray, metrics } = this.props;
+ const options: Array<Option & { domain?: string }> = sortBy(
+ metricsArray.map((m) => ({
+ value: m.key,
+ label: getLocalizedMetricNameNoDiffMetric(m, metrics),
+ domain: m.domain,
+ })),
+ 'domain'
+ );
- const options: Array<Option & { domain?: string }> = sortBy(
- metricsArray.map((m) => ({
- value: m.key,
- label: getLocalizedMetricNameNoDiffMetric(m, metrics),
- domain: m.domain,
- })),
- 'domain',
- );
+ // Use "disabled" property to emulate optgroups.
+ const optionsWithDomains: Option[] = [];
+ options.forEach((option, index, options) => {
+ const previous = index > 0 ? options[index - 1] : null;
+ if (option.domain && (!previous || previous.domain !== option.domain)) {
+ optionsWithDomains.push({
+ value: '<domain>',
+ label: getLocalizedMetricDomain(option.domain),
+ isDisabled: true,
+ });
+ }
+ optionsWithDomains.push(option);
+ });
- // Use "disabled" property to emulate optgroups.
- const optionsWithDomains: Option[] = [];
- options.forEach((option, index, options) => {
- const previous = index > 0 ? options[index - 1] : null;
- if (option.domain && (!previous || previous.domain !== option.domain)) {
- optionsWithDomains.push({
- value: '<domain>',
- label: getLocalizedMetricDomain(option.domain),
- isDisabled: true,
- });
- }
- optionsWithDomains.push(option);
- });
+ const handleAssigneeSearch = React.useCallback(
+ (query: string, resolve: (options: Options<LabelValueSelectOption<string>>) => void) => {
+ resolve(options.filter((opt) => opt.label.toLowerCase().includes(query.toLowerCase())));
+ },
+ [options]
+ );
- return (
- <Select
- className="text-middle quality-gate-metric-select"
- id="condition-metric"
- onChange={this.handleChange}
- options={optionsWithDomains}
- placeholder={translate('search.search_for_metrics')}
- value={optionsWithDomains.find((o) => o.value === metric?.key)}
- />
- );
- }
+ return (
+ <SearchSelectDropdown
+ aria-label={translate('search.search_for_metrics')}
+ size="large"
+ controlSize="full"
+ inputId="condition-metric"
+ isClearable
+ defaultOptions={optionsWithDomains}
+ loadOptions={handleAssigneeSearch}
+ onChange={handleChange}
+ placeholder={translate('search.search_for_metrics')}
+ controlLabel={
+ optionsWithDomains.find((o) => o.value === metric?.key)?.label ?? translate('select_verb')
+ }
+ />
+ );
}
export default withMetricsContext(MetricSelect);
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { InputField, InputSelect } from 'design-system';
import * as React from 'react';
-import Select from '../../../components/controls/Select';
+import { LabelValueSelectOption } from '../../../components/controls/Select';
import { Metric } from '../../../types/types';
interface Props {
this.props.onChange(e.currentTarget.value);
};
- handleSelectChange = (option: { value: string }) => {
- this.props.onChange(option.value);
+ handleSelectChange = (option: LabelValueSelectOption) => {
+ if (option) {
+ this.props.onChange(option.value);
+ } else {
+ this.props.onChange('');
+ }
};
renderRatingInput() {
];
return (
- <Select
- className="input-tiny text-middle"
- aria-labelledby="condition-threshold-label"
- isClearable={false}
- id="condition-threshold"
+ <InputSelect
+ className="sw-w-abs-150"
+ inputId="condition-threshold"
name={name}
onChange={this.handleSelectChange}
options={options}
placeholder=""
- isSearchable={false}
+ size="small"
value={options.find((o) => o.value === value)}
/>
);
}
return (
- <input
- className="input-tiny text-middle"
- aria-labelledby="condition-threshold-label"
+ <InputField
+ size="small"
data-type={metric.type}
id="condition-threshold"
name={name}
await user.click(dialog.getByRole('radio', { name: 'quality_gates.conditions.new_code' }));
await selectEvent.select(dialog.getByRole('combobox'), ['Issues']);
await user.click(dialog.getByRole('textbox', { name: 'quality_gates.conditions.value' }));
- await user.keyboard('12{Enter}');
-
+ await user.keyboard('12');
+ await user.click(dialog.getByRole('button', { name: 'quality_gates.add_condition' }));
const newConditions = within(await screen.findByTestId('quality-gates__conditions-new'));
expect(await newConditions.findByRole('cell', { name: 'Issues' })).toBeInTheDocument();
expect(await newConditions.findByRole('cell', { name: '12' })).toBeInTheDocument();
await user.click(dialog.getByText('quality_gates.operator.LT'));
await user.click(dialog.getByRole('textbox', { name: 'quality_gates.conditions.value' }));
- await user.keyboard('42{Enter}');
+ await user.keyboard('42');
+ await user.click(dialog.getByRole('button', { name: 'quality_gates.add_condition' }));
const overallConditions = within(await screen.findByTestId('quality-gates__conditions-overall'));
quality_gates.conditions.warning=Warning
quality_gates.conditions.warning.tooltip=Warning status is deprecated and will disappear with the next update of the Quality Gate.
quality_gates.conditions.value=Value
+quality_gates.conditions.where=Where?
quality_gates.duplicated_conditions=This quality gate has duplicated conditions:
quality_gates.intro.1=Quality Gate is the set of conditions the project must meet before it can be released into production.
quality_gates.intro.2=It is possible to set a default Quality Gate, which will be applied to all projects not explicitly assigned to some other gate.