1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-09 01:17:06 +02:00

Playground result segments, constraints initial

This commit is contained in:
andreas-unleash 2022-07-29 08:52:22 +03:00
parent 352a4cca13
commit 9e38cf3ff9
12 changed files with 275 additions and 33 deletions

View File

@ -1,5 +1,11 @@
import { useState } from 'react'; import { useState } from 'react';
import { Accordion, AccordionSummary, AccordionDetails } from '@mui/material'; import {
Accordion,
AccordionSummary,
AccordionDetails,
SxProps,
Theme,
} from '@mui/material';
import { IConstraint } from 'interfaces/strategy'; import { IConstraint } from 'interfaces/strategy';
import { ConstraintAccordionViewBody } from './ConstraintAccordionViewBody/ConstraintAccordionViewBody'; import { ConstraintAccordionViewBody } from './ConstraintAccordionViewBody/ConstraintAccordionViewBody';
import { ConstraintAccordionViewHeader } from './ConstraintAccordionViewHeader/ConstraintAccordionViewHeader'; import { ConstraintAccordionViewHeader } from './ConstraintAccordionViewHeader/ConstraintAccordionViewHeader';
@ -15,12 +21,14 @@ interface IConstraintAccordionViewProps {
constraint: IConstraint; constraint: IConstraint;
onDelete?: () => void; onDelete?: () => void;
onEdit?: () => void; onEdit?: () => void;
sx?: SxProps<Theme>;
} }
export const ConstraintAccordionView = ({ export const ConstraintAccordionView = ({
constraint, constraint,
onEdit, onEdit,
onDelete, onDelete,
sx = undefined,
}: IConstraintAccordionViewProps) => { }: IConstraintAccordionViewProps) => {
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
const [expandable, setExpandable] = useState(true); const [expandable, setExpandable] = useState(true);
@ -42,6 +50,7 @@ export const ConstraintAccordionView = ({
className={styles.accordion} className={styles.accordion}
classes={{ root: styles.accordionRoot }} classes={{ root: styles.accordionRoot }}
expanded={expanded} expanded={expanded}
sx={sx}
> >
<AccordionSummary <AccordionSummary
classes={{ root: styles.summary }} classes={{ root: styles.summary }}

View File

@ -1,8 +1,9 @@
import { styled } from '@mui/material'; import { styled, SxProps, Theme } from '@mui/material';
import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
interface IStrategySeparatorProps { interface IStrategySeparatorProps {
text: 'AND' | 'OR'; text: 'AND' | 'OR';
sx?: SxProps<Theme>;
} }
const StyledContainer = styled('div')(({ theme }) => ({ const StyledContainer = styled('div')(({ theme }) => ({
@ -32,8 +33,8 @@ const StyledCenteredContent = styled(StyledContent)(({ theme }) => ({
border: `1px solid ${theme.palette.primary.border}`, border: `1px solid ${theme.palette.primary.border}`,
})); }));
export const StrategySeparator = ({ text }: IStrategySeparatorProps) => ( export const StrategySeparator = ({ text, sx }: IStrategySeparatorProps) => (
<StyledContainer> <StyledContainer sx={sx}>
<ConditionallyRender <ConditionallyRender
condition={text === 'AND'} condition={text === 'AND'}
show={() => <StyledContent>{text}</StyledContent>} show={() => <StyledContent>{text}</StyledContent>}

View File

@ -9,6 +9,7 @@ export const useStyles = makeStyles()(theme => ({
gap: '24px', gap: '24px',
width: '728px', width: '728px',
height: 'auto', height: 'auto',
// overflowY: 'scroll', overflowY: 'scroll',
backgroundColor: theme.palette.tertiary.light,
}, },
})); }));

View File

@ -2,13 +2,10 @@ import {
PlaygroundFeatureSchema, PlaygroundFeatureSchema,
PlaygroundFeatureStrategyResult, PlaygroundFeatureStrategyResult,
} from '../../../../../hooks/api/actions/usePlayground/playground.model'; } from '../../../../../hooks/api/actions/usePlayground/playground.model';
import { Box, IconButton, Popover, Typography } from '@mui/material'; import { IconButton, Popover, styled, Typography } from '@mui/material';
import { InfoOutlined } from '@mui/icons-material'; import { InfoOutlined } from '@mui/icons-material';
import { IconCell } from '../../../../common/Table/cells/IconCell/IconCell';
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import { ConditionallyRender } from '../../../../common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from '../../../../common/ConditionallyRender/ConditionallyRender';
import { StrategyDraggableItem } from '../../../../feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem';
import { FeatureStrategyEmpty } from '../../../../feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty';
import { PlaygroundResultFeatureStrategyItem } from './PlaygroundResultFeatureStrategyItem/PlaygroundResultFeatureStrategyItem'; import { PlaygroundResultFeatureStrategyItem } from './PlaygroundResultFeatureStrategyItem/PlaygroundResultFeatureStrategyItem';
import { useStyles } from './FeatureResultInfoPopoverCell.styles'; import { useStyles } from './FeatureResultInfoPopoverCell.styles';
import { PlaygroundResultFeatureDetails } from './PlaygroundResultFeatureDetails/PlaygroundResultFeatureDetails'; import { PlaygroundResultFeatureDetails } from './PlaygroundResultFeatureDetails/PlaygroundResultFeatureDetails';
@ -17,12 +14,14 @@ interface FeatureResultInfoPopoverCellProps {
feature?: PlaygroundFeatureSchema; feature?: PlaygroundFeatureSchema;
} }
const FeatureResultPopoverWrapper = styled('div')(({ theme }) => ({
alignItems: 'flex-end',
color: theme.palette.tertiary.main,
}));
export const FeatureResultInfoPopoverCell = ({ export const FeatureResultInfoPopoverCell = ({
feature, feature,
}: FeatureResultInfoPopoverCellProps) => { }: FeatureResultInfoPopoverCellProps) => {
if (!feature) {
return null;
}
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
const ref = useRef(null); const ref = useRef(null);
@ -36,7 +35,7 @@ export const FeatureResultInfoPopoverCell = ({
name: 'default', name: 'default',
id: 'strategy-id', id: 'strategy-id',
parameters: {}, parameters: {},
result: false, result: true,
constraints: [ constraints: [
{ {
result: false, result: false,
@ -44,7 +43,12 @@ export const FeatureResultInfoPopoverCell = ({
operator: 'IN', operator: 'IN',
caseInsensitive: true, caseInsensitive: true,
inverted: false, inverted: false,
values: ['a', 'b'], values: [
'a',
'b',
'sdlghigoiahr;g',
'WOGIHwegoihwlwEGHLwgklWEGK;L',
],
}, },
], ],
segments: [ segments: [
@ -55,9 +59,9 @@ export const FeatureResultInfoPopoverCell = ({
constraints: [ constraints: [
{ {
result: false, result: false,
contextName: 'appName', contextName: 'environment',
operator: 'IN', operator: 'IN',
caseInsensitive: true, caseInsensitive: false,
inverted: false, inverted: false,
values: ['a', 'b'], values: ['a', 'b'],
}, },
@ -67,8 +71,12 @@ export const FeatureResultInfoPopoverCell = ({
}, },
]; ];
if (!feature) {
return null;
}
return ( return (
<> <FeatureResultPopoverWrapper>
<IconButton onClick={togglePopover}> <IconButton onClick={togglePopover}>
<InfoOutlined ref={ref} /> <InfoOutlined ref={ref} />
</IconButton> </IconButton>
@ -92,7 +100,7 @@ export const FeatureResultInfoPopoverCell = ({
show={ show={
<> <>
<Typography <Typography
variant={'subtitle2'} variant={'subtitle1'}
>{`Strategies (${strategies.length})`}</Typography> >{`Strategies (${strategies.length})`}</Typography>
{strategies.map((strategy, index) => ( {strategies.map((strategy, index) => (
<PlaygroundResultFeatureStrategyItem <PlaygroundResultFeatureStrategyItem
@ -105,6 +113,6 @@ export const FeatureResultInfoPopoverCell = ({
} }
/> />
</Popover> </Popover>
</> </FeatureResultPopoverWrapper>
); );
}; };

View File

@ -0,0 +1,35 @@
import { PlaygroundFeatureStrategyConstraintResult } from 'hooks/api/actions/usePlayground/playground.model';
import React, { Fragment } from 'react';
import { objectId } from '../../../../../../utils/objectId';
import { ConditionallyRender } from '../../../../../common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from '../../../../../common/StrategySeparator/StrategySeparator';
import { ConstraintAccordionView } from '../../../../../common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView';
interface PlaygroundResultConstraintExecutionProps {
constraints?: PlaygroundFeatureStrategyConstraintResult[];
}
export const PlaygroundResultConstraintExecution = ({
constraints,
}: PlaygroundResultConstraintExecutionProps) => {
if (!constraints) return null;
return (
<>
{constraints?.map((constraint, index) => (
<Fragment key={objectId(constraint)}>
<ConditionallyRender
condition={index > 0}
show={<StrategySeparator text="AND" />}
/>
<ConstraintAccordionView
constraint={constraint}
sx={{
backgroundColor: 'transparent!important',
}}
/>
</Fragment>
))}
</>
);
};

View File

@ -8,8 +8,8 @@ import {
} from '../../../../../../utils/strategyNames'; } from '../../../../../../utils/strategyNames';
import StringTruncator from '../../../../../common/StringTruncator/StringTruncator'; import StringTruncator from '../../../../../common/StringTruncator/StringTruncator';
import { PlaygroundResultChip } from '../../PlaygroundResultChip/PlaygroundResultChip'; import { PlaygroundResultChip } from '../../PlaygroundResultChip/PlaygroundResultChip';
import { StrategyExecution } from '../../../../../feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution';
import { PlaygroundFeatureStrategyResult } from 'hooks/api/actions/usePlayground/playground.model'; import { PlaygroundFeatureStrategyResult } from 'hooks/api/actions/usePlayground/playground.model';
import { PlaygroundResultStrategyExecution } from '../PlaygroundResultStrategyExecution/PlaygroundResultStrategyExecution';
interface IPlaygroundResultFeatureStrategyItemProps { interface IPlaygroundResultFeatureStrategyItemProps {
strategy: PlaygroundFeatureStrategyResult; strategy: PlaygroundFeatureStrategyResult;
@ -31,14 +31,14 @@ export const PlaygroundResultFeatureStrategyItem = ({
strategy, strategy,
index, index,
}: IPlaygroundResultFeatureStrategyItemProps) => { }: IPlaygroundResultFeatureStrategyItemProps) => {
const { result } = strategy; const { result, name } = strategy;
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
const theme = useTheme(); const theme = useTheme();
const Icon = getFeatureStrategyIcon(strategy.name); const Icon = getFeatureStrategyIcon(strategy.name);
const label = const label =
result === undefined ? 'Not found' : result ? 'True' : 'False'; result === undefined ? 'Not found' : result ? 'True' : 'False';
const border = Boolean(result) const border = Boolean(result)
? `2px solid ${theme.palette.success.main}` ? `1px solid ${theme.palette.success.main}`
: `1px solid ${theme.palette.divider}`; : `1px solid ${theme.palette.divider}`;
return ( return (
@ -54,7 +54,7 @@ export const PlaygroundResultFeatureStrategyItem = ({
<StringTruncator <StringTruncator
maxWidth="150" maxWidth="150"
maxLength={15} maxLength={15}
text={formatStrategyName(strategy.name)} text={formatStrategyName(name)}
/> />
</div> </div>
<PlaygroundResultChip <PlaygroundResultChip
@ -64,9 +64,9 @@ export const PlaygroundResultFeatureStrategyItem = ({
/> />
</div> </div>
<div className={styles.body}> <div className={styles.body}>
<StrategyExecution <PlaygroundResultStrategyExecution
strategy={strategy} strategyResult={strategy}
percentageFill={theme.palette.grey[200]} percentageFill={theme.palette.tertiary.light}
/> />
</div> </div>
</div> </div>

View File

@ -0,0 +1,12 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
container: {},
link: {
textDecoration: 'none',
marginLeft: theme.spacing(1),
'&:hover': {
textDecoration: 'underline',
},
},
}));

View File

@ -0,0 +1,103 @@
import { PlaygroundFeatureStrategySegmentResult } from '../../../../../../hooks/api/actions/usePlayground/playground.model';
import { PlaygroundResultConstraintExecution } from '../PlaygroundResultConstraintExecution/PlaygroundResultConstraintExecution';
import { CancelOutlined, DonutLarge } from '@mui/icons-material';
import { Link } from 'react-router-dom';
import { StrategySeparator } from '../../../../../common/StrategySeparator/StrategySeparator';
import { useStyles } from './PlaygroundResultSegmentExecution.styles';
import { styled, Typography } from '@mui/material';
import { ConditionallyRender } from '../../../../../common/ConditionallyRender/ConditionallyRender';
interface PlaygroundResultSegmentExecutionProps {
segments?: PlaygroundFeatureStrategySegmentResult[];
}
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: '8px',
}));
const SegmentExecutionConstraintWrapper = styled('div')(({ theme }) => ({
padding: '12px',
}));
const SegmentResultTextWrapper = styled('div')(({ theme }) => ({
color: theme.palette.error.main,
display: 'inline-flex',
justifyContent: 'center',
marginRight: '12px',
gap: '8px',
}));
export const PlaygroundResultSegmentExecution = ({
segments,
}: PlaygroundResultSegmentExecutionProps) => {
const { classes: styles } = useStyles();
if (!segments) return null;
return (
<>
{segments.map(segment => (
<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>
<PlaygroundResultConstraintExecution
constraints={segment.constraints}
/>
</SegmentExecutionConstraintWrapper>
<StrategySeparator text="AND" sx={{ pt: 1 }} />
</SegmentExecutionWrapper>
))}
</>
);
};

View File

@ -0,0 +1,18 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
valueContainer: {
display: 'flex',
alignItems: 'center',
gap: '1ch',
},
valueSeparator: {
color: theme.palette.grey[700],
},
summary: {
width: '100%',
padding: theme.spacing(2, 3),
borderRadius: theme.shape.borderRadius,
border: `1px solid ${theme.palette.divider}`,
},
}));

View File

@ -0,0 +1,61 @@
import { ConditionallyRender } from '../../../../../common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from '../../../../../common/StrategySeparator/StrategySeparator';
import { Box, Chip } from '@mui/material';
import { useStyles } from './PlaygroundResultStrategyExecution.styles';
import { PlaygroundFeatureStrategyResult } from '../../../../../../hooks/api/actions/usePlayground/playground.model';
import useUiConfig from '../../../../../../hooks/api/getters/useUiConfig/useUiConfig';
import React from 'react';
import { PlaygroundResultConstraintExecution } from '../PlaygroundResultConstraintExecution/PlaygroundResultConstraintExecution';
import { PlaygroundResultSegmentExecution } from '../PlaygroundResultSegmentExecution/PlaygroundResultSegmentExecution';
interface PlaygroundResultStrategyExecutionProps {
strategyResult: PlaygroundFeatureStrategyResult;
percentageFill?: string;
}
export const PlaygroundResultStrategyExecution = ({
strategyResult,
}: PlaygroundResultStrategyExecutionProps) => {
const { name, constraints, segments } = strategyResult;
const { uiConfig } = useUiConfig();
const { classes: styles } = useStyles();
return (
<>
<ConditionallyRender
condition={
Boolean(uiConfig.flags.SE) &&
Boolean(segments && segments.length > 0)
}
show={<PlaygroundResultSegmentExecution segments={segments} />}
/>
<ConditionallyRender
condition={Boolean(constraints && constraints.length > 0)}
show={
<>
<PlaygroundResultConstraintExecution
constraints={constraints}
/>
<StrategySeparator text="AND" />
</>
}
/>
<ConditionallyRender
condition={name === 'default'}
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>
}
/>
</>
);
};

View File

@ -1,9 +1,5 @@
import React from 'react'; import React from 'react';
import { colors } from 'themes/colors'; import { Box, styled } from '@mui/material';
import { ReactComponent as FeatureEnabledIcon } from 'assets/icons/isenabled-true.svg';
import { ReactComponent as FeatureDisabledIcon } from 'assets/icons/isenabled-false.svg';
import { Box, Chip, styled, useTheme } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { PlaygroundResultChip } from '../PlaygroundResultChip/PlaygroundResultChip'; import { PlaygroundResultChip } from '../PlaygroundResultChip/PlaygroundResultChip';
interface IFeatureStatusCellProps { interface IFeatureStatusCellProps {

View File

@ -23,8 +23,6 @@ import { PlaygroundFeatureSchema } from 'hooks/api/actions/usePlayground/playgro
import { Box, Typography, useMediaQuery, useTheme } from '@mui/material'; import { Box, Typography, useMediaQuery, useTheme } from '@mui/material';
import useLoading from 'hooks/useLoading'; import useLoading from 'hooks/useLoading';
import { VariantCell } from './VariantCell/VariantCell'; import { VariantCell } from './VariantCell/VariantCell';
import { IconCell } from '../../../common/Table/cells/IconCell/IconCell';
import { InfoOutlined } from '@mui/icons-material';
import { FeatureResultInfoPopoverCell } from './FeatureResultInfoPopoverCell/FeatureResultInfoPopoverCell'; import { FeatureResultInfoPopoverCell } from './FeatureResultInfoPopoverCell/FeatureResultInfoPopoverCell';
const defaultSort: SortingRule<string> = { id: 'name' }; const defaultSort: SortingRule<string> = { id: 'name' };