1
0
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:
Fredrik Strand Oseberg 2022-03-11 13:46:00 +01:00 committed by GitHub
parent bc9ae58c20
commit 472acecdad
32 changed files with 381 additions and 110 deletions

View File

@ -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');
});

View File

@ -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;

View File

@ -128,6 +128,7 @@ const EnvironmentPermissionAccordion = ({
text={environment.name}
className={styles.header}
maxWidth="120"
maxLength={25}
/>
 
<p className={styles.header}>

View File

@ -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"
/>

View File

@ -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}

View File

@ -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',

View File

@ -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: () => {

View File

@ -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'}
/>
</>
);

View File

@ -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>
);

View File

@ -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}

View File

@ -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');
});

View File

@ -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, ''];

View File

@ -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 />}

View File

@ -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}
/>
))}

View File

@ -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>

View File

@ -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}</>}
/>
);
};

View File

@ -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}>

View File

@ -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}

View File

@ -23,6 +23,7 @@ export const FeatureStrategyEmpty = ({
<StringTruncator
text={environmentId}
maxWidth={'130'}
maxLength={15}
className={styles.envName}
/>{' '}
environment

View File

@ -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>
);

View File

@ -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>

View File

@ -239,6 +239,7 @@ const FeatureToggleListNew = ({
>
<StringTruncator
text={env.name}
maxLength={15}
maxWidth="90"
data-loading
/>

View File

@ -86,7 +86,7 @@ const FeatureOverviewEnvSwitch = ({
{' '}
<span data-loading>{env.enabled ? 'enabled' : 'disabled'} in</span>
&nbsp;
<StringTruncator text={env.name} maxWidth="120" />
<StringTruncator text={env.name} maxWidth="120" maxLength={15} />
</>
);

View File

@ -95,6 +95,7 @@ export const useStyles = makeStyles(theme => ({
},
headerTitle: {
flexDirection: 'column',
textAlign: 'center',
},
headerIcon: {
marginBottom: '0.5rem',

View File

@ -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&nbsp;
<StringTruncator
text={env.name}
className={styles.truncator}
maxWidth="100"
/>
<p>
Feature toggle execution for&nbsp;
<StringTruncator
text={env.name}
className={styles.truncator}
maxWidth="100"
maxLength={15}
/>
</p>
</div>
<div className={styles.container}>
<div className={styles.strategyMenu}>

View File

@ -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}

View File

@ -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>

View File

@ -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',
},
}));

View File

@ -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}

View File

@ -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>
&nbsp; environment is{' '}
<strong>{env.enabled ? 'enabled' : 'disabled'}</strong>
</p>
</div>
);
const renderEnvironments = () => {

View File

@ -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';

View File

@ -33,7 +33,7 @@ const mainTheme = {
},
grey: {
main: '#6C6C6C',
light: '#7e7e7e',
light: '#F6F6FA',
},
neutral: {
main: '#18243e',