mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-15 17:50:48 +02:00
Feature toggle page update (#1140)
* feat: add icon to custom strategies * feat: update feature toggle screen layout * strategy and constraints separators * style disabled envirnments * strategy constraint style * strategy drag and drop * feature env emtpy state * quick add strategy api * reorder strategies api integration * feature strategy header title * openapi update * style small chip component * fix comments after review * fix issues with strategy constraint operators * Revert "openapi update" This reverts commit 27e7651ebae26f61ca76ec910e1f209bae7f2955. * fix tooltip ref
This commit is contained in:
parent
d1d23d1b4c
commit
c70b38a62a
@ -33,7 +33,7 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
},
|
},
|
||||||
headerMetaInfo: {
|
headerMetaInfo: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'stretch',
|
||||||
[theme.breakpoints.down(710)]: { flexDirection: 'column' },
|
[theme.breakpoints.down(710)]: { flexDirection: 'column' },
|
||||||
},
|
},
|
||||||
headerContainer: {
|
headerContainer: {
|
||||||
@ -48,12 +48,11 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
},
|
},
|
||||||
headerValuesContainerWrapper: {
|
headerValuesContainerWrapper: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
alignItems: 'stretch',
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
},
|
||||||
headerValuesContainer: {
|
headerValuesContainer: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
alignItems: 'stretch',
|
||||||
},
|
},
|
||||||
headerValues: {
|
headerValues: {
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
@ -106,7 +105,10 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
},
|
},
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
},
|
},
|
||||||
headerSelect: { marginRight: '1rem', width: '200px' },
|
headerSelect: {
|
||||||
|
marginRight: '1rem',
|
||||||
|
width: '200px',
|
||||||
|
},
|
||||||
chip: {
|
chip: {
|
||||||
margin: '0 0.5rem 0.5rem 0',
|
margin: '0 0.5rem 0.5rem 0',
|
||||||
},
|
},
|
||||||
@ -121,7 +123,7 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
accordionDetails: {
|
accordionDetails: {
|
||||||
borderTop: `1px solid ${theme.palette.grey[300]}`,
|
borderTop: `1px dashed ${theme.palette.grey[300]}`,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
},
|
},
|
||||||
@ -132,33 +134,16 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
},
|
},
|
||||||
summary: {
|
summary: {
|
||||||
border: 'none',
|
border: 'none',
|
||||||
padding: '0.25rem 1rem',
|
padding: theme.spacing(0.5, 3),
|
||||||
'&:hover .valuesExpandLabel': {
|
'&:hover .valuesExpandLabel': {
|
||||||
textDecoration: 'underline',
|
textDecoration: 'underline',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
settingsParagraph: {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: '0.5rem 0',
|
|
||||||
},
|
|
||||||
settingsIcon: {
|
settingsIcon: {
|
||||||
height: '32.5px',
|
height: '32.5px',
|
||||||
width: '32.5px',
|
width: '32.5px',
|
||||||
marginRight: '0.5rem',
|
marginRight: '0.5rem',
|
||||||
fill: theme.palette.inactiveIcon,
|
fill: theme.palette.inactiveIcon,
|
||||||
},
|
},
|
||||||
singleValueView: {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
[theme.breakpoints.down(600)]: { flexDirection: 'column' },
|
|
||||||
},
|
|
||||||
singleValueText: {
|
|
||||||
marginRight: '0.75rem',
|
|
||||||
[theme.breakpoints.down(600)]: {
|
|
||||||
marginBottom: '0.75rem',
|
|
||||||
marginRight: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
form: { padding: 0, margin: 0, width: '100%' },
|
form: { padding: 0, margin: 0, width: '100%' },
|
||||||
}));
|
}));
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Tooltip } from '@mui/material';
|
import { Tooltip, Box } from '@mui/material';
|
||||||
import { ReactComponent as CaseSensitive } from 'assets/icons/24_Text format.svg';
|
import { ReactComponent as CaseSensitive } from 'assets/icons/24_Text format.svg';
|
||||||
import { ReactComponent as CaseSensitiveOff } from 'assets/icons/24_Text format off.svg';
|
import { ReactComponent as CaseSensitiveOff } from 'assets/icons/24_Text format off.svg';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@ -17,30 +17,35 @@ interface CaseSensitiveButtonProps {
|
|||||||
export const CaseSensitiveButton = ({
|
export const CaseSensitiveButton = ({
|
||||||
localConstraint,
|
localConstraint,
|
||||||
setCaseInsensitive,
|
setCaseInsensitive,
|
||||||
}: CaseSensitiveButtonProps) => {
|
}: CaseSensitiveButtonProps) => (
|
||||||
return (
|
<Tooltip
|
||||||
|
title={
|
||||||
|
Boolean(localConstraint.caseInsensitive)
|
||||||
|
? 'Make it case sensitive'
|
||||||
|
: 'Make it case insensitive'
|
||||||
|
}
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'stretch' }}>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={Boolean(localConstraint.caseInsensitive)}
|
condition={Boolean(localConstraint.caseInsensitive)}
|
||||||
show={
|
show={
|
||||||
<Tooltip title="Make it case sensitive" arrow>
|
|
||||||
<StyledToggleButtonOff
|
<StyledToggleButtonOff
|
||||||
onClick={setCaseInsensitive}
|
onClick={setCaseInsensitive}
|
||||||
disableRipple
|
disableRipple
|
||||||
>
|
>
|
||||||
<CaseSensitiveOff />
|
<CaseSensitiveOff />
|
||||||
</StyledToggleButtonOff>
|
</StyledToggleButtonOff>
|
||||||
</Tooltip>
|
|
||||||
}
|
}
|
||||||
elseShow={
|
elseShow={
|
||||||
<Tooltip title="Make it case insensitive" arrow>
|
|
||||||
<StyledToggleButtonOn
|
<StyledToggleButtonOn
|
||||||
onClick={setCaseInsensitive}
|
onClick={setCaseInsensitive}
|
||||||
disableRipple
|
disableRipple
|
||||||
>
|
>
|
||||||
<CaseSensitive />
|
<CaseSensitive />
|
||||||
</StyledToggleButtonOn>
|
</StyledToggleButtonOn>
|
||||||
</Tooltip>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
</Box>
|
||||||
};
|
</Tooltip>
|
||||||
|
);
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { Tooltip } from '@mui/material';
|
import { Box, Tooltip } from '@mui/material';
|
||||||
import { ReactComponent as NegatedIcon } from '../../../../../../assets/icons/24_Negator.svg';
|
import { ReactComponent as NegatedIcon } from 'assets/icons/24_Negator.svg';
|
||||||
import { ReactComponent as NegatedIconOff } from '../../../../../../assets/icons/24_Negator off.svg';
|
import { ReactComponent as NegatedIconOff } from 'assets/icons/24_Negator off.svg';
|
||||||
import React from 'react';
|
import { IConstraint } from 'interfaces/strategy';
|
||||||
import { IConstraint } from '../../../../../../interfaces/strategy';
|
|
||||||
import {
|
import {
|
||||||
StyledToggleButtonOff,
|
StyledToggleButtonOff,
|
||||||
StyledToggleButtonOn,
|
StyledToggleButtonOn,
|
||||||
@ -17,30 +16,35 @@ interface InvertedOperatorButtonProps {
|
|||||||
export const InvertedOperatorButton = ({
|
export const InvertedOperatorButton = ({
|
||||||
localConstraint,
|
localConstraint,
|
||||||
setInvertedOperator,
|
setInvertedOperator,
|
||||||
}: InvertedOperatorButtonProps) => {
|
}: InvertedOperatorButtonProps) => (
|
||||||
return (
|
<Tooltip
|
||||||
|
title={
|
||||||
|
Boolean(localConstraint.inverted)
|
||||||
|
? 'Remove negation'
|
||||||
|
: 'Negate operator'
|
||||||
|
}
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'stretch' }}>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={Boolean(localConstraint.inverted)}
|
condition={Boolean(localConstraint.inverted)}
|
||||||
show={
|
show={
|
||||||
<Tooltip title="Remove negation" arrow>
|
|
||||||
<StyledToggleButtonOn
|
<StyledToggleButtonOn
|
||||||
onClick={setInvertedOperator}
|
onClick={setInvertedOperator}
|
||||||
disableRipple
|
disableRipple
|
||||||
>
|
>
|
||||||
<NegatedIcon />
|
<NegatedIcon />
|
||||||
</StyledToggleButtonOn>
|
</StyledToggleButtonOn>
|
||||||
</Tooltip>
|
|
||||||
}
|
}
|
||||||
elseShow={
|
elseShow={
|
||||||
<Tooltip title="Negate operator" arrow>
|
|
||||||
<StyledToggleButtonOff
|
<StyledToggleButtonOff
|
||||||
onClick={setInvertedOperator}
|
onClick={setInvertedOperator}
|
||||||
disableRipple
|
disableRipple
|
||||||
>
|
>
|
||||||
<NegatedIconOff />
|
<NegatedIconOff />
|
||||||
</StyledToggleButtonOff>
|
</StyledToggleButtonOff>
|
||||||
</Tooltip>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
</Box>
|
||||||
};
|
</Tooltip>
|
||||||
|
);
|
||||||
|
@ -3,8 +3,8 @@ import { makeStyles } from 'tss-react/mui';
|
|||||||
export const useStyles = makeStyles()(theme => ({
|
export const useStyles = makeStyles()(theme => ({
|
||||||
container: {
|
container: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
display: 'grid',
|
display: 'flex',
|
||||||
gap: '1rem',
|
flexDirection: 'column',
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
fill: theme.palette.grey[600],
|
fill: theme.palette.grey[600],
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
import React, { forwardRef, Fragment, useImperativeHandle } from 'react';
|
||||||
|
import { Button, Tooltip } from '@mui/material';
|
||||||
|
import { Help } from '@mui/icons-material';
|
||||||
import { IConstraint } from 'interfaces/strategy';
|
import { IConstraint } from 'interfaces/strategy';
|
||||||
import React, { forwardRef, useImperativeHandle } from 'react';
|
|
||||||
import { ConstraintAccordion } from 'component/common/ConstraintAccordion/ConstraintAccordion';
|
import { ConstraintAccordion } from 'component/common/ConstraintAccordion/ConstraintAccordion';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
|
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
|
||||||
@ -8,8 +10,7 @@ import { objectId } from 'utils/objectId';
|
|||||||
import { useStyles } from './ConstraintAccordionList.styles';
|
import { useStyles } from './ConstraintAccordionList.styles';
|
||||||
import { createEmptyConstraint } from 'component/common/ConstraintAccordion/ConstraintAccordionList/createEmptyConstraint';
|
import { createEmptyConstraint } from 'component/common/ConstraintAccordion/ConstraintAccordionList/createEmptyConstraint';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { Button, Tooltip } from '@mui/material';
|
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
|
||||||
import { Help } from '@mui/icons-material';
|
|
||||||
|
|
||||||
interface IConstraintAccordionListProps {
|
interface IConstraintAccordionListProps {
|
||||||
constraints: IConstraint[];
|
constraints: IConstraint[];
|
||||||
@ -129,6 +130,11 @@ export const ConstraintAccordionList = forwardRef<
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{constraints.map((constraint, index) => (
|
{constraints.map((constraint, index) => (
|
||||||
|
<Fragment key={`${constraint.contextName}-${index}`}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={index > 0}
|
||||||
|
show={<StrategySeparator text="AND" />}
|
||||||
|
/>
|
||||||
<ConstraintAccordion
|
<ConstraintAccordion
|
||||||
key={objectId(constraint)}
|
key={objectId(constraint)}
|
||||||
constraint={constraint}
|
constraint={constraint}
|
||||||
@ -139,6 +145,7 @@ export const ConstraintAccordionList = forwardRef<
|
|||||||
editing={Boolean(state.get(constraint)?.editing)}
|
editing={Boolean(state.get(constraint)?.editing)}
|
||||||
compact
|
compact
|
||||||
/>
|
/>
|
||||||
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import { Accordion, AccordionSummary, AccordionDetails } from '@mui/material';
|
import { Accordion, AccordionSummary, AccordionDetails } from '@mui/material';
|
||||||
import { IConstraint } from 'interfaces/strategy';
|
import { IConstraint } from 'interfaces/strategy';
|
||||||
|
|
||||||
import { ConstraintAccordionViewBody } from './ConstraintAccordionViewBody/ConstraintAccordionViewBody';
|
import { ConstraintAccordionViewBody } from './ConstraintAccordionViewBody/ConstraintAccordionViewBody';
|
||||||
import { ConstraintAccordionViewHeader } from './ConstraintAccordionViewHeader/ConstraintAccordionViewHeader';
|
import { ConstraintAccordionViewHeader } from './ConstraintAccordionViewHeader/ConstraintAccordionViewHeader';
|
||||||
import { oneOf } from 'utils/oneOf';
|
import { oneOf } from 'utils/oneOf';
|
||||||
@ -9,9 +9,8 @@ import {
|
|||||||
numOperators,
|
numOperators,
|
||||||
semVerOperators,
|
semVerOperators,
|
||||||
} from 'constants/operators';
|
} from 'constants/operators';
|
||||||
|
|
||||||
import { useStyles } from '../ConstraintAccordion.styles';
|
import { useStyles } from '../ConstraintAccordion.styles';
|
||||||
import { useState } from 'react';
|
|
||||||
interface IConstraintAccordionViewProps {
|
interface IConstraintAccordionViewProps {
|
||||||
constraint: IConstraint;
|
constraint: IConstraint;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
@ -46,7 +45,7 @@ export const ConstraintAccordionView = ({
|
|||||||
sx={{ cursor: expandable ? 'pointer' : 'default' }}
|
sx={{ cursor: expandable ? 'pointer' : 'default' }}
|
||||||
>
|
>
|
||||||
<AccordionSummary
|
<AccordionSummary
|
||||||
className={styles.summary}
|
classes={{ root: styles.summary }}
|
||||||
expandIcon={null}
|
expandIcon={null}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
import { makeStyles } from 'tss-react/mui';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles()(theme => ({
|
||||||
|
chip: {
|
||||||
|
margin: '0 0.5rem 0.5rem 0',
|
||||||
|
},
|
||||||
|
chipValue: {
|
||||||
|
whiteSpace: 'pre',
|
||||||
|
},
|
||||||
|
singleValueView: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
[theme.breakpoints.down(600)]: { flexDirection: 'column' },
|
||||||
|
},
|
||||||
|
singleValueText: {
|
||||||
|
marginRight: '0.75rem',
|
||||||
|
[theme.breakpoints.down(600)]: {
|
||||||
|
marginBottom: '0.75rem',
|
||||||
|
marginRight: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
settingsParagraph: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '0.5rem 0',
|
||||||
|
},
|
||||||
|
}));
|
@ -1,15 +1,14 @@
|
|||||||
import { Chip } from '@mui/material';
|
|
||||||
import { ImportExportOutlined, TextFormatOutlined } from '@mui/icons-material';
|
import { ImportExportOutlined, TextFormatOutlined } from '@mui/icons-material';
|
||||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { stringOperators } from 'constants/operators';
|
import { stringOperators } from 'constants/operators';
|
||||||
import { IConstraint } from 'interfaces/strategy';
|
import { IConstraint } from 'interfaces/strategy';
|
||||||
import { oneOf } from 'utils/oneOf';
|
import { oneOf } from 'utils/oneOf';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { useStyles } from 'component/common/ConstraintAccordion/ConstraintAccordion.styles';
|
|
||||||
import { ConstraintValueSearch } from 'component/common/ConstraintAccordion/ConstraintValueSearch/ConstraintValueSearch';
|
|
||||||
import { formatConstraintValue } from 'utils/formatConstraintValue';
|
import { formatConstraintValue } from 'utils/formatConstraintValue';
|
||||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||||
|
import { useStyles as useAccordionStyles } from 'component/common/ConstraintAccordion/ConstraintAccordion.styles';
|
||||||
|
import { useStyles } from './ConstraintAccordionViewBody.style';
|
||||||
|
import { MultipleValues } from './MultipleValues/MultipleValues';
|
||||||
|
import { SingleValue } from './SingleValue/SingleValue';
|
||||||
|
|
||||||
interface IConstraintAccordionViewBodyProps {
|
interface IConstraintAccordionViewBodyProps {
|
||||||
constraint: IConstraint;
|
constraint: IConstraint;
|
||||||
@ -18,7 +17,8 @@ interface IConstraintAccordionViewBodyProps {
|
|||||||
export const ConstraintAccordionViewBody = ({
|
export const ConstraintAccordionViewBody = ({
|
||||||
constraint,
|
constraint,
|
||||||
}: IConstraintAccordionViewBodyProps) => {
|
}: IConstraintAccordionViewBodyProps) => {
|
||||||
const { classes: styles } = useStyles();
|
const { classes: styles } = useAccordionStyles();
|
||||||
|
const { classes } = useStyles();
|
||||||
const { locationSettings } = useLocationSettings();
|
const { locationSettings } = useLocationSettings();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -29,7 +29,7 @@ export const ConstraintAccordionViewBody = ({
|
|||||||
Boolean(constraint.caseInsensitive)
|
Boolean(constraint.caseInsensitive)
|
||||||
}
|
}
|
||||||
show={
|
show={
|
||||||
<p className={styles.settingsParagraph}>
|
<p className={classes.settingsParagraph}>
|
||||||
<TextFormatOutlined className={styles.settingsIcon} />{' '}
|
<TextFormatOutlined className={styles.settingsIcon} />{' '}
|
||||||
Case insensitive setting is active
|
Case insensitive setting is active
|
||||||
</p>
|
</p>
|
||||||
@ -39,7 +39,7 @@ export const ConstraintAccordionViewBody = ({
|
|||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={Boolean(constraint.inverted)}
|
condition={Boolean(constraint.inverted)}
|
||||||
show={
|
show={
|
||||||
<p className={styles.settingsParagraph}>
|
<p className={classes.settingsParagraph}>
|
||||||
<ImportExportOutlined className={styles.settingsIcon} />{' '}
|
<ImportExportOutlined className={styles.settingsIcon} />{' '}
|
||||||
Operator is negated
|
Operator is negated
|
||||||
</p>
|
</p>
|
||||||
@ -56,70 +56,3 @@ export const ConstraintAccordionViewBody = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ISingleValueProps {
|
|
||||||
value: string | undefined;
|
|
||||||
operator: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SingleValue = ({ value, operator }: ISingleValueProps) => {
|
|
||||||
const { classes: styles } = useStyles();
|
|
||||||
if (!value) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.singleValueView}>
|
|
||||||
<p className={styles.singleValueText}>Value must be {operator}</p>{' '}
|
|
||||||
<Chip
|
|
||||||
label={
|
|
||||||
<StringTruncator
|
|
||||||
maxWidth="400"
|
|
||||||
text={value}
|
|
||||||
maxLength={50}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
className={styles.chip}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IMultipleValuesProps {
|
|
||||||
values: string[] | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MultipleValues = ({ values }: IMultipleValuesProps) => {
|
|
||||||
const [filter, setFilter] = useState('');
|
|
||||||
const { classes: styles } = useStyles();
|
|
||||||
|
|
||||||
if (!values || values.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={values.length > 20}
|
|
||||||
show={
|
|
||||||
<ConstraintValueSearch
|
|
||||||
filter={filter}
|
|
||||||
setFilter={setFilter}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{values
|
|
||||||
.filter(value => value.includes(filter))
|
|
||||||
.map((value, index) => (
|
|
||||||
<Chip
|
|
||||||
key={`${value}-${index}`}
|
|
||||||
label={
|
|
||||||
<StringTruncator
|
|
||||||
maxWidth="400"
|
|
||||||
text={value}
|
|
||||||
maxLength={50}
|
|
||||||
className={styles.chipValue}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
className={styles.chip}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Chip } from '@mui/material';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||||
|
import { ConstraintValueSearch } from '../../../ConstraintValueSearch/ConstraintValueSearch';
|
||||||
|
import { useStyles } from '../ConstraintAccordionViewBody.style';
|
||||||
|
|
||||||
|
interface IMultipleValuesProps {
|
||||||
|
values: string[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MultipleValues = ({ values }: IMultipleValuesProps) => {
|
||||||
|
const [filter, setFilter] = useState('');
|
||||||
|
const { classes: styles } = useStyles();
|
||||||
|
|
||||||
|
if (!values || values.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={values.length > 20}
|
||||||
|
show={
|
||||||
|
<ConstraintValueSearch
|
||||||
|
filter={filter}
|
||||||
|
setFilter={setFilter}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{values
|
||||||
|
.filter(value => value.includes(filter))
|
||||||
|
.map((value, index) => (
|
||||||
|
<Chip
|
||||||
|
key={`${value}-${index}`}
|
||||||
|
label={
|
||||||
|
<StringTruncator
|
||||||
|
maxWidth="400"
|
||||||
|
text={value}
|
||||||
|
maxLength={50}
|
||||||
|
className={styles.chipValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
className={styles.chip}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,29 @@
|
|||||||
|
import { Chip } from '@mui/material';
|
||||||
|
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||||
|
import { useStyles } from '../ConstraintAccordionViewBody.style';
|
||||||
|
|
||||||
|
interface ISingleValueProps {
|
||||||
|
value: string | undefined;
|
||||||
|
operator: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SingleValue = ({ value, operator }: ISingleValueProps) => {
|
||||||
|
const { classes: styles } = useStyles();
|
||||||
|
if (!value) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.singleValueView}>
|
||||||
|
<p className={styles.singleValueText}>Value must be {operator}</p>{' '}
|
||||||
|
<Chip
|
||||||
|
label={
|
||||||
|
<StringTruncator
|
||||||
|
maxWidth="400"
|
||||||
|
text={value}
|
||||||
|
maxLength={50}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
className={styles.chip}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,10 +1,8 @@
|
|||||||
import { ConstraintIcon } from 'component/common/ConstraintAccordion/ConstraintIcon';
|
import { ConstraintIcon } from 'component/common/ConstraintAccordion/ConstraintIcon';
|
||||||
import { IConstraint } from 'interfaces/strategy';
|
import { IConstraint } from 'interfaces/strategy';
|
||||||
|
|
||||||
import { useStyles } from 'component/common/ConstraintAccordion/ConstraintAccordion.styles';
|
|
||||||
import React from 'react';
|
|
||||||
import { ConstraintAccordionViewHeaderInfo } from './ConstraintAccordionViewHeaderInfo/ConstraintAccordionViewHeaderInfo';
|
import { ConstraintAccordionViewHeaderInfo } from './ConstraintAccordionViewHeaderInfo/ConstraintAccordionViewHeaderInfo';
|
||||||
import { ConstraintAccordionHeaderActions } from '../../ConstraintAccordionHeaderActions/ConstraintAccordionHeaderActions';
|
import { ConstraintAccordionHeaderActions } from '../../ConstraintAccordionHeaderActions/ConstraintAccordionHeaderActions';
|
||||||
|
import { useStyles } from 'component/common/ConstraintAccordion/ConstraintAccordion.styles';
|
||||||
|
|
||||||
interface IConstraintAccordionViewHeaderProps {
|
interface IConstraintAccordionViewHeaderProps {
|
||||||
constraint: IConstraint;
|
constraint: IConstraint;
|
||||||
|
@ -15,6 +15,8 @@ const StyledHeaderText = styled('span')(({ theme }) => ({
|
|||||||
maxWidth: '100px',
|
maxWidth: '100px',
|
||||||
minWidth: '100px',
|
minWidth: '100px',
|
||||||
marginRight: '10px',
|
marginRight: '10px',
|
||||||
|
marginTop: 'auto',
|
||||||
|
marginBottom: 'auto',
|
||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
[theme.breakpoints.down(710)]: {
|
[theme.breakpoints.down(710)]: {
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { IConstraint } from '../../../../../../interfaces/strategy';
|
import { IConstraint } from '../../../../../../interfaces/strategy';
|
||||||
import { ConditionallyRender } from '../../../../ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from '../../../../ConditionallyRender/ConditionallyRender';
|
||||||
import { Tooltip } from '@mui/material';
|
import { Tooltip, Box } from '@mui/material';
|
||||||
import { ReactComponent as NegatedIcon } from '../../../../../../assets/icons/24_Negator.svg';
|
import { ReactComponent as NegatedIcon } from '../../../../../../assets/icons/24_Negator.svg';
|
||||||
import { ConstraintOperator } from '../../../ConstraintOperator/ConstraintOperator';
|
import { ConstraintOperator } from '../../../ConstraintOperator/ConstraintOperator';
|
||||||
import React from 'react';
|
|
||||||
import { useStyles } from '../../../ConstraintAccordion.styles';
|
import { useStyles } from '../../../ConstraintAccordion.styles';
|
||||||
import { StyledIconWrapper } from '../StyledIconWrapper/StyledIconWrapper';
|
import { StyledIconWrapper } from '../StyledIconWrapper/StyledIconWrapper';
|
||||||
|
|
||||||
@ -22,14 +21,19 @@ export const ConstraintViewHeaderOperator = ({
|
|||||||
condition={Boolean(constraint.inverted)}
|
condition={Boolean(constraint.inverted)}
|
||||||
show={
|
show={
|
||||||
<Tooltip title={'Operator is negated'} arrow>
|
<Tooltip title={'Operator is negated'} arrow>
|
||||||
<StyledIconWrapper marginright={'0'}>
|
<Box sx={{ display: 'flex' }}>
|
||||||
|
<StyledIconWrapper isPrefix>
|
||||||
<NegatedIcon />
|
<NegatedIcon />
|
||||||
</StyledIconWrapper>
|
</StyledIconWrapper>
|
||||||
|
</Box>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<div className={styles.headerConstraintContainer}>
|
<div className={styles.headerConstraintContainer}>
|
||||||
<ConstraintOperator constraint={constraint} />
|
<ConstraintOperator
|
||||||
|
constraint={constraint}
|
||||||
|
hasPrefix={Boolean(constraint.inverted)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -17,6 +17,7 @@ const StyledValuesSpan = styled('span')(({ theme }) => ({
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
margin: 'auto 0',
|
||||||
[theme.breakpoints.down(710)]: {
|
[theme.breakpoints.down(710)]: {
|
||||||
margin: theme.spacing(1, 0),
|
margin: theme.spacing(1, 0),
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
@ -90,8 +91,7 @@ export const ConstraintAccordionViewHeaderMultipleValues = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!expanded
|
{!expanded
|
||||||
? `View all (
|
? `View all (${constraint?.values?.length})`
|
||||||
${constraint?.values?.length})`
|
|
||||||
: 'View less'}
|
: 'View less'}
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
|
@ -4,13 +4,13 @@ import { stringOperators } from '../../../../../../constants/operators';
|
|||||||
import { Chip, styled, Tooltip } from '@mui/material';
|
import { Chip, styled, Tooltip } from '@mui/material';
|
||||||
import { ReactComponent as CaseSensitive } from '../../../../../../assets/icons/24_Text format.svg';
|
import { ReactComponent as CaseSensitive } from '../../../../../../assets/icons/24_Text format.svg';
|
||||||
import { formatConstraintValue } from '../../../../../../utils/formatConstraintValue';
|
import { formatConstraintValue } from '../../../../../../utils/formatConstraintValue';
|
||||||
import React from 'react';
|
|
||||||
import { useStyles } from '../../../ConstraintAccordion.styles';
|
import { useStyles } from '../../../ConstraintAccordion.styles';
|
||||||
import { StyledIconWrapper } from '../StyledIconWrapper/StyledIconWrapper';
|
import { StyledIconWrapper } from '../StyledIconWrapper/StyledIconWrapper';
|
||||||
import { IConstraint } from '../../../../../../interfaces/strategy';
|
import { IConstraint } from '../../../../../../interfaces/strategy';
|
||||||
import { useLocationSettings } from '../../../../../../hooks/useLocationSettings';
|
import { useLocationSettings } from '../../../../../../hooks/useLocationSettings';
|
||||||
|
|
||||||
const StyledSingleValueChip = styled(Chip)(({ theme }) => ({
|
const StyledSingleValueChip = styled(Chip)(({ theme }) => ({
|
||||||
|
margin: 'auto 0',
|
||||||
[theme.breakpoints.down(710)]: {
|
[theme.breakpoints.down(710)]: {
|
||||||
margin: theme.spacing(1, 0),
|
margin: theme.spacing(1, 0),
|
||||||
},
|
},
|
||||||
|
@ -1,16 +1,34 @@
|
|||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { FC, forwardRef } from 'react';
|
||||||
|
|
||||||
export const StyledIconWrapper = styled('div')<{
|
export const StyledIconWrapperBase = styled('div')<{
|
||||||
marginright?: string;
|
prefix?: boolean;
|
||||||
}>(({ theme, marginright }) => ({
|
}>(({ theme }) => ({
|
||||||
backgroundColor: theme.palette.grey[200],
|
backgroundColor: theme.palette.grey[200],
|
||||||
width: 28,
|
width: 28,
|
||||||
height: 48,
|
display: 'flex',
|
||||||
display: 'inline-flex',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
padding: '10px 0',
|
alignSelf: 'stretch',
|
||||||
color: theme.palette.primary.main,
|
color: theme.palette.primary.main,
|
||||||
marginRight: marginright ? marginright : '1rem',
|
marginRight: '1rem',
|
||||||
marginTop: 'auto',
|
borderRadius: theme.shape.borderRadius,
|
||||||
marginBottom: 'auto',
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const StyledPrefixIconWrapper = styled(StyledIconWrapperBase)(() => ({
|
||||||
|
marginRight: 0,
|
||||||
|
borderTopRightRadius: 0,
|
||||||
|
borderBottomRightRadius: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledIconWrapper = forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
{ isPrefix?: boolean; children?: React.ReactNode }
|
||||||
|
>(({ isPrefix, ...props }, ref) => (
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(isPrefix)}
|
||||||
|
show={() => <StyledPrefixIconWrapper ref={ref} {...props} />}
|
||||||
|
elseShow={() => <StyledIconWrapperBase ref={ref} {...props} />}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
@ -2,7 +2,7 @@ import { makeStyles } from 'tss-react/mui';
|
|||||||
|
|
||||||
export const useStyles = makeStyles()(theme => ({
|
export const useStyles = makeStyles()(theme => ({
|
||||||
container: {
|
container: {
|
||||||
padding: '0.5rem 0.75rem',
|
padding: theme.spacing(0.5, 1.5),
|
||||||
borderRadius: theme.shape.borderRadius,
|
borderRadius: theme.shape.borderRadius,
|
||||||
backgroundColor: theme.palette.grey[200],
|
backgroundColor: theme.palette.grey[200],
|
||||||
lineHeight: 1.25,
|
lineHeight: 1.25,
|
||||||
|
@ -5,10 +5,12 @@ import React from 'react';
|
|||||||
|
|
||||||
interface IConstraintOperatorProps {
|
interface IConstraintOperatorProps {
|
||||||
constraint: IConstraint;
|
constraint: IConstraint;
|
||||||
|
hasPrefix?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConstraintOperator = ({
|
export const ConstraintOperator = ({
|
||||||
constraint,
|
constraint,
|
||||||
|
hasPrefix,
|
||||||
}: IConstraintOperatorProps) => {
|
}: IConstraintOperatorProps) => {
|
||||||
const { classes: styles } = useStyles();
|
const { classes: styles } = useStyles();
|
||||||
|
|
||||||
@ -16,7 +18,13 @@ export const ConstraintOperator = ({
|
|||||||
const operatorText = formatOperatorDescription(constraint.operator);
|
const operatorText = formatOperatorDescription(constraint.operator);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div
|
||||||
|
className={styles.container}
|
||||||
|
style={{
|
||||||
|
borderTopLeftRadius: hasPrefix ? 0 : undefined,
|
||||||
|
borderBottomLeftRadius: hasPrefix ? 0 : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className={styles.name}>{operatorName}</div>
|
<div className={styles.name}>{operatorName}</div>
|
||||||
<div className={styles.text}>{operatorText}</div>
|
<div className={styles.text}>{operatorText}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,24 +1,45 @@
|
|||||||
import { useTheme } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
|
import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
interface IStrategySeparatorProps {
|
interface IStrategySeparatorProps {
|
||||||
text: string;
|
text: 'AND' | 'OR';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StrategySeparator = ({ text }: IStrategySeparatorProps) => {
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
const theme = useTheme();
|
height: theme.spacing(2),
|
||||||
|
position: 'relative',
|
||||||
|
width: '100%',
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
const StyledContent = styled('div')(({ theme }) => ({
|
||||||
<div
|
padding: theme.spacing(0.75, 1.5),
|
||||||
style={{
|
color: theme.palette.text.primary,
|
||||||
color: theme.palette.primary.main,
|
|
||||||
padding: '0.1rem 0.25rem',
|
|
||||||
border: `1px solid ${theme.palette.primary.main}`,
|
|
||||||
borderRadius: '0.25rem',
|
|
||||||
fontSize: theme.fontSizes.smallerBody,
|
fontSize: theme.fontSizes.smallerBody,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: theme.palette.secondaryContainer,
|
||||||
}}
|
borderRadius: theme.shape.borderRadius,
|
||||||
>
|
position: 'absolute',
|
||||||
{text}
|
zIndex: theme.zIndex.fab,
|
||||||
</div>
|
top: '50%',
|
||||||
);
|
left: theme.spacing(3),
|
||||||
};
|
transform: 'translateY(-50%)',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledCenteredContent = styled(StyledContent)(({ theme }) => ({
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
backgroundColor: theme.palette.secondary.light,
|
||||||
|
border: `1px solid ${theme.palette.primary.border}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StrategySeparator = ({ text }: IStrategySeparatorProps) => (
|
||||||
|
<StyledContainer>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={text === 'AND'}
|
||||||
|
show={() => <StyledContent>{text}</StyledContent>}
|
||||||
|
elseShow={() => (
|
||||||
|
<StyledCenteredContent>{text}</StyledCenteredContent>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
@ -2,7 +2,7 @@ import { makeStyles } from 'tss-react/mui';
|
|||||||
|
|
||||||
export const useStyles = makeStyles()(theme => ({
|
export const useStyles = makeStyles()(theme => ({
|
||||||
emptyStateListItem: {
|
emptyStateListItem: {
|
||||||
border: `2px dashed ${theme.palette.grey[100]}`,
|
border: `2px dashed ${theme.palette.neutral.light}`,
|
||||||
padding: '0.8rem',
|
padding: '0.8rem',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
@ -1,14 +1,22 @@
|
|||||||
import { makeStyles } from 'tss-react/mui';
|
import { makeStyles } from 'tss-react/mui';
|
||||||
|
|
||||||
export const useStyles = makeStyles()(theme => ({
|
export const useStyles = makeStyles()(theme => ({
|
||||||
noItemsParagraph: {
|
container: {
|
||||||
margin: '1rem 0',
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
link: {
|
title: {
|
||||||
display: 'block',
|
fontSize: theme.fontSizes.bodySize,
|
||||||
margin: '1rem 0 0 0',
|
textAlign: 'center',
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
},
|
},
|
||||||
envName: {
|
description: {
|
||||||
fontWeight: 'bold',
|
color: theme.palette.text.secondary,
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: theme.spacing(3),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
import NoItems from 'component/common/NoItems/NoItems';
|
import { Link } from 'react-router-dom';
|
||||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
import { Box } from '@mui/material';
|
||||||
import { useStyles } from './FeatureStrategyEmpty.styles';
|
import { SectionSeparator } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/SectionSeparator/SectionSeparator';
|
||||||
|
import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||||
import { FeatureStrategyMenu } from '../FeatureStrategyMenu/FeatureStrategyMenu';
|
import { FeatureStrategyMenu } from '../FeatureStrategyMenu/FeatureStrategyMenu';
|
||||||
|
import { PresetCard } from './PresetCard/PresetCard';
|
||||||
|
import { useStyles } from './FeatureStrategyEmpty.styles';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
|
||||||
interface IFeatureStrategyEmptyProps {
|
interface IFeatureStrategyEmptyProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -15,30 +21,58 @@ export const FeatureStrategyEmpty = ({
|
|||||||
environmentId,
|
environmentId,
|
||||||
}: IFeatureStrategyEmptyProps) => {
|
}: IFeatureStrategyEmptyProps) => {
|
||||||
const { classes: styles } = useStyles();
|
const { classes: styles } = useStyles();
|
||||||
|
const { addStrategyToFeature } = useFeatureStrategyApi();
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const { refetchFeature } = useFeature(projectId, featureId);
|
||||||
|
|
||||||
|
const onAfterAddStrategy = () => {
|
||||||
|
refetchFeature();
|
||||||
|
setToastData({
|
||||||
|
title: 'Strategy created',
|
||||||
|
text: 'Successfully created strategy',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddSimpleStrategy = async () => {
|
||||||
|
try {
|
||||||
|
await addStrategyToFeature(projectId, featureId, environmentId, {
|
||||||
|
name: 'default',
|
||||||
|
parameters: {},
|
||||||
|
constraints: [],
|
||||||
|
});
|
||||||
|
onAfterAddStrategy();
|
||||||
|
} catch (error) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddGradualRolloutStrategy = async () => {
|
||||||
|
try {
|
||||||
|
await addStrategyToFeature(projectId, featureId, environmentId, {
|
||||||
|
name: 'flexibleRollout',
|
||||||
|
parameters: {
|
||||||
|
rollout: '50',
|
||||||
|
stickiness: 'default',
|
||||||
|
},
|
||||||
|
constraints: [],
|
||||||
|
});
|
||||||
|
onAfterAddStrategy();
|
||||||
|
} catch (error) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NoItems>
|
<div className={styles.container}>
|
||||||
<p className={styles.noItemsParagraph}>
|
<div className={styles.title}>
|
||||||
No strategies added in the{' '}
|
You have not defined any strategies yet.
|
||||||
<StringTruncator
|
</div>
|
||||||
text={environmentId}
|
<p className={styles.description}>
|
||||||
maxWidth={'130'}
|
|
||||||
maxLength={15}
|
|
||||||
className={styles.envName}
|
|
||||||
/>{' '}
|
|
||||||
environment
|
|
||||||
</p>
|
|
||||||
<p className={styles.noItemsParagraph}>
|
|
||||||
Strategies added in this environment will only be executed if
|
Strategies added in this environment will only be executed if
|
||||||
the SDK is using an API key configured for this environment.
|
the SDK is using an{' '}
|
||||||
<a
|
<Link to="/admin/api">API key configured</Link> for this
|
||||||
className={styles.link}
|
environment.
|
||||||
href="https://docs.getunleash.io/user_guide/environments"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Read more here
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
<FeatureStrategyMenu
|
<FeatureStrategyMenu
|
||||||
label="Add your first strategy"
|
label="Add your first strategy"
|
||||||
@ -46,6 +80,31 @@ export const FeatureStrategyEmpty = ({
|
|||||||
featureId={featureId}
|
featureId={featureId}
|
||||||
environmentId={environmentId}
|
environmentId={environmentId}
|
||||||
/>
|
/>
|
||||||
</NoItems>
|
<Box sx={{ width: '100%', mt: 3 }}>
|
||||||
|
<SectionSeparator>Or use a strategy template</SectionSeparator>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'grid',
|
||||||
|
width: '100%',
|
||||||
|
gap: 2,
|
||||||
|
gridTemplateColumns: '1fr 1fr',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PresetCard
|
||||||
|
title="Standard strategy"
|
||||||
|
onClick={onAddSimpleStrategy}
|
||||||
|
>
|
||||||
|
The standard strategy is strictly on/off for your entire
|
||||||
|
userbase.
|
||||||
|
</PresetCard>
|
||||||
|
<PresetCard
|
||||||
|
title="Gradual rollout"
|
||||||
|
onClick={onAddGradualRolloutStrategy}
|
||||||
|
>
|
||||||
|
Roll out to a percentage of your userbase.
|
||||||
|
</PresetCard>
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
import { Button, Card, CardContent, Typography } from '@mui/material';
|
||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
interface IPresetCardProps {
|
||||||
|
title: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PresetCard: FC<IPresetCardProps> = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
}) => (
|
||||||
|
<Card variant="outlined" sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<CardContent
|
||||||
|
sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}
|
||||||
|
>
|
||||||
|
<Typography variant="body1" fontWeight="medium" sx={{ mb: 0.5 }}>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" component="p">
|
||||||
|
{children}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
sx={{ ml: 'auto', mt: 'auto' }}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
Use template
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
@ -16,7 +16,9 @@ export const FeatureStrategyIcon = ({
|
|||||||
return (
|
return (
|
||||||
<StyledIcon>
|
<StyledIcon>
|
||||||
<Tooltip title={formatStrategyName(strategyName)} arrow>
|
<Tooltip title={formatStrategyName(strategyName)} arrow>
|
||||||
|
<>
|
||||||
<Icon />
|
<Icon />
|
||||||
|
</>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</StyledIcon>
|
</StyledIcon>
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
import PermissionButton, {
|
import PermissionButton, {
|
||||||
IPermissionButtonProps,
|
IPermissionButtonProps,
|
||||||
} from 'component/common/PermissionButton/PermissionButton';
|
} from 'component/common/PermissionButton/PermissionButton';
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
|
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
|
||||||
import { Popover } from '@mui/material';
|
import { Popover } from '@mui/material';
|
||||||
import { FeatureStrategyMenuCards } from './FeatureStrategyMenuCards/FeatureStrategyMenuCards';
|
import { FeatureStrategyMenuCards } from './FeatureStrategyMenuCards/FeatureStrategyMenuCards';
|
||||||
|
@ -3,7 +3,7 @@ import { makeStyles } from 'tss-react/mui';
|
|||||||
export const useStyles = makeStyles()(theme => ({
|
export const useStyles = makeStyles()(theme => ({
|
||||||
item: {
|
item: {
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
background: theme.palette.grey[100],
|
background: theme.palette.secondaryContainer,
|
||||||
borderRadius: theme.spacing(2),
|
borderRadius: theme.spacing(2),
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
[theme.breakpoints.up('md')]: {
|
[theme.breakpoints.up('md')]: {
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
import { makeStyles } from 'tss-react/mui';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles()(theme => ({
|
||||||
|
accordionBodyInnerContainer: {
|
||||||
|
[theme.breakpoints.down(400)]: {
|
||||||
|
padding: '0.5rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
accordionBody: {
|
||||||
|
width: '100%',
|
||||||
|
position: 'relative',
|
||||||
|
paddingBottom: '1rem',
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,113 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Alert } from '@mui/material';
|
||||||
|
import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { StrategyDraggableItem } from './StrategyDraggableItem/StrategyDraggableItem';
|
||||||
|
import { IFeatureEnvironment } from 'interfaces/featureToggle';
|
||||||
|
import { FeatureStrategyEmpty } from 'component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty';
|
||||||
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
|
import { useStyles } from './EnvironmentAccordionBody.styles';
|
||||||
|
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||||
|
|
||||||
|
interface IEnvironmentAccordionBodyProps {
|
||||||
|
isDisabled: boolean;
|
||||||
|
featureEnvironment?: IFeatureEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EnvironmentAccordionBody = ({
|
||||||
|
featureEnvironment,
|
||||||
|
isDisabled,
|
||||||
|
}: IEnvironmentAccordionBodyProps) => {
|
||||||
|
const projectId = useRequiredPathParam('projectId');
|
||||||
|
const featureId = useRequiredPathParam('featureId');
|
||||||
|
const { setStrategiesSortOrder } = useFeatureStrategyApi();
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const { refetchFeature } = useFeature(projectId, featureId);
|
||||||
|
|
||||||
|
const [strategies, setStrategies] = useState(
|
||||||
|
featureEnvironment?.strategies || []
|
||||||
|
);
|
||||||
|
const { classes: styles } = useStyles();
|
||||||
|
useEffect(() => {
|
||||||
|
// Use state to enable drag and drop, but switch to API output when it arrives
|
||||||
|
setStrategies(featureEnvironment?.strategies || []);
|
||||||
|
}, [featureEnvironment?.strategies]);
|
||||||
|
|
||||||
|
if (!featureEnvironment) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDragAndDrop = async (
|
||||||
|
from: number,
|
||||||
|
to: number,
|
||||||
|
dropped?: boolean
|
||||||
|
) => {
|
||||||
|
if (from !== to && dropped) {
|
||||||
|
const newStrategies = [...strategies];
|
||||||
|
const movedStrategy = newStrategies.splice(from, 1)[0];
|
||||||
|
newStrategies.splice(to, 0, movedStrategy);
|
||||||
|
setStrategies(newStrategies);
|
||||||
|
try {
|
||||||
|
await setStrategiesSortOrder(
|
||||||
|
projectId,
|
||||||
|
featureId,
|
||||||
|
featureEnvironment.name,
|
||||||
|
[...newStrategies].map((strategy, sortOrder) => ({
|
||||||
|
id: strategy.id,
|
||||||
|
sortOrder,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
refetchFeature();
|
||||||
|
setToastData({
|
||||||
|
title: 'Order of strategies updated',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.accordionBody}>
|
||||||
|
<div className={styles.accordionBodyInnerContainer}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={strategies.length > 0 && isDisabled}
|
||||||
|
show={() => (
|
||||||
|
<Alert severity="warning" sx={{ mb: 2 }}>
|
||||||
|
This environment is disabled, which means that none
|
||||||
|
of your strategies are executing.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={strategies.length > 0}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
{strategies.map((strategy, index) => (
|
||||||
|
<StrategyDraggableItem
|
||||||
|
key={strategy.id}
|
||||||
|
onDragAndDrop={onDragAndDrop}
|
||||||
|
strategy={strategy}
|
||||||
|
index={index}
|
||||||
|
environmentName={featureEnvironment.name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<FeatureStrategyEmpty
|
||||||
|
projectId={projectId}
|
||||||
|
featureId={featureId}
|
||||||
|
environmentId={featureEnvironment.name}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EnvironmentAccordionBody;
|
@ -0,0 +1,35 @@
|
|||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
|
||||||
|
import { MoveListItem, useDragItem } from 'hooks/useDragItem';
|
||||||
|
import { IFeatureStrategy } from 'interfaces/strategy';
|
||||||
|
import { StrategyItem } from './StrategyItem/StrategyItem';
|
||||||
|
|
||||||
|
interface IStrategyDraggableItemProps {
|
||||||
|
strategy: IFeatureStrategy;
|
||||||
|
environmentName: string;
|
||||||
|
index: number;
|
||||||
|
onDragAndDrop: MoveListItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StrategyDraggableItem = ({
|
||||||
|
strategy,
|
||||||
|
index,
|
||||||
|
environmentName,
|
||||||
|
onDragAndDrop,
|
||||||
|
}: IStrategyDraggableItemProps) => {
|
||||||
|
const ref = useDragItem(index, onDragAndDrop);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={strategy.id} ref={ref}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={index > 0}
|
||||||
|
show={<StrategySeparator text="OR" />}
|
||||||
|
/>
|
||||||
|
<StrategyItem
|
||||||
|
strategy={strategy}
|
||||||
|
environmentId={environmentName}
|
||||||
|
isDraggable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,7 +1,12 @@
|
|||||||
import { makeStyles } from 'tss-react/mui';
|
import { makeStyles } from 'tss-react/mui';
|
||||||
|
|
||||||
export const useStyles = makeStyles()(theme => ({
|
export const useStyles = makeStyles()(theme => ({
|
||||||
container: { textAlign: 'center' },
|
container: {
|
||||||
|
width: '100%',
|
||||||
|
padding: theme.spacing(2, 3),
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
|
},
|
||||||
chip: {
|
chip: {
|
||||||
margin: '0.25rem',
|
margin: '0.25rem',
|
||||||
},
|
},
|
@ -1,17 +1,14 @@
|
|||||||
import { Chip } from '@mui/material';
|
import { Chip } from '@mui/material';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { useStyles } from './FeatureOverviewExecutionChips.styles';
|
import { useStyles } from './ConstraintItem.styles';
|
||||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||||
|
|
||||||
interface IFeatureOverviewExecutionChipsProps {
|
interface IConstraintItemProps {
|
||||||
value: string[];
|
value: string[];
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FeatureOverviewExecutionChips = ({
|
export const ConstraintItem = ({ value, text }: IConstraintItemProps) => {
|
||||||
value,
|
|
||||||
text,
|
|
||||||
}: IFeatureOverviewExecutionChipsProps) => {
|
|
||||||
const { classes: styles } = useStyles();
|
const { classes: styles } = useStyles();
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
@ -44,5 +41,3 @@ const FeatureOverviewExecutionChips = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FeatureOverviewExecutionChips;
|
|
@ -9,4 +9,10 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
valueSeparator: {
|
valueSeparator: {
|
||||||
color: theme.palette.grey[700],
|
color: theme.palette.grey[700],
|
||||||
},
|
},
|
||||||
|
summary: {
|
||||||
|
width: '100%',
|
||||||
|
padding: theme.spacing(2, 3),
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
|
},
|
||||||
}));
|
}));
|
@ -1,37 +1,29 @@
|
|||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
import {
|
import { Box, Chip } from '@mui/material';
|
||||||
IFeatureStrategy,
|
import { IFeatureStrategy } from 'interfaces/strategy';
|
||||||
IFeatureStrategyParameters,
|
|
||||||
IConstraint,
|
|
||||||
} from 'interfaces/strategy';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/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 FeatureOverviewExecutionChips from './FeatureOverviewExecutionChips/FeatureOverviewExecutionChips';
|
import { ConstraintItem } from './ConstraintItem/ConstraintItem';
|
||||||
import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
|
import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
|
||||||
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';
|
import { ConstraintAccordionList } from 'component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList';
|
||||||
import { useStyles } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewExecution/FeatureOverviewExecution.styles';
|
import { useStyles } from './StrategyExecution.styles';
|
||||||
import {
|
import {
|
||||||
parseParameterString,
|
parseParameterString,
|
||||||
parseParameterNumber,
|
parseParameterNumber,
|
||||||
parseParameterStrings,
|
parseParameterStrings,
|
||||||
} from 'utils/parseParameter';
|
} from 'utils/parseParameter';
|
||||||
|
|
||||||
interface IFeatureOverviewExecutionProps {
|
interface IStrategyExecutionProps {
|
||||||
parameters: IFeatureStrategyParameters;
|
|
||||||
constraints?: IConstraint[];
|
|
||||||
strategy: IFeatureStrategy;
|
strategy: IFeatureStrategy;
|
||||||
percentageFill?: string;
|
percentageFill?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FeatureOverviewExecution = ({
|
export const StrategyExecution = ({ strategy }: IStrategyExecutionProps) => {
|
||||||
parameters,
|
const { parameters, constraints = [] } = strategy;
|
||||||
constraints = [],
|
|
||||||
strategy,
|
|
||||||
}: IFeatureOverviewExecutionProps) => {
|
|
||||||
const { classes: styles } = useStyles();
|
const { classes: styles } = useStyles();
|
||||||
const { strategies } = useStrategies();
|
const { strategies } = useStrategies();
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
@ -52,51 +44,51 @@ const FeatureOverviewExecution = ({
|
|||||||
case 'rollout':
|
case 'rollout':
|
||||||
case 'Rollout':
|
case 'Rollout':
|
||||||
return (
|
return (
|
||||||
<Fragment key={key}>
|
<Box
|
||||||
<p>
|
className={styles.summary}
|
||||||
{parameters[key]}% of your base{' '}
|
key={key}
|
||||||
{constraints.length > 0
|
sx={{ display: 'flex', alignItems: 'center' }}
|
||||||
? 'who match constraints'
|
>
|
||||||
: ''}{' '}
|
|
||||||
is included.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<PercentageCircle
|
<PercentageCircle
|
||||||
percentage={parseParameterNumber(
|
percentage={parseParameterNumber(
|
||||||
parameters[key]
|
parameters[key]
|
||||||
)}
|
)}
|
||||||
|
styles={{
|
||||||
|
width: '2rem',
|
||||||
|
height: '2rem',
|
||||||
|
marginRight: '1rem',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
<div>
|
||||||
|
<Chip
|
||||||
|
color="success"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
label={`${parameters[key]}%`}
|
||||||
|
/>{' '}
|
||||||
|
of your base{' '}
|
||||||
|
{constraints.length > 0
|
||||||
|
? 'who match constraints'
|
||||||
|
: ''}{' '}
|
||||||
|
is included.
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
case 'userIds':
|
case 'userIds':
|
||||||
case 'UserIds':
|
case 'UserIds':
|
||||||
const users = parseParameterStrings(parameters[key]);
|
const users = parseParameterStrings(parameters[key]);
|
||||||
return (
|
return (
|
||||||
<FeatureOverviewExecutionChips
|
<ConstraintItem key={key} value={users} text="user" />
|
||||||
key={key}
|
|
||||||
value={users}
|
|
||||||
text="user"
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
case 'hostNames':
|
case 'hostNames':
|
||||||
case 'HostNames':
|
case 'HostNames':
|
||||||
const hosts = parseParameterStrings(parameters[key]);
|
const hosts = parseParameterStrings(parameters[key]);
|
||||||
return (
|
return (
|
||||||
<FeatureOverviewExecutionChips
|
<ConstraintItem key={key} value={hosts} text={'host'} />
|
||||||
key={key}
|
|
||||||
value={hosts}
|
|
||||||
text={'host'}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
case 'IPs':
|
case 'IPs':
|
||||||
const IPs = parseParameterStrings(parameters[key]);
|
const IPs = parseParameterStrings(parameters[key]);
|
||||||
return (
|
return <ConstraintItem key={key} value={IPs} text={'IP'} />;
|
||||||
<FeatureOverviewExecutionChips
|
|
||||||
key={key}
|
|
||||||
value={IPs}
|
|
||||||
text={'IP'}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'stickiness':
|
case 'stickiness':
|
||||||
case 'groupId':
|
case 'groupId':
|
||||||
return null;
|
return null;
|
||||||
@ -117,10 +109,7 @@ const FeatureOverviewExecution = ({
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Fragment key={param?.name}>
|
<Fragment key={param?.name}>
|
||||||
<FeatureOverviewExecutionChips
|
<ConstraintItem value={values} text={param.name} />
|
||||||
value={values}
|
|
||||||
text={param.name}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={notLastItem}
|
condition={notLastItem}
|
||||||
show={<StrategySeparator text="AND" />}
|
show={<StrategySeparator text="AND" />}
|
||||||
@ -130,13 +119,21 @@ const FeatureOverviewExecution = ({
|
|||||||
case 'percentage':
|
case 'percentage':
|
||||||
return (
|
return (
|
||||||
<Fragment key={param?.name}>
|
<Fragment key={param?.name}>
|
||||||
<p>
|
<div>
|
||||||
{strategy?.parameters[param.name]}% of your base{' '}
|
<Chip
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
color="success"
|
||||||
|
label={`${
|
||||||
|
strategy?.parameters[param.name]
|
||||||
|
}%`}
|
||||||
|
/>{' '}
|
||||||
|
of your base{' '}
|
||||||
{constraints?.length > 0
|
{constraints?.length > 0
|
||||||
? 'who match constraints'
|
? 'who match constraints'
|
||||||
: ''}{' '}
|
: ''}{' '}
|
||||||
is included.
|
is included.
|
||||||
</p>
|
</div>
|
||||||
<PercentageCircle
|
<PercentageCircle
|
||||||
percentage={parseParameterNumber(
|
percentage={parseParameterNumber(
|
||||||
strategy.parameters[param.name]
|
strategy.parameters[param.name]
|
||||||
@ -266,12 +263,21 @@ const FeatureOverviewExecution = ({
|
|||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={strategy.name === 'default'}
|
condition={strategy.name === 'default'}
|
||||||
show={<p>The standard strategy is on for all users.</p>}
|
show={
|
||||||
|
<Box sx={{ width: '100%' }} className={styles.summary}>
|
||||||
|
The standard strategy is{' '}
|
||||||
|
<Chip
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
color="success"
|
||||||
|
label="ON"
|
||||||
|
/>{' '}
|
||||||
|
for all users.
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{renderParameters()}
|
{renderParameters()}
|
||||||
{renderCustomStrategy()}
|
{renderCustomStrategy()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FeatureOverviewExecution;
|
|
@ -2,18 +2,20 @@ import { makeStyles } from 'tss-react/mui';
|
|||||||
|
|
||||||
export const useStyles = makeStyles()(theme => ({
|
export const useStyles = makeStyles()(theme => ({
|
||||||
container: {
|
container: {
|
||||||
borderRadius: theme.shape.borderRadius,
|
borderRadius: theme.shape.borderRadiusMedium,
|
||||||
border: `1px solid ${theme.palette.grey[300]}`,
|
border: `1px solid ${theme.palette.grey[300]}`,
|
||||||
'& + &': {
|
'& + &': {
|
||||||
marginTop: '1rem',
|
marginTop: '1rem',
|
||||||
},
|
},
|
||||||
|
background: theme.palette.background.default,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
padding: '0.5rem',
|
padding: theme.spacing(0.5, 2),
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: '0.5rem',
|
gap: '0.5rem',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderBottom: `1px solid ${theme.palette.grey[300]}`,
|
borderBottom: `1px solid ${theme.palette.grey[300]}`,
|
||||||
|
fontWeight: theme.typography.fontWeightMedium,
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
fill: theme.palette.inactiveIcon,
|
fill: theme.palette.inactiveIcon,
|
||||||
@ -25,7 +27,6 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
body: {
|
body: {
|
||||||
padding: '1rem',
|
padding: '1rem',
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gap: '1rem',
|
|
||||||
justifyItems: 'center',
|
justifyItems: 'center',
|
||||||
},
|
},
|
||||||
}));
|
}));
|
@ -1,5 +1,5 @@
|
|||||||
import { Edit } from '@mui/icons-material';
|
import { DragIndicator, Edit } from '@mui/icons-material';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { styled, useTheme, IconButton } from '@mui/material';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { IFeatureStrategy } from 'interfaces/strategy';
|
import { IFeatureStrategy } from 'interfaces/strategy';
|
||||||
import {
|
import {
|
||||||
@ -8,28 +8,36 @@ import {
|
|||||||
} from 'utils/strategyNames';
|
} from 'utils/strategyNames';
|
||||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||||
import { UPDATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
|
import { UPDATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
|
||||||
import FeatureOverviewExecution from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewExecution/FeatureOverviewExecution';
|
|
||||||
import { useStyles } from './FeatureOverviewEnvironmentStrategy.styles';
|
|
||||||
import { formatEditStrategyPath } from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
|
import { formatEditStrategyPath } from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
|
||||||
import { FeatureStrategyRemove } from 'component/feature/FeatureStrategy/FeatureStrategyRemove/FeatureStrategyRemove';
|
import { FeatureStrategyRemove } from 'component/feature/FeatureStrategy/FeatureStrategyRemove/FeatureStrategyRemove';
|
||||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
|
import { StrategyExecution } from './StrategyExecution/StrategyExecution';
|
||||||
|
import { useStyles } from './StrategyItem.styles';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
interface IFeatureOverviewEnvironmentStrategyProps {
|
interface IStrategyItemProps {
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
strategy: IFeatureStrategy;
|
strategy: IFeatureStrategy;
|
||||||
|
isDraggable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FeatureOverviewEnvironmentStrategy = ({
|
const DragIcon = styled(IconButton)(({ theme }) => ({
|
||||||
|
padding: 0,
|
||||||
|
cursor: 'inherit',
|
||||||
|
transition: 'color 0.2s ease-in-out',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StrategyItem = ({
|
||||||
environmentId,
|
environmentId,
|
||||||
strategy,
|
strategy,
|
||||||
}: IFeatureOverviewEnvironmentStrategyProps) => {
|
isDraggable,
|
||||||
|
}: IStrategyItemProps) => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const featureId = useRequiredPathParam('featureId');
|
const featureId = useRequiredPathParam('featureId');
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { classes: styles } = useStyles();
|
const { classes: styles } = useStyles();
|
||||||
const Icon = getFeatureStrategyIcon(strategy.name);
|
const Icon = getFeatureStrategyIcon(strategy.name);
|
||||||
const { parameters, constraints } = strategy;
|
|
||||||
|
|
||||||
const editStrategyPath = formatEditStrategyPath(
|
const editStrategyPath = formatEditStrategyPath(
|
||||||
projectId,
|
projectId,
|
||||||
@ -41,6 +49,17 @@ const FeatureOverviewEnvironmentStrategy = ({
|
|||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(isDraggable)}
|
||||||
|
show={() => (
|
||||||
|
<DragIcon disableRipple disabled size="small">
|
||||||
|
<DragIndicator
|
||||||
|
titleAccess="Drag to reorder"
|
||||||
|
cursor="grab"
|
||||||
|
/>
|
||||||
|
</DragIcon>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Icon className={styles.icon} />
|
<Icon className={styles.icon} />
|
||||||
<StringTruncator
|
<StringTruncator
|
||||||
maxWidth="150"
|
maxWidth="150"
|
||||||
@ -68,15 +87,11 @@ const FeatureOverviewEnvironmentStrategy = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
<FeatureOverviewExecution
|
<StrategyExecution
|
||||||
parameters={parameters}
|
|
||||||
strategy={strategy}
|
strategy={strategy}
|
||||||
constraints={constraints}
|
|
||||||
percentageFill={theme.palette.grey[200]}
|
percentageFill={theme.palette.grey[200]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FeatureOverviewEnvironmentStrategy;
|
|
@ -1,27 +1,22 @@
|
|||||||
import { IFeatureEnvironmentMetrics } from 'interfaces/featureToggle';
|
import { IFeatureEnvironmentMetrics } from 'interfaces/featureToggle';
|
||||||
import { useStyles } from '../FeatureOverviewEnvironment.styles';
|
|
||||||
import { FeatureMetricsStats } from 'component/feature/FeatureView/FeatureMetrics/FeatureMetricsStats/FeatureMetricsStats';
|
import { FeatureMetricsStats } from 'component/feature/FeatureView/FeatureMetrics/FeatureMetricsStats/FeatureMetricsStats';
|
||||||
|
import { SectionSeparator } from '../SectionSeparator/SectionSeparator';
|
||||||
|
|
||||||
interface IFeatureOverviewEnvironmentFooterProps {
|
interface IEnvironmentFooterProps {
|
||||||
environmentMetric?: IFeatureEnvironmentMetrics;
|
environmentMetric?: IFeatureEnvironmentMetrics;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FeatureOverviewEnvironmentFooter = ({
|
export const EnvironmentFooter = ({
|
||||||
environmentMetric,
|
environmentMetric,
|
||||||
}: IFeatureOverviewEnvironmentFooterProps) => {
|
}: IEnvironmentFooterProps) => {
|
||||||
const { classes: styles } = useStyles();
|
|
||||||
|
|
||||||
if (!environmentMetric) {
|
if (!environmentMetric) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.resultInfo}>
|
<SectionSeparator>Feature toggle exposure</SectionSeparator>
|
||||||
<div className={styles.leftWing} />
|
|
||||||
<div className={styles.separatorText}>Result</div>
|
|
||||||
<div className={styles.rightWing} />
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<FeatureMetricsStats
|
<FeatureMetricsStats
|
||||||
totalYes={environmentMetric.yes}
|
totalYes={environmentMetric.yes}
|
||||||
@ -32,4 +27,3 @@ const FeatureOverviewEnvironmentFooter = ({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default FeatureOverviewEnvironmentFooter;
|
|
@ -2,13 +2,13 @@ import { makeStyles } from 'tss-react/mui';
|
|||||||
|
|
||||||
export const useStyles = makeStyles()(theme => ({
|
export const useStyles = makeStyles()(theme => ({
|
||||||
featureOverviewEnvironment: {
|
featureOverviewEnvironment: {
|
||||||
borderRadius: '12.5px',
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
padding: '0.2rem',
|
marginBottom: theme.spacing(2),
|
||||||
marginBottom: '1rem',
|
background: theme.palette.background.default,
|
||||||
backgroundColor: '#fff',
|
|
||||||
},
|
},
|
||||||
accordionContainer: {
|
accordion: {
|
||||||
width: '100%',
|
boxShadow: 'none',
|
||||||
|
background: 'none',
|
||||||
},
|
},
|
||||||
accordionHeader: {
|
accordionHeader: {
|
||||||
boxShadow: 'none',
|
boxShadow: 'none',
|
||||||
@ -22,16 +22,26 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
padding: '0.5rem',
|
padding: '0.5rem',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
accordionDetails: {
|
||||||
|
padding: theme.spacing(3),
|
||||||
|
background: theme.palette.secondaryContainer,
|
||||||
|
borderBottomLeftRadius: theme.shape.borderRadiusLarge,
|
||||||
|
borderBottomRightRadius: theme.shape.borderRadiusLarge,
|
||||||
|
borderBottom: `4px solid ${theme.palette.primary.light}`,
|
||||||
|
},
|
||||||
|
accordionDetailsDisabled: {
|
||||||
|
borderBottom: `4px solid ${theme.palette.dividerAlternative}`,
|
||||||
|
},
|
||||||
accordionBody: {
|
accordionBody: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
paddingBottom: '1rem',
|
paddingBottom: theme.spacing(2),
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
paddingTop: '1.5rem',
|
// paddingTop: '1.5rem',
|
||||||
},
|
},
|
||||||
headerTitle: {
|
headerTitle: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -46,14 +56,6 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
marginBottom: '0.5rem',
|
marginBottom: '0.5rem',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
disabledIndicatorPos: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: '15px',
|
|
||||||
left: '20px',
|
|
||||||
[theme.breakpoints.down(560)]: {
|
|
||||||
top: '13px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
iconContainer: {
|
iconContainer: {
|
||||||
backgroundColor: theme.palette.primary.light,
|
backgroundColor: theme.palette.primary.light,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
@ -69,32 +71,14 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
width: '17px',
|
width: '17px',
|
||||||
height: '17px',
|
height: '17px',
|
||||||
},
|
},
|
||||||
resultInfo: {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
margin: '1rem 0',
|
|
||||||
},
|
|
||||||
leftWing: {
|
|
||||||
height: '2px',
|
|
||||||
backgroundColor: theme.palette.grey[300],
|
|
||||||
width: '90%',
|
|
||||||
},
|
|
||||||
separatorText: {
|
|
||||||
fontSize: theme.fontSizes.smallBody,
|
|
||||||
textAlign: 'center',
|
|
||||||
padding: '0 1rem',
|
|
||||||
},
|
|
||||||
rightWing: {
|
|
||||||
height: '2px',
|
|
||||||
backgroundColor: theme.palette.grey[300],
|
|
||||||
width: '90%',
|
|
||||||
},
|
|
||||||
linkContainer: {
|
linkContainer: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
marginBottom: '1rem',
|
marginBottom: '1rem',
|
||||||
},
|
},
|
||||||
truncator: {
|
truncator: {
|
||||||
|
fontSize: theme.fontSizes.bodySize,
|
||||||
|
fontWeight: theme.typography.fontWeightMedium,
|
||||||
[theme.breakpoints.down(560)]: {
|
[theme.breakpoints.down(560)]: {
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
import { Accordion, AccordionDetails, AccordionSummary } from '@mui/material';
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
|
Box,
|
||||||
|
Chip,
|
||||||
|
useTheme,
|
||||||
|
} from '@mui/material';
|
||||||
|
import classNames from 'classnames';
|
||||||
import { ExpandMore } from '@mui/icons-material';
|
import { ExpandMore } from '@mui/icons-material';
|
||||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||||
import useFeatureMetrics from 'hooks/api/getters/useFeatureMetrics/useFeatureMetrics';
|
import useFeatureMetrics from 'hooks/api/getters/useFeatureMetrics/useFeatureMetrics';
|
||||||
@ -8,14 +16,14 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
|
|||||||
import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon';
|
import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon';
|
||||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||||
import { useStyles } from './FeatureOverviewEnvironment.styles';
|
import { useStyles } from './FeatureOverviewEnvironment.styles';
|
||||||
import FeatureOverviewEnvironmentBody from './FeatureOverviewEnvironmentBody/FeatureOverviewEnvironmentBody';
|
import EnvironmentAccordionBody from './EnvironmentAccordionBody/EnvironmentAccordionBody';
|
||||||
import FeatureOverviewEnvironmentFooter from './FeatureOverviewEnvironmentFooter/FeatureOverviewEnvironmentFooter';
|
import { EnvironmentFooter } from './EnvironmentFooter/EnvironmentFooter';
|
||||||
import FeatureOverviewEnvironmentMetrics from './FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics';
|
import FeatureOverviewEnvironmentMetrics from './FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics';
|
||||||
import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu';
|
import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu';
|
||||||
import { FEATURE_ENVIRONMENT_ACCORDION } from 'utils/testIds';
|
import { FEATURE_ENVIRONMENT_ACCORDION } from 'utils/testIds';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { FeatureStrategyIcons } from 'component/feature/FeatureStrategy/FeatureStrategyIcons/FeatureStrategyIcons';
|
import { FeatureStrategyIcons } from 'component/feature/FeatureStrategy/FeatureStrategyIcons/FeatureStrategyIcons';
|
||||||
import { Badge } from 'component/common/Badge/Badge';
|
// import { Badge } from 'component/common/Badge/Badge';
|
||||||
|
|
||||||
interface IFeatureOverviewEnvironmentProps {
|
interface IFeatureOverviewEnvironmentProps {
|
||||||
env: IFeatureEnvironment;
|
env: IFeatureEnvironment;
|
||||||
@ -25,6 +33,7 @@ const FeatureOverviewEnvironment = ({
|
|||||||
env,
|
env,
|
||||||
}: IFeatureOverviewEnvironmentProps) => {
|
}: IFeatureOverviewEnvironmentProps) => {
|
||||||
const { classes: styles } = useStyles();
|
const { classes: styles } = useStyles();
|
||||||
|
const theme = useTheme();
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const featureId = useRequiredPathParam('featureId');
|
const featureId = useRequiredPathParam('featureId');
|
||||||
const { metrics } = useFeatureMetrics(projectId, featureId);
|
const { metrics } = useFeatureMetrics(projectId, featureId);
|
||||||
@ -38,38 +47,57 @@ const FeatureOverviewEnvironment = ({
|
|||||||
featureEnvironment => featureEnvironment.name === env.name
|
featureEnvironment => featureEnvironment.name === env.name
|
||||||
);
|
);
|
||||||
|
|
||||||
const getOverviewText = () => {
|
|
||||||
if (env.enabled) {
|
|
||||||
return `${environmentMetric?.yes} received this feature because the following strategies are executing`;
|
|
||||||
}
|
|
||||||
return `This environment is disabled, which means that none of your strategies are executing`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.featureOverviewEnvironment}>
|
<div
|
||||||
|
className={styles.featureOverviewEnvironment}
|
||||||
|
style={{
|
||||||
|
background: !env.enabled
|
||||||
|
? theme.palette.neutral.light
|
||||||
|
: theme.palette.background.default,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Accordion
|
<Accordion
|
||||||
style={{ boxShadow: 'none' }}
|
className={styles.accordion}
|
||||||
data-testid={`${FEATURE_ENVIRONMENT_ACCORDION}_${env.name}`}
|
data-testid={`${FEATURE_ENVIRONMENT_ACCORDION}_${env.name}`}
|
||||||
>
|
>
|
||||||
<AccordionSummary
|
<AccordionSummary
|
||||||
className={styles.accordionHeader}
|
className={styles.accordionHeader}
|
||||||
expandIcon={<ExpandMore titleAccess="Toggle" />}
|
expandIcon={<ExpandMore titleAccess="Toggle" />}
|
||||||
>
|
>
|
||||||
<div className={styles.header} data-loading>
|
<div
|
||||||
|
className={styles.header}
|
||||||
|
data-loading
|
||||||
|
style={{
|
||||||
|
color: !env.enabled
|
||||||
|
? theme.palette.text.disabled
|
||||||
|
: theme.palette.text.primary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className={styles.headerTitle}>
|
<div className={styles.headerTitle}>
|
||||||
<EnvironmentIcon
|
<EnvironmentIcon
|
||||||
enabled={env.enabled}
|
enabled={env.enabled}
|
||||||
className={styles.headerIcon}
|
className={styles.headerIcon}
|
||||||
/>
|
/>
|
||||||
<p>
|
<div>
|
||||||
Feature toggle execution for
|
|
||||||
<StringTruncator
|
<StringTruncator
|
||||||
text={env.name}
|
text={env.name}
|
||||||
className={styles.truncator}
|
className={styles.truncator}
|
||||||
maxWidth="100"
|
maxWidth="100"
|
||||||
maxLength={15}
|
maxLength={15}
|
||||||
/>
|
/>
|
||||||
</p>
|
<ConditionallyRender
|
||||||
|
condition={!env.enabled}
|
||||||
|
show={
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
// severity="disabled"
|
||||||
|
label="Disabled"
|
||||||
|
sx={{ ml: 1 }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<FeatureStrategyMenu
|
<FeatureStrategyMenu
|
||||||
@ -83,17 +111,6 @@ const FeatureOverviewEnvironment = ({
|
|||||||
strategies={featureEnvironment?.strategies}
|
strategies={featureEnvironment?.strategies}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ConditionallyRender
|
|
||||||
condition={!env.enabled}
|
|
||||||
show={
|
|
||||||
<Badge
|
|
||||||
color="warning"
|
|
||||||
className={styles.disabledIndicatorPos}
|
|
||||||
>
|
|
||||||
Disabled
|
|
||||||
</Badge>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FeatureOverviewEnvironmentMetrics
|
<FeatureOverviewEnvironmentMetrics
|
||||||
@ -101,26 +118,41 @@ const FeatureOverviewEnvironment = ({
|
|||||||
/>
|
/>
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
|
|
||||||
<AccordionDetails>
|
<AccordionDetails
|
||||||
<div className={styles.accordionContainer}>
|
className={classNames(styles.accordionDetails, {
|
||||||
<FeatureOverviewEnvironmentBody
|
[styles.accordionDetailsDisabled]: !env.enabled,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<EnvironmentAccordionBody
|
||||||
featureEnvironment={featureEnvironment}
|
featureEnvironment={featureEnvironment}
|
||||||
getOverviewText={getOverviewText}
|
isDisabled={!env.enabled}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={
|
condition={
|
||||||
// @ts-expect-error
|
(featureEnvironment?.strategies?.length || 0) > 0
|
||||||
featureEnvironment?.strategies?.length > 0
|
|
||||||
}
|
}
|
||||||
show={
|
show={
|
||||||
<FeatureOverviewEnvironmentFooter
|
<>
|
||||||
// @ts-expect-error
|
<Box
|
||||||
env={env}
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
py: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FeatureStrategyMenu
|
||||||
|
label="Add strategy"
|
||||||
|
projectId={projectId}
|
||||||
|
featureId={featureId}
|
||||||
|
environmentId={env.name}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<EnvironmentFooter
|
||||||
environmentMetric={environmentMetric}
|
environmentMetric={environmentMetric}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import FeatureOverviewEnvironmentStrategies from '../FeatureOverviewEnvironmentStrategies/FeatureOverviewEnvironmentStrategies';
|
|
||||||
import { useStyles } from '../FeatureOverviewEnvironment.styles';
|
|
||||||
import { IFeatureEnvironment } from 'interfaces/featureToggle';
|
|
||||||
import { FeatureStrategyEmpty } from 'component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty';
|
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
|
||||||
|
|
||||||
interface IFeatureOverviewEnvironmentBodyProps {
|
|
||||||
getOverviewText: () => string;
|
|
||||||
featureEnvironment?: IFeatureEnvironment;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FeatureOverviewEnvironmentBody = ({
|
|
||||||
featureEnvironment,
|
|
||||||
getOverviewText,
|
|
||||||
}: IFeatureOverviewEnvironmentBodyProps) => {
|
|
||||||
const projectId = useRequiredPathParam('projectId');
|
|
||||||
const featureId = useRequiredPathParam('featureId');
|
|
||||||
const { classes: styles } = useStyles();
|
|
||||||
|
|
||||||
if (!featureEnvironment) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.accordionBody}>
|
|
||||||
<div className={styles.accordionBodyInnerContainer}>
|
|
||||||
<div className={styles.resultInfo}>
|
|
||||||
<div className={styles.leftWing} />
|
|
||||||
<div className={styles.separatorText}>
|
|
||||||
{getOverviewText()}
|
|
||||||
</div>
|
|
||||||
<div className={styles.rightWing} />
|
|
||||||
</div>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={featureEnvironment?.strategies.length > 0}
|
|
||||||
show={
|
|
||||||
<>
|
|
||||||
<FeatureOverviewEnvironmentStrategies
|
|
||||||
strategies={featureEnvironment?.strategies}
|
|
||||||
environmentName={featureEnvironment.name}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
elseShow={
|
|
||||||
<FeatureStrategyEmpty
|
|
||||||
projectId={projectId}
|
|
||||||
featureId={featureId}
|
|
||||||
environmentId={featureEnvironment.name}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default FeatureOverviewEnvironmentBody;
|
|
@ -20,7 +20,7 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
infoParagraph: {
|
infoParagraph: {
|
||||||
maxWidth: '215px',
|
maxWidth: '270px',
|
||||||
marginTop: '0.25rem',
|
marginTop: '0.25rem',
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
[theme.breakpoints.down(700)]: {
|
[theme.breakpoints.down(700)]: {
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
import { IFeatureStrategy } from 'interfaces/strategy';
|
|
||||||
import FeatureOverviewEnvironmentStrategy from './FeatureOverviewEnvironmentStrategy/FeatureOverviewEnvironmentStrategy';
|
|
||||||
|
|
||||||
interface IFeatureOverviewEnvironmentStrategiesProps {
|
|
||||||
strategies: IFeatureStrategy[];
|
|
||||||
environmentName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FeatureOverviewEnvironmentStrategies = ({
|
|
||||||
strategies,
|
|
||||||
environmentName,
|
|
||||||
}: IFeatureOverviewEnvironmentStrategiesProps) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{strategies.map(strategy => (
|
|
||||||
<FeatureOverviewEnvironmentStrategy
|
|
||||||
key={strategy.id}
|
|
||||||
strategy={strategy}
|
|
||||||
environmentId={environmentName}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FeatureOverviewEnvironmentStrategies;
|
|
@ -0,0 +1,35 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import { styled } from '@mui/material';
|
||||||
|
|
||||||
|
const SeparatorContainer = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
margin: '1rem 0',
|
||||||
|
position: 'relative',
|
||||||
|
'&:before': {
|
||||||
|
content: '""',
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
height: 2,
|
||||||
|
width: '100%',
|
||||||
|
backgroundColor: theme.palette.divider,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const SeparatorContent = styled('span')(({ theme }) => ({
|
||||||
|
fontSize: theme.fontSizes.subHeader,
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '0 1rem',
|
||||||
|
background: theme.palette.secondaryContainer,
|
||||||
|
position: 'relative',
|
||||||
|
maxWidth: '80%',
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const SectionSeparator: FC = ({ children }) => (
|
||||||
|
<SeparatorContainer>
|
||||||
|
<SeparatorContent>{children}</SeparatorContent>
|
||||||
|
</SeparatorContainer>
|
||||||
|
);
|
@ -11,13 +11,13 @@ const FeatureOverviewEnvironments = () => {
|
|||||||
|
|
||||||
const { environments } = feature;
|
const { environments } = feature;
|
||||||
|
|
||||||
const renderEnvironments = () => {
|
return (
|
||||||
return environments?.map(env => {
|
<>
|
||||||
return <FeatureOverviewEnvironment env={env} key={env.name} />;
|
{environments?.map(env => (
|
||||||
});
|
<FeatureOverviewEnvironment env={env} key={env.name} />
|
||||||
};
|
))}
|
||||||
|
</>
|
||||||
return <>{renderEnvironments()}</>;
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FeatureOverviewEnvironments;
|
export default FeatureOverviewEnvironments;
|
||||||
|
@ -2,7 +2,7 @@ import { makeStyles } from 'tss-react/mui';
|
|||||||
|
|
||||||
export const useStyles = makeStyles()(theme => ({
|
export const useStyles = makeStyles()(theme => ({
|
||||||
eventEntry: {
|
eventEntry: {
|
||||||
border: `1px solid ${theme.palette.grey[100]}`,
|
border: `1px solid ${theme.palette.neutral.light}`,
|
||||||
padding: '1rem',
|
padding: '1rem',
|
||||||
margin: '1rem 0',
|
margin: '1rem 0',
|
||||||
borderRadius: theme.shape.borderRadius,
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
@ -14,7 +14,7 @@ interface IPlaygroundEditorProps {
|
|||||||
|
|
||||||
const StyledEditorHeader = styled('aside')(({ theme }) => ({
|
const StyledEditorHeader = styled('aside')(({ theme }) => ({
|
||||||
height: '50px',
|
height: '50px',
|
||||||
backgroundColor: theme.palette.grey[100],
|
backgroundColor: theme.palette.neutral.light,
|
||||||
borderTopRightRadius: theme.shape.borderRadiusMedium,
|
borderTopRightRadius: theme.shape.borderRadiusMedium,
|
||||||
borderTopLeftRadius: theme.shape.borderRadiusMedium,
|
borderTopLeftRadius: theme.shape.borderRadiusMedium,
|
||||||
padding: theme.spacing(1, 2),
|
padding: theme.spacing(1, 2),
|
||||||
|
@ -16,7 +16,7 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
},
|
},
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
transition: 'background-color 0.2s ease-in-out',
|
transition: 'background-color 0.2s ease-in-out',
|
||||||
backgroundColor: theme.palette.grey[100],
|
backgroundColor: theme.palette.neutral.light,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
|
@ -10,7 +10,7 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
},
|
},
|
||||||
headerContainer: { display: 'flex', padding: '0.5rem' },
|
headerContainer: { display: 'flex', padding: '0.5rem' },
|
||||||
divider: {
|
divider: {
|
||||||
backgroundColor: theme.palette.grey[100],
|
backgroundColor: theme.palette.neutral.light,
|
||||||
height: '1px',
|
height: '1px',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
import { IFeatureStrategyPayload, IFeatureStrategy } from 'interfaces/strategy';
|
import {
|
||||||
|
IFeatureStrategyPayload,
|
||||||
|
IFeatureStrategy,
|
||||||
|
IFeatureStrategySortOrder,
|
||||||
|
} from 'interfaces/strategy';
|
||||||
import useAPI from '../useApi/useApi';
|
import useAPI from '../useApi/useApi';
|
||||||
|
|
||||||
const useFeatureStrategyApi = () => {
|
const useFeatureStrategyApi = () => {
|
||||||
@ -52,10 +56,26 @@ const useFeatureStrategyApi = () => {
|
|||||||
await makeRequest(req.caller, req.id);
|
await makeRequest(req.caller, req.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setStrategiesSortOrder = async (
|
||||||
|
projectId: string,
|
||||||
|
featureId: string,
|
||||||
|
environmentId: string,
|
||||||
|
payload: IFeatureStrategySortOrder[]
|
||||||
|
): Promise<void> => {
|
||||||
|
const path = `api/admin/projects/${projectId}/features/${featureId}/environments/${environmentId}/strategies/set-sort-order`;
|
||||||
|
const req = createRequest(
|
||||||
|
path,
|
||||||
|
{ method: 'POST', body: JSON.stringify(payload) },
|
||||||
|
'setStrategiesSortOrderOnFeature'
|
||||||
|
);
|
||||||
|
await makeRequest(req.caller, req.id);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
addStrategyToFeature,
|
addStrategyToFeature,
|
||||||
updateStrategyOnFeature,
|
updateStrategyOnFeature,
|
||||||
deleteStrategyFromFeature,
|
deleteStrategyFromFeature,
|
||||||
|
setStrategiesSortOrder,
|
||||||
loading,
|
loading,
|
||||||
errors,
|
errors,
|
||||||
};
|
};
|
||||||
|
@ -51,3 +51,8 @@ export interface IConstraint {
|
|||||||
operator: Operator;
|
operator: Operator;
|
||||||
contextName: string;
|
contextName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IFeatureStrategySortOrder {
|
||||||
|
id: string;
|
||||||
|
sortOrder: number;
|
||||||
|
}
|
||||||
|
@ -26,6 +26,9 @@ export default createTheme({
|
|||||||
fontSize: '1.5rem',
|
fontSize: '1.5rem',
|
||||||
lineHeight: 1.875,
|
lineHeight: 1.875,
|
||||||
},
|
},
|
||||||
|
caption: {
|
||||||
|
fontSize: `${12 / 16}rem`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
fontSizes: {
|
fontSizes: {
|
||||||
mainHeader: '1.25rem',
|
mainHeader: '1.25rem',
|
||||||
@ -287,5 +290,28 @@ export default createTheme({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
MuiChip: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: ({ ownerState, theme }) => ({
|
||||||
|
...(ownerState.variant === 'outlined' &&
|
||||||
|
ownerState.size === 'small' && {
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
margin: 0,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderStyle: 'solid',
|
||||||
|
fontWeight: theme.typography.fontWeightBold,
|
||||||
|
fontSize: theme.typography.caption.fontSize,
|
||||||
|
...(ownerState.color === 'success' && {
|
||||||
|
backgroundColor: colors.green[50],
|
||||||
|
borderColor: theme.palette.success.border,
|
||||||
|
color: theme.palette.success.dark,
|
||||||
|
}),
|
||||||
|
...(ownerState.color === 'default' && {
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -2,6 +2,7 @@ import LocationOnIcon from '@mui/icons-material/LocationOn';
|
|||||||
import PeopleIcon from '@mui/icons-material/People';
|
import PeopleIcon from '@mui/icons-material/People';
|
||||||
import LanguageIcon from '@mui/icons-material/Language';
|
import LanguageIcon from '@mui/icons-material/Language';
|
||||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||||
|
import CodeIcon from '@mui/icons-material/Code';
|
||||||
import { ReactComponent as RolloutIcon } from 'assets/icons/rollout.svg';
|
import { ReactComponent as RolloutIcon } from 'assets/icons/rollout.svg';
|
||||||
import { ElementType } from 'react';
|
import { ElementType } from 'react';
|
||||||
|
|
||||||
@ -11,6 +12,8 @@ export const formatStrategyName = (strategyName: string): string => {
|
|||||||
|
|
||||||
export const getFeatureStrategyIcon = (strategyName: string): ElementType => {
|
export const getFeatureStrategyIcon = (strategyName: string): ElementType => {
|
||||||
switch (strategyName) {
|
switch (strategyName) {
|
||||||
|
case 'default':
|
||||||
|
return PowerSettingsNewIcon;
|
||||||
case 'remoteAddress':
|
case 'remoteAddress':
|
||||||
return LanguageIcon;
|
return LanguageIcon;
|
||||||
case 'flexibleRollout':
|
case 'flexibleRollout':
|
||||||
@ -20,7 +23,7 @@ export const getFeatureStrategyIcon = (strategyName: string): ElementType => {
|
|||||||
case 'applicationHostname':
|
case 'applicationHostname':
|
||||||
return LocationOnIcon;
|
return LocationOnIcon;
|
||||||
default:
|
default:
|
||||||
return PowerSettingsNewIcon;
|
return CodeIcon;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user