1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-22 01:16:07 +02:00

Refine Playground UI (#1217)

* fix playground border radius consistency

* improve playground alerts

* fix: playground segments constraint type logic

* fix: refactor segment execution

* fix: comments

* fix: add summary width

* align playground spacing and borders

* fix build - ts segment type in playground

* fix status cell logic

* update playground disabled env info

* fix playground filter by status and sort

Co-authored-by: Nuno Góis <github@nunogois.com>

Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
Co-authored-by: andreas-unleash <104830839+andreas-unleash@users.noreply.github.com>
Co-authored-by: Nuno Góis <github@nunogois.com>
This commit is contained in:
Tymoteusz Czech 2022-08-12 12:13:07 +02:00 committed by GitHub
parent 5ffb63e342
commit 859aa435e0
32 changed files with 417 additions and 800 deletions

View File

@ -1,20 +1,6 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
constraintIconContainer: {
backgroundColor: theme.palette.primary.main,
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginRight: theme.spacing(1),
[theme.breakpoints.down(710)]: {
marginRight: 0,
},
},
constraintIcon: {
fill: '#fff',
},
accordion: {
border: `1px solid ${theme.palette.dividerAlternative}`,
borderRadius: theme.shape.borderRadiusMedium,
@ -33,9 +19,12 @@ export const useStyles = makeStyles()(theme => ({
headerMetaInfo: {
display: 'flex',
alignItems: 'stretch',
marginLeft: theme.spacing(1),
[theme.breakpoints.down(710)]: {
marginLeft: 0,
flexDirection: 'column',
alignItems: 'center',
width: '100%',
},
},
headerContainer: {
@ -58,6 +47,10 @@ export const useStyles = makeStyles()(theme => ({
justifyContent: 'stretch',
margin: 'auto 0',
flexDirection: 'column',
marginLeft: theme.spacing(1),
[theme.breakpoints.down(710)]: {
marginLeft: 0,
},
},
headerValues: {
fontSize: theme.fontSizes.smallBody,
@ -71,13 +64,8 @@ export const useStyles = makeStyles()(theme => ({
},
},
headerConstraintContainer: {
minWidth: '220px',
minWidth: '152px',
position: 'relative',
paddingRight: '1rem',
[theme.breakpoints.between(1101, 1365)]: {
minWidth: '152px',
paddingRight: '0.5rem',
},
[theme.breakpoints.down(710)]: {
paddingRight: 0,
},

View File

@ -22,6 +22,8 @@ interface IConstraintAccordionViewProps {
onDelete?: () => void;
onEdit?: () => void;
sx?: SxProps<Theme>;
compact?: boolean;
renderAfter?: JSX.Element;
}
export const ConstraintAccordionView = ({
@ -29,6 +31,8 @@ export const ConstraintAccordionView = ({
onEdit,
onDelete,
sx = undefined,
compact = false,
renderAfter,
}: IConstraintAccordionViewProps) => {
const { classes: styles } = useStyles();
const [expandable, setExpandable] = useState(true);
@ -62,14 +66,24 @@ export const ConstraintAccordionView = ({
},
}}
>
<ConstraintAccordionViewHeader
constraint={constraint}
onEdit={onEdit}
onDelete={onDelete}
singleValue={singleValue}
allowExpand={setExpandable}
expanded={expanded}
/>
<div
style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
}}
>
<ConstraintAccordionViewHeader
constraint={constraint}
onEdit={onEdit}
onDelete={onDelete}
singleValue={singleValue}
allowExpand={setExpandable}
expanded={expanded}
compact={compact}
/>
{renderAfter}
</div>
</AccordionSummary>
<AccordionDetails className={styles.accordionDetails}>

View File

@ -11,6 +11,7 @@ interface IConstraintAccordionViewHeaderProps {
singleValue: boolean;
expanded: boolean;
allowExpand: (shouldExpand: boolean) => void;
compact?: boolean;
}
export const ConstraintAccordionViewHeader = ({
@ -20,12 +21,13 @@ export const ConstraintAccordionViewHeader = ({
singleValue,
allowExpand,
expanded,
compact,
}: IConstraintAccordionViewHeaderProps) => {
const { classes: styles } = useStyles();
return (
<div className={styles.headerContainer}>
<ConstraintIcon />
<ConstraintIcon compact={compact} />
<ConstraintAccordionViewHeaderInfo
constraint={constraint}
singleValue={singleValue}

View File

@ -4,7 +4,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
import { ConstraintAccordionViewHeaderSingleValue } from '../ContraintAccordionViewHeaderSingleValue/ConstraintAccordionViewHeaderSingleValue';
import { ConstraintAccordionViewHeaderMultipleValues } from '../ContraintAccordionViewHeaderMultipleValues/ConstraintAccordionViewHeaderMultipleValues';
import React from 'react';
import { IConstraint } from '../../../../../../interfaces/strategy';
import { IConstraint } from 'interfaces/strategy';
import { useStyles } from '../../../ConstraintAccordion.styles';
const StyledHeaderText = styled('span')(({ theme }) => ({

View File

@ -1,8 +1,8 @@
import { ConditionallyRender } from '../../../../ConditionallyRender/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { styled } from '@mui/material';
import React, { useEffect, useMemo, useState } from 'react';
import classnames from 'classnames';
import { IConstraint } from '../../../../../../interfaces/strategy';
import { IConstraint } from 'interfaces/strategy';
import { useStyles } from '../../../ConstraintAccordion.styles';
const StyledValuesSpan = styled('span')(({ theme }) => ({

View File

@ -2,11 +2,12 @@ import React, { useEffect } from 'react';
import { Chip, styled } from '@mui/material';
import { formatConstraintValue } from 'utils/formatConstraintValue';
import { useStyles } from '../../../ConstraintAccordion.styles';
import { IConstraint } from '../../../../../../interfaces/strategy';
import { IConstraint } from 'interfaces/strategy';
import { useLocationSettings } from 'hooks/useLocationSettings';
const StyledSingleValueChip = styled(Chip)(({ theme }) => ({
margin: 'auto 0',
marginLeft: theme.spacing(1),
[theme.breakpoints.down(710)]: {
margin: theme.spacing(1, 0),
},

View File

@ -12,12 +12,12 @@ export const StyledIconWrapperBase = styled('div')<{
justifyContent: 'center',
alignSelf: 'stretch',
color: theme.palette.primary.main,
marginRight: theme.spacing(1),
marginLeft: theme.spacing(1),
borderRadius: theme.shape.borderRadius,
}));
const StyledPrefixIconWrapper = styled(StyledIconWrapperBase)(() => ({
marginRight: 0,
marginLeft: 0,
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
}));

View File

@ -1,12 +1,28 @@
import { useStyles } from './ConstraintAccordion.styles';
import { VFC } from 'react';
import { Box } from '@mui/material';
import { TrackChanges } from '@mui/icons-material';
export const ConstraintIcon = () => {
const { classes: styles } = useStyles();
interface IConstraintIconProps {
compact?: boolean;
}
return (
<div className={styles.constraintIconContainer}>
<TrackChanges className={styles.constraintIcon} />
</div>
);
};
export const ConstraintIcon: VFC<IConstraintIconProps> = ({ compact }) => (
<Box
sx={{
backgroundColor: 'primary.light',
p: compact ? '1px' : '2px',
borderRadius: '50%',
width: compact ? '18px' : '24px',
height: compact ? '18px' : '24px',
}}
>
<TrackChanges
sx={{
fill: 'white',
display: 'block',
width: compact ? '16px' : '20px',
height: compact ? '16px' : '20px',
}}
/>
</Box>
);

View File

@ -14,13 +14,17 @@ import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender'
import { useStyles } from './SegmentItem.styles';
interface ISegmentItemProps {
segment: ISegment;
segment: Partial<ISegment>;
isExpanded?: boolean;
constraintList?: JSX.Element;
headerContent?: JSX.Element;
}
export const SegmentItem: VFC<ISegmentItemProps> = ({
segment,
isExpanded,
headerContent,
constraintList,
}) => {
const { classes } = useStyles();
const [isOpen, setIsOpen] = useState(isExpanded || false);
@ -46,6 +50,10 @@ export const SegmentItem: VFC<ISegmentItemProps> = ({
>
{segment.name}
</Link>
<ConditionallyRender
condition={Boolean(headerContent)}
show={headerContent}
/>
<ConditionallyRender
condition={!isExpanded}
show={
@ -62,17 +70,23 @@ export const SegmentItem: VFC<ISegmentItemProps> = ({
</AccordionSummary>
<AccordionDetails sx={{ pt: 0 }}>
<ConditionallyRender
condition={segment!.constraints?.length > 0}
show={
<ConstraintAccordionList
constraints={segment.constraints}
showLabel={false}
/>
}
condition={Boolean(constraintList)}
show={constraintList}
elseShow={
<Typography>
This segment has no constraints.
</Typography>
<ConditionallyRender
condition={(segment?.constraints?.length || 0) > 0}
show={
<ConstraintAccordionList
constraints={segment!.constraints!}
showLabel={false}
/>
}
elseShow={
<Typography>
This segment has no constraints.
</Typography>
}
/>
}
/>
</AccordionDetails>

View File

@ -17,11 +17,11 @@ const StyledSlider = withStyles(Slider, theme => ({
valueLabel: {},
track: {
height: 8,
borderRadius: 4,
borderRadius: theme.shape.borderRadius,
},
rail: {
height: 8,
borderRadius: 4,
borderRadius: theme.shape.borderRadius,
},
}));

View File

@ -32,7 +32,7 @@ export const PlaygroundGuidancePopper = () => {
sx={theme => ({
padding: theme.spacing(8, 4),
maxWidth: '500px',
borderRadius: theme.shape.borderRadiusExtraLarge,
borderRadius: `${theme.shape.borderRadiusExtraLarge}px`,
})}
>
<IconButton

View File

@ -52,7 +52,7 @@ export const FeatureDetails = ({
: undefined;
const customStrategiesTxt = hasCustomStrategies(feature)
? `This feature uses custom strategies. Custom strategies can't be evaluated, so they will be marked as Unevaluated`
? `This feature uses custom strategies. Custom strategies can't be evaluated, so they will be marked accordingly.`
: undefined;
const onCloseClick =
@ -97,6 +97,9 @@ export const FeatureDetails = ({
<Typography variant="subtitle1" color={color} component="span">
{reason}
</Typography>
<Typography variant="body1" component="span">
.
</Typography>
</div>
<ConditionallyRender
condition={Boolean(noValueTxt)}
@ -110,7 +113,9 @@ export const FeatureDetails = ({
condition={Boolean(customStrategiesTxt)}
show={
<div className={styles.alertRow}>
<Alert color={'info'}>{customStrategiesTxt}</Alert>
<Alert severity="warning" color="info">
{customStrategiesTxt}
</Alert>
</div>
}
/>

View File

@ -10,5 +10,6 @@ export const useStyles = makeStyles()(theme => ({
height: 'auto',
overflowY: 'auto',
backgroundColor: theme.palette.tertiary.light,
borderRadius: theme.shape.borderRadiusLarge,
},
}));

View File

@ -17,16 +17,25 @@ export const PlaygroundResultFeatureStrategyList = ({
}: PlaygroundResultFeatureStrategyListProps) => {
return (
<>
<ConditionallyRender
condition={feature?.strategies?.data?.length === 0}
show={
<Alert severity="warning" sx={{ mt: 2 }}>
There are no strategies added to this feature toggle in
selected environment.
</Alert>
}
/>
<ConditionallyRender
condition={
!feature.isEnabledInCurrentEnvironment &&
Boolean(feature?.strategies?.data)
(feature?.strategies?.data?.length || 0) > 0
}
show={
<Alert severity={'info'} color={'info'}>
If environment would be enabled then this feature would
be {feature.strategies?.result ? 'TRUE' : 'FALSE'} and
the strategies would evaluate like this:{' '}
<Alert severity="info" color="warning">
If environment was enabled, then this feature toggle
would be {feature.strategies?.result ? 'TRUE' : 'FALSE'}{' '}
with strategies evaluated like so:{' '}
</Alert>
}
/>

View File

@ -35,6 +35,6 @@ export const useStyles = makeStyles()(theme => ({
background: theme.palette.background.default,
},
successBorder: {
border: `1px solid ${theme.palette.success.main}`,
borderColor: theme.palette.success.main,
},
}));

View File

@ -1,106 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
constraintIconContainer: {
backgroundColor: theme.palette.primary.main,
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginRight: theme.spacing(1),
[theme.breakpoints.down(650)]: {
marginBottom: '1rem',
marginRight: 0,
},
},
constraintIcon: {
fill: '#fff',
},
accordion: {
border: `1px solid ${theme.palette.dividerAlternative}`,
borderRadius: theme.spacing(1),
backgroundColor: '#fff',
boxShadow: 'none',
margin: 0,
},
accordionRoot: {
'&:before': {
opacity: '0 !important',
},
},
headerMetaInfo: {
display: 'flex',
alignItems: 'stretch',
[theme.breakpoints.down(710)]: { flexDirection: 'column' },
},
headerContainer: {
display: 'flex',
alignItems: 'center',
[theme.breakpoints.down(710)]: {
flexDirection: 'column',
alignItems: 'center',
position: 'relative',
},
},
headerValuesContainerWrapper: {
display: 'flex',
alignItems: 'stretch',
margin: 'auto 0',
},
headerValuesContainer: {
display: 'flex',
justifyContent: 'stretch',
margin: 'auto 0',
flexDirection: 'column',
},
headerValues: {
fontSize: theme.fontSizes.smallBody,
},
headerValuesExpand: {
fontSize: theme.fontSizes.smallBody,
marginTop: '4px',
color: theme.palette.primary.dark,
[theme.breakpoints.down(710)]: {
textAlign: 'center',
},
},
headerConstraintContainer: {
minWidth: '220px',
position: 'relative',
paddingRight: '1rem',
[theme.breakpoints.between(1101, 1365)]: {
minWidth: '152px',
paddingRight: '0.5rem',
},
},
headerText: {
maxWidth: '400px',
fontSize: theme.fontSizes.smallBody,
[theme.breakpoints.down('xl')]: {
display: 'none',
},
},
chip: {
margin: '0 0.5rem 0.5rem 0',
},
chipValue: {
whiteSpace: 'pre',
},
accordionDetails: {
borderTop: `1px dashed ${theme.palette.grey[300]}`,
display: 'flex',
flexDirection: 'column',
},
valuesContainer: {
padding: '1rem 0rem',
maxHeight: '400px',
overflowY: 'auto',
},
summary: {
border: 'none',
padding: theme.spacing(0.5, 3),
'&:hover .valuesExpandLabel': {
textDecoration: 'underline',
},
},
}));

View File

@ -1,91 +0,0 @@
import { useState, VFC } from 'react';
import {
Accordion,
AccordionSummary,
AccordionDetails,
SxProps,
Theme,
useTheme,
} from '@mui/material';
import { ConstraintAccordionViewHeader } from './ConstraintAccordionViewHeader/ConstraintAccordionViewHeader';
import { oneOf } from 'utils/oneOf';
import {
dateOperators,
numOperators,
semVerOperators,
} from 'constants/operators';
import { useStyles } from './ConstraintAccordion.styles';
import {
PlaygroundConstraintSchema,
PlaygroundRequestSchema,
} from 'component/playground/Playground/interfaces/playground.model';
import { ConstraintAccordionViewBody } from 'component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewBody/ConstraintAccordionViewBody';
interface IConstraintAccordionViewProps {
constraint: PlaygroundConstraintSchema;
playgroundInput?: PlaygroundRequestSchema;
maxLength?: number;
sx?: SxProps<Theme>;
}
export const ConstraintAccordionView: VFC<IConstraintAccordionViewProps> = ({
constraint,
sx = undefined,
maxLength,
playgroundInput,
}) => {
const { classes: styles } = useStyles();
const [expandable, setExpandable] = useState(true);
const [expanded, setExpanded] = useState(false);
const theme = useTheme();
const singleValue = oneOf(
[...semVerOperators, ...numOperators, ...dateOperators],
constraint.operator
);
const handleClick = () => {
if (expandable) {
setExpanded(!expanded);
}
};
const backgroundColor = Boolean(playgroundInput)
? !Boolean((constraint as PlaygroundConstraintSchema).result)
? theme.palette.neutral.light
: 'inherit'
: 'inherit';
return (
<Accordion
className={styles.accordion}
classes={{ root: styles.accordionRoot }}
expanded={expanded}
sx={sx}
>
<AccordionSummary
classes={{ root: styles.summary }}
expandIcon={null}
onClick={handleClick}
sx={{
cursor: expandable ? 'pointer' : 'default!important',
'&:hover': {
cursor: expandable ? 'pointer' : 'default!important',
},
backgroundColor: backgroundColor,
}}
>
<ConstraintAccordionViewHeader
constraint={constraint}
singleValue={singleValue}
allowExpand={setExpandable}
expanded={expanded}
maxLength={maxLength ?? 112}
playgroundInput={playgroundInput}
/>
</AccordionSummary>
<AccordionDetails className={styles.accordionDetails}>
<ConstraintAccordionViewBody constraint={constraint} />
</AccordionDetails>
</Accordion>
);
};

View File

@ -1,42 +0,0 @@
import { ConstraintIcon } from 'component/common/ConstraintAccordion/ConstraintIcon';
import { ConstraintAccordionViewHeaderInfo } from './ConstraintAccordionViewHeaderInfo/ConstraintAccordionViewHeaderInfo';
import { useStyles } from 'component/common/ConstraintAccordion/ConstraintAccordion.styles';
import {
PlaygroundConstraintSchema,
PlaygroundRequestSchema,
} from 'component/playground/Playground/interfaces/playground.model';
interface PlaygroundConstraintAccordionViewHeaderProps {
constraint: PlaygroundConstraintSchema;
singleValue: boolean;
expanded: boolean;
allowExpand: (shouldExpand: boolean) => void;
playgroundInput?: PlaygroundRequestSchema;
maxLength?: number;
}
export const ConstraintAccordionViewHeader = ({
constraint,
singleValue,
allowExpand,
expanded,
maxLength,
playgroundInput,
}: PlaygroundConstraintAccordionViewHeaderProps) => {
const { classes: styles } = useStyles();
return (
<div className={styles.headerContainer}>
<ConstraintIcon />
<ConstraintAccordionViewHeaderInfo
constraint={constraint}
singleValue={singleValue}
allowExpand={allowExpand}
expanded={expanded}
result={constraint.result}
maxLength={maxLength}
playgroundInput={playgroundInput}
/>
</div>
);
};

View File

@ -1,111 +0,0 @@
import { styled, Tooltip, Typography, useTheme } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { PlaygroundSingleValue } from './PlaygroundSingleValue/PlaygroundSingleValue';
import { PLaygroundMultipleValues } from './PlaygroundMultipleValues/PlaygroundMultipleValues';
import React from 'react';
import { useStyles } from '../../ConstraintAccordion.styles';
import { CancelOutlined } from '@mui/icons-material';
import {
PlaygroundConstraintSchema,
PlaygroundRequestSchema,
} from 'component/playground/Playground/interfaces/playground.model';
import { ConstraintViewHeaderOperator } from 'component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintViewHeaderOperator/ConstraintViewHeaderOperator';
const StyledHeaderText = styled('span')(({ theme }) => ({
display: '-webkit-box',
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
maxWidth: '100px',
minWidth: '100px',
marginRight: '10px',
marginTop: 'auto',
marginBottom: 'auto',
wordBreak: 'break-word',
fontSize: theme.fontSizes.smallBody,
[theme.breakpoints.down(710)]: {
textAlign: 'center',
padding: theme.spacing(1, 0),
marginRight: 'inherit',
maxWidth: 'inherit',
},
}));
const StyledHeaderWrapper = styled('div')(({ theme }) => ({
display: 'flex',
width: '100%',
justifyContent: 'space-between',
borderRadius: theme.spacing(1),
}));
interface PlaygroundConstraintAccordionViewHeaderInfoProps {
constraint: PlaygroundConstraintSchema;
singleValue: boolean;
expanded: boolean;
allowExpand: (shouldExpand: boolean) => void;
result?: boolean;
maxLength?: number;
playgroundInput?: PlaygroundRequestSchema;
}
export const ConstraintAccordionViewHeaderInfo = ({
constraint,
singleValue,
allowExpand,
expanded,
result,
playgroundInput,
maxLength = 112,
}: PlaygroundConstraintAccordionViewHeaderInfoProps) => {
const { classes: styles } = useStyles();
const theme = useTheme();
const constraintExistsInContext = Boolean(
playgroundInput?.context[constraint.contextName]
);
return (
<StyledHeaderWrapper>
<div className={styles.headerMetaInfo}>
<Tooltip title={constraint.contextName} arrow>
<StyledHeaderText>
{constraint.contextName}
<Typography
variant={'body1'}
color={
constraintExistsInContext
? theme.palette.neutral.dark
: theme.palette.error.main
}
>
{playgroundInput?.context[constraint.contextName] ||
'no value'}
</Typography>
</StyledHeaderText>
</Tooltip>
<ConstraintViewHeaderOperator constraint={constraint} />
<ConditionallyRender
condition={singleValue}
show={
<PlaygroundSingleValue
constraint={constraint}
allowExpand={allowExpand}
/>
}
elseShow={
<PLaygroundMultipleValues
constraint={constraint}
expanded={expanded}
allowExpand={allowExpand}
maxLength={maxLength}
/>
}
/>
</div>
<ConditionallyRender
condition={result !== undefined && !Boolean(result)}
show={<CancelOutlined color="error" sx={{ mt: 1 }} />}
/>
</StyledHeaderWrapper>
);
};

View File

@ -1,85 +0,0 @@
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { styled, Typography } from '@mui/material';
import React, { useEffect, useMemo, useState } from 'react';
import classnames from 'classnames';
import { useStyles } from '../../../ConstraintAccordion.styles';
import { PlaygroundConstraintSchema } from 'component/playground/Playground/interfaces/playground.model';
const StyledValuesSpan = styled('span')(({ theme }) => ({
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
wordBreak: 'break-word',
fontSize: theme.fontSizes.smallBody,
margin: 'auto 0',
[theme.breakpoints.down(710)]: {
margin: theme.spacing(1, 0),
textAlign: 'center',
},
}));
interface PLaygroundConstraintAccordionViewHeaderMultipleValueProps {
constraint: PlaygroundConstraintSchema;
expanded: boolean;
maxLength: number;
allowExpand: (shouldExpand: boolean) => void;
}
export const PLaygroundMultipleValues = ({
constraint,
expanded,
allowExpand,
maxLength,
}: PLaygroundConstraintAccordionViewHeaderMultipleValueProps) => {
const { classes: styles } = useStyles();
const [expandable, setExpandable] = useState(false);
const text = useMemo(() => {
return constraint?.values?.map(value => value).join(', ');
}, [constraint]);
useEffect(() => {
if (text) {
allowExpand((text?.length ?? 0) > maxLength);
setExpandable((text?.length ?? 0) > maxLength);
}
}, [text, maxLength, allowExpand, setExpandable]);
return (
<div className={styles.headerValuesContainerWrapper}>
<div className={styles.headerValuesContainer}>
<ConditionallyRender
condition={!Boolean(constraint.result)}
show={
<Typography
variant={'body2'}
color={'error'}
noWrap={true}
sx={{ mr: 1 }}
>
does not match values{' '}
</Typography>
}
/>
<StyledValuesSpan>{text}</StyledValuesSpan>
<ConditionallyRender
condition={expandable}
show={
<p
className={classnames(
styles.headerValuesExpand,
'valuesExpandLabel'
)}
>
{!expanded
? `View all (${constraint?.values?.length})`
: 'View less'}
</p>
}
/>
</div>
</div>
);
};

View File

@ -1,47 +0,0 @@
import React, { useEffect } from 'react';
import { Chip, styled, Typography } from '@mui/material';
import { formatConstraintValue } from 'utils/formatConstraintValue';
import { useStyles } from '../../../ConstraintAccordion.styles';
import { useLocationSettings } from 'hooks/useLocationSettings';
import { PlaygroundConstraintSchema } from 'component/playground/Playground/interfaces/playground.model';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
const StyledSingleValueChip = styled(Chip)(({ theme }) => ({
margin: 'auto 0',
[theme.breakpoints.down(710)]: {
margin: theme.spacing(1, 0),
},
}));
interface PlaygroundConstraintAccordionViewHeaderSingleValueProps {
constraint: PlaygroundConstraintSchema;
allowExpand: (shouldExpand: boolean) => void;
}
export const PlaygroundSingleValue = ({
constraint,
allowExpand,
}: PlaygroundConstraintAccordionViewHeaderSingleValueProps) => {
const { locationSettings } = useLocationSettings();
const { classes: styles } = useStyles();
useEffect(() => {
allowExpand(false);
}, [allowExpand]);
return (
<div className={styles.headerValuesContainerWrapper}>
<ConditionallyRender
condition={!Boolean(constraint.result)}
show={
<Typography variant={'body1'} color={'error'}>
does not match values{' '}
</Typography>
}
/>
<StyledSingleValueChip
label={formatConstraintValue(constraint, locationSettings)}
/>
</div>
);
};

View File

@ -0,0 +1,40 @@
import { styled, Typography } from '@mui/material';
import { CancelOutlined } from '@mui/icons-material';
import {
PlaygroundConstraintSchema,
PlaygroundRequestSchema,
} from 'component/playground/Playground/interfaces/playground.model';
const StyledConstraintErrorDiv = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
marginTop: theme.spacing(1),
color: theme.palette.error.main,
}));
interface IConstraintErrorProps {
constraint: PlaygroundConstraintSchema;
input?: PlaygroundRequestSchema;
}
export const ConstraintError = ({
constraint,
input,
}: IConstraintErrorProps) => {
const formatText = () => {
const value = input?.context[constraint.contextName];
if (value) {
return `Constraint not met the value in the context: { ${value} } is not ${constraint.operator} ${constraint.contextName}`;
}
return `Constraint not met no value was specified for ${constraint.contextName}`;
};
return (
<StyledConstraintErrorDiv>
<CancelOutlined style={{ marginRight: '0.25rem' }} />
<Typography variant="body2">{formatText()}</Typography>
</StyledConstraintErrorDiv>
);
};

View File

@ -7,11 +7,12 @@ import { objectId } from 'utils/objectId';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
import { styled } from '@mui/material';
import { ConstraintAccordionView } from './ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView';
import { ConstraintAccordionView } from 'component/common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView';
import { ConstraintError } from './ConstraintError/ConstraintError';
import { ConstraintOk } from './ConstraintOk/ConstraintOk';
interface IConstraintExecutionProps {
constraints?: PlaygroundConstraintSchema[];
compact: boolean;
input?: PlaygroundRequestSchema;
}
@ -23,7 +24,6 @@ export const ConstraintExecutionWrapper = styled('div')(() => ({
export const ConstraintExecution: VFC<IConstraintExecutionProps> = ({
constraints,
compact,
input,
}) => {
if (!constraints) return null;
@ -33,16 +33,24 @@ export const ConstraintExecution: VFC<IConstraintExecutionProps> = ({
{constraints?.map((constraint, index) => (
<Fragment key={objectId(constraint)}>
<ConditionallyRender
condition={index > 0 && constraints?.length > 1}
condition={index > 0}
show={<StrategySeparator text="AND" />}
/>
<ConstraintAccordionView
constraint={constraint}
playgroundInput={input}
maxLength={compact ? 25 : 50}
sx={{
backgroundColor: 'transparent!important',
}}
compact
renderAfter={
<ConditionallyRender
condition={constraint.result}
show={<ConstraintOk />}
elseShow={
<ConstraintError
input={input}
constraint={constraint}
/>
}
/>
}
/>
</Fragment>
))}

View File

@ -0,0 +1,24 @@
import { CheckCircleOutline } from '@mui/icons-material';
import { styled, Typography } from '@mui/material';
const StyledCheckOutline = styled(CheckCircleOutline)(({ theme }) => ({
color: theme.palette.success.main,
}));
const StyledConstraintOKDiv = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
marginTop: theme.spacing(1),
color: theme.palette.success.main,
}));
export const ConstraintOk = () => {
return (
<StyledConstraintOKDiv>
<StyledCheckOutline style={{ marginRight: '0.25rem' }} />
<Typography variant="body2">
Constraint met by value in context
</Typography>
</StyledConstraintOKDiv>
);
};

View File

@ -1,118 +1,77 @@
import { VFC } from 'react';
import { Fragment, VFC } from 'react';
import {
PlaygroundSegmentSchema,
PlaygroundRequestSchema,
} from 'component/playground/Playground/interfaces/playground.model';
import { ConstraintExecution } from '../ConstraintExecution/ConstraintExecution';
import { CancelOutlined, DonutLarge } from '@mui/icons-material';
import { Link } from 'react-router-dom';
import { CancelOutlined } from '@mui/icons-material';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
import { useStyles } from './SegmentExecution.styles';
import { styled, Typography } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { SegmentItem } from 'component/common/SegmentItem/SegmentItem';
interface ISegmentExecutionProps {
segments?: PlaygroundSegmentSchema[];
input?: PlaygroundRequestSchema;
hasConstraints: boolean;
}
const SegmentExecutionLinkWrapper = styled('div')(({ theme }) => ({
padding: theme.spacing(2, 3),
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
fontSize: theme.fontSizes.smallBody,
position: 'relative',
}));
const SegmentExecutionHeader = styled('div')(({ theme }) => ({
width: '100%',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'space-between',
'& + &': {
margin: theme.spacing(2),
},
}));
const SegmentExecutionWrapper = styled('div')(({ theme }) => ({
flexDirection: 'column',
borderRadius: theme.shape.borderRadiusMedium,
border: `1px solid ${theme.palette.dividerAlternative}`,
'& + &': {
marginTop: theme.spacing(2),
},
background: theme.palette.neutral.light,
marginBottom: theme.spacing(1),
}));
const SegmentExecutionConstraintWrapper = styled('div')(() => ({
padding: '12px',
}));
const SegmentResultTextWrapper = styled('div')(({ theme }) => ({
color: theme.palette.error.main,
display: 'inline-flex',
justifyContent: 'center',
marginRight: '12px',
marginLeft: 'auto',
gap: theme.spacing(1),
}));
export const SegmentExecution: VFC<ISegmentExecutionProps> = ({
segments,
input,
hasConstraints,
}) => {
const { classes: styles } = useStyles();
if (!segments) return null;
return (
<>
{segments.map((segment, index) => (
<SegmentExecutionWrapper key={segment.id}>
<SegmentExecutionHeader>
<SegmentExecutionLinkWrapper>
<DonutLarge color="secondary" sx={{ mr: 1 }} />{' '}
Segment:{' '}
<Link
to={`/segments/edit/${segment.id}`}
className={styles.link}
>
{segment.name}
</Link>
</SegmentExecutionLinkWrapper>
<ConditionallyRender
condition={!Boolean(segment.result)}
show={
<SegmentResultTextWrapper>
<Typography
variant={'subtitle2'}
sx={{ pt: 0.25 }}
>
segment is false
</Typography>
<span>
<CancelOutlined />
</span>
</SegmentResultTextWrapper>
}
/>
</SegmentExecutionHeader>
<SegmentExecutionConstraintWrapper>
<ConstraintExecution
constraints={segment.constraints}
input={input}
compact
/>
</SegmentExecutionConstraintWrapper>
<Fragment key={segment.id}>
<SegmentItem
segment={segment}
constraintList={
<ConstraintExecution
constraints={segment.constraints}
input={input}
/>
}
headerContent={
<ConditionallyRender
condition={!Boolean(segment.result)}
show={
<SegmentResultTextWrapper>
<Typography
variant={'subtitle2'}
sx={{ pt: 0.25 }}
>
segment is false
</Typography>
<span>
<CancelOutlined />
</span>
</SegmentResultTextWrapper>
}
/>
}
isExpanded
/>
<ConditionallyRender
condition={
index === segments?.length - 1 && hasConstraints
// Add IF there is a next segment
index >= 0 &&
segments.length > 1 &&
// Don't add if it's the last segment item
index !== segments.length - 1
}
show={<StrategySeparator text="AND" />}
/>
</SegmentExecutionWrapper>
</Fragment>
))}
</>
);

View File

@ -1,4 +1,4 @@
import { VFC } from 'react';
import { Fragment, VFC } from 'react';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
import { Box, Chip, styled } from '@mui/material';
@ -12,6 +12,7 @@ import { ConstraintExecution } from './ConstraintExecution/ConstraintExecution';
import { SegmentExecution } from './SegmentExecution/SegmentExecution';
import { PlaygroundResultStrategyExecutionParameters } from './StrategyExecutionParameters/StrategyExecutionParameters';
import { CustomStrategyParams } from './CustomStrategyParams/CustomStrategyParams';
import { formattedStrategyNames } from 'utils/strategyNames';
interface IStrategyExecutionProps {
strategyResult: PlaygroundStrategySchema;
@ -32,72 +33,60 @@ export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
const { uiConfig } = useUiConfig();
const { classes: styles } = useStyles();
const hasSegments =
Boolean(uiConfig.flags.SE) && Boolean(segments && segments.length > 0);
const hasConstraints = Boolean(constraints && constraints?.length > 0);
const hasParameters = Object.keys(parameters).length === 0;
const hasExecutionParameters =
name !== 'default' &&
Object.keys(formattedStrategyNames).includes(name);
const hasCustomStrategyParameters =
Object.keys(parameters).length > 0 &&
strategyResult.result.evaluationStatus === 'incomplete'; // Use of custom strategy can be more explicit from the API
if (!parameters) {
return null;
}
return (
<StyledStrategyExecutionWrapper>
<ConditionallyRender
condition={
Boolean(uiConfig.flags.SE) &&
Boolean(segments && segments.length > 0)
}
show={
<SegmentExecution
segments={segments}
hasConstraints={hasConstraints}
input={input}
/>
}
/>
<ConditionallyRender
condition={Boolean(constraints && constraints.length > 0)}
show={
<>
<ConstraintExecution
constraints={constraints}
compact={true}
input={input}
/>
<ConditionallyRender
condition={Boolean(
constraints &&
constraints.length > 0 &&
!hasParameters
)}
show={<StrategySeparator text="AND" />}
/>
</>
}
/>
<ConditionallyRender
condition={name === 'default'}
show={
<Box sx={{ width: '100%' }} className={styles.summary}>
The standard strategyResult is{' '}
<Chip
variant="outlined"
size="small"
color="success"
label="ON"
/>{' '}
for all users.
</Box>
}
/>
const items = [
hasSegments && <SegmentExecution segments={segments} input={input} />,
hasConstraints && (
<ConstraintExecution constraints={constraints} input={input} />
),
hasExecutionParameters && (
<PlaygroundResultStrategyExecutionParameters
parameters={parameters}
constraints={constraints}
input={input}
/>
<CustomStrategyParams
strategyName={strategyResult.name}
parameters={parameters}
/>
),
hasCustomStrategyParameters && (
<CustomStrategyParams strategyName={name} parameters={parameters} />
),
name === 'default' && (
<Box sx={{ width: '100%' }} className={styles.summary}>
The standard strategy is{' '}
<Chip
variant="outlined"
size="small"
color="success"
label="ON"
/>{' '}
for all users.
</Box>
),
].filter(Boolean);
return (
<StyledStrategyExecutionWrapper>
{items.map((item, index) => (
<Fragment key={index}>
<ConditionallyRender
condition={index > 0}
show={<StrategySeparator text="AND" />}
/>
{item}
</Fragment>
))}
</StyledStrategyExecutionWrapper>
);
};

View File

@ -25,102 +25,117 @@ export const PlaygroundResultStrategyExecutionParameters = ({
input,
}: PlaygroundResultStrategyExecutionParametersProps) => {
const { classes: styles } = useStyles();
const renderParameters = () => {
return Object.keys(parameters).map(key => {
switch (key) {
case 'rollout':
case 'Rollout':
const percentage = parseParameterNumber(parameters[key]);
return (
<Box
className={styles.summary}
key={key}
sx={{ display: 'flex', alignItems: 'center' }}
>
<Box sx={{ mr: '1rem' }}>
<PercentageCircle
percentage={percentage}
size="2rem"
/>
</Box>
<div>
<Chip
color="success"
variant="outlined"
size="small"
label={`${percentage}%`}
/>{' '}
of your base{' '}
{constraints.length > 0
? 'who match constraints'
: ''}{' '}
is included.
</div>
</Box>
);
case 'userIds':
case 'UserIds':
const users = parseParameterStrings(parameters[key]);
return (
<PlaygroundParameterItem
key={key}
value={users}
text="user"
input={
Boolean(input?.context?.[getMappedParam(key)])
? input?.context?.[getMappedParam(key)]
: 'no value'
}
showReason={
Boolean(input?.context?.[getMappedParam(key)])
? !users.includes(
input?.context?.[getMappedParam(key)]
)
: undefined
}
/>
);
case 'hostNames':
case 'HostNames':
const hosts = parseParameterStrings(parameters[key]);
return (
<PlaygroundParameterItem
key={key}
value={hosts}
text={'host'}
input={'no value'}
showReason={undefined}
/>
);
case 'IPs':
const IPs = parseParameterStrings(parameters[key]);
return (
<PlaygroundParameterItem
key={key}
value={IPs}
text={'IP'}
input={
Boolean(input?.context?.[getMappedParam(key)])
? input?.context?.[getMappedParam(key)]
: 'no value'
}
showReason={
Boolean(input?.context?.[getMappedParam(key)])
? !IPs.includes(
input?.context?.[getMappedParam(key)]
)
: undefined
}
/>
);
case 'stickiness':
case 'groupId':
return null;
default:
return null;
}
});
};
return <>{renderParameters()}</>;
return (
<>
{Object.keys(parameters).map(key => {
switch (key) {
case 'rollout':
case 'Rollout':
const percentage = parseParameterNumber(
parameters[key]
);
return (
<Box
className={styles.summary}
key={key}
sx={{ display: 'flex', alignItems: 'center' }}
>
<Box sx={{ mr: '1rem' }}>
<PercentageCircle
percentage={percentage}
size="2rem"
/>
</Box>
<div>
<Chip
color="success"
variant="outlined"
size="small"
label={`${percentage}%`}
/>{' '}
of your base{' '}
{constraints.length > 0
? 'who match constraints'
: ''}{' '}
is included.
</div>
</Box>
);
case 'userIds':
case 'UserIds':
const users = parseParameterStrings(parameters[key]);
return (
<PlaygroundParameterItem
key={key}
value={users}
text="user"
input={
Boolean(
input?.context?.[getMappedParam(key)]
)
? input?.context?.[getMappedParam(key)]
: 'no value'
}
showReason={
Boolean(
input?.context?.[getMappedParam(key)]
)
? !users.includes(
input?.context?.[
getMappedParam(key)
]
)
: undefined
}
/>
);
case 'hostNames':
case 'HostNames':
const hosts = parseParameterStrings(parameters[key]);
return (
<PlaygroundParameterItem
key={key}
value={hosts}
text={'host'}
input={'no value'}
showReason={undefined}
/>
);
case 'IPs':
const IPs = parseParameterStrings(parameters[key]);
return (
<PlaygroundParameterItem
key={key}
value={IPs}
text={'IP'}
input={
Boolean(
input?.context?.[getMappedParam(key)]
)
? input?.context?.[getMappedParam(key)]
: 'no value'
}
showReason={
Boolean(
input?.context?.[getMappedParam(key)]
)
? !IPs.includes(
input?.context?.[
getMappedParam(key)
]
)
: undefined
}
/>
);
case 'stickiness':
case 'groupId':
return null;
default:
return null;
}
})}
</>
);
};

View File

@ -22,10 +22,10 @@ export const FeatureStatusCell = ({ feature }: IFeatureStatusCellProps) => {
if (feature?.isEnabled) {
return [true, 'True'];
}
if (feature?.strategies?.result === false) {
return [false, 'False'];
if (feature?.strategies?.result === 'unknown') {
return ['unknown', 'Unknown'];
}
return ['unknown', 'Unknown'];
return [false, 'False'];
})();
return (

View File

@ -101,14 +101,19 @@ export const PlaygroundResultsTable = ({
),
},
{
id: 'isEnabled',
Header: 'isEnabled',
accessor: 'isEnabled',
filterName: 'isEnabled',
filterParsing: (value: boolean) => (value ? 'true' : 'false'),
accessor: (row: PlaygroundFeatureSchema) =>
row?.isEnabled
? 'true'
: row?.strategies?.result === 'unknown'
? 'unknown'
: 'false',
Cell: ({ row }: any) => (
<FeatureStatusCell feature={row.original} />
),
sortType: 'boolean',
sortType: 'playgroundResultState',
maxWidth: 120,
sortInverted: true,
},

View File

@ -53,8 +53,7 @@ export const VariantCell: VFC<IVariantCellProps> = ({
id={`${feature}-result-variants`}
PaperProps={{
sx: {
borderRadius:
theme.shape.borderRadiusExtraLarge,
borderRadius: `${theme.shape.borderRadiusLarge}px`,
},
}}
onClose={onClose}

View File

@ -18,4 +18,14 @@ export const sortTypes = {
(a?.values?.[id] || '')
?.toLowerCase()
.localeCompare(b?.values?.[id]?.toLowerCase() || ''),
playgroundResultState: (v1: any, v2: any, id: string) => {
const a = v1?.values?.[id];
const b = v2?.values?.[id];
if (a === b) return 0;
if (a === 'true') return 1;
if (b === 'true') return -1;
if (a === 'false') return -1;
if (b === 'false') return 1;
return 0;
},
};

View File

@ -36,7 +36,7 @@ export const getFeatureStrategyIcon = (strategyName: string): ElementType => {
}
};
const formattedStrategyNames: Record<string, string> = {
export const formattedStrategyNames: Record<string, string> = {
applicationHostname: 'Hosts',
default: 'Standard',
flexibleRollout: 'Gradual rollout',