mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
Fix/constraints UI (#779)
* fix: add fixed height to summary * fix: change wording to negated * fix: change header margin * fix: label click length for negated property * fix: cut values that exceed allow length while leaving others alone * fix: set edit bg color * fix: add enter to add values * fix: expand if constraint changes * fix: add string truncator to param names * fix: add validation tests * fix: string truncator * fix: accordion margins on expanded * fix: accordion expansion * fix: update e2e * fix: update parseISO * fix: review comments * fix: update spec * fix: add negated visual indicator
This commit is contained in:
parent
bc9ae58c20
commit
472acecdad
@ -179,9 +179,7 @@ describe('feature', () => {
|
||||
});
|
||||
|
||||
it('can delete a strategy in the development environment', () => {
|
||||
cy.visit(
|
||||
`/projects/default/features/${featureToggleName}/strategies/edit?environmentId=development&strategyId=${strategyId}`
|
||||
);
|
||||
cy.visit(`/projects/default/features/${featureToggleName}`);
|
||||
|
||||
cy.intercept(
|
||||
'DELETE',
|
||||
@ -193,9 +191,8 @@ describe('feature', () => {
|
||||
}
|
||||
).as('deleteStrategy');
|
||||
|
||||
cy.get(
|
||||
'[data-test=SIDEBAR_MODAL_ID] [data-test=STRATEGY_FORM_REMOVE_ID]'
|
||||
).click();
|
||||
cy.get('[data-test=FEATURE_ENVIRONMENT_ACCORDION_development]').click();
|
||||
cy.get('[data-test=STRATEGY_FORM_REMOVE_ID]').click();
|
||||
cy.get('[data-test=DIALOGUE_CONFIRM_ID]').click();
|
||||
cy.wait('@deleteStrategy');
|
||||
});
|
||||
|
@ -17,6 +17,14 @@ button {
|
||||
font-family: 'Sen', sans-serif;
|
||||
}
|
||||
|
||||
.MuiInputBase-root {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.MuiAccordion-root.Mui-expanded {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.MuiButton-root {
|
||||
border-radius: 3px;
|
||||
text-transform: none;
|
||||
|
@ -128,6 +128,7 @@ const EnvironmentPermissionAccordion = ({
|
||||
text={environment.name}
|
||||
className={styles.header}
|
||||
maxWidth="120"
|
||||
maxLength={25}
|
||||
/>
|
||||
|
||||
<p className={styles.header}>
|
||||
|
@ -54,6 +54,7 @@ const BreadcrumbNav = () => {
|
||||
<StringTruncator
|
||||
text={path}
|
||||
maxWidth="200"
|
||||
maxLength={25}
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
@ -76,6 +77,7 @@ const BreadcrumbNav = () => {
|
||||
to={link}
|
||||
>
|
||||
<StringTruncator
|
||||
maxLength={25}
|
||||
text={path}
|
||||
maxWidth="200"
|
||||
/>
|
||||
|
@ -47,7 +47,11 @@ const Constraint = ({
|
||||
return (
|
||||
<div className={classes + ' ' + className} {...rest}>
|
||||
<div className={classes + ' ' + className} {...rest}>
|
||||
<StringTruncator text={constraint.contextName} maxWidth="125" />
|
||||
<StringTruncator
|
||||
text={constraint.contextName}
|
||||
maxWidth="125"
|
||||
maxLength={25}
|
||||
/>
|
||||
<StrategySeparator text={constraint.operator} maxWidth="none" />
|
||||
<span className={styles.values}>
|
||||
{constraint.values?.join(', ') ?? constraint.value}
|
||||
|
@ -20,15 +20,31 @@ 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',
|
||||
margin: '1rem 0',
|
||||
backgroundColor: '#fff',
|
||||
margin: 0,
|
||||
|
||||
['&:before']: {
|
||||
height: 0,
|
||||
},
|
||||
},
|
||||
accordionEdit: {
|
||||
backgroundColor: '#F6F6FA',
|
||||
},
|
||||
operator: {
|
||||
border: `1px solid ${theme.palette.secondary.main}`,
|
||||
padding: '0.25rem 1rem',
|
||||
@ -53,6 +69,17 @@ export const useStyles = makeStyles(theme => ({
|
||||
position: 'relative',
|
||||
},
|
||||
},
|
||||
headerValuesContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
headerValues: {
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
color: theme.palette.primary.light,
|
||||
},
|
||||
headerValuesExpand: {
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
},
|
||||
headerViewValuesContainer: {
|
||||
[theme.breakpoints.down(990)]: {
|
||||
display: 'none',
|
||||
@ -117,7 +144,14 @@ export const useStyles = makeStyles(theme => ({
|
||||
maxHeight: '400px',
|
||||
overflowY: 'auto',
|
||||
},
|
||||
summary: { border: 'none', padding: '0.25rem 1rem' },
|
||||
summary: {
|
||||
border: 'none',
|
||||
padding: '0.25rem 1rem',
|
||||
height: '85px',
|
||||
[theme.breakpoints.down(770)]: {
|
||||
height: '175px',
|
||||
},
|
||||
},
|
||||
settingsParagraph: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { IConstraint } from '../../../../interfaces/strategy';
|
||||
import { useStyles } from '../ConstraintAccordion.styles';
|
||||
import { ConstraintAccordionEditBody } from './ConstraintAccordionEditBody/ConstraintAccordionEditBody';
|
||||
@ -184,8 +185,11 @@ export const ConstraintAccordionEdit = ({
|
||||
return (
|
||||
<div className={styles.form}>
|
||||
<Accordion
|
||||
className={classnames(styles.accordion, styles.accordionEdit)}
|
||||
classes={{
|
||||
expanded: styles.accordionRoot,
|
||||
}}
|
||||
style={{ boxShadow: 'none' }}
|
||||
className={styles.accordion}
|
||||
expanded={expanded}
|
||||
TransitionProps={{
|
||||
onExited: () => {
|
||||
|
@ -80,7 +80,7 @@ const InvertedOperator = ({
|
||||
the opposite)
|
||||
</ConstraintFormHeader>
|
||||
<FormControlLabel
|
||||
style={{ display: 'block' }}
|
||||
style={{ display: 'inline-block' }}
|
||||
control={
|
||||
<Switch
|
||||
checked={inverted}
|
||||
@ -88,7 +88,7 @@ const InvertedOperator = ({
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={'inverted'}
|
||||
label={'negated'}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -6,6 +6,7 @@ const useStyles = makeStyles(theme => ({
|
||||
fontSize: theme.fontSizes.bodySize,
|
||||
fontWeight: 'normal',
|
||||
marginTop: '1rem',
|
||||
marginBottom: '0.25rem',
|
||||
},
|
||||
}));
|
||||
|
||||
@ -14,7 +15,7 @@ export const ConstraintFormHeader: React.FC<
|
||||
> = ({ children, ...rest }) => {
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<h3 className={styles.header} {...rest}>
|
||||
<h3 {...rest} className={styles.header}>
|
||||
{children}
|
||||
</h3>
|
||||
);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Button, Chip, makeStyles } from '@material-ui/core';
|
||||
import Input from 'component/common/Input/Input';
|
||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||
import React, { useState } from 'react';
|
||||
import { ConstraintFormHeader } from '../ConstraintFormHeader/ConstraintFormHeader';
|
||||
|
||||
@ -42,6 +43,8 @@ const useStyles = makeStyles(theme => ({
|
||||
valuesContainer: { marginTop: '1rem' },
|
||||
}));
|
||||
|
||||
const ENTER = 'Enter';
|
||||
|
||||
export const FreeTextInput = ({
|
||||
values,
|
||||
removeValue,
|
||||
@ -52,6 +55,14 @@ export const FreeTextInput = ({
|
||||
const [inputValues, setInputValues] = useState('');
|
||||
const styles = useStyles();
|
||||
|
||||
const onKeyDown = (event: React.KeyboardEvent) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (event.key === ENTER) {
|
||||
addValues();
|
||||
}
|
||||
};
|
||||
|
||||
const addValues = () => {
|
||||
if (inputValues.length === 0) {
|
||||
setError('values can not be empty');
|
||||
@ -80,6 +91,7 @@ export const FreeTextInput = ({
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.inputInnerContainer}>
|
||||
<Input
|
||||
onKeyDown={onKeyDown}
|
||||
label="Values"
|
||||
name="values"
|
||||
value={inputValues}
|
||||
@ -129,7 +141,13 @@ const ConstraintValueChips = ({
|
||||
// be unique here.
|
||||
return (
|
||||
<Chip
|
||||
label={value}
|
||||
label={
|
||||
<StringTruncator
|
||||
text={value}
|
||||
maxLength={35}
|
||||
maxWidth="100"
|
||||
/>
|
||||
}
|
||||
key={`${value}-${index}`}
|
||||
onDelete={() => removeValue(index)}
|
||||
className={styles.valueChip}
|
||||
|
@ -0,0 +1,110 @@
|
||||
import {
|
||||
numberValidatorGenerator,
|
||||
semVerValidatorGenerator,
|
||||
dateValidatorGenerator,
|
||||
stringValidatorGenerator,
|
||||
} from './constraintValidators';
|
||||
|
||||
test('numbervalidator should accept 0', () => {
|
||||
const numValidator = numberValidatorGenerator(0);
|
||||
const [result, err] = numValidator();
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(err).toBe('');
|
||||
});
|
||||
|
||||
test('number validator should reject value that cannot be parsed to number', () => {
|
||||
const numValidator = numberValidatorGenerator('testa31');
|
||||
const [result, err] = numValidator();
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(err).toBe('Value must be a number');
|
||||
});
|
||||
|
||||
test('number validator should reject NaN', () => {
|
||||
const numValidator = numberValidatorGenerator(NaN);
|
||||
const [result, err] = numValidator();
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(err).toBe('Value must be a number');
|
||||
});
|
||||
|
||||
test('number validator should accept value that can be parsed to number', () => {
|
||||
const numValidator = numberValidatorGenerator('31');
|
||||
const [result, err] = numValidator();
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(err).toBe('');
|
||||
});
|
||||
|
||||
test('number validator should accept float values', () => {
|
||||
const numValidator = numberValidatorGenerator('31.12');
|
||||
const [result, err] = numValidator();
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(err).toBe('');
|
||||
});
|
||||
|
||||
test('semver validator should reject prefixed values', () => {
|
||||
const semVerValidator = semVerValidatorGenerator('v1.4.2');
|
||||
const [result, err] = semVerValidator();
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(err).toBe('Value is not a valid semver. For example 1.2.4');
|
||||
});
|
||||
|
||||
test('semver validator should reject partial semver values', () => {
|
||||
const semVerValidator = semVerValidatorGenerator('4.2');
|
||||
const [result, err] = semVerValidator();
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(err).toBe('Value is not a valid semver. For example 1.2.4');
|
||||
});
|
||||
|
||||
test('semver validator should accept semver complient values', () => {
|
||||
const semVerValidator = semVerValidatorGenerator('1.4.2');
|
||||
const [result, err] = semVerValidator();
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(err).toBe('');
|
||||
});
|
||||
|
||||
test('date validator should reject invalid date', () => {
|
||||
const dateValidator = dateValidatorGenerator('114mydate2005');
|
||||
const [result, err] = dateValidator();
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(err).toBe('Value must be a valid date matching RFC3339');
|
||||
});
|
||||
|
||||
test('date validator should accept valid date', () => {
|
||||
const dateValidator = dateValidatorGenerator('2022-03-03T10:15:23.262Z');
|
||||
const [result, err] = dateValidator();
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(err).toBe('');
|
||||
});
|
||||
|
||||
test('string validator should accept a list of strings', () => {
|
||||
const stringValidator = stringValidatorGenerator(['1234', '4121']);
|
||||
const [result, err] = stringValidator();
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(err).toBe('');
|
||||
});
|
||||
|
||||
test('string validator should reject values that are not arrays', () => {
|
||||
const stringValidator = stringValidatorGenerator(4);
|
||||
const [result, err] = stringValidator();
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(err).toBe('Values must be a list of strings');
|
||||
});
|
||||
|
||||
test('string validator should reject arrays that are not arrays of strings', () => {
|
||||
const stringValidator = stringValidatorGenerator(['test', NaN, 5]);
|
||||
const [result, err] = stringValidator();
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(err).toBe('Values must be a list of strings');
|
||||
});
|
@ -1,11 +1,13 @@
|
||||
import { isValid } from 'date-fns';
|
||||
import { isValid, parseISO } from 'date-fns';
|
||||
import semver from 'semver';
|
||||
|
||||
export type ConstraintValidatorOutput = [boolean, string];
|
||||
|
||||
export const numberValidatorGenerator = (value: unknown) => {
|
||||
return (): ConstraintValidatorOutput => {
|
||||
if (!Number(value)) {
|
||||
const converted = Number(value);
|
||||
|
||||
if (typeof converted !== 'number' || Number.isNaN(converted)) {
|
||||
return [false, 'Value must be a number'];
|
||||
}
|
||||
|
||||
@ -13,27 +15,39 @@ export const numberValidatorGenerator = (value: unknown) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const stringValidatorGenerator = (values: string[]) => {
|
||||
export const stringValidatorGenerator = (values: unknown) => {
|
||||
return (): ConstraintValidatorOutput => {
|
||||
const error: ConstraintValidatorOutput = [
|
||||
false,
|
||||
'Values must be a list of strings',
|
||||
];
|
||||
if (!Array.isArray(values)) {
|
||||
return [false, 'Values must be a list of strings'];
|
||||
return error;
|
||||
}
|
||||
|
||||
if (!values.every(value => typeof value === 'string')) {
|
||||
return error;
|
||||
}
|
||||
|
||||
return [true, ''];
|
||||
};
|
||||
};
|
||||
|
||||
export const semVerValidatorGenerator = (value: string) => {
|
||||
return (): ConstraintValidatorOutput => {
|
||||
if (!semver.valid(value)) {
|
||||
const isCleanValue = semver.clean(value) === value;
|
||||
|
||||
if (!semver.valid(value) || !isCleanValue) {
|
||||
return [false, 'Value is not a valid semver. For example 1.2.4'];
|
||||
}
|
||||
|
||||
return [true, ''];
|
||||
};
|
||||
};
|
||||
|
||||
export const dateValidatorGenerator = (value: string) => {
|
||||
return (): ConstraintValidatorOutput => {
|
||||
if (isValid(value)) {
|
||||
if (!isValid(parseISO(value))) {
|
||||
return [false, 'Value must be a valid date matching RFC3339'];
|
||||
}
|
||||
return [true, ''];
|
||||
|
@ -8,7 +8,6 @@ import { IConstraint } from '../../../../interfaces/strategy';
|
||||
|
||||
import { ConstraintAccordionViewBody } from './ConstraintAccordionViewBody/ConstraintAccordionViewBody';
|
||||
import { ConstraintAccordionViewHeader } from './ConstraintAccordionViewHeader/ConstraintAccordionViewHeader';
|
||||
import { useStyles } from '../ConstraintAccordion.styles';
|
||||
import { oneOf } from '../../../../utils/one-of';
|
||||
import {
|
||||
dateOperators,
|
||||
@ -16,6 +15,7 @@ import {
|
||||
semVerOperators,
|
||||
} from '../../../../constants/operators';
|
||||
|
||||
import { useStyles } from '../ConstraintAccordion.styles';
|
||||
interface IConstraintAccordionViewProps {
|
||||
environmentId: string;
|
||||
constraint: IConstraint;
|
||||
@ -39,7 +39,13 @@ export const ConstraintAccordionView = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<Accordion style={{ boxShadow: 'none' }} className={styles.accordion}>
|
||||
<Accordion
|
||||
className={styles.accordion}
|
||||
classes={{
|
||||
root: styles.accordionRoot,
|
||||
}}
|
||||
style={{ boxShadow: 'none' }}
|
||||
>
|
||||
<AccordionSummary
|
||||
className={styles.summary}
|
||||
expandIcon={<ExpandMore />}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Chip } from '@material-ui/core';
|
||||
import { ImportExportOutlined, TextFormatOutlined } from '@material-ui/icons';
|
||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||
import { useState } from 'react';
|
||||
import { stringOperators } from '../../../../../constants/operators';
|
||||
import { IConstraint } from '../../../../../interfaces/strategy';
|
||||
@ -37,7 +38,7 @@ export const ConstraintAccordionViewBody = ({
|
||||
show={
|
||||
<p className={styles.settingsParagraph}>
|
||||
<ImportExportOutlined className={styles.settingsIcon} />{' '}
|
||||
Operator is inverted
|
||||
Operator is negated
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
@ -65,7 +66,16 @@ const SingleValue = ({ value, operator }: ISingleValueProps) => {
|
||||
return (
|
||||
<div className={styles.singleValueView}>
|
||||
<p className={styles.singleValueText}>Value must {operator}</p>{' '}
|
||||
<Chip label={value} className={styles.chip} />
|
||||
<Chip
|
||||
label={
|
||||
<StringTruncator
|
||||
maxWidth="200"
|
||||
text={value}
|
||||
maxLength={25}
|
||||
/>
|
||||
}
|
||||
className={styles.chip}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -88,7 +98,13 @@ const MultipleValues = ({ values }: IMultipleValuesProps) => {
|
||||
.map((value, index) => (
|
||||
<Chip
|
||||
key={`${value}-${index}`}
|
||||
label={value}
|
||||
label={
|
||||
<StringTruncator
|
||||
maxWidth="200"
|
||||
text={value}
|
||||
maxLength={25}
|
||||
/>
|
||||
}
|
||||
className={styles.chip}
|
||||
/>
|
||||
))}
|
||||
|
@ -53,10 +53,15 @@ export const ConstraintAccordionViewHeader = ({
|
||||
<StringTruncator
|
||||
text={constraint.contextName}
|
||||
maxWidth="175px"
|
||||
maxLength={25}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ minWidth: '220px' }}>
|
||||
<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>
|
||||
<div className={styles.headerViewValuesContainer}>
|
||||
@ -64,10 +69,14 @@ export const ConstraintAccordionViewHeader = ({
|
||||
condition={singleValue}
|
||||
show={<Chip label={constraint.value} />}
|
||||
elseShow={
|
||||
<p>
|
||||
{constraint?.values?.length} values. Expand to
|
||||
view
|
||||
</p>
|
||||
<div className={styles.headerValuesContainer}>
|
||||
<p className={styles.headerValues}>
|
||||
{constraint?.values?.length} values
|
||||
</p>
|
||||
<p className={styles.headerValuesExpand}>
|
||||
Expand to view
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1,34 +1,44 @@
|
||||
import { Tooltip } from '@material-ui/core';
|
||||
import ConditionallyRender from '../ConditionallyRender';
|
||||
|
||||
interface IStringTruncatorProps {
|
||||
text: string;
|
||||
maxWidth: string;
|
||||
className?: string;
|
||||
maxLength: number;
|
||||
}
|
||||
|
||||
const StringTruncator = ({
|
||||
text,
|
||||
maxWidth,
|
||||
maxLength,
|
||||
className,
|
||||
...rest
|
||||
}: IStringTruncatorProps) => {
|
||||
return (
|
||||
<Tooltip title={text} arrow>
|
||||
<span
|
||||
data-loading
|
||||
className={className}
|
||||
style={{
|
||||
maxWidth: `${maxWidth}px`,
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'inline-block',
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<ConditionallyRender
|
||||
condition={text.length > maxLength}
|
||||
show={
|
||||
<Tooltip title={text} arrow>
|
||||
<span
|
||||
data-loading
|
||||
className={className}
|
||||
style={{
|
||||
maxWidth: `${maxWidth}px`,
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
elseShow={<>{text}</>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -19,7 +19,11 @@ const EnvironmentCard = ({ name, type }: IEnvironmentProps) => {
|
||||
<div className={styles.infoInnerContainer}>
|
||||
<div className={styles.infoTitle}>Id</div>
|
||||
<div>
|
||||
<StringTruncator text={name} maxWidth={'250'} />
|
||||
<StringTruncator
|
||||
text={name}
|
||||
maxWidth={'250'}
|
||||
maxLength={30}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.infoInnerContainer}>
|
||||
|
@ -135,7 +135,11 @@ const EnvironmentListItem = ({
|
||||
primary={
|
||||
<>
|
||||
<strong>
|
||||
<StringTruncator text={env.name} maxWidth={'125'} />
|
||||
<StringTruncator
|
||||
text={env.name}
|
||||
maxWidth={'125'}
|
||||
maxLength={25}
|
||||
/>
|
||||
</strong>
|
||||
<ConditionallyRender
|
||||
condition={!env.enabled}
|
||||
|
@ -23,6 +23,7 @@ export const FeatureStrategyEmpty = ({
|
||||
<StringTruncator
|
||||
text={environmentId}
|
||||
maxWidth={'130'}
|
||||
maxLength={15}
|
||||
className={styles.envName}
|
||||
/>{' '}
|
||||
environment
|
||||
|
@ -5,7 +5,6 @@ import {
|
||||
formatStrategyName,
|
||||
} from 'utils/strategy-names';
|
||||
import { FeatureStrategyType } from '../FeatureStrategyType/FeatureStrategyType';
|
||||
import { FeatureStrategyRemove } from '../FeatureStrategyRemove/FeatureStrategyRemove';
|
||||
import { FeatureStrategyEnabled } from '../FeatureStrategyEnabled/FeatureStrategyEnabled';
|
||||
import { FeatureStrategyConstraints } from '../FeatureStrategyConstraints/FeatureStrategyConstraints';
|
||||
import { Button } from '@material-ui/core';
|
||||
@ -118,33 +117,6 @@ export const FeatureStrategyForm = ({
|
||||
)}
|
||||
/>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
type="button"
|
||||
color="secondary"
|
||||
onClick={onCancel}
|
||||
disabled={loading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(strategy.id)}
|
||||
show={
|
||||
<FeatureStrategyRemove
|
||||
projectId={feature.project}
|
||||
featureId={feature.name}
|
||||
environmentId={environmentId}
|
||||
strategyId={strategy.id!}
|
||||
disabled={loading}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FeatureStrategyProdGuard
|
||||
open={showProdGuard}
|
||||
onClose={() => setShowProdGuard(false)}
|
||||
onClick={onSubmit}
|
||||
loading={loading}
|
||||
label="Save strategy"
|
||||
/>
|
||||
<PermissionButton
|
||||
permission={permission}
|
||||
projectId={feature.project}
|
||||
@ -157,6 +129,22 @@ export const FeatureStrategyForm = ({
|
||||
>
|
||||
Save strategy
|
||||
</PermissionButton>
|
||||
<Button
|
||||
type="button"
|
||||
color="secondary"
|
||||
onClick={onCancel}
|
||||
disabled={loading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<FeatureStrategyProdGuard
|
||||
open={showProdGuard}
|
||||
onClose={() => setShowProdGuard(false)}
|
||||
onClick={onSubmit}
|
||||
loading={loading}
|
||||
label="Save strategy"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
@ -42,6 +42,7 @@ export const FeatureStrategyMenuCard = ({
|
||||
text={strategy.displayName || strategyName}
|
||||
className={styles.name}
|
||||
maxWidth="200"
|
||||
maxLength={25}
|
||||
/>
|
||||
<div className={styles.description}>{strategy.description}</div>
|
||||
</div>
|
||||
|
@ -239,6 +239,7 @@ const FeatureToggleListNew = ({
|
||||
>
|
||||
<StringTruncator
|
||||
text={env.name}
|
||||
maxLength={15}
|
||||
maxWidth="90"
|
||||
data-loading
|
||||
/>
|
||||
|
@ -86,7 +86,7 @@ const FeatureOverviewEnvSwitch = ({
|
||||
{' '}
|
||||
<span data-loading>{env.enabled ? 'enabled' : 'disabled'} in</span>
|
||||
|
||||
<StringTruncator text={env.name} maxWidth="120" />
|
||||
<StringTruncator text={env.name} maxWidth="120" maxLength={15} />
|
||||
</>
|
||||
);
|
||||
|
||||
|
@ -95,6 +95,7 @@ export const useStyles = makeStyles(theme => ({
|
||||
},
|
||||
headerTitle: {
|
||||
flexDirection: 'column',
|
||||
textAlign: 'center',
|
||||
},
|
||||
headerIcon: {
|
||||
marginBottom: '0.5rem',
|
||||
|
@ -25,6 +25,7 @@ import FeatureOverviewEnvironmentBody from './FeatureOverviewEnvironmentBody/Fea
|
||||
import FeatureOverviewEnvironmentFooter from './FeatureOverviewEnvironmentFooter/FeatureOverviewEnvironmentFooter';
|
||||
import FeatureOverviewEnvironmentMetrics from './FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics';
|
||||
import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu';
|
||||
import { FEATURE_ENVIRONMENT_ACCORDION } from 'testIds';
|
||||
|
||||
interface IStrategyIconObject {
|
||||
count: number;
|
||||
@ -86,7 +87,10 @@ const FeatureOverviewEnvironment = ({
|
||||
|
||||
return (
|
||||
<div className={styles.featureOverviewEnvironment}>
|
||||
<Accordion style={{ boxShadow: 'none' }}>
|
||||
<Accordion
|
||||
style={{ boxShadow: 'none' }}
|
||||
data-test={`${FEATURE_ENVIRONMENT_ACCORDION}_${env.name}`}
|
||||
>
|
||||
<AccordionSummary
|
||||
className={styles.accordionHeader}
|
||||
expandIcon={<ExpandMore />}
|
||||
@ -97,12 +101,15 @@ const FeatureOverviewEnvironment = ({
|
||||
enabled={env.enabled}
|
||||
className={styles.headerIcon}
|
||||
/>
|
||||
Feature toggle execution for
|
||||
<StringTruncator
|
||||
text={env.name}
|
||||
className={styles.truncator}
|
||||
maxWidth="100"
|
||||
/>
|
||||
<p>
|
||||
Feature toggle execution for
|
||||
<StringTruncator
|
||||
text={env.name}
|
||||
className={styles.truncator}
|
||||
maxWidth="100"
|
||||
maxLength={15}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.strategyMenu}>
|
||||
|
@ -4,7 +4,6 @@ import ConditionallyRender from 'component/common/ConditionallyRender';
|
||||
import FeatureOverviewEnvironmentStrategies from '../FeatureOverviewEnvironmentStrategies/FeatureOverviewEnvironmentStrategies';
|
||||
import { useStyles } from '../FeatureOverviewEnvironment.styles';
|
||||
import { IFeatureEnvironment } from 'interfaces/featureToggle';
|
||||
import { FeatureStrategyMenu } from '../../../../../FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu';
|
||||
import { FeatureStrategyEmpty } from '../../../../../FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty';
|
||||
|
||||
interface IFeatureOverviewEnvironmentBodyProps {
|
||||
@ -37,14 +36,6 @@ const FeatureOverviewEnvironmentBody = ({
|
||||
condition={featureEnvironment?.strategies.length > 0}
|
||||
show={
|
||||
<>
|
||||
<div className={styles.linkContainer}>
|
||||
<FeatureStrategyMenu
|
||||
label="Add strategy"
|
||||
projectId={projectId}
|
||||
featureId={featureId}
|
||||
environmentId={featureEnvironment.name}
|
||||
/>
|
||||
</div>
|
||||
<FeatureOverviewEnvironmentStrategies
|
||||
strategies={featureEnvironment?.strategies}
|
||||
environmentName={featureEnvironment.name}
|
||||
|
@ -13,6 +13,7 @@ import FeatureOverviewExecution from '../../../../FeatureOverviewExecution/Featu
|
||||
import { useStyles } from './FeatureOverviewEnvironmentStrategy.styles';
|
||||
import { formatEditStrategyPath } from '../../../../../../FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
|
||||
import { FeatureStrategyRemove } from 'component/feature/FeatureStrategy/FeatureStrategyRemove/FeatureStrategyRemove';
|
||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||
|
||||
interface IFeatureOverviewEnvironmentStrategyProps {
|
||||
environmentId: string;
|
||||
@ -40,15 +41,12 @@ const FeatureOverviewEnvironmentStrategy = ({
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<Icon className={styles.icon} />
|
||||
{formatStrategyName(strategy.name)}
|
||||
<StringTruncator
|
||||
maxWidth="150"
|
||||
maxLength={15}
|
||||
text={formatStrategyName(strategy.name)}
|
||||
/>
|
||||
<div className={styles.actions}>
|
||||
<FeatureStrategyRemove
|
||||
projectId={projectId}
|
||||
featureId={featureId}
|
||||
environmentId={environmentId}
|
||||
strategyId={strategy.id}
|
||||
icon
|
||||
/>
|
||||
<PermissionIconButton
|
||||
permission={UPDATE_FEATURE_STRATEGY}
|
||||
environmentId={environmentId}
|
||||
@ -59,6 +57,13 @@ const FeatureOverviewEnvironmentStrategy = ({
|
||||
>
|
||||
<Edit titleAccess="Edit" />
|
||||
</PermissionIconButton>
|
||||
<FeatureStrategyRemove
|
||||
projectId={projectId}
|
||||
featureId={featureId}
|
||||
environmentId={environmentId}
|
||||
strategyId={strategy.id}
|
||||
icon
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -29,5 +29,10 @@ export const useStyles = makeStyles(theme => ({
|
||||
overflow: 'hidden',
|
||||
maxWidth: '50%',
|
||||
},
|
||||
text: { textAlign: 'center', margin: '0.2rem 0 0.5rem' },
|
||||
text: {
|
||||
textAlign: 'center',
|
||||
margin: '0.2rem 0 0.5rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
}));
|
||||
|
@ -11,6 +11,7 @@ import { useStyles } from './FeatureOverviewExecution.styles';
|
||||
import FeatureOverviewExecutionChips from './FeatureOverviewExecutionChips/FeatureOverviewExecutionChips';
|
||||
import { useStrategies } from '../../../../../hooks/api/getters/useStrategies/useStrategies';
|
||||
import Constraint from '../../../../common/Constraint/Constraint';
|
||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||
|
||||
interface IFeatureOverviewExecutionProps {
|
||||
parameters: IParameter;
|
||||
@ -166,7 +167,11 @@ const FeatureOverviewExecution = ({
|
||||
return (
|
||||
<Fragment key={param.name}>
|
||||
<p className={styles.text} key={param.name}>
|
||||
{param.name} must be{' '}
|
||||
<StringTruncator
|
||||
maxLength={15}
|
||||
maxWidth="150"
|
||||
text={param.name}
|
||||
/>{' '}
|
||||
{strategy.parameters[param.name]}
|
||||
</p>
|
||||
<ConditionallyRender
|
||||
@ -189,7 +194,12 @@ const FeatureOverviewExecution = ({
|
||||
show={
|
||||
<>
|
||||
<p className={styles.text}>
|
||||
{param.name} is set to {numValue}
|
||||
<StringTruncator
|
||||
maxWidth="150"
|
||||
maxLength={15}
|
||||
text={param.name}
|
||||
/>{' '}
|
||||
is set to {numValue}
|
||||
</p>
|
||||
<ConditionallyRender
|
||||
condition={notLastItem}
|
||||
@ -208,7 +218,12 @@ const FeatureOverviewExecution = ({
|
||||
show={
|
||||
<>
|
||||
<p className={styles.text}>
|
||||
{param.name} is set to {value}
|
||||
<StringTruncator
|
||||
maxLength={15}
|
||||
maxWidth="150"
|
||||
text={param.name}
|
||||
/>{' '}
|
||||
is set to {value}
|
||||
</p>
|
||||
<ConditionallyRender
|
||||
condition={notLastItem}
|
||||
|
@ -20,6 +20,8 @@ import { Alert } from '@material-ui/lab';
|
||||
import PermissionSwitch from '../../common/PermissionSwitch/PermissionSwitch';
|
||||
import { IProjectEnvironment } from '../../../interfaces/environments';
|
||||
import { getEnabledEnvs } from './helpers';
|
||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||
import { useCommonStyles } from 'common.styles';
|
||||
|
||||
interface ProjectEnvironmentListProps {
|
||||
projectId: string;
|
||||
@ -39,6 +41,7 @@ const ProjectEnvironmentList = ({ projectId }: ProjectEnvironmentListProps) => {
|
||||
const { project, refetch: refetchProject } = useProject(projectId);
|
||||
const { removeEnvironmentFromProject, addEnvironmentToProject } =
|
||||
useProjectApi();
|
||||
const commonStyles = useCommonStyles();
|
||||
|
||||
// local state
|
||||
const [selectedEnv, setSelectedEnv] = useState<IProjectEnvironment>();
|
||||
@ -129,10 +132,20 @@ const ProjectEnvironmentList = ({ projectId }: ProjectEnvironmentListProps) => {
|
||||
};
|
||||
|
||||
const genLabel = (env: IProjectEnvironment) => (
|
||||
<>
|
||||
<code>{env.name}</code> environment is{' '}
|
||||
<strong>{env.enabled ? 'enabled' : 'disabled'}</strong>
|
||||
</>
|
||||
<div className={commonStyles.flexRow}>
|
||||
<code>
|
||||
<StringTruncator
|
||||
text={env.name}
|
||||
maxLength={50}
|
||||
maxWidth="150"
|
||||
/>
|
||||
</code>
|
||||
{/* This is ugly - but regular {" "} doesn't work here*/}
|
||||
<p>
|
||||
environment is{' '}
|
||||
<strong>{env.enabled ? 'enabled' : 'disabled'}</strong>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderEnvironments = () => {
|
||||
|
@ -16,6 +16,7 @@ export const SSO_LOGIN_BUTTON = 'SSO_LOGIN_BUTTON';
|
||||
export const FORGOTTEN_PASSWORD_FIELD = 'FORGOTTEN_PASSWORD_FIELD';
|
||||
|
||||
/* STRATEGY */
|
||||
export const FEATURE_ENVIRONMENT_ACCORDION = 'FEATURE_ENVIRONMENT_ACCORDION';
|
||||
export const ADD_NEW_STRATEGY_ID = 'ADD_NEW_STRATEGY_ID';
|
||||
export const ROLLOUT_SLIDER_ID = 'ROLLOUT_SLIDER_ID';
|
||||
export const DIALOGUE_CONFIRM_ID = 'DIALOGUE_CONFIRM_ID';
|
||||
|
@ -33,7 +33,7 @@ const mainTheme = {
|
||||
},
|
||||
grey: {
|
||||
main: '#6C6C6C',
|
||||
light: '#7e7e7e',
|
||||
light: '#F6F6FA',
|
||||
},
|
||||
neutral: {
|
||||
main: '#18243e',
|
||||
|
Loading…
Reference in New Issue
Block a user