diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepActions/ProjectActionsActionItem.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepActions/ProjectActionsActionItem.tsx index cf5a06d11d..5b0573d820 100644 --- a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepActions/ProjectActionsActionItem.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepActions/ProjectActionsActionItem.tsx @@ -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 ( @@ -130,7 +132,7 @@ export const ProjectActionsActionItem = ({ action: value, }) } - actionDefinitions={actionDefinitions} + actionConfigurations={actionConfigurations} /> @@ -138,21 +140,24 @@ export const ProjectActionsActionItem = ({ condition={parameters.length > 0} show={ - {parameters.map(({ name, label, options }) => ( - - ( + + stateChanged({ ...action, executionParams: { ...executionParams, - [name]: value, + [parameter.name]: value, }, }) } - options={options} /> ))} diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepActions/ProjectActionsActionParameter/ProjectActionsActionParameter.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepActions/ProjectActionsActionParameter/ProjectActionsActionParameter.tsx new file mode 100644 index 0000000000..7992dca3ab --- /dev/null +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepActions/ProjectActionsActionParameter/ProjectActionsActionParameter.tsx @@ -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 ( + + ); + } + + return null; +}; diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepActions/ProjectActionsActionSelect.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepActions/ProjectActionsActionSelect.tsx index 8a9619ae5c..11cc8025df 100644 --- a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepActions/ProjectActionsActionSelect.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepActions/ProjectActionsActionSelect.tsx @@ -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, @@ -33,7 +33,7 @@ export const ProjectActionsActionSelect = ({ ); - const actionOptions = [...actionDefinitions].map( + const actionOptions = [...actionConfigurations].map( ([key, actionDefinition]) => ({ key, ...actionDefinition, diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepActions/ProjectActionsFormStepActions.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepActions/ProjectActionsFormStepActions.tsx index ad096a1ffb..15c7b196f9 100644 --- a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepActions/ProjectActionsFormStepActions.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepActions/ProjectActionsFormStepActions.tsx @@ -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} /> ))} diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepActions/useActionDefinitions.ts b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepActions/useActionDefinitions.ts deleted file mode 100644 index 6c3352e523..0000000000 --- a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsFormStep/ProjectActionsFormStepActions/useActionDefinitions.ts +++ /dev/null @@ -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(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; -}; diff --git a/frontend/src/hooks/api/getters/useActionConfigurations/useActionConfigurations.ts b/frontend/src/hooks/api/getters/useActionConfigurations/useActionConfigurations.ts new file mode 100644 index 0000000000..88f6ac4767 --- /dev/null +++ b/frontend/src/hooks/api/getters/useActionConfigurations/useActionConfigurations.ts @@ -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 = {}; + +export const useActionConfigurations = (project: string) => { + const { isEnterprise } = useUiConfig(); + const actionsEnabled = useUiFlag('automatedActions'); + + const { data, error, mutate } = useConditionalSWR< + Record + >( + isEnterprise() && actionsEnabled, + DEFAULT_DATA, + formatApiPath(`api/admin/projects/${project}/actions/config`), + fetcher, + ); + + return useMemo( + () => ({ + actionConfigurations: new Map( + 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()); +}; diff --git a/frontend/src/interfaces/action.ts b/frontend/src/interfaces/action.ts index b7be670946..ab0a00f671 100644 --- a/frontend/src/interfaces/action.ts +++ b/frontend/src/interfaces/action.ts @@ -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; diff --git a/src/lib/util/constants/action-parameters.ts b/src/lib/util/constants/action-parameters.ts deleted file mode 100644 index 5004f4dfcf..0000000000 --- a/src/lib/util/constants/action-parameters.ts +++ /dev/null @@ -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, -]; diff --git a/src/lib/util/constants/actions.ts b/src/lib/util/constants/actions.ts deleted file mode 100644 index 71f6b4cc8f..0000000000 --- a/src/lib/util/constants/actions.ts +++ /dev/null @@ -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([ - [ - '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, - }, - ], -]);