mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
feat: new variants table (#1025)
* fix: cleanup * fix: text * fix: stable references * refactor: fix VARIANT_WEIGH test id * refactor: fix variant element selection in e2e test * fix: update variants table * fix: refactor action cell Co-authored-by: olav <mail@olav.io>
This commit is contained in:
parent
f4d02e37b7
commit
7c52f0fcc8
@ -6,6 +6,8 @@ const ENTERPRISE = Boolean(Cypress.env('ENTERPRISE'));
|
|||||||
const randomId = String(Math.random()).split('.')[1];
|
const randomId = String(Math.random()).split('.')[1];
|
||||||
const featureToggleName = `unleash-e2e-${randomId}`;
|
const featureToggleName = `unleash-e2e-${randomId}`;
|
||||||
const baseUrl = Cypress.config().baseUrl;
|
const baseUrl = Cypress.config().baseUrl;
|
||||||
|
const variant1 = 'variant1';
|
||||||
|
const variant2 = 'variant2';
|
||||||
let strategyId = '';
|
let strategyId = '';
|
||||||
|
|
||||||
// Disable the prod guard modal by marking it as seen.
|
// Disable the prod guard modal by marking it as seen.
|
||||||
@ -233,9 +235,6 @@ describe('feature', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can add two variant to the feature', () => {
|
it('can add two variant to the feature', () => {
|
||||||
const variantName = 'my-new-variant';
|
|
||||||
const secondVariantName = 'my-second-variant';
|
|
||||||
|
|
||||||
cy.visit(`/projects/default/features/${featureToggleName}/variants`);
|
cy.visit(`/projects/default/features/${featureToggleName}/variants`);
|
||||||
|
|
||||||
cy.intercept(
|
cy.intercept(
|
||||||
@ -245,26 +244,26 @@ describe('feature', () => {
|
|||||||
if (req.body.length === 1) {
|
if (req.body.length === 1) {
|
||||||
expect(req.body[0].op).to.equal('add');
|
expect(req.body[0].op).to.equal('add');
|
||||||
expect(req.body[0].path).to.match(/\//);
|
expect(req.body[0].path).to.match(/\//);
|
||||||
expect(req.body[0].value.name).to.equal(variantName);
|
expect(req.body[0].value.name).to.equal(variant1);
|
||||||
} else if (req.body.length === 2) {
|
} else if (req.body.length === 2) {
|
||||||
expect(req.body[0].op).to.equal('replace');
|
expect(req.body[0].op).to.equal('replace');
|
||||||
expect(req.body[0].path).to.match(/weight/);
|
expect(req.body[0].path).to.match(/weight/);
|
||||||
expect(req.body[0].value).to.equal(500);
|
expect(req.body[0].value).to.equal(500);
|
||||||
expect(req.body[1].op).to.equal('add');
|
expect(req.body[1].op).to.equal('add');
|
||||||
expect(req.body[1].path).to.match(/\//);
|
expect(req.body[1].path).to.match(/\//);
|
||||||
expect(req.body[1].value.name).to.equal(secondVariantName);
|
expect(req.body[1].value.name).to.equal(variant2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
).as('variantCreation');
|
).as('variantCreation');
|
||||||
|
|
||||||
cy.get('[data-testid=ADD_VARIANT_BUTTON]').click();
|
cy.get('[data-testid=ADD_VARIANT_BUTTON]').click();
|
||||||
cy.wait(1000);
|
cy.wait(1000);
|
||||||
cy.get('[data-testid=VARIANT_NAME_INPUT]').type(variantName);
|
cy.get('[data-testid=VARIANT_NAME_INPUT]').type(variant1);
|
||||||
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
|
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
|
||||||
cy.wait('@variantCreation');
|
cy.wait('@variantCreation');
|
||||||
cy.get('[data-testid=ADD_VARIANT_BUTTON]').click();
|
cy.get('[data-testid=ADD_VARIANT_BUTTON]').click();
|
||||||
cy.wait(1000);
|
cy.wait(1000);
|
||||||
cy.get('[data-testid=VARIANT_NAME_INPUT]').type(secondVariantName);
|
cy.get('[data-testid=VARIANT_NAME_INPUT]').type(variant2);
|
||||||
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
|
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
|
||||||
cy.wait('@variantCreation');
|
cy.wait('@variantCreation');
|
||||||
});
|
});
|
||||||
@ -272,7 +271,7 @@ describe('feature', () => {
|
|||||||
it('can set weight to fixed value for one of the variants', () => {
|
it('can set weight to fixed value for one of the variants', () => {
|
||||||
cy.visit(`/projects/default/features/${featureToggleName}/variants`);
|
cy.visit(`/projects/default/features/${featureToggleName}/variants`);
|
||||||
|
|
||||||
cy.get('[data-testid=VARIANT_EDIT_BUTTON]').first().click();
|
cy.get(`[data-testid=VARIANT_EDIT_BUTTON_${variant1}]`).click();
|
||||||
cy.wait(1000);
|
cy.wait(1000);
|
||||||
cy.get('[data-testid=VARIANT_NAME_INPUT]')
|
cy.get('[data-testid=VARIANT_NAME_INPUT]')
|
||||||
.children()
|
.children()
|
||||||
@ -299,9 +298,10 @@ describe('feature', () => {
|
|||||||
|
|
||||||
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
|
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
|
||||||
cy.wait('@variantUpdate');
|
cy.wait('@variantUpdate');
|
||||||
cy.get('[data-testid=VARIANT_WEIGHT]')
|
cy.get(`[data-testid=VARIANT_WEIGHT_${variant1}]`).should(
|
||||||
.first()
|
'have.text',
|
||||||
.should('have.text', '15 %');
|
'15 %'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can delete variant', () => {
|
it('can delete variant', () => {
|
||||||
|
@ -5,18 +5,22 @@ import { useStyles } from './TextCell.styles';
|
|||||||
interface ITextCellProps {
|
interface ITextCellProps {
|
||||||
value?: string | null;
|
value?: string | null;
|
||||||
lineClamp?: number;
|
lineClamp?: number;
|
||||||
|
'data-testid'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TextCell: FC<ITextCellProps> = ({
|
export const TextCell: FC<ITextCellProps> = ({
|
||||||
value,
|
value,
|
||||||
children,
|
children,
|
||||||
lineClamp,
|
lineClamp,
|
||||||
|
'data-testid': testid,
|
||||||
}) => {
|
}) => {
|
||||||
const { classes } = useStyles({ lineClamp });
|
const { classes } = useStyles({ lineClamp });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={classes.wrapper}>
|
<Box className={classes.wrapper}>
|
||||||
<span data-loading>{children ?? value}</span>
|
<span data-loading="true" data-testid={testid}>
|
||||||
|
{children ?? value}
|
||||||
|
</span>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,17 +1,10 @@
|
|||||||
import { useStyles } from './FeatureVariants.styles';
|
import { FeatureVariantsList } from './FeatureVariantsList/FeatureVariantsList';
|
||||||
import FeatureOverviewVariants from './FeatureVariantsList/FeatureVariantsList';
|
|
||||||
import { usePageTitle } from 'hooks/usePageTitle';
|
import { usePageTitle } from 'hooks/usePageTitle';
|
||||||
|
|
||||||
const FeatureVariants = () => {
|
const FeatureVariants = () => {
|
||||||
usePageTitle('Variants');
|
usePageTitle('Variants');
|
||||||
|
|
||||||
const { classes: styles } = useStyles();
|
return <FeatureVariantsList />;
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<FeatureOverviewVariants />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FeatureVariants;
|
export default FeatureVariants;
|
||||||
|
@ -1,21 +1,19 @@
|
|||||||
import classnames from 'classnames';
|
|
||||||
import * as jsonpatch from 'fast-json-patch';
|
import * as jsonpatch from 'fast-json-patch';
|
||||||
|
|
||||||
import styles from './variants.module.scss';
|
|
||||||
import {
|
import {
|
||||||
|
Alert,
|
||||||
|
Box,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
|
||||||
TableRow,
|
TableRow,
|
||||||
Typography,
|
useMediaQuery,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { AddVariant } from './AddFeatureVariant/AddFeatureVariant';
|
import { AddVariant } from './AddFeatureVariant/AddFeatureVariant';
|
||||||
|
|
||||||
import { useContext, useEffect, useState } from 'react';
|
import { useContext, useEffect, useState, useMemo, useCallback } from 'react';
|
||||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||||
import AccessContext from 'contexts/AccessContext';
|
import AccessContext from 'contexts/AccessContext';
|
||||||
import FeatureVariantListItem from './FeatureVariantsListItem/FeatureVariantsListItem';
|
|
||||||
import { UPDATE_FEATURE_VARIANTS } from 'component/providers/AccessProvider/permissions';
|
import { UPDATE_FEATURE_VARIANTS } from 'component/providers/AccessProvider/permissions';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
|
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
|
||||||
@ -25,26 +23,43 @@ import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
|||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { updateWeight } from 'component/common/util';
|
import { updateWeight } from 'component/common/util';
|
||||||
import cloneDeep from 'lodash.clonedeep';
|
import cloneDeep from 'lodash.clonedeep';
|
||||||
import useDeleteVariantMarkup from './FeatureVariantsListItem/useDeleteVariantMarkup';
|
import useDeleteVariantMarkup from './useDeleteVariantMarkup';
|
||||||
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
|
import { Edit, Delete } from '@mui/icons-material';
|
||||||
|
import { useTable, useSortBy, useGlobalFilter } from 'react-table';
|
||||||
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
|
import { SortableTableHeader, TablePlaceholder } from 'component/common/Table';
|
||||||
|
import { sortTypes } from 'utils/sortTypes';
|
||||||
|
import { PayloadOverridesCell } from './PayloadOverridesCell/PayloadOverridesCell';
|
||||||
|
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||||
|
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||||
|
import theme from 'themes/theme';
|
||||||
|
import { VariantsActionCell } from './VariantsActionsCell/VariantsActionsCell';
|
||||||
|
|
||||||
const FeatureOverviewVariants = () => {
|
export const FeatureVariantsList = () => {
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const featureId = useRequiredPathParam('featureId');
|
const featureId = useRequiredPathParam('featureId');
|
||||||
const { feature, refetchFeature } = useFeature(projectId, featureId);
|
const { feature, refetchFeature, loading } = useFeature(
|
||||||
|
projectId,
|
||||||
|
featureId
|
||||||
|
);
|
||||||
const [variants, setVariants] = useState<IFeatureVariant[]>([]);
|
const [variants, setVariants] = useState<IFeatureVariant[]>([]);
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const { context } = useUnleashContext();
|
const { context } = useUnleashContext();
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
const { patchFeatureVariants } = useFeatureApi();
|
const { patchFeatureVariants } = useFeatureApi();
|
||||||
const [editVariant, setEditVariant] = useState({});
|
const [variantToEdit, setVariantToEdit] = useState({});
|
||||||
const [showAddVariant, setShowAddVariant] = useState(false);
|
const [showAddVariant, setShowAddVariant] = useState(false);
|
||||||
const [stickinessOptions, setStickinessOptions] = useState<string[]>([]);
|
const [stickinessOptions, setStickinessOptions] = useState<string[]>([]);
|
||||||
const [delDialog, setDelDialog] = useState({ name: '', show: false });
|
const [delDialog, setDelDialog] = useState({ name: '', show: false });
|
||||||
|
|
||||||
|
const isMediumScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
const isLargeScreen = useMediaQuery(theme.breakpoints.down('lg'));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (feature) {
|
if (feature) {
|
||||||
setClonedVariants(feature.variants);
|
setClonedVariants(feature.variants);
|
||||||
@ -63,6 +78,146 @@ const FeatureOverviewVariants = () => {
|
|||||||
|
|
||||||
const editable = hasAccess(UPDATE_FEATURE_VARIANTS, projectId);
|
const editable = hasAccess(UPDATE_FEATURE_VARIANTS, projectId);
|
||||||
|
|
||||||
|
const data = useMemo(() => {
|
||||||
|
if (loading) {
|
||||||
|
return Array(5).fill({
|
||||||
|
name: 'Context name',
|
||||||
|
description: 'Context description when loading',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return feature.variants;
|
||||||
|
}, [feature.variants, loading]);
|
||||||
|
|
||||||
|
const editVariant = useCallback(
|
||||||
|
(name: string) => {
|
||||||
|
const variant = {
|
||||||
|
...variants.find(variant => variant.name === name),
|
||||||
|
};
|
||||||
|
setVariantToEdit(variant);
|
||||||
|
setEditing(true);
|
||||||
|
setShowAddVariant(true);
|
||||||
|
},
|
||||||
|
[variants, setVariantToEdit, setEditing, setShowAddVariant]
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
Header: 'Name',
|
||||||
|
accessor: 'name',
|
||||||
|
width: '25%',
|
||||||
|
Cell: ({
|
||||||
|
row: {
|
||||||
|
original: { name },
|
||||||
|
},
|
||||||
|
}: any) => {
|
||||||
|
return <TextCell data-loading>{name}</TextCell>;
|
||||||
|
},
|
||||||
|
sortType: 'alphanumeric',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Payload/Overrides',
|
||||||
|
accessor: 'data',
|
||||||
|
Cell: ({
|
||||||
|
row: {
|
||||||
|
original: { overrides, payload },
|
||||||
|
},
|
||||||
|
}: any) => {
|
||||||
|
return (
|
||||||
|
<PayloadOverridesCell
|
||||||
|
overrides={overrides}
|
||||||
|
payload={payload}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
disableSortBy: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Weight',
|
||||||
|
accessor: 'weight',
|
||||||
|
width: '20%',
|
||||||
|
Cell: ({
|
||||||
|
row: {
|
||||||
|
original: { name, weight },
|
||||||
|
},
|
||||||
|
}: any) => {
|
||||||
|
return (
|
||||||
|
<TextCell data-testid={`VARIANT_WEIGHT_${name}`}>
|
||||||
|
{weight / 10.0} %
|
||||||
|
</TextCell>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
sortType: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Type',
|
||||||
|
accessor: 'weightType',
|
||||||
|
width: '20%',
|
||||||
|
Cell: ({
|
||||||
|
row: {
|
||||||
|
original: { weightType },
|
||||||
|
},
|
||||||
|
}: any) => {
|
||||||
|
return <TextCell>{weightType}</TextCell>;
|
||||||
|
},
|
||||||
|
sortType: 'alphanumeric',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Actions',
|
||||||
|
id: 'Actions',
|
||||||
|
align: 'right',
|
||||||
|
Cell: ({ row: { original } }: any) => (
|
||||||
|
<VariantsActionCell
|
||||||
|
editVariant={editVariant}
|
||||||
|
setDelDialog={setDelDialog}
|
||||||
|
variant={original as IFeatureVariant}
|
||||||
|
projectId={projectId}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
width: 150,
|
||||||
|
disableSortBy: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[projectId, editVariant]
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialState = useMemo(
|
||||||
|
() => ({
|
||||||
|
sortBy: [{ id: 'name', desc: false }],
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
getTableProps,
|
||||||
|
getTableBodyProps,
|
||||||
|
headerGroups,
|
||||||
|
rows,
|
||||||
|
prepareRow,
|
||||||
|
setHiddenColumns,
|
||||||
|
} = useTable(
|
||||||
|
{
|
||||||
|
columns: columns as any[],
|
||||||
|
data: data as any[],
|
||||||
|
initialState,
|
||||||
|
sortTypes,
|
||||||
|
autoResetGlobalFilter: false,
|
||||||
|
autoResetSortBy: false,
|
||||||
|
disableSortRemove: true,
|
||||||
|
},
|
||||||
|
useGlobalFilter,
|
||||||
|
useSortBy
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isMediumScreen) {
|
||||||
|
setHiddenColumns(['weightType', 'data']);
|
||||||
|
} else if (isLargeScreen) {
|
||||||
|
setHiddenColumns(['weightType']);
|
||||||
|
}
|
||||||
|
}, [setHiddenColumns, isMediumScreen, isLargeScreen]);
|
||||||
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
const setClonedVariants = clonedVariants =>
|
const setClonedVariants = clonedVariants =>
|
||||||
setVariants(cloneDeep(clonedVariants));
|
setVariants(cloneDeep(clonedVariants));
|
||||||
@ -70,26 +225,7 @@ const FeatureOverviewVariants = () => {
|
|||||||
const handleCloseAddVariant = () => {
|
const handleCloseAddVariant = () => {
|
||||||
setShowAddVariant(false);
|
setShowAddVariant(false);
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
setEditVariant({});
|
setVariantToEdit({});
|
||||||
};
|
|
||||||
|
|
||||||
const renderVariants = () => {
|
|
||||||
return variants.map(variant => {
|
|
||||||
return (
|
|
||||||
<FeatureVariantListItem
|
|
||||||
key={variant.name}
|
|
||||||
variant={variant}
|
|
||||||
editVariant={(name: string) => {
|
|
||||||
const v = { ...variants.find(v => v.name === name) };
|
|
||||||
setEditVariant(v);
|
|
||||||
setEditing(true);
|
|
||||||
setShowAddVariant(true);
|
|
||||||
}}
|
|
||||||
setDelDialog={setDelDialog}
|
|
||||||
editable={editable}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderStickiness = () => {
|
const renderStickiness = () => {
|
||||||
@ -118,10 +254,7 @@ const FeatureOverviewVariants = () => {
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<small
|
<small style={{ display: 'block', marginTop: '0.5rem' }}>
|
||||||
className={classnames(styles.paragraph, styles.helperText)}
|
|
||||||
style={{ display: 'block', marginTop: '0.5rem' }}
|
|
||||||
>
|
|
||||||
By overriding the stickiness you can control which parameter
|
By overriding the stickiness you can control which parameter
|
||||||
is used to ensure consistent traffic allocation across
|
is used to ensure consistent traffic allocation across
|
||||||
variants.{' '}
|
variants.{' '}
|
||||||
@ -245,55 +378,82 @@ const FeatureOverviewVariants = () => {
|
|||||||
return jsonpatch.compare(feature.variants, newVariants);
|
return jsonpatch.compare(feature.variants, newVariants);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addVariant = () => {
|
||||||
|
setEditing(false);
|
||||||
|
if (variants.length === 0) {
|
||||||
|
setVariantToEdit({ weight: 1000 });
|
||||||
|
} else {
|
||||||
|
setVariantToEdit({
|
||||||
|
weightType: 'variable',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setShowAddVariant(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section style={{ padding: '16px' }}>
|
<PageContent
|
||||||
<Typography variant="body1">
|
isLoading={loading}
|
||||||
|
header={
|
||||||
|
<PageHeader
|
||||||
|
title="Variants"
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<PermissionButton
|
||||||
|
onClick={addVariant}
|
||||||
|
data-testid={'ADD_VARIANT_BUTTON'}
|
||||||
|
permission={UPDATE_FEATURE_VARIANTS}
|
||||||
|
projectId={projectId}
|
||||||
|
>
|
||||||
|
New variant
|
||||||
|
</PermissionButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Alert severity="info" sx={{ marginBottom: '1rem' }}>
|
||||||
Variants allows you to return a variant object if the feature
|
Variants allows you to return a variant object if the feature
|
||||||
toggle is considered enabled for the current request. When using
|
toggle is considered enabled for the current request. When using
|
||||||
variants you should use the{' '}
|
variants you should use the{' '}
|
||||||
<code style={{ color: 'navy' }}>getVariant()</code> method in
|
<code style={{ fontWeight: 'bold' }}>getVariant()</code> method
|
||||||
the Client SDK.
|
in the Client SDK.
|
||||||
</Typography>
|
</Alert>
|
||||||
|
<Table {...getTableProps()}>
|
||||||
<ConditionallyRender
|
<SortableTableHeader headerGroups={headerGroups} />
|
||||||
condition={variants?.length > 0}
|
<TableBody {...getTableBodyProps()}>
|
||||||
show={
|
{rows.map(row => {
|
||||||
<Table className={styles.variantTable}>
|
prepareRow(row);
|
||||||
<TableHead>
|
return (
|
||||||
<TableRow>
|
<TableRow
|
||||||
<TableCell>Variant name</TableCell>
|
hover
|
||||||
<TableCell className={styles.labels} />
|
{...row.getRowProps()}
|
||||||
<TableCell>Weight</TableCell>
|
style={{ height: '75px' }}
|
||||||
<TableCell>Weight Type</TableCell>
|
>
|
||||||
<TableCell className={styles.actions} />
|
{row.cells.map(cell => (
|
||||||
|
<TableCell
|
||||||
|
{...cell.getCellProps()}
|
||||||
|
padding="none"
|
||||||
|
>
|
||||||
|
{cell.render('Cell')}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
);
|
||||||
<TableBody>{renderVariants()}</TableBody>
|
})}
|
||||||
</Table>
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={rows.length === 0}
|
||||||
|
show={
|
||||||
|
<TablePlaceholder>
|
||||||
|
No variants available. Get started by adding one.
|
||||||
|
</TablePlaceholder>
|
||||||
}
|
}
|
||||||
elseShow={<p>No variants defined.</p>}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<PermissionButton
|
|
||||||
onClick={() => {
|
|
||||||
setEditing(false);
|
|
||||||
if (variants.length === 0) {
|
|
||||||
setEditVariant({ weight: 1000 });
|
|
||||||
} else {
|
|
||||||
setEditVariant({ weightType: 'variable' });
|
|
||||||
}
|
|
||||||
setShowAddVariant(true);
|
|
||||||
}}
|
|
||||||
className={styles.addVariantButton}
|
|
||||||
data-testid={'ADD_VARIANT_BUTTON'}
|
|
||||||
permission={UPDATE_FEATURE_VARIANTS}
|
|
||||||
projectId={projectId}
|
|
||||||
>
|
|
||||||
New variant
|
|
||||||
</PermissionButton>
|
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={editable}
|
condition={editable}
|
||||||
show={renderStickiness()}
|
show={renderStickiness()}
|
||||||
@ -314,13 +474,11 @@ const FeatureOverviewVariants = () => {
|
|||||||
validateName={validateName}
|
validateName={validateName}
|
||||||
validateWeight={validateWeight}
|
validateWeight={validateWeight}
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
editVariant={editVariant}
|
editVariant={variantToEdit}
|
||||||
title={editing ? 'Edit variant' : 'Add variant'}
|
title={editing ? 'Edit variant' : 'Add variant'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{delDialogueMarkup}
|
{delDialogueMarkup}
|
||||||
</section>
|
</PageContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FeatureOverviewVariants;
|
|
||||||
|
@ -1,90 +0,0 @@
|
|||||||
import { Chip, IconButton, TableCell, TableRow, Tooltip } from '@mui/material';
|
|
||||||
import { Delete, Edit } from '@mui/icons-material';
|
|
||||||
|
|
||||||
import styles from '../variants.module.scss';
|
|
||||||
import { IFeatureVariant } from 'interfaces/featureToggle';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import { weightTypes } from '../AddFeatureVariant/enums';
|
|
||||||
|
|
||||||
interface IFeatureVariantListItem {
|
|
||||||
variant: IFeatureVariant;
|
|
||||||
editVariant: any;
|
|
||||||
setDelDialog: any;
|
|
||||||
editable: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FeatureVariantListItem = ({
|
|
||||||
variant,
|
|
||||||
editVariant,
|
|
||||||
setDelDialog,
|
|
||||||
editable,
|
|
||||||
}: IFeatureVariantListItem) => {
|
|
||||||
const { FIX } = weightTypes;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell data-testid={'VARIANT_NAME'}>{variant.name}</TableCell>
|
|
||||||
<TableCell className={styles.chipContainer}>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(variant.payload)}
|
|
||||||
show={<Chip label="Payload" />}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={
|
|
||||||
variant.overrides && variant.overrides.length > 0
|
|
||||||
}
|
|
||||||
show={
|
|
||||||
<Chip
|
|
||||||
style={{
|
|
||||||
backgroundColor: 'rgba(173, 216, 230, 0.2)',
|
|
||||||
}}
|
|
||||||
label="Overrides"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell data-testid={'VARIANT_WEIGHT'}>
|
|
||||||
{variant.weight / 10.0} %
|
|
||||||
</TableCell>
|
|
||||||
<TableCell data-testid={'VARIANT_WEIGHT_TYPE'}>
|
|
||||||
{variant.weightType === FIX ? 'Fix' : 'Variable'}
|
|
||||||
</TableCell>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={editable}
|
|
||||||
show={
|
|
||||||
<TableCell className={styles.actions}>
|
|
||||||
<div className={styles.actionsContainer}>
|
|
||||||
<Tooltip title="Edit variant" arrow>
|
|
||||||
<IconButton
|
|
||||||
data-testid={'VARIANT_EDIT_BUTTON'}
|
|
||||||
onClick={() => editVariant(variant.name)}
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
<Edit />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="Delete variant" arrow>
|
|
||||||
<IconButton
|
|
||||||
data-testid={`VARIANT_DELETE_BUTTON_${variant.name}`}
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setDelDialog({
|
|
||||||
show: true,
|
|
||||||
name: variant.name,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
<Delete />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
}
|
|
||||||
elseShow={<TableCell className={styles.actions} />}
|
|
||||||
/>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FeatureVariantListItem;
|
|
@ -0,0 +1,27 @@
|
|||||||
|
import { Chip } from '@mui/material';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||||
|
import { IOverride, IPayload } from 'interfaces/featureToggle';
|
||||||
|
|
||||||
|
interface IPayloadOverridesCellProps {
|
||||||
|
payload: IPayload;
|
||||||
|
overrides: IOverride[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PayloadOverridesCell = ({
|
||||||
|
payload,
|
||||||
|
overrides,
|
||||||
|
}: IPayloadOverridesCellProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(payload)}
|
||||||
|
show={<TextCell>Payload</TextCell>}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={overrides && overrides.length > 0}
|
||||||
|
show={<TextCell>Overrides</TextCell>}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,55 @@
|
|||||||
|
import { Edit, Delete } from '@mui/icons-material';
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||||
|
import { UPDATE_FEATURE_VARIANTS } from 'component/providers/AccessProvider/permissions';
|
||||||
|
import { IFeatureVariant } from 'interfaces/featureToggle';
|
||||||
|
|
||||||
|
interface IVarintsActionCellProps {
|
||||||
|
projectId: string;
|
||||||
|
editVariant: (name: string) => void;
|
||||||
|
setDelDialog: React.Dispatch<
|
||||||
|
React.SetStateAction<{
|
||||||
|
name: string;
|
||||||
|
show: boolean;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
variant: IFeatureVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VariantsActionCell = ({
|
||||||
|
projectId,
|
||||||
|
setDelDialog,
|
||||||
|
variant,
|
||||||
|
editVariant,
|
||||||
|
}: IVarintsActionCellProps) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
style={{ display: 'flex', justifyContent: 'flex-end' }}
|
||||||
|
data-loading
|
||||||
|
>
|
||||||
|
<PermissionIconButton
|
||||||
|
size="large"
|
||||||
|
data-testid={`VARIANT_EDIT_BUTTON_${variant.name}`}
|
||||||
|
permission={UPDATE_FEATURE_VARIANTS}
|
||||||
|
projectId={projectId}
|
||||||
|
onClick={() => editVariant(variant.name)}
|
||||||
|
>
|
||||||
|
<Edit />
|
||||||
|
</PermissionIconButton>
|
||||||
|
<PermissionIconButton
|
||||||
|
size="large"
|
||||||
|
permission={UPDATE_FEATURE_VARIANTS}
|
||||||
|
data-testid={`VARIANT_DELETE_BUTTON_${variant.name}`}
|
||||||
|
projectId={projectId}
|
||||||
|
onClick={() =>
|
||||||
|
setDelDialog({
|
||||||
|
show: true,
|
||||||
|
name: variant.name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Delete />
|
||||||
|
</PermissionIconButton>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
@ -1,100 +0,0 @@
|
|||||||
.variantTable {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 700px;
|
|
||||||
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
th:first-of-type,
|
|
||||||
td:first-of-type {
|
|
||||||
text-align: left;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
tbody tr:hover {
|
|
||||||
background-color: rgba(173, 216, 230, 0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
th.labels {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
td.labels {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
th.labels {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
td.labels {
|
|
||||||
text-align: right;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
th.actions {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
td.actions {
|
|
||||||
height: 100%;
|
|
||||||
text-align: right;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actionsContainer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal {
|
|
||||||
max-width: 90%;
|
|
||||||
width: 600px;
|
|
||||||
position: absolute !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.modal {
|
|
||||||
top: 0 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip {
|
|
||||||
i {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.inputWeight {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flexCenter {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.marginL10 {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.addVariantButton {
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph {
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.helperText {
|
|
||||||
display: block;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user