mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: visualize variants diff in CR (#2979)
https://linear.app/unleash/issue/2-582/display-the-change-request-created-with-variants-in-the-ui  Includes a basic diff visualisation on variants change requests. It seems like components like `CodeSnippetPopover` and `PopoverDiff` are currently very tightly coupled together with strategies, so I preferred to follow my own approach and leave those alone for now instead of trying to refactor them. `patchVariant` could also be renamed to a more fitting name in the future as well, since we're now doing more of an override than applying a patch. `Diff` is a generic diff component that uses `EventDiff` internally and simply takes into account a "before" and "after" state, as `preData` and `data`. I made some changes to `EventDiff` that made some sense to me: - Cover edge cases where `path` is undefined and `.join` crashes, also fallback to the diff index (or undefined); - Leverage the key to correctly sort the change items in the diff;
This commit is contained in:
		
							parent
							
								
									80c444aa99
								
							
						
					
					
						commit
						c5fced89fb
					
				@ -1,4 +1,4 @@
 | 
			
		||||
import React, { FC, ReactNode } from 'react';
 | 
			
		||||
import { FC, ReactNode } from 'react';
 | 
			
		||||
import {
 | 
			
		||||
    hasNameField,
 | 
			
		||||
    IChange,
 | 
			
		||||
@ -20,6 +20,7 @@ import {
 | 
			
		||||
    StrategyDeletedChange,
 | 
			
		||||
    StrategyEditedChange,
 | 
			
		||||
} from './StrategyChange';
 | 
			
		||||
import { VariantPatch } from './VariantPatch/VariantPatch';
 | 
			
		||||
 | 
			
		||||
const StyledSingleChangeBox = styled(Box, {
 | 
			
		||||
    shouldForwardProp: (prop: string) => !prop.startsWith('$'),
 | 
			
		||||
@ -138,6 +139,15 @@ export const Change: FC<{
 | 
			
		||||
                        <StrategyExecution strategy={change.payload} />
 | 
			
		||||
                    </>
 | 
			
		||||
                )}
 | 
			
		||||
                {change.action === 'patchVariant' && (
 | 
			
		||||
                    <VariantPatch
 | 
			
		||||
                        feature={feature.name}
 | 
			
		||||
                        project={changeRequest.project}
 | 
			
		||||
                        environment={changeRequest.environment}
 | 
			
		||||
                        change={change}
 | 
			
		||||
                        discard={discard}
 | 
			
		||||
                    />
 | 
			
		||||
                )}
 | 
			
		||||
            </Box>
 | 
			
		||||
        </StyledSingleChangeBox>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,29 @@
 | 
			
		||||
import { styled } from '@mui/material';
 | 
			
		||||
import EventDiff from 'component/events/EventDiff/EventDiff';
 | 
			
		||||
 | 
			
		||||
const StyledCodeSection = styled('div')(({ theme }) => ({
 | 
			
		||||
    overflowX: 'auto',
 | 
			
		||||
    '& code': {
 | 
			
		||||
        wordWrap: 'break-word',
 | 
			
		||||
        whiteSpace: 'pre-wrap',
 | 
			
		||||
        fontFamily: 'monospace',
 | 
			
		||||
        lineHeight: 1.5,
 | 
			
		||||
        fontSize: theme.fontSizes.smallBody,
 | 
			
		||||
    },
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
interface IDiffProps {
 | 
			
		||||
    preData: any;
 | 
			
		||||
    data: any;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Diff = ({ preData, data }: IDiffProps) => (
 | 
			
		||||
    <StyledCodeSection>
 | 
			
		||||
        <EventDiff
 | 
			
		||||
            entry={{
 | 
			
		||||
                preData,
 | 
			
		||||
                data,
 | 
			
		||||
            }}
 | 
			
		||||
        />
 | 
			
		||||
    </StyledCodeSection>
 | 
			
		||||
);
 | 
			
		||||
@ -0,0 +1,50 @@
 | 
			
		||||
import { Box, styled, Typography } from '@mui/material';
 | 
			
		||||
import { IChangeRequestPatchVariant } from 'component/changeRequest/changeRequest.types';
 | 
			
		||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
 | 
			
		||||
import { ReactNode } from 'react';
 | 
			
		||||
import { Diff } from './Diff';
 | 
			
		||||
 | 
			
		||||
export const ChangeItemCreateEditWrapper = styled(Box)(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    justifyContent: 'space-between',
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
    marginBottom: theme.spacing(2),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const ChangeItemInfo = styled(Box)(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    flexDirection: 'column',
 | 
			
		||||
    gap: theme.spacing(1),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
interface IVariantPatchProps {
 | 
			
		||||
    feature: string;
 | 
			
		||||
    project: string;
 | 
			
		||||
    environment: string;
 | 
			
		||||
    change: IChangeRequestPatchVariant;
 | 
			
		||||
    discard?: ReactNode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const VariantPatch = ({
 | 
			
		||||
    feature,
 | 
			
		||||
    project,
 | 
			
		||||
    environment,
 | 
			
		||||
    change,
 | 
			
		||||
    discard,
 | 
			
		||||
}: IVariantPatchProps) => {
 | 
			
		||||
    const { feature: featureData } = useFeature(project, feature);
 | 
			
		||||
 | 
			
		||||
    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>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import { IFeatureVariant } from 'interfaces/featureToggle';
 | 
			
		||||
import { IFeatureStrategy } from '../../interfaces/strategy';
 | 
			
		||||
import { IUser } from '../../interfaces/user';
 | 
			
		||||
 | 
			
		||||
@ -60,7 +61,8 @@ type ChangeRequestPayload =
 | 
			
		||||
    | ChangeRequestEnabled
 | 
			
		||||
    | ChangeRequestAddStrategy
 | 
			
		||||
    | ChangeRequestEditStrategy
 | 
			
		||||
    | ChangeRequestDeleteStrategy;
 | 
			
		||||
    | ChangeRequestDeleteStrategy
 | 
			
		||||
    | ChangeRequestVariantPatch;
 | 
			
		||||
 | 
			
		||||
export interface IChangeRequestAddStrategy extends IChangeRequestBase {
 | 
			
		||||
    action: 'addStrategy';
 | 
			
		||||
@ -82,11 +84,21 @@ export interface IChangeRequestEnabled extends IChangeRequestBase {
 | 
			
		||||
    payload: ChangeRequestEnabled;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IChangeRequestPatchVariant extends IChangeRequestBase {
 | 
			
		||||
    action: 'patchVariant';
 | 
			
		||||
    payload: ChangeRequestVariantPatch;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type IChange =
 | 
			
		||||
    | IChangeRequestAddStrategy
 | 
			
		||||
    | IChangeRequestDeleteStrategy
 | 
			
		||||
    | IChangeRequestUpdateStrategy
 | 
			
		||||
    | IChangeRequestEnabled;
 | 
			
		||||
    | IChangeRequestEnabled
 | 
			
		||||
    | IChangeRequestPatchVariant;
 | 
			
		||||
 | 
			
		||||
type ChangeRequestVariantPatch = {
 | 
			
		||||
    variants: IFeatureVariant[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type ChangeRequestEnabled = { enabled: boolean };
 | 
			
		||||
 | 
			
		||||
@ -106,7 +118,8 @@ export type ChangeRequestAction =
 | 
			
		||||
    | 'updateEnabled'
 | 
			
		||||
    | 'addStrategy'
 | 
			
		||||
    | 'updateStrategy'
 | 
			
		||||
    | 'deleteStrategy';
 | 
			
		||||
    | 'deleteStrategy'
 | 
			
		||||
    | 'patchVariant';
 | 
			
		||||
 | 
			
		||||
export const hasNameField = (payload: unknown): payload is { name: string } =>
 | 
			
		||||
    typeof payload === 'object' && payload !== null && 'name' in payload;
 | 
			
		||||
 | 
			
		||||
@ -50,7 +50,7 @@ const EventDiff = ({ entry }: IEventDiffProps) => {
 | 
			
		||||
 | 
			
		||||
    const buildDiff = (diff: any, idx: number) => {
 | 
			
		||||
        let change;
 | 
			
		||||
        const key = diff.path.join('.');
 | 
			
		||||
        const key = diff.path?.join('.') ?? diff.index;
 | 
			
		||||
 | 
			
		||||
        if (diff.item) {
 | 
			
		||||
            change = buildItemDiff(diff.item, key);
 | 
			
		||||
@ -74,13 +74,16 @@ const EventDiff = ({ entry }: IEventDiffProps) => {
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return <div key={idx}>{change}</div>;
 | 
			
		||||
        return { key: key.toString(), value: <div key={idx}>{change}</div> };
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let changes;
 | 
			
		||||
 | 
			
		||||
    if (diffs) {
 | 
			
		||||
        changes = diffs.map(buildDiff);
 | 
			
		||||
        changes = diffs
 | 
			
		||||
            .map(buildDiff)
 | 
			
		||||
            .sort((a, b) => a.key.localeCompare(b.key))
 | 
			
		||||
            .map(({ value }) => value);
 | 
			
		||||
    } else {
 | 
			
		||||
        // Just show the data if there is no diff yet.
 | 
			
		||||
        const data = entry.data || entry.preData;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user