1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-13 11:17:26 +02:00

bug fixes, improvements and env handling

This commit is contained in:
andreas-unleash 2022-08-02 11:57:23 +03:00
parent 9fee273cff
commit 42fbc27148
19 changed files with 361 additions and 253 deletions

View File

@ -18,14 +18,14 @@ import {
import { useStyles } from '../ConstraintAccordion.styles'; import { useStyles } from '../ConstraintAccordion.styles';
import { import {
PlaygroundFeatureStrategyConstraintResult, PlaygroundFeatureStrategyConstraintResult,
SdkContextSchema, PlaygroundRequestSchema,
} from '../../../../hooks/api/actions/usePlayground/playground.model'; } from '../../../../hooks/api/actions/usePlayground/playground.model';
interface IConstraintAccordionViewProps { interface IConstraintAccordionViewProps {
constraint: IConstraint | PlaygroundFeatureStrategyConstraintResult; constraint: IConstraint | PlaygroundFeatureStrategyConstraintResult;
onDelete?: () => void; onDelete?: () => void;
onEdit?: () => void; onEdit?: () => void;
playgroundContext?: SdkContextSchema; playgroundInput?: PlaygroundRequestSchema;
maxLength?: number; maxLength?: number;
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
} }
@ -36,7 +36,7 @@ export const ConstraintAccordionView = ({
onDelete, onDelete,
sx = undefined, sx = undefined,
maxLength, maxLength,
playgroundContext, playgroundInput,
}: IConstraintAccordionViewProps) => { }: IConstraintAccordionViewProps) => {
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
const [expandable, setExpandable] = useState(true); const [expandable, setExpandable] = useState(true);
@ -46,7 +46,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);
@ -79,7 +78,7 @@ export const ConstraintAccordionView = ({
allowExpand={setExpandable} allowExpand={setExpandable}
expanded={expanded} expanded={expanded}
maxLength={maxLength ?? 112} maxLength={maxLength ?? 112}
playgroundContext={playgroundContext} playgroundInput={playgroundInput}
/> />
</AccordionSummary> </AccordionSummary>

View File

@ -5,7 +5,7 @@ import { ConstraintAccordionHeaderActions } from '../../ConstraintAccordionHeade
import { useStyles } from 'component/common/ConstraintAccordion/ConstraintAccordion.styles'; import { useStyles } from 'component/common/ConstraintAccordion/ConstraintAccordion.styles';
import { import {
PlaygroundFeatureStrategyConstraintResult, PlaygroundFeatureStrategyConstraintResult,
SdkContextSchema, PlaygroundRequestSchema,
} from '../../../../../hooks/api/actions/usePlayground/playground.model'; } from '../../../../../hooks/api/actions/usePlayground/playground.model';
interface IConstraintAccordionViewHeaderProps { interface IConstraintAccordionViewHeaderProps {
@ -15,7 +15,7 @@ interface IConstraintAccordionViewHeaderProps {
singleValue: boolean; singleValue: boolean;
expanded: boolean; expanded: boolean;
allowExpand: (shouldExpand: boolean) => void; allowExpand: (shouldExpand: boolean) => void;
playgroundContext?: SdkContextSchema; playgroundInput?: PlaygroundRequestSchema;
maxLength?: number; maxLength?: number;
} }
@ -27,7 +27,7 @@ export const ConstraintAccordionViewHeader = ({
allowExpand, allowExpand,
expanded, expanded,
maxLength, maxLength,
playgroundContext, playgroundInput,
}: IConstraintAccordionViewHeaderProps) => { }: IConstraintAccordionViewHeaderProps) => {
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
@ -41,7 +41,7 @@ export const ConstraintAccordionViewHeader = ({
expanded={expanded} expanded={expanded}
result={'result' in constraint ? constraint.result : undefined} result={'result' in constraint ? constraint.result : undefined}
maxLength={maxLength} maxLength={maxLength}
playgroundContext={playgroundContext} playgroundInput={playgroundInput}
/> />
<ConstraintAccordionHeaderActions <ConstraintAccordionHeaderActions
onEdit={onEdit} onEdit={onEdit}

View File

@ -7,7 +7,7 @@ import React from 'react';
import { IConstraint } from '../../../../../../interfaces/strategy'; import { IConstraint } from '../../../../../../interfaces/strategy';
import { useStyles } from '../../../ConstraintAccordion.styles'; import { useStyles } from '../../../ConstraintAccordion.styles';
import { CancelOutlined } from '@mui/icons-material'; import { CancelOutlined } from '@mui/icons-material';
import { SdkContextSchema } from '../../../../../../hooks/api/actions/usePlayground/playground.model'; import { PlaygroundRequestSchema } from '../../../../../../hooks/api/actions/usePlayground/playground.model';
const StyledHeaderText = styled('span')(({ theme }) => ({ const StyledHeaderText = styled('span')(({ theme }) => ({
display: '-webkit-box', display: '-webkit-box',
@ -43,7 +43,7 @@ interface ConstraintAccordionViewHeaderMetaInfoProps {
allowExpand: (shouldExpand: boolean) => void; allowExpand: (shouldExpand: boolean) => void;
result?: boolean; result?: boolean;
maxLength?: number; maxLength?: number;
playgroundContext?: SdkContextSchema; playgroundInput?: PlaygroundRequestSchema;
} }
export const ConstraintAccordionViewHeaderInfo = ({ export const ConstraintAccordionViewHeaderInfo = ({
@ -52,12 +52,17 @@ export const ConstraintAccordionViewHeaderInfo = ({
allowExpand, allowExpand,
expanded, expanded,
result, result,
playgroundInput,
maxLength = 112, maxLength = 112,
playgroundContext,
}: ConstraintAccordionViewHeaderMetaInfoProps) => { }: ConstraintAccordionViewHeaderMetaInfoProps) => {
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
const theme = useTheme(); const theme = useTheme();
const isPlayground = Boolean(playgroundInput);
const constrainExistsInContext =
isPlayground &&
Boolean(playgroundInput?.context[constraint.contextName]);
return ( return (
<StyledHeaderWrapper> <StyledHeaderWrapper>
<div className={styles.headerMetaInfo}> <div className={styles.headerMetaInfo}>
@ -65,24 +70,17 @@ export const ConstraintAccordionViewHeaderInfo = ({
<StyledHeaderText> <StyledHeaderText>
{constraint.contextName} {constraint.contextName}
<ConditionallyRender <ConditionallyRender
condition={ condition={isPlayground}
result !== undefined &&
Boolean(playgroundContext)
}
show={ show={
<Typography <Typography
variant={'body1'} variant={'body1'}
color={ color={
Boolean( constrainExistsInContext
playgroundContext![
constraint.contextName
]
)
? theme.palette.neutral.dark ? theme.palette.neutral.dark
: theme.palette.error.main : theme.palette.error.main
} }
> >
{playgroundContext![ {playgroundInput?.context[
constraint.contextName constraint.contextName
] || 'no value'} ] || 'no value'}
</Typography> </Typography>

View File

@ -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,17 +1,21 @@
import { import {
PlaygroundFeatureSchema, PlaygroundFeatureSchema,
PlaygroundFeatureStrategyResult, PlaygroundRequestSchema,
} from 'hooks/api/actions/usePlayground/playground.model'; } from 'hooks/api/actions/usePlayground/playground.model';
import { IconButton, Popover, styled, Typography } from '@mui/material'; import { IconButton, Popover, styled } from '@mui/material';
import { InfoOutlined } from '@mui/icons-material'; import { InfoOutlined } from '@mui/icons-material';
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import { ConditionallyRender } from '../../../../common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from '../../../../common/ConditionallyRender/ConditionallyRender';
import { PlaygroundResultFeatureStrategyItem } from './PlaygroundResultFeatureStrategyItem/PlaygroundResultFeatureStrategyItem';
import { useStyles } from './FeatureResultInfoPopoverCell.styles'; import { useStyles } from './FeatureResultInfoPopoverCell.styles';
import { PlaygroundResultFeatureDetails } from './PlaygroundResultFeatureDetails/PlaygroundResultFeatureDetails'; import { PlaygroundResultFeatureDetails } from './PlaygroundResultFeatureDetails/PlaygroundResultFeatureDetails';
import {
PlaygroundResultStrategyList,
WrappedPlaygroundResultStrategyList,
} from './PlaygroundResultStrategyList/PlaygroundResultStrategyList';
interface FeatureResultInfoPopoverCellProps { interface FeatureResultInfoPopoverCellProps {
feature?: PlaygroundFeatureSchema; feature: PlaygroundFeatureSchema;
input?: PlaygroundRequestSchema;
} }
const FeatureResultPopoverWrapper = styled('div')(({ theme }) => ({ const FeatureResultPopoverWrapper = styled('div')(({ theme }) => ({
@ -21,6 +25,7 @@ const FeatureResultPopoverWrapper = styled('div')(({ theme }) => ({
export const FeatureResultInfoPopoverCell = ({ export const FeatureResultInfoPopoverCell = ({
feature, feature,
input,
}: FeatureResultInfoPopoverCellProps) => { }: FeatureResultInfoPopoverCellProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
@ -30,77 +35,6 @@ export const FeatureResultInfoPopoverCell = ({
setOpen(!open); setOpen(!open);
}; };
const dummyPlaygroundFeatureTrue: PlaygroundFeatureSchema = {
name: feature?.name as any,
projectId: 'default',
isEnabled: true,
} as any;
const dummyPlaygroundFeatureFalse: PlaygroundFeatureSchema = {
name: feature?.name as any,
projectId: 'default',
isEnabled: false,
} as any;
const strategies: PlaygroundFeatureStrategyResult[] = [
{
name: 'default',
id: 'strategy-id',
parameters: {},
result: feature?.isEnabled as any,
constraints: [
{
result: true,
contextName: 'appName',
operator: 'IN',
caseInsensitive: false,
inverted: false,
values: ['MyApp', 'MyOtherApp', 'Unleash'],
},
],
segments: [
{
result: feature?.isEnabled as any,
id: 5,
name: 'my-segment',
constraints: [
{
result: feature?.isEnabled as any,
contextName: 'environment',
operator: 'IN',
caseInsensitive: false,
inverted: false,
values: ['development'],
},
],
},
],
},
{
name: 'flexibleRollout',
id: 'strategy-id',
parameters: {},
result: false,
segments: [
{
result: false,
id: 6,
name: 'my-segment',
constraints: [
{
result: false,
contextName: 'appName',
operator: 'IN',
caseInsensitive: false,
inverted: false,
values: ['MyApp2'],
},
],
},
],
},
];
if (!feature) { if (!feature) {
return null; return null;
} }
@ -125,29 +59,24 @@ export const FeatureResultInfoPopoverCell = ({
classes={{ paper: styles.popoverPaper }} classes={{ paper: styles.popoverPaper }}
> >
<PlaygroundResultFeatureDetails <PlaygroundResultFeatureDetails
feature={ feature={feature}
feature.isEnabled input={input}
? dummyPlaygroundFeatureTrue
: dummyPlaygroundFeatureFalse
}
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
/> />
<ConditionallyRender <ConditionallyRender
condition={strategies.length > 0} condition={!feature.isEnabledInCurrentEnvironment}
show={ show={
<> <PlaygroundResultStrategyList
<Typography strategies={feature?.strategies}
variant={'subtitle1'} input={input}
sx={{ mb: 2, color: 'text.secondary' }} />
>{`Strategies (${strategies.length})`}</Typography> }
{strategies.map((strategy, index) => ( elseShow={
<PlaygroundResultFeatureStrategyItem <WrappedPlaygroundResultStrategyList
key={strategy.id} strategies={feature?.strategies}
strategy={strategy} feature={feature}
index={index} input={input}
/> />
))}
</>
} }
/> />
</Popover> </Popover>

View File

@ -9,9 +9,11 @@ export const useStyles = makeStyles()(theme => ({
titleRow: { titleRow: {
display: 'inline-flex', display: 'inline-flex',
alignItems: 'flex-start', alignItems: 'flex-start',
justifyItems: 'center', justifyContent: 'center',
gap: '12px', gap: '12px',
marginTop: '12px',
}, },
alertRow: {},
descriptionRow: { descriptionRow: {
marginBottom: theme.spacing(2), marginBottom: theme.spacing(2),
}, },

View File

@ -1,30 +1,45 @@
import { PlaygroundFeatureSchema } from '../../../../../../hooks/api/actions/usePlayground/playground.model'; import {
import { IconButton, Typography, useTheme } from '@mui/material'; PlaygroundFeatureSchema,
PlaygroundRequestSchema,
} from '../../../../../../hooks/api/actions/usePlayground/playground.model';
import { Alert, IconButton, Typography, useTheme } from '@mui/material';
import { PlaygroundResultChip } from '../../PlaygroundResultChip/PlaygroundResultChip'; import { PlaygroundResultChip } from '../../PlaygroundResultChip/PlaygroundResultChip';
import { useStyles } from './PlaygroundResultFeatureDetails.styles'; import { useStyles } from './PlaygroundResultFeatureDetails.styles';
import { CloseOutlined } from '@mui/icons-material'; import { CloseOutlined } from '@mui/icons-material';
import React from 'react'; import React from 'react';
import { ConditionallyRender } from '../../../../../common/ConditionallyRender/ConditionallyRender';
import { checkForEmptyValues } from './helpers';
interface PlaygroundFeatureResultDetailsProps { interface PlaygroundFeatureResultDetailsProps {
feature: PlaygroundFeatureSchema; feature: PlaygroundFeatureSchema;
input?: PlaygroundRequestSchema;
onClose: () => void; onClose: () => void;
} }
export const PlaygroundResultFeatureDetails = ({ export const PlaygroundResultFeatureDetails = ({
feature, feature,
input,
onClose, onClose,
}: PlaygroundFeatureResultDetailsProps) => { }: PlaygroundFeatureResultDetailsProps) => {
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
const theme = useTheme(); const theme = useTheme();
const description = feature.isEnabled const description = Boolean(feature.isEnabled)
? 'This feature toggle is True in production because ' ? `This feature toggle is True in ${input?.environment} because `
: 'This feature toggle is False in production because '; : `This feature toggle is False in ${input?.environment} because `;
const reason = feature.isEnabled
const reason = Boolean(feature.isEnabled)
? 'at least one strategy is True' ? 'at least one strategy is True'
: feature?.isEnabledInCurrentEnvironment
? 'the environment is disabled'
: 'all strategies are False'; : 'all strategies are False';
const color = feature.isEnabled
const color = Boolean(feature.isEnabled)
? theme.palette.success.main ? theme.palette.success.main
: theme.palette.error.main; : theme.palette.error.main;
const noValueTxt = checkForEmptyValues(input?.context)
? 'You did not provide a value for your context filed in step 2 of the configuration'
: undefined;
const onCloseClick = const onCloseClick =
onClose && onClose &&
((event: React.SyntheticEvent) => { ((event: React.SyntheticEvent) => {
@ -55,6 +70,14 @@ export const PlaygroundResultFeatureDetails = ({
{reason} {reason}
</Typography> </Typography>
</div> </div>
<ConditionallyRender
condition={Boolean(noValueTxt)}
show={
<div className={styles.alertRow}>
<Alert color={'info'}>{noValueTxt}</Alert>
</div>
}
/>
</> </>
); );
}; };

View File

@ -0,0 +1,8 @@
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
);
}

View File

@ -1,16 +1,18 @@
import { import {
PlaygroundFeatureStrategyConstraintResult, PlaygroundFeatureStrategyConstraintResult,
SdkContextSchema, PlaygroundRequestSchema,
} from 'hooks/api/actions/usePlayground/playground.model'; } from 'hooks/api/actions/usePlayground/playground.model';
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import { objectId } from '../../../../../../utils/objectId'; import { objectId } from '../../../../../../../utils/objectId';
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 { ConstraintAccordionView } from '../../../../../common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView'; import { ConstraintAccordionView } from '../../../../../../common/ConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
interface PlaygroundResultConstraintExecutionProps { interface PlaygroundResultConstraintExecutionProps {
constraints?: PlaygroundFeatureStrategyConstraintResult[]; constraints?: PlaygroundFeatureStrategyConstraintResult[];
compact: boolean;
input?: PlaygroundRequestSchema;
} }
export const PlaygroundResultConstraintExecutionWrapper = styled('div')( export const PlaygroundResultConstraintExecutionWrapper = styled('div')(
@ -23,18 +25,9 @@ export const PlaygroundResultConstraintExecutionWrapper = styled('div')(
export const PlaygroundResultConstraintExecution = ({ export const PlaygroundResultConstraintExecution = ({
constraints, constraints,
compact,
input,
}: PlaygroundResultConstraintExecutionProps) => { }: PlaygroundResultConstraintExecutionProps) => {
// const context = usePlaygroundContext();
const contextFalse: SdkContextSchema = {
appName: 'MyApp',
environment: '',
};
const contextTrue: SdkContextSchema = {
appName: 'MyApp',
environment: 'development',
};
if (!constraints) return null; if (!constraints) return null;
return ( return (
@ -47,12 +40,8 @@ export const PlaygroundResultConstraintExecution = ({
/> />
<ConstraintAccordionView <ConstraintAccordionView
constraint={constraint} constraint={constraint}
playgroundContext={ playgroundInput={input}
Boolean(constraint.result) maxLength={compact ? 25 : 50}
? contextTrue
: contextFalse
}
maxLength={80}
sx={{ sx={{
backgroundColor: 'transparent!important', backgroundColor: 'transparent!important',
}} }}

View File

@ -28,6 +28,7 @@ export const useStyles = makeStyles()(theme => ({
[theme.breakpoints.down(400)]: { [theme.breakpoints.down(400)]: {
padding: '0.5rem', padding: '0.5rem',
}, },
width: '100%',
paddingBottom: '1rem', paddingBottom: '1rem',
borderRadius: theme.shape.borderRadiusMedium, borderRadius: theme.shape.borderRadiusMedium,
background: theme.palette.background.default, background: theme.palette.background.default,

View File

@ -1,4 +1,4 @@
import { Box, useTheme } from '@mui/material'; import { Box, styled, Typography, useTheme } 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 { import {
@ -7,18 +7,33 @@ import {
} from 'utils/strategyNames'; } from 'utils/strategyNames';
import StringTruncator from 'component/common/StringTruncator/StringTruncator'; import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import { PlaygroundResultChip } from '../../PlaygroundResultChip/PlaygroundResultChip'; import { PlaygroundResultChip } from '../../PlaygroundResultChip/PlaygroundResultChip';
import { PlaygroundFeatureStrategyResult } from 'hooks/api/actions/usePlayground/playground.model'; import {
import { PlaygroundResultStrategyExecution } from '../PlaygroundResultStrategyExecution/PlaygroundResultStrategyExecution'; PlaygroundFeatureStrategyResult,
PlaygroundRequestSchema,
} from 'hooks/api/actions/usePlayground/playground.model';
import { PlaygroundResultStrategyExecution } from './PlaygroundResultStrategyExecution/PlaygroundResultStrategyExecution';
import { useStyles } from './PlaygroundResultFeatureStrategyItem.styles'; import { useStyles } from './PlaygroundResultFeatureStrategyItem.styles';
interface IPlaygroundResultFeatureStrategyItemProps { interface IPlaygroundResultFeatureStrategyItemProps {
strategy: PlaygroundFeatureStrategyResult; strategy: PlaygroundFeatureStrategyResult;
index: number; index: number;
input?: PlaygroundRequestSchema;
compact: boolean;
} }
const StyledItemWrapper = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
marginTop: '4px',
gap: '4px',
}));
export const PlaygroundResultFeatureStrategyItem = ({ export const PlaygroundResultFeatureStrategyItem = ({
strategy, strategy,
input,
index, index,
compact,
}: IPlaygroundResultFeatureStrategyItemProps) => { }: IPlaygroundResultFeatureStrategyItemProps) => {
const { result, name } = strategy; const { result, name } = strategy;
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
@ -31,34 +46,44 @@ export const PlaygroundResultFeatureStrategyItem = ({
: `1px solid ${theme.palette.divider}`; : `1px solid ${theme.palette.divider}`;
return ( return (
<Box key={strategy.id} sx={{ width: '100%', position: 'relative' }}> <Box
sx={{
width: '100%',
position: 'relative',
paddingRight: compact ? '12px' : 0,
}}
>
<ConditionallyRender <ConditionallyRender
condition={index > 0} condition={index > 0}
show={<StrategySeparator text="OR" />} show={<StrategySeparator text="OR" />}
/> />
<Box className={styles.innerContainer} sx={{ border }}> <StyledItemWrapper>
<div className={styles.header}> <Typography variant={'subtitle1'}>{index + 1}</Typography>
<div className={styles.headerName}> <Box className={styles.innerContainer} sx={{ border }}>
<Icon className={styles.icon} /> <div className={styles.header}>
<StringTruncator <div className={styles.headerName}>
maxWidth="150" <Icon className={styles.icon} />
maxLength={15} <StringTruncator
text={formatStrategyName(name)} maxWidth="150"
maxLength={15}
text={formatStrategyName(name)}
/>
</div>
<PlaygroundResultChip
showIcon={false}
enabled={Boolean(result)}
label={label}
/> />
</div> </div>
<PlaygroundResultChip <div className={styles.body}>
showIcon={false} <PlaygroundResultStrategyExecution
enabled={Boolean(result)} strategyResult={strategy}
label={label} input={input}
/> percentageFill={theme.palette.tertiary.light}
</div> />
<div className={styles.body}> </div>
<PlaygroundResultStrategyExecution </Box>
strategyResult={strategy} </StyledItemWrapper>
percentageFill={theme.palette.tertiary.light}
/>
</div>
</Box>
</Box> </Box>
); );
}; };

View File

@ -1,14 +1,19 @@
import { PlaygroundFeatureStrategySegmentResult } from '../../../../../../hooks/api/actions/usePlayground/playground.model'; import {
PlaygroundFeatureStrategySegmentResult,
PlaygroundRequestSchema,
} from '../../../../../../../hooks/api/actions/usePlayground/playground.model';
import { PlaygroundResultConstraintExecution } from '../PlaygroundResultConstraintExecution/PlaygroundResultConstraintExecution'; import { PlaygroundResultConstraintExecution } from '../PlaygroundResultConstraintExecution/PlaygroundResultConstraintExecution';
import { CancelOutlined, DonutLarge } from '@mui/icons-material'; import { CancelOutlined, DonutLarge } from '@mui/icons-material';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { StrategySeparator } from '../../../../../common/StrategySeparator/StrategySeparator'; import { StrategySeparator } from '../../../../../../common/StrategySeparator/StrategySeparator';
import { useStyles } from './PlaygroundResultSegmentExecution.styles'; import { useStyles } from './PlaygroundResultSegmentExecution.styles';
import { styled, Typography } from '@mui/material'; import { styled, Typography } from '@mui/material';
import { ConditionallyRender } from '../../../../../common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from '../../../../../../common/ConditionallyRender/ConditionallyRender';
interface PlaygroundResultSegmentExecutionProps { interface PlaygroundResultSegmentExecutionProps {
segments?: PlaygroundFeatureStrategySegmentResult[]; segments?: PlaygroundFeatureStrategySegmentResult[];
input?: PlaygroundRequestSchema;
hasConstraints: boolean;
} }
const SegmentExecutionLinkWrapper = styled('div')(({ theme }) => ({ const SegmentExecutionLinkWrapper = styled('div')(({ theme }) => ({
@ -55,8 +60,11 @@ const SegmentResultTextWrapper = styled('div')(({ theme }) => ({
export const PlaygroundResultSegmentExecution = ({ export const PlaygroundResultSegmentExecution = ({
segments, segments,
input,
hasConstraints,
}: PlaygroundResultSegmentExecutionProps) => { }: PlaygroundResultSegmentExecutionProps) => {
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
if (!segments) return null; if (!segments) return null;
return ( return (
<> <>
@ -93,10 +101,14 @@ export const PlaygroundResultSegmentExecution = ({
<SegmentExecutionConstraintWrapper> <SegmentExecutionConstraintWrapper>
<PlaygroundResultConstraintExecution <PlaygroundResultConstraintExecution
constraints={segment.constraints} constraints={segment.constraints}
compact={true}
input={input}
/> />
</SegmentExecutionConstraintWrapper> </SegmentExecutionConstraintWrapper>
<ConditionallyRender <ConditionallyRender
condition={index < segments?.length - 1} condition={
index === segments?.length - 1 && hasConstraints
}
show={<StrategySeparator text="AND" sx={{ pt: 1 }} />} show={<StrategySeparator text="AND" sx={{ pt: 1 }} />}
/> />
</SegmentExecutionWrapper> </SegmentExecutionWrapper>

View File

@ -10,7 +10,7 @@ export const useStyles = makeStyles()(theme => ({
color: theme.palette.grey[700], color: theme.palette.grey[700],
}, },
summary: { summary: {
width: '100%', width: 'auto',
padding: theme.spacing(2, 3), padding: theme.spacing(2, 3),
borderRadius: theme.shape.borderRadius, borderRadius: theme.shape.borderRadius,
border: `1px solid ${theme.palette.divider}`, border: `1px solid ${theme.palette.divider}`,

View File

@ -1,9 +1,12 @@
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 { Box, Chip } from '@mui/material'; import { Box, Chip, styled } from '@mui/material';
import { useStyles } from './PlaygroundResultStrategyExecution.styles'; import { useStyles } from './PlaygroundResultStrategyExecution.styles';
import { PlaygroundFeatureStrategyResult } from '../../../../../../hooks/api/actions/usePlayground/playground.model'; import {
import useUiConfig from '../../../../../../hooks/api/getters/useUiConfig/useUiConfig'; PlaygroundFeatureStrategyResult,
PlaygroundRequestSchema,
} from '../../../../../../../hooks/api/actions/usePlayground/playground.model';
import useUiConfig from '../../../../../../../hooks/api/getters/useUiConfig/useUiConfig';
import React from 'react'; import React from 'react';
import { PlaygroundResultConstraintExecution } from '../PlaygroundResultConstraintExecution/PlaygroundResultConstraintExecution'; import { PlaygroundResultConstraintExecution } from '../PlaygroundResultConstraintExecution/PlaygroundResultConstraintExecution';
import { PlaygroundResultSegmentExecution } from '../PlaygroundResultSegmentExecution/PlaygroundResultSegmentExecution'; import { PlaygroundResultSegmentExecution } from '../PlaygroundResultSegmentExecution/PlaygroundResultSegmentExecution';
@ -11,24 +14,38 @@ import { PlaygroundResultSegmentExecution } from '../PlaygroundResultSegmentExec
interface PlaygroundResultStrategyExecutionProps { interface PlaygroundResultStrategyExecutionProps {
strategyResult: PlaygroundFeatureStrategyResult; strategyResult: PlaygroundFeatureStrategyResult;
percentageFill?: string; percentageFill?: string;
input?: PlaygroundRequestSchema;
} }
const StyledStrategyExecutionWrapper = styled('div')(({ theme }) => ({
padding: theme.spacing(1),
}));
export const PlaygroundResultStrategyExecution = ({ export const PlaygroundResultStrategyExecution = ({
strategyResult, strategyResult,
input,
}: PlaygroundResultStrategyExecutionProps) => { }: PlaygroundResultStrategyExecutionProps) => {
const { name, constraints, segments } = strategyResult; const { name, constraints, segments } = strategyResult;
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
const hasConstraints = Boolean(constraints && constraints.length > 0);
return ( return (
<> <StyledStrategyExecutionWrapper>
<ConditionallyRender <ConditionallyRender
condition={ condition={
Boolean(uiConfig.flags.SE) && Boolean(uiConfig.flags.SE) &&
Boolean(segments && segments.length > 0) Boolean(segments && segments.length > 0)
} }
show={<PlaygroundResultSegmentExecution segments={segments} />} show={
<PlaygroundResultSegmentExecution
segments={segments}
hasConstraints={hasConstraints}
input={input}
/>
}
/> />
<ConditionallyRender <ConditionallyRender
condition={Boolean(constraints && constraints.length > 0)} condition={Boolean(constraints && constraints.length > 0)}
@ -36,6 +53,8 @@ export const PlaygroundResultStrategyExecution = ({
<> <>
<PlaygroundResultConstraintExecution <PlaygroundResultConstraintExecution
constraints={constraints} constraints={constraints}
compact={true}
input={input}
/> />
<StrategySeparator text="AND" /> <StrategySeparator text="AND" />
</> </>
@ -56,6 +75,6 @@ export const PlaygroundResultStrategyExecution = ({
</Box> </Box>
} }
/> />
</> </StyledStrategyExecutionWrapper>
); );
}; };

View File

@ -0,0 +1,88 @@
import {
PlaygroundFeatureSchema,
PlaygroundFeatureStrategyResult,
PlaygroundRequestSchema,
} from '../../../../../../hooks/api/actions/usePlayground/playground.model';
import { ConditionallyRender } from '../../../../../common/ConditionallyRender/ConditionallyRender';
import { Alert, styled, Typography } from '@mui/material';
import { PlaygroundResultFeatureStrategyItem } from '../PlaygroundResultFeatureStrategyItem/PlaygroundResultFeatureStrategyItem';
interface PlaygroundResultStrategyListProps {
strategies: PlaygroundFeatureStrategyResult[];
input?: PlaygroundRequestSchema;
compact?: boolean;
}
export const PlaygroundResultStrategyList = ({
strategies,
input,
compact = false,
}: PlaygroundResultStrategyListProps) => {
return (
<ConditionallyRender
condition={strategies.length > 0}
show={
<>
<Typography
variant={'subtitle1'}
sx={{ mt: 2, ml: 1, mb: 2, color: 'text.secondary' }}
>{`Strategies (${strategies.length})`}</Typography>
{strategies.map((strategy, index) => (
<PlaygroundResultFeatureStrategyItem
key={strategy.id}
strategy={strategy}
index={index}
compact={compact}
input={input}
/>
))}
</>
}
/>
);
};
const StyledAlertWrapper = styled('div')(({ theme }) => ({
width: '100%',
display: 'flex',
flexDirection: 'column',
borderRadius: theme.shape.borderRadiusMedium,
border: `1px solid ${theme.palette.info.main}`,
}));
const StyledListWrapper = styled('div')(({ theme }) => ({
padding: theme.spacing(1, 0.5),
}));
const StyledAlert = styled(Alert)(({ theme }) => ({
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
}));
interface WrappedPlaygroundResultStrategyListProps
extends PlaygroundResultStrategyListProps {
feature: PlaygroundFeatureSchema;
}
export const WrappedPlaygroundResultStrategyList = ({
strategies,
feature,
input,
}: WrappedPlaygroundResultStrategyListProps) => {
return (
<StyledAlertWrapper>
<StyledAlert severity={'info'} color={'info'}>
If environment would be enabled then this feature would be{' '}
{feature.isEnabled ? 'TRUE' : 'FALSE'} and the strategies would
evaluate like this:{' '}
</StyledAlert>
<StyledListWrapper>
<PlaygroundResultStrategyList
strategies={strategies}
input={input}
compact
/>
</StyledListWrapper>
</StyledAlertWrapper>
);
};

View File

@ -6,7 +6,7 @@ import { ReactComponent as FeatureEnabledIcon } from '../../../../../assets/icon
import { ReactComponent as FeatureDisabledIcon } from '../../../../../assets/icons/isenabled-false.svg'; import { ReactComponent as FeatureDisabledIcon } from '../../../../../assets/icons/isenabled-false.svg';
interface IResultChipProps { interface IResultChipProps {
enabled: boolean; enabled: boolean | 'unevaluated';
// Result icon - defaults to true // Result icon - defaults to true
showIcon?: boolean; showIcon?: boolean;
label?: string; label?: string;
@ -53,7 +53,7 @@ export const PlaygroundResultChip = ({
const theme = useTheme(); const theme = useTheme();
const icon = ( const icon = (
<ConditionallyRender <ConditionallyRender
condition={enabled} condition={Boolean(enabled)}
show={ show={
<FeatureEnabledIcon <FeatureEnabledIcon
color={theme.palette.success.main} color={theme.palette.success.main}
@ -73,7 +73,7 @@ export const PlaygroundResultChip = ({
return ( return (
<ConditionallyRender <ConditionallyRender
condition={enabled} condition={Boolean(enabled)}
show={ show={
<StyledTrueChip <StyledTrueChip
icon={showIcon ? icon : undefined} icon={showIcon ? icon : undefined}

View File

@ -19,7 +19,10 @@ 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 'hooks/api/actions/usePlayground/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';
@ -33,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();
@ -49,6 +54,75 @@ 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: ({ value }: any) => <FeatureStatusCell enabled={value} />,
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,
@ -236,67 +310,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,
},
{
Header: '',
id: 'info',
Cell: ({ row }: any) => (
<FeatureResultInfoPopoverCell feature={row.original} />
),
},
];

View File

@ -65,13 +65,17 @@ export interface PlaygroundFeatureSchema {
* @type {boolean} * @type {boolean}
* @memberof PlaygroundFeatureSchema * @memberof PlaygroundFeatureSchema
*/ */
isEnabled: boolean; isEnabled: boolean | 'unevaluated';
isEnabledInCurrentEnvironment: boolean;
/** /**
* *
* @type {PlaygroundFeatureSchemaVariant} * @type {PlaygroundFeatureSchemaVariant}
* @memberof PlaygroundFeatureSchema * @memberof PlaygroundFeatureSchema
*/ */
variant: PlaygroundFeatureSchemaVariant | null; variant: PlaygroundFeatureSchemaVariant | null;
strategies: PlaygroundFeatureStrategyResult[];
} }
export interface PlaygroundResponseSchema { export interface PlaygroundResponseSchema {
/** /**