1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-21 13:47:39 +02:00

Merge pull request #1168 from Unleash/task/Add_strategy_information_to_playground_results

Task/add strategy information to playground results
This commit is contained in:
andreas-unleash 2022-08-09 18:12:47 +03:00 committed by GitHub
commit 2e8ebb1d9e
45 changed files with 2411 additions and 490 deletions

View File

@ -1,12 +1,12 @@
import { useEffect, useMemo, useState, VFC } from 'react'; import { useEffect, useMemo, useState, VFC } from 'react';
import { useGroups } from 'hooks/api/getters/useGroups/useGroups'; import { useGroups } from 'hooks/api/getters/useGroups/useGroups';
import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { IGroup } from 'interfaces/group'; import { IGroup } from 'interfaces/group';
import { PageContent } from 'component/common/PageContent/PageContent'; import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Search } from 'component/common/Search/Search'; import { Search } from 'component/common/Search/Search';
import { Button, Grid, useMediaQuery } from '@mui/material'; import { Grid, useMediaQuery } from '@mui/material';
import theme from 'themes/theme'; import theme from 'themes/theme';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { TablePlaceholder } from 'component/common/Table'; import { TablePlaceholder } from 'component/common/Table';

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);
@ -30,7 +38,6 @@ export const ConstraintAccordionView = ({
[...semVerOperators, ...numOperators, ...dateOperators], [...semVerOperators, ...numOperators, ...dateOperators],
constraint.operator constraint.operator
); );
const handleClick = () => { const handleClick = () => {
if (expandable) { if (expandable) {
setExpanded(!expanded); setExpanded(!expanded);
@ -42,6 +49,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,6 +1,6 @@
import { styled, Tooltip } from '@mui/material'; import { styled, Tooltip } from '@mui/material';
import { ConstraintViewHeaderOperator } from '../ConstraintViewHeaderOperator/ConstraintViewHeaderOperator'; import { ConstraintViewHeaderOperator } from '../ConstraintViewHeaderOperator/ConstraintViewHeaderOperator';
import { ConditionallyRender } from '../../../../ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { ConstraintAccordionViewHeaderSingleValue } from '../ContraintAccordionViewHeaderSingleValue/ConstraintAccordionViewHeaderSingleValue'; import { ConstraintAccordionViewHeaderSingleValue } from '../ContraintAccordionViewHeaderSingleValue/ConstraintAccordionViewHeaderSingleValue';
import { ConstraintAccordionViewHeaderMultipleValues } from '../ContraintAccordionViewHeaderMultipleValues/ConstraintAccordionViewHeaderMultipleValues'; import { ConstraintAccordionViewHeaderMultipleValues } from '../ContraintAccordionViewHeaderMultipleValues/ConstraintAccordionViewHeaderMultipleValues';
import React from 'react'; import React from 'react';
@ -27,11 +27,19 @@ const StyledHeaderText = styled('span')(({ theme }) => ({
}, },
})); }));
const StyledHeaderWrapper = styled('div')(({ theme }) => ({
display: 'flex',
width: '100%',
justifyContent: 'space-between',
borderRadius: theme.spacing(1),
}));
interface ConstraintAccordionViewHeaderMetaInfoProps { interface ConstraintAccordionViewHeaderMetaInfoProps {
constraint: IConstraint; constraint: IConstraint;
singleValue: boolean; singleValue: boolean;
expanded: boolean; expanded: boolean;
allowExpand: (shouldExpand: boolean) => void; allowExpand: (shouldExpand: boolean) => void;
maxLength?: number;
} }
export const ConstraintAccordionViewHeaderInfo = ({ export const ConstraintAccordionViewHeaderInfo = ({
@ -39,31 +47,37 @@ export const ConstraintAccordionViewHeaderInfo = ({
singleValue, singleValue,
allowExpand, allowExpand,
expanded, expanded,
maxLength = 112, //The max number of characters in the values text for NOT allowing expansion
}: ConstraintAccordionViewHeaderMetaInfoProps) => { }: ConstraintAccordionViewHeaderMetaInfoProps) => {
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
return ( return (
<div className={styles.headerMetaInfo}> <StyledHeaderWrapper>
<Tooltip title={constraint.contextName} arrow> <div className={styles.headerMetaInfo}>
<StyledHeaderText>{constraint.contextName}</StyledHeaderText> <Tooltip title={constraint.contextName} arrow>
</Tooltip> <StyledHeaderText>
<ConstraintViewHeaderOperator constraint={constraint} /> {constraint.contextName}
<ConditionallyRender </StyledHeaderText>
condition={singleValue} </Tooltip>
show={ <ConstraintViewHeaderOperator constraint={constraint} />
<ConstraintAccordionViewHeaderSingleValue <ConditionallyRender
constraint={constraint} condition={singleValue}
allowExpand={allowExpand} show={
/> <ConstraintAccordionViewHeaderSingleValue
} constraint={constraint}
elseShow={ allowExpand={allowExpand}
<ConstraintAccordionViewHeaderMultipleValues />
constraint={constraint} }
expanded={expanded} elseShow={
allowExpand={allowExpand} <ConstraintAccordionViewHeaderMultipleValues
maxLength={112} constraint={constraint}
/> expanded={expanded}
} allowExpand={allowExpand}
/> maxLength={maxLength}
</div> />
}
/>
</div>
</StyledHeaderWrapper>
); );
}; };

View File

@ -1,9 +1,9 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { Chip, styled } from '@mui/material'; import { Chip, styled } from '@mui/material';
import { formatConstraintValue } from '../../../../../../utils/formatConstraintValue'; import { formatConstraintValue } from 'utils/formatConstraintValue';
import { useStyles } from '../../../ConstraintAccordion.styles'; import { useStyles } from '../../../ConstraintAccordion.styles';
import { IConstraint } from '../../../../../../interfaces/strategy'; import { IConstraint } from '../../../../../../interfaces/strategy';
import { useLocationSettings } from '../../../../../../hooks/useLocationSettings'; import { useLocationSettings } from 'hooks/useLocationSettings';
const StyledSingleValueChip = styled(Chip)(({ theme }) => ({ const StyledSingleValueChip = styled(Chip)(({ theme }) => ({
margin: 'auto 0', margin: 'auto 0',

View File

@ -26,6 +26,11 @@ export const useStyles = makeStyles()(theme => ({
actions: { actions: {
marginLeft: 'auto', marginLeft: 'auto',
display: 'flex', display: 'flex',
minHeight: theme.spacing(6),
alignItems: 'center',
},
resultChip: {
marginLeft: 'auto',
}, },
body: { body: {
padding: theme.spacing(2), padding: theme.spacing(2),

View File

@ -0,0 +1,96 @@
import { DragEventHandler, FC, ReactNode } from 'react';
import { DragIndicator } from '@mui/icons-material';
import { styled, IconButton, Box } from '@mui/material';
import classNames from 'classnames';
import { IFeatureStrategy } from 'interfaces/strategy';
import {
getFeatureStrategyIcon,
formatStrategyName,
} from 'utils/strategyNames';
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useStyles } from './StrategyItemContainer.styles';
interface IStrategyItemContainerProps {
strategy: IFeatureStrategy;
onDragStart?: DragEventHandler<HTMLButtonElement>;
onDragEnd?: DragEventHandler<HTMLButtonElement>;
actions?: ReactNode;
orderNumber?: number;
className?: string;
}
const DragIcon = styled(IconButton)(({ theme }) => ({
padding: 0,
cursor: 'inherit',
transition: 'color 0.2s ease-in-out',
}));
const StyledIndexLabel = styled('div')(({ theme }) => ({
fontSize: theme.typography.fontSize,
color: theme.palette.text.secondary,
position: 'absolute',
display: 'none',
right: 'calc(100% + 6px)',
top: theme.spacing(2.5),
[theme.breakpoints.up('md')]: {
display: 'block',
},
}));
export const StrategyItemContainer: FC<IStrategyItemContainerProps> = ({
strategy,
onDragStart,
onDragEnd,
actions,
children,
orderNumber,
className,
}) => {
const { classes: styles } = useStyles();
const Icon = getFeatureStrategyIcon(strategy.name);
return (
<Box sx={{ position: 'relative' }}>
<ConditionallyRender
condition={orderNumber !== undefined}
show={<StyledIndexLabel>{orderNumber}</StyledIndexLabel>}
/>
<Box className={classNames(styles.container, className)}>
<div
className={classNames(styles.header, {
[styles.headerDraggable]: Boolean(onDragStart),
})}
>
<ConditionallyRender
condition={Boolean(onDragStart)}
show={() => (
<DragIcon
draggable
disableRipple
size="small"
onDragStart={onDragStart}
onDragEnd={onDragEnd}
sx={{ cursor: 'move' }}
>
<DragIndicator
titleAccess="Drag to reorder"
cursor="grab"
sx={{ color: 'neutral.main' }}
/>
</DragIcon>
)}
/>
<Icon className={styles.icon} />
<StringTruncator
maxWidth="150"
maxLength={15}
text={formatStrategyName(strategy.name)}
/>
<div className={styles.actions}>{actions}</div>
</div>
<div className={styles.body}>{children}</div>
</Box>
</Box>
);
};

View File

@ -1,5 +1,5 @@
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import { ReactNode } from 'react'; import React, { ReactNode } from 'react';
interface IIconCellProps { interface IIconCellProps {
icon: ReactNode; icon: ReactNode;

View File

@ -1,9 +1,9 @@
import { Box, styled } from '@mui/material'; import { DragEventHandler, RefObject, useRef } from 'react';
import { Box } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
import { IFeatureEnvironment } from 'interfaces/featureToggle'; import { IFeatureEnvironment } from 'interfaces/featureToggle';
import { IFeatureStrategy } from 'interfaces/strategy'; import { IFeatureStrategy } from 'interfaces/strategy';
import { DragEventHandler, RefObject, useRef } from 'react';
import { StrategyItem } from './StrategyItem/StrategyItem'; import { StrategyItem } from './StrategyItem/StrategyItem';
interface IStrategyDraggableItemProps { interface IStrategyDraggableItemProps {
@ -22,19 +22,6 @@ interface IStrategyDraggableItemProps {
) => DragEventHandler<HTMLDivElement>; ) => DragEventHandler<HTMLDivElement>;
onDragEnd: () => void; onDragEnd: () => void;
} }
const StyledIndexLabel = styled('div')(({ theme }) => ({
fontSize: theme.typography.fontSize,
color: theme.palette.text.secondary,
position: 'absolute',
display: 'none',
right: 'calc(100% + 6px)',
top: theme.spacing(2.5),
[theme.breakpoints.up('md')]: {
display: 'block',
},
}));
export const StrategyDraggableItem = ({ export const StrategyDraggableItem = ({
strategy, strategy,
index, index,
@ -58,16 +45,15 @@ export const StrategyDraggableItem = ({
condition={index > 0} condition={index > 0}
show={<StrategySeparator text="OR" />} show={<StrategySeparator text="OR" />}
/> />
<Box sx={{ position: 'relative' }}>
<StyledIndexLabel>{index + 1}</StyledIndexLabel> <StrategyItem
<StrategyItem strategy={strategy}
strategy={strategy} environmentId={environmentName}
environmentId={environmentName} otherEnvironments={otherEnvironments}
otherEnvironments={otherEnvironments} onDragStart={onDragStartRef(ref, index)}
onDragStart={onDragStartRef(ref, index)} onDragEnd={onDragEnd}
onDragEnd={onDragEnd} orderNumber={index + 1}
/> />
</Box>
</Box> </Box>
); );
}; };

View File

@ -20,7 +20,6 @@ import StringTruncator from 'component/common/StringTruncator/StringTruncator';
interface IStrategyExecutionProps { interface IStrategyExecutionProps {
strategy: IFeatureStrategy; strategy: IFeatureStrategy;
percentageFill?: string;
} }
const NoItems: VFC = () => ( const NoItems: VFC = () => (

View File

@ -1,24 +1,17 @@
import { DragEventHandler } from 'react'; import { DragEventHandler, VFC } from 'react';
import { DragIndicator, Edit } from '@mui/icons-material'; import { Edit } from '@mui/icons-material';
import { styled, useTheme, IconButton } from '@mui/material';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import classNames from 'classnames';
import { IFeatureEnvironment } from 'interfaces/featureToggle'; import { IFeatureEnvironment } from 'interfaces/featureToggle';
import { IFeatureStrategy } from 'interfaces/strategy'; import { IFeatureStrategy } from 'interfaces/strategy';
import {
getFeatureStrategyIcon,
formatStrategyName,
} from 'utils/strategyNames';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { UPDATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions'; import { UPDATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
import { formatEditStrategyPath } from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit'; import { formatEditStrategyPath } from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
import { FeatureStrategyRemove } from 'component/feature/FeatureStrategy/FeatureStrategyRemove/FeatureStrategyRemove'; import { FeatureStrategyRemove } from 'component/feature/FeatureStrategy/FeatureStrategyRemove/FeatureStrategyRemove';
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { StrategyExecution } from './StrategyExecution/StrategyExecution'; import { StrategyExecution } from './StrategyExecution/StrategyExecution';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { CopyStrategyIconMenu } from './CopyStrategyIconMenu/CopyStrategyIconMenu'; import { CopyStrategyIconMenu } from './CopyStrategyIconMenu/CopyStrategyIconMenu';
import { useStyles } from './StrategyItem.styles'; import { StrategyItemContainer } from 'component/common/StrategyItemContainer/StrategyItemContainer';
interface IStrategyItemProps { interface IStrategyItemProps {
environmentId: string; environmentId: string;
@ -26,26 +19,19 @@ interface IStrategyItemProps {
onDragStart?: DragEventHandler<HTMLButtonElement>; onDragStart?: DragEventHandler<HTMLButtonElement>;
onDragEnd?: DragEventHandler<HTMLButtonElement>; onDragEnd?: DragEventHandler<HTMLButtonElement>;
otherEnvironments?: IFeatureEnvironment['name'][]; otherEnvironments?: IFeatureEnvironment['name'][];
orderNumber?: number;
} }
const DragIcon = styled(IconButton)(({ theme }) => ({ export const StrategyItem: VFC<IStrategyItemProps> = ({
padding: 0,
cursor: 'inherit',
transition: 'color 0.2s ease-in-out',
}));
export const StrategyItem = ({
environmentId, environmentId,
strategy, strategy,
onDragStart, onDragStart,
onDragEnd, onDragEnd,
otherEnvironments, otherEnvironments,
}: IStrategyItemProps) => { orderNumber,
}) => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId'); const featureId = useRequiredPathParam('featureId');
const theme = useTheme();
const { classes: styles } = useStyles();
const Icon = getFeatureStrategyIcon(strategy.name);
const editStrategyPath = formatEditStrategyPath( const editStrategyPath = formatEditStrategyPath(
projectId, projectId,
@ -55,38 +41,13 @@ export const StrategyItem = ({
); );
return ( return (
<div className={styles.container}> <StrategyItemContainer
<div strategy={strategy}
className={classNames(styles.header, { onDragStart={onDragStart}
[styles.headerDraggable]: Boolean(onDragStart), onDragEnd={onDragEnd}
})} orderNumber={orderNumber}
> actions={
<ConditionallyRender <>
condition={Boolean(onDragStart)}
show={() => (
<DragIcon
draggable
disableRipple
size="small"
onDragStart={onDragStart}
onDragEnd={onDragEnd}
sx={{ cursor: 'move' }}
>
<DragIndicator
titleAccess="Drag to reorder"
cursor="grab"
sx={{ color: 'neutral.main' }}
/>
</DragIcon>
)}
/>
<Icon className={styles.icon} />
<StringTruncator
maxWidth="150"
maxLength={15}
text={formatStrategyName(strategy.name)}
/>
<div className={styles.actions}>
<ConditionallyRender <ConditionallyRender
condition={Boolean( condition={Boolean(
otherEnvironments && otherEnvironments?.length > 0 otherEnvironments && otherEnvironments?.length > 0
@ -115,14 +76,10 @@ export const StrategyItem = ({
strategyId={strategy.id} strategyId={strategy.id}
icon icon
/> />
</div> </>
</div> }
<div className={styles.body}> >
<StrategyExecution <StrategyExecution strategy={strategy} />
strategy={strategy} </StrategyItemContainer>
percentageFill={theme.palette.grey[200]}
/>
</div>
</div>
); );
}; };

View File

@ -8,7 +8,7 @@ import { formatUnknownError } from 'utils/formatUnknownError';
import { PlaygroundResultsTable } from './PlaygroundResultsTable/PlaygroundResultsTable'; import { PlaygroundResultsTable } from './PlaygroundResultsTable/PlaygroundResultsTable';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { usePlaygroundApi } from 'hooks/api/actions/usePlayground/usePlayground'; import { usePlaygroundApi } from 'hooks/api/actions/usePlayground/usePlayground';
import { PlaygroundResponseSchema } from 'hooks/api/actions/usePlayground/playground.model'; import { PlaygroundResponseSchema } from 'component/playground/Playground/interfaces/playground.model';
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments'; import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
import { PlaygroundForm } from './PlaygroundForm/PlaygroundForm'; import { PlaygroundForm } from './PlaygroundForm/PlaygroundForm';
import { import {
@ -203,6 +203,7 @@ export const Playground: VFC<{}> = () => {
<PlaygroundResultsTable <PlaygroundResultsTable
loading={loading} loading={loading}
features={results?.features} features={results?.features}
input={results?.input}
/> />
} }
elseShow={<PlaygroundGuidance />} elseShow={<PlaygroundGuidance />}

View File

@ -1,6 +1,6 @@
import { colors } from 'themes/colors'; import { colors } from 'themes/colors';
import { Alert, styled } from '@mui/material'; import { Alert, styled } from '@mui/material';
import { SdkContextSchema } from 'hooks/api/actions/usePlayground/playground.model'; import { SdkContextSchema } from 'component/playground/Playground/interfaces/playground.model';
interface IContextBannerProps { interface IContextBannerProps {
environment: string; environment: string;

View File

@ -0,0 +1,29 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
titleRowWrapper: {
display: 'flex',
justifyContent: 'space-between',
width: '100%',
},
titleRow: {
display: 'inline-flex',
alignItems: 'flex-start',
justifyContent: 'center',
gap: theme.spacing(1.5),
marginTop: theme.spacing(1.5),
},
alertRow: {
margin: theme.spacing(1, 0),
},
descriptionRow: {
margin: theme.spacing(1, 0.5),
},
name: {
fontWeight: 600,
padding: theme.spacing(0.5),
},
icon: {
textAlign: 'right',
},
}));

View File

@ -0,0 +1,109 @@
import {
PlaygroundFeatureSchema,
PlaygroundRequestSchema,
} from 'component/playground/Playground/interfaces/playground.model';
import { Alert, IconButton, Typography, useTheme } from '@mui/material';
import { PlaygroundResultChip } from '../../PlaygroundResultChip/PlaygroundResultChip';
import { useStyles } from './FeatureDetails.styles';
import { CloseOutlined } from '@mui/icons-material';
import React from 'react';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import {
checkForEmptyValues,
hasCustomStrategies,
hasOnlyCustomStrategies,
} from './helpers';
interface PlaygroundFeatureResultDetailsProps {
feature: PlaygroundFeatureSchema;
input?: PlaygroundRequestSchema;
onClose: () => void;
}
export const FeatureDetails = ({
feature,
input,
onClose,
}: PlaygroundFeatureResultDetailsProps) => {
const { classes: styles } = useStyles();
const theme = useTheme();
const description = feature.isEnabled
? `This feature toggle is True in ${input?.environment} because `
: `This feature toggle is False in ${input?.environment} because `;
const reason = (() => {
if (feature.isEnabled) return 'at least one strategy is True';
if (!feature.isEnabledInCurrentEnvironment)
return 'the environment is disabled';
if (hasOnlyCustomStrategies(feature))
return 'no strategies could be fully evaluated';
return 'all strategies are either False or could not be fully evaluated';
})();
const color = feature.isEnabled
? theme.palette.success.main
: theme.palette.error.main;
const noValueTxt = checkForEmptyValues(input?.context)
? 'You did not provide a value for your context field in step 2 of the configuration'
: undefined;
const customStrategiesTxt = hasCustomStrategies(feature)
? `This feature uses custom strategies. Custom strategies can't be evaluated, so they will be marked as Unevaluated`
: undefined;
const onCloseClick =
onClose &&
((event: React.SyntheticEvent) => {
event.stopPropagation();
onClose();
});
return (
<>
<div className={styles.titleRowWrapper}>
<div className={styles.titleRow}>
<Typography variant={'subtitle1'} className={styles.name}>
{feature.name}
</Typography>
<span>
<PlaygroundResultChip
enabled={feature.isEnabled}
label={feature.isEnabled ? 'True' : 'False'}
/>
</span>
</div>
<IconButton onClick={onCloseClick} className={styles.icon}>
<CloseOutlined />
</IconButton>
</div>
<div className={styles.descriptionRow}>
<Typography variant="body1" component="span">
{description}
</Typography>
<Typography variant="subtitle1" color={color} component="span">
{reason}
</Typography>
</div>
<ConditionallyRender
condition={Boolean(noValueTxt)}
show={
<div className={styles.alertRow}>
<Alert color={'info'}>{noValueTxt}</Alert>
</div>
}
/>
<ConditionallyRender
condition={Boolean(customStrategiesTxt)}
show={
<div className={styles.alertRow}>
<Alert color={'info'}>{customStrategiesTxt}</Alert>
</div>
}
/>
</>
);
};

View File

@ -0,0 +1,33 @@
import { PlaygroundFeatureSchema } from 'component/playground/Playground/interfaces/playground.model';
export const DEFAULT_STRATEGIES = [
'default',
'applicationHostname',
'flexibleRollout',
'gradualRolloutRandom',
'gradualRolloutSessionId',
'gradualRolloutUserId',
'remoteAddress',
'userWithId',
];
export function checkForEmptyValues(object?: Object): boolean {
if (object === undefined) {
return true;
}
return Object.values(object).every(v =>
v && typeof v === 'object' ? checkForEmptyValues(v) : v === null
);
}
export const hasCustomStrategies = (feature: PlaygroundFeatureSchema) => {
return feature.strategies?.data?.find(
strategy => !DEFAULT_STRATEGIES.includes(strategy.name)
);
};
export const hasOnlyCustomStrategies = (feature: PlaygroundFeatureSchema) => {
return !feature.strategies?.data?.find(strategy =>
DEFAULT_STRATEGIES.includes(strategy.name)
);
};

View File

@ -0,0 +1,14 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
popoverPaper: {
display: 'flex',
flexDirection: 'column',
padding: theme.spacing(6),
width: 728,
maxWidth: '100%',
height: 'auto',
overflowY: 'auto',
backgroundColor: theme.palette.tertiary.light,
},
}));

View File

@ -0,0 +1,69 @@
import {
PlaygroundFeatureSchema,
PlaygroundRequestSchema,
} from 'component/playground/Playground/interfaces/playground.model';
import { IconButton, Popover, styled } from '@mui/material';
import { InfoOutlined } from '@mui/icons-material';
import React, { useRef, useState } from 'react';
import { useStyles } from './FeatureResultInfoPopoverCell.styles';
import { FeatureDetails } from './FeatureDetails/FeatureDetails';
import { PlaygroundResultFeatureStrategyList } from './FeatureStrategyList/PlaygroundResultFeatureStrategyList';
interface FeatureResultInfoPopoverCellProps {
feature: PlaygroundFeatureSchema;
input?: PlaygroundRequestSchema;
}
const FeatureResultPopoverWrapper = styled('div')(({ theme }) => ({
alignItems: 'flex-end',
color: theme.palette.tertiary.main,
}));
export const FeatureResultInfoPopoverCell = ({
feature,
input,
}: FeatureResultInfoPopoverCellProps) => {
const [open, setOpen] = useState(false);
const { classes: styles } = useStyles();
const ref = useRef(null);
const togglePopover = () => {
setOpen(!open);
};
if (!feature) {
return null;
}
return (
<FeatureResultPopoverWrapper>
<IconButton onClick={togglePopover}>
<InfoOutlined ref={ref} />
</IconButton>
<Popover
open={open}
onClose={() => setOpen(false)}
anchorEl={ref.current}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'center',
horizontal: 'left',
}}
classes={{ paper: styles.popoverPaper }}
>
<FeatureDetails
feature={feature}
input={input}
onClose={() => setOpen(false)}
/>
<PlaygroundResultFeatureStrategyList
feature={feature}
input={input}
/>
</Popover>
</FeatureResultPopoverWrapper>
);
};

View File

@ -0,0 +1,39 @@
import { PlaygroundResultStrategyLists } from './StrategyList/playgroundResultStrategyLists';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import {
PlaygroundFeatureSchema,
PlaygroundRequestSchema,
} from 'component/playground/Playground/interfaces/playground.model';
import { Alert } from '@mui/material';
interface PlaygroundResultFeatureStrategyListProps {
feature: PlaygroundFeatureSchema;
input?: PlaygroundRequestSchema;
}
export const PlaygroundResultFeatureStrategyList = ({
feature,
input,
}: PlaygroundResultFeatureStrategyListProps) => {
return (
<>
<ConditionallyRender
condition={
!feature.isEnabledInCurrentEnvironment &&
Boolean(feature?.strategies?.data)
}
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>
}
/>
<PlaygroundResultStrategyLists
strategies={feature?.strategies?.data || []}
input={input}
/>
</>
);
};

View File

@ -0,0 +1,40 @@
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',
},
width: '100%',
flexShrink: 0,
paddingBottom: '1rem',
borderRadius: theme.shape.borderRadiusMedium,
background: theme.palette.background.default,
},
successBorder: {
border: `1px solid ${theme.palette.success.main}`,
},
}));

View File

@ -0,0 +1,62 @@
import { useTheme } from '@mui/material';
import { PlaygroundResultChip } from '../../../../PlaygroundResultChip/PlaygroundResultChip';
import {
PlaygroundStrategySchema,
PlaygroundRequestSchema,
} from 'component/playground/Playground/interfaces/playground.model';
import { StrategyExecution } from './StrategyExecution/StrategyExecution';
import { useStyles } from './FeatureStrategyItem.styles';
import { StrategyItemContainer } from 'component/common/StrategyItemContainer/StrategyItemContainer';
import { objectId } from 'utils/objectId';
interface IFeatureStrategyItemProps {
strategy: PlaygroundStrategySchema;
index: number;
input?: PlaygroundRequestSchema;
}
export const FeatureStrategyItem = ({
strategy,
input,
index,
}: IFeatureStrategyItemProps) => {
const { result } = strategy;
const { classes: styles } = useStyles();
const theme = useTheme();
const label =
result.evaluationStatus === 'incomplete'
? 'Unevaluated'
: result.enabled
? 'True'
: 'False';
return (
<StrategyItemContainer
className={
result.enabled && result.evaluationStatus === 'complete'
? styles.successBorder
: undefined
}
strategy={{ ...strategy, id: `${objectId(strategy)}` }}
orderNumber={index + 1}
actions={
<PlaygroundResultChip
showIcon={false}
enabled={result.enabled}
label={label}
size={
result.evaluationStatus === 'incomplete'
? 'large'
: 'default'
}
/>
}
>
<StrategyExecution
strategyResult={strategy}
input={input}
percentageFill={theme.palette.tertiary.light}
/>
</StrategyItemContainer>
);
};

View File

@ -0,0 +1,106 @@
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

@ -0,0 +1,91 @@
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

@ -0,0 +1,42 @@
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

@ -0,0 +1,111 @@
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

@ -0,0 +1,85 @@
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 any 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

@ -0,0 +1,47 @@
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 any values{' '}
</Typography>
}
/>
<StyledSingleValueChip
label={formatConstraintValue(constraint, locationSettings)}
/>
</div>
);
};

View File

@ -0,0 +1,51 @@
import { Fragment, VFC } from 'react';
import {
PlaygroundConstraintSchema,
PlaygroundRequestSchema,
} from 'component/playground/Playground/interfaces/playground.model';
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';
interface IConstraintExecutionProps {
constraints?: PlaygroundConstraintSchema[];
compact: boolean;
input?: PlaygroundRequestSchema;
}
export const ConstraintExecutionWrapper = styled('div')(() => ({
width: '100%',
display: 'flex',
flexDirection: 'column',
}));
export const ConstraintExecution: VFC<IConstraintExecutionProps> = ({
constraints,
compact,
input,
}) => {
if (!constraints) return null;
return (
<ConstraintExecutionWrapper>
{constraints?.map((constraint, index) => (
<Fragment key={objectId(constraint)}>
<ConditionallyRender
condition={index > 0 && constraints?.length > 1}
show={<StrategySeparator text="AND" />}
/>
<ConstraintAccordionView
constraint={constraint}
playgroundInput={input}
maxLength={compact ? 25 : 50}
sx={{
backgroundColor: 'transparent!important',
}}
/>
</Fragment>
))}
</ConstraintExecutionWrapper>
);
};

View File

@ -0,0 +1,140 @@
import React, { Fragment, VFC } from 'react';
import {
parseParameterNumber,
parseParameterString,
parseParameterStrings,
} from 'utils/parseParameter';
import { PlaygroundParameterItem } from '../PlaygroundParameterItem/PlaygroundParameterItem';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
import { Chip } from '@mui/material';
import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle';
import { PlaygroundConstraintSchema } from 'component/playground/Playground/interfaces/playground.model';
import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
interface ICustomStrategyProps {
parameters: { [key: string]: string };
strategyName: string;
constraints: PlaygroundConstraintSchema[];
}
export const CustomStrategyParams: VFC<ICustomStrategyProps> = ({
strategyName,
constraints,
parameters,
}) => {
const { strategies } = useStrategies();
const definition = strategies.find(strategyDefinition => {
return strategyDefinition.name === strategyName;
});
if (!definition?.editable) {
return null;
}
const renderCustomStrategyParameters = () => {
return definition?.parameters.map((param: any, index: number) => {
const notLastItem = index !== definition?.parameters?.length - 1;
switch (param?.type) {
case 'list':
const values = parseParameterStrings(
parameters[param.name]
);
return (
<Fragment key={param?.name}>
<PlaygroundParameterItem
value={values}
text={param.name}
/>
<ConditionallyRender
condition={notLastItem}
show={<StrategySeparator text="AND" />}
/>
</Fragment>
);
case 'percentage':
return (
<Fragment key={param?.name}>
<div>
<Chip
size="small"
variant="outlined"
color="success"
label={`${parameters[param.name]}%`}
/>{' '}
of your base{' '}
{constraints?.length > 0
? 'who match constraints'
: ''}{' '}
is included.
</div>
<PercentageCircle
percentage={parseParameterNumber(
parameters[param.name]
)}
/>
<ConditionallyRender
condition={notLastItem}
show={<StrategySeparator text="AND" />}
/>
</Fragment>
);
case 'boolean':
const bool = Boolean(parameters[param?.name]);
return (
<Fragment key={param?.name}>
<PlaygroundParameterItem
value={bool ? ['True'] : []}
text={param.name}
showReason={!bool}
input={bool ? bool : 'no value'}
/>
<ConditionallyRender
condition={notLastItem}
show={<StrategySeparator text="AND" />}
/>
</Fragment>
);
case 'string':
const value =
parseParameterString(parameters[param.name]) ??
'no value';
return (
<Fragment key={param?.name}>
<PlaygroundParameterItem
value={value !== '' ? [value] : []}
text={param.name}
showReason={value === ''}
input={value !== '' ? value : 'no value'}
/>
<ConditionallyRender
condition={notLastItem}
show={<StrategySeparator text="AND" />}
/>
</Fragment>
);
case 'number':
const number = parseParameterNumber(parameters[param.name]);
return (
<Fragment key={param?.name}>
<PlaygroundParameterItem
value={Boolean(number) ? [number] : []}
text={param.name}
showReason={Boolean(number)}
input={Boolean(number) ? number : 'no value'}
/>
<ConditionallyRender
condition={notLastItem}
show={<StrategySeparator text="AND" />}
/>
</Fragment>
);
case 'default':
return null;
}
return null;
});
};
return <>{renderCustomStrategyParameters()}</>;
};

View File

@ -0,0 +1,83 @@
import { Chip, Typography, useTheme } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useStyles } from './PlaygroundParametertem.styles';
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import { CancelOutlined } from '@mui/icons-material';
import classnames from 'classnames';
interface IConstraintItemProps {
value: Array<string | number>;
text: string;
input?: string | number | boolean | 'no value';
showReason?: boolean;
}
export const PlaygroundParameterItem = ({
value,
text,
input,
showReason = false,
}: IConstraintItemProps) => {
const { classes: styles } = useStyles();
const theme = useTheme();
const color = input === 'no value' ? 'error' : 'neutral';
const reason = `value does not match any ${text}`;
return (
<div
className={classnames(
styles.container,
showReason ? styles.disabled : ''
)}
>
<Typography variant="subtitle1" color={theme.palette[color].main}>
{`${input}`}
</Typography>
<div className={styles.column}>
<ConditionallyRender
condition={Boolean(showReason)}
show={
<Typography
variant="subtitle1"
color={theme.palette.error.main}
>
{reason}
</Typography>
}
/>
<ConditionallyRender
condition={value.length === 0}
show={<p>No {text}s added yet.</p>}
elseShow={
<div>
<p className={styles.paragraph}>
{value.length}{' '}
{value.length > 1 ? `${text}s` : text} will get
access.
</p>
{value.map((v: string | number) => (
<Chip
key={v}
label={
<StringTruncator
maxWidth="300"
text={v.toString()}
maxLength={50}
/>
}
className={styles.chip}
/>
))}
</div>
}
/>
</div>
<ConditionallyRender
condition={Boolean(showReason)}
show={<CancelOutlined color={'error'} />}
elseShow={<div />}
/>
</div>
);
};

View File

@ -0,0 +1,32 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
container: {
width: '100%',
padding: theme.spacing(2, 3),
borderRadius: theme.shape.borderRadius,
border: `1px solid ${theme.palette.divider}`,
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: theme.spacing(2),
},
disabled: {
backgroundColor: theme.palette.neutral.light,
opacity: '90%',
},
chip: {
margin: '0.25rem',
},
column: {
flexDirection: 'column',
},
paragraph: {
display: 'inline',
margin: '0.25rem 0',
maxWidth: '95%',
textAlign: 'center',
wordBreak: 'break-word',
},
}));

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,119 @@
import { 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 { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
import { useStyles } from './SegmentExecution.styles';
import { styled, Typography } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
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',
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>
<ConditionallyRender
condition={
index === segments?.length - 1 && hasConstraints
}
show={<StrategySeparator text="AND" />}
/>
</SegmentExecutionWrapper>
))}
</>
);
};

View File

@ -0,0 +1,19 @@
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: 'auto',
height: 'auto',
padding: theme.spacing(2, 3),
borderRadius: theme.shape.borderRadius,
border: `1px solid ${theme.palette.divider}`,
},
}));

View File

@ -0,0 +1,112 @@
import { VFC } from 'react';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
import { Box, Chip, styled } from '@mui/material';
import { useStyles } from './StrategyExecution.styles';
import {
PlaygroundRequestSchema,
PlaygroundStrategySchema,
} from 'component/playground/Playground/interfaces/playground.model';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { ConstraintExecution } from './ConstraintExecution/ConstraintExecution';
import { SegmentExecution } from './SegmentExecution/SegmentExecution';
import { PlaygroundResultStrategyExecutionParameters } from './StrategyExecutionParameters/StrategyExecutionParameters';
import { CustomStrategyParams } from './CustomStrategyParams/CustomStrategyParams';
interface IStrategyExecutionProps {
strategyResult: PlaygroundStrategySchema;
percentageFill?: string;
input?: PlaygroundRequestSchema;
}
const StyledStrategyExecutionWrapper = styled('div')(({ theme }) => ({
padding: theme.spacing(0),
}));
const StyledParamWrapper = styled('div')(({ theme }) => ({
padding: theme.spacing(0, 0),
}));
export const StrategyExecution: VFC<IStrategyExecutionProps> = ({
strategyResult,
input,
}) => {
const { name, constraints, segments, parameters } = strategyResult;
const { uiConfig } = useUiConfig();
const { classes: styles } = useStyles();
const hasConstraints = Boolean(constraints && constraints?.length > 0);
const hasParameters = Object.keys(parameters).length === 0;
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>
}
/>
<StyledParamWrapper>
<PlaygroundResultStrategyExecutionParameters
parameters={parameters}
constraints={constraints}
input={input}
/>
<StyledParamWrapper sx={{ pt: 2 }}>
<CustomStrategyParams
strategyName={strategyResult.name}
parameters={parameters}
constraints={constraints}
/>
</StyledParamWrapper>
</StyledParamWrapper>
</StyledStrategyExecutionWrapper>
);
};

