mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-14 01:16:17 +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:
parent
f45dbc647e
commit
a86b8d36b3
@ -6,8 +6,8 @@ import { ProjectActionsFormItem } from '../ProjectActionsFormItem';
|
|||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { useServiceAccountAccessMatrix } from 'hooks/api/getters/useServiceAccountAccessMatrix/useServiceAccountAccessMatrix';
|
import { useServiceAccountAccessMatrix } from 'hooks/api/getters/useServiceAccountAccessMatrix/useServiceAccountAccessMatrix';
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { ProjectActionsActionParameterAutocomplete } from './ProjectActionsActionParameter/ProjectActionsActionParameterAutocomplete';
|
import { ProjectActionsActionParameter } from './ProjectActionsActionParameter/ProjectActionsActionParameter';
|
||||||
import { ActionDefinitions } from './useActionDefinitions';
|
import { ActionConfigurations } from 'interfaces/action';
|
||||||
import { ProjectActionsActionSelect } from './ProjectActionsActionSelect';
|
import { ProjectActionsActionSelect } from './ProjectActionsActionSelect';
|
||||||
|
|
||||||
const StyledItemBody = styled('div')(({ theme }) => ({
|
const StyledItemBody = styled('div')(({ theme }) => ({
|
||||||
@ -36,7 +36,7 @@ interface IProjectActionsItemProps {
|
|||||||
stateChanged: (action: ActionsActionState) => void;
|
stateChanged: (action: ActionsActionState) => void;
|
||||||
actorId: number;
|
actorId: number;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
actionDefinitions: ActionDefinitions;
|
actionConfigurations: ActionConfigurations;
|
||||||
validated: boolean;
|
validated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ export const ProjectActionsActionItem = ({
|
|||||||
stateChanged,
|
stateChanged,
|
||||||
actorId,
|
actorId,
|
||||||
onDelete,
|
onDelete,
|
||||||
actionDefinitions,
|
actionConfigurations,
|
||||||
validated,
|
validated,
|
||||||
}: IProjectActionsItemProps) => {
|
}: IProjectActionsItemProps) => {
|
||||||
const { action: actionName, executionParams, error } = action;
|
const { action: actionName, executionParams, error } = action;
|
||||||
@ -57,10 +57,10 @@ export const ProjectActionsActionItem = ({
|
|||||||
executionParams.environment as string,
|
executionParams.environment as string,
|
||||||
);
|
);
|
||||||
|
|
||||||
const actionDefinition = actionDefinitions.get(actionName);
|
const actionConfiguration = actionConfigurations.get(actionName);
|
||||||
|
|
||||||
const hasPermission = useMemo(() => {
|
const hasPermission = useMemo(() => {
|
||||||
const requiredPermissions = actionDefinition?.permissions;
|
const requiredPermissions = actionConfiguration?.permissions;
|
||||||
|
|
||||||
const { environment: actionEnvironment } = executionParams;
|
const { environment: actionEnvironment } = executionParams;
|
||||||
|
|
||||||
@ -80,7 +80,7 @@ export const ProjectActionsActionItem = ({
|
|||||||
environment === actionEnvironment,
|
environment === actionEnvironment,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}, [actionDefinition, permissions]);
|
}, [actionConfiguration, permissions]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
stateChanged({
|
stateChanged({
|
||||||
@ -89,7 +89,7 @@ export const ProjectActionsActionItem = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const requiredParameters =
|
const requiredParameters =
|
||||||
actionDefinition?.parameters
|
actionConfiguration?.parameters
|
||||||
.filter(({ optional }) => !optional)
|
.filter(({ optional }) => !optional)
|
||||||
.map(({ name }) => name) || [];
|
.map(({ name }) => name) || [];
|
||||||
|
|
||||||
@ -99,7 +99,7 @@ export const ProjectActionsActionItem = ({
|
|||||||
error: 'Please fill all required fields.',
|
error: 'Please fill all required fields.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [actionDefinition, executionParams]);
|
}, [actionConfiguration, executionParams]);
|
||||||
|
|
||||||
const header = (
|
const header = (
|
||||||
<>
|
<>
|
||||||
@ -115,7 +115,9 @@ export const ProjectActionsActionItem = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const parameters =
|
const parameters =
|
||||||
actionDefinition?.parameters.filter(({ hidden }) => !hidden) || [];
|
actionConfiguration?.parameters.filter(
|
||||||
|
({ type }) => type !== 'hidden',
|
||||||
|
) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProjectActionsFormItem index={index} header={header} separator='THEN'>
|
<ProjectActionsFormItem index={index} header={header} separator='THEN'>
|
||||||
@ -130,7 +132,7 @@ export const ProjectActionsActionItem = ({
|
|||||||
action: value,
|
action: value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
actionDefinitions={actionDefinitions}
|
actionConfigurations={actionConfigurations}
|
||||||
/>
|
/>
|
||||||
</StyledFieldContainer>
|
</StyledFieldContainer>
|
||||||
</StyledItemRow>
|
</StyledItemRow>
|
||||||
@ -138,21 +140,24 @@ export const ProjectActionsActionItem = ({
|
|||||||
condition={parameters.length > 0}
|
condition={parameters.length > 0}
|
||||||
show={
|
show={
|
||||||
<StyledItemRow>
|
<StyledItemRow>
|
||||||
{parameters.map(({ name, label, options }) => (
|
{parameters.map((parameter) => (
|
||||||
<StyledFieldContainer key={name}>
|
<StyledFieldContainer key={parameter.name}>
|
||||||
<ProjectActionsActionParameterAutocomplete
|
<ProjectActionsActionParameter
|
||||||
label={label}
|
parameter={parameter}
|
||||||
value={executionParams[name] as string}
|
value={
|
||||||
|
executionParams[
|
||||||
|
parameter.name
|
||||||
|
] as string
|
||||||
|
}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
stateChanged({
|
stateChanged({
|
||||||
...action,
|
...action,
|
||||||
executionParams: {
|
executionParams: {
|
||||||
...executionParams,
|
...executionParams,
|
||||||
[name]: value,
|
[parameter.name]: value,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
options={options}
|
|
||||||
/>
|
/>
|
||||||
</StyledFieldContainer>
|
</StyledFieldContainer>
|
||||||
))}
|
))}
|
||||||
|
@ -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;
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
import { Autocomplete, TextField, styled } from '@mui/material';
|
import { Autocomplete, TextField, styled } from '@mui/material';
|
||||||
import { ActionDefinitions } from './useActionDefinitions';
|
import { ActionConfigurations } from 'interfaces/action';
|
||||||
|
|
||||||
const StyledActionOption = styled('div')(({ theme }) => ({
|
const StyledActionOption = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -13,13 +13,13 @@ const StyledActionOption = styled('div')(({ theme }) => ({
|
|||||||
interface IProjectActionsActionSelectProps {
|
interface IProjectActionsActionSelectProps {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
actionDefinitions: ActionDefinitions;
|
actionConfigurations: ActionConfigurations;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProjectActionsActionSelect = ({
|
export const ProjectActionsActionSelect = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
actionDefinitions,
|
actionConfigurations,
|
||||||
}: IProjectActionsActionSelectProps) => {
|
}: IProjectActionsActionSelectProps) => {
|
||||||
const renderActionOption = (
|
const renderActionOption = (
|
||||||
props: React.HTMLAttributes<HTMLLIElement>,
|
props: React.HTMLAttributes<HTMLLIElement>,
|
||||||
@ -33,7 +33,7 @@ export const ProjectActionsActionSelect = ({
|
|||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|
||||||
const actionOptions = [...actionDefinitions].map(
|
const actionOptions = [...actionConfigurations].map(
|
||||||
([key, actionDefinition]) => ({
|
([key, actionDefinition]) => ({
|
||||||
key,
|
key,
|
||||||
...actionDefinition,
|
...actionDefinition,
|
||||||
|
@ -9,7 +9,7 @@ import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
|
|||||||
import Add from '@mui/icons-material/Add';
|
import Add from '@mui/icons-material/Add';
|
||||||
import { IServiceAccount } from 'interfaces/service-account';
|
import { IServiceAccount } from 'interfaces/service-account';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { useActionDefinitions } from './useActionDefinitions';
|
import { useActionConfigurations } from 'hooks/api/getters/useActionConfigurations/useActionConfigurations';
|
||||||
|
|
||||||
const StyledDivider = styled(Divider)(({ theme }) => ({
|
const StyledDivider = styled(Divider)(({ theme }) => ({
|
||||||
margin: theme.spacing(2, 0),
|
margin: theme.spacing(2, 0),
|
||||||
@ -45,7 +45,7 @@ export const ProjectActionsFormStepActions = ({
|
|||||||
validated,
|
validated,
|
||||||
}: IProjectActionsFormStepActionsProps) => {
|
}: IProjectActionsFormStepActionsProps) => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const actionDefinitions = useActionDefinitions(projectId);
|
const { actionConfigurations } = useActionConfigurations(projectId);
|
||||||
|
|
||||||
const addAction = (projectId: string) => {
|
const addAction = (projectId: string) => {
|
||||||
const id = uuidv4();
|
const id = uuidv4();
|
||||||
@ -114,7 +114,7 @@ export const ProjectActionsFormStepActions = ({
|
|||||||
actions.filter((a) => a.id !== action.id),
|
actions.filter((a) => a.id !== action.id),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
actionDefinitions={actionDefinitions}
|
actionConfigurations={actionConfigurations}
|
||||||
validated={validated}
|
validated={validated}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -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;
|
|
||||||
};
|
|
@ -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());
|
||||||
|
};
|
@ -56,3 +56,32 @@ export interface IActionSetEvent {
|
|||||||
signal: ISignal;
|
signal: ISignal;
|
||||||
actionSet: IActionSetEventActionSet;
|
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>;
|
||||||
|
@ -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,
|
|
||||||
];
|
|
@ -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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]);
|
|
Loading…
Reference in New Issue
Block a user