mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-10 01:16:39 +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: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
alignItems: 'stretch',
|
||||
[theme.breakpoints.down(710)]: { flexDirection: 'column' },
|
||||
},
|
||||
headerContainer: {
|
||||
@ -48,12 +48,11 @@ export const useStyles = makeStyles()(theme => ({
|
||||
},
|
||||
headerValuesContainerWrapper: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'stretch',
|
||||
},
|
||||
headerValuesContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'stretch',
|
||||
},
|
||||
headerValues: {
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
@ -106,7 +105,10 @@ export const useStyles = makeStyles()(theme => ({
|
||||
},
|
||||
display: 'inline-flex',
|
||||
},
|
||||
headerSelect: { marginRight: '1rem', width: '200px' },
|
||||
headerSelect: {
|
||||
marginRight: '1rem',
|
||||
width: '200px',
|
||||
},
|
||||
chip: {
|
||||
margin: '0 0.5rem 0.5rem 0',
|
||||
},
|
||||
@ -121,7 +123,7 @@ export const useStyles = makeStyles()(theme => ({
|
||||
},
|
||||
},
|
||||
accordionDetails: {
|
||||
borderTop: `1px solid ${theme.palette.grey[300]}`,
|
||||
borderTop: `1px dashed ${theme.palette.grey[300]}`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
@ -132,33 +134,16 @@ export const useStyles = makeStyles()(theme => ({
|
||||
},
|
||||
summary: {
|
||||
border: 'none',
|
||||
padding: '0.25rem 1rem',
|
||||
padding: theme.spacing(0.5, 3),
|
||||
'&:hover .valuesExpandLabel': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
},
|
||||
settingsParagraph: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0.5rem 0',
|
||||
},
|
||||
settingsIcon: {
|
||||
height: '32.5px',
|
||||
width: '32.5px',
|
||||
marginRight: '0.5rem',
|
||||
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%' },
|
||||
}));
|
||||
|
@ -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 CaseSensitiveOff } from 'assets/icons/24_Text format off.svg';
|
||||
import React from 'react';
|
||||
@ -17,30 +17,35 @@ interface CaseSensitiveButtonProps {
|
||||
export const CaseSensitiveButton = ({
|
||||
localConstraint,
|
||||
setCaseInsensitive,
|
||||
}: CaseSensitiveButtonProps) => {
|
||||
return (
|
||||
<ConditionallyRender
|
||||
condition={Boolean(localConstraint.caseInsensitive)}
|
||||
show={
|
||||
<Tooltip title="Make it case sensitive" arrow>
|
||||
}: CaseSensitiveButtonProps) => (
|
||||
<Tooltip
|
||||
title={
|
||||
Boolean(localConstraint.caseInsensitive)
|
||||
? 'Make it case sensitive'
|
||||
: 'Make it case insensitive'
|
||||
}
|
||||
arrow
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'stretch' }}>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(localConstraint.caseInsensitive)}
|
||||
show={
|
||||
<StyledToggleButtonOff
|
||||
onClick={setCaseInsensitive}
|
||||
disableRipple
|
||||
>
|
||||
<CaseSensitiveOff />
|
||||
</StyledToggleButtonOff>
|
||||
</Tooltip>
|
||||
}
|
||||
elseShow={
|
||||
<Tooltip title="Make it case insensitive" arrow>
|
||||
}
|
||||
elseShow={
|
||||
<StyledToggleButtonOn
|
||||
onClick={setCaseInsensitive}
|
||||
disableRipple
|
||||
>
|
||||
<CaseSensitive />
|
||||
</StyledToggleButtonOn>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { Tooltip } from '@mui/material';
|
||||
import { ReactComponent as NegatedIcon } from '../../../../../../assets/icons/24_Negator.svg';
|
||||
import { ReactComponent as NegatedIconOff } from '../../../../../../assets/icons/24_Negator off.svg';
|
||||
import React from 'react';
|
||||
import { IConstraint } from '../../../../../../interfaces/strategy';
|
||||
import { Box, Tooltip } from '@mui/material';
|
||||
import { ReactComponent as NegatedIcon } from 'assets/icons/24_Negator.svg';
|
||||
import { ReactComponent as NegatedIconOff } from 'assets/icons/24_Negator off.svg';
|
||||
import { IConstraint } from 'interfaces/strategy';
|
||||
import {
|
||||
StyledToggleButtonOff,
|
||||
StyledToggleButtonOn,
|
||||
@ -17,30 +16,35 @@ interface InvertedOperatorButtonProps {
|
||||
export const InvertedOperatorButton = ({
|
||||
localConstraint,
|
||||
setInvertedOperator,
|
||||
}: InvertedOperatorButtonProps) => {
|
||||
return (
|
||||
<ConditionallyRender
|
||||
condition={Boolean(localConstraint.inverted)}
|
||||
show={
|
||||
<Tooltip title="Remove negation" arrow>
|
||||
}: InvertedOperatorButtonProps) => (
|
||||
<Tooltip
|
||||
title={
|
||||
Boolean(localConstraint.inverted)
|
||||
? 'Remove negation'
|
||||
: 'Negate operator'
|
||||
}
|
||||
arrow
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'stretch' }}>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(localConstraint.inverted)}
|
||||
show={
|
||||
<StyledToggleButtonOn
|
||||
onClick={setInvertedOperator}
|
||||
disableRipple
|
||||
>
|
||||
<NegatedIcon />
|
||||
</StyledToggleButtonOn>
|
||||
</Tooltip>
|
||||
}
|
||||
elseShow={
|
||||
<Tooltip title="Negate operator" arrow>
|
||||
}
|
||||
elseShow={
|
||||
<StyledToggleButtonOff
|
||||
onClick={setInvertedOperator}
|
||||
disableRipple
|
||||
>
|
||||
<NegatedIconOff />
|
||||
</StyledToggleButtonOff>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
|
@ -3,8 +3,8 @@ import { makeStyles } from 'tss-react/mui';
|
||||
export const useStyles = makeStyles()(theme => ({
|
||||
container: {
|
||||
width: '100%',
|
||||
display: 'grid',
|
||||
gap: '1rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
help: {
|
||||
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 React, { forwardRef, useImperativeHandle } from 'react';
|
||||
import { ConstraintAccordion } from 'component/common/ConstraintAccordion/ConstraintAccordion';
|
||||
import produce from 'immer';
|
||||
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
|
||||
@ -8,8 +10,7 @@ import { objectId } from 'utils/objectId';
|
||||
import { useStyles } from './ConstraintAccordionList.styles';
|
||||
import { createEmptyConstraint } from 'component/common/ConstraintAccordion/ConstraintAccordionList/createEmptyConstraint';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { Button, Tooltip } from '@mui/material';
|
||||
import { Help } from '@mui/icons-material';
|
||||
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
|
||||
|
||||
interface IConstraintAccordionListProps {
|
||||
constraints: IConstraint[];
|
||||
@ -129,16 +130,22 @@ export const ConstraintAccordionList = forwardRef<
|
||||
}
|
||||
/>
|
||||
{constraints.map((constraint, index) => (
|
||||
<ConstraintAccordion
|
||||
key={objectId(constraint)}
|
||||
constraint={constraint}
|
||||
onEdit={onEdit && onEdit.bind(null, constraint)}
|
||||
onCancel={onCancel.bind(null, index)}
|
||||
onDelete={onRemove && onRemove.bind(null, index)}
|
||||
onSave={onSave && onSave.bind(null, index)}
|
||||
editing={Boolean(state.get(constraint)?.editing)}
|
||||
compact
|
||||
/>
|
||||
<Fragment key={`${constraint.contextName}-${index}`}>
|
||||
<ConditionallyRender
|
||||
condition={index > 0}
|
||||
show={<StrategySeparator text="AND" />}
|
||||
/>
|
||||
<ConstraintAccordion
|
||||
key={objectId(constraint)}
|
||||
constraint={constraint}
|
||||
onEdit={onEdit && onEdit.bind(null, constraint)}
|
||||
onCancel={onCancel.bind(null, index)}
|
||||
onDelete={onRemove && onRemove.bind(null, index)}
|
||||
onSave={onSave && onSave.bind(null, index)}
|
||||
editing={Boolean(state.get(constraint)?.editing)}
|
||||
compact
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { Accordion, AccordionSummary, AccordionDetails } from '@mui/material';
|
||||
import { IConstraint } from 'interfaces/strategy';
|
||||
|
||||
import { ConstraintAccordionViewBody } from './ConstraintAccordionViewBody/ConstraintAccordionViewBody';
|
||||
import { ConstraintAccordionViewHeader } from './ConstraintAccordionViewHeader/ConstraintAccordionViewHeader';
|
||||
import { oneOf } from 'utils/oneOf';
|
||||
@ -9,9 +9,8 @@ import {
|
||||
numOperators,
|
||||
semVerOperators,
|
||||
} from 'constants/operators';
|
||||
|
||||
import { useStyles } from '../ConstraintAccordion.styles';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface IConstraintAccordionViewProps {
|
||||
constraint: IConstraint;
|
||||
onDelete?: () => void;
|
||||
@ -46,7 +45,7 @@ export const ConstraintAccordionView = ({
|
||||
sx={{ cursor: expandable ? 'pointer' : 'default' }}
|
||||
>
|
||||
<AccordionSummary
|
||||
className={styles.summary}
|
||||
classes={{ root: styles.summary }}
|
||||
expandIcon={null}
|
||||
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 StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||
import { useState } from 'react';
|
||||
import { stringOperators } from 'constants/operators';
|
||||
import { IConstraint } from 'interfaces/strategy';
|
||||
import { oneOf } from 'utils/oneOf';
|
||||
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 { 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 {
|
||||
constraint: IConstraint;
|
||||
@ -18,7 +17,8 @@ interface IConstraintAccordionViewBodyProps {
|
||||
export const ConstraintAccordionViewBody = ({
|
||||
constraint,
|
||||
}: IConstraintAccordionViewBodyProps) => {
|
||||
const { classes: styles } = useStyles();
|
||||
const { classes: styles } = useAccordionStyles();
|
||||
const { classes } = useStyles();
|
||||
const { locationSettings } = useLocationSettings();
|
||||
|
||||
return (
|
||||
@ -29,7 +29,7 @@ export const ConstraintAccordionViewBody = ({
|
||||
Boolean(constraint.caseInsensitive)
|
||||
}
|
||||
show={
|
||||
<p className={styles.settingsParagraph}>
|
||||
<p className={classes.settingsParagraph}>
|
||||
<TextFormatOutlined className={styles.settingsIcon} />{' '}
|
||||
Case insensitive setting is active
|
||||
</p>
|
||||
@ -39,7 +39,7 @@ export const ConstraintAccordionViewBody = ({
|
||||
<ConditionallyRender
|
||||
condition={Boolean(constraint.inverted)}
|
||||
show={
|
||||
<p className={styles.settingsParagraph}>
|
||||
<p className={classes.settingsParagraph}>
|
||||
<ImportExportOutlined className={styles.settingsIcon} />{' '}
|
||||
Operator is negated
|
||||
</p>
|
||||
@ -56,70 +56,3 @@ export const ConstraintAccordionViewBody = ({
|
||||
</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 { IConstraint } from 'interfaces/strategy';
|
||||
|
||||
import { useStyles } from 'component/common/ConstraintAccordion/ConstraintAccordion.styles';
|
||||
import React from 'react';
|
||||
import { ConstraintAccordionViewHeaderInfo } from './ConstraintAccordionViewHeaderInfo/ConstraintAccordionViewHeaderInfo';
|
||||
import { ConstraintAccordionHeaderActions } from '../../ConstraintAccordionHeaderActions/ConstraintAccordionHeaderActions';
|
||||
import { useStyles } from 'component/common/ConstraintAccordion/ConstraintAccordion.styles';
|
||||
|
||||
interface IConstraintAccordionViewHeaderProps {
|
||||
constraint: IConstraint;
|
||||
|
@ -15,6 +15,8 @@ const StyledHeaderText = styled('span')(({ theme }) => ({
|
||||
maxWidth: '100px',
|
||||
minWidth: '100px',
|
||||
marginRight: '10px',
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
wordBreak: 'break-word',
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
[theme.breakpoints.down(710)]: {
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { IConstraint } from '../../../../../../interfaces/strategy';
|
||||
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 { ConstraintOperator } from '../../../ConstraintOperator/ConstraintOperator';
|
||||
import React from 'react';
|
||||
import { useStyles } from '../../../ConstraintAccordion.styles';
|
||||
import { StyledIconWrapper } from '../StyledIconWrapper/StyledIconWrapper';
|
||||
|
||||
@ -22,14 +21,19 @@ export const ConstraintViewHeaderOperator = ({
|
||||
condition={Boolean(constraint.inverted)}
|
||||
show={
|
||||
<Tooltip title={'Operator is negated'} arrow>
|
||||
<StyledIconWrapper marginright={'0'}>
|
||||
<NegatedIcon />
|
||||
</StyledIconWrapper>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<StyledIconWrapper isPrefix>
|
||||
<NegatedIcon />
|
||||
</StyledIconWrapper>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<div className={styles.headerConstraintContainer}>
|
||||
<ConstraintOperator constraint={constraint} />
|
||||
<ConstraintOperator
|
||||
constraint={constraint}
|
||||
hasPrefix={Boolean(constraint.inverted)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -17,6 +17,7 @@ const StyledValuesSpan = styled('span')(({ theme }) => ({
|
||||
overflow: 'hidden',
|
||||
wordBreak: 'break-word',
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
margin: 'auto 0',
|
||||
[theme.breakpoints.down(710)]: {
|
||||
margin: theme.spacing(1, 0),
|
||||
textAlign: 'center',
|
||||
@ -90,8 +91,7 @@ export const ConstraintAccordionViewHeaderMultipleValues = ({
|
||||
)}
|
||||
>
|
||||
{!expanded
|
||||
? `View all (
|
||||
${constraint?.values?.length})`
|
||||
? `View all (${constraint?.values?.length})`
|
||||
: 'View less'}
|
||||
</p>
|
||||
}
|
||||
|
@ -4,13 +4,13 @@ import { stringOperators } from '../../../../../../constants/operators';
|
||||
import { Chip, styled, Tooltip } from '@mui/material';
|
||||
import { ReactComponent as CaseSensitive } from '../../../../../../assets/icons/24_Text format.svg';
|
||||
import { formatConstraintValue } from '../../../../../../utils/formatConstraintValue';
|
||||
import React from 'react';
|
||||
import { useStyles } from '../../../ConstraintAccordion.styles';
|
||||
import { StyledIconWrapper } from '../StyledIconWrapper/StyledIconWrapper';
|
||||
import { IConstraint } from '../../../../../../interfaces/strategy';
|
||||
import { useLocationSettings } from '../../../../../../hooks/useLocationSettings';
|
||||
|
||||
const StyledSingleValueChip = styled(Chip)(({ theme }) => ({
|
||||
margin: 'auto 0',
|
||||
[theme.breakpoints.down(710)]: {
|
||||
margin: theme.spacing(1, 0),
|
||||
},
|
||||
|
@ -1,16 +1,34 @@
|
||||
import { styled } from '@mui/material';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { FC, forwardRef } from 'react';
|
||||
|
||||
export const StyledIconWrapper = styled('div')<{
|
||||
marginright?: string;
|
||||
}>(({ theme, marginright }) => ({
|
||||
export const StyledIconWrapperBase = styled('div')<{
|
||||
prefix?: boolean;
|
||||
}>(({ theme }) => ({
|
||||
backgroundColor: theme.palette.grey[200],
|
||||
width: 28,
|
||||
height: 48,
|
||||
display: 'inline-flex',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '10px 0',
|
||||
alignSelf: 'stretch',
|
||||
color: theme.palette.primary.main,
|
||||
marginRight: marginright ? marginright : '1rem',
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
marginRight: '1rem',
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
}));
|
||||
|
||||
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 => ({
|
||||
container: {
|
||||
padding: '0.5rem 0.75rem',
|
||||
padding: theme.spacing(0.5, 1.5),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: theme.palette.grey[200],
|
||||
lineHeight: 1.25,
|
||||
|
@ -5,10 +5,12 @@ import React from 'react';
|
||||
|
||||
interface IConstraintOperatorProps {
|
||||
constraint: IConstraint;
|
||||
hasPrefix?: boolean;
|
||||
}
|
||||
|
||||
export const ConstraintOperator = ({
|
||||
constraint,
|
||||
hasPrefix,
|
||||
}: IConstraintOperatorProps) => {
|
||||
const { classes: styles } = useStyles();
|
||||
|
||||
@ -16,7 +18,13 @@ export const ConstraintOperator = ({
|
||||
const operatorText = formatOperatorDescription(constraint.operator);
|
||||
|
||||
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.text}>{operatorText}</div>
|
||||
</div>
|
||||
|
@ -1,24 +1,45 @@
|
||||
import { useTheme } from '@mui/material';
|
||||
import { styled } from '@mui/material';
|
||||
import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
|
||||
|
||||
interface IStrategySeparatorProps {
|
||||
text: string;
|
||||
text: 'AND' | 'OR';
|
||||
}
|
||||
|
||||
export const StrategySeparator = ({ text }: IStrategySeparatorProps) => {
|
||||
const theme = useTheme();
|
||||
const StyledContainer = styled('div')(({ theme }) => ({
|
||||
height: theme.spacing(2),
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
}));
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
color: theme.palette.primary.main,
|
||||
padding: '0.1rem 0.25rem',
|
||||
border: `1px solid ${theme.palette.primary.main}`,
|
||||
borderRadius: '0.25rem',
|
||||
fontSize: theme.fontSizes.smallerBody,
|
||||
backgroundColor: '#fff',
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const StyledContent = styled('div')(({ theme }) => ({
|
||||
padding: theme.spacing(0.75, 1.5),
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: theme.fontSizes.smallerBody,
|
||||
backgroundColor: theme.palette.secondaryContainer,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
position: 'absolute',
|
||||
zIndex: theme.zIndex.fab,
|
||||
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 => ({
|
||||
emptyStateListItem: {
|
||||
border: `2px dashed ${theme.palette.grey[100]}`,
|
||||
border: `2px dashed ${theme.palette.neutral.light}`,
|
||||
padding: '0.8rem',
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
|
@ -1,14 +1,22 @@
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
export const useStyles = makeStyles()(theme => ({
|
||||
noItemsParagraph: {
|
||||
margin: '1rem 0',
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
link: {
|
||||
display: 'block',
|
||||
margin: '1rem 0 0 0',
|
||||
title: {
|
||||
fontSize: theme.fontSizes.bodySize,
|
||||
textAlign: 'center',
|
||||
color: theme.palette.text.primary,
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
envName: {
|
||||
fontWeight: 'bold',
|
||||
description: {
|
||||
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 StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||
import { useStyles } from './FeatureStrategyEmpty.styles';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Box } from '@mui/material';
|
||||
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 { PresetCard } from './PresetCard/PresetCard';
|
||||
import { useStyles } from './FeatureStrategyEmpty.styles';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
|
||||
interface IFeatureStrategyEmptyProps {
|
||||
projectId: string;
|
||||
@ -15,30 +21,58 @@ export const FeatureStrategyEmpty = ({
|
||||
environmentId,
|
||||
}: IFeatureStrategyEmptyProps) => {
|
||||
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 (
|
||||
<NoItems>
|
||||
<p className={styles.noItemsParagraph}>
|
||||
No strategies added in the{' '}
|
||||
<StringTruncator
|
||||
text={environmentId}
|
||||
maxWidth={'130'}
|
||||
maxLength={15}
|
||||
className={styles.envName}
|
||||
/>{' '}
|
||||
environment
|
||||
</p>
|
||||
<p className={styles.noItemsParagraph}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.title}>
|
||||
You have not defined any strategies yet.
|
||||
</div>
|
||||
<p className={styles.description}>
|
||||
Strategies added in this environment will only be executed if
|
||||
the SDK is using an API key configured for this environment.
|
||||
<a
|
||||
className={styles.link}
|
||||
href="https://docs.getunleash.io/user_guide/environments"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Read more here
|
||||
</a>
|
||||
the SDK is using an{' '}
|
||||
<Link to="/admin/api">API key configured</Link> for this
|
||||
environment.
|
||||
</p>
|
||||
<FeatureStrategyMenu
|
||||
label="Add your first strategy"
|
||||
@ -46,6 +80,31 @@ export const FeatureStrategyEmpty = ({
|
||||
featureId={featureId}
|
||||
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 (
|
||||
<StyledIcon>
|
||||
<Tooltip title={formatStrategyName(strategyName)} arrow>
|
||||
<Icon />
|
||||
<>
|
||||
<Icon />
|
||||
</>
|
||||
</Tooltip>
|
||||
</StyledIcon>
|
||||
);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import PermissionButton, {
|
||||
IPermissionButtonProps,
|
||||
} from 'component/common/PermissionButton/PermissionButton';
|
||||
import React, { useState } from 'react';
|
||||
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
|
||||
import { Popover } from '@mui/material';
|
||||
import { FeatureStrategyMenuCards } from './FeatureStrategyMenuCards/FeatureStrategyMenuCards';
|
||||
|
@ -3,7 +3,7 @@ import { makeStyles } from 'tss-react/mui';
|
||||
export const useStyles = makeStyles()(theme => ({
|
||||
item: {
|
||||
padding: theme.spacing(2),
|
||||
background: theme.palette.grey[100],
|
||||
background: theme.palette.secondaryContainer,
|
||||
borderRadius: theme.spacing(2),
|
||||
textAlign: 'center',
|
||||
[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';
|
||||
|
||||
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: {
|
||||
margin: '0.25rem',
|
||||
},
|
@ -1,17 +1,14 @@
|
||||
import { Chip } from '@mui/material';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { useStyles } from './FeatureOverviewExecutionChips.styles';
|
||||
import { useStyles } from './ConstraintItem.styles';
|
||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||
|
||||
interface IFeatureOverviewExecutionChipsProps {
|
||||
interface IConstraintItemProps {
|
||||
value: string[];
|
||||
text: string;
|
||||
}
|
||||
|
||||
const FeatureOverviewExecutionChips = ({
|
||||
value,
|
||||
text,
|
||||
}: IFeatureOverviewExecutionChipsProps) => {
|
||||
export const ConstraintItem = ({ value, text }: IConstraintItemProps) => {
|
||||
const { classes: styles } = useStyles();
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
@ -44,5 +41,3 @@ const FeatureOverviewExecutionChips = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeatureOverviewExecutionChips;
|
@ -9,4 +9,10 @@ export const useStyles = makeStyles()(theme => ({
|
||||
valueSeparator: {
|
||||
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 {
|
||||
IFeatureStrategy,
|
||||
IFeatureStrategyParameters,
|
||||
IConstraint,
|
||||
} from 'interfaces/strategy';
|
||||
import { Box, Chip } from '@mui/material';
|
||||
import { IFeatureStrategy } from 'interfaces/strategy';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle';
|
||||
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 StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { FeatureOverviewSegment } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSegment/FeatureOverviewSegment';
|
||||
import { ConstraintAccordionList } from 'component/common/ConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList';
|
||||
import { useStyles } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewExecution/FeatureOverviewExecution.styles';
|
||||
import { useStyles } from './StrategyExecution.styles';
|
||||
import {
|
||||
parseParameterString,
|
||||
parseParameterNumber,
|
||||
parseParameterStrings,
|
||||
} from 'utils/parseParameter';
|
||||
|
||||
interface IFeatureOverviewExecutionProps {
|
||||
parameters: IFeatureStrategyParameters;
|
||||
constraints?: IConstraint[];
|
||||
interface IStrategyExecutionProps {
|
||||
strategy: IFeatureStrategy;
|
||||
percentageFill?: string;
|
||||
}
|
||||
|
||||
const FeatureOverviewExecution = ({
|
||||
parameters,
|
||||
constraints = [],
|
||||
strategy,
|
||||
}: IFeatureOverviewExecutionProps) => {
|
||||
export const StrategyExecution = ({ strategy }: IStrategyExecutionProps) => {
|
||||
const { parameters, constraints = [] } = strategy;
|
||||
const { classes: styles } = useStyles();
|
||||
const { strategies } = useStrategies();
|
||||
const { uiConfig } = useUiConfig();
|
||||
@ -52,51 +44,51 @@ const FeatureOverviewExecution = ({
|
||||
case 'rollout':
|
||||
case 'Rollout':
|
||||
return (
|
||||
<Fragment key={key}>
|
||||
<p>
|
||||
{parameters[key]}% of your base{' '}
|
||||
{constraints.length > 0
|
||||
? 'who match constraints'
|
||||
: ''}{' '}
|
||||
is included.
|
||||
</p>
|
||||
|
||||
<Box
|
||||
className={styles.summary}
|
||||
key={key}
|
||||
sx={{ display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
<PercentageCircle
|
||||
percentage={parseParameterNumber(
|
||||
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':
|
||||
const users = parseParameterStrings(parameters[key]);
|
||||
return (
|
||||
<FeatureOverviewExecutionChips
|
||||
key={key}
|
||||
value={users}
|
||||
text="user"
|
||||
/>
|
||||
<ConstraintItem key={key} value={users} text="user" />
|
||||
);
|
||||
case 'hostNames':
|
||||
case 'HostNames':
|
||||
const hosts = parseParameterStrings(parameters[key]);
|
||||
return (
|
||||
<FeatureOverviewExecutionChips
|
||||
key={key}
|
||||
value={hosts}
|
||||
text={'host'}
|
||||
/>
|
||||
<ConstraintItem key={key} value={hosts} text={'host'} />
|
||||
);
|
||||
case 'IPs':
|
||||
const IPs = parseParameterStrings(parameters[key]);
|
||||
return (
|
||||
<FeatureOverviewExecutionChips
|
||||
key={key}
|
||||
value={IPs}
|
||||
text={'IP'}
|
||||
/>
|
||||
);
|
||||
return <ConstraintItem key={key} value={IPs} text={'IP'} />;
|
||||
case 'stickiness':
|
||||
case 'groupId':
|
||||
return null;
|
||||
@ -117,10 +109,7 @@ const FeatureOverviewExecution = ({
|
||||
);
|
||||
return (
|
||||
<Fragment key={param?.name}>
|
||||
<FeatureOverviewExecutionChips
|
||||
value={values}
|
||||
text={param.name}
|
||||
/>
|
||||
<ConstraintItem value={values} text={param.name} />
|
||||
<ConditionallyRender
|
||||
condition={notLastItem}
|
||||
show={<StrategySeparator text="AND" />}
|
||||
@ -130,13 +119,21 @@ const FeatureOverviewExecution = ({
|
||||
case 'percentage':
|
||||
return (
|
||||
<Fragment key={param?.name}>
|
||||
<p>
|
||||
{strategy?.parameters[param.name]}% of your base{' '}
|
||||
<div>
|
||||
<Chip
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="success"
|
||||
label={`${
|
||||
strategy?.parameters[param.name]
|
||||
}%`}
|
||||
/>{' '}
|
||||
of your base{' '}
|
||||
{constraints?.length > 0
|
||||
? 'who match constraints'
|
||||
: ''}{' '}
|
||||
is included.
|
||||
</p>
|
||||
</div>
|
||||
<PercentageCircle
|
||||
percentage={parseParameterNumber(
|
||||
strategy.parameters[param.name]
|
||||
@ -266,12 +263,21 @@ const FeatureOverviewExecution = ({
|
||||
/>
|
||||
<ConditionallyRender
|
||||
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()}
|
||||
{renderCustomStrategy()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeatureOverviewExecution;
|
@ -2,18 +2,20 @@ import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
export const useStyles = makeStyles()(theme => ({
|
||||
container: {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
border: `1px solid ${theme.palette.grey[300]}`,
|
||||
'& + &': {
|
||||
marginTop: '1rem',
|
||||
},
|
||||
background: theme.palette.background.default,
|
||||
},
|
||||
header: {
|
||||
padding: '0.5rem',
|
||||
padding: theme.spacing(0.5, 2),
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
alignItems: 'center',
|
||||
borderBottom: `1px solid ${theme.palette.grey[300]}`,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
},
|
||||
icon: {
|
||||
fill: theme.palette.inactiveIcon,
|
||||
@ -25,7 +27,6 @@ export const useStyles = makeStyles()(theme => ({
|
||||
body: {
|
||||
padding: '1rem',
|
||||
display: 'grid',
|
||||
gap: '1rem',
|
||||
justifyItems: 'center',
|
||||
},
|
||||
}));
|
@ -1,5 +1,5 @@
|
||||
import { Edit } from '@mui/icons-material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { DragIndicator, Edit } from '@mui/icons-material';
|
||||
import { styled, useTheme, IconButton } from '@mui/material';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { IFeatureStrategy } from 'interfaces/strategy';
|
||||
import {
|
||||
@ -8,28 +8,36 @@ import {
|
||||
} from 'utils/strategyNames';
|
||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||
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 { FeatureStrategyRemove } from 'component/feature/FeatureStrategy/FeatureStrategyRemove/FeatureStrategyRemove';
|
||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||
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;
|
||||
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,
|
||||
strategy,
|
||||
}: IFeatureOverviewEnvironmentStrategyProps) => {
|
||||
isDraggable,
|
||||
}: IStrategyItemProps) => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const featureId = useRequiredPathParam('featureId');
|
||||
const theme = useTheme();
|
||||
const { classes: styles } = useStyles();
|
||||
const Icon = getFeatureStrategyIcon(strategy.name);
|
||||
const { parameters, constraints } = strategy;
|
||||
|
||||
const editStrategyPath = formatEditStrategyPath(
|
||||
projectId,
|
||||
@ -41,6 +49,17 @@ const FeatureOverviewEnvironmentStrategy = ({
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<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} />
|
||||
<StringTruncator
|
||||
maxWidth="150"
|
||||
@ -68,15 +87,11 @@ const FeatureOverviewEnvironmentStrategy = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
<FeatureOverviewExecution
|
||||
parameters={parameters}
|
||||
<StrategyExecution
|
||||
strategy={strategy}
|
||||
constraints={constraints}
|
||||
percentageFill={theme.palette.grey[200]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeatureOverviewEnvironmentStrategy;
|
@ -1,27 +1,22 @@
|
||||
import { IFeatureEnvironmentMetrics } from 'interfaces/featureToggle';
|
||||
import { useStyles } from '../FeatureOverviewEnvironment.styles';
|
||||
import { FeatureMetricsStats } from 'component/feature/FeatureView/FeatureMetrics/FeatureMetricsStats/FeatureMetricsStats';
|
||||
import { SectionSeparator } from '../SectionSeparator/SectionSeparator';
|
||||
|
||||
interface IFeatureOverviewEnvironmentFooterProps {
|
||||
interface IEnvironmentFooterProps {
|
||||
environmentMetric?: IFeatureEnvironmentMetrics;
|
||||
}
|
||||
|
||||
const FeatureOverviewEnvironmentFooter = ({
|
||||
export const EnvironmentFooter = ({
|
||||
environmentMetric,
|
||||
}: IFeatureOverviewEnvironmentFooterProps) => {
|
||||
const { classes: styles } = useStyles();
|
||||
|
||||
}: IEnvironmentFooterProps) => {
|
||||
if (!environmentMetric) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.resultInfo}>
|
||||
<div className={styles.leftWing} />
|
||||
<div className={styles.separatorText}>Result</div>
|
||||
<div className={styles.rightWing} />
|
||||
</div>
|
||||
<SectionSeparator>Feature toggle exposure</SectionSeparator>
|
||||
|
||||
<div>
|
||||
<FeatureMetricsStats
|
||||
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 => ({
|
||||
featureOverviewEnvironment: {
|
||||
borderRadius: '12.5px',
|
||||
padding: '0.2rem',
|
||||
marginBottom: '1rem',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: theme.shape.borderRadiusLarge,
|
||||
marginBottom: theme.spacing(2),
|
||||
background: theme.palette.background.default,
|
||||
},
|
||||
accordionContainer: {
|
||||
width: '100%',
|
||||
accordion: {
|
||||
boxShadow: 'none',
|
||||
background: 'none',
|
||||
},
|
||||
accordionHeader: {
|
||||
boxShadow: 'none',
|
||||
@ -22,16 +22,26 @@ export const useStyles = makeStyles()(theme => ({
|
||||
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: {
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
paddingBottom: '1rem',
|
||||
paddingBottom: theme.spacing(2),
|
||||
},
|
||||
header: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
paddingTop: '1.5rem',
|
||||
// paddingTop: '1.5rem',
|
||||
},
|
||||
headerTitle: {
|
||||
display: 'flex',
|
||||
@ -46,14 +56,6 @@ export const useStyles = makeStyles()(theme => ({
|
||||
marginBottom: '0.5rem',
|
||||
},
|
||||
},
|
||||
disabledIndicatorPos: {
|
||||
position: 'absolute',
|
||||
top: '15px',
|
||||
left: '20px',
|
||||
[theme.breakpoints.down(560)]: {
|
||||
top: '13px',
|
||||
},
|
||||
},
|
||||
iconContainer: {
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
borderRadius: '50%',
|
||||
@ -69,32 +71,14 @@ export const useStyles = makeStyles()(theme => ({
|
||||
width: '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: {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
marginBottom: '1rem',
|
||||
},
|
||||
truncator: {
|
||||
fontSize: theme.fontSizes.bodySize,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
[theme.breakpoints.down(560)]: {
|
||||
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 { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
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 StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||
import { useStyles } from './FeatureOverviewEnvironment.styles';
|
||||
import FeatureOverviewEnvironmentBody from './FeatureOverviewEnvironmentBody/FeatureOverviewEnvironmentBody';
|
||||
import FeatureOverviewEnvironmentFooter from './FeatureOverviewEnvironmentFooter/FeatureOverviewEnvironmentFooter';
|
||||
import EnvironmentAccordionBody from './EnvironmentAccordionBody/EnvironmentAccordionBody';
|
||||
import { EnvironmentFooter } from './EnvironmentFooter/EnvironmentFooter';
|
||||
import FeatureOverviewEnvironmentMetrics from './FeatureOverviewEnvironmentMetrics/FeatureOverviewEnvironmentMetrics';
|
||||
import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu';
|
||||
import { FEATURE_ENVIRONMENT_ACCORDION } from 'utils/testIds';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { FeatureStrategyIcons } from 'component/feature/FeatureStrategy/FeatureStrategyIcons/FeatureStrategyIcons';
|
||||
import { Badge } from 'component/common/Badge/Badge';
|
||||
// import { Badge } from 'component/common/Badge/Badge';
|
||||
|
||||
interface IFeatureOverviewEnvironmentProps {
|
||||
env: IFeatureEnvironment;
|
||||
@ -25,6 +33,7 @@ const FeatureOverviewEnvironment = ({
|
||||
env,
|
||||
}: IFeatureOverviewEnvironmentProps) => {
|
||||
const { classes: styles } = useStyles();
|
||||
const theme = useTheme();
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const featureId = useRequiredPathParam('featureId');
|
||||
const { metrics } = useFeatureMetrics(projectId, featureId);
|
||||
@ -38,38 +47,57 @@ const FeatureOverviewEnvironment = ({
|
||||
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 (
|
||||
<div className={styles.featureOverviewEnvironment}>
|
||||
<div
|
||||
className={styles.featureOverviewEnvironment}
|
||||
style={{
|
||||
background: !env.enabled
|
||||
? theme.palette.neutral.light
|
||||
: theme.palette.background.default,
|
||||
}}
|
||||
>
|
||||
<Accordion
|
||||
style={{ boxShadow: 'none' }}
|
||||
className={styles.accordion}
|
||||
data-testid={`${FEATURE_ENVIRONMENT_ACCORDION}_${env.name}`}
|
||||
>
|
||||
<AccordionSummary
|
||||
className={styles.accordionHeader}
|
||||
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}>
|
||||
<EnvironmentIcon
|
||||
enabled={env.enabled}
|
||||
className={styles.headerIcon}
|
||||
/>
|
||||
<p>
|
||||
Feature toggle execution for
|
||||
<div>
|
||||
<StringTruncator
|
||||
text={env.name}
|
||||
className={styles.truncator}
|
||||
maxWidth="100"
|
||||
maxLength={15}
|
||||
/>
|
||||
</p>
|
||||
<ConditionallyRender
|
||||
condition={!env.enabled}
|
||||
show={
|
||||
<Chip
|
||||
size="small"
|
||||
variant="outlined"
|
||||
// severity="disabled"
|
||||
label="Disabled"
|
||||
sx={{ ml: 1 }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.container}>
|
||||
<FeatureStrategyMenu
|
||||
@ -83,17 +111,6 @@ const FeatureOverviewEnvironment = ({
|
||||
strategies={featureEnvironment?.strategies}
|
||||
/>
|
||||
</div>
|
||||
<ConditionallyRender
|
||||
condition={!env.enabled}
|
||||
show={
|
||||
<Badge
|
||||
color="warning"
|
||||
className={styles.disabledIndicatorPos}
|
||||
>
|
||||
Disabled
|
||||
</Badge>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FeatureOverviewEnvironmentMetrics
|
||||
@ -101,26 +118,41 @@ const FeatureOverviewEnvironment = ({
|
||||
/>
|
||||
</AccordionSummary>
|
||||
|
||||
<AccordionDetails>
|
||||
<div className={styles.accordionContainer}>
|
||||
<FeatureOverviewEnvironmentBody
|
||||
featureEnvironment={featureEnvironment}
|
||||
getOverviewText={getOverviewText}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
// @ts-expect-error
|
||||
featureEnvironment?.strategies?.length > 0
|
||||
}
|
||||
show={
|
||||
<FeatureOverviewEnvironmentFooter
|
||||
// @ts-expect-error
|
||||
env={env}
|
||||
<AccordionDetails
|
||||
className={classNames(styles.accordionDetails, {
|
||||
[styles.accordionDetailsDisabled]: !env.enabled,
|
||||
})}
|
||||
>
|
||||
<EnvironmentAccordionBody
|
||||
featureEnvironment={featureEnvironment}
|
||||
isDisabled={!env.enabled}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
(featureEnvironment?.strategies?.length || 0) > 0
|
||||
}
|
||||
show={
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
py: 1,
|
||||
}}
|
||||
>
|
||||
<FeatureStrategyMenu
|
||||
label="Add strategy"
|
||||
projectId={projectId}
|
||||
featureId={featureId}
|
||||
environmentId={env.name}
|
||||
/>
|
||||
</Box>
|
||||
<EnvironmentFooter
|
||||
environmentMetric={environmentMetric}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</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: {
|
||||
maxWidth: '215px',
|
||||
maxWidth: '270px',
|
||||
marginTop: '0.25rem',
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
[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 renderEnvironments = () => {
|
||||
return environments?.map(env => {
|
||||
return <FeatureOverviewEnvironment env={env} key={env.name} />;
|
||||
});
|
||||
};
|
||||
|
||||
return <>{renderEnvironments()}</>;
|
||||
return (
|
||||
<>
|
||||
{environments?.map(env => (
|
||||
<FeatureOverviewEnvironment env={env} key={env.name} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeatureOverviewEnvironments;
|
||||
|
@ -2,7 +2,7 @@ import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
export const useStyles = makeStyles()(theme => ({
|
||||
eventEntry: {
|
||||
border: `1px solid ${theme.palette.grey[100]}`,
|
||||
border: `1px solid ${theme.palette.neutral.light}`,
|
||||
padding: '1rem',
|
||||
margin: '1rem 0',
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
|
@ -14,7 +14,7 @@ interface IPlaygroundEditorProps {
|
||||
|
||||
const StyledEditorHeader = styled('aside')(({ theme }) => ({
|
||||
height: '50px',
|
||||
backgroundColor: theme.palette.grey[100],
|
||||
backgroundColor: theme.palette.neutral.light,
|
||||
borderTopRightRadius: theme.shape.borderRadiusMedium,
|
||||
borderTopLeftRadius: theme.shape.borderRadiusMedium,
|
||||
padding: theme.spacing(1, 2),
|
||||
|
@ -16,7 +16,7 @@ export const useStyles = makeStyles()(theme => ({
|
||||
},
|
||||
'&:hover': {
|
||||
transition: 'background-color 0.2s ease-in-out',
|
||||
backgroundColor: theme.palette.grey[100],
|
||||
backgroundColor: theme.palette.neutral.light,
|
||||
},
|
||||
},
|
||||
header: {
|
||||
|
@ -10,7 +10,7 @@ export const useStyles = makeStyles()(theme => ({
|
||||
},
|
||||
headerContainer: { display: 'flex', padding: '0.5rem' },
|
||||
divider: {
|
||||
backgroundColor: theme.palette.grey[100],
|
||||
backgroundColor: theme.palette.neutral.light,
|
||||
height: '1px',
|
||||
width: '100%',
|
||||
},
|
||||
|
@ -1,4 +1,8 @@
|
||||
import { IFeatureStrategyPayload, IFeatureStrategy } from 'interfaces/strategy';
|
||||
import {
|
||||
IFeatureStrategyPayload,
|
||||
IFeatureStrategy,
|
||||
IFeatureStrategySortOrder,
|
||||
} from 'interfaces/strategy';
|
||||
import useAPI from '../useApi/useApi';
|
||||
|
||||
const useFeatureStrategyApi = () => {
|
||||
@ -52,10 +56,26 @@ const useFeatureStrategyApi = () => {
|
||||
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 {
|
||||
addStrategyToFeature,
|
||||
updateStrategyOnFeature,
|
||||
deleteStrategyFromFeature,
|
||||
setStrategiesSortOrder,
|
||||
loading,
|
||||
errors,
|
||||
};
|
||||
|
@ -51,3 +51,8 @@ export interface IConstraint {
|
||||
operator: Operator;
|
||||
contextName: string;
|
||||
}
|
||||
|
||||
export interface IFeatureStrategySortOrder {
|
||||
id: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
@ -26,6 +26,9 @@ export default createTheme({
|
||||
fontSize: '1.5rem',
|
||||
lineHeight: 1.875,
|
||||
},
|
||||
caption: {
|
||||
fontSize: `${12 / 16}rem`,
|
||||
},
|
||||
},
|
||||
fontSizes: {
|
||||
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 LanguageIcon from '@mui/icons-material/Language';
|
||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||
import CodeIcon from '@mui/icons-material/Code';
|
||||
import { ReactComponent as RolloutIcon } from 'assets/icons/rollout.svg';
|
||||
import { ElementType } from 'react';
|
||||
|
||||
@ -11,6 +12,8 @@ export const formatStrategyName = (strategyName: string): string => {
|
||||
|
||||
export const getFeatureStrategyIcon = (strategyName: string): ElementType => {
|
||||
switch (strategyName) {
|
||||
case 'default':
|
||||
return PowerSettingsNewIcon;
|
||||
case 'remoteAddress':
|
||||
return LanguageIcon;
|
||||
case 'flexibleRollout':
|
||||
@ -20,7 +23,7 @@ export const getFeatureStrategyIcon = (strategyName: string): ElementType => {
|
||||
case 'applicationHostname':
|
||||
return LocationOnIcon;
|
||||
default:
|
||||
return PowerSettingsNewIcon;
|
||||
return CodeIcon;
|
||||
}
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user