diff --git a/frontend/src/component/common/ConstraintsList/ConstraintItemHeader/ConstraintItemHeader.tsx b/frontend/src/component/common/ConstraintsList/ConstraintItemHeader/ConstraintItemHeader.tsx index 880de40317..4d84dead7f 100644 --- a/frontend/src/component/common/ConstraintsList/ConstraintItemHeader/ConstraintItemHeader.tsx +++ b/frontend/src/component/common/ConstraintsList/ConstraintItemHeader/ConstraintItemHeader.tsx @@ -1,12 +1,14 @@ -import type { FC } from 'react'; -import { - StrategyEvaluationItem, - type StrategyEvaluationItemProps, -} from '../StrategyEvaluationItem/StrategyEvaluationItem'; +import type { ComponentProps, FC } from 'react'; +import { StrategyEvaluationItem } from '../StrategyEvaluationItem/StrategyEvaluationItem'; import type { ConstraintSchema } from 'openapi'; import { formatOperatorDescription } from 'component/common/ConstraintAccordion/ConstraintOperator/formatOperatorDescription'; import { StrategyEvaluationChip } from '../StrategyEvaluationChip/StrategyEvaluationChip'; 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 { isCaseSensitive } from './isCaseSensitive'; @@ -52,32 +54,45 @@ const StyledOperatorGroup = styled('div')(({ theme }) => ({ gap: theme.spacing(0.5), })); +const StyledConstraintName = styled('div')(({ theme }) => ({ + maxWidth: '150px', + paddingRight: theme.spacing(0.5), + overflow: 'hidden', +})); + export const ConstraintItemHeader: FC< - ConstraintSchema & Pick -> = ({ - caseInsensitive, - contextName, - inverted, - operator, - value, - values, - onSetTruncated, -}) => { - const items = value ? [value, ...(values || [])] : values || []; + ConstraintSchema & Pick, 'onSetTruncated'> +> = ({ onSetTruncated, ...constraint }) => { + const { caseInsensitive, contextName, inverted, operator, value, values } = + constraint; + const { locationSettings } = useLocationSettings(); + const items = value + ? [ + formatConstraintValue(constraint, locationSettings) || '', + ...(values || []), + ] + : values || []; + + const tooltips = useConstraintTooltips(contextName, values || []); return ( - - {contextName} + + + + {contextName} + + {isCaseSensitive(operator, caseInsensitive) ? ( ) : null} + ); }; diff --git a/frontend/src/component/common/ConstraintsList/ConstraintItemHeader/hooks/useConstraintTooltips.test.ts b/frontend/src/component/common/ConstraintsList/ConstraintItemHeader/hooks/useConstraintTooltips.test.ts new file mode 100644 index 0000000000..76fb8a6c69 --- /dev/null +++ b/frontend/src/component/common/ConstraintsList/ConstraintItemHeader/hooks/useConstraintTooltips.test.ts @@ -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 + ).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 + ).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 + ).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({}); + }); +}); diff --git a/frontend/src/component/common/ConstraintsList/ConstraintItemHeader/hooks/useConstraintTooltips.ts b/frontend/src/component/common/ConstraintsList/ConstraintItemHeader/hooks/useConstraintTooltips.ts new file mode 100644 index 0000000000..1e0b7ca774 --- /dev/null +++ b/frontend/src/component/common/ConstraintsList/ConstraintItemHeader/hooks/useConstraintTooltips.ts @@ -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>( + () => + Object.fromEntries( + values + ?.map((item) => [ + item, + contextDefinition?.legalValues?.find( + ({ value }) => value === item, + )?.description, + ]) + .filter(([_, tooltip]) => !!tooltip) || [], + ), + [context, values], + ); +}; diff --git a/frontend/src/component/common/ConstraintsList/StrategyEvaluationItem/StrategyEvaluationItem.tsx b/frontend/src/component/common/ConstraintsList/StrategyEvaluationItem/StrategyEvaluationItem.tsx index 239fe69968..2fc8948c9a 100644 --- a/frontend/src/component/common/ConstraintsList/StrategyEvaluationItem/StrategyEvaluationItem.tsx +++ b/frontend/src/component/common/ConstraintsList/StrategyEvaluationItem/StrategyEvaluationItem.tsx @@ -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 { styled } from '@mui/material'; +import { disabledStrategyClassName } from 'component/common/StrategyItemContainer/disabled-strategy-utils'; export type StrategyEvaluationItemProps = { type?: ReactNode; children?: ReactNode; - values?: string[]; -} & Pick; +}; const StyledContainer = styled('div')(({ theme }) => ({ display: 'flex', @@ -38,35 +33,17 @@ const StyledType = styled('span')(({ theme }) => ({ color: theme.palette.text.secondary, width: theme.spacing(10), })); - /** * Abstract building block for a list of constraints, segments and other items inside a strategy */ export const StrategyEvaluationItem: FC = ({ type, children, - values, - onSetTruncated, -}) => ( - - {type} - - {children} - {values && values?.length === 1 ? ( - onSetTruncated?.(false)} - > - {values[0]} - - ) : null} - {values && values?.length > 1 ? ( - - {values.join(', ')} - - ) : null} - - -); +}) => { + return ( + + {type} + {children} + + ); +}; diff --git a/frontend/src/component/common/ConstraintsList/ValuesList/ValuesList.tsx b/frontend/src/component/common/ConstraintsList/ValuesList/ValuesList.tsx new file mode 100644 index 0000000000..394cd1302d --- /dev/null +++ b/frontend/src/component/common/ConstraintsList/ValuesList/ValuesList.tsx @@ -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; +} & Pick; + +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 = ({ + values, + tooltips, + onSetTruncated, +}) => ( + + {values && values?.length === 1 ? ( + + onSetTruncated?.(false)} + > + + {values[0]} + + + + ) : null} + {values && values?.length > 1 ? ( + + {values.map((value) => ( + + + {value} + + + ))} + + ) : null} + +); diff --git a/frontend/src/component/common/SegmentItem/SegmentItem.tsx b/frontend/src/component/common/SegmentItem/SegmentItem.tsx index 610b422fb7..33f2c2666b 100644 --- a/frontend/src/component/common/SegmentItem/SegmentItem.tsx +++ b/frontend/src/component/common/SegmentItem/SegmentItem.tsx @@ -45,8 +45,7 @@ const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({ })); const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({ - borderTop: `1px dashed ${theme.palette.divider}`, - padding: theme.spacing(1.5, 3, 2.5), + padding: theme.spacing(0.5, 3, 2.5), })); const StyledLink = styled(Link)({ diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/hooks/useCustomStrategyParameters.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/hooks/useCustomStrategyParameters.tsx index 2d7fd503a2..ba5bd6de5c 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/hooks/useCustomStrategyParameters.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/hooks/useCustomStrategyParameters.tsx @@ -13,6 +13,7 @@ import type { StrategySchemaParametersItem, } from 'openapi'; import type { IFeatureStrategyPayload } from 'interfaces/strategy'; +import { ValuesList } from 'component/common/ConstraintsList/ValuesList/ValuesList'; export const useCustomStrategyParameters = ( strategy: Pick< @@ -48,14 +49,11 @@ export const useCustomStrategyParameters = ( } return ( - + {values.length === 1 ? 'has 1 item:' : `has ${values.length} items:`} + ); } @@ -82,12 +80,11 @@ export const useCustomStrategyParameters = ( const value = parseParameterString(parameters[name]); return ( - + {value === '' ? 'is an empty string' : 'is set to'} + ); } @@ -95,12 +92,9 @@ export const useCustomStrategyParameters = ( case 'number': { const value = parseParameterNumber(parameters[name]); return ( - + is a number set to + ); } diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/hooks/useStrategyParameters.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/hooks/useStrategyParameters.tsx index f867c04a4f..ba081d8cd8 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/hooks/useStrategyParameters.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/hooks/useStrategyParameters.tsx @@ -1,8 +1,9 @@ import { useMemo } from 'react'; -import { parseParameterStrings } from 'utils/parseParameter'; import { StrategyEvaluationItem } from 'component/common/ConstraintsList/StrategyEvaluationItem/StrategyEvaluationItem'; import type { FeatureStrategySchema } from 'openapi'; import { RolloutParameter } from '../RolloutParameter/RolloutParameter'; +import { ValuesList } from 'component/common/ConstraintsList/ValuesList/ValuesList'; +import { parseParameterStrings } from 'utils/parseParameter'; export const useStrategyParameters = ( strategy: Partial< @@ -36,11 +37,9 @@ export const useStrategyParameters = ( if (['userids', 'hostnames', 'ips'].includes(type)) { return ( - + + + ); } diff --git a/frontend/src/utils/formatConstraintValue.ts b/frontend/src/utils/formatConstraintValue.ts index b7a73748a7..c4a098716b 100644 --- a/frontend/src/utils/formatConstraintValue.ts +++ b/frontend/src/utils/formatConstraintValue.ts @@ -1,10 +1,10 @@ -import type { IConstraint } from 'interfaces/strategy'; import { formatDateYMDHMS } from 'utils/formatDate'; import type { ILocationSettings } from 'hooks/useLocationSettings'; import { CURRENT_TIME_CONTEXT_FIELD } from 'utils/operatorsForContext'; +import type { ConstraintSchema } from 'openapi'; export const formatConstraintValue = ( - constraint: IConstraint, + constraint: Pick, locationSettings: ILocationSettings, ): string | undefined => { if (