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

chore: redesign action configurations (#6588)

https://linear.app/unleash/issue/2-2050/redesign-action-definitions-oss

Depends on a PR on Enterprise that will provide a new endpoint with
action configurations.

We should also clean up the now unnecessary constants and hook.
This commit is contained in:
Nuno Góis 2024-03-18 11:09:49 +00:00 committed by GitHub
parent f45dbc647e
commit a86b8d36b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 131 additions and 165 deletions

View File

@ -6,8 +6,8 @@ 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 { ProjectActionsActionParameterAutocomplete } from './ProjectActionsActionParameter/ProjectActionsActionParameterAutocomplete';
import { ActionDefinitions } from './useActionDefinitions';
import { ProjectActionsActionParameter } from './ProjectActionsActionParameter/ProjectActionsActionParameter';
import { ActionConfigurations } from 'interfaces/action';
import { ProjectActionsActionSelect } from './ProjectActionsActionSelect';
const StyledItemBody = styled('div')(({ theme }) => ({
@ -36,7 +36,7 @@ interface IProjectActionsItemProps {
stateChanged: (action: ActionsActionState) => void;
actorId: number;
onDelete: () => void;
actionDefinitions: ActionDefinitions;
actionConfigurations: ActionConfigurations;
validated: boolean;
}
@ -46,7 +46,7 @@ export const ProjectActionsActionItem = ({
stateChanged,
actorId,
onDelete,
actionDefinitions,
actionConfigurations,
validated,
}: IProjectActionsItemProps) => {
const { action: actionName, executionParams, error } = action;
@ -57,10 +57,10 @@ export const ProjectActionsActionItem = ({
executionParams.environment as string,
);
const actionDefinition = actionDefinitions.get(actionName);
const actionConfiguration = actionConfigurations.get(actionName);
const hasPermission = useMemo(() => {
const requiredPermissions = actionDefinition?.permissions;
const requiredPermissions = actionConfiguration?.permissions;
const { environment: actionEnvironment } = executionParams;
@ -80,7 +80,7 @@ export const ProjectActionsActionItem = ({
environment === actionEnvironment,
),
);
}, [actionDefinition, permissions]);
}, [actionConfiguration, permissions]);
useEffect(() => {
stateChanged({
@ -89,7 +89,7 @@ export const ProjectActionsActionItem = ({
});
const requiredParameters =
actionDefinition?.parameters
actionConfiguration?.parameters
.filter(({ optional }) => !optional)
.map(({ name }) => name) || [];
@ -99,7 +99,7 @@ export const ProjectActionsActionItem = ({
error: 'Please fill all required fields.',
});
}
}, [actionDefinition, executionParams]);
}, [actionConfiguration, executionParams]);
const header = (
<>
@ -115,7 +115,9 @@ export const ProjectActionsActionItem = ({
);
const parameters =
actionDefinition?.parameters.filter(({ hidden }) => !hidden) || [];
actionConfiguration?.parameters.filter(
({ type }) => type !== 'hidden',
) || [];
return (
<ProjectActionsFormItem index={index} header={header} separator='THEN'>
@ -130,7 +132,7 @@ export const ProjectActionsActionItem = ({
action: value,
})
}
actionDefinitions={actionDefinitions}
actionConfigurations={actionConfigurations}
/>
</StyledFieldContainer>
</StyledItemRow>
@ -138,21 +140,24 @@ export const ProjectActionsActionItem = ({
condition={parameters.length > 0}
show={
<StyledItemRow>
{parameters.map(({ name, label, options }) => (
<StyledFieldContainer key={name}>
<ProjectActionsActionParameterAutocomplete
label={label}
value={executionParams[name] as string}
{parameters.map((parameter) => (
<StyledFieldContainer key={parameter.name}>
<ProjectActionsActionParameter
parameter={parameter}
value={
executionParams[
parameter.name
] as string
}
onChange={(value) =>
stateChanged({
...action,
executionParams: {
...executionParams,
[name]: value,
[parameter.name]: value,
},
})
}
options={options}
/>
</StyledFieldContainer>
))}

View File

@ -0,0 +1,31 @@
import { ActionConfigurationParameter } from 'interfaces/action';
import { ProjectActionsActionParameterAutocomplete } from './ProjectActionsActionParameterAutocomplete';
interface IProjectActionsActionParameterProps {
parameter: ActionConfigurationParameter;
value: string;
onChange: (value: string) => void;
}
export const ProjectActionsActionParameter = ({
parameter,
value,
onChange,
}: IProjectActionsActionParameterProps) => {
const { label, type } = parameter;
if (type === 'select') {
const { options } = parameter;
return (
<ProjectActionsActionParameterAutocomplete
label={label}
value={value}
onChange={onChange}
options={options}
/>
);
}
return null;
};

View File

@ -1,5 +1,5 @@
import { Autocomplete, TextField, styled } from '@mui/material';
import { ActionDefinitions } from './useActionDefinitions';
import { ActionConfigurations } from 'interfaces/action';
const StyledActionOption = styled('div')(({ theme }) => ({
display: 'flex',
@ -13,13 +13,13 @@ const StyledActionOption = styled('div')(({ theme }) => ({
interface IProjectActionsActionSelectProps {
value: string;
onChange: (value: string) => void;
actionDefinitions: ActionDefinitions;
actionConfigurations: ActionConfigurations;
}
export const ProjectActionsActionSelect = ({
value,
onChange,
actionDefinitions,
actionConfigurations,
}: IProjectActionsActionSelectProps) => {
const renderActionOption = (
props: React.HTMLAttributes<HTMLLIElement>,
@ -33,7 +33,7 @@ export const ProjectActionsActionSelect = ({
</li>
);
const actionOptions = [...actionDefinitions].map(
const actionOptions = [...actionConfigurations].map(
([key, actionDefinition]) => ({
key,
...actionDefinition,

View File

@ -9,7 +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 { useActionDefinitions } from './useActionDefinitions';
import { useActionConfigurations } from 'hooks/api/getters/useActionConfigurations/useActionConfigurations';
const StyledDivider = styled(Divider)(({ theme }) => ({
margin: theme.spacing(2, 0),
@ -45,7 +45,7 @@ export const ProjectActionsFormStepActions = ({
validated,
}: IProjectActionsFormStepActionsProps) => {
const projectId = useRequiredPathParam('projectId');
const actionDefinitions = useActionDefinitions(projectId);
const { actionConfigurations } = useActionConfigurations(projectId);
const addAction = (projectId: string) => {
const id = uuidv4();
@ -114,7 +114,7 @@ export const ProjectActionsFormStepActions = ({
actions.filter((a) => a.id !== action.id),
)
}
actionDefinitions={actionDefinitions}
actionConfigurations={actionConfigurations}
validated={validated}
/>
))}

View File

@ -1,67 +0,0 @@
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,41 @@
import { useMemo } from 'react';
import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler';
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
import useUiConfig from '../useUiConfig/useUiConfig';
import { ActionConfiguration } from 'interfaces/action';
import { useUiFlag } from 'hooks/useUiFlag';
const DEFAULT_DATA: Record<string, ActionConfiguration> = {};
export const useActionConfigurations = (project: string) => {
const { isEnterprise } = useUiConfig();
const actionsEnabled = useUiFlag('automatedActions');
const { data, error, mutate } = useConditionalSWR<
Record<string, ActionConfiguration>
>(
isEnterprise() && actionsEnabled,
DEFAULT_DATA,
formatApiPath(`api/admin/projects/${project}/actions/config`),
fetcher,
);
return useMemo(
() => ({
actionConfigurations: new Map<string, ActionConfiguration>(
Object.entries(data || {}),
),
loading: !error && !data,
refetch: () => mutate(),
error,
}),
[data, error, mutate],
);
};
const fetcher = (path: string) => {
return fetch(path)
.then(handleErrorResponses('Actions configuration'))
.then((res) => res.json());
};

View File

@ -56,3 +56,32 @@ export interface IActionSetEvent {
signal: ISignal;
actionSet: IActionSetEventActionSet;
}
type BaseParameter = {
name: string;
label: string;
optional?: boolean;
};
type ActionConfigurationHiddenParameter = BaseParameter & {
type: 'hidden';
};
type ActionConfigurationSelectParameter = BaseParameter & {
type: 'select';
options: string[];
};
export type ActionConfigurationParameter =
| ActionConfigurationHiddenParameter
| ActionConfigurationSelectParameter;
export type ActionConfiguration = {
label: string;
description?: string;
category?: string;
permissions: string[];
parameters: ActionConfigurationParameter[];
};
export type ActionConfigurations = Map<string, ActionConfiguration>;

View File

@ -1,37 +0,0 @@
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,36 +0,0 @@
import {
ActionDefinitionParameter,
toggleFeatureParameters,
} from './action-parameters';
export type ActionDefinition = {
label: string;
description?: string;
category?: string;
permissions: string[];
parameters: ActionDefinitionParameter[];
};
export const ACTIONS = new Map<string, ActionDefinition>([
[
'TOGGLE_FEATURE_ON',
{
label: 'Enable feature toggle',
description: 'Enables a feature toggle for a specific environment.',
category: 'Feature toggles',
permissions: ['UPDATE_FEATURE_ENVIRONMENT'],
parameters: toggleFeatureParameters,
},
],
[
'TOGGLE_FEATURE_OFF',
{
label: 'Disable feature toggle',
description:
'Disables a feature toggle for a specific environment.',
category: 'Feature toggles',
permissions: ['UPDATE_FEATURE_ENVIRONMENT'],
parameters: toggleFeatureParameters,
},
],
]);