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 { Alert, Box, styled } from '@mui/material';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CodeSnippetPopover,
|
StrategyTooltipLink,
|
||||||
PopoverDiff,
|
StrategyDiff,
|
||||||
} from '../../CodeSnippetPopover/CodeSnippetPopover';
|
} from 'component/changeRequest/ChangeRequest/StrategyTooltipLink/StrategyTooltipLink';
|
||||||
import { StrategyExecution } from '../../../../feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution';
|
import { StrategyExecution } from '../../../../feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution';
|
||||||
import { ToggleStatusChange } from './ToggleStatusChange';
|
import { ToggleStatusChange } from './ToggleStatusChange';
|
||||||
import {
|
import {
|
||||||
@ -98,14 +98,14 @@ export const Change: FC<{
|
|||||||
{change.action === 'addStrategy' && (
|
{change.action === 'addStrategy' && (
|
||||||
<>
|
<>
|
||||||
<StrategyAddedChange discard={discard}>
|
<StrategyAddedChange discard={discard}>
|
||||||
<CodeSnippetPopover change={change}>
|
<StrategyTooltipLink change={change}>
|
||||||
<PopoverDiff
|
<StrategyDiff
|
||||||
change={change}
|
change={change}
|
||||||
feature={feature.name}
|
feature={feature.name}
|
||||||
environmentName={changeRequest.environment}
|
environmentName={changeRequest.environment}
|
||||||
project={changeRequest.project}
|
project={changeRequest.project}
|
||||||
/>
|
/>
|
||||||
</CodeSnippetPopover>
|
</StrategyTooltipLink>
|
||||||
</StrategyAddedChange>
|
</StrategyAddedChange>
|
||||||
<StrategyExecution strategy={change.payload} />
|
<StrategyExecution strategy={change.payload} />
|
||||||
</>
|
</>
|
||||||
@ -113,28 +113,28 @@ export const Change: FC<{
|
|||||||
{change.action === 'deleteStrategy' && (
|
{change.action === 'deleteStrategy' && (
|
||||||
<StrategyDeletedChange discard={discard}>
|
<StrategyDeletedChange discard={discard}>
|
||||||
{hasNameField(change.payload) && (
|
{hasNameField(change.payload) && (
|
||||||
<CodeSnippetPopover change={change}>
|
<StrategyTooltipLink change={change}>
|
||||||
<PopoverDiff
|
<StrategyDiff
|
||||||
change={change}
|
change={change}
|
||||||
feature={feature.name}
|
feature={feature.name}
|
||||||
environmentName={changeRequest.environment}
|
environmentName={changeRequest.environment}
|
||||||
project={changeRequest.project}
|
project={changeRequest.project}
|
||||||
/>
|
/>
|
||||||
</CodeSnippetPopover>
|
</StrategyTooltipLink>
|
||||||
)}
|
)}
|
||||||
</StrategyDeletedChange>
|
</StrategyDeletedChange>
|
||||||
)}
|
)}
|
||||||
{change.action === 'updateStrategy' && (
|
{change.action === 'updateStrategy' && (
|
||||||
<>
|
<>
|
||||||
<StrategyEditedChange discard={discard}>
|
<StrategyEditedChange discard={discard}>
|
||||||
<CodeSnippetPopover change={change}>
|
<StrategyTooltipLink change={change}>
|
||||||
<PopoverDiff
|
<StrategyDiff
|
||||||
change={change}
|
change={change}
|
||||||
feature={feature.name}
|
feature={feature.name}
|
||||||
environmentName={changeRequest.environment}
|
environmentName={changeRequest.environment}
|
||||||
project={changeRequest.project}
|
project={changeRequest.project}
|
||||||
/>
|
/>
|
||||||
</CodeSnippetPopover>
|
</StrategyTooltipLink>
|
||||||
</StrategyEditedChange>
|
</StrategyEditedChange>
|
||||||
<StrategyExecution strategy={change.payload} />
|
<StrategyExecution strategy={change.payload} />
|
||||||
</>
|
</>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import EventDiff from 'component/events/EventDiff/EventDiff';
|
import EventDiff from 'component/events/EventDiff/EventDiff';
|
||||||
|
import { IFeatureVariant } from 'interfaces/featureToggle';
|
||||||
|
|
||||||
const StyledCodeSection = styled('div')(({ theme }) => ({
|
const StyledCodeSection = styled('div')(({ theme }) => ({
|
||||||
overflowX: 'auto',
|
overflowX: 'auto',
|
||||||
@ -13,17 +14,24 @@ const StyledCodeSection = styled('div')(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
interface IDiffProps {
|
interface IDiffProps {
|
||||||
preData: any;
|
preData: IFeatureVariant[];
|
||||||
data: any;
|
data: IFeatureVariant[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const variantsArrayToObject = (variants: IFeatureVariant[]) =>
|
||||||
|
variants.reduce(
|
||||||
|
(object, { name, ...variant }) => ({ ...object, [name]: variant }),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
export const Diff = ({ preData, data }: IDiffProps) => (
|
export const Diff = ({ preData, data }: IDiffProps) => (
|
||||||
<StyledCodeSection>
|
<StyledCodeSection>
|
||||||
<EventDiff
|
<EventDiff
|
||||||
entry={{
|
entry={{
|
||||||
preData,
|
preData: variantsArrayToObject(preData),
|
||||||
data,
|
data: variantsArrayToObject(data),
|
||||||
}}
|
}}
|
||||||
|
sort={(a, b) => a.index - b.index}
|
||||||
/>
|
/>
|
||||||
</StyledCodeSection>
|
</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 { 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 { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Diff } from './Diff';
|
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',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: theme.spacing(2),
|
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',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
alignItems: 'center',
|
||||||
gap: theme.spacing(1),
|
gap: theme.spacing(1.5),
|
||||||
|
marginBottom: theme.spacing(0.5),
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface IVariantPatchProps {
|
interface IVariantPatchProps {
|
||||||
@ -34,17 +47,44 @@ export const VariantPatch = ({
|
|||||||
}: IVariantPatchProps) => {
|
}: IVariantPatchProps) => {
|
||||||
const { feature: featureData } = useFeature(project, feature);
|
const { feature: featureData } = useFeature(project, feature);
|
||||||
|
|
||||||
const preData = featureData.environments.find(
|
const preData =
|
||||||
({ name }) => environment === name
|
featureData.environments.find(({ name }) => environment === name)
|
||||||
)?.variants;
|
?.variants ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChangeItemCreateEditWrapper>
|
<ChangeItemInfo>
|
||||||
<ChangeItemInfo>
|
<StyledChangeHeader>
|
||||||
<Typography>Updating variants:</Typography>
|
<TooltipLink
|
||||||
<Diff preData={preData} data={change.payload.variants} />
|
tooltip={
|
||||||
</ChangeItemInfo>
|
<Diff
|
||||||
{discard}
|
preData={preData}
|
||||||
</ChangeItemCreateEditWrapper>
|
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>
|
ref: ForwardedRef<HTMLDivElement>
|
||||||
) => (
|
) => (
|
||||||
<StyledBadge
|
<StyledBadge
|
||||||
|
tabIndex={0}
|
||||||
color={color}
|
color={color}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
className={className}
|
className={className}
|
||||||
|
@ -36,7 +36,6 @@ const StyledContainer = styled('section', {
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
margin: '0 auto',
|
margin: '0 auto',
|
||||||
overflow: 'hidden',
|
|
||||||
[theme.breakpoints.down(1100)]: {
|
[theme.breakpoints.down(1100)]: {
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
|
@ -27,7 +27,7 @@ export const TooltipLink = ({
|
|||||||
...props
|
...props
|
||||||
}: ITooltipLinkProps) => (
|
}: ITooltipLinkProps) => (
|
||||||
<HtmlTooltip title={tooltip} {...tooltipProps} arrow>
|
<HtmlTooltip title={tooltip} {...tooltipProps} arrow>
|
||||||
<StyledLink highlighted={highlighted} {...props}>
|
<StyledLink tabIndex={0} highlighted={highlighted} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
</HtmlTooltip>
|
</HtmlTooltip>
|
||||||
|
@ -10,11 +10,21 @@ const DIFF_PREFIXES: Record<string, string> = {
|
|||||||
N: '+',
|
N: '+',
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IEventDiffProps {
|
interface IEventDiffResult {
|
||||||
entry: Partial<IEvent>;
|
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 theme = useTheme();
|
||||||
|
|
||||||
const styles: Record<string, CSSProperties> = {
|
const styles: Record<string, CSSProperties> = {
|
||||||
@ -48,7 +58,7 @@ const EventDiff = ({ entry }: IEventDiffProps) => {
|
|||||||
return change;
|
return change;
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildDiff = (diff: any, idx: number) => {
|
const buildDiff = (diff: any, index: number): IEventDiffResult => {
|
||||||
let change;
|
let change;
|
||||||
const key = diff.path?.join('.') ?? diff.index;
|
const key = diff.path?.join('.') ?? diff.index;
|
||||||
|
|
||||||
@ -66,15 +76,24 @@ const EventDiff = ({ entry }: IEventDiffProps) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
const changeValue = JSON.stringify(diff.rhs || diff.item);
|
||||||
change = (
|
change = (
|
||||||
<div style={styles[diff.kind]}>
|
<div style={styles[diff.kind]}>
|
||||||
{DIFF_PREFIXES[diff.kind]} {key}:{' '}
|
{DIFF_PREFIXES[diff.kind]} {key}
|
||||||
{JSON.stringify(diff.rhs || diff.item)}
|
{changeValue
|
||||||
|
? `: ${changeValue}`
|
||||||
|
: diff.kind === 'D'
|
||||||
|
? ' (deleted)'
|
||||||
|
: ''}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { key: key.toString(), value: <div key={idx}>{change}</div> };
|
return {
|
||||||
|
key: key.toString(),
|
||||||
|
value: <div key={index}>{change}</div>,
|
||||||
|
index,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
let changes;
|
let changes;
|
||||||
@ -82,7 +101,7 @@ const EventDiff = ({ entry }: IEventDiffProps) => {
|
|||||||
if (diffs) {
|
if (diffs) {
|
||||||
changes = diffs
|
changes = diffs
|
||||||
.map(buildDiff)
|
.map(buildDiff)
|
||||||
.sort((a, b) => a.key.localeCompare(b.key))
|
.sort(sort)
|
||||||
.map(({ value }) => value);
|
.map(({ value }) => value);
|
||||||
} else {
|
} else {
|
||||||
// Just show the data if there is no diff yet.
|
// Just show the data if there is no diff yet.
|
||||||
|
@ -48,6 +48,10 @@ const StyledDescription = styled('p')(({ theme }) => ({
|
|||||||
marginBottom: theme.spacing(1.5),
|
marginBottom: theme.spacing(1.5),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const StyledTableContainer = styled('div')(({ theme }) => ({
|
||||||
|
margin: theme.spacing(3, 0),
|
||||||
|
}));
|
||||||
|
|
||||||
const StyledStickinessContainer = styled('div')(({ theme }) => ({
|
const StyledStickinessContainer = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@ -84,10 +88,12 @@ export const EnvironmentVariantsCard = ({
|
|||||||
condition={variants.length > 0}
|
condition={variants.length > 0}
|
||||||
show={
|
show={
|
||||||
<>
|
<>
|
||||||
<EnvironmentVariantsTable
|
<StyledTableContainer>
|
||||||
environment={environment}
|
<EnvironmentVariantsTable
|
||||||
searchValue={searchValue}
|
variants={variants}
|
||||||
/>
|
searchValue={searchValue}
|
||||||
|
/>
|
||||||
|
</StyledTableContainer>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={variants.length > 1}
|
condition={variants.length > 1}
|
||||||
show={
|
show={
|
||||||
|
@ -1,10 +1,4 @@
|
|||||||
import {
|
import { TableBody, TableRow, useMediaQuery, useTheme } from '@mui/material';
|
||||||
styled,
|
|
||||||
TableBody,
|
|
||||||
TableRow,
|
|
||||||
useMediaQuery,
|
|
||||||
useTheme,
|
|
||||||
} from '@mui/material';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import {
|
import {
|
||||||
SortableTableHeader,
|
SortableTableHeader,
|
||||||
@ -18,11 +12,7 @@ import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightC
|
|||||||
import { calculateVariantWeight } from 'component/common/util';
|
import { calculateVariantWeight } from 'component/common/util';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { useSearch } from 'hooks/useSearch';
|
import { useSearch } from 'hooks/useSearch';
|
||||||
import {
|
import { IFeatureVariant, IOverride, IPayload } from 'interfaces/featureToggle';
|
||||||
IFeatureEnvironment,
|
|
||||||
IOverride,
|
|
||||||
IPayload,
|
|
||||||
} from 'interfaces/featureToggle';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useSortBy, useTable } from 'react-table';
|
import { useSortBy, useTable } from 'react-table';
|
||||||
import { sortTypes } from 'utils/sortTypes';
|
import { sortTypes } from 'utils/sortTypes';
|
||||||
@ -30,18 +20,14 @@ import { PayloadCell } from './PayloadCell/PayloadCell';
|
|||||||
import { OverridesCell } from './OverridesCell/OverridesCell';
|
import { OverridesCell } from './OverridesCell/OverridesCell';
|
||||||
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
||||||
|
|
||||||
const StyledTableContainer = styled('div')(({ theme }) => ({
|
|
||||||
margin: theme.spacing(3, 0),
|
|
||||||
}));
|
|
||||||
|
|
||||||
interface IEnvironmentVariantsTableProps {
|
interface IEnvironmentVariantsTableProps {
|
||||||
environment: IFeatureEnvironment;
|
variants: IFeatureVariant[];
|
||||||
searchValue: string;
|
searchValue?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EnvironmentVariantsTable = ({
|
export const EnvironmentVariantsTable = ({
|
||||||
environment,
|
variants,
|
||||||
searchValue,
|
searchValue = '',
|
||||||
}: IEnvironmentVariantsTableProps) => {
|
}: IEnvironmentVariantsTableProps) => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
|
|
||||||
@ -49,8 +35,6 @@ export const EnvironmentVariantsTable = ({
|
|||||||
const isMediumScreen = useMediaQuery(theme.breakpoints.down('md'));
|
const isMediumScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
const isLargeScreen = useMediaQuery(theme.breakpoints.down('lg'));
|
const isLargeScreen = useMediaQuery(theme.breakpoints.down('lg'));
|
||||||
|
|
||||||
const variants = environment.variants ?? [];
|
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@ -107,7 +91,7 @@ export const EnvironmentVariantsTable = ({
|
|||||||
sortType: 'alphanumeric',
|
sortType: 'alphanumeric',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[projectId, variants, environment]
|
[projectId, variants]
|
||||||
);
|
);
|
||||||
|
|
||||||
const initialState = useMemo(
|
const initialState = useMemo(
|
||||||
@ -156,7 +140,7 @@ export const EnvironmentVariantsTable = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledTableContainer>
|
<>
|
||||||
<SearchHighlightProvider value={getSearchText(searchValue)}>
|
<SearchHighlightProvider value={getSearchText(searchValue)}>
|
||||||
<Table {...getTableProps()}>
|
<Table {...getTableProps()}>
|
||||||
<SortableTableHeader headerGroups={headerGroups as any} />
|
<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 FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
||||||
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
@ -29,8 +29,14 @@ const StyledFormSubtitle = styled('div')(({ theme }) => ({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
marginTop: theme.spacing(-1.5),
|
marginTop: theme.spacing(-3.5),
|
||||||
marginBottom: theme.spacing(4),
|
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, {
|
const StyledCloudCircle = styled(CloudCircle, {
|
||||||
@ -68,7 +74,7 @@ const StyledAlert = styled(Alert)(({ theme }) => ({
|
|||||||
|
|
||||||
const StyledVariantForms = styled('div')({
|
const StyledVariantForms = styled('div')({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column-reverse',
|
flexDirection: 'column',
|
||||||
});
|
});
|
||||||
|
|
||||||
const StyledStickinessContainer = styled('div')(({ theme }) => ({
|
const StyledStickinessContainer = styled('div')(({ theme }) => ({
|
||||||
@ -84,6 +90,10 @@ const StyledDescription = styled('p')(({ theme }) => ({
|
|||||||
marginBottom: theme.spacing(1.5),
|
marginBottom: theme.spacing(1.5),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const StyledDivider = styled(Divider)(({ theme }) => ({
|
||||||
|
margin: theme.spacing(4, 0),
|
||||||
|
}));
|
||||||
|
|
||||||
const StyledStickinessSelect = styled(StickinessSelect)(({ theme }) => ({
|
const StyledStickinessSelect = styled(StickinessSelect)(({ theme }) => ({
|
||||||
minWidth: theme.spacing(20),
|
minWidth: theme.spacing(20),
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@ -144,6 +154,7 @@ export const EnvironmentVariantsModal = ({
|
|||||||
|
|
||||||
const oldVariants = environment?.variants || [];
|
const oldVariants = environment?.variants || [];
|
||||||
const [variantsEdit, setVariantsEdit] = useState<IFeatureVariantEdit[]>([]);
|
const [variantsEdit, setVariantsEdit] = useState<IFeatureVariantEdit[]>([]);
|
||||||
|
const [newVariant, setNewVariant] = useState<string>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setVariantsEdit(
|
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(
|
const variants = variantsEdit.map(
|
||||||
({ new: _, isValid: __, id: ___, ...rest }) => rest
|
({ new: _, isValid: __, id: ___, ...rest }) => rest
|
||||||
);
|
);
|
||||||
@ -286,24 +329,7 @@ export const EnvironmentVariantsModal = ({
|
|||||||
</div>
|
</div>
|
||||||
<PermissionButton
|
<PermissionButton
|
||||||
data-testid="MODAL_ADD_VARIANT_BUTTON"
|
data-testid="MODAL_ADD_VARIANT_BUTTON"
|
||||||
onClick={() =>
|
onClick={addVariant}
|
||||||
setVariantsEdit(variantsEdit => [
|
|
||||||
...variantsEdit,
|
|
||||||
{
|
|
||||||
name: '',
|
|
||||||
weightType: WeightType.VARIABLE,
|
|
||||||
weight: 0,
|
|
||||||
overrides: [],
|
|
||||||
stickiness:
|
|
||||||
variantsEdit?.length > 0
|
|
||||||
? variantsEdit[0].stickiness
|
|
||||||
: 'default',
|
|
||||||
new: true,
|
|
||||||
isValid: false,
|
|
||||||
id: uuidv4(),
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}
|
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
permission={UPDATE_FEATURE_ENVIRONMENT_VARIANTS}
|
permission={UPDATE_FEATURE_ENVIRONMENT_VARIANTS}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
@ -359,6 +385,16 @@ export const EnvironmentVariantsModal = ({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</StyledVariantForms>
|
</StyledVariantForms>
|
||||||
|
<PermissionButton
|
||||||
|
onClick={addVariant}
|
||||||
|
variant="outlined"
|
||||||
|
permission={UPDATE_FEATURE_ENVIRONMENT_VARIANTS}
|
||||||
|
projectId={projectId}
|
||||||
|
environmentId={environment?.name}
|
||||||
|
>
|
||||||
|
Add variant
|
||||||
|
</PermissionButton>
|
||||||
|
<StyledDivider />
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={variantsEdit.length > 0}
|
condition={variantsEdit.length > 0}
|
||||||
show={
|
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)(() => ({
|
const StyledInput = styled(Input)(() => ({
|
||||||
width: '100%',
|
width: '100%',
|
||||||
}));
|
}));
|
||||||
@ -103,13 +112,15 @@ const StyledTopRow = styled(StyledRow)({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const StyledSelectMenu = styled(SelectMenu)(({ theme }) => ({
|
const StyledSelectMenu = styled(SelectMenu)(({ theme }) => ({
|
||||||
minWidth: theme.spacing(20),
|
|
||||||
marginRight: theme.spacing(10),
|
marginRight: theme.spacing(10),
|
||||||
|
[theme.breakpoints.up('sm')]: {
|
||||||
|
minWidth: theme.spacing(20),
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledAddOverrideButton = styled(Button)(({ theme }) => ({
|
const StyledAddOverrideButton = styled(Button)(({ theme }) => ({
|
||||||
width: theme.spacing(20),
|
marginTop: theme.spacing(-1),
|
||||||
maxWidth: '100%',
|
marginLeft: theme.spacing(-1),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const payloadOptions = [
|
const payloadOptions = [
|
||||||
@ -319,8 +330,8 @@ export const VariantForm = ({
|
|||||||
This will be used to identify the variant in your code
|
This will be used to identify the variant in your code
|
||||||
</StyledSubLabel>
|
</StyledSubLabel>
|
||||||
<StyledInput
|
<StyledInput
|
||||||
|
id={`variant-name-input-${variant.id}`}
|
||||||
data-testid="VARIANT_NAME_INPUT"
|
data-testid="VARIANT_NAME_INPUT"
|
||||||
autoFocus
|
|
||||||
label="Variant name"
|
label="Variant name"
|
||||||
error={Boolean(errors.name)}
|
error={Boolean(errors.name)}
|
||||||
errorText={errors.name}
|
errorText={errors.name}
|
||||||
@ -391,27 +402,31 @@ export const VariantForm = ({
|
|||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<StyledInput
|
<StyledFieldColumn>
|
||||||
id="variant-payload-value"
|
<StyledInput
|
||||||
name="variant-payload-value"
|
id="variant-payload-value"
|
||||||
label="Value"
|
name="variant-payload-value"
|
||||||
multiline={payload.type !== 'string'}
|
label="Value"
|
||||||
rows={payload.type === 'string' ? 1 : 4}
|
multiline={payload.type !== 'string'}
|
||||||
value={payload.value}
|
rows={payload.type === 'string' ? 1 : 4}
|
||||||
onChange={e => {
|
value={payload.value}
|
||||||
clearError(ErrorField.PAYLOAD);
|
onChange={e => {
|
||||||
setPayload(payload => ({
|
clearError(ErrorField.PAYLOAD);
|
||||||
...payload,
|
setPayload(payload => ({
|
||||||
value: e.target.value,
|
...payload,
|
||||||
}));
|
value: e.target.value,
|
||||||
}}
|
}));
|
||||||
placeholder={
|
}}
|
||||||
payload.type === 'json' ? '{ "hello": "world" }' : ''
|
placeholder={
|
||||||
}
|
payload.type === 'json'
|
||||||
onBlur={() => validatePayload(payload)}
|
? '{ "hello": "world" }'
|
||||||
error={Boolean(errors.payload)}
|
: ''
|
||||||
errorText={errors.payload}
|
}
|
||||||
/>
|
onBlur={() => validatePayload(payload)}
|
||||||
|
error={Boolean(errors.payload)}
|
||||||
|
errorText={errors.payload}
|
||||||
|
/>
|
||||||
|
</StyledFieldColumn>
|
||||||
</StyledRow>
|
</StyledRow>
|
||||||
<StyledMarginLabel>
|
<StyledMarginLabel>
|
||||||
Overrides
|
Overrides
|
||||||
@ -421,13 +436,15 @@ export const VariantForm = ({
|
|||||||
overrides={overrides}
|
overrides={overrides}
|
||||||
overridesDispatch={overridesDispatch}
|
overridesDispatch={overridesDispatch}
|
||||||
/>
|
/>
|
||||||
<StyledAddOverrideButton
|
<div>
|
||||||
onClick={onAddOverride}
|
<StyledAddOverrideButton
|
||||||
variant="outlined"
|
onClick={onAddOverride}
|
||||||
color="primary"
|
variant="text"
|
||||||
>
|
color="primary"
|
||||||
Add override
|
>
|
||||||
</StyledAddOverrideButton>
|
Add override
|
||||||
|
</StyledAddOverrideButton>
|
||||||
|
</div>
|
||||||
</StyledVariantForm>
|
</StyledVariantForm>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -24,8 +24,10 @@ const StyledRow = styled('div')(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledSelectMenu = styled(SelectMenu)(({ theme }) => ({
|
const StyledSelectMenu = styled(SelectMenu)(({ theme }) => ({
|
||||||
minWidth: theme.spacing(20),
|
|
||||||
marginRight: theme.spacing(10),
|
marginRight: theme.spacing(10),
|
||||||
|
[theme.breakpoints.up('sm')]: {
|
||||||
|
minWidth: theme.spacing(20),
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledFieldColumn = styled('div')(({ theme }) => ({
|
const StyledFieldColumn = styled('div')(({ theme }) => ({
|
||||||
|
Loading…
Reference in New Issue
Block a user