View File

@ -0,0 +1,126 @@
import {
parseParameterNumber,
parseParameterStrings,
} from 'utils/parseParameter';
import { Box, Chip } from '@mui/material';
import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle';
import { PlaygroundParameterItem } from '../PlaygroundParameterItem/PlaygroundParameterItem';
import React from 'react';
import { useStyles } from '../StrategyExecution.styles';
import {
PlaygroundConstraintSchema,
PlaygroundRequestSchema,
} from 'component/playground/Playground/interfaces/playground.model';
import { getMappedParam } from '../helpers';
export interface PlaygroundResultStrategyExecutionParametersProps {
parameters: { [key: string]: string };
constraints: PlaygroundConstraintSchema[];
input?: PlaygroundRequestSchema;
}
export const PlaygroundResultStrategyExecutionParameters = ({
parameters,
constraints,
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()}</>;
};

View File

@ -0,0 +1,10 @@
export const getMappedParam = (key: string) => {
switch (key.toUpperCase()) {
case 'USERIDS':
return 'userId';
case 'IPS':
return 'remoteAddress';
default:
return key;
}
};

View File

@ -0,0 +1,92 @@
import { Fragment } from 'react';
import { Alert, Box, styled, Typography } from '@mui/material';
import {
PlaygroundFeatureSchema,
PlaygroundStrategySchema,
PlaygroundRequestSchema,
} from 'component/playground/Playground/interfaces/playground.model';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { FeatureStrategyItem } from './StrategyItem/FeatureStrategyItem';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
const StyledAlertWrapper = styled('div')(({ theme }) => ({
display: 'flex',
padding: `0, 4px`,
flexDirection: 'column',
borderRadius: theme.shape.borderRadiusMedium,
border: `1px solid ${theme.palette.info.border}`,
}));
const StyledListWrapper = styled('div')(({ theme }) => ({
padding: theme.spacing(1, 0.5),
}));
const StyledAlert = styled(Alert)(() => ({
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
}));
interface PlaygroundResultStrategyListProps {
strategies: PlaygroundStrategySchema[];
input?: PlaygroundRequestSchema;
}
export const PlaygroundResultStrategyLists = ({
strategies,
input,
}: PlaygroundResultStrategyListProps) => (
<ConditionallyRender
condition={strategies.length > 0}
show={
<>
<Typography
variant={'subtitle1'}
sx={{ mt: 2, ml: 1, mb: 2, color: 'text.secondary' }}
>{`Strategies (${strategies.length})`}</Typography>
<Box sx={{ width: '100%' }}>
{strategies.map((strategy, index) => (
<Fragment key={strategy.id}>
<ConditionallyRender
condition={index > 0}
show={<StrategySeparator text="OR" />}
/>
<FeatureStrategyItem
key={strategy.id}
strategy={strategy}
index={index}
input={input}
/>
</Fragment>
))}
</Box>
</>
}
/>
);
interface WrappedPlaygroundResultStrategyListProps
extends PlaygroundResultStrategyListProps {
feature: PlaygroundFeatureSchema;
}
export const WrappedPlaygroundResultStrategyList = ({
strategies,
feature,
input,
}: WrappedPlaygroundResultStrategyListProps) => {
return (
<StyledAlertWrapper sx={{ pb: 1 }}>
<StyledAlert 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:{' '}
</StyledAlert>
<StyledListWrapper>
<PlaygroundResultStrategyLists
strategies={strategies}
input={input}
/>
</StyledListWrapper>
</StyledAlertWrapper>
);
};

View File

@ -1,40 +1,12 @@
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 { PlaygroundResultChip } from '../PlaygroundResultChip/PlaygroundResultChip';
import { ReactComponent as FeatureDisabledIcon } from 'assets/icons/isenabled-false.svg'; import { PlaygroundFeatureSchema } from '../../interfaces/playground.model';
import { Box, Chip, styled, useTheme } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
interface IFeatureStatusCellProps { interface IFeatureStatusCellProps {
enabled: boolean; feature: PlaygroundFeatureSchema;
} }
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,
},
}));
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,
},
}));
const StyledCellBox = styled(Box)(({ theme }) => ({ const StyledCellBox = styled(Box)(({ theme }) => ({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
@ -45,35 +17,25 @@ const StyledChipWrapper = styled(Box)(() => ({
marginRight: 'auto', marginRight: 'auto',
})); }));
export const FeatureStatusCell = ({ enabled }: IFeatureStatusCellProps) => { export const FeatureStatusCell = ({ feature }: IFeatureStatusCellProps) => {
const theme = useTheme(); const enabled = feature.isEnabled
const icon = ( ? true
<ConditionallyRender : feature.strategies.result === false
condition={enabled} ? false
show={ : 'unknown';
<FeatureEnabledIcon const label = feature.isEnabled
color={theme.palette.success.main} ? 'True'
strokeWidth="0.25" : feature.strategies.result === false
/> ? 'False'
} : 'Unknown';
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>
<ConditionallyRender <PlaygroundResultChip
condition={enabled} enabled={enabled}
show={<StyledTrueChip icon={icon} label={label} />} label={label}
elseShow={<StyledFalseChip icon={icon} label={label} />} showIcon={enabled !== 'unknown'}
size={'medium'}
/> />
</StyledChipWrapper> </StyledChipWrapper>
</StyledCellBox> </StyledCellBox>

View File

@ -0,0 +1,129 @@
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';
import { WarningOutlined } from '@mui/icons-material';
interface IResultChipProps {
enabled: boolean | 'unevaluated' | 'unknown';
label: string;
// Result icon - defaults to true
showIcon?: boolean;
size?: 'default' | 'medium' | 'large';
}
export const StyledChip = styled(Chip)<{ width?: number }>(
({ theme, width }) => ({
width: width ?? 60,
height: 24,
borderRadius: theme.shape.borderRadius,
fontWeight: theme.typography.fontWeightMedium,
['& .MuiChip-label']: {
padding: 0,
paddingLeft: theme.spacing(0.5),
},
})
);
export const StyledFalseChip = styled(StyledChip)(({ theme }) => ({
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(StyledChip)(({ theme }) => ({
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 StyledUnknownChip = styled(StyledChip)(({ theme }) => ({
border: `1px solid ${theme.palette.warning.main}`,
backgroundColor: colors.orange['100'],
['& .MuiChip-label']: {
color: theme.palette.warning.main,
},
['& .MuiChip-icon']: {
color: theme.palette.warning.main,
},
}));
export const PlaygroundResultChip = ({
enabled,
label,
showIcon = true,
size = 'default',
}: IResultChipProps) => {
const theme = useTheme();
const icon = (
<ConditionallyRender
condition={enabled === 'unknown' || enabled === 'unevaluated'}
show={<WarningOutlined color={'warning'} fontSize="inherit" />}
elseShow={
<ConditionallyRender
condition={typeof enabled === 'boolean' && Boolean(enabled)}
show={
<FeatureEnabledIcon
color={theme.palette.success.main}
strokeWidth="0.25"
/>
}
elseShow={
<FeatureDisabledIcon
color={theme.palette.error.main}
strokeWidth="0.25"
/>
}
/>
}
/>
);
let chipWidth = 60;
if (size === 'medium') chipWidth = 72;
if (size === 'large') chipWidth = 100;
return (
<ConditionallyRender
condition={enabled === 'unknown' || enabled === 'unevaluated'}
show={
<StyledUnknownChip
icon={showIcon ? icon : undefined}
label={label}
width={chipWidth}
/>
}
elseShow={
<ConditionallyRender
condition={typeof enabled === 'boolean' && Boolean(enabled)}
show={
<StyledTrueChip
icon={showIcon ? icon : undefined}
label={label}
width={chipWidth}
/>
}
elseShow={
<StyledFalseChip
icon={showIcon ? icon : undefined}
label={label}
width={chipWidth}
/>
}
/>
}
/>
);
};

View File

@ -19,10 +19,14 @@ import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { useSearch } from 'hooks/useSearch'; import { useSearch } from 'hooks/useSearch';
import { createLocalStorage } from 'utils/createLocalStorage'; import { createLocalStorage } from 'utils/createLocalStorage';
import { FeatureStatusCell } from './FeatureStatusCell/FeatureStatusCell'; import { FeatureStatusCell } from './FeatureStatusCell/FeatureStatusCell';
import { PlaygroundFeatureSchema } from 'hooks/api/actions/usePlayground/playground.model'; import {
PlaygroundFeatureSchema,
PlaygroundRequestSchema,
} from 'component/playground/Playground/interfaces/playground.model';
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 { FeatureResultInfoPopoverCell } from './FeatureResultInfoPopoverCell/FeatureResultInfoPopoverCell';
const defaultSort: SortingRule<string> = { id: 'name' }; const defaultSort: SortingRule<string> = { id: 'name' };
const { value, setValue } = createLocalStorage( const { value, setValue } = createLocalStorage(
@ -32,11 +36,13 @@ const { value, setValue } = createLocalStorage(
interface IPlaygroundResultsTableProps { interface IPlaygroundResultsTableProps {
features?: PlaygroundFeatureSchema[]; features?: PlaygroundFeatureSchema[];
input?: PlaygroundRequestSchema;
loading: boolean; loading: boolean;
} }
export const PlaygroundResultsTable = ({ export const PlaygroundResultsTable = ({
features, features,
input,
loading, loading,
}: IPlaygroundResultsTableProps) => { }: IPlaygroundResultsTableProps) => {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
@ -48,6 +54,77 @@ export const PlaygroundResultsTable = ({
const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm')); const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
const COLUMNS = useMemo(() => {
return [
{
Header: 'Name',
accessor: 'name',
searchable: true,
minWidth: 160,
Cell: ({ value, row: { original } }: any) => (
<LinkCell
title={value}
to={`/projects/${original?.projectId}/features/${value}`}
/>
),
},
{
Header: 'Project ID',
accessor: 'projectId',
sortType: 'alphanumeric',
filterName: 'projectId',
searchable: true,
maxWidth: 170,
Cell: ({ value }: any) => (
<LinkCell title={value} to={`/projects/${value}`} />
),
},
{
Header: 'Variant',
id: 'variant',
accessor: 'variant.name',
sortType: 'alphanumeric',
filterName: 'variant',
searchable: true,
width: 200,
Cell: ({
value,
row: {
original: { variant, feature, variants, isEnabled },
},
}: any) => (
<VariantCell
variant={variant?.enabled ? value : ''}
variants={variants}
feature={feature}
isEnabled={isEnabled}
/>
),
},
{
Header: 'isEnabled',
accessor: 'isEnabled',
filterName: 'isEnabled',
filterParsing: (value: boolean) => (value ? 'true' : 'false'),
Cell: ({ row }: any) => (
<FeatureStatusCell feature={row.original} />
),
sortType: 'boolean',
sortInverted: true,
},
{
Header: '',
id: 'info',
Cell: ({ row }: any) => (
<FeatureResultInfoPopoverCell
feature={row.original}
input={input}
/>
),
},
];
}, [input]);
const { const {
data: searchedData, data: searchedData,
getSearchText, getSearchText,
@ -235,60 +312,3 @@ export const PlaygroundResultsTable = ({
</> </>
); );
}; };
const COLUMNS = [
{
Header: 'Name',
accessor: 'name',
searchable: true,
minWidth: 160,
Cell: ({ value, row: { original } }: any) => (
<LinkCell
title={value}
to={`/projects/${original?.projectId}/features/${value}`}
/>
),
},
{
Header: 'Project ID',
accessor: 'projectId',
sortType: 'alphanumeric',
filterName: 'projectId',
searchable: true,
maxWidth: 170,
Cell: ({ value }: any) => (
<LinkCell title={value} to={`/projects/${value}`} />
),
},
{
Header: 'Variant',
id: 'variant',
accessor: 'variant.name',
sortType: 'alphanumeric',
filterName: 'variant',
searchable: true,
width: 200,
Cell: ({
value,
row: {
original: { variant, feature, variants, isEnabled },
},
}: any) => (
<VariantCell
variant={variant?.enabled ? value : ''}
variants={variants}
feature={feature}
isEnabled={isEnabled}
/>
),
},
{
Header: 'isEnabled',
accessor: 'isEnabled',
filterName: 'isEnabled',
filterParsing: (value: boolean) => (value ? 'true' : 'false'),
Cell: ({ value }: any) => <FeatureStatusCell enabled={value} />,
sortType: 'boolean',
sortInverted: true,
},
];

View File

@ -0,0 +1,321 @@
/**
*
* 09/08/2022
* This was copied from the openapi-generator generated files and slightly modified
* because of malformed generation of `anyOf`, `oneOf`
*
* https://github.com/OpenAPITools/openapi-generator/issues/12256
*/
import { VariantSchema } from 'openapi';
import { Operator } from 'constants/operators';
export interface PlaygroundConstraintSchema {
/**
* The name of the context field that this constraint should apply to.
* @type {string}
* @memberof PlaygroundConstraintSchema
*/
contextName: string;
/**
* The operator to use when evaluating this constraint. For more information about the various operators, refer to [the strategy constraint operator documentation](https://docs.getunleash.io/advanced/strategy_constraints#strategy-constraint-operators).
* @type {string}
* @memberof PlaygroundConstraintSchema
*/
operator: Operator;
/**
* Whether the operator should be case-sensitive or not. Defaults to `false` (being case-sensitive).
* @type {boolean}
* @memberof PlaygroundConstraintSchema
*/
caseInsensitive?: boolean;
/**
* Whether the result should be negated or not. If `true`, will turn a `true` result into a `false` result and vice versa.
* @type {boolean}
* @memberof PlaygroundConstraintSchema
*/
inverted?: boolean;
/**
* The context values that should be used for constraint evaluation. Use this property instead of `value` for properties that accept multiple values.
* @type {Array<string>}
* @memberof PlaygroundConstraintSchema
*/
values?: Array<string>;
/**
* The context value that should be used for constraint evaluation. Use this property instead of `values` for properties that only accept single values.
* @type {string}
* @memberof PlaygroundConstraintSchema
*/
value?: string;
/**
* Whether this was evaluated as true or false.
* @type {boolean}
* @memberof PlaygroundConstraintSchema
*/
result: boolean;
}
export interface PlaygroundFeatureSchema {
/**
* The feature's name.
* @type {string}
* @memberof PlaygroundFeatureSchema
*/
name: string;
/**
* The ID of the project that contains this feature.
* @type {string}
* @memberof PlaygroundFeatureSchema
*/
projectId: string;
/**
* The strategies that apply to this feature.
* @type {Array<PlaygroundStrategySchema>}
* @memberof PlaygroundFeatureSchema
*/
strategies: PlaygroundStrategyResultSchema;
/**
* Whether the feature is active and would be evaluated in the provided environment in a normal SDK context.
* @type {boolean}
* @memberof PlaygroundFeatureSchema
*/
isEnabledInCurrentEnvironment: boolean;
/**
*
* @type {boolean | 'unevaluated'}
* @memberof PlaygroundFeatureSchema
*/
isEnabled: boolean;
/**
*
* @type {PlaygroundFeatureSchemaVariant}
* @memberof PlaygroundFeatureSchema
*/
variant: PlaygroundFeatureSchemaVariant | null;
/**
*
* @type {Array<VariantSchema>}
* @memberof PlaygroundFeatureSchema
*/
variants: Array<VariantSchema>;
}
export interface PlaygroundFeatureSchemaVariant {
/**
* The variant's name. If there is no variant or if the toggle is disabled, this will be `disabled`
* @type {string}
* @memberof PlaygroundFeatureSchemaVariant
*/
name: string;
/**
* Whether the variant is enabled or not. If the feature is disabled or if it doesn't have variants, this property will be `false`
* @type {boolean}
* @memberof PlaygroundFeatureSchemaVariant
*/
enabled: boolean;
/**
*
* @type {PlaygroundFeatureSchemaVariantPayload}
* @memberof PlaygroundFeatureSchemaVariant
*/
payload?: PlaygroundFeatureSchemaVariantPayload;
}
export interface PlaygroundFeatureSchemaVariantPayload {
/**
* The format of the payload.
* @type {string}
* @memberof PlaygroundFeatureSchemaVariantPayload
*/
type: PlaygroundFeatureSchemaVariantPayloadTypeEnum;
/**
* The payload value stringified.
* @type {string}
* @memberof PlaygroundFeatureSchemaVariantPayload
*/
value: string;
}
export const playgroundFeatureSchemaVariantPayloadTypeEnum = {
Json: 'json',
Csv: 'csv',
String: 'string',
} as const;
export type PlaygroundFeatureSchemaVariantPayloadTypeEnum =
typeof playgroundFeatureSchemaVariantPayloadTypeEnum[keyof typeof playgroundFeatureSchemaVariantPayloadTypeEnum];
export interface PlaygroundRequestSchema {
/**
* The environment to evaluate toggles in.
* @type {string}
* @memberof PlaygroundRequestSchema
*/
environment: string;
/**
*
* @type {PlaygroundRequestSchemaProjects}
* @memberof PlaygroundRequestSchema
*/
projects?: PlaygroundRequestSchemaProjects;
/**
*
* @type {SdkContextSchema}
* @memberof PlaygroundRequestSchema
*/
context: SdkContextSchema;
}
export type PlaygroundRequestSchemaProjects = Array<string> | string;
export interface PlaygroundResponseSchema {
/**
*
* @type {PlaygroundRequestSchema}
* @memberof PlaygroundResponseSchema
*/
input: PlaygroundRequestSchema;
/**
* The list of features that have been evaluated.
* @type {Array<PlaygroundFeatureSchema>}
* @memberof PlaygroundResponseSchema
*/
features: Array<PlaygroundFeatureSchema>;
}
export interface PlaygroundSegmentSchema {
/**
* The segment's id.
* @type {number}
* @memberof PlaygroundSegmentSchema
*/
id: number;
/**
* The name of the segment.
* @type {string}
* @memberof PlaygroundSegmentSchema
*/
name: string;
/**
* Whether this was evaluated as true or false.
* @type {boolean}
* @memberof PlaygroundSegmentSchema
*/
result: boolean;
/**
* The list of constraints in this segment.
* @type {Array<PlaygroundConstraintSchema>}
* @memberof PlaygroundSegmentSchema
*/
constraints: Array<PlaygroundConstraintSchema>;
}
export interface PlaygroundStrategyResultSchema {
result: boolean | 'unknown';
data?: Array<PlaygroundStrategySchema>;
}
export interface PlaygroundStrategySchema {
/**
* The strategy's name.
* @type {string}
* @memberof PlaygroundStrategySchema
*/
name: string;
/**
* The strategy's id.
* @type {string}
* @memberof PlaygroundStrategySchema
*/
id?: string;
/**
*
* @type {PlaygroundStrategySchemaResult}
* @memberof PlaygroundStrategySchema
*/
result: PlaygroundStrategySchemaResult;
/**
* The strategy's segments and their evaluation results.
* @type {Array<PlaygroundSegmentSchema>}
* @memberof PlaygroundStrategySchema
*/
segments: Array<PlaygroundSegmentSchema>;
/**
* The strategy's constraints and their evaluation results.
* @type {Array<PlaygroundConstraintSchema>}
* @memberof PlaygroundStrategySchema
*/
constraints: Array<PlaygroundConstraintSchema>;
/**
*
* @type {{ [key: string]: string; }}
* @memberof PlaygroundStrategySchema
*/
parameters: { [key: string]: string };
}
export enum PlaygroundStrategyResultEvaluationStatusEnum {
complete = 'complete',
incomplete = 'incomplete',
}
export interface PlaygroundStrategySchemaResult {
/**
* Signals that this strategy was evaluated successfully.
* @type {string}
* @memberof PlaygroundStrategySchemaResult
*/
evaluationStatus?: PlaygroundStrategyResultEvaluationStatusEnum;
/**
* Whether this strategy evaluates to true or not.
* @type {boolean}
* @memberof PlaygroundStrategySchemaResult
*/
enabled: boolean;
}
export interface SdkContextSchema {
[key: string]: string | any;
/**
*
* @type {string}
* @memberof SdkContextSchema
*/
appName: string;
/**
*
* @type {Date}
* @memberof SdkContextSchema
*/
currentTime?: Date;
/**
*
* @type {string}
* @memberof SdkContextSchema
* @deprecated
*/
environment?: string;
/**
*
* @type {{ [key: string]: string; }}
* @memberof SdkContextSchema
*/
properties?: { [key: string]: string };
/**
*
* @type {string}
* @memberof SdkContextSchema
*/
remoteAddress?: string;
/**
*
* @type {string}
* @memberof SdkContextSchema
*/
sessionId?: string;
/**
*
* @type {string}
* @memberof SdkContextSchema
*/
userId?: string;
}

View File

@ -1,4 +1,4 @@
import { PlaygroundResponseSchema } from 'hooks/api/actions/usePlayground/playground.model'; import { PlaygroundResponseSchema } from 'component/playground/Playground/interfaces/playground.model';
import { IEnvironment } from 'interfaces/environments'; import { IEnvironment } from 'interfaces/environments';
export const resolveProjects = ( export const resolveProjects = (

View File

@ -1,250 +0,0 @@
// TODO: replace with auto-generated openapi code
export enum PlaygroundFeatureSchemaVariantPayloadTypeEnum {
Json = 'json',
Csv = 'csv',
String = 'string',
}
export interface PlaygroundFeatureSchemaVariantPayload {
/**
*
* @type {string}
* @memberof PlaygroundFeatureSchemaVariantPayload
*/
type: PlaygroundFeatureSchemaVariantPayloadTypeEnum;
/**
*
* @type {string}
* @memberof PlaygroundFeatureSchemaVariantPayload
*/
value: string;
}
export interface PlaygroundFeatureSchemaVariant {
/**
*
* @type {string}
* @memberof PlaygroundFeatureSchemaVariant
*/
name: string;
/**
*
* @type {boolean}
* @memberof PlaygroundFeatureSchemaVariant
*/
enabled: boolean;
/**
*
* @type {PlaygroundFeatureSchemaVariantPayload}
* @memberof PlaygroundFeatureSchemaVariant
*/
payload?: PlaygroundFeatureSchemaVariantPayload;
}
export interface PlaygroundFeatureSchema {
/**
*
* @type {string}
* @memberof PlaygroundFeatureSchema
*/
name: string;
/**
*
* @type {string}
* @memberof PlaygroundFeatureSchema
*/
projectId: string;
/**
*
* @type {boolean}
* @memberof PlaygroundFeatureSchema
*/
isEnabled: boolean;
/**
*
* @type {PlaygroundFeatureSchemaVariant}
* @memberof PlaygroundFeatureSchema
*/
variant: PlaygroundFeatureSchemaVariant | null;
}
export interface PlaygroundResponseSchema {
/**
*
* @type {PlaygroundRequestSchema}
* @memberof PlaygroundResponseSchema
*/
input: PlaygroundRequestSchema;
/**
*
* @type {Array<PlaygroundFeatureSchema>}
* @memberof PlaygroundResponseSchema
*/
features: Array<PlaygroundFeatureSchema>;
}
export interface PlaygroundRequestSchema {
/**
*
* @type {string}
* @memberof PlaygroundRequestSchema
*/
environment: string;
/**
*
* @type {PlaygroundRequestSchemaProjects}
* @memberof PlaygroundRequestSchema
*/
projects?: Array<string> | string;
/**
*
* @type {SdkContextSchema}
* @memberof PlaygroundRequestSchema
*/
context: SdkContextSchema;
}
export interface PlaygroundFeatureSchemaVariantPayload {
/**
*
* @type {string}
* @memberof PlaygroundFeatureSchemaVariantPayload
*/
type: PlaygroundFeatureSchemaVariantPayloadTypeEnum;
/**
*
* @type {string}
* @memberof PlaygroundFeatureSchemaVariantPayload
*/
value: string;
}
export interface PlaygroundFeatureSchemaVariant {
/**
*
* @type {string}
* @memberof PlaygroundFeatureSchemaVariant
*/
name: string;
/**
*
* @type {boolean}
* @memberof PlaygroundFeatureSchemaVariant
*/
enabled: boolean;
/**
*
* @type {PlaygroundFeatureSchemaVariantPayload}
* @memberof PlaygroundFeatureSchemaVariant
*/
payload?: PlaygroundFeatureSchemaVariantPayload;
}
export interface PlaygroundFeatureSchema {
/**
*
* @type {string}
* @memberof PlaygroundFeatureSchema
*/
name: string;
/**
*
* @type {string}
* @memberof PlaygroundFeatureSchema
*/
projectId: string;
/**
*
* @type {boolean}
* @memberof PlaygroundFeatureSchema
*/
isEnabled: boolean;
/**
*
* @type {PlaygroundFeatureSchemaVariant}
* @memberof PlaygroundFeatureSchema
*/
variant: PlaygroundFeatureSchemaVariant | null;
}
export interface PlaygroundResponseSchema {
/**
*
* @type {PlaygroundRequestSchema}
* @memberof PlaygroundResponseSchema
*/
input: PlaygroundRequestSchema;
/**
*
* @type {Array<PlaygroundFeatureSchema>}
* @memberof PlaygroundResponseSchema
*/
features: Array<PlaygroundFeatureSchema>;
}
export interface PlaygroundRequestSchema {
/**
*
* @type {string}
* @memberof PlaygroundRequestSchema
*/
environment: string;
/**
*
* @type Array<string> | string
* @memberof PlaygroundRequestSchema
*/
projects?: Array<string> | string;
/**
*
* @type {SdkContextSchema}
* @memberof PlaygroundRequestSchema
*/
context: SdkContextSchema;
}
export interface SdkContextSchema {
[key: string]: string | any;
/**
*
* @type {string}
* @memberof SdkContextSchema
*/
appName: string;
/**
*
* @type {Date}
* @memberof SdkContextSchema
*/
currentTime?: Date;
/**
*
* @type {string}
* @memberof SdkContextSchema
* @deprecated
*/
environment?: string;
/**
*
* @type {{ [key: string]: string; }}
* @memberof SdkContextSchema
*/
properties?: { [key: string]: string };
/**
*
* @type {string}
* @memberof SdkContextSchema
*/
remoteAddress?: string;
/**
*
* @type {string}
* @memberof SdkContextSchema
*/
sessionId?: string;
/**
*
* @type {string}
* @memberof SdkContextSchema
*/
userId?: string;
}

View File

@ -2,7 +2,7 @@ import useAPI from '../useApi/useApi';
import { import {
PlaygroundRequestSchema, PlaygroundRequestSchema,
PlaygroundResponseSchema, PlaygroundResponseSchema,
} from './playground.model'; } from '../../../../component/playground/Playground/interfaces/playground.model';
export const usePlaygroundApi = () => { export const usePlaygroundApi = () => {
const { makeRequest, createRequest, errors, loading } = useAPI({ const { makeRequest, createRequest, errors, loading } = useAPI({

View File

@ -9,7 +9,7 @@ const useHiddenColumns = (
useEffect(() => { useEffect(() => {
const hidden = condition ? hiddenColumns : []; const hidden = condition ? hiddenColumns : [];
setHiddenColumns(hidden); setHiddenColumns(hidden);
}, [setHiddenColumns, condition]); }, [setHiddenColumns, hiddenColumns, condition]);
}; };
export default useHiddenColumns; export default useHiddenColumns;