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

Playground result info structure and initial styles

This commit is contained in:
andreas-unleash 2022-07-28 19:23:38 +03:00
parent cdaf7299be
commit 352a4cca13
13 changed files with 331 additions and 154 deletions

View File

@ -16,9 +16,10 @@ import {
parseParameterNumber, parseParameterNumber,
parseParameterStrings, parseParameterStrings,
} from 'utils/parseParameter'; } from 'utils/parseParameter';
import { PlaygroundFeatureStrategyResult } from 'hooks/api/actions/usePlayground/playground.model';
interface IStrategyExecutionProps { interface IStrategyExecutionProps {
strategy: IFeatureStrategy; strategy: IFeatureStrategy | PlaygroundFeatureStrategyResult;
percentageFill?: string; percentageFill?: string;
} }

View File

@ -1,7 +1,7 @@
import { DragIndicator, Edit } from '@mui/icons-material'; import { DragIndicator, Edit } from '@mui/icons-material';
import { styled, useTheme, IconButton, Chip } from '@mui/material'; import { styled, useTheme, IconButton } from '@mui/material';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import {IFeatureStrategy, IPlaygroundFeatureStrategyResult} from 'interfaces/strategy'; import { IFeatureStrategy } from 'interfaces/strategy';
import { import {
getFeatureStrategyIcon, getFeatureStrategyIcon,
formatStrategyName, formatStrategyName,
@ -18,10 +18,8 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
interface IStrategyItemProps { interface IStrategyItemProps {
environmentId: string; environmentId: string;
strategy: IFeatureStrategy | IPlaygroundFeatureStrategyResult; strategy: IFeatureStrategy;
isDraggable?: boolean; isDraggable?: boolean;
showActions?: boolean;
result?: boolean;
} }
const DragIcon = styled(IconButton)(({ theme }) => ({ const DragIcon = styled(IconButton)(({ theme }) => ({
@ -34,8 +32,6 @@ export const StrategyItem = ({
environmentId, environmentId,
strategy, strategy,
isDraggable, isDraggable,
showActions = true,
result,
}: IStrategyItemProps) => { }: IStrategyItemProps) => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId'); const featureId = useRequiredPathParam('featureId');
@ -50,8 +46,6 @@ export const StrategyItem = ({
strategy.id strategy.id
); );
const showShouldShowResultChip = result !== undefined;
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.header}> <div className={styles.header}>
@ -72,36 +66,25 @@ export const StrategyItem = ({
maxLength={15} maxLength={15}
text={formatStrategyName(strategy.name)} text={formatStrategyName(strategy.name)}
/> />
<ConditionallyRender <div className={styles.actions}>
condition={showActions} <PermissionIconButton
show={ permission={UPDATE_FEATURE_STRATEGY}
<div className={styles.actions}> environmentId={environmentId}
<PermissionIconButton projectId={projectId}
permission={UPDATE_FEATURE_STRATEGY} component={Link}
environmentId={environmentId} to={editStrategyPath}
projectId={projectId} tooltipProps={{ title: 'Edit strategy' }}
component={Link} >
to={editStrategyPath} <Edit />
tooltipProps={{ title: 'Edit strategy' }} </PermissionIconButton>
> <FeatureStrategyRemove
<Edit /> projectId={projectId}
</PermissionIconButton> featureId={featureId}
<FeatureStrategyRemove environmentId={environmentId}
projectId={projectId} strategyId={strategy.id}
featureId={featureId} icon
environmentId={environmentId} />
strategyId={strategy.id} </div>
icon
/>
</div>
}
/>
<ConditionallyRender
condition={showShouldShowResultChip}
show={
<Featur>
}
/>
</div> </div>
<div className={styles.body}> <div className={styles.body}>
<StrategyExecution <StrategyExecution

View File

@ -0,0 +1,14 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
popoverPaper: {
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
padding: '48px',
gap: '24px',
width: '728px',
height: 'auto',
// overflowY: 'scroll',
},
}));

View File

@ -1,8 +1,17 @@
import { PlaygroundFeatureSchema } from '../../../../../hooks/api/actions/usePlayground/playground.model'; import {
import { Box, IconButton, Popover } from '@mui/material'; PlaygroundFeatureSchema,
PlaygroundFeatureStrategyResult,
} from '../../../../../hooks/api/actions/usePlayground/playground.model';
import { Box, IconButton, Popover, 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 { 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 { StrategyDraggableItem } from '../../../../feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem';
import { FeatureStrategyEmpty } from '../../../../feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty';
import { PlaygroundResultFeatureStrategyItem } from './PlaygroundResultFeatureStrategyItem/PlaygroundResultFeatureStrategyItem';
import { useStyles } from './FeatureResultInfoPopoverCell.styles';
import { PlaygroundResultFeatureDetails } from './PlaygroundResultFeatureDetails/PlaygroundResultFeatureDetails';
interface FeatureResultInfoPopoverCellProps { interface FeatureResultInfoPopoverCellProps {
feature?: PlaygroundFeatureSchema; feature?: PlaygroundFeatureSchema;
@ -15,16 +24,18 @@ export const FeatureResultInfoPopoverCell = ({
return null; return null;
} }
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { classes: styles } = useStyles();
const ref = useRef(null); const ref = useRef(null);
const togglePopover = (event: React.SyntheticEvent) => { const togglePopover = (event: React.SyntheticEvent) => {
setOpen(!open); setOpen(!open);
}; };
const strategies = [ const strategies: PlaygroundFeatureStrategyResult[] = [
{ {
type: 'standard', name: 'default',
id: 'strategy-id', id: 'strategy-id',
parameters: {},
result: false, result: false,
constraints: [ constraints: [
{ {
@ -54,10 +65,6 @@ export const FeatureResultInfoPopoverCell = ({
}, },
], ],
}, },
{
type: 'default',
result: true,
},
]; ];
return ( return (
@ -77,8 +84,26 @@ export const FeatureResultInfoPopoverCell = ({
vertical: 'center', vertical: 'center',
horizontal: 'left', horizontal: 'left',
}} }}
classes={{ paper: styles.popoverPaper }}
> >
{feature.name} <PlaygroundResultFeatureDetails feature={feature} />
<ConditionallyRender
condition={strategies.length > 0}
show={
<>
<Typography
variant={'subtitle2'}
>{`Strategies (${strategies.length})`}</Typography>
{strategies.map((strategy, index) => (
<PlaygroundResultFeatureStrategyItem
key={strategy.id}
strategy={strategy}
index={index}
/>
))}
</>
}
/>
</Popover> </Popover>
</> </>
); );

View File

@ -0,0 +1,21 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
titleRow: {
display: 'inline-flex',
alignItems: 'flex-start',
justifyItems: 'center',
gap: '12px',
},
descriptionRow: {
flexDirection: 'row',
display: 'inline-flex',
alignItems: 'flex-start',
justifyItems: 'center',
gap: '6px',
},
name: {
fontWeight: 600,
padding: '4px',
},
}));

View File

@ -0,0 +1,39 @@
import { PlaygroundFeatureSchema } from '../../../../../../hooks/api/actions/usePlayground/playground.model';
import { Typography } from '@mui/material';
import { PlaygroundResultChip } from '../../PlaygroundResultChip/PlaygroundResultChip';
import { useStyles } from './PlaygroundResultFeatureDetails.styles';
interface PlaygroundFeatureResultDetailsProps {
feature: PlaygroundFeatureSchema;
}
export const PlaygroundResultFeatureDetails = ({
feature,
}: PlaygroundFeatureResultDetailsProps) => {
const { classes: styles } = useStyles();
const description = feature.isEnabled
? 'This feature toggle is True in production because '
: 'This feature toggle is False in production because ';
const reason = feature.isEnabled
? 'at least one strategy is True'
: 'all strategies are False';
const color = feature.isEnabled ? 'success' : 'error';
return (
<>
<div className={styles.titleRow}>
<Typography variant={'subtitle1'} className={styles.name}>
{feature.name}
</Typography>
<span>
<PlaygroundResultChip enabled={feature.isEnabled} />
</span>
</div>
<div className={styles.descriptionRow}>
<Typography variant={'body1'}>{description}</Typography>
<Typography variant={'body1'} color={color}>
{reason}
</Typography>
</div>
</>
);
};

View File

@ -0,0 +1,32 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
header: {
display: 'flex',
padding: theme.spacing(2, 2),
justifyContent: 'space-between',
},
headerName: {
padding: theme.spacing(0.5, 2),
display: 'flex',
gap: theme.spacing(1),
alignItems: 'center',
borderBottom: `1px solid ${theme.palette.divider}`,
fontWeight: theme.typography.fontWeightMedium,
},
icon: {
fill: theme.palette.inactiveIcon,
},
resultChip: {
marginLeft: 'auto',
},
body: {
padding: theme.spacing(2),
justifyItems: 'center',
},
innerContainer: {
[theme.breakpoints.down(400)]: {
padding: '0.5rem',
},
},
}));

View File

@ -1,38 +1,75 @@
import { ConditionallyRender } from '../../../../../common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from '../../../../../common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from '../../../../../common/StrategySeparator/StrategySeparator'; import { StrategySeparator } from '../../../../../common/StrategySeparator/StrategySeparator';
import { StrategyItem } from '../../../../../feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem'; import { styled, useTheme } from '@mui/material';
import { useStyles } from './PlaygroundResultFeatureStrategyItem.styles';
import { import {
IConstraint, formatStrategyName,
IFeatureStrategy, IPlaygroundFeatureStrategyResult, getFeatureStrategyIcon,
} from '../../../../../../interfaces/strategy'; } from '../../../../../../utils/strategyNames';
import { ISegment } from '../../../../../../interfaces/segment'; import StringTruncator from '../../../../../common/StringTruncator/StringTruncator';
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';
interface IPlaygroundResultFeatureStrategyItemProps { interface IPlaygroundResultFeatureStrategyItemProps {
strategy: IPlaygroundFeatureStrategyResult; strategy: PlaygroundFeatureStrategyResult;
environmentName: string;
index: number; index: number;
} }
const StyledStrategyResultBox = styled('div')(({ theme }) => ({
width: '100%',
position: 'relative',
paddingBottom: '1rem',
borderRadius: theme.shape.borderRadiusMedium,
'& + &': {
marginTop: theme.spacing(2),
},
background: theme.palette.background.default,
}));
export const PlaygroundResultFeatureStrategyItem = ({ export const PlaygroundResultFeatureStrategyItem = ({
strategy, strategy,
environmentName,
index, index,
}: IPlaygroundResultFeatureStrategyItemProps) => { }: IPlaygroundResultFeatureStrategyItemProps) => {
const { result } = strategy; const { result } = strategy;
const { classes: styles } = useStyles();
const theme = useTheme();
const Icon = getFeatureStrategyIcon(strategy.name);
const label =
result === undefined ? 'Not found' : result ? 'True' : 'False';
const border = Boolean(result)
? `2px solid ${theme.palette.success.main}`
: `1px solid ${theme.palette.divider}`;
return ( return (
<div key={strategy.id} className={``}> <StyledStrategyResultBox key={strategy.id} sx={{ border }}>
<ConditionallyRender <ConditionallyRender
condition={index > 0} condition={index > 0}
show={<StrategySeparator text="OR" />} show={<StrategySeparator text="OR" />}
/> />
<StrategyItem <div className={styles.innerContainer}>
strategy={strategy} <div className={styles.header}>
result={result} <div className={styles.headerName}>
environmentId={environmentName} <Icon className={styles.icon} />
isDraggable={false} <StringTruncator
/> maxWidth="150"
</div> maxLength={15}
text={formatStrategyName(strategy.name)}
/>
</div>
<PlaygroundResultChip
showIcon={false}
enabled={Boolean(result)}
label={label}
/>
</div>
<div className={styles.body}>
<StrategyExecution
strategy={strategy}
percentageFill={theme.palette.grey[200]}
/>
</div>
</div>
</StyledStrategyResultBox>
); );
}; };

View File

@ -4,7 +4,7 @@ import { ReactComponent as FeatureEnabledIcon } from 'assets/icons/isenabled-tru
import { ReactComponent as FeatureDisabledIcon } from 'assets/icons/isenabled-false.svg'; import { ReactComponent as FeatureDisabledIcon } from 'assets/icons/isenabled-false.svg';
import { Box, Chip, styled, useTheme } from '@mui/material'; import { Box, Chip, styled, useTheme } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import {ResultChip} from "../ResultChip/ResultChip"; import { PlaygroundResultChip } from '../PlaygroundResultChip/PlaygroundResultChip';
interface IFeatureStatusCellProps { interface IFeatureStatusCellProps {
enabled: boolean; enabled: boolean;
@ -21,31 +21,10 @@ const StyledChipWrapper = styled(Box)(() => ({
})); }));
export const FeatureStatusCell = ({ enabled }: IFeatureStatusCellProps) => { export const FeatureStatusCell = ({ enabled }: IFeatureStatusCellProps) => {
const theme = useTheme();
const icon = (
<ConditionallyRender
condition={enabled}
show={
<FeatureEnabledIcon
color={theme.palette.success.main}
strokeWidth="0.25"
/>
}
elseShow={
<FeatureDisabledIcon
color={theme.palette.error.main}
strokeWidth="0.25"
/>
}
/>
);
const label = enabled ? 'True' : 'False';
return ( return (
<StyledCellBox> <StyledCellBox>
<StyledChipWrapper data-loading> <StyledChipWrapper data-loading>
<ResultChip enabled label={label} icon={icon} /> <PlaygroundResultChip enabled />
</StyledChipWrapper> </StyledChipWrapper>
</StyledCellBox> </StyledCellBox>
); );

View File

@ -0,0 +1,84 @@
import { Chip, styled, useTheme } from '@mui/material';
import { colors } from '../../../../../themes/colors';
import { ConditionallyRender } from '../../../../common/ConditionallyRender/ConditionallyRender';
import React from 'react';
import { ReactComponent as FeatureEnabledIcon } from '../../../../../assets/icons/isenabled-true.svg';
import { ReactComponent as FeatureDisabledIcon } from '../../../../../assets/icons/isenabled-false.svg';
interface IResultChipProps {
enabled: boolean;
// Result icon - defaults to true
showIcon?: boolean;
label?: string;
}
export const StyledFalseChip = styled(Chip)(({ theme }) => ({
width: 80,
borderRadius: '5px',
border: `1px solid ${theme.palette.error.main}`,
backgroundColor: colors.red['200'],
['& .MuiChip-label']: {
color: theme.palette.error.main,
},
['& .MuiChip-icon']: {
color: theme.palette.error.main,
},
}));
export const StyledTrueChip = styled(Chip)(({ theme }) => ({
width: 80,
borderRadius: '5px',
border: `1px solid ${theme.palette.success.main}`,
backgroundColor: colors.green['100'],
['& .MuiChip-label']: {
color: theme.palette.success.main,
},
['& .MuiChip-icon']: {
color: theme.palette.success.main,
},
}));
export const PlaygroundResultChip = ({
enabled,
showIcon = true,
label,
}: IResultChipProps) => {
const theme = useTheme();
const icon = (
<ConditionallyRender
condition={enabled}
show={
<FeatureEnabledIcon
color={theme.palette.success.main}
strokeWidth="0.25"
/>
}
elseShow={
<FeatureDisabledIcon
color={theme.palette.error.main}
strokeWidth="0.25"
/>
}
/>
);
const defaultLabel = enabled ? 'True' : 'False';
return (
<ConditionallyRender
condition={enabled}
show={
<StyledTrueChip
icon={showIcon ? icon : undefined}
label={label || defaultLabel}
/>
}
elseShow={
<StyledFalseChip
icon={showIcon ? icon : undefined}
label={label || defaultLabel}
/>
}
/>
);
};

View File

@ -1,46 +0,0 @@
import {Box, Chip, styled} from "@mui/material";
import {colors} from "../../../../../themes/colors";
import {ConditionallyRender} from "../../../../common/ConditionallyRender/ConditionallyRender";
import React, {ReactElement} from "react";
interface IResultChipProps {
enabled: boolean;
icon?: ReactElement;
label?: string;
}
export const StyledFalseChip = styled(Chip)(({ theme }) => ({
width: 80,
borderRadius: '5px',
border: `1px solid ${theme.palette.error.main}`,
backgroundColor: colors.red['200'],
['& .MuiChip-label']: {
color: theme.palette.error.main,
},
['& .MuiChip-icon']: {
color: theme.palette.error.main,
},
}));
export const StyledTrueChip = styled(Chip)(({ theme }) => ({
width: 80,
borderRadius: '5px',
border: `1px solid ${theme.palette.success.main}`,
backgroundColor: colors.green['100'],
['& .MuiChip-label']: {
color: theme.palette.success.main,
},
['& .MuiChip-icon']: {
color: theme.palette.success.main,
},
}));
export const ResultChip = ({ enabled, icon, label}: IResultChipProps) => {
return (
<ConditionallyRender
condition={enabled}
show={<StyledTrueChip icon={Boolean(icon) ? icon : undefined} label={label}/>}
elseShow={<StyledFalseChip icon={Boolean(icon) ? icon : undefined} label={label}/>}
/>
);
}

View File

@ -1,5 +1,10 @@
// TODO: replace with auto-generated openapi code // TODO: replace with auto-generated openapi code
import {
IConstraint,
IFeatureStrategyParameters,
} from '../../../../interfaces/strategy';
export enum PlaygroundFeatureSchemaVariantPayloadTypeEnum { export enum PlaygroundFeatureSchemaVariantPayloadTypeEnum {
Json = 'json', Json = 'json',
Csv = 'csv', Csv = 'csv',
@ -158,7 +163,7 @@ export interface PlaygroundFeatureSchema {
* @type {boolean} * @type {boolean}
* @memberof PlaygroundFeatureSchema * @memberof PlaygroundFeatureSchema
*/ */
isEnabled: boolean; enabled: boolean | 'unevaluated';
/** /**
* *
* @type {PlaygroundFeatureSchemaVariant} * @type {PlaygroundFeatureSchemaVariant}
@ -248,3 +253,24 @@ export interface SdkContextSchema {
*/ */
userId?: string; userId?: string;
} }
export interface PlaygroundFeatureStrategyConstraintResult extends IConstraint {
result: boolean;
}
export interface PlaygroundFeatureStrategySegmentResult {
id: number;
name: string;
result: boolean;
constraints?: PlaygroundFeatureStrategyConstraintResult[];
}
export interface PlaygroundFeatureStrategyResult {
id: string;
name: string;
result: boolean | 'not found';
type?: string;
constraints?: PlaygroundFeatureStrategyConstraintResult[];
segments?: PlaygroundFeatureStrategySegmentResult[];
parameters: IFeatureStrategyParameters;
}

View File

@ -1,5 +1,4 @@
import { Operator } from 'constants/operators'; import { Operator } from 'constants/operators';
import {ISegment} from "./segment";
export interface IFeatureStrategy { export interface IFeatureStrategy {
id: string; id: string;
@ -57,20 +56,3 @@ export interface IFeatureStrategySortOrder {
id: string; id: string;
sortOrder: number; sortOrder: number;
} }
export interface IPlaygroundFeatureStrategyConstraintResult extends IConstraint {
result: boolean;
}
export interface IPlaygroundFeatureStrategySegmentResult extends ISegment {
result: boolean;
}
export interface IPlaygroundFeatureStrategyResult {
type: string;
result: boolean;
id?: string;
constraints?: IPlaygroundFeatureStrategyConstraintResult[];
segments?: IPlaygroundFeatureStrategySegmentResult[];
}