mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-31 13:47:02 +02:00
fix: add actions validation (#6481)
https://linear.app/unleash/issue/2-2022/improve-actions-validation Improves our current actions form validation. Empty actions are now ignored on the payload and we get errors in actions where any of the required fields are empty. Also refactored our current actions into a constant map that can be shared across frontend and backend.
This commit is contained in:
parent
62a7633ed5
commit
7d827442ee
@ -57,6 +57,8 @@ interface IProjectActionsFormProps {
|
|||||||
setActions: React.Dispatch<React.SetStateAction<ActionsActionState[]>>;
|
setActions: React.Dispatch<React.SetStateAction<ActionsActionState[]>>;
|
||||||
errors: ProjectActionsFormErrors;
|
errors: ProjectActionsFormErrors;
|
||||||
validateName: (name: string) => boolean;
|
validateName: (name: string) => boolean;
|
||||||
|
validateSourceId: (sourceId: number) => boolean;
|
||||||
|
validateActorId: (actorId: number) => boolean;
|
||||||
validated: boolean;
|
validated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,6 +79,8 @@ export const ProjectActionsForm = ({
|
|||||||
setActions,
|
setActions,
|
||||||
errors,
|
errors,
|
||||||
validateName,
|
validateName,
|
||||||
|
validateSourceId,
|
||||||
|
validateActorId,
|
||||||
validated,
|
validated,
|
||||||
}: IProjectActionsFormProps) => {
|
}: IProjectActionsFormProps) => {
|
||||||
const { serviceAccounts, loading: serviceAccountsLoading } =
|
const { serviceAccounts, loading: serviceAccountsLoading } =
|
||||||
@ -142,6 +146,7 @@ export const ProjectActionsForm = ({
|
|||||||
setSourceId={setSourceId}
|
setSourceId={setSourceId}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
setFilters={setFilters}
|
setFilters={setFilters}
|
||||||
|
validateSourceId={validateSourceId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ProjectActionsFormStepActions
|
<ProjectActionsFormStepActions
|
||||||
@ -151,6 +156,8 @@ export const ProjectActionsForm = ({
|
|||||||
setActions={setActions}
|
setActions={setActions}
|
||||||
actorId={actorId}
|
actorId={actorId}
|
||||||
setActorId={setActorId}
|
setActorId={setActorId}
|
||||||
|
validateActorId={validateActorId}
|
||||||
|
validated={validated}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
|
@ -8,7 +8,8 @@ import { ProjectActionsFormItem } from '../ProjectActionsFormItem';
|
|||||||
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
||||||
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 { useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
|
import { ACTIONS } from '@server/util/constants/actions';
|
||||||
|
|
||||||
const StyledItemBody = styled('div')(({ theme }) => ({
|
const StyledItemBody = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -28,25 +29,13 @@ const StyledFieldContainer = styled('div')({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const options = [
|
|
||||||
{
|
|
||||||
label: 'Enable flag',
|
|
||||||
key: 'TOGGLE_FEATURE_ON',
|
|
||||||
permissions: ['UPDATE_FEATURE_ENVIRONMENT'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Disable flag',
|
|
||||||
key: 'TOGGLE_FEATURE_OFF',
|
|
||||||
permissions: ['UPDATE_FEATURE_ENVIRONMENT'],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
interface IProjectActionsItemProps {
|
interface IProjectActionsItemProps {
|
||||||
action: ActionsActionState;
|
action: ActionsActionState;
|
||||||
index: number;
|
index: number;
|
||||||
stateChanged: (action: ActionsActionState) => void;
|
stateChanged: (action: ActionsActionState) => void;
|
||||||
actorId: number;
|
actorId: number;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
|
validated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProjectActionsActionItem = ({
|
export const ProjectActionsActionItem = ({
|
||||||
@ -55,22 +44,23 @@ export const ProjectActionsActionItem = ({
|
|||||||
stateChanged,
|
stateChanged,
|
||||||
actorId,
|
actorId,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
validated,
|
||||||
}: IProjectActionsItemProps) => {
|
}: IProjectActionsItemProps) => {
|
||||||
const { action: actionName } = action;
|
const { action: actionName, executionParams, error } = action;
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const { project } = useProjectOverview(projectId);
|
const { project } = useProjectOverview(projectId);
|
||||||
const { permissions } = useServiceAccountAccessMatrix(
|
const { permissions } = useServiceAccountAccessMatrix(
|
||||||
actorId,
|
actorId,
|
||||||
projectId,
|
projectId,
|
||||||
action.executionParams.environment as string,
|
executionParams.environment as string,
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasPermission = useMemo(() => {
|
const actionDefinition = ACTIONS.get(actionName);
|
||||||
const requiredPermissions = options.find(
|
|
||||||
({ key }) => key === actionName,
|
|
||||||
)?.permissions;
|
|
||||||
|
|
||||||
const { environment: actionEnvironment } = action.executionParams;
|
const hasPermission = useMemo(() => {
|
||||||
|
const requiredPermissions = actionDefinition?.permissions;
|
||||||
|
|
||||||
|
const { environment: actionEnvironment } = executionParams;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
permissions.length === 0 ||
|
permissions.length === 0 ||
|
||||||
@ -87,7 +77,24 @@ export const ProjectActionsActionItem = ({
|
|||||||
environment === actionEnvironment,
|
environment === actionEnvironment,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}, [actionName, permissions]);
|
}, [actionDefinition, permissions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
stateChanged({
|
||||||
|
...action,
|
||||||
|
error: undefined,
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
actionDefinition?.required.some(
|
||||||
|
(required) => !executionParams[required],
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
stateChanged({
|
||||||
|
...action,
|
||||||
|
error: 'Please fill all required fields.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [actionDefinition, executionParams]);
|
||||||
|
|
||||||
const environments = project.environments.map(
|
const environments = project.environments.map(
|
||||||
({ environment }) => environment,
|
({ environment }) => environment,
|
||||||
@ -116,7 +123,10 @@ export const ProjectActionsActionItem = ({
|
|||||||
<GeneralSelect
|
<GeneralSelect
|
||||||
label='Action'
|
label='Action'
|
||||||
name='action'
|
name='action'
|
||||||
options={options}
|
options={[...ACTIONS].map(([key, { label }]) => ({
|
||||||
|
key,
|
||||||
|
label,
|
||||||
|
}))}
|
||||||
value={actionName}
|
value={actionName}
|
||||||
onChange={(selected) =>
|
onChange={(selected) =>
|
||||||
stateChanged({
|
stateChanged({
|
||||||
@ -135,12 +145,12 @@ export const ProjectActionsActionItem = ({
|
|||||||
label: environment,
|
label: environment,
|
||||||
key: environment,
|
key: environment,
|
||||||
}))}
|
}))}
|
||||||
value={action.executionParams.environment as string}
|
value={executionParams.environment as string}
|
||||||
onChange={(selected) =>
|
onChange={(selected) =>
|
||||||
stateChanged({
|
stateChanged({
|
||||||
...action,
|
...action,
|
||||||
executionParams: {
|
executionParams: {
|
||||||
...action.executionParams,
|
...executionParams,
|
||||||
environment: selected,
|
environment: selected,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -156,12 +166,12 @@ export const ProjectActionsActionItem = ({
|
|||||||
label: feature.name,
|
label: feature.name,
|
||||||
key: feature.name,
|
key: feature.name,
|
||||||
}))}
|
}))}
|
||||||
value={action.executionParams.featureName as string}
|
value={executionParams.featureName as string}
|
||||||
onChange={(selected) =>
|
onChange={(selected) =>
|
||||||
stateChanged({
|
stateChanged({
|
||||||
...action,
|
...action,
|
||||||
executionParams: {
|
executionParams: {
|
||||||
...action.executionParams,
|
...executionParams,
|
||||||
featureName: selected,
|
featureName: selected,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -170,6 +180,10 @@ export const ProjectActionsActionItem = ({
|
|||||||
/>
|
/>
|
||||||
</StyledFieldContainer>
|
</StyledFieldContainer>
|
||||||
</StyledItemRow>
|
</StyledItemRow>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={validated && Boolean(error)}
|
||||||
|
show={<Alert severity='error'>{error}</Alert>}
|
||||||
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={!hasPermission}
|
condition={!hasPermission}
|
||||||
show={
|
show={
|
||||||
|
@ -29,6 +29,8 @@ interface IProjectActionsFormStepActionsProps {
|
|||||||
setActions: React.Dispatch<React.SetStateAction<ActionsActionState[]>>;
|
setActions: React.Dispatch<React.SetStateAction<ActionsActionState[]>>;
|
||||||
actorId: number;
|
actorId: number;
|
||||||
setActorId: React.Dispatch<React.SetStateAction<number>>;
|
setActorId: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
validateActorId: (actorId: number) => boolean;
|
||||||
|
validated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProjectActionsFormStepActions = ({
|
export const ProjectActionsFormStepActions = ({
|
||||||
@ -38,6 +40,8 @@ export const ProjectActionsFormStepActions = ({
|
|||||||
setActions,
|
setActions,
|
||||||
actorId,
|
actorId,
|
||||||
setActorId,
|
setActorId,
|
||||||
|
validateActorId,
|
||||||
|
validated,
|
||||||
}: IProjectActionsFormStepActionsProps) => {
|
}: IProjectActionsFormStepActionsProps) => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
|
|
||||||
@ -91,6 +95,7 @@ export const ProjectActionsFormStepActions = ({
|
|||||||
options={serviceAccountOptions}
|
options={serviceAccountOptions}
|
||||||
value={`${actorId}`}
|
value={`${actorId}`}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
|
validateActorId(Number(v));
|
||||||
setActorId(parseInt(v));
|
setActorId(parseInt(v));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -107,6 +112,7 @@ export const ProjectActionsFormStepActions = ({
|
|||||||
actions.filter((a) => a.id !== action.id),
|
actions.filter((a) => a.id !== action.id),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
validated={validated}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<StyledButtonContainer>
|
<StyledButtonContainer>
|
||||||
|
@ -37,6 +37,7 @@ interface IProjectActionsFormStepSourceProps {
|
|||||||
setSourceId: React.Dispatch<React.SetStateAction<number>>;
|
setSourceId: React.Dispatch<React.SetStateAction<number>>;
|
||||||
filters: ActionsFilterState[];
|
filters: ActionsFilterState[];
|
||||||
setFilters: React.Dispatch<React.SetStateAction<ActionsFilterState[]>>;
|
setFilters: React.Dispatch<React.SetStateAction<ActionsFilterState[]>>;
|
||||||
|
validateSourceId: (sourceId: number) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProjectActionsFormStepSource = ({
|
export const ProjectActionsFormStepSource = ({
|
||||||
@ -44,6 +45,7 @@ export const ProjectActionsFormStepSource = ({
|
|||||||
setSourceId,
|
setSourceId,
|
||||||
filters,
|
filters,
|
||||||
setFilters,
|
setFilters,
|
||||||
|
validateSourceId,
|
||||||
}: IProjectActionsFormStepSourceProps) => {
|
}: IProjectActionsFormStepSourceProps) => {
|
||||||
const { signalEndpoints, loading: signalEndpointsLoading } =
|
const { signalEndpoints, loading: signalEndpointsLoading } =
|
||||||
useSignalEndpoints();
|
useSignalEndpoints();
|
||||||
@ -102,6 +104,7 @@ export const ProjectActionsFormStepSource = ({
|
|||||||
options={signalEndpointOptions}
|
options={signalEndpointOptions}
|
||||||
value={`${sourceId}`}
|
value={`${sourceId}`}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
|
validateSourceId(Number(v));
|
||||||
setSourceId(parseInt(v));
|
setSourceId(parseInt(v));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -23,6 +23,7 @@ export type ActionsActionState = Omit<
|
|||||||
'id' | 'createdAt' | 'createdByUserId'
|
'id' | 'createdAt' | 'createdByUserId'
|
||||||
> & {
|
> & {
|
||||||
id: string;
|
id: string;
|
||||||
|
error?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_PROJECT_ACTIONS_FORM_ERRORS = {
|
const DEFAULT_PROJECT_ACTIONS_FORM_ERRORS = {
|
||||||
@ -99,12 +100,13 @@ export const useProjectActionsForm = (action?: IActionSet) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearError(ErrorField.FILTERS);
|
validateFilters(filters);
|
||||||
if (filters.some(({ error }) => error)) {
|
|
||||||
setError(ErrorField.FILTERS, 'One or more filters have errors.');
|
|
||||||
}
|
|
||||||
}, [filters]);
|
}, [filters]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
validateActions(actions);
|
||||||
|
}, [actions]);
|
||||||
|
|
||||||
const isEmpty = (value: string) => !value.length;
|
const isEmpty = (value: string) => !value.length;
|
||||||
|
|
||||||
const isNameNotUnique = (value: string) =>
|
const isNameNotUnique = (value: string) =>
|
||||||
@ -137,6 +139,16 @@ export const useProjectActionsForm = (action?: IActionSet) => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validateFilters = (filters: ActionsFilterState[]) => {
|
||||||
|
if (filters.some(({ error }) => error)) {
|
||||||
|
setError(ErrorField.FILTERS, 'One or more filters have errors.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearError(ErrorField.FILTERS);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const validateActorId = (sourceId: number) => {
|
const validateActorId = (sourceId: number) => {
|
||||||
if (isIdEmpty(sourceId)) {
|
if (isIdEmpty(sourceId)) {
|
||||||
setError(ErrorField.ACTOR, 'Service account is required.');
|
setError(ErrorField.ACTOR, 'Service account is required.');
|
||||||
@ -148,11 +160,17 @@ export const useProjectActionsForm = (action?: IActionSet) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const validateActions = (actions: ActionsActionState[]) => {
|
const validateActions = (actions: ActionsActionState[]) => {
|
||||||
if (actions.length === 0) {
|
if (actions.filter(({ action }) => Boolean(action)).length === 0) {
|
||||||
setError(ErrorField.ACTIONS, 'At least one action is required.');
|
setError(ErrorField.ACTIONS, 'At least one action is required.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearError(ErrorField.ACTIONS);
|
||||||
|
if (actions.some(({ error }) => error)) {
|
||||||
|
setError(ErrorField.ACTIONS, 'One or more actions have errors.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
clearError(ErrorField.ACTIONS);
|
clearError(ErrorField.ACTIONS);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@ -160,12 +178,19 @@ export const useProjectActionsForm = (action?: IActionSet) => {
|
|||||||
const validate = () => {
|
const validate = () => {
|
||||||
const validName = validateName(name);
|
const validName = validateName(name);
|
||||||
const validSourceId = validateSourceId(sourceId);
|
const validSourceId = validateSourceId(sourceId);
|
||||||
|
const validFilters = validateFilters(filters);
|
||||||
const validActorId = validateActorId(actorId);
|
const validActorId = validateActorId(actorId);
|
||||||
const validActions = validateActions(actions);
|
const validActions = validateActions(actions);
|
||||||
|
|
||||||
setValidated(true);
|
setValidated(true);
|
||||||
|
|
||||||
return validName && validSourceId && validActorId && validActions;
|
return (
|
||||||
|
validName &&
|
||||||
|
validSourceId &&
|
||||||
|
validFilters &&
|
||||||
|
validActorId &&
|
||||||
|
validActions
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -187,6 +212,8 @@ export const useProjectActionsForm = (action?: IActionSet) => {
|
|||||||
setErrors,
|
setErrors,
|
||||||
validated,
|
validated,
|
||||||
validateName,
|
validateName,
|
||||||
|
validateSourceId,
|
||||||
|
validateActorId,
|
||||||
validate,
|
validate,
|
||||||
reloadForm,
|
reloadForm,
|
||||||
};
|
};
|
||||||
|
@ -81,6 +81,8 @@ export const ProjectActionsModal = ({
|
|||||||
setActions,
|
setActions,
|
||||||
errors,
|
errors,
|
||||||
validateName,
|
validateName,
|
||||||
|
validateSourceId,
|
||||||
|
validateActorId,
|
||||||
validate,
|
validate,
|
||||||
validated,
|
validated,
|
||||||
reloadForm,
|
reloadForm,
|
||||||
@ -127,11 +129,13 @@ export const ProjectActionsModal = ({
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
actorId,
|
actorId,
|
||||||
actions: actions.map(({ action, sortOrder, executionParams }) => ({
|
actions: actions
|
||||||
action,
|
.filter(({ action }) => Boolean(action))
|
||||||
sortOrder,
|
.map(({ action, sortOrder, executionParams }) => ({
|
||||||
executionParams,
|
action,
|
||||||
})),
|
sortOrder,
|
||||||
|
executionParams,
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatApiCode = () => `curl --location --request ${
|
const formatApiCode = () => `curl --location --request ${
|
||||||
@ -206,6 +210,8 @@ export const ProjectActionsModal = ({
|
|||||||
setActions={setActions}
|
setActions={setActions}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
validateName={validateName}
|
validateName={validateName}
|
||||||
|
validateSourceId={validateSourceId}
|
||||||
|
validateActorId={validateActorId}
|
||||||
validated={validated}
|
validated={validated}
|
||||||
/>
|
/>
|
||||||
<StyledButtonContainer>
|
<StyledButtonContainer>
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { formatApiPath } from 'utils/formatPath';
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||||
import useSWR from 'swr';
|
|
||||||
import { IRole } from 'interfaces/role';
|
import { IRole } from 'interfaces/role';
|
||||||
import { IServiceAccount } from 'interfaces/service-account';
|
import { IServiceAccount } from 'interfaces/service-account';
|
||||||
import { IMatrixPermission } from 'interfaces/permissions';
|
import { IMatrixPermission } from 'interfaces/permissions';
|
||||||
import { IPermission } from 'interfaces/user';
|
import { IPermission } from 'interfaces/user';
|
||||||
|
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
|
||||||
|
|
||||||
interface IServiceAccountAccessMatrix {
|
interface IServiceAccountAccessMatrix {
|
||||||
root: IMatrixPermission[];
|
root: IMatrixPermission[];
|
||||||
@ -30,7 +30,7 @@ interface IServiceAccountAccessMatrixOutput
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useServiceAccountAccessMatrix = (
|
export const useServiceAccountAccessMatrix = (
|
||||||
id: number,
|
id?: number,
|
||||||
project?: string,
|
project?: string,
|
||||||
environment?: string,
|
environment?: string,
|
||||||
): IServiceAccountAccessMatrixOutput => {
|
): IServiceAccountAccessMatrixOutput => {
|
||||||
@ -39,10 +39,9 @@ export const useServiceAccountAccessMatrix = (
|
|||||||
}`;
|
}`;
|
||||||
const url = `api/admin/service-account/${id}/permissions${queryParams}`;
|
const url = `api/admin/service-account/${id}/permissions${queryParams}`;
|
||||||
|
|
||||||
const { data, error, mutate } = useSWR<IServiceAccountAccessMatrixResponse>(
|
const { data, error, mutate } = useConditionalSWR<
|
||||||
formatApiPath(url),
|
IServiceAccountAccessMatrixResponse | undefined
|
||||||
fetcher,
|
>(Boolean(id), undefined, formatApiPath(url), fetcher);
|
||||||
);
|
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
24
src/lib/util/constants/actions.ts
Normal file
24
src/lib/util/constants/actions.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
type ActionDefinition = {
|
||||||
|
label: string;
|
||||||
|
permissions: string[];
|
||||||
|
required: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ACTIONS = new Map<string, ActionDefinition>([
|
||||||
|
[
|
||||||
|
'TOGGLE_FEATURE_ON',
|
||||||
|
{
|
||||||
|
label: 'Enable flag',
|
||||||
|
permissions: ['UPDATE_FEATURE_ENVIRONMENT'],
|
||||||
|
required: ['project', 'environment', 'featureName'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'TOGGLE_FEATURE_OFF',
|
||||||
|
{
|
||||||
|
label: 'Disable flag',
|
||||||
|
permissions: ['UPDATE_FEATURE_ENVIRONMENT'],
|
||||||
|
required: ['project', 'environment', 'featureName'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
Loading…
Reference in New Issue
Block a user