mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-24 17:51:14 +02:00
* feat: create screen * fix: import accordion summary * feat: add accordions * fix: add codebox * feat: select permissions * fix: permission checker * fix: update permission checker * feat: wire up role list * fix: change icon color in project roles list * fix: add color to icon in project roles * add confirm dialog on role deletion * feat: add created screen * fix: cleanup * fix: update access permissions * fix: update admin panel * feat: add edit screen * fix: use color from palette and show toast when fails * fix: refactor * feat: validation * feat: implement checked all * fix: experimental toast * fix: error handling * fix: toast * feat: unique name validation * fix: update toasts * fix: remove toast * fix: reset flag * fix: remove unused vars * fix: update tests * feat: add error icon for toast * fix: replace wrong import for setToastData * feat: Patch keying on ui to handle uniqueness for permissions across multiple envs * fix: hasAccess handles * * fix: update permission switch * fix: use flag for environments rbac * fix: do not include check all keys in payload * fix: filter roles * fix: account for new permissions in variants list * fix: use effect on length property * fix: set polling interval on user * 4.5.0-beta.0 * fix: set initial permissions correctly to avoid race condition * fix: handle activeEnvironment when it is null * fix: remove unused imports * fix: unused imports * fix: Include missing project in hasAccess for deleteinng a tag * fix: Move add/delete tag to use update feature permissions * fix: use rest parameter * fix: remove sandbox from scripts * 4.6.0-beta.1 * fix: remove loading deduping * fix: disable editing on builtin roles * fix: check all * fix: feature overview environment * fix: refetch user on project create * fix: update snaphots * fix: frontend permissions * fix: delete create confirm * fix: remove unused permission * 4.6.0-beta.4 * fix: update permissions * fix: permissions * fix: set error to string * 4.6.0-beta.5 * fix: add permissions for project view * fix: add permissions to useEffect deps * fix: update permission for move feature toggle * fix: add permissions data to useEffect * fix: move settings * fix: key on confetti * fix: refetch project permissions on environment create/delete * fix: optional coalescing error object * fix: remove logging error * fix: reorder disable importance in permissionbutton * fix: add project roles to menu * fix: add disabled check to revive * fix: update snapshots * fix: change text to select all * fix: change text to select * 4.6.0-beta.6 Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com> Co-authored-by: sighphyre <liquidwicked64@gmail.com>
329 lines
12 KiB
TypeScript
329 lines
12 KiB
TypeScript
import classnames from 'classnames';
|
|
import * as jsonpatch from 'fast-json-patch';
|
|
|
|
import styles from './variants.module.scss';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableRow,
|
|
Typography,
|
|
} from '@material-ui/core';
|
|
import AddVariant from './AddFeatureVariant/AddFeatureVariant';
|
|
|
|
import { useContext, useEffect, useState } from 'react';
|
|
import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature';
|
|
import { useParams } from 'react-router';
|
|
import { IFeatureViewParams } from '../../../../../interfaces/params';
|
|
import AccessContext from '../../../../../contexts/AccessContext';
|
|
import FeatureVariantListItem from './FeatureVariantsListItem/FeatureVariantsListItem';
|
|
import { UPDATE_FEATURE_VARIANTS } from '../../../../providers/AccessProvider/permissions';
|
|
import ConditionallyRender from '../../../../common/ConditionallyRender';
|
|
import useUnleashContext from '../../../../../hooks/api/getters/useUnleashContext/useUnleashContext';
|
|
import GeneralSelect from '../../../../common/GeneralSelect/GeneralSelect';
|
|
import { IFeatureVariant } from '../../../../../interfaces/featureToggle';
|
|
import useFeatureApi from '../../../../../hooks/api/actions/useFeatureApi/useFeatureApi';
|
|
import useToast from '../../../../../hooks/useToast';
|
|
import { updateWeight } from '../../../../common/util';
|
|
import cloneDeep from 'lodash.clonedeep';
|
|
import useDeleteVariantMarkup from './FeatureVariantsListItem/useDeleteVariantMarkup';
|
|
import PermissionButton from '../../../../common/PermissionButton/PermissionButton';
|
|
import { mutate } from 'swr';
|
|
|
|
const FeatureOverviewVariants = () => {
|
|
const { hasAccess } = useContext(AccessContext);
|
|
const { projectId, featureId } = useParams<IFeatureViewParams>();
|
|
const { feature, FEATURE_CACHE_KEY } = useFeature(projectId, featureId);
|
|
const [variants, setVariants] = useState<IFeatureVariant[]>([]);
|
|
const [editing, setEditing] = useState(false);
|
|
const { context } = useUnleashContext();
|
|
const { setToastData, setToastApiError } = useToast();
|
|
const { patchFeatureVariants } = useFeatureApi();
|
|
const [editVariant, setEditVariant] = useState({});
|
|
const [showAddVariant, setShowAddVariant] = useState(false);
|
|
const [stickinessOptions, setStickinessOptions] = useState([]);
|
|
const [delDialog, setDelDialog] = useState({ name: '', show: false });
|
|
|
|
useEffect(() => {
|
|
if (feature) {
|
|
setClonedVariants(feature.variants);
|
|
}
|
|
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
|
}, [feature.variants]);
|
|
|
|
useEffect(() => {
|
|
const options = [
|
|
'default',
|
|
...context.filter(c => c.stickiness).map(c => c.name),
|
|
];
|
|
|
|
setStickinessOptions(options);
|
|
}, [context]);
|
|
|
|
const editable = hasAccess(UPDATE_FEATURE_VARIANTS, projectId);
|
|
|
|
const setClonedVariants = clonedVariants =>
|
|
setVariants(cloneDeep(clonedVariants));
|
|
|
|
const handleCloseAddVariant = () => {
|
|
setShowAddVariant(false);
|
|
setEditing(false);
|
|
setEditVariant({});
|
|
};
|
|
|
|
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 = () => {
|
|
if (!variants || variants.length < 2) {
|
|
return null;
|
|
}
|
|
|
|
const value = variants[0].stickiness || 'default';
|
|
const options = stickinessOptions.map(c => ({ key: c, label: c }));
|
|
|
|
// guard on stickiness being disabled for context field.
|
|
if (!stickinessOptions.includes(value)) {
|
|
options.push({ key: value, label: value });
|
|
}
|
|
|
|
const onChange = event => {
|
|
updateStickiness(event.target.value);
|
|
};
|
|
|
|
return (
|
|
<section style={{ paddingTop: '16px' }}>
|
|
<GeneralSelect
|
|
label="Stickiness"
|
|
options={options}
|
|
value={value}
|
|
onChange={onChange}
|
|
/>
|
|
|
|
<small
|
|
className={classnames(styles.paragraph, styles.helperText)}
|
|
style={{ display: 'block', marginTop: '0.5rem' }}
|
|
>
|
|
By overriding the stickiness you can control which parameter
|
|
is used to ensure consistent traffic allocation across
|
|
variants.{' '}
|
|
<a
|
|
href="https://docs.getunleash.io/advanced/toggle_variants"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>
|
|
Read more
|
|
</a>
|
|
</small>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
const updateStickiness = async (stickiness: string) => {
|
|
const newVariants = [...variants].map(variant => {
|
|
return { ...variant, stickiness };
|
|
});
|
|
|
|
const patch = createPatch(newVariants);
|
|
|
|
if (patch.length === 0) return;
|
|
|
|
try {
|
|
const res = await patchFeatureVariants(projectId, featureId, patch);
|
|
// @ts-ignore
|
|
const { variants } = await res.json();
|
|
mutate(FEATURE_CACHE_KEY, { ...feature, variants }, false);
|
|
setToastData({
|
|
title: 'Updated variant',
|
|
confetti: true,
|
|
type: 'success',
|
|
text: 'Successfully updated variant stickiness',
|
|
});
|
|
} catch (e) {
|
|
setToastApiError(e.message);
|
|
}
|
|
};
|
|
|
|
const removeVariant = async (name: string) => {
|
|
let updatedVariants = variants.filter(v => v.name !== name);
|
|
try {
|
|
await updateVariants(
|
|
updatedVariants,
|
|
'Successfully removed variant'
|
|
);
|
|
} catch (e) {
|
|
setToastApiError(e.message);
|
|
}
|
|
};
|
|
const updateVariant = async (variant: IFeatureVariant) => {
|
|
const updatedVariants = cloneDeep(variants);
|
|
const variantIdxToUpdate = updatedVariants.findIndex(
|
|
(v: IFeatureVariant) => v.name === variant.name
|
|
);
|
|
updatedVariants[variantIdxToUpdate] = variant;
|
|
await updateVariants(updatedVariants, 'Successfully updated variant');
|
|
};
|
|
|
|
const saveNewVariant = async (variant: IFeatureVariant) => {
|
|
let stickiness = 'default';
|
|
if (variants?.length > 0) {
|
|
stickiness = variants[0].stickiness || 'default';
|
|
}
|
|
variant.stickiness = stickiness;
|
|
|
|
await updateVariants(
|
|
[...variants, variant],
|
|
'Successfully added a variant'
|
|
);
|
|
};
|
|
|
|
const updateVariants = async (
|
|
variants: IFeatureVariant[],
|
|
successText: string
|
|
) => {
|
|
const newVariants = updateWeight(variants, 1000);
|
|
const patch = createPatch(newVariants);
|
|
|
|
if (patch.length === 0) return;
|
|
try {
|
|
const res = await patchFeatureVariants(projectId, featureId, patch);
|
|
// @ts-ignore
|
|
const { variants } = await res.json();
|
|
mutate(FEATURE_CACHE_KEY, { ...feature, variants }, false);
|
|
setToastData({
|
|
title: 'Updated variant',
|
|
type: 'success',
|
|
text: successText,
|
|
});
|
|
} catch (e) {
|
|
setToastApiError(e.message);
|
|
}
|
|
};
|
|
|
|
const validateName = (name: string) => {
|
|
if (!name) {
|
|
return { name: 'Name is required' };
|
|
}
|
|
};
|
|
|
|
const validateWeight = (weight: number) => {
|
|
const weightValue = parseInt(weight);
|
|
if (weightValue > 100 || weightValue < 0) {
|
|
return { weight: 'weight must be between 0 and 100' };
|
|
}
|
|
};
|
|
|
|
const delDialogueMarkup = useDeleteVariantMarkup({
|
|
show: delDialog.show,
|
|
onClick: e => {
|
|
removeVariant(delDialog.name);
|
|
setDelDialog({ name: '', show: false });
|
|
setToastData({
|
|
title: 'Deleted variant',
|
|
type: 'success',
|
|
text: `Successfully deleted variant`,
|
|
});
|
|
},
|
|
onClose: () => setDelDialog({ show: false, name: '' }),
|
|
});
|
|
|
|
const createPatch = (newVariants: IFeatureVariant[]) => {
|
|
return jsonpatch.compare(feature.variants, newVariants);
|
|
};
|
|
|
|
return (
|
|
<section style={{ padding: '16px' }}>
|
|
<Typography variant="body1">
|
|
Variants allows you to return a variant object if the feature
|
|
toggle is considered enabled for the current request. When using
|
|
variants you should use the{' '}
|
|
<code style={{ color: 'navy' }}>getVariant()</code> method in
|
|
the Client SDK.
|
|
</Typography>
|
|
|
|
<ConditionallyRender
|
|
condition={variants?.length > 0}
|
|
show={
|
|
<Table className={styles.variantTable}>
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell>Variant name</TableCell>
|
|
<TableCell className={styles.labels} />
|
|
<TableCell>Weight</TableCell>
|
|
<TableCell>Weight Type</TableCell>
|
|
<TableCell className={styles.actions} />
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>{renderVariants()}</TableBody>
|
|
</Table>
|
|
}
|
|
elseShow={<p>No variants defined.</p>}
|
|
/>
|
|
|
|
<br />
|
|
|
|
<div>
|
|
<PermissionButton
|
|
onClick={() => {
|
|
setEditing(false);
|
|
if (variants.length === 0) {
|
|
setEditVariant({ weight: 1000 });
|
|
} else {
|
|
setEditVariant({ weightType: 'variable' });
|
|
}
|
|
setShowAddVariant(true);
|
|
}}
|
|
className={styles.addVariantButton}
|
|
data-test={'ADD_VARIANT_BUTTON'}
|
|
permission={UPDATE_FEATURE_VARIANTS}
|
|
projectId={projectId}
|
|
>
|
|
Add variant
|
|
</PermissionButton>
|
|
<ConditionallyRender
|
|
condition={editable}
|
|
show={renderStickiness()}
|
|
/>
|
|
</div>
|
|
|
|
<AddVariant
|
|
showDialog={showAddVariant}
|
|
closeDialog={handleCloseAddVariant}
|
|
save={async (variantToSave: IFeatureVariant) => {
|
|
if (!editing) {
|
|
return saveNewVariant(variantToSave);
|
|
} else {
|
|
return updateVariant(variantToSave);
|
|
}
|
|
}}
|
|
editing={editing}
|
|
validateName={validateName}
|
|
validateWeight={validateWeight}
|
|
editVariant={editVariant}
|
|
title={editing ? 'Edit variant' : 'Add variant'}
|
|
/>
|
|
|
|
{delDialogueMarkup}
|
|
</section>
|
|
);
|
|
};
|
|
|
|
export default FeatureOverviewVariants;
|