mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-10 01:16:39 +02:00
feat: variant dependencies ui (#6739)
This commit is contained in:
parent
9eee6546f8
commit
7f043c7cab
@ -42,8 +42,9 @@ export const DependencyChange: VFC<{
|
||||
>
|
||||
{change.payload.feature}
|
||||
</StyledLink>
|
||||
{change.payload.enabled === false
|
||||
? ' (disabled)'
|
||||
{!change.payload.enabled ? ' (disabled)' : null}
|
||||
{change.payload.variants?.length
|
||||
? `(${change.payload.variants?.join(', ')})`
|
||||
: null}
|
||||
</AddDependencyWrapper>
|
||||
{actions}
|
||||
|
@ -223,7 +223,11 @@ type ChangeRequestVariantPatch = {
|
||||
|
||||
type ChangeRequestEnabled = { enabled: boolean };
|
||||
|
||||
type ChangeRequestAddDependency = { feature: string; enabled: boolean };
|
||||
type ChangeRequestAddDependency = {
|
||||
feature: string;
|
||||
enabled: boolean;
|
||||
variants?: string[];
|
||||
};
|
||||
|
||||
export type ChangeRequestAddStrategy = Pick<
|
||||
IFeatureStrategy,
|
||||
|
@ -91,7 +91,7 @@ test('Edit dependency', async () => {
|
||||
<AddDependencyDialogue
|
||||
project='default'
|
||||
featureId='child'
|
||||
parentFeatureId='parentB'
|
||||
parentDependency={{ feature: 'parentB' }}
|
||||
showDependencyDialogue={true}
|
||||
onClose={() => {
|
||||
closed = true;
|
||||
|
@ -1,205 +1,60 @@
|
||||
import { type FC, useState } from 'react';
|
||||
import { Box, styled, Typography } from '@mui/material';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
|
||||
import { useDependentFeaturesApi } from 'hooks/api/actions/useDependentFeaturesApi/useDependentFeaturesApi';
|
||||
import { useParentOptions } from 'hooks/api/getters/useParentOptions/useParentOptions';
|
||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { useHighestPermissionChangeRequestEnvironment } from 'hooks/useHighestPermissionChangeRequestEnvironment';
|
||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
||||
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||
import { DependenciesUpgradeAlert } from './DependenciesUpgradeAlert';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import type { IDependency } from '../../../interfaces/featureToggle';
|
||||
import { ParentVariantOptions } from './ParentVariantOptions';
|
||||
import { type ParentValue, REMOVE_DEPENDENCY_OPTION } from './constants';
|
||||
import { FeatureStatusOptions } from './FeatureStatusOptions';
|
||||
import { useManageDependency } from './useManageDependency';
|
||||
import { LazyParentOptions } from './LazyParentOptions';
|
||||
|
||||
interface IAddDependencyDialogueProps {
|
||||
project: string;
|
||||
featureId: string;
|
||||
parentFeatureId?: string;
|
||||
parentFeatureValue?: ParentValue;
|
||||
parentDependency?: IDependency;
|
||||
showDependencyDialogue: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const StyledSelect = styled(GeneralSelect)(({ theme }) => ({
|
||||
marginTop: theme.spacing(2),
|
||||
marginBottom: theme.spacing(1.5),
|
||||
}));
|
||||
|
||||
const REMOVE_DEPENDENCY_OPTION = {
|
||||
key: 'none (remove dependency)',
|
||||
label: 'none (remove dependency)',
|
||||
};
|
||||
|
||||
// Project can have 100s of parents. We want to read them only when the modal for dependencies opens.
|
||||
const LazyOptions: FC<{
|
||||
project: string;
|
||||
featureId: string;
|
||||
parent: string;
|
||||
onSelect: (parent: string) => void;
|
||||
}> = ({ project, featureId, parent, onSelect }) => {
|
||||
const { parentOptions } = useParentOptions(project, featureId);
|
||||
|
||||
const options = parentOptions
|
||||
? [
|
||||
REMOVE_DEPENDENCY_OPTION,
|
||||
...parentOptions.map((parent) => ({
|
||||
key: parent,
|
||||
label: parent,
|
||||
})),
|
||||
]
|
||||
: [REMOVE_DEPENDENCY_OPTION];
|
||||
return (
|
||||
<StyledSelect
|
||||
fullWidth
|
||||
options={options}
|
||||
value={parent}
|
||||
onChange={onSelect}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const FeatureValueOptions: FC<{
|
||||
parentValue: ParentValue;
|
||||
onSelect: (parent: string) => void;
|
||||
}> = ({ onSelect, parentValue }) => {
|
||||
return (
|
||||
<StyledSelect
|
||||
fullWidth
|
||||
options={[
|
||||
{ key: 'enabled', label: 'enabled' },
|
||||
{ key: 'disabled', label: 'disabled' },
|
||||
]}
|
||||
value={parentValue.status}
|
||||
onChange={onSelect}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type ParentValue = { status: 'enabled' } | { status: 'disabled' };
|
||||
|
||||
const useManageDependency = (
|
||||
project: string,
|
||||
featureId: string,
|
||||
parent: string,
|
||||
parentValue: ParentValue,
|
||||
onClose: () => void,
|
||||
) => {
|
||||
const { trackEvent } = usePlausibleTracker();
|
||||
const { addChange } = useChangeRequestApi();
|
||||
const { refetch: refetchChangeRequests } =
|
||||
usePendingChangeRequests(project);
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { refetchFeature } = useFeature(project, featureId);
|
||||
const environment = useHighestPermissionChangeRequestEnvironment(project)();
|
||||
const { isChangeRequestConfiguredInAnyEnv } =
|
||||
useChangeRequestsEnabled(project);
|
||||
const { addDependency, removeDependencies } =
|
||||
useDependentFeaturesApi(project);
|
||||
|
||||
const handleAddChange = async (
|
||||
actionType: 'addDependency' | 'deleteDependency',
|
||||
) => {
|
||||
if (!environment) {
|
||||
console.error('No change request environment');
|
||||
return;
|
||||
}
|
||||
if (actionType === 'addDependency') {
|
||||
await addChange(project, environment, [
|
||||
{
|
||||
action: actionType,
|
||||
feature: featureId,
|
||||
payload: {
|
||||
feature: parent,
|
||||
enabled: parentValue.status !== 'disabled',
|
||||
},
|
||||
},
|
||||
]);
|
||||
trackEvent('dependent_features', {
|
||||
props: {
|
||||
eventType: 'dependency added',
|
||||
},
|
||||
});
|
||||
}
|
||||
if (actionType === 'deleteDependency') {
|
||||
await addChange(project, environment, [
|
||||
{ action: actionType, feature: featureId, payload: undefined },
|
||||
]);
|
||||
}
|
||||
void refetchChangeRequests();
|
||||
setToastData({
|
||||
text:
|
||||
actionType === 'addDependency'
|
||||
? `${featureId} will depend on ${parent}`
|
||||
: `${featureId} dependency will be removed`,
|
||||
type: 'success',
|
||||
title: 'Change added to a draft',
|
||||
});
|
||||
};
|
||||
|
||||
return async () => {
|
||||
try {
|
||||
if (isChangeRequestConfiguredInAnyEnv()) {
|
||||
const actionType =
|
||||
parent === REMOVE_DEPENDENCY_OPTION.key
|
||||
? 'deleteDependency'
|
||||
: 'addDependency';
|
||||
await handleAddChange(actionType);
|
||||
trackEvent('dependent_features', {
|
||||
props: {
|
||||
eventType:
|
||||
actionType === 'addDependency'
|
||||
? 'add dependency added to change request'
|
||||
: 'delete dependency added to change request',
|
||||
},
|
||||
});
|
||||
} else if (parent === REMOVE_DEPENDENCY_OPTION.key) {
|
||||
await removeDependencies(featureId);
|
||||
trackEvent('dependent_features', {
|
||||
props: {
|
||||
eventType: 'dependency removed',
|
||||
},
|
||||
});
|
||||
setToastData({ title: 'Dependency removed', type: 'success' });
|
||||
} else {
|
||||
await addDependency(featureId, {
|
||||
feature: parent,
|
||||
enabled: parentValue.status !== 'disabled',
|
||||
});
|
||||
trackEvent('dependent_features', {
|
||||
props: {
|
||||
eventType: 'dependency added',
|
||||
},
|
||||
});
|
||||
setToastData({ title: 'Dependency added', type: 'success' });
|
||||
}
|
||||
} catch (error) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
void refetchFeature();
|
||||
onClose();
|
||||
};
|
||||
};
|
||||
|
||||
export const AddDependencyDialogue = ({
|
||||
project,
|
||||
featureId,
|
||||
parentFeatureId,
|
||||
parentFeatureValue,
|
||||
parentDependency,
|
||||
showDependencyDialogue,
|
||||
onClose,
|
||||
}: IAddDependencyDialogueProps) => {
|
||||
const [parent, setParent] = useState(
|
||||
parentFeatureId || REMOVE_DEPENDENCY_OPTION.key,
|
||||
parentDependency?.feature || REMOVE_DEPENDENCY_OPTION.key,
|
||||
);
|
||||
|
||||
const getInitialParentValue = (): ParentValue => {
|
||||
if (!parentDependency) return { status: 'enabled' };
|
||||
if (parentDependency.variants?.length)
|
||||
return {
|
||||
status: 'enabled_with_variants',
|
||||
variants: parentDependency.variants,
|
||||
};
|
||||
if (parentDependency.enabled === false) return { status: 'disabled' };
|
||||
return { status: 'enabled' };
|
||||
};
|
||||
const [parentValue, setParentValue] = useState<ParentValue>(
|
||||
parentFeatureValue || { status: 'enabled' },
|
||||
getInitialParentValue,
|
||||
);
|
||||
const handleClick = useManageDependency(
|
||||
|
||||
const resetState = () => {
|
||||
setParent(parentDependency?.feature || REMOVE_DEPENDENCY_OPTION.key);
|
||||
setParentValue(getInitialParentValue());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
resetState();
|
||||
}, [JSON.stringify(parentDependency)]);
|
||||
|
||||
const manageDependency = useManageDependency(
|
||||
project,
|
||||
featureId,
|
||||
parent,
|
||||
@ -210,13 +65,38 @@ export const AddDependencyDialogue = ({
|
||||
useChangeRequestsEnabled(project);
|
||||
|
||||
const variantDependenciesEnabled = useUiFlag('variantDependencies');
|
||||
const showStatus =
|
||||
parent !== REMOVE_DEPENDENCY_OPTION.key && variantDependenciesEnabled;
|
||||
const showVariants =
|
||||
parent !== REMOVE_DEPENDENCY_OPTION.key &&
|
||||
variantDependenciesEnabled &&
|
||||
parentValue.status === 'enabled_with_variants';
|
||||
|
||||
const selectStatus = (value: string) => {
|
||||
if (value === 'enabled' || value === 'disabled') {
|
||||
setParentValue({ status: value });
|
||||
}
|
||||
if (value === 'enabled_with_variants') {
|
||||
setParentValue({
|
||||
status: value,
|
||||
variants: [],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const selectVariants = (variants: string[]) => {
|
||||
setParentValue({
|
||||
status: 'enabled_with_variants',
|
||||
variants,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialogue
|
||||
open={showDependencyDialogue}
|
||||
title='Add parent feature dependency'
|
||||
onClose={onClose}
|
||||
onClick={handleClick}
|
||||
onClick={manageDependency}
|
||||
primaryButtonText={
|
||||
isChangeRequestConfiguredInAnyEnv()
|
||||
? 'Add change to draft'
|
||||
@ -245,7 +125,7 @@ export const AddDependencyDialogue = ({
|
||||
<ConditionallyRender
|
||||
condition={showDependencyDialogue}
|
||||
show={
|
||||
<LazyOptions
|
||||
<LazyParentOptions
|
||||
project={project}
|
||||
featureId={featureId}
|
||||
parent={parent}
|
||||
@ -258,30 +138,40 @@ export const AddDependencyDialogue = ({
|
||||
/>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
parent !== REMOVE_DEPENDENCY_OPTION.key &&
|
||||
variantDependenciesEnabled
|
||||
}
|
||||
condition={showStatus}
|
||||
show={
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography>
|
||||
What <b>feature status</b> do you want to depend
|
||||
on?
|
||||
</Typography>
|
||||
<FeatureValueOptions
|
||||
<FeatureStatusOptions
|
||||
parentValue={parentValue}
|
||||
onSelect={(value) =>
|
||||
setParentValue({
|
||||
status:
|
||||
value === 'disabled'
|
||||
? 'disabled'
|
||||
: 'enabled',
|
||||
})
|
||||
}
|
||||
onSelect={selectStatus}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={showVariants}
|
||||
show={
|
||||
parentValue.status === 'enabled_with_variants' && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography>
|
||||
What <b>variant</b> do you want to depend
|
||||
on?
|
||||
</Typography>
|
||||
<ParentVariantOptions
|
||||
parent={parent}
|
||||
project={project}
|
||||
selectedValues={parentValue.variants}
|
||||
onSelect={selectVariants}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Dialogue>
|
||||
);
|
||||
|
@ -0,0 +1,29 @@
|
||||
import type { FC } from 'react';
|
||||
import type { ParentValue } from './constants';
|
||||
import { styled } from '@mui/material';
|
||||
import GeneralSelect from '../../common/GeneralSelect/GeneralSelect';
|
||||
|
||||
export const StyledSelect = styled(GeneralSelect)(({ theme }) => ({
|
||||
marginTop: theme.spacing(2),
|
||||
marginBottom: theme.spacing(1.5),
|
||||
}));
|
||||
export const FeatureStatusOptions: FC<{
|
||||
parentValue: ParentValue;
|
||||
onSelect: (parent: string) => void;
|
||||
}> = ({ onSelect, parentValue }) => {
|
||||
return (
|
||||
<StyledSelect
|
||||
fullWidth
|
||||
options={[
|
||||
{ key: 'enabled', label: 'enabled' },
|
||||
{
|
||||
key: 'enabled_with_variants',
|
||||
label: 'enabled with variants',
|
||||
},
|
||||
{ key: 'disabled', label: 'disabled' },
|
||||
]}
|
||||
value={parentValue.status}
|
||||
onChange={onSelect}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,33 @@
|
||||
import type { FC } from 'react';
|
||||
import { useParentOptions } from 'hooks/api/getters/useFeatureDependencyOptions/useFeatureDependencyOptions';
|
||||
import { REMOVE_DEPENDENCY_OPTION } from './constants';
|
||||
import { StyledSelect } from './FeatureStatusOptions';
|
||||
|
||||
// Project can have 100s of parents. We want to read them only when the modal for dependencies opens.
|
||||
export const LazyParentOptions: FC<{
|
||||
project: string;
|
||||
featureId: string;
|
||||
parent: string;
|
||||
onSelect: (parent: string) => void;
|
||||
}> = ({ project, featureId, parent, onSelect }) => {
|
||||
const { parentOptions } = useParentOptions(project, featureId);
|
||||
|
||||
const options = parentOptions
|
||||
? [
|
||||
REMOVE_DEPENDENCY_OPTION,
|
||||
...parentOptions.map((parent) => ({
|
||||
key: parent,
|
||||
label: parent,
|
||||
})),
|
||||
]
|
||||
: [REMOVE_DEPENDENCY_OPTION];
|
||||
|
||||
return (
|
||||
<StyledSelect
|
||||
fullWidth
|
||||
options={options}
|
||||
value={parent}
|
||||
onChange={onSelect}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,48 @@
|
||||
import { Autocomplete, Checkbox, styled, TextField } from '@mui/material';
|
||||
import type { FC } from 'react';
|
||||
import { useParentVariantOptions } from 'hooks/api/getters/useFeatureDependencyOptions/useFeatureDependencyOptions';
|
||||
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
|
||||
import CheckBoxIcon from '@mui/icons-material/CheckBox';
|
||||
|
||||
const StyledAutocomplete = styled(Autocomplete)(({ theme }) => ({
|
||||
marginTop: theme.spacing(2),
|
||||
marginBottom: theme.spacing(1.5),
|
||||
}));
|
||||
|
||||
export const ParentVariantOptions: FC<{
|
||||
project: string;
|
||||
parent: string;
|
||||
selectedValues: string[];
|
||||
onSelect: (values: string[]) => void;
|
||||
}> = ({ project, parent, onSelect, selectedValues }) => {
|
||||
const { parentVariantOptions } = useParentVariantOptions(project, parent);
|
||||
const icon = <CheckBoxOutlineBlankIcon fontSize='small' />;
|
||||
const checkedIcon = <CheckBoxIcon fontSize='small' />;
|
||||
return (
|
||||
<StyledAutocomplete
|
||||
multiple
|
||||
id='parent-variant-options'
|
||||
options={parentVariantOptions}
|
||||
disableCloseOnSelect
|
||||
renderOption={(props, option, { selected }) => (
|
||||
<li {...props}>
|
||||
<Checkbox
|
||||
icon={icon}
|
||||
checkedIcon={checkedIcon}
|
||||
style={{ marginRight: 8 }}
|
||||
checked={selected}
|
||||
/>
|
||||
{option}
|
||||
</li>
|
||||
)}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} placeholder='Select values' />
|
||||
)}
|
||||
fullWidth
|
||||
value={selectedValues}
|
||||
onChange={(_, selectedValues) => {
|
||||
onSelect(selectedValues as string[]);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
12
frontend/src/component/feature/Dependencies/constants.ts
Normal file
12
frontend/src/component/feature/Dependencies/constants.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export type ParentValue =
|
||||
| { status: 'enabled' }
|
||||
| { status: 'disabled' }
|
||||
| {
|
||||
status: 'enabled_with_variants';
|
||||
variants: string[];
|
||||
};
|
||||
|
||||
export const REMOVE_DEPENDENCY_OPTION = {
|
||||
key: 'none (remove dependency)',
|
||||
label: 'none (remove dependency)',
|
||||
};
|
@ -0,0 +1,121 @@
|
||||
import { type ParentValue, REMOVE_DEPENDENCY_OPTION } from './constants';
|
||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
||||
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
import { useHighestPermissionChangeRequestEnvironment } from 'hooks/useHighestPermissionChangeRequestEnvironment';
|
||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||
import { useDependentFeaturesApi } from 'hooks/api/actions/useDependentFeaturesApi/useDependentFeaturesApi';
|
||||
import { formatUnknownError } from '../../../utils/formatUnknownError';
|
||||
|
||||
export const useManageDependency = (
|
||||
project: string,
|
||||
featureId: string,
|
||||
parent: string,
|
||||
parentValue: ParentValue,
|
||||
onClose: () => void,
|
||||
) => {
|
||||
const { trackEvent } = usePlausibleTracker();
|
||||
const { addChange } = useChangeRequestApi();
|
||||
const { refetch: refetchChangeRequests } =
|
||||
usePendingChangeRequests(project);
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { refetchFeature } = useFeature(project, featureId);
|
||||
const environment = useHighestPermissionChangeRequestEnvironment(project)();
|
||||
const { isChangeRequestConfiguredInAnyEnv } =
|
||||
useChangeRequestsEnabled(project);
|
||||
const { addDependency, removeDependencies } =
|
||||
useDependentFeaturesApi(project);
|
||||
|
||||
const handleAddChange = async (
|
||||
actionType: 'addDependency' | 'deleteDependency',
|
||||
) => {
|
||||
if (!environment) {
|
||||
console.error('No change request environment');
|
||||
return;
|
||||
}
|
||||
if (actionType === 'addDependency') {
|
||||
await addChange(project, environment, [
|
||||
{
|
||||
action: actionType,
|
||||
feature: featureId,
|
||||
payload: {
|
||||
feature: parent,
|
||||
enabled: parentValue.status !== 'disabled',
|
||||
variants:
|
||||
parentValue.status === 'enabled_with_variants'
|
||||
? parentValue.variants
|
||||
: [],
|
||||
},
|
||||
},
|
||||
]);
|
||||
trackEvent('dependent_features', {
|
||||
props: {
|
||||
eventType: 'dependency added',
|
||||
},
|
||||
});
|
||||
}
|
||||
if (actionType === 'deleteDependency') {
|
||||
await addChange(project, environment, [
|
||||
{ action: actionType, feature: featureId, payload: undefined },
|
||||
]);
|
||||
}
|
||||
void refetchChangeRequests();
|
||||
setToastData({
|
||||
text:
|
||||
actionType === 'addDependency'
|
||||
? `${featureId} will depend on ${parent}`
|
||||
: `${featureId} dependency will be removed`,
|
||||
type: 'success',
|
||||
title: 'Change added to a draft',
|
||||
});
|
||||
};
|
||||
|
||||
return async () => {
|
||||
try {
|
||||
if (isChangeRequestConfiguredInAnyEnv()) {
|
||||
const actionType =
|
||||
parent === REMOVE_DEPENDENCY_OPTION.key
|
||||
? 'deleteDependency'
|
||||
: 'addDependency';
|
||||
await handleAddChange(actionType);
|
||||
trackEvent('dependent_features', {
|
||||
props: {
|
||||
eventType:
|
||||
actionType === 'addDependency'
|
||||
? 'add dependency added to change request'
|
||||
: 'delete dependency added to change request',
|
||||
},
|
||||
});
|
||||
} else if (parent === REMOVE_DEPENDENCY_OPTION.key) {
|
||||
await removeDependencies(featureId);
|
||||
trackEvent('dependent_features', {
|
||||
props: {
|
||||
eventType: 'dependency removed',
|
||||
},
|
||||
});
|
||||
setToastData({ title: 'Dependency removed', type: 'success' });
|
||||
} else {
|
||||
await addDependency(featureId, {
|
||||
feature: parent,
|
||||
enabled: parentValue.status !== 'disabled',
|
||||
variants:
|
||||
parentValue.status === 'enabled_with_variants'
|
||||
? parentValue.variants
|
||||
: [],
|
||||
});
|
||||
trackEvent('dependent_features', {
|
||||
props: {
|
||||
eventType: 'dependency added',
|
||||
},
|
||||
});
|
||||
setToastData({ title: 'Dependency added', type: 'success' });
|
||||
}
|
||||
} catch (error) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
void refetchFeature();
|
||||
onClose();
|
||||
};
|
||||
};
|
@ -17,6 +17,7 @@ import { useHighestPermissionChangeRequestEnvironment } from 'hooks/useHighestPe
|
||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||
import { VariantsTooltip } from './VariantsTooltip';
|
||||
|
||||
const useDeleteDependency = (project: string, featureId: string) => {
|
||||
const { trackEvent } = usePlausibleTracker();
|
||||
@ -152,11 +153,25 @@ export const DependencyRow: FC<{ feature: IFeatureToggle }> = ({ feature }) => {
|
||||
<FlexRow>
|
||||
<StyledDetail>
|
||||
<StyledLabel>Dependency value:</StyledLabel>
|
||||
<span>
|
||||
{feature.dependencies[0]?.enabled
|
||||
? 'enabled'
|
||||
: 'disabled'}
|
||||
</span>
|
||||
<span>disabled</span>
|
||||
</StyledDetail>
|
||||
</FlexRow>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
hasParentDependency &&
|
||||
Boolean(feature.dependencies[0]?.variants?.length)
|
||||
}
|
||||
show={
|
||||
<FlexRow>
|
||||
<StyledDetail>
|
||||
<StyledLabel>Dependency value:</StyledLabel>
|
||||
<VariantsTooltip
|
||||
variants={
|
||||
feature.dependencies[0]?.variants || []
|
||||
}
|
||||
/>
|
||||
</StyledDetail>
|
||||
</FlexRow>
|
||||
}
|
||||
@ -182,13 +197,7 @@ export const DependencyRow: FC<{ feature: IFeatureToggle }> = ({ feature }) => {
|
||||
<AddDependencyDialogue
|
||||
project={feature.project}
|
||||
featureId={feature.name}
|
||||
parentFeatureId={feature.dependencies[0]?.feature}
|
||||
parentFeatureValue={{
|
||||
status:
|
||||
feature.dependencies[0]?.enabled === false
|
||||
? 'disabled'
|
||||
: 'enabled',
|
||||
}}
|
||||
parentDependency={feature.dependencies[0]}
|
||||
onClose={() => setShowDependencyDialogue(false)}
|
||||
showDependencyDialogue={showDependencyDialogue}
|
||||
/>
|
||||
|
@ -246,7 +246,6 @@ test('delete dependency with change request', async () => {
|
||||
});
|
||||
|
||||
test('edit dependency', async () => {
|
||||
setupChangeRequestApi();
|
||||
render(
|
||||
<FeatureOverviewSidePanelDetails
|
||||
feature={
|
||||
@ -281,3 +280,27 @@ test('edit dependency', async () => {
|
||||
|
||||
await screen.findByText('Add parent feature dependency');
|
||||
});
|
||||
|
||||
test('show variant dependencies', async () => {
|
||||
render(
|
||||
<FeatureOverviewSidePanelDetails
|
||||
feature={
|
||||
{
|
||||
name: 'feature',
|
||||
project: 'default',
|
||||
dependencies: [
|
||||
{
|
||||
feature: 'some_parent',
|
||||
enabled: true,
|
||||
variants: ['a', 'b'],
|
||||
},
|
||||
],
|
||||
children: [] as string[],
|
||||
} as IFeatureToggle
|
||||
}
|
||||
header={''}
|
||||
/>,
|
||||
);
|
||||
|
||||
await screen.findByText('2 variants');
|
||||
});
|
||||
|
@ -0,0 +1,25 @@
|
||||
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
|
||||
import type { FC } from 'react';
|
||||
|
||||
export const VariantsTooltip: FC<{
|
||||
variants: string[];
|
||||
}> = ({ variants }) => {
|
||||
if (variants.length === 1 && variants[0].length < 20) {
|
||||
return <span>{variants[0]}</span>;
|
||||
}
|
||||
return (
|
||||
<TooltipLink
|
||||
tooltip={
|
||||
<>
|
||||
{variants.map((child) => (
|
||||
<div>{child}</div>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{variants.length === 1
|
||||
? '1 variant'
|
||||
: `${variants.length} variants`}
|
||||
</TooltipLink>
|
||||
);
|
||||
};
|
@ -0,0 +1,29 @@
|
||||
import { formatApiPath } from 'utils/formatPath';
|
||||
import { fetcher, useApiGetter } from '../useApiGetter/useApiGetter';
|
||||
|
||||
const parentOptionsPath = (projectId: string, childFeatureId: string) =>
|
||||
`/api/admin/projects/${projectId}/features/${childFeatureId}/parents`;
|
||||
|
||||
export const useParentOptions = (projectId: string, childFeatureId: string) => {
|
||||
const path = formatApiPath(parentOptionsPath(projectId, childFeatureId));
|
||||
const { data, refetch, loading, error } = useApiGetter<string[]>(path, () =>
|
||||
fetcher(path, 'Parent Options'),
|
||||
);
|
||||
|
||||
return { parentOptions: data, loading, error };
|
||||
};
|
||||
|
||||
const parentVariantsPath = (projectId: string, parentFeatureId: string) =>
|
||||
`/api/admin/projects/${projectId}/features/${parentFeatureId}/parent-variants`;
|
||||
|
||||
export const useParentVariantOptions = (
|
||||
projectId: string,
|
||||
parentFeatureId: string,
|
||||
) => {
|
||||
const path = formatApiPath(parentVariantsPath(projectId, parentFeatureId));
|
||||
const { data, refetch, loading, error } = useApiGetter<string[]>(path, () =>
|
||||
fetcher(path, 'Parent Variant Options'),
|
||||
);
|
||||
|
||||
return { parentVariantOptions: data || [], loading, error };
|
||||
};
|
@ -1,26 +0,0 @@
|
||||
import useSWR, { type SWRConfiguration } from 'swr';
|
||||
import { formatApiPath } from 'utils/formatPath';
|
||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||
|
||||
export const useParentOptions = (
|
||||
project: string,
|
||||
childFeatureId: string,
|
||||
options: SWRConfiguration = {},
|
||||
) => {
|
||||
const path = formatApiPath(
|
||||
`/api/admin/projects/${project}/features/${childFeatureId}/parents`,
|
||||
);
|
||||
const { data, error, mutate } = useSWR(path, fetcher, options);
|
||||
|
||||
return {
|
||||
parentOptions: data,
|
||||
error,
|
||||
loading: !error && !data,
|
||||
};
|
||||
};
|
||||
|
||||
const fetcher = async (path: string): Promise<string[]> => {
|
||||
const res = await fetch(path).then(handleErrorResponses('Parent Options'));
|
||||
const data = await res.json();
|
||||
return data;
|
||||
};
|
@ -54,7 +54,8 @@ export interface IFeatureToggle {
|
||||
|
||||
export interface IDependency {
|
||||
feature: string;
|
||||
enabled: boolean;
|
||||
enabled?: boolean;
|
||||
variants?: string[];
|
||||
}
|
||||
|
||||
export interface IFeatureEnvironment {
|
||||
|
Loading…
Reference in New Issue
Block a user