1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-14 00:19:16 +01:00

feat: expand constraint operator descriptions (2) (#858)

* refactor: remove pre-CO constraints list

* refactor: improve constraints dropdown order

* refactor: simplify prop value

* refactor: add missing space around parameter names

* refactor: remove constraint accordion box shadow

* refactor: show operator descriptions in constraints accordion

* refactor: show operator descriptions in constraints dropdown

* refactor: use ConstraintAccordionList in FeatureOverviewExecution

* refactor: add separators between operators in constraints dropdown

* refactor: remove unnecessary comment
This commit is contained in:
olav 2022-04-07 10:31:06 +02:00 committed by GitHub
parent e909d22300
commit f33ca9db4b
28 changed files with 331 additions and 510 deletions

View File

@ -1,37 +0,0 @@
import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({
constraintHeader: {
fontWeight: 'bold',
fontSize: theme.fontSizes.smallBody,
},
constraint: {
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '0.1rem 0.5rem',
fontSize: theme.fontSizes.smallBody,
backgroundColor: theme.palette.grey[200],
margin: '0.5rem 0',
position: 'relative',
borderRadius: '5px',
},
constraintBtn: {
color: theme.palette.primary.main,
fontWeight: 'normal',
marginBottom: '0.5rem',
},
btnContainer: {
position: 'absolute',
top: '6px',
right: 0,
},
column: {
flexDirection: 'column',
},
values: {
marginLeft: '1.5rem',
whiteSpace: 'pre-wrap',
},
}));

View File

@ -1,94 +0,0 @@
import { Delete, Edit } from '@material-ui/icons';
import classnames from 'classnames';
import { IN, NOT_IN } from 'constants/operators';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useParams } from 'react-router';
import { IFeatureViewParams } from 'interfaces/params';
import { IConstraint } from 'interfaces/strategy';
import { StrategySeparator } from '../StrategySeparator/StrategySeparator';
import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
import ConditionallyRender from '../ConditionallyRender';
import PermissionIconButton from '../PermissionIconButton/PermissionIconButton';
import StringTruncator from '../StringTruncator/StringTruncator';
import { useStyles } from './Constraint.styles';
import { useLocationSettings } from 'hooks/useLocationSettings';
import { formatConstraintValuesOrValue } from 'component/common/Constraint/formatConstraintValue';
interface IConstraintProps {
constraint: IConstraint;
className?: string;
deleteCallback?: () => void;
editCallback?: () => void;
}
const Constraint = ({
constraint,
deleteCallback,
editCallback,
className,
...rest
}: IConstraintProps) => {
// CHANGEME - Feat: Constraint Operators
const { uiConfig } = useUiConfig();
const styles = useStyles();
const { locationSettings } = useLocationSettings();
const { projectId } = useParams<IFeatureViewParams>();
const classes = classnames(styles.constraint, {
[styles.column]:
Array.isArray(constraint.values) && constraint.values.length > 2,
});
const editable = !!(deleteCallback && editCallback);
// CHANGEME - Feat: Constraint Operators
// Disable the edit button for constraints that are using new operators if
// the new operators are not enabled
const operatorIsNew =
constraint.operator !== IN && constraint.operator !== NOT_IN;
const disabledEdit = !uiConfig.flags.CO && operatorIsNew;
return (
<div className={classes + ' ' + className} {...rest}>
<div className={classes + ' ' + className} {...rest}>
<StringTruncator
text={constraint.contextName}
maxWidth="125"
maxLength={25}
/>
<StrategySeparator text={constraint.operator} maxWidth="none" />
<span className={styles.values}>
{formatConstraintValuesOrValue(
constraint,
locationSettings
)}
</span>
</div>
<ConditionallyRender
condition={editable}
show={
<div className={styles.btnContainer}>
<PermissionIconButton
onClick={editCallback!}
permission={UPDATE_FEATURE}
projectId={projectId}
disabled={disabledEdit}
>
<Edit titleAccess="Edit constraint" />
</PermissionIconButton>
<PermissionIconButton
onClick={deleteCallback!}
permission={UPDATE_FEATURE}
projectId={projectId}
>
<Delete titleAccess="Delete constraint" />
</PermissionIconButton>
</div>
}
/>
</div>
);
};
export default Constraint;

View File

@ -20,40 +20,21 @@ export const useStyles = makeStyles(theme => ({
width: '26px',
height: '26px',
},
accordionRoot: { margin: 0, boxShadow: 'none' },
negated: {
position: 'absolute',
color: '#fff',
backgroundColor: theme.palette.primary.light,
padding: '0.1rem 0.2rem',
fontSize: '0.7rem',
fontWeight: 'bold',
top: '-15px',
left: '42px',
borderRadius: '3px',
},
accordion: {
border: `1px solid ${theme.palette.grey[300]}`,
borderRadius: '5px',
backgroundColor: '#fff',
boxShadow: 'none',
margin: 0,
['&:before']: {
height: 0,
},
accordionRoot: {
'&:before': {
opacity: '0 !important',
},
},
accordionEdit: {
backgroundColor: '#F6F6FA',
},
operator: {
border: `1px solid ${theme.palette.secondary.main}`,
padding: '0.25rem 1rem',
color: theme.palette.secondary.main,
textTransform: 'uppercase',
borderRadius: '5px',
margin: '0rem 2rem',
fontSize: theme.fontSizes.smallBody,
},
headerMetaInfo: {
display: 'flex',
alignItems: 'center',
@ -80,6 +61,14 @@ export const useStyles = makeStyles(theme => ({
headerValuesExpand: {
fontSize: theme.fontSizes.smallBody,
},
headerConstraintContainer: {
minWidth: '220px',
position: 'relative',
paddingRight: '1rem',
[theme.breakpoints.down(650)]: {
paddingRight: 0,
},
},
headerViewValuesContainer: {
[theme.breakpoints.down(990)]: {
display: 'none',
@ -132,6 +121,7 @@ export const useStyles = makeStyles(theme => ({
},
headerActions: {
marginLeft: 'auto',
whiteSpace: 'nowrap',
[theme.breakpoints.down(660)]: {
marginLeft: '0',
marginTop: '0.5rem',
@ -152,7 +142,7 @@ export const useStyles = makeStyles(theme => ({
padding: '0.25rem 1rem',
height: '85px',
[theme.breakpoints.down(770)]: {
height: '175px',
height: '200px',
},
},
settingsParagraph: {

View File

@ -97,7 +97,7 @@ const InvertedOperator = ({
color="primary"
/>
}
label={'negated'}
label="Negated"
/>
</>
);

View File

@ -6,17 +6,17 @@ import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import { ConstraintIcon } from 'component/common/ConstraintAccordion/ConstraintIcon';
import { Help } from '@material-ui/icons';
import ConditionallyRender from 'component/common/ConditionallyRender';
import {
allOperators,
dateOperators,
DATE_AFTER,
IN,
} from 'constants/operators';
import { dateOperators, DATE_AFTER, IN } from 'constants/operators';
import { SAVE } from '../ConstraintAccordionEdit';
import { resolveText } from './helpers';
import { oneOf } from 'utils/oneOf';
import { useEffect } from 'react';
import React, { useEffect } from 'react';
import { Operator } from 'constants/operators';
import { ConstraintOperatorSelect } from 'component/common/ConstraintAccordion/ConstraintOperatorSelect/ConstraintOperatorSelect';
import {
operatorsForContext,
CURRENT_TIME_CONTEXT_FIELD,
} from 'utils/operatorsForContext';
interface IConstraintAccordionViewHeader {
localConstraint: IConstraint;
@ -27,12 +27,6 @@ interface IConstraintAccordionViewHeader {
compact: boolean;
}
const constraintOperators = allOperators.map(operator => {
return { key: operator, label: operator };
});
export const CURRENT_TIME_CONTEXT_FIELD = 'currentTime';
export const ConstraintAccordionEditHeader = ({
compact,
localConstraint,
@ -43,6 +37,7 @@ export const ConstraintAccordionEditHeader = ({
}: IConstraintAccordionViewHeader) => {
const styles = useStyles();
const { context } = useUnleashContext();
const { contextName, operator } = localConstraint;
/* We need a special case to handle the currenTime context field. Since
this field will be the only one to allow DATE_BEFORE and DATE_AFTER operators
@ -51,8 +46,8 @@ export const ConstraintAccordionEditHeader = ({
data). */
useEffect(() => {
if (
localConstraint.contextName === CURRENT_TIME_CONTEXT_FIELD &&
!oneOf(dateOperators, localConstraint.operator)
contextName === CURRENT_TIME_CONTEXT_FIELD &&
!oneOf(dateOperators, operator)
) {
setLocalConstraint(prev => ({
...prev,
@ -60,48 +55,22 @@ export const ConstraintAccordionEditHeader = ({
value: new Date().toISOString(),
}));
} else if (
localConstraint.contextName !== CURRENT_TIME_CONTEXT_FIELD &&
oneOf(dateOperators, localConstraint.operator)
contextName !== CURRENT_TIME_CONTEXT_FIELD &&
oneOf(dateOperators, operator)
) {
setOperator(IN);
}
}, [
localConstraint.contextName,
setOperator,
localConstraint.operator,
setLocalConstraint,
]);
}, [contextName, setOperator, operator, setLocalConstraint]);
if (!context) {
return null;
}
if (!context) return null;
const constraintNameOptions = context.map(context => {
return { key: context.name, label: context.name };
});
const filteredOperators = constraintOperators.filter(operator => {
if (
oneOf(dateOperators, operator.label) &&
localConstraint.contextName !== CURRENT_TIME_CONTEXT_FIELD
) {
return false;
}
if (
!oneOf(dateOperators, operator.label) &&
localConstraint.contextName === CURRENT_TIME_CONTEXT_FIELD
) {
return false;
}
return true;
});
const onChange = (
event: React.ChangeEvent<{
name?: string;
value: unknown;
}>
) => {
const operator = event.target.value as Operator;
const onOperatorChange = (operator: Operator) => {
if (oneOf(dateOperators, operator)) {
setLocalConstraint(prev => ({
...prev,
@ -124,42 +93,34 @@ export const ConstraintAccordionEditHeader = ({
label="Context Field"
autoFocus
options={constraintNameOptions}
value={localConstraint.contextName || ''}
value={contextName || ''}
onChange={e => setContextName(String(e.target.value))}
className={styles.headerSelect}
/>
</div>
<div className={styles.bottomSelect}>
<GeneralSelect
id="operator-select"
name="operator"
label="Operator"
options={filteredOperators}
value={localConstraint.operator}
onChange={onChange}
className={styles.headerSelect}
/>
<div className={styles.headerSelect}>
<ConstraintOperatorSelect
options={operatorsForContext(contextName)}
value={operator}
onChange={onOperatorChange}
/>
</div>
</div>
</div>
<ConditionallyRender
condition={!compact}
show={
<p className={styles.headerText}>
{resolveText(
localConstraint.operator,
localConstraint.contextName
)}
{resolveText(operator, contextName)}
</p>
}
/>
<ConditionallyRender
condition={action === SAVE}
show={<p className={styles.editingBadge}>Updating...</p>}
elseShow={<p className={styles.editingBadge}>Editing</p>}
/>
<a
href="https://docs.getunleash.io/advanced/strategy_constraints"
style={{ marginLeft: 'auto' }}

View File

@ -1,7 +1,7 @@
import { dateOperators } from 'constants/operators';
import { IConstraint } from 'interfaces/strategy';
import { oneOf } from 'utils/oneOf';
import { operatorsForContext } from 'utils/operatorUtils';
import { operatorsForContext } from 'utils/operatorsForContext';
export const createEmptyConstraint = (contextName: string): IConstraint => {
const operator = operatorsForContext(contextName)[0];

View File

@ -41,10 +41,7 @@ export const ConstraintAccordionView = ({
return (
<Accordion
className={styles.accordion}
classes={{
root: styles.accordionRoot,
}}
style={{ boxShadow: 'none' }}
classes={{ root: styles.accordionRoot }}
>
<AccordionSummary
className={styles.summary}

View File

@ -8,7 +8,7 @@ import { oneOf } from 'utils/oneOf';
import ConditionallyRender from 'component/common/ConditionallyRender';
import { useStyles } from 'component/common/ConstraintAccordion/ConstraintAccordion.styles';
import { ConstraintValueSearch } from 'component/common/ConstraintAccordion/ConstraintValueSearch/ConstraintValueSearch';
import { formatConstraintValue } from 'component/common/Constraint/formatConstraintValue';
import { formatConstraintValue } from 'utils/formatConstraintValue';
import { useLocationSettings } from 'hooks/useLocationSettings';
interface IConstraintAccordionViewBodyProps {

View File

@ -11,8 +11,9 @@ import { UPDATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/perm
import { useParams } from 'react-router-dom';
import { IFeatureViewParams } from 'interfaces/params';
import React from 'react';
import { formatConstraintValue } from 'component/common/Constraint/formatConstraintValue';
import { formatConstraintValue } from 'utils/formatConstraintValue';
import { useLocationSettings } from 'hooks/useLocationSettings';
import { ConstraintOperator } from 'component/common/ConstraintAccordion/ConstraintOperator/ConstraintOperator';
interface IConstraintAccordionViewHeaderProps {
compact: boolean;
@ -63,13 +64,8 @@ export const ConstraintAccordionViewHeader = ({
maxLength={25}
/>
</div>
<div style={{ minWidth: '220px', position: 'relative' }}>
<ConditionallyRender
condition={Boolean(constraint.inverted)}
show={<div className={styles.negated}>NOT</div>}
/>
<p className={styles.operator}>{constraint.operator}</p>
<div className={styles.headerConstraintContainer}>
<ConstraintOperator constraint={constraint} />
</div>
<div className={styles.headerViewValuesContainer}>
<ConditionallyRender
@ -85,7 +81,10 @@ export const ConstraintAccordionViewHeader = ({
elseShow={
<div className={styles.headerValuesContainer}>
<p className={styles.headerValues}>
{constraint?.values?.length} values
{constraint?.values?.length}{' '}
{constraint?.values?.length === 1
? 'value'
: 'values'}
</p>
<p className={styles.headerValuesExpand}>
Expand to view

View File

@ -0,0 +1,30 @@
import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({
container: {
padding: '0.5rem 0.75rem',
borderRadius: theme.borders.radius.main,
backgroundColor: theme.palette.grey[200],
lineHeight: 1.25,
},
name: {
fontSize: theme.fontSizes.smallBody,
},
text: {
fontSize: theme.fontSizes.smallerBody,
color: theme.palette.grey[700],
},
not: {
display: 'block',
margin: '-1rem 0 0.25rem 0',
height: '1rem',
'& > span': {
display: 'inline-block',
padding: '0 0.25rem',
borderRadius: theme.borders.radius.main,
fontSize: theme.fontSizes.smallerBody,
backgroundColor: theme.palette.primary.light,
color: 'white',
},
},
}));

View File

@ -0,0 +1,30 @@
import { IConstraint } from 'interfaces/strategy';
import { formatOperatorDescription } from 'component/common/ConstraintAccordion/ConstraintOperator/formatOperatorDescription';
import { useStyles } from 'component/common/ConstraintAccordion/ConstraintOperator/ConstraintOperator.styles';
interface IConstraintOperatorProps {
constraint: IConstraint;
}
export const ConstraintOperator = ({
constraint,
}: IConstraintOperatorProps) => {
const styles = useStyles();
const operatorName = constraint.operator;
const operatorText = formatOperatorDescription(constraint.operator);
const notLabel = constraint.inverted && (
<div className={styles.not}>
<span>NOT</span>
</div>
);
return (
<div className={styles.container}>
{notLabel}
<div className={styles.name}>{operatorName}</div>
<div className={styles.text}>{operatorText}</div>
</div>
);
};

View File

@ -0,0 +1,23 @@
import { Operator } from 'constants/operators';
export const formatOperatorDescription = (operator: Operator): string => {
return constraintOperatorDescriptions[operator];
};
const constraintOperatorDescriptions = {
IN: 'is one of',
NOT_IN: 'is not one of',
STR_CONTAINS: 'is a string that contains',
STR_STARTS_WITH: 'is a string that starts with',
STR_ENDS_WITH: 'is a string that ends with',
NUM_EQ: 'is a number equal to',
NUM_GT: 'is a number greater than',
NUM_GTE: 'is a number greater than or equal to',
NUM_LT: 'is a number less than',
NUM_LTE: 'is a number less than or equal to',
DATE_BEFORE: 'is a date before',
DATE_AFTER: 'is a date after',
SEMVER_EQ: 'is a SemVer equal to',
SEMVER_GT: 'is a SemVer greater than',
SEMVER_LT: 'is a SemVer less than',
};

View File

@ -0,0 +1,37 @@
import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({
valueContainer: {
lineHeight: 1.1,
marginTop: -5,
marginBottom: -10,
},
optionContainer: {
lineHeight: 1.2,
},
label: {
fontSize: theme.fontSizes.smallBody,
},
description: {
fontSize: theme.fontSizes.smallerBody,
color: theme.palette.grey[700],
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
},
separator: {
position: 'relative',
overflow: 'visible',
marginTop: '1rem',
'&:before': {
content: '""',
display: 'block',
position: 'absolute',
top: '-0.5rem',
left: 0,
right: 0,
borderTop: '1px solid',
borderTopColor: theme.palette.grey[300],
},
},
}));

View File

@ -0,0 +1,91 @@
import { Select, MenuItem, FormControl, InputLabel } from '@material-ui/core';
import {
Operator,
stringOperators,
semVerOperators,
dateOperators,
numOperators,
} from 'constants/operators';
import React, { useState, ChangeEvent } from 'react';
import { formatOperatorDescription } from 'component/common/ConstraintAccordion/ConstraintOperator/formatOperatorDescription';
import { useStyles } from 'component/common/ConstraintAccordion/ConstraintOperatorSelect/ConstraintOperatorSelect.styles';
import classNames from 'classnames';
interface IConstraintOperatorSelectProps {
options: Operator[];
value: Operator;
onChange: (value: Operator) => void;
}
export const ConstraintOperatorSelect = ({
options,
value,
onChange,
}: IConstraintOperatorSelectProps) => {
const styles = useStyles();
const [open, setOpen] = useState(false);
const onSelectChange = (
event: ChangeEvent<{ name?: string; value: unknown }>
) => {
onChange(event.target.value as Operator);
};
const renderValue = () => {
return (
<div className={styles.valueContainer}>
<div className={styles.label}>{value}</div>
<div className={styles.description}>
{formatOperatorDescription(value)}
</div>
</div>
);
};
return (
<FormControl variant="outlined" size="small" fullWidth>
<InputLabel htmlFor="operator-select">Operator</InputLabel>
<Select
id="operator-select"
name="operator"
label="Operator"
value={value}
open={open}
onOpen={() => setOpen(true)}
onClose={() => setOpen(false)}
onChange={onSelectChange}
renderValue={renderValue}
>
{options.map(operator => (
<MenuItem
key={operator}
value={operator}
className={classNames(
needSeparatorAbove(operator) && styles.separator
)}
>
<div className={styles.optionContainer}>
<div className={styles.label}>{operator}</div>
<div className={styles.description}>
{formatOperatorDescription(operator)}
</div>
</div>
</MenuItem>
))}
</Select>
</FormControl>
);
};
const needSeparatorAbove = (operator: Operator): boolean => {
const groups = [
stringOperators,
numOperators,
dateOperators,
semVerOperators,
];
return groups.some(group => {
return group[0] === operator;
});
};

View File

@ -2,14 +2,11 @@ import { useTheme } from '@material-ui/core';
interface IStrategySeparatorProps {
text: string;
maxWidth?: string;
}
export const StrategySeparator = ({
text,
maxWidth = '50px',
}: IStrategySeparatorProps) => {
export const StrategySeparator = ({ text }: IStrategySeparatorProps) => {
const theme = useTheme();
return (
<div
style={{
@ -17,10 +14,7 @@ export const StrategySeparator = ({
padding: '0.1rem 0.25rem',
border: `1px solid ${theme.palette.primary.main}`,
borderRadius: '0.25rem',
maxWidth,
fontSize: theme.fontSizes.smallerBody,
textAlign: 'center',
margin: '0.5rem 0rem 0.5rem 1rem',
backgroundColor: '#fff',
}}
>

View File

@ -1,15 +1,6 @@
import { IConstraint, IFeatureStrategy } from 'interfaces/strategy';
import Constraint from 'component/common/Constraint/Constraint';
import Dialogue from 'component/common/Dialogue/Dialogue';
import React, { useState } from 'react';
import StrategyConstraints from 'component/feature/StrategyConstraints/StrategyConstraints';
import { List, ListItem } from '@material-ui/core';
import produce from 'immer';
import {
CREATE_FEATURE_STRATEGY,
UPDATE_FEATURE_STRATEGY,
} from 'component/providers/AccessProvider/permissions';
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
import React, { useMemo } from 'react';
import { ConstraintAccordionList } from 'component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList';
interface IFeatureStrategyConstraintsProps {
projectId: string;
@ -26,102 +17,24 @@ export const FeatureStrategyConstraints = ({
strategy,
setStrategy,
}: IFeatureStrategyConstraintsProps) => {
const [showConstraintsDialog, setShowConstraintsDialog] = useState(false);
const constraints = useMemo(() => {
return strategy.constraints ?? [];
}, [strategy]);
const [constraintErrors, setConstraintErrors] = useState<
Record<string, string>
>({});
const updateConstraints = (constraints: IConstraint[]) => {
setStrategy(prev => ({ ...prev, constraints }));
};
const removeConstraint = (index: number) => {
setStrategy(
produce(draft => {
draft.constraints?.splice(index, 1);
})
);
};
const onConstraintsDialogSave = () => {
const errors = findConstraintErrors(strategy.constraints);
if (Object.keys(errors).length > 0) {
setConstraintErrors(errors);
} else {
setShowConstraintsDialog(false);
}
};
const onConstraintsDialogClose = () => {
setStrategy(
produce(draft => {
draft.constraints = removeEmptyConstraints(draft.constraints);
})
);
setShowConstraintsDialog(false);
const setConstraints = (value: React.SetStateAction<IConstraint[]>) => {
setStrategy(prev => ({
...prev,
constraints: value instanceof Function ? value(constraints) : value,
}));
};
return (
<div>
<List disablePadding dense>
{strategy.constraints?.map((constraint, index) => (
<ListItem key={index} disableGutters dense>
<Constraint
constraint={constraint}
editCallback={() => setShowConstraintsDialog(true)}
deleteCallback={removeConstraint.bind(null, index)}
/>
</ListItem>
))}
</List>
<Dialogue
title="Define constraints"
open={showConstraintsDialog}
onClick={onConstraintsDialogSave}
primaryButtonText="Update constraints"
secondaryButtonText="Cancel"
onClose={onConstraintsDialogClose}
fullWidth
maxWidth="md"
>
<StrategyConstraints
updateConstraints={updateConstraints}
constraints={strategy.constraints ?? []}
constraintError={constraintErrors}
setConstraintError={setConstraintErrors}
/>
</Dialogue>
<PermissionButton
onClick={() => setShowConstraintsDialog(true)}
variant="text"
permission={[UPDATE_FEATURE_STRATEGY, CREATE_FEATURE_STRATEGY]}
environmentId={environmentId}
projectId={projectId}
>
Add constraints
</PermissionButton>
</div>
<ConstraintAccordionList
projectId={projectId}
environmentId={environmentId}
constraints={constraints}
setConstraints={setConstraints}
showCreateButton
/>
);
};
const findConstraintErrors = (
constraints: IConstraint[] = []
): Record<string, string> => {
const entries = constraints
.filter(isEmptyConstraint)
.map((constraint, index) => `${constraint.contextName}-${index}`)
.map(id => [id, 'You need to specify at least one value']);
return Object.fromEntries(entries);
};
const removeEmptyConstraints = (
constraints: IConstraint[] = []
): IConstraint[] => {
return constraints.filter(constraint => !isEmptyConstraint(constraint));
};
const isEmptyConstraint = (constraint: IConstraint): boolean => {
return !constraint.values || constraint.values.length === 0;
};

View File

@ -1,40 +0,0 @@
import { IConstraint, IFeatureStrategy } from 'interfaces/strategy';
import React, { useMemo } from 'react';
import { ConstraintAccordionList } from 'component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList';
interface IFeatureStrategyConstraintsProps {
projectId: string;
environmentId: string;
strategy: Partial<IFeatureStrategy>;
setStrategy: React.Dispatch<
React.SetStateAction<Partial<IFeatureStrategy>>
>;
}
export const FeatureStrategyConstraintsCO = ({
projectId,
environmentId,
strategy,
setStrategy,
}: IFeatureStrategyConstraintsProps) => {
const constraints = useMemo(() => {
return strategy.constraints ?? [];
}, [strategy]);
const setConstraints = (value: React.SetStateAction<IConstraint[]>) => {
setStrategy(prev => ({
...prev,
constraints: value instanceof Function ? value(constraints) : value,
}));
};
return (
<ConstraintAccordionList
projectId={projectId}
environmentId={environmentId}
constraints={constraints}
setConstraints={setConstraints}
showCreateButton
/>
);
};

View File

@ -18,7 +18,6 @@ import { STRATEGY_FORM_SUBMIT_ID } from 'utils/testIds';
import { useConstraintsValidation } from 'hooks/api/getters/useConstraintsValidation/useConstraintsValidation';
import AccessContext from 'contexts/AccessContext';
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
import { FeatureStrategyConstraintsCO } from 'component/feature/FeatureStrategy/FeatureStrategyConstraints/FeatureStrategyConstraintsCO';
import { FeatureStrategySegment } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegment';
import { ISegment } from 'interfaces/segment';
@ -77,19 +76,11 @@ export const FeatureStrategyForm = ({
throw uiConfigError;
}
// Wait for uiConfig to load for the correct uiConfig.flags.CO value.
// Wait for uiConfig to load to get the correct flags.
if (uiConfigLoading) {
return null;
}
// TODO(olav): Remove FeatureStrategyConstraints when CO is out.
const FeatureStrategyConstraintsImplementation = uiConfig.flags.CO
? FeatureStrategyConstraintsCO
: FeatureStrategyConstraints;
const disableSubmitButtonFromConstraints = uiConfig.flags.CO
? !hasValidConstraints
: false;
return (
<form className={styles.form} onSubmit={onSubmitOrProdGuard}>
<div>
@ -109,9 +100,9 @@ export const FeatureStrategyForm = ({
}
/>
<ConditionallyRender
condition={Boolean(uiConfig.flags.C)}
condition={Boolean(uiConfig.flags.C || uiConfig.flags.CO)}
show={
<FeatureStrategyConstraintsImplementation
<FeatureStrategyConstraints
projectId={feature.project}
environmentId={environmentId}
strategy={strategy}
@ -120,7 +111,9 @@ export const FeatureStrategyForm = ({
}
/>
<ConditionallyRender
condition={Boolean(uiConfig.flags.SE || uiConfig.flags.C)}
condition={Boolean(
uiConfig.flags.SE || uiConfig.flags.C || uiConfig.flags.CO
)}
show={<hr className={styles.hr} />}
/>
<FeatureStrategyType
@ -141,7 +134,7 @@ export const FeatureStrategyForm = ({
variant="contained"
color="primary"
type="submit"
disabled={loading || disableSubmitButtonFromConstraints}
disabled={loading || !hasValidConstraints}
data-test={STRATEGY_FORM_SUBMIT_ID}
>
Save strategy

View File

@ -61,6 +61,7 @@ export const useStyles = makeStyles(theme => ({
},
separatorText: {
fontSize: theme.fontSizes.smallBody,
textAlign: 'center',
padding: '0 1rem',
},
rightWing: {

View File

@ -10,28 +10,16 @@ const FeatureOverviewEnvironmentStrategies = ({
strategies,
environmentName,
}: FeatureOverviewEnvironmentStrategiesProps) => {
const renderStrategies = () => {
return strategies.map(strategy => {
return (
return (
<>
{strategies.map(strategy => (
<FeatureOverviewEnvironmentStrategy
key={strategy.id}
strategy={strategy}
environmentId={environmentName}
/>
);
});
};
return (
<div
style={{
display: 'flex',
justifyContent: 'center',
flexWrap: 'wrap',
}}
>
{renderStrategies()}
</div>
))}
</>
);
};

View File

@ -2,10 +2,11 @@ import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({
container: {
borderRadius: '12.5px',
borderRadius: theme.borders.radius.main,
border: `1px solid ${theme.palette.grey[300]}`,
width: '400px',
margin: '0.3rem',
'& + &': {
marginTop: '1rem',
},
},
header: {
padding: '0.5rem',
@ -22,9 +23,8 @@ export const useStyles = makeStyles(theme => ({
},
body: {
padding: '1rem',
display: 'flex',
justifyContent: 'center',
flexDirection: 'column',
alignItems: 'center',
display: 'grid',
gap: '1rem',
justifyItems: 'center',
},
}));

View File

@ -65,7 +65,6 @@ const FeatureOverviewEnvironmentStrategy = ({
/>
</div>
</div>
<div className={styles.body}>
<FeatureOverviewExecution
parameters={parameters}

View File

@ -1,38 +0,0 @@
import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({
constraintsContainer: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
marginTop: '0.5rem',
width: '100%',
},
constraint: {
fontSize: theme.fontSizes.smallBody,
alignItems: 'center;',
margin: '0.5rem 0',
display: 'flex',
border: `1px solid ${theme.palette.grey[300]}`,
padding: '0.2rem',
borderRadius: '5px',
},
constraintName: {
minWidth: '100px',
marginRight: '0.5rem',
},
constraintOperator: {
marginRight: '0.5rem',
},
constraintValues: {
textOverflow: 'ellipsis',
overflow: 'hidden',
maxWidth: '50%',
},
text: {
textAlign: 'center',
margin: '0.2rem 0 0.5rem',
display: 'flex',
alignItems: 'center',
},
}));

View File

@ -3,13 +3,12 @@ import { IConstraint, IFeatureStrategy, IParameter } from 'interfaces/strategy';
import ConditionallyRender from 'component/common/ConditionallyRender';
import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
import { useStyles } from './FeatureOverviewExecution.styles';
import FeatureOverviewExecutionChips from './FeatureOverviewExecutionChips/FeatureOverviewExecutionChips';
import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
import Constraint from 'component/common/Constraint/Constraint';
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { FeatureOverviewSegment } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSegment/FeatureOverviewSegment';
import { ConstraintAccordionList } from 'component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList';
interface IFeatureOverviewExecutionProps {
parameters: IParameter;
@ -23,46 +22,27 @@ const FeatureOverviewExecution = ({
constraints = [],
strategy,
}: IFeatureOverviewExecutionProps) => {
const styles = useStyles();
const { strategies } = useStrategies();
const { uiConfig } = useUiConfig();
if (!parameters) return null;
if (!parameters) {
return null;
}
const definition = strategies.find(strategyDefinition => {
return strategyDefinition.name === strategy.name;
});
const renderConstraints = () => {
return constraints.map((constraint, index) => {
if (index !== constraints.length - 1) {
return (
<Fragment key={`${constraint.contextName}-${index}`}>
<Constraint constraint={constraint} />
<StrategySeparator text="AND" />
</Fragment>
);
}
return (
<Constraint
constraint={constraint}
key={`${constraint.contextName}-${index}`}
/>
);
});
};
const renderParameters = () => {
if (definition?.editable) return null;
return Object.keys(parameters).map((key, index) => {
return Object.keys(parameters).map(key => {
switch (key) {
case 'rollout':
case 'Rollout':
return (
<Fragment key={key}>
<p className={styles.text}>
<p>
{parameters[key]}% of your base{' '}
{constraints.length > 0
? 'who match constraints'
@ -145,7 +125,7 @@ const FeatureOverviewExecution = ({
case 'percentage':
return (
<Fragment key={param?.name}>
<p className={styles.text}>
<p>
{strategy?.parameters[param.name]}% of your base{' '}
{constraints?.length > 0
? 'who match constraints'
@ -165,7 +145,7 @@ const FeatureOverviewExecution = ({
case 'boolean':
return (
<Fragment key={param.name}>
<p className={styles.text} key={param.name}>
<p key={param.name}>
<StringTruncator
maxLength={15}
maxWidth="150"
@ -192,7 +172,7 @@ const FeatureOverviewExecution = ({
key={param.name}
show={
<>
<p className={styles.text}>
<p>
<StringTruncator
maxWidth="150"
maxLength={15}
@ -216,7 +196,7 @@ const FeatureOverviewExecution = ({
key={param.name}
show={
<>
<p className={styles.text}>
<p>
<StringTruncator
maxLength={15}
maxWidth="150"
@ -248,10 +228,10 @@ const FeatureOverviewExecution = ({
<ConditionallyRender
condition={constraints.length > 0}
show={
<div className={styles.constraintsContainer}>
{renderConstraints()}
<>
<ConstraintAccordionList constraints={constraints} />
<StrategySeparator text="AND" />
</div>
</>
}
/>
<ConditionallyRender

View File

@ -6,7 +6,6 @@ export const useStyles = makeStyles(theme => ({
padding: '1rem',
fontSize: theme.fontSizes.smallBody,
backgroundColor: theme.palette.grey[200],
margin: '0.5rem 0',
position: 'relative',
borderRadius: '5px',
textAlign: 'center',

View File

@ -32,29 +32,31 @@ export const SEMVER_GT = 'SEMVER_GT';
export const SEMVER_LT = 'SEMVER_LT';
export const allOperators: Operator[] = [
NOT_IN,
IN,
STR_ENDS_WITH,
STR_STARTS_WITH,
NOT_IN,
STR_CONTAINS,
STR_STARTS_WITH,
STR_ENDS_WITH,
NUM_EQ,
NUM_GT,
NUM_GTE,
NUM_LT,
NUM_LTE,
DATE_AFTER,
DATE_BEFORE,
DATE_AFTER,
SEMVER_EQ,
SEMVER_GT,
SEMVER_LT,
];
export const stringOperators: Operator[] = [
STR_ENDS_WITH,
STR_STARTS_WITH,
STR_CONTAINS,
STR_STARTS_WITH,
STR_ENDS_WITH,
];
export const inOperators: Operator[] = [IN, NOT_IN];
export const numOperators: Operator[] = [
NUM_EQ,
NUM_GT,
@ -62,7 +64,9 @@ export const numOperators: Operator[] = [
NUM_LT,
NUM_LTE,
];
export const dateOperators: Operator[] = [DATE_AFTER, DATE_BEFORE];
export const dateOperators: Operator[] = [DATE_BEFORE, DATE_AFTER];
export const semVerOperators: Operator[] = [SEMVER_EQ, SEMVER_GT, SEMVER_LT];
export const singleValueOperators: Operator[] = [

View File

@ -1,7 +1,7 @@
import { IConstraint } from 'interfaces/strategy';
import { CURRENT_TIME_CONTEXT_FIELD } from 'component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditHeader/ConstraintAccordionEditHeader';
import { formatDateYMDHMS } from 'utils/formatDate';
import { ILocationSettings } from 'hooks/useLocationSettings';
import { CURRENT_TIME_CONTEXT_FIELD } from 'utils/operatorsForContext';
export const formatConstraintValuesOrValue = (
constraint: IConstraint,

View File

@ -1,7 +1,8 @@
import { CURRENT_TIME_CONTEXT_FIELD } from 'component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditHeader/ConstraintAccordionEditHeader';
import { allOperators, dateOperators, Operator } from 'constants/operators';
import { oneOf } from 'utils/oneOf';
export const CURRENT_TIME_CONTEXT_FIELD = 'currentTime';
export const operatorsForContext = (contextName: string): Operator[] => {
return allOperators.filter(operator => {
if (