mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-22 01:16:07 +02:00
feat: improve variants modal UI/UX (#3307)
https://linear.app/unleash/issue/2-758/add-variant-improve-the-flow   ### Variants form - Fix variants edit form to follow natural tab order; - Update variants form UI to new design with multiple improvements and fixes, including a sticky header; - New variants are now added at the bottom of the edit form instead of at the top, with a smooth scroll and focus; ### Change requests - On the variants diff, use variant names instead of index; - Use an object-based diff logic (instead of array-based) for cleaner diffs on variants (thanks @thomasheartman !); - Display a table with the new variants data and display the diff on a `TooltipLink`; - Adapt strategy CR changes to the new `TooltipLink` logic for consistency; ### Other - `TooltipLink` and `Badge` components are now tab-selectable; - Small enhancements, refactors and improvements; --------- Co-authored-by: Gastón Fournier <gaston@getunleash.io>
This commit is contained in:
parent
6aeadfff33
commit
4e36981c96
@ -10,9 +10,9 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
|
||||
import { Alert, Box, styled } from '@mui/material';
|
||||
|
||||
import {
|
||||
CodeSnippetPopover,
|
||||
PopoverDiff,
|
||||
} from '../../CodeSnippetPopover/CodeSnippetPopover';
|
||||
StrategyTooltipLink,
|
||||
StrategyDiff,
|
||||
} from 'component/changeRequest/ChangeRequest/StrategyTooltipLink/StrategyTooltipLink';
|
||||
import { StrategyExecution } from '../../../../feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution';
|
||||
import { ToggleStatusChange } from './ToggleStatusChange';
|
||||
import {
|
||||
@ -98,14 +98,14 @@ export const Change: FC<{
|
||||
{change.action === 'addStrategy' && (
|
||||
<>
|
||||
<StrategyAddedChange discard={discard}>
|
||||
<CodeSnippetPopover change={change}>
|
||||
<PopoverDiff
|
||||
<StrategyTooltipLink change={change}>
|
||||
<StrategyDiff
|
||||
change={change}
|
||||
feature={feature.name}
|
||||
environmentName={changeRequest.environment}
|
||||
project={changeRequest.project}
|
||||
/>
|
||||
</CodeSnippetPopover>
|
||||
</StrategyTooltipLink>
|
||||
</StrategyAddedChange>
|
||||
<StrategyExecution strategy={change.payload} />
|
||||
</>
|
||||
@ -113,28 +113,28 @@ export const Change: FC<{
|
||||
{change.action === 'deleteStrategy' && (
|
||||
<StrategyDeletedChange discard={discard}>
|
||||
{hasNameField(change.payload) && (
|
||||
<CodeSnippetPopover change={change}>
|
||||
<PopoverDiff
|
||||
<StrategyTooltipLink change={change}>
|
||||
<StrategyDiff
|
||||
change={change}
|
||||
feature={feature.name}
|
||||
environmentName={changeRequest.environment}
|
||||
project={changeRequest.project}
|
||||
/>
|
||||
</CodeSnippetPopover>
|
||||
</StrategyTooltipLink>
|
||||
)}
|
||||
</StrategyDeletedChange>
|
||||
)}
|
||||
{change.action === 'updateStrategy' && (
|
||||
<>
|
||||
<StrategyEditedChange discard={discard}>
|
||||
<CodeSnippetPopover change={change}>
|
||||
<PopoverDiff
|
||||
<StrategyTooltipLink change={change}>
|
||||
<StrategyDiff
|
||||
change={change}
|
||||
feature={feature.name}
|
||||
environmentName={changeRequest.environment}
|
||||
project={changeRequest.project}
|
||||
/>
|
||||
</CodeSnippetPopover>
|
||||
</StrategyTooltipLink>
|
||||
</StrategyEditedChange>
|
||||
<StrategyExecution strategy={change.payload} />
|
||||
</>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { styled } from '@mui/material';
|
||||
import EventDiff from 'component/events/EventDiff/EventDiff';
|
||||
import { IFeatureVariant } from 'interfaces/featureToggle';
|
||||
|
||||
const StyledCodeSection = styled('div')(({ theme }) => ({
|
||||
overflowX: 'auto',
|
||||
@ -13,17 +14,24 @@ const StyledCodeSection = styled('div')(({ theme }) => ({
|
||||
}));
|
||||
|
||||
interface IDiffProps {
|
||||
preData: any;
|
||||
data: any;
|
||||
preData: IFeatureVariant[];
|
||||
data: IFeatureVariant[];
|
||||
}
|
||||
|
||||
const variantsArrayToObject = (variants: IFeatureVariant[]) =>
|
||||
variants.reduce(
|
||||
(object, { name, ...variant }) => ({ ...object, [name]: variant }),
|
||||
{}
|
||||
);
|
||||
|
||||
export const Diff = ({ preData, data }: IDiffProps) => (
|
||||
<StyledCodeSection>
|
||||
<EventDiff
|
||||
entry={{
|
||||
preData,
|
||||
data,
|
||||
preData: variantsArrayToObject(preData),
|
||||
data: variantsArrayToObject(data),
|
||||
}}
|
||||
sort={(a, b) => a.index - b.index}
|
||||
/>
|
||||
</StyledCodeSection>
|
||||
);
|
||||
|
@ -1,20 +1,33 @@
|
||||
import { Box, styled, Typography } from '@mui/material';
|
||||
import { Box, styled } from '@mui/material';
|
||||
import { IChangeRequestPatchVariant } from 'component/changeRequest/changeRequest.types';
|
||||
import { Badge } from 'component/common/Badge/Badge';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
|
||||
import { EnvironmentVariantsTable } from 'component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsCard/EnvironmentVariantsTable/EnvironmentVariantsTable';
|
||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
import { ReactNode } from 'react';
|
||||
import { Diff } from './Diff';
|
||||
|
||||
export const ChangeItemCreateEditWrapper = styled(Box)(({ theme }) => ({
|
||||
const ChangeItemInfo = styled(Box)({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
const StyledChangeHeader = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: theme.spacing(2),
|
||||
lineHeight: theme.spacing(3),
|
||||
}));
|
||||
|
||||
const ChangeItemInfo = styled(Box)(({ theme }) => ({
|
||||
const StyledStickinessContainer = styled('div')(({ theme }) => ({
|
||||
marginTop: theme.spacing(1.5),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(1),
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(1.5),
|
||||
marginBottom: theme.spacing(0.5),
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
}));
|
||||
|
||||
interface IVariantPatchProps {
|
||||
@ -34,17 +47,44 @@ export const VariantPatch = ({
|
||||
}: IVariantPatchProps) => {
|
||||
const { feature: featureData } = useFeature(project, feature);
|
||||
|
||||
const preData = featureData.environments.find(
|
||||
({ name }) => environment === name
|
||||
)?.variants;
|
||||
const preData =
|
||||
featureData.environments.find(({ name }) => environment === name)
|
||||
?.variants ?? [];
|
||||
|
||||
return (
|
||||
<ChangeItemCreateEditWrapper>
|
||||
<ChangeItemInfo>
|
||||
<Typography>Updating variants:</Typography>
|
||||
<Diff preData={preData} data={change.payload.variants} />
|
||||
</ChangeItemInfo>
|
||||
{discard}
|
||||
</ChangeItemCreateEditWrapper>
|
||||
<ChangeItemInfo>
|
||||
<StyledChangeHeader>
|
||||
<TooltipLink
|
||||
tooltip={
|
||||
<Diff
|
||||
preData={preData}
|
||||
data={change.payload.variants}
|
||||
/>
|
||||
}
|
||||
tooltipProps={{
|
||||
maxWidth: 500,
|
||||
maxHeight: 600,
|
||||
}}
|
||||
>
|
||||
Updating variants to:
|
||||
</TooltipLink>
|
||||
{discard}
|
||||
</StyledChangeHeader>
|
||||
<EnvironmentVariantsTable variants={change.payload.variants} />
|
||||
<ConditionallyRender
|
||||
condition={change.payload.variants.length > 1}
|
||||
show={
|
||||
<>
|
||||
<StyledStickinessContainer>
|
||||
<p>Stickiness:</p>
|
||||
<Badge>
|
||||
{change.payload.variants[0]?.stickiness ||
|
||||
'default'}
|
||||
</Badge>
|
||||
</StyledStickinessContainer>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</ChangeItemInfo>
|
||||
);
|
||||
};
|
||||
|
@ -1,122 +0,0 @@
|
||||
import {
|
||||
IChangeRequestAddStrategy,
|
||||
IChangeRequestDeleteStrategy,
|
||||
IChangeRequestUpdateStrategy,
|
||||
} from '../../changeRequest.types';
|
||||
import React, { FC } from 'react';
|
||||
import {
|
||||
formatStrategyName,
|
||||
GetFeatureStrategyIcon,
|
||||
} from '../../../../utils/strategyNames';
|
||||
import { Popover, Typography } from '@mui/material';
|
||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
import { StyledCodeSection } from '../../../events/EventCard/EventCard';
|
||||
import EventDiff from '../../../events/EventDiff/EventDiff';
|
||||
import omit from 'lodash.omit';
|
||||
|
||||
const useCurrentStrategy = (
|
||||
change:
|
||||
| IChangeRequestAddStrategy
|
||||
| IChangeRequestUpdateStrategy
|
||||
| IChangeRequestDeleteStrategy,
|
||||
project: string,
|
||||
feature: string,
|
||||
environmentName: string
|
||||
) => {
|
||||
const currentFeature = useFeature(project, feature);
|
||||
const currentStrategy = currentFeature.feature?.environments
|
||||
.find(environment => environment.name === environmentName)
|
||||
?.strategies.find(
|
||||
strategy =>
|
||||
'id' in change.payload && strategy.id === change.payload.id
|
||||
);
|
||||
return currentStrategy;
|
||||
};
|
||||
|
||||
export const PopoverDiff: FC<{
|
||||
change:
|
||||
| IChangeRequestAddStrategy
|
||||
| IChangeRequestUpdateStrategy
|
||||
| IChangeRequestDeleteStrategy;
|
||||
project: string;
|
||||
feature: string;
|
||||
environmentName: string;
|
||||
}> = ({ change, project, feature, environmentName }) => {
|
||||
const currentStrategy = useCurrentStrategy(
|
||||
change,
|
||||
project,
|
||||
feature,
|
||||
environmentName
|
||||
);
|
||||
|
||||
const changeRequestStrategy =
|
||||
change.action === 'deleteStrategy' ? undefined : change.payload;
|
||||
|
||||
return (
|
||||
<StyledCodeSection>
|
||||
<EventDiff
|
||||
entry={{
|
||||
preData: omit(currentStrategy, 'sortOrder'),
|
||||
data: changeRequestStrategy,
|
||||
}}
|
||||
/>
|
||||
</StyledCodeSection>
|
||||
);
|
||||
};
|
||||
interface ICodeSnippetPopoverProps {
|
||||
change:
|
||||
| IChangeRequestAddStrategy
|
||||
| IChangeRequestUpdateStrategy
|
||||
| IChangeRequestDeleteStrategy;
|
||||
}
|
||||
|
||||
// based on: https://mui.com/material-ui/react-popover/#mouse-over-interaction
|
||||
export const CodeSnippetPopover: FC<ICodeSnippetPopoverProps> = ({
|
||||
change,
|
||||
children,
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
|
||||
|
||||
const handlePopoverOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handlePopoverClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GetFeatureStrategyIcon strategyName={change.payload.name} />
|
||||
|
||||
<Typography
|
||||
onMouseEnter={handlePopoverOpen}
|
||||
onMouseLeave={handlePopoverClose}
|
||||
>
|
||||
{formatStrategyName(change.payload.name)}
|
||||
</Typography>
|
||||
<Popover
|
||||
id={String(change.id)}
|
||||
sx={{
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
onClose={handlePopoverClose}
|
||||
disableRestoreFocus
|
||||
>
|
||||
{children}
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,100 @@
|
||||
import {
|
||||
IChangeRequestAddStrategy,
|
||||
IChangeRequestDeleteStrategy,
|
||||
IChangeRequestUpdateStrategy,
|
||||
} from 'component/changeRequest/changeRequest.types';
|
||||
import { FC } from 'react';
|
||||
import {
|
||||
formatStrategyName,
|
||||
GetFeatureStrategyIcon,
|
||||
} from 'utils/strategyNames';
|
||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
import EventDiff from 'component/events/EventDiff/EventDiff';
|
||||
import omit from 'lodash.omit';
|
||||
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
|
||||
import { styled } from '@mui/material';
|
||||
|
||||
const StyledCodeSection = styled('div')(({ theme }) => ({
|
||||
overflowX: 'auto',
|
||||
'& code': {
|
||||
wordWrap: 'break-word',
|
||||
whiteSpace: 'pre-wrap',
|
||||
fontFamily: 'monospace',
|
||||
lineHeight: 1.5,
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
},
|
||||
}));
|
||||
|
||||
const useCurrentStrategy = (
|
||||
change:
|
||||
| IChangeRequestAddStrategy
|
||||
| IChangeRequestUpdateStrategy
|
||||
| IChangeRequestDeleteStrategy,
|
||||
project: string,
|
||||
feature: string,
|
||||
environmentName: string
|
||||
) => {
|
||||
const currentFeature = useFeature(project, feature);
|
||||
const currentStrategy = currentFeature.feature?.environments
|
||||
.find(environment => environment.name === environmentName)
|
||||
?.strategies.find(
|
||||
strategy =>
|
||||
'id' in change.payload && strategy.id === change.payload.id
|
||||
);
|
||||
return currentStrategy;
|
||||
};
|
||||
|
||||
export const StrategyDiff: FC<{
|
||||
change:
|
||||
| IChangeRequestAddStrategy
|
||||
| IChangeRequestUpdateStrategy
|
||||
| IChangeRequestDeleteStrategy;
|
||||
project: string;
|
||||
feature: string;
|
||||
environmentName: string;
|
||||
}> = ({ change, project, feature, environmentName }) => {
|
||||
const currentStrategy = useCurrentStrategy(
|
||||
change,
|
||||
project,
|
||||
feature,
|
||||
environmentName
|
||||
);
|
||||
|
||||
const changeRequestStrategy =
|
||||
change.action === 'deleteStrategy' ? undefined : change.payload;
|
||||
|
||||
return (
|
||||
<StyledCodeSection>
|
||||
<EventDiff
|
||||
entry={{
|
||||
preData: omit(currentStrategy, 'sortOrder'),
|
||||
data: changeRequestStrategy,
|
||||
}}
|
||||
/>
|
||||
</StyledCodeSection>
|
||||
);
|
||||
};
|
||||
interface IStrategyTooltipLinkProps {
|
||||
change:
|
||||
| IChangeRequestAddStrategy
|
||||
| IChangeRequestUpdateStrategy
|
||||
| IChangeRequestDeleteStrategy;
|
||||
}
|
||||
|
||||
export const StrategyTooltipLink: FC<IStrategyTooltipLinkProps> = ({
|
||||
change,
|
||||
children,
|
||||
}) => (
|
||||
<>
|
||||
<GetFeatureStrategyIcon strategyName={change.payload.name} />
|
||||
<TooltipLink
|
||||
tooltip={children}
|
||||
tooltipProps={{
|
||||
maxWidth: 500,
|
||||
maxHeight: 600,
|
||||
}}
|
||||
>
|
||||
{formatStrategyName(change.payload.name)}
|
||||
</TooltipLink>
|
||||
</>
|
||||
);
|
@ -80,6 +80,7 @@ export const Badge: FC<IBadgeProps> = forwardRef(
|
||||
ref: ForwardedRef<HTMLDivElement>
|
||||
) => (
|
||||
<StyledBadge
|
||||
tabIndex={0}
|
||||
color={color}
|
||||
icon={icon}
|
||||
className={className}
|
||||
|
@ -36,7 +36,6 @@ const StyledContainer = styled('section', {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
margin: '0 auto',
|
||||
overflow: 'hidden',
|
||||
[theme.breakpoints.down(1100)]: {
|
||||
flexDirection: 'column',
|
||||
minHeight: 0,
|
||||
|
@ -27,7 +27,7 @@ export const TooltipLink = ({
|
||||
...props
|
||||
}: ITooltipLinkProps) => (
|
||||
<HtmlTooltip title={tooltip} {...tooltipProps} arrow>
|
||||
<StyledLink highlighted={highlighted} {...props}>
|
||||
<StyledLink tabIndex={0} highlighted={highlighted} {...props}>
|
||||
{children}
|
||||
</StyledLink>
|
||||
</HtmlTooltip>
|
||||
|
@ -10,11 +10,21 @@ const DIFF_PREFIXES: Record<string, string> = {
|
||||
N: '+',
|
||||
};
|
||||
|
||||
interface IEventDiffProps {
|
||||
entry: Partial<IEvent>;
|
||||
interface IEventDiffResult {
|
||||
key: string;
|
||||
value: JSX.Element;
|
||||
index: number;
|
||||
}
|
||||
|
||||
const EventDiff = ({ entry }: IEventDiffProps) => {
|
||||
interface IEventDiffProps {
|
||||
entry: Partial<IEvent>;
|
||||
sort?: (a: IEventDiffResult, b: IEventDiffResult) => number;
|
||||
}
|
||||
|
||||
const EventDiff = ({
|
||||
entry,
|
||||
sort = (a, b) => a.key.localeCompare(b.key),
|
||||
}: IEventDiffProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const styles: Record<string, CSSProperties> = {
|
||||
@ -48,7 +58,7 @@ const EventDiff = ({ entry }: IEventDiffProps) => {
|
||||
return change;
|
||||
};
|
||||
|
||||
const buildDiff = (diff: any, idx: number) => {
|
||||
const buildDiff = (diff: any, index: number): IEventDiffResult => {
|
||||
let change;
|
||||
const key = diff.path?.join('.') ?? diff.index;
|
||||
|
||||
@ -66,15 +76,24 @@ const EventDiff = ({ entry }: IEventDiffProps) => {
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const changeValue = JSON.stringify(diff.rhs || diff.item);
|
||||
change = (
|
||||
<div style={styles[diff.kind]}>
|
||||
{DIFF_PREFIXES[diff.kind]} {key}:{' '}
|
||||
{JSON.stringify(diff.rhs || diff.item)}
|
||||
{DIFF_PREFIXES[diff.kind]} {key}
|
||||
{changeValue
|
||||
? `: ${changeValue}`
|
||||
: diff.kind === 'D'
|
||||
? ' (deleted)'
|
||||
: ''}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return { key: key.toString(), value: <div key={idx}>{change}</div> };
|
||||
return {
|
||||
key: key.toString(),
|
||||
value: <div key={index}>{change}</div>,
|
||||
index,
|
||||
};
|
||||
};
|
||||
|
||||
let changes;
|
||||
@ -82,7 +101,7 @@ const EventDiff = ({ entry }: IEventDiffProps) => {
|
||||
if (diffs) {
|
||||
changes = diffs
|
||||
.map(buildDiff)
|
||||
.sort((a, b) => a.key.localeCompare(b.key))
|
||||
.sort(sort)
|
||||
.map(({ value }) => value);
|
||||
} else {
|
||||
// Just show the data if there is no diff yet.
|
||||
|
@ -48,6 +48,10 @@ const StyledDescription = styled('p')(({ theme }) => ({
|
||||
marginBottom: theme.spacing(1.5),
|
||||
}));
|
||||
|
||||
const StyledTableContainer = styled('div')(({ theme }) => ({
|
||||
margin: theme.spacing(3, 0),
|
||||
}));
|
||||
|
||||
const StyledStickinessContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
@ -84,10 +88,12 @@ export const EnvironmentVariantsCard = ({
|
||||
condition={variants.length > 0}
|
||||
show={
|
||||
<>
|
||||
<EnvironmentVariantsTable
|
||||
environment={environment}
|
||||
searchValue={searchValue}
|
||||
/>
|
||||
<StyledTableContainer>
|
||||
<EnvironmentVariantsTable
|
||||
variants={variants}
|
||||
searchValue={searchValue}
|
||||
/>
|
||||
</StyledTableContainer>
|
||||
<ConditionallyRender
|
||||
condition={variants.length > 1}
|
||||
show={
|
||||
|
@ -1,10 +1,4 @@
|
||||
import {
|
||||
styled,
|
||||
TableBody,
|
||||
TableRow,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import { TableBody, TableRow, useMediaQuery, useTheme } from '@mui/material';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import {
|
||||
SortableTableHeader,
|
||||
@ -18,11 +12,7 @@ import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightC
|
||||
import { calculateVariantWeight } from 'component/common/util';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { useSearch } from 'hooks/useSearch';
|
||||
import {
|
||||
IFeatureEnvironment,
|
||||
IOverride,
|
||||
IPayload,
|
||||
} from 'interfaces/featureToggle';
|
||||
import { IFeatureVariant, IOverride, IPayload } from 'interfaces/featureToggle';
|
||||
import { useMemo } from 'react';
|
||||
import { useSortBy, useTable } from 'react-table';
|
||||
import { sortTypes } from 'utils/sortTypes';
|
||||
@ -30,18 +20,14 @@ import { PayloadCell } from './PayloadCell/PayloadCell';
|
||||
import { OverridesCell } from './OverridesCell/OverridesCell';
|
||||
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
||||
|
||||
const StyledTableContainer = styled('div')(({ theme }) => ({
|
||||
margin: theme.spacing(3, 0),
|
||||
}));
|
||||
|
||||
interface IEnvironmentVariantsTableProps {
|
||||
environment: IFeatureEnvironment;
|
||||
searchValue: string;
|
||||
variants: IFeatureVariant[];
|
||||
searchValue?: string;
|
||||
}
|
||||
|
||||
export const EnvironmentVariantsTable = ({
|
||||
environment,
|
||||
searchValue,
|
||||
variants,
|
||||
searchValue = '',
|
||||
}: IEnvironmentVariantsTableProps) => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
|
||||
@ -49,8 +35,6 @@ export const EnvironmentVariantsTable = ({
|
||||
const isMediumScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const isLargeScreen = useMediaQuery(theme.breakpoints.down('lg'));
|
||||
|
||||
const variants = environment.variants ?? [];
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
@ -107,7 +91,7 @@ export const EnvironmentVariantsTable = ({
|
||||
sortType: 'alphanumeric',
|
||||
},
|
||||
],
|
||||
[projectId, variants, environment]
|
||||
[projectId, variants]
|
||||
);
|
||||
|
||||
const initialState = useMemo(
|
||||
@ -156,7 +140,7 @@ export const EnvironmentVariantsTable = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledTableContainer>
|
||||
<>
|
||||
<SearchHighlightProvider value={getSearchText(searchValue)}>
|
||||
<Table {...getTableProps()}>
|
||||
<SortableTableHeader headerGroups={headerGroups as any} />
|
||||
@ -191,6 +175,6 @@ export const EnvironmentVariantsTable = ({
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</StyledTableContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Alert, Button, styled } from '@mui/material';
|
||||
import { Alert, Button, Divider, styled } from '@mui/material';
|
||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
||||
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
@ -29,8 +29,14 @@ const StyledFormSubtitle = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
marginTop: theme.spacing(-1.5),
|
||||
marginBottom: theme.spacing(4),
|
||||
marginTop: theme.spacing(-3.5),
|
||||
marginBottom: theme.spacing(2),
|
||||
backgroundColor: theme.palette.background.default,
|
||||
paddingTop: theme.spacing(2),
|
||||
paddingBottom: theme.spacing(2),
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 2,
|
||||
}));
|
||||
|
||||
const StyledCloudCircle = styled(CloudCircle, {
|
||||
@ -68,7 +74,7 @@ const StyledAlert = styled(Alert)(({ theme }) => ({
|
||||
|
||||
const StyledVariantForms = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column-reverse',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
const StyledStickinessContainer = styled('div')(({ theme }) => ({
|
||||
@ -84,6 +90,10 @@ const StyledDescription = styled('p')(({ theme }) => ({
|
||||
marginBottom: theme.spacing(1.5),
|
||||
}));
|
||||
|
||||
const StyledDivider = styled(Divider)(({ theme }) => ({
|
||||
margin: theme.spacing(4, 0),
|
||||
}));
|
||||
|
||||
const StyledStickinessSelect = styled(StickinessSelect)(({ theme }) => ({
|
||||
minWidth: theme.spacing(20),
|
||||
width: '100%',
|
||||
@ -144,6 +154,7 @@ export const EnvironmentVariantsModal = ({
|
||||
|
||||
const oldVariants = environment?.variants || [];
|
||||
const [variantsEdit, setVariantsEdit] = useState<IFeatureVariantEdit[]>([]);
|
||||
const [newVariant, setNewVariant] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
setVariantsEdit(
|
||||
@ -183,6 +194,38 @@ export const EnvironmentVariantsModal = ({
|
||||
);
|
||||
};
|
||||
|
||||
const addVariant = () => {
|
||||
const id = uuidv4();
|
||||
setVariantsEdit(variantsEdit => [
|
||||
...variantsEdit,
|
||||
{
|
||||
name: '',
|
||||
weightType: WeightType.VARIABLE,
|
||||
weight: 0,
|
||||
overrides: [],
|
||||
stickiness:
|
||||
variantsEdit?.length > 0
|
||||
? variantsEdit[0].stickiness
|
||||
: 'default',
|
||||
new: true,
|
||||
isValid: false,
|
||||
id,
|
||||
},
|
||||
]);
|
||||
setNewVariant(id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (newVariant) {
|
||||
const element = document.getElementById(
|
||||
`variant-name-input-${newVariant}`
|
||||
);
|
||||
element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
element?.focus({ preventScroll: true });
|
||||
setNewVariant(undefined);
|
||||
}
|
||||
}, [newVariant]);
|
||||
|
||||
const variants = variantsEdit.map(
|
||||
({ new: _, isValid: __, id: ___, ...rest }) => rest
|
||||
);
|
||||
@ -286,24 +329,7 @@ export const EnvironmentVariantsModal = ({
|
||||
</div>
|
||||
<PermissionButton
|
||||
data-testid="MODAL_ADD_VARIANT_BUTTON"
|
||||
onClick={() =>
|
||||
setVariantsEdit(variantsEdit => [
|
||||
...variantsEdit,
|
||||
{
|
||||
name: '',
|
||||
weightType: WeightType.VARIABLE,
|
||||
weight: 0,
|
||||
overrides: [],
|
||||
stickiness:
|
||||
variantsEdit?.length > 0
|
||||
? variantsEdit[0].stickiness
|
||||
: 'default',
|
||||
new: true,
|
||||
isValid: false,
|
||||
id: uuidv4(),
|
||||
},
|
||||
])
|
||||
}
|
||||
onClick={addVariant}
|
||||
variant="outlined"
|
||||
permission={UPDATE_FEATURE_ENVIRONMENT_VARIANTS}
|
||||
projectId={projectId}
|
||||
@ -359,6 +385,16 @@ export const EnvironmentVariantsModal = ({
|
||||
/>
|
||||
))}
|
||||
</StyledVariantForms>
|
||||
<PermissionButton
|
||||
onClick={addVariant}
|
||||
variant="outlined"
|
||||
permission={UPDATE_FEATURE_ENVIRONMENT_VARIANTS}
|
||||
projectId={projectId}
|
||||
environmentId={environment?.name}
|
||||
>
|
||||
Add variant
|
||||
</PermissionButton>
|
||||
<StyledDivider />
|
||||
<ConditionallyRender
|
||||
condition={variantsEdit.length > 0}
|
||||
show={
|
||||
|
@ -65,6 +65,15 @@ const StyledFormControlLabel = styled(FormControlLabel)(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledFieldColumn = styled('div')(({ theme }) => ({
|
||||
width: '100%',
|
||||
gap: theme.spacing(1.5),
|
||||
display: 'flex',
|
||||
'& > div': {
|
||||
width: '100%',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledInput = styled(Input)(() => ({
|
||||
width: '100%',
|
||||
}));
|
||||
@ -103,13 +112,15 @@ const StyledTopRow = styled(StyledRow)({
|
||||
});
|
||||
|
||||
const StyledSelectMenu = styled(SelectMenu)(({ theme }) => ({
|
||||
minWidth: theme.spacing(20),
|
||||
marginRight: theme.spacing(10),
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
minWidth: theme.spacing(20),
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledAddOverrideButton = styled(Button)(({ theme }) => ({
|
||||
width: theme.spacing(20),
|
||||
maxWidth: '100%',
|
||||
marginTop: theme.spacing(-1),
|
||||
marginLeft: theme.spacing(-1),
|
||||
}));
|
||||
|
||||
const payloadOptions = [
|
||||
@ -319,8 +330,8 @@ export const VariantForm = ({
|
||||
This will be used to identify the variant in your code
|
||||
</StyledSubLabel>
|
||||
<StyledInput
|
||||
id={`variant-name-input-${variant.id}`}
|
||||
data-testid="VARIANT_NAME_INPUT"
|
||||
autoFocus
|
||||
label="Variant name"
|
||||
error={Boolean(errors.name)}
|
||||
errorText={errors.name}
|
||||
@ -391,27 +402,31 @@ export const VariantForm = ({
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<StyledInput
|
||||
id="variant-payload-value"
|
||||
name="variant-payload-value"
|
||||
label="Value"
|
||||
multiline={payload.type !== 'string'}
|
||||
rows={payload.type === 'string' ? 1 : 4}
|
||||
value={payload.value}
|
||||
onChange={e => {
|
||||
clearError(ErrorField.PAYLOAD);
|
||||
setPayload(payload => ({
|
||||
...payload,
|
||||
value: e.target.value,
|
||||
}));
|
||||
}}
|
||||
placeholder={
|
||||
payload.type === 'json' ? '{ "hello": "world" }' : ''
|
||||
}
|
||||
onBlur={() => validatePayload(payload)}
|
||||
error={Boolean(errors.payload)}
|
||||
errorText={errors.payload}
|
||||
/>
|
||||
<StyledFieldColumn>
|
||||
<StyledInput
|
||||
id="variant-payload-value"
|
||||
name="variant-payload-value"
|
||||
label="Value"
|
||||
multiline={payload.type !== 'string'}
|
||||
rows={payload.type === 'string' ? 1 : 4}
|
||||
value={payload.value}
|
||||
onChange={e => {
|
||||
clearError(ErrorField.PAYLOAD);
|
||||
setPayload(payload => ({
|
||||
...payload,
|
||||
value: e.target.value,
|
||||
}));
|
||||
}}
|
||||
placeholder={
|
||||
payload.type === 'json'
|
||||
? '{ "hello": "world" }'
|
||||
: ''
|
||||
}
|
||||
onBlur={() => validatePayload(payload)}
|
||||
error={Boolean(errors.payload)}
|
||||
errorText={errors.payload}
|
||||
/>
|
||||
</StyledFieldColumn>
|
||||
</StyledRow>
|
||||
<StyledMarginLabel>
|
||||
Overrides
|
||||
@ -421,13 +436,15 @@ export const VariantForm = ({
|
||||
overrides={overrides}
|
||||
overridesDispatch={overridesDispatch}
|
||||
/>
|
||||
<StyledAddOverrideButton
|
||||
onClick={onAddOverride}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
>
|
||||
Add override
|
||||
</StyledAddOverrideButton>
|
||||
<div>
|
||||
<StyledAddOverrideButton
|
||||
onClick={onAddOverride}
|
||||
variant="text"
|
||||
color="primary"
|
||||
>
|
||||
Add override
|
||||
</StyledAddOverrideButton>
|
||||
</div>
|
||||
</StyledVariantForm>
|
||||
);
|
||||
};
|
||||
|
@ -24,8 +24,10 @@ const StyledRow = styled('div')(({ theme }) => ({
|
||||
}));
|
||||
|
||||
const StyledSelectMenu = styled(SelectMenu)(({ theme }) => ({
|
||||
minWidth: theme.spacing(20),
|
||||
marginRight: theme.spacing(10),
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
minWidth: theme.spacing(20),
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledFieldColumn = styled('div')(({ theme }) => ({
|
||||
|
Loading…
Reference in New Issue
Block a user