1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-20 00:08:02 +01:00

chore: dynamic action parameters (#6554)

https://linear.app/unleash/issue/2-2046/implement-dynamic-action-parameters

Implements dynamic action parameters.

Also improves the action dropdown to better prepare for future actions.

<img width="771" alt="image"
src="https://github.com/Unleash/unleash/assets/14320932/ec3fcaf2-40c8-4dc8-8834-7a0d54671fd2">
This commit is contained in:
Nuno Góis 2024-03-14 15:25:23 +00:00 committed by GitHub
parent 05c014cde7
commit 7a3d2d6d87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 224 additions and 66 deletions

View File

@ -1,5 +1,4 @@
import { Alert, IconButton, Tooltip, styled } from '@mui/material';
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import Delete from '@mui/icons-material/Delete';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { ActionsActionState } from '../../useProjectActionsForm';
@ -7,8 +6,9 @@ import { ProjectActionsFormItem } from '../ProjectActionsFormItem';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useServiceAccountAccessMatrix } from 'hooks/api/getters/useServiceAccountAccessMatrix/useServiceAccountAccessMatrix';
import { useEffect, useMemo } from 'react';
import { ACTIONS } from '@server/util/constants/actions';
import { ProjectActionsActionParameterAutocomplete } from './ProjectActionsActionParameter/ProjectActionsActionParameterAutocomplete';
import { ActionDefinitions } from './useActionDefinitions';
import { ProjectActionsActionSelect } from './ProjectActionsActionSelect';
const StyledItemBody = styled('div')(({ theme }) => ({
display: 'flex',
@ -22,11 +22,13 @@ const StyledItemRow = styled('div')(({ theme }) => ({
alignItems: 'center',
gap: theme.spacing(1),
width: '100%',
flexWrap: 'wrap',
}));
const StyledFieldContainer = styled('div')({
const StyledFieldContainer = styled('div')(({ theme }) => ({
flex: 1,
});
minWidth: theme.spacing(25),
}));
interface IProjectActionsItemProps {
action: ActionsActionState;
@ -34,8 +36,7 @@ interface IProjectActionsItemProps {
stateChanged: (action: ActionsActionState) => void;
actorId: number;
onDelete: () => void;
featureToggles: string[];
environments: string[];
actionDefinitions: ActionDefinitions;
validated: boolean;
}
@ -45,8 +46,7 @@ export const ProjectActionsActionItem = ({
stateChanged,
actorId,
onDelete,
featureToggles,
environments,
actionDefinitions,
validated,
}: IProjectActionsItemProps) => {
const { action: actionName, executionParams, error } = action;
@ -57,7 +57,7 @@ export const ProjectActionsActionItem = ({
executionParams.environment as string,
);
const actionDefinition = ACTIONS.get(actionName);
const actionDefinition = actionDefinitions.get(actionName);
const hasPermission = useMemo(() => {
const requiredPermissions = actionDefinition?.permissions;
@ -112,61 +112,51 @@ export const ProjectActionsActionItem = ({
</>
);
const parameters =
actionDefinition?.parameters.filter(({ hidden }) => !hidden) || [];
return (
<ProjectActionsFormItem index={index} header={header} separator='THEN'>
<StyledItemBody>
<StyledItemRow>
<StyledFieldContainer>
<GeneralSelect
label='Action'
name='action'
options={[...ACTIONS].map(([key, { label }]) => ({
key,
label,
}))}
<ProjectActionsActionSelect
value={actionName}
onChange={(selected) =>
onChange={(value) =>
stateChanged({
...action,
action: selected,
action: value,
})
}
fullWidth
/>
</StyledFieldContainer>
<StyledFieldContainer>
<ProjectActionsActionParameterAutocomplete
label='Environment'
value={executionParams.environment as string}
onChange={(selected) =>
stateChanged({
...action,
executionParams: {
...executionParams,
environment: selected,
},
})
}
options={environments}
/>
</StyledFieldContainer>
<StyledFieldContainer>
<ProjectActionsActionParameterAutocomplete
label='Flag name'
value={executionParams.featureName as string}
onChange={(selected) =>
stateChanged({
...action,
executionParams: {
...executionParams,
featureName: selected,
},
})
}
options={featureToggles}
actionDefinitions={actionDefinitions}
/>
</StyledFieldContainer>
</StyledItemRow>
<ConditionallyRender
condition={parameters.length > 0}
show={
<StyledItemRow>
{parameters.map(({ name, label, options }) => (
<StyledFieldContainer key={name}>
<ProjectActionsActionParameterAutocomplete
label={label}
value={executionParams[name] as string}
onChange={(value) =>
stateChanged({
...action,
executionParams: {
...executionParams,
[name]: value,
},
})
}
options={options}
/>
</StyledFieldContainer>
))}
</StyledItemRow>
}
/>
<ConditionallyRender
condition={validated && Boolean(error)}
show={<Alert severity='error'>{error}</Alert>}

View File

@ -0,0 +1,57 @@
import { Autocomplete, TextField, styled } from '@mui/material';
import { ActionDefinitions } from './useActionDefinitions';
const StyledActionOption = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
'& > span:last-of-type': {
fontSize: theme.fontSizes.smallerBody,
color: theme.palette.text.secondary,
},
}));
interface IProjectActionsActionSelectProps {
value: string;
onChange: (value: string) => void;
actionDefinitions: ActionDefinitions;
}
export const ProjectActionsActionSelect = ({
value,
onChange,
actionDefinitions,
}: IProjectActionsActionSelectProps) => {
const renderActionOption = (
props: React.HTMLAttributes<HTMLLIElement>,
option: { label: string; description?: string },
) => (
<li {...props}>
<StyledActionOption>
<span>{option.label}</span>
<span>{option.description}</span>
</StyledActionOption>
</li>
);
const actionOptions = [...actionDefinitions].map(
([key, actionDefinition]) => ({
key,
...actionDefinition,
}),
);
return (
<Autocomplete
options={actionOptions}
autoHighlight
autoSelect
value={actionOptions.find(({ key }) => key === value)}
onChange={(_, value) => onChange(value ? value.key : '')}
renderOption={renderActionOption}
getOptionLabel={({ label }) => label}
renderInput={(params) => (
<TextField {...params} size='small' label='Action' />
)}
/>
);
};

View File

@ -9,8 +9,7 @@ import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import Add from '@mui/icons-material/Add';
import { IServiceAccount } from 'interfaces/service-account';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
import { useFeatureSearch } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
import { useActionDefinitions } from './useActionDefinitions';
const StyledDivider = styled(Divider)(({ theme }) => ({
margin: theme.spacing(2, 0),
@ -46,14 +45,7 @@ export const ProjectActionsFormStepActions = ({
validated,
}: IProjectActionsFormStepActionsProps) => {
const projectId = useRequiredPathParam('projectId');
const { project } = useProjectOverview(projectId);
const { features } = useFeatureSearch({ project: `IS:${projectId}` });
const featureToggles = features.map(({ name }) => name).sort();
const environments = project.environments.map(
({ environment }) => environment,
);
const actionDefinitions = useActionDefinitions(projectId);
const addAction = (projectId: string) => {
const id = uuidv4();
@ -122,8 +114,7 @@ export const ProjectActionsFormStepActions = ({
actions.filter((a) => a.id !== action.id),
)
}
featureToggles={featureToggles}
environments={environments}
actionDefinitions={actionDefinitions}
validated={validated}
/>
))}

View File

@ -0,0 +1,67 @@
import { useEffect, useState } from 'react';
import { ActionDefinitionParameter } from '@server/util/constants/action-parameters';
import { ACTIONS, ActionDefinition } from '@server/util/constants/actions';
import { useFeatureSearch } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
type ActionDefinitionParameterWithOption = ActionDefinitionParameter & {
options: string[];
};
type ActionDefinitionWithParameterOptions = Omit<
ActionDefinition,
'parameters'
> & {
parameters: ActionDefinitionParameterWithOption[];
};
export type ActionDefinitions = Map<
string,
ActionDefinitionWithParameterOptions
>;
export const useActionDefinitions = (projectId: string): ActionDefinitions => {
const { project, loading: isProjectLoading } =
useProjectOverview(projectId);
const { features, loading: isFeaturesLoading } = useFeatureSearch({
project: `IS:${projectId}`,
});
const [actionDefinitions, setActionDefinitions] =
useState<ActionDefinitions>(new Map());
useEffect(() => {
if (isProjectLoading || isFeaturesLoading) return;
const optionsByType: Record<
ActionDefinitionParameter['type'],
string[]
> = {
project: [],
environment: project.environments.map(
({ environment }) => environment,
),
featureToggle: features.map(({ name }) => name).sort(),
};
const actionDefinitionsWithParameterOptions = new Map<
string,
ActionDefinitionWithParameterOptions
>(
[...ACTIONS].map(([key, action]) => [
key,
{
...action,
parameters: action.parameters.map((parameter) => ({
...parameter,
options: optionsByType[parameter.type],
})),
},
]),
);
setActionDefinitions(actionDefinitionsWithParameterOptions);
}, [projectId, project, features, isProjectLoading, isFeaturesLoading]);
return actionDefinitions;
};

View File

@ -0,0 +1,37 @@
type ActionDefinitionParameterType =
| 'project'
| 'featureToggle'
| 'environment';
export type ActionDefinitionParameter = {
name: string;
label: string;
type: ActionDefinitionParameterType;
hidden?: boolean;
optional?: boolean;
};
const projectParameter: ActionDefinitionParameter = {
name: 'project',
label: 'Project',
type: 'project',
hidden: true,
};
const environmentParameter: ActionDefinitionParameter = {
name: 'environment',
label: 'Environment',
type: 'environment',
};
const featureToggleParameter: ActionDefinitionParameter = {
name: 'featureName',
label: 'Feature toggle',
type: 'featureToggle',
};
export const toggleFeatureParameters = [
projectParameter,
environmentParameter,
featureToggleParameter,
];

View File

@ -1,6 +1,15 @@
type ActionDefinition = {
import {
ActionDefinitionParameter,
toggleFeatureParameters,
} from './action-parameters';
export type ActionDefinition = {
label: string;
description?: string;
category?: string;
permissions: string[];
parameters: ActionDefinitionParameter[];
// TODO: Remove this in favor of parameters (filter by !optional)
required: string[];
};
@ -8,16 +17,23 @@ export const ACTIONS = new Map<string, ActionDefinition>([
[
'TOGGLE_FEATURE_ON',
{
label: 'Enable flag',
label: 'Enable feature toggle',
description: 'Enables a feature toggle for a specific environment.',
category: 'Feature toggles',
permissions: ['UPDATE_FEATURE_ENVIRONMENT'],
parameters: toggleFeatureParameters,
required: ['project', 'environment', 'featureName'],
},
],
[
'TOGGLE_FEATURE_OFF',
{
label: 'Disable flag',
label: 'Disable feature toggle',
description:
'Disables a feature toggle for a specific environment.',
category: 'Feature toggles',
permissions: ['UPDATE_FEATURE_ENVIRONMENT'],
parameters: toggleFeatureParameters,
required: ['project', 'environment', 'featureName'],
},
],