mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-14 01:16:17 +02:00
Constraint values list (#9592)
This commit is contained in:
parent
e018ee2f34
commit
dd62b3dbcd
@ -1,12 +1,14 @@
|
|||||||
import type { FC } from 'react';
|
import type { ComponentProps, FC } from 'react';
|
||||||
import {
|
import { StrategyEvaluationItem } from '../StrategyEvaluationItem/StrategyEvaluationItem';
|
||||||
StrategyEvaluationItem,
|
|
||||||
type StrategyEvaluationItemProps,
|
|
||||||
} from '../StrategyEvaluationItem/StrategyEvaluationItem';
|
|
||||||
import type { ConstraintSchema } from 'openapi';
|
import type { ConstraintSchema } from 'openapi';
|
||||||
import { formatOperatorDescription } from 'component/common/ConstraintAccordion/ConstraintOperator/formatOperatorDescription';
|
import { formatOperatorDescription } from 'component/common/ConstraintAccordion/ConstraintOperator/formatOperatorDescription';
|
||||||
import { StrategyEvaluationChip } from '../StrategyEvaluationChip/StrategyEvaluationChip';
|
import { StrategyEvaluationChip } from '../StrategyEvaluationChip/StrategyEvaluationChip';
|
||||||
import { styled, Tooltip } from '@mui/material';
|
import { styled, Tooltip } from '@mui/material';
|
||||||
|
import { Truncator } from 'component/common/Truncator/Truncator';
|
||||||
|
import { ValuesList } from '../ValuesList/ValuesList';
|
||||||
|
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||||
|
import { formatConstraintValue } from 'utils/formatConstraintValue';
|
||||||
|
import { useConstraintTooltips } from './hooks/useConstraintTooltips';
|
||||||
import { ReactComponent as CaseSensitiveIcon } from 'assets/icons/case-sensitive.svg';
|
import { ReactComponent as CaseSensitiveIcon } from 'assets/icons/case-sensitive.svg';
|
||||||
import { isCaseSensitive } from './isCaseSensitive';
|
import { isCaseSensitive } from './isCaseSensitive';
|
||||||
|
|
||||||
@ -52,32 +54,45 @@ const StyledOperatorGroup = styled('div')(({ theme }) => ({
|
|||||||
gap: theme.spacing(0.5),
|
gap: theme.spacing(0.5),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const StyledConstraintName = styled('div')(({ theme }) => ({
|
||||||
|
maxWidth: '150px',
|
||||||
|
paddingRight: theme.spacing(0.5),
|
||||||
|
overflow: 'hidden',
|
||||||
|
}));
|
||||||
|
|
||||||
export const ConstraintItemHeader: FC<
|
export const ConstraintItemHeader: FC<
|
||||||
ConstraintSchema & Pick<StrategyEvaluationItemProps, 'onSetTruncated'>
|
ConstraintSchema & Pick<ComponentProps<typeof ValuesList>, 'onSetTruncated'>
|
||||||
> = ({
|
> = ({ onSetTruncated, ...constraint }) => {
|
||||||
caseInsensitive,
|
const { caseInsensitive, contextName, inverted, operator, value, values } =
|
||||||
contextName,
|
constraint;
|
||||||
inverted,
|
const { locationSettings } = useLocationSettings();
|
||||||
operator,
|
const items = value
|
||||||
value,
|
? [
|
||||||
values,
|
formatConstraintValue(constraint, locationSettings) || '',
|
||||||
onSetTruncated,
|
...(values || []),
|
||||||
}) => {
|
]
|
||||||
const items = value ? [value, ...(values || [])] : values || [];
|
: values || [];
|
||||||
|
|
||||||
|
const tooltips = useConstraintTooltips(contextName, values || []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StrategyEvaluationItem
|
<StrategyEvaluationItem type='Constraint'>
|
||||||
type='Constraint'
|
<StyledConstraintName>
|
||||||
values={items}
|
<Truncator lines={2} title={contextName} arrow>
|
||||||
onSetTruncated={onSetTruncated}
|
{contextName}
|
||||||
>
|
</Truncator>
|
||||||
{contextName}
|
</StyledConstraintName>
|
||||||
<StyledOperatorGroup>
|
<StyledOperatorGroup>
|
||||||
<Operator label={operator} inverted={inverted} />
|
<Operator label={operator} inverted={inverted} />
|
||||||
{isCaseSensitive(operator, caseInsensitive) ? (
|
{isCaseSensitive(operator, caseInsensitive) ? (
|
||||||
<CaseSensitive />
|
<CaseSensitive />
|
||||||
) : null}
|
) : null}
|
||||||
</StyledOperatorGroup>
|
</StyledOperatorGroup>
|
||||||
|
<ValuesList
|
||||||
|
values={items}
|
||||||
|
onSetTruncated={onSetTruncated}
|
||||||
|
tooltips={tooltips}
|
||||||
|
/>
|
||||||
</StrategyEvaluationItem>
|
</StrategyEvaluationItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,99 @@
|
|||||||
|
import { vi } from 'vitest';
|
||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { useConstraintTooltips } from './useConstraintTooltips';
|
||||||
|
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
|
||||||
|
|
||||||
|
vi.mock('hooks/api/getters/useUnleashContext/useUnleashContext', () => ({
|
||||||
|
default: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('useConstraintTooltips', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns tooltip mapping for legal values with descriptions', () => {
|
||||||
|
(
|
||||||
|
useUnleashContext as unknown as ReturnType<typeof vi.fn>
|
||||||
|
).mockReturnValue({
|
||||||
|
context: [
|
||||||
|
{
|
||||||
|
name: 'contextA',
|
||||||
|
description: 'Test context A',
|
||||||
|
createdAt: '2021-01-01',
|
||||||
|
sortOrder: 1,
|
||||||
|
stickiness: false,
|
||||||
|
legalValues: [
|
||||||
|
{ value: 'value1', description: 'Tooltip 1' },
|
||||||
|
{ value: 'value2', description: 'Tooltip 2' },
|
||||||
|
{ value: 'value3' }, // No description provided
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useConstraintTooltips('contextA', [
|
||||||
|
'value1',
|
||||||
|
'value2',
|
||||||
|
'value3',
|
||||||
|
'nonExisting',
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
value1: 'Tooltip 1',
|
||||||
|
value2: 'Tooltip 2',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an empty object when the context is not found', () => {
|
||||||
|
(
|
||||||
|
useUnleashContext as unknown as ReturnType<typeof vi.fn>
|
||||||
|
).mockReturnValue({
|
||||||
|
context: [
|
||||||
|
{
|
||||||
|
name: 'otherContext',
|
||||||
|
description: 'Other context',
|
||||||
|
createdAt: '2021-01-01',
|
||||||
|
sortOrder: 1,
|
||||||
|
stickiness: false,
|
||||||
|
legalValues: [
|
||||||
|
{ value: 'value1', description: 'Tooltip 1' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useConstraintTooltips('contextA', ['value1']),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.current).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an empty object when no values are provided', () => {
|
||||||
|
(
|
||||||
|
useUnleashContext as unknown as ReturnType<typeof vi.fn>
|
||||||
|
).mockReturnValue({
|
||||||
|
context: [
|
||||||
|
{
|
||||||
|
name: 'contextA',
|
||||||
|
description: 'Test context A',
|
||||||
|
createdAt: '2021-01-01',
|
||||||
|
sortOrder: 1,
|
||||||
|
stickiness: false,
|
||||||
|
legalValues: [
|
||||||
|
{ value: 'value1', description: 'Tooltip 1' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useConstraintTooltips('contextA', []),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.current).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,27 @@
|
|||||||
|
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
export const useConstraintTooltips = (
|
||||||
|
contextName: string,
|
||||||
|
values: string[],
|
||||||
|
) => {
|
||||||
|
const { context } = useUnleashContext();
|
||||||
|
const contextDefinition = useMemo(
|
||||||
|
() => context.find(({ name }) => name === contextName),
|
||||||
|
[contextName, context],
|
||||||
|
);
|
||||||
|
return useMemo<Record<string, string>>(
|
||||||
|
() =>
|
||||||
|
Object.fromEntries(
|
||||||
|
values
|
||||||
|
?.map((item) => [
|
||||||
|
item,
|
||||||
|
contextDefinition?.legalValues?.find(
|
||||||
|
({ value }) => value === item,
|
||||||
|
)?.description,
|
||||||
|
])
|
||||||
|
.filter(([_, tooltip]) => !!tooltip) || [],
|
||||||
|
),
|
||||||
|
[context, values],
|
||||||
|
);
|
||||||
|
};
|
@ -1,16 +1,11 @@
|
|||||||
import { styled } from '@mui/material';
|
|
||||||
import {
|
|
||||||
Truncator,
|
|
||||||
type TruncatorProps,
|
|
||||||
} from 'component/common/Truncator/Truncator';
|
|
||||||
import { disabledStrategyClassName } from 'component/common/StrategyItemContainer/disabled-strategy-utils';
|
|
||||||
import type { FC, ReactNode } from 'react';
|
import type { FC, ReactNode } from 'react';
|
||||||
|
import { styled } from '@mui/material';
|
||||||
|
import { disabledStrategyClassName } from 'component/common/StrategyItemContainer/disabled-strategy-utils';
|
||||||
|
|
||||||
export type StrategyEvaluationItemProps = {
|
export type StrategyEvaluationItemProps = {
|
||||||
type?: ReactNode;
|
type?: ReactNode;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
values?: string[];
|
};
|
||||||
} & Pick<TruncatorProps, 'onSetTruncated'>;
|
|
||||||
|
|
||||||
const StyledContainer = styled('div')(({ theme }) => ({
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -38,35 +33,17 @@ const StyledType = styled('span')(({ theme }) => ({
|
|||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
width: theme.spacing(10),
|
width: theme.spacing(10),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract building block for a list of constraints, segments and other items inside a strategy
|
* Abstract building block for a list of constraints, segments and other items inside a strategy
|
||||||
*/
|
*/
|
||||||
export const StrategyEvaluationItem: FC<StrategyEvaluationItemProps> = ({
|
export const StrategyEvaluationItem: FC<StrategyEvaluationItemProps> = ({
|
||||||
type,
|
type,
|
||||||
children,
|
children,
|
||||||
values,
|
}) => {
|
||||||
onSetTruncated,
|
return (
|
||||||
}) => (
|
<StyledContainer>
|
||||||
<StyledContainer>
|
<StyledType>{type}</StyledType>
|
||||||
<StyledType>{type}</StyledType>
|
<StyledContent>{children}</StyledContent>
|
||||||
<StyledContent>
|
</StyledContainer>
|
||||||
{children}
|
);
|
||||||
{values && values?.length === 1 ? (
|
};
|
||||||
<Truncator
|
|
||||||
title={values[0]}
|
|
||||||
arrow
|
|
||||||
lines={2}
|
|
||||||
onSetTruncated={() => onSetTruncated?.(false)}
|
|
||||||
>
|
|
||||||
{values[0]}
|
|
||||||
</Truncator>
|
|
||||||
) : null}
|
|
||||||
{values && values?.length > 1 ? (
|
|
||||||
<Truncator title='' lines={2} onSetTruncated={onSetTruncated}>
|
|
||||||
{values.join(', ')}
|
|
||||||
</Truncator>
|
|
||||||
) : null}
|
|
||||||
</StyledContent>
|
|
||||||
</StyledContainer>
|
|
||||||
);
|
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
import { styled, Tooltip } from '@mui/material';
|
||||||
|
import {
|
||||||
|
Truncator,
|
||||||
|
type TruncatorProps,
|
||||||
|
} from 'component/common/Truncator/Truncator';
|
||||||
|
|
||||||
|
export type ValuesListProps = {
|
||||||
|
values?: string[];
|
||||||
|
tooltips?: Record<string, string | undefined>;
|
||||||
|
} & Pick<TruncatorProps, 'onSetTruncated'>;
|
||||||
|
|
||||||
|
const StyledValuesContainer = styled('div')({
|
||||||
|
flex: '1 1 0',
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledValueItem = styled('span')(({ theme }) => ({
|
||||||
|
padding: theme.spacing(0.25),
|
||||||
|
display: 'inline-block',
|
||||||
|
span: {
|
||||||
|
background: theme.palette.background.elevation2,
|
||||||
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: theme.spacing(0.25, 1),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledSingleValue = styled('div')(({ theme }) => ({
|
||||||
|
padding: theme.spacing(0.25, 1),
|
||||||
|
background: theme.palette.background.elevation2,
|
||||||
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const ValuesList: FC<ValuesListProps> = ({
|
||||||
|
values,
|
||||||
|
tooltips,
|
||||||
|
onSetTruncated,
|
||||||
|
}) => (
|
||||||
|
<StyledValuesContainer>
|
||||||
|
{values && values?.length === 1 ? (
|
||||||
|
<StyledSingleValue>
|
||||||
|
<Truncator
|
||||||
|
title={values[0]}
|
||||||
|
arrow
|
||||||
|
lines={2}
|
||||||
|
onSetTruncated={() => onSetTruncated?.(false)}
|
||||||
|
>
|
||||||
|
<Tooltip title={tooltips?.[values[0]] || ''}>
|
||||||
|
<span>{values[0]}</span>
|
||||||
|
</Tooltip>
|
||||||
|
</Truncator>
|
||||||
|
</StyledSingleValue>
|
||||||
|
) : null}
|
||||||
|
{values && values?.length > 1 ? (
|
||||||
|
<Truncator title='' lines={2} onSetTruncated={onSetTruncated}>
|
||||||
|
{values.map((value) => (
|
||||||
|
<Tooltip title={tooltips?.[value] || ''} key={value}>
|
||||||
|
<StyledValueItem>
|
||||||
|
<span>{value}</span>
|
||||||
|
</StyledValueItem>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</Truncator>
|
||||||
|
) : null}
|
||||||
|
</StyledValuesContainer>
|
||||||
|
);
|
@ -45,8 +45,7 @@ const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
|
const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
|
||||||
borderTop: `1px dashed ${theme.palette.divider}`,
|
padding: theme.spacing(0.5, 3, 2.5),
|
||||||
padding: theme.spacing(1.5, 3, 2.5),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledLink = styled(Link)({
|
const StyledLink = styled(Link)({
|
||||||
|
@ -13,6 +13,7 @@ import type {
|
|||||||
StrategySchemaParametersItem,
|
StrategySchemaParametersItem,
|
||||||
} from 'openapi';
|
} from 'openapi';
|
||||||
import type { IFeatureStrategyPayload } from 'interfaces/strategy';
|
import type { IFeatureStrategyPayload } from 'interfaces/strategy';
|
||||||
|
import { ValuesList } from 'component/common/ConstraintsList/ValuesList/ValuesList';
|
||||||
|
|
||||||
export const useCustomStrategyParameters = (
|
export const useCustomStrategyParameters = (
|
||||||
strategy: Pick<
|
strategy: Pick<
|
||||||
@ -48,14 +49,11 @@ export const useCustomStrategyParameters = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StrategyEvaluationItem
|
<StrategyEvaluationItem key={key} type={typeItem}>
|
||||||
key={key}
|
|
||||||
type={typeItem}
|
|
||||||
values={values}
|
|
||||||
>
|
|
||||||
{values.length === 1
|
{values.length === 1
|
||||||
? 'has 1 item:'
|
? 'has 1 item:'
|
||||||
: `has ${values.length} items:`}
|
: `has ${values.length} items:`}
|
||||||
|
<ValuesList values={values} />
|
||||||
</StrategyEvaluationItem>
|
</StrategyEvaluationItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -82,12 +80,11 @@ export const useCustomStrategyParameters = (
|
|||||||
const value = parseParameterString(parameters[name]);
|
const value = parseParameterString(parameters[name]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StrategyEvaluationItem
|
<StrategyEvaluationItem key={key} type={typeItem}>
|
||||||
key={key}
|
|
||||||
type={typeItem}
|
|
||||||
values={value === '' ? undefined : [value]}
|
|
||||||
>
|
|
||||||
{value === '' ? 'is an empty string' : 'is set to'}
|
{value === '' ? 'is an empty string' : 'is set to'}
|
||||||
|
<ValuesList
|
||||||
|
values={value === '' ? undefined : [value]}
|
||||||
|
/>
|
||||||
</StrategyEvaluationItem>
|
</StrategyEvaluationItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -95,12 +92,9 @@ export const useCustomStrategyParameters = (
|
|||||||
case 'number': {
|
case 'number': {
|
||||||
const value = parseParameterNumber(parameters[name]);
|
const value = parseParameterNumber(parameters[name]);
|
||||||
return (
|
return (
|
||||||
<StrategyEvaluationItem
|
<StrategyEvaluationItem key={key} type={typeItem}>
|
||||||
key={key}
|
|
||||||
type={typeItem}
|
|
||||||
values={[`${value}`]}
|
|
||||||
>
|
|
||||||
is a number set to
|
is a number set to
|
||||||
|
<ValuesList values={[`${value}`]} />
|
||||||
</StrategyEvaluationItem>
|
</StrategyEvaluationItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { parseParameterStrings } from 'utils/parseParameter';
|
|
||||||
import { StrategyEvaluationItem } from 'component/common/ConstraintsList/StrategyEvaluationItem/StrategyEvaluationItem';
|
import { StrategyEvaluationItem } from 'component/common/ConstraintsList/StrategyEvaluationItem/StrategyEvaluationItem';
|
||||||
import type { FeatureStrategySchema } from 'openapi';
|
import type { FeatureStrategySchema } from 'openapi';
|
||||||
import { RolloutParameter } from '../RolloutParameter/RolloutParameter';
|
import { RolloutParameter } from '../RolloutParameter/RolloutParameter';
|
||||||
|
import { ValuesList } from 'component/common/ConstraintsList/ValuesList/ValuesList';
|
||||||
|
import { parseParameterStrings } from 'utils/parseParameter';
|
||||||
|
|
||||||
export const useStrategyParameters = (
|
export const useStrategyParameters = (
|
||||||
strategy: Partial<
|
strategy: Partial<
|
||||||
@ -36,11 +37,9 @@ export const useStrategyParameters = (
|
|||||||
|
|
||||||
if (['userids', 'hostnames', 'ips'].includes(type)) {
|
if (['userids', 'hostnames', 'ips'].includes(type)) {
|
||||||
return (
|
return (
|
||||||
<StrategyEvaluationItem
|
<StrategyEvaluationItem key={key} type={key}>
|
||||||
key={key}
|
<ValuesList values={parseParameterStrings(value)} />
|
||||||
type={key}
|
</StrategyEvaluationItem>
|
||||||
values={parseParameterStrings(value)}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import type { IConstraint } from 'interfaces/strategy';
|
|
||||||
import { formatDateYMDHMS } from 'utils/formatDate';
|
import { formatDateYMDHMS } from 'utils/formatDate';
|
||||||
import type { ILocationSettings } from 'hooks/useLocationSettings';
|
import type { ILocationSettings } from 'hooks/useLocationSettings';
|
||||||
import { CURRENT_TIME_CONTEXT_FIELD } from 'utils/operatorsForContext';
|
import { CURRENT_TIME_CONTEXT_FIELD } from 'utils/operatorsForContext';
|
||||||
|
import type { ConstraintSchema } from 'openapi';
|
||||||
|
|
||||||
export const formatConstraintValue = (
|
export const formatConstraintValue = (
|
||||||
constraint: IConstraint,
|
constraint: Pick<ConstraintSchema, 'value' | 'contextName'>,
|
||||||
locationSettings: ILocationSettings,
|
locationSettings: ILocationSettings,
|
||||||
): string | undefined => {
|
): string | undefined => {
|
||||||
if (
|
if (
|
||||||
|
Loading…
Reference in New Issue
Block a user