mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-09 01:17:06 +02: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:
parent
e909d22300
commit
f33ca9db4b
@ -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',
|
|
||||||
},
|
|
||||||
}));
|
|
@ -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;
|
|
@ -20,40 +20,21 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
width: '26px',
|
width: '26px',
|
||||||
height: '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: {
|
accordion: {
|
||||||
border: `1px solid ${theme.palette.grey[300]}`,
|
border: `1px solid ${theme.palette.grey[300]}`,
|
||||||
borderRadius: '5px',
|
borderRadius: '5px',
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
|
boxShadow: 'none',
|
||||||
margin: 0,
|
margin: 0,
|
||||||
|
},
|
||||||
['&:before']: {
|
accordionRoot: {
|
||||||
height: 0,
|
'&:before': {
|
||||||
|
opacity: '0 !important',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
accordionEdit: {
|
accordionEdit: {
|
||||||
backgroundColor: '#F6F6FA',
|
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: {
|
headerMetaInfo: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@ -80,6 +61,14 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
headerValuesExpand: {
|
headerValuesExpand: {
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
},
|
},
|
||||||
|
headerConstraintContainer: {
|
||||||
|
minWidth: '220px',
|
||||||
|
position: 'relative',
|
||||||
|
paddingRight: '1rem',
|
||||||
|
[theme.breakpoints.down(650)]: {
|
||||||
|
paddingRight: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
headerViewValuesContainer: {
|
headerViewValuesContainer: {
|
||||||
[theme.breakpoints.down(990)]: {
|
[theme.breakpoints.down(990)]: {
|
||||||
display: 'none',
|
display: 'none',
|
||||||
@ -132,6 +121,7 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
},
|
},
|
||||||
headerActions: {
|
headerActions: {
|
||||||
marginLeft: 'auto',
|
marginLeft: 'auto',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
[theme.breakpoints.down(660)]: {
|
[theme.breakpoints.down(660)]: {
|
||||||
marginLeft: '0',
|
marginLeft: '0',
|
||||||
marginTop: '0.5rem',
|
marginTop: '0.5rem',
|
||||||
@ -152,7 +142,7 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
padding: '0.25rem 1rem',
|
padding: '0.25rem 1rem',
|
||||||
height: '85px',
|
height: '85px',
|
||||||
[theme.breakpoints.down(770)]: {
|
[theme.breakpoints.down(770)]: {
|
||||||
height: '175px',
|
height: '200px',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
settingsParagraph: {
|
settingsParagraph: {
|
||||||
|
@ -97,7 +97,7 @@ const InvertedOperator = ({
|
|||||||
color="primary"
|
color="primary"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={'negated'}
|
label="Negated"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -6,17 +6,17 @@ import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
|
|||||||
import { ConstraintIcon } from 'component/common/ConstraintAccordion/ConstraintIcon';
|
import { ConstraintIcon } from 'component/common/ConstraintAccordion/ConstraintIcon';
|
||||||
import { Help } from '@material-ui/icons';
|
import { Help } from '@material-ui/icons';
|
||||||
import ConditionallyRender from 'component/common/ConditionallyRender';
|
import ConditionallyRender from 'component/common/ConditionallyRender';
|
||||||
import {
|
import { dateOperators, DATE_AFTER, IN } from 'constants/operators';
|
||||||
allOperators,
|
|
||||||
dateOperators,
|
|
||||||
DATE_AFTER,
|
|
||||||
IN,
|
|
||||||
} from 'constants/operators';
|
|
||||||
import { SAVE } from '../ConstraintAccordionEdit';
|
import { SAVE } from '../ConstraintAccordionEdit';
|
||||||
import { resolveText } from './helpers';
|
import { resolveText } from './helpers';
|
||||||
import { oneOf } from 'utils/oneOf';
|
import { oneOf } from 'utils/oneOf';
|
||||||
import { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Operator } from 'constants/operators';
|
import { Operator } from 'constants/operators';
|
||||||
|
import { ConstraintOperatorSelect } from 'component/common/ConstraintAccordion/ConstraintOperatorSelect/ConstraintOperatorSelect';
|
||||||
|
import {
|
||||||
|
operatorsForContext,
|
||||||
|
CURRENT_TIME_CONTEXT_FIELD,
|
||||||
|
} from 'utils/operatorsForContext';
|
||||||
|
|
||||||
interface IConstraintAccordionViewHeader {
|
interface IConstraintAccordionViewHeader {
|
||||||
localConstraint: IConstraint;
|
localConstraint: IConstraint;
|
||||||
@ -27,12 +27,6 @@ interface IConstraintAccordionViewHeader {
|
|||||||
compact: boolean;
|
compact: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const constraintOperators = allOperators.map(operator => {
|
|
||||||
return { key: operator, label: operator };
|
|
||||||
});
|
|
||||||
|
|
||||||
export const CURRENT_TIME_CONTEXT_FIELD = 'currentTime';
|
|
||||||
|
|
||||||
export const ConstraintAccordionEditHeader = ({
|
export const ConstraintAccordionEditHeader = ({
|
||||||
compact,
|
compact,
|
||||||
localConstraint,
|
localConstraint,
|
||||||
@ -43,6 +37,7 @@ export const ConstraintAccordionEditHeader = ({
|
|||||||
}: IConstraintAccordionViewHeader) => {
|
}: IConstraintAccordionViewHeader) => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const { context } = useUnleashContext();
|
const { context } = useUnleashContext();
|
||||||
|
const { contextName, operator } = localConstraint;
|
||||||
|
|
||||||
/* We need a special case to handle the currenTime context field. Since
|
/* 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
|
this field will be the only one to allow DATE_BEFORE and DATE_AFTER operators
|
||||||
@ -51,8 +46,8 @@ export const ConstraintAccordionEditHeader = ({
|
|||||||
data). */
|
data). */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
localConstraint.contextName === CURRENT_TIME_CONTEXT_FIELD &&
|
contextName === CURRENT_TIME_CONTEXT_FIELD &&
|
||||||
!oneOf(dateOperators, localConstraint.operator)
|
!oneOf(dateOperators, operator)
|
||||||
) {
|
) {
|
||||||
setLocalConstraint(prev => ({
|
setLocalConstraint(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
@ -60,48 +55,22 @@ export const ConstraintAccordionEditHeader = ({
|
|||||||
value: new Date().toISOString(),
|
value: new Date().toISOString(),
|
||||||
}));
|
}));
|
||||||
} else if (
|
} else if (
|
||||||
localConstraint.contextName !== CURRENT_TIME_CONTEXT_FIELD &&
|
contextName !== CURRENT_TIME_CONTEXT_FIELD &&
|
||||||
oneOf(dateOperators, localConstraint.operator)
|
oneOf(dateOperators, operator)
|
||||||
) {
|
) {
|
||||||
setOperator(IN);
|
setOperator(IN);
|
||||||
}
|
}
|
||||||
}, [
|
}, [contextName, setOperator, operator, setLocalConstraint]);
|
||||||
localConstraint.contextName,
|
|
||||||
setOperator,
|
if (!context) {
|
||||||
localConstraint.operator,
|
return null;
|
||||||
setLocalConstraint,
|
}
|
||||||
]);
|
|
||||||
|
|
||||||
if (!context) return null;
|
|
||||||
const constraintNameOptions = context.map(context => {
|
const constraintNameOptions = context.map(context => {
|
||||||
return { key: context.name, label: context.name };
|
return { key: context.name, label: context.name };
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredOperators = constraintOperators.filter(operator => {
|
const onOperatorChange = (operator: 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;
|
|
||||||
if (oneOf(dateOperators, operator)) {
|
if (oneOf(dateOperators, operator)) {
|
||||||
setLocalConstraint(prev => ({
|
setLocalConstraint(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
@ -124,42 +93,34 @@ export const ConstraintAccordionEditHeader = ({
|
|||||||
label="Context Field"
|
label="Context Field"
|
||||||
autoFocus
|
autoFocus
|
||||||
options={constraintNameOptions}
|
options={constraintNameOptions}
|
||||||
value={localConstraint.contextName || ''}
|
value={contextName || ''}
|
||||||
onChange={e => setContextName(String(e.target.value))}
|
onChange={e => setContextName(String(e.target.value))}
|
||||||
className={styles.headerSelect}
|
className={styles.headerSelect}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.bottomSelect}>
|
<div className={styles.bottomSelect}>
|
||||||
<GeneralSelect
|
<div className={styles.headerSelect}>
|
||||||
id="operator-select"
|
<ConstraintOperatorSelect
|
||||||
name="operator"
|
options={operatorsForContext(contextName)}
|
||||||
label="Operator"
|
value={operator}
|
||||||
options={filteredOperators}
|
onChange={onOperatorChange}
|
||||||
value={localConstraint.operator}
|
|
||||||
onChange={onChange}
|
|
||||||
className={styles.headerSelect}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={!compact}
|
condition={!compact}
|
||||||
show={
|
show={
|
||||||
<p className={styles.headerText}>
|
<p className={styles.headerText}>
|
||||||
{resolveText(
|
{resolveText(operator, contextName)}
|
||||||
localConstraint.operator,
|
|
||||||
localConstraint.contextName
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={action === SAVE}
|
condition={action === SAVE}
|
||||||
show={<p className={styles.editingBadge}>Updating...</p>}
|
show={<p className={styles.editingBadge}>Updating...</p>}
|
||||||
elseShow={<p className={styles.editingBadge}>Editing</p>}
|
elseShow={<p className={styles.editingBadge}>Editing</p>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://docs.getunleash.io/advanced/strategy_constraints"
|
href="https://docs.getunleash.io/advanced/strategy_constraints"
|
||||||
style={{ marginLeft: 'auto' }}
|
style={{ marginLeft: 'auto' }}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { dateOperators } from 'constants/operators';
|
import { dateOperators } from 'constants/operators';
|
||||||
import { IConstraint } from 'interfaces/strategy';
|
import { IConstraint } from 'interfaces/strategy';
|
||||||
import { oneOf } from 'utils/oneOf';
|
import { oneOf } from 'utils/oneOf';
|
||||||
import { operatorsForContext } from 'utils/operatorUtils';
|
import { operatorsForContext } from 'utils/operatorsForContext';
|
||||||
|
|
||||||
export const createEmptyConstraint = (contextName: string): IConstraint => {
|
export const createEmptyConstraint = (contextName: string): IConstraint => {
|
||||||
const operator = operatorsForContext(contextName)[0];
|
const operator = operatorsForContext(contextName)[0];
|
||||||
|
@ -41,10 +41,7 @@ export const ConstraintAccordionView = ({
|
|||||||
return (
|
return (
|
||||||
<Accordion
|
<Accordion
|
||||||
className={styles.accordion}
|
className={styles.accordion}
|
||||||
classes={{
|
classes={{ root: styles.accordionRoot }}
|
||||||
root: styles.accordionRoot,
|
|
||||||
}}
|
|
||||||
style={{ boxShadow: 'none' }}
|
|
||||||
>
|
>
|
||||||
<AccordionSummary
|
<AccordionSummary
|
||||||
className={styles.summary}
|
className={styles.summary}
|
||||||
|
@ -8,7 +8,7 @@ import { oneOf } from 'utils/oneOf';
|
|||||||
import ConditionallyRender from 'component/common/ConditionallyRender';
|
import ConditionallyRender from 'component/common/ConditionallyRender';
|
||||||
import { useStyles } from 'component/common/ConstraintAccordion/ConstraintAccordion.styles';
|
import { useStyles } from 'component/common/ConstraintAccordion/ConstraintAccordion.styles';
|
||||||
import { ConstraintValueSearch } from 'component/common/ConstraintAccordion/ConstraintValueSearch/ConstraintValueSearch';
|
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';
|
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||||
|
|
||||||
interface IConstraintAccordionViewBodyProps {
|
interface IConstraintAccordionViewBodyProps {
|
||||||
|
@ -11,8 +11,9 @@ import { UPDATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/perm
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { IFeatureViewParams } from 'interfaces/params';
|
import { IFeatureViewParams } from 'interfaces/params';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { formatConstraintValue } from 'component/common/Constraint/formatConstraintValue';
|
import { formatConstraintValue } from 'utils/formatConstraintValue';
|
||||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||||
|
import { ConstraintOperator } from 'component/common/ConstraintAccordion/ConstraintOperator/ConstraintOperator';
|
||||||
|
|
||||||
interface IConstraintAccordionViewHeaderProps {
|
interface IConstraintAccordionViewHeaderProps {
|
||||||
compact: boolean;
|
compact: boolean;
|
||||||
@ -63,13 +64,8 @@ export const ConstraintAccordionViewHeader = ({
|
|||||||
maxLength={25}
|
maxLength={25}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={styles.headerConstraintContainer}>
|
||||||
<div style={{ minWidth: '220px', position: 'relative' }}>
|
<ConstraintOperator constraint={constraint} />
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(constraint.inverted)}
|
|
||||||
show={<div className={styles.negated}>NOT</div>}
|
|
||||||
/>
|
|
||||||
<p className={styles.operator}>{constraint.operator}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.headerViewValuesContainer}>
|
<div className={styles.headerViewValuesContainer}>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
@ -85,7 +81,10 @@ export const ConstraintAccordionViewHeader = ({
|
|||||||
elseShow={
|
elseShow={
|
||||||
<div className={styles.headerValuesContainer}>
|
<div className={styles.headerValuesContainer}>
|
||||||
<p className={styles.headerValues}>
|
<p className={styles.headerValues}>
|
||||||
{constraint?.values?.length} values
|
{constraint?.values?.length}{' '}
|
||||||
|
{constraint?.values?.length === 1
|
||||||
|
? 'value'
|
||||||
|
: 'values'}
|
||||||
</p>
|
</p>
|
||||||
<p className={styles.headerValuesExpand}>
|
<p className={styles.headerValuesExpand}>
|
||||||
Expand to view
|
Expand to view
|
||||||
|
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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',
|
||||||
|
};
|
@ -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],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
@ -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;
|
||||||
|
});
|
||||||
|
};
|
@ -2,14 +2,11 @@ import { useTheme } from '@material-ui/core';
|
|||||||
|
|
||||||
interface IStrategySeparatorProps {
|
interface IStrategySeparatorProps {
|
||||||
text: string;
|
text: string;
|
||||||
maxWidth?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StrategySeparator = ({
|
export const StrategySeparator = ({ text }: IStrategySeparatorProps) => {
|
||||||
text,
|
|
||||||
maxWidth = '50px',
|
|
||||||
}: IStrategySeparatorProps) => {
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -17,10 +14,7 @@ export const StrategySeparator = ({
|
|||||||
padding: '0.1rem 0.25rem',
|
padding: '0.1rem 0.25rem',
|
||||||
border: `1px solid ${theme.palette.primary.main}`,
|
border: `1px solid ${theme.palette.primary.main}`,
|
||||||
borderRadius: '0.25rem',
|
borderRadius: '0.25rem',
|
||||||
maxWidth,
|
|
||||||
fontSize: theme.fontSizes.smallerBody,
|
fontSize: theme.fontSizes.smallerBody,
|
||||||
textAlign: 'center',
|
|
||||||
margin: '0.5rem 0rem 0.5rem 1rem',
|
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -1,15 +1,6 @@
|
|||||||
import { IConstraint, IFeatureStrategy } from 'interfaces/strategy';
|
import { IConstraint, IFeatureStrategy } from 'interfaces/strategy';
|
||||||
import Constraint from 'component/common/Constraint/Constraint';
|
import React, { useMemo } from 'react';
|
||||||
import Dialogue from 'component/common/Dialogue/Dialogue';
|
import { ConstraintAccordionList } from 'component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList';
|
||||||
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';
|
|
||||||
|
|
||||||
interface IFeatureStrategyConstraintsProps {
|
interface IFeatureStrategyConstraintsProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -26,102 +17,24 @@ export const FeatureStrategyConstraints = ({
|
|||||||
strategy,
|
strategy,
|
||||||
setStrategy,
|
setStrategy,
|
||||||
}: IFeatureStrategyConstraintsProps) => {
|
}: IFeatureStrategyConstraintsProps) => {
|
||||||
const [showConstraintsDialog, setShowConstraintsDialog] = useState(false);
|
const constraints = useMemo(() => {
|
||||||
|
return strategy.constraints ?? [];
|
||||||
|
}, [strategy]);
|
||||||
|
|
||||||
const [constraintErrors, setConstraintErrors] = useState<
|
const setConstraints = (value: React.SetStateAction<IConstraint[]>) => {
|
||||||
Record<string, string>
|
setStrategy(prev => ({
|
||||||
>({});
|
...prev,
|
||||||
|
constraints: value instanceof Function ? value(constraints) : value,
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<ConstraintAccordionList
|
||||||
<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}
|
projectId={projectId}
|
||||||
>
|
environmentId={environmentId}
|
||||||
Add constraints
|
constraints={constraints}
|
||||||
</PermissionButton>
|
setConstraints={setConstraints}
|
||||||
</div>
|
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;
|
|
||||||
};
|
|
||||||
|
@ -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
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
@ -18,7 +18,6 @@ import { STRATEGY_FORM_SUBMIT_ID } from 'utils/testIds';
|
|||||||
import { useConstraintsValidation } from 'hooks/api/getters/useConstraintsValidation/useConstraintsValidation';
|
import { useConstraintsValidation } from 'hooks/api/getters/useConstraintsValidation/useConstraintsValidation';
|
||||||
import AccessContext from 'contexts/AccessContext';
|
import AccessContext from 'contexts/AccessContext';
|
||||||
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||||
import { FeatureStrategyConstraintsCO } from 'component/feature/FeatureStrategy/FeatureStrategyConstraints/FeatureStrategyConstraintsCO';
|
|
||||||
import { FeatureStrategySegment } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegment';
|
import { FeatureStrategySegment } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegment';
|
||||||
import { ISegment } from 'interfaces/segment';
|
import { ISegment } from 'interfaces/segment';
|
||||||
|
|
||||||
@ -77,19 +76,11 @@ export const FeatureStrategyForm = ({
|
|||||||
throw uiConfigError;
|
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) {
|
if (uiConfigLoading) {
|
||||||
return null;
|
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 (
|
return (
|
||||||
<form className={styles.form} onSubmit={onSubmitOrProdGuard}>
|
<form className={styles.form} onSubmit={onSubmitOrProdGuard}>
|
||||||
<div>
|
<div>
|
||||||
@ -109,9 +100,9 @@ export const FeatureStrategyForm = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={Boolean(uiConfig.flags.C)}
|
condition={Boolean(uiConfig.flags.C || uiConfig.flags.CO)}
|
||||||
show={
|
show={
|
||||||
<FeatureStrategyConstraintsImplementation
|
<FeatureStrategyConstraints
|
||||||
projectId={feature.project}
|
projectId={feature.project}
|
||||||
environmentId={environmentId}
|
environmentId={environmentId}
|
||||||
strategy={strategy}
|
strategy={strategy}
|
||||||
@ -120,7 +111,9 @@ export const FeatureStrategyForm = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<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} />}
|
show={<hr className={styles.hr} />}
|
||||||
/>
|
/>
|
||||||
<FeatureStrategyType
|
<FeatureStrategyType
|
||||||
@ -141,7 +134,7 @@ export const FeatureStrategyForm = ({
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading || disableSubmitButtonFromConstraints}
|
disabled={loading || !hasValidConstraints}
|
||||||
data-test={STRATEGY_FORM_SUBMIT_ID}
|
data-test={STRATEGY_FORM_SUBMIT_ID}
|
||||||
>
|
>
|
||||||
Save strategy
|
Save strategy
|
||||||
|
@ -61,6 +61,7 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
},
|
},
|
||||||
separatorText: {
|
separatorText: {
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
textAlign: 'center',
|
||||||
padding: '0 1rem',
|
padding: '0 1rem',
|
||||||
},
|
},
|
||||||
rightWing: {
|
rightWing: {
|
||||||
|
@ -10,28 +10,16 @@ const FeatureOverviewEnvironmentStrategies = ({
|
|||||||
strategies,
|
strategies,
|
||||||
environmentName,
|
environmentName,
|
||||||
}: FeatureOverviewEnvironmentStrategiesProps) => {
|
}: FeatureOverviewEnvironmentStrategiesProps) => {
|
||||||
const renderStrategies = () => {
|
|
||||||
return strategies.map(strategy => {
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{strategies.map(strategy => (
|
||||||
<FeatureOverviewEnvironmentStrategy
|
<FeatureOverviewEnvironmentStrategy
|
||||||
key={strategy.id}
|
key={strategy.id}
|
||||||
strategy={strategy}
|
strategy={strategy}
|
||||||
environmentId={environmentName}
|
environmentId={environmentName}
|
||||||
/>
|
/>
|
||||||
);
|
))}
|
||||||
});
|
</>
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{renderStrategies()}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,10 +2,11 @@ import { makeStyles } from '@material-ui/core/styles';
|
|||||||
|
|
||||||
export const useStyles = makeStyles(theme => ({
|
export const useStyles = makeStyles(theme => ({
|
||||||
container: {
|
container: {
|
||||||
borderRadius: '12.5px',
|
borderRadius: theme.borders.radius.main,
|
||||||
border: `1px solid ${theme.palette.grey[300]}`,
|
border: `1px solid ${theme.palette.grey[300]}`,
|
||||||
width: '400px',
|
'& + &': {
|
||||||
margin: '0.3rem',
|
marginTop: '1rem',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
padding: '0.5rem',
|
padding: '0.5rem',
|
||||||
@ -22,9 +23,8 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
padding: '1rem',
|
padding: '1rem',
|
||||||
display: 'flex',
|
display: 'grid',
|
||||||
justifyContent: 'center',
|
gap: '1rem',
|
||||||
flexDirection: 'column',
|
justifyItems: 'center',
|
||||||
alignItems: 'center',
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -65,7 +65,6 @@ const FeatureOverviewEnvironmentStrategy = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
<FeatureOverviewExecution
|
<FeatureOverviewExecution
|
||||||
parameters={parameters}
|
parameters={parameters}
|
||||||
|
@ -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',
|
|
||||||
},
|
|
||||||
}));
|
|
@ -3,13 +3,12 @@ import { IConstraint, IFeatureStrategy, IParameter } from 'interfaces/strategy';
|
|||||||
import ConditionallyRender from 'component/common/ConditionallyRender';
|
import ConditionallyRender from 'component/common/ConditionallyRender';
|
||||||
import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle';
|
import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle';
|
||||||
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
|
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
|
||||||
import { useStyles } from './FeatureOverviewExecution.styles';
|
|
||||||
import FeatureOverviewExecutionChips from './FeatureOverviewExecutionChips/FeatureOverviewExecutionChips';
|
import FeatureOverviewExecutionChips from './FeatureOverviewExecutionChips/FeatureOverviewExecutionChips';
|
||||||
import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
|
import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
|
||||||
import Constraint from 'component/common/Constraint/Constraint';
|
|
||||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { FeatureOverviewSegment } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSegment/FeatureOverviewSegment';
|
import { FeatureOverviewSegment } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSegment/FeatureOverviewSegment';
|
||||||
|
import { ConstraintAccordionList } from 'component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList';
|
||||||
|
|
||||||
interface IFeatureOverviewExecutionProps {
|
interface IFeatureOverviewExecutionProps {
|
||||||
parameters: IParameter;
|
parameters: IParameter;
|
||||||
@ -23,46 +22,27 @@ const FeatureOverviewExecution = ({
|
|||||||
constraints = [],
|
constraints = [],
|
||||||
strategy,
|
strategy,
|
||||||
}: IFeatureOverviewExecutionProps) => {
|
}: IFeatureOverviewExecutionProps) => {
|
||||||
const styles = useStyles();
|
|
||||||
const { strategies } = useStrategies();
|
const { strategies } = useStrategies();
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
|
|
||||||
if (!parameters) return null;
|
if (!parameters) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const definition = strategies.find(strategyDefinition => {
|
const definition = strategies.find(strategyDefinition => {
|
||||||
return strategyDefinition.name === strategy.name;
|
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 = () => {
|
const renderParameters = () => {
|
||||||
if (definition?.editable) return null;
|
if (definition?.editable) return null;
|
||||||
|
|
||||||
return Object.keys(parameters).map((key, index) => {
|
return Object.keys(parameters).map(key => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'rollout':
|
case 'rollout':
|
||||||
case 'Rollout':
|
case 'Rollout':
|
||||||
return (
|
return (
|
||||||
<Fragment key={key}>
|
<Fragment key={key}>
|
||||||
<p className={styles.text}>
|
<p>
|
||||||
{parameters[key]}% of your base{' '}
|
{parameters[key]}% of your base{' '}
|
||||||
{constraints.length > 0
|
{constraints.length > 0
|
||||||
? 'who match constraints'
|
? 'who match constraints'
|
||||||
@ -145,7 +125,7 @@ const FeatureOverviewExecution = ({
|
|||||||
case 'percentage':
|
case 'percentage':
|
||||||
return (
|
return (
|
||||||
<Fragment key={param?.name}>
|
<Fragment key={param?.name}>
|
||||||
<p className={styles.text}>
|
<p>
|
||||||
{strategy?.parameters[param.name]}% of your base{' '}
|
{strategy?.parameters[param.name]}% of your base{' '}
|
||||||
{constraints?.length > 0
|
{constraints?.length > 0
|
||||||
? 'who match constraints'
|
? 'who match constraints'
|
||||||
@ -165,7 +145,7 @@ const FeatureOverviewExecution = ({
|
|||||||
case 'boolean':
|
case 'boolean':
|
||||||
return (
|
return (
|
||||||
<Fragment key={param.name}>
|
<Fragment key={param.name}>
|
||||||
<p className={styles.text} key={param.name}>
|
<p key={param.name}>
|
||||||
<StringTruncator
|
<StringTruncator
|
||||||
maxLength={15}
|
maxLength={15}
|
||||||
maxWidth="150"
|
maxWidth="150"
|
||||||
@ -192,7 +172,7 @@ const FeatureOverviewExecution = ({
|
|||||||
key={param.name}
|
key={param.name}
|
||||||
show={
|
show={
|
||||||
<>
|
<>
|
||||||
<p className={styles.text}>
|
<p>
|
||||||
<StringTruncator
|
<StringTruncator
|
||||||
maxWidth="150"
|
maxWidth="150"
|
||||||
maxLength={15}
|
maxLength={15}
|
||||||
@ -216,7 +196,7 @@ const FeatureOverviewExecution = ({
|
|||||||
key={param.name}
|
key={param.name}
|
||||||
show={
|
show={
|
||||||
<>
|
<>
|
||||||
<p className={styles.text}>
|
<p>
|
||||||
<StringTruncator
|
<StringTruncator
|
||||||
maxLength={15}
|
maxLength={15}
|
||||||
maxWidth="150"
|
maxWidth="150"
|
||||||
@ -248,10 +228,10 @@ const FeatureOverviewExecution = ({
|
|||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={constraints.length > 0}
|
condition={constraints.length > 0}
|
||||||
show={
|
show={
|
||||||
<div className={styles.constraintsContainer}>
|
<>
|
||||||
{renderConstraints()}
|
<ConstraintAccordionList constraints={constraints} />
|
||||||
<StrategySeparator text="AND" />
|
<StrategySeparator text="AND" />
|
||||||
</div>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
|
@ -6,7 +6,6 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
padding: '1rem',
|
padding: '1rem',
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
backgroundColor: theme.palette.grey[200],
|
backgroundColor: theme.palette.grey[200],
|
||||||
margin: '0.5rem 0',
|
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
borderRadius: '5px',
|
borderRadius: '5px',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
|
@ -32,29 +32,31 @@ export const SEMVER_GT = 'SEMVER_GT';
|
|||||||
export const SEMVER_LT = 'SEMVER_LT';
|
export const SEMVER_LT = 'SEMVER_LT';
|
||||||
|
|
||||||
export const allOperators: Operator[] = [
|
export const allOperators: Operator[] = [
|
||||||
NOT_IN,
|
|
||||||
IN,
|
IN,
|
||||||
STR_ENDS_WITH,
|
NOT_IN,
|
||||||
STR_STARTS_WITH,
|
|
||||||
STR_CONTAINS,
|
STR_CONTAINS,
|
||||||
|
STR_STARTS_WITH,
|
||||||
|
STR_ENDS_WITH,
|
||||||
NUM_EQ,
|
NUM_EQ,
|
||||||
NUM_GT,
|
NUM_GT,
|
||||||
NUM_GTE,
|
NUM_GTE,
|
||||||
NUM_LT,
|
NUM_LT,
|
||||||
NUM_LTE,
|
NUM_LTE,
|
||||||
DATE_AFTER,
|
|
||||||
DATE_BEFORE,
|
DATE_BEFORE,
|
||||||
|
DATE_AFTER,
|
||||||
SEMVER_EQ,
|
SEMVER_EQ,
|
||||||
SEMVER_GT,
|
SEMVER_GT,
|
||||||
SEMVER_LT,
|
SEMVER_LT,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const stringOperators: Operator[] = [
|
export const stringOperators: Operator[] = [
|
||||||
STR_ENDS_WITH,
|
|
||||||
STR_STARTS_WITH,
|
|
||||||
STR_CONTAINS,
|
STR_CONTAINS,
|
||||||
|
STR_STARTS_WITH,
|
||||||
|
STR_ENDS_WITH,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const inOperators: Operator[] = [IN, NOT_IN];
|
export const inOperators: Operator[] = [IN, NOT_IN];
|
||||||
|
|
||||||
export const numOperators: Operator[] = [
|
export const numOperators: Operator[] = [
|
||||||
NUM_EQ,
|
NUM_EQ,
|
||||||
NUM_GT,
|
NUM_GT,
|
||||||
@ -62,7 +64,9 @@ export const numOperators: Operator[] = [
|
|||||||
NUM_LT,
|
NUM_LT,
|
||||||
NUM_LTE,
|
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 semVerOperators: Operator[] = [SEMVER_EQ, SEMVER_GT, SEMVER_LT];
|
||||||
|
|
||||||
export const singleValueOperators: Operator[] = [
|
export const singleValueOperators: Operator[] = [
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { IConstraint } from 'interfaces/strategy';
|
import { IConstraint } from 'interfaces/strategy';
|
||||||
import { CURRENT_TIME_CONTEXT_FIELD } from 'component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditHeader/ConstraintAccordionEditHeader';
|
|
||||||
import { formatDateYMDHMS } from 'utils/formatDate';
|
import { formatDateYMDHMS } from 'utils/formatDate';
|
||||||
import { ILocationSettings } from 'hooks/useLocationSettings';
|
import { ILocationSettings } from 'hooks/useLocationSettings';
|
||||||
|
import { CURRENT_TIME_CONTEXT_FIELD } from 'utils/operatorsForContext';
|
||||||
|
|
||||||
export const formatConstraintValuesOrValue = (
|
export const formatConstraintValuesOrValue = (
|
||||||
constraint: IConstraint,
|
constraint: IConstraint,
|
@ -1,7 +1,8 @@
|
|||||||
import { CURRENT_TIME_CONTEXT_FIELD } from 'component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditHeader/ConstraintAccordionEditHeader';
|
|
||||||
import { allOperators, dateOperators, Operator } from 'constants/operators';
|
import { allOperators, dateOperators, Operator } from 'constants/operators';
|
||||||
import { oneOf } from 'utils/oneOf';
|
import { oneOf } from 'utils/oneOf';
|
||||||
|
|
||||||
|
export const CURRENT_TIME_CONTEXT_FIELD = 'currentTime';
|
||||||
|
|
||||||
export const operatorsForContext = (contextName: string): Operator[] => {
|
export const operatorsForContext = (contextName: string): Operator[] => {
|
||||||
return allOperators.filter(operator => {
|
return allOperators.filter(operator => {
|
||||||
if (
|
if (
|
Loading…
Reference in New Issue
Block a user