- {actions.map(({ id, action, executionParams }) => (
-
- {action}
-
- {Object.entries(executionParams).map(
- ([param, value]) => (
-
- {param}:{' '}
- {value}
-
- ),
- )}
-
-
- ))}
+ {actions.map(
+ ({ action, executionParams, sortOrder }) => (
+
+ {action}
+
+ {Object.entries(executionParams).map(
+ ([param, value]) => (
+
+ {param}:{' '}
+ {value}
+
+ ),
+ )}
+
+
+ ),
+ )}
}
>
diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ActionItem.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ActionItem.tsx
new file mode 100644
index 0000000000..910c1657fc
--- /dev/null
+++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ActionItem.tsx
@@ -0,0 +1,134 @@
+import { IconButton, Tooltip } from '@mui/material';
+import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
+import { IAction } from 'interfaces/action';
+import { Fragment } from 'react';
+import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
+import { Delete } from '@mui/icons-material';
+import { useProjectEnvironments } from 'hooks/api/getters/useProjectEnvironments/useProjectEnvironments';
+import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
+import mapValues from 'lodash.mapvalues';
+import { useFeatureSearch } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
+import {
+ BoxSeparator,
+ Col,
+ InnerBoxHeader,
+ Row,
+ StyledInnerBox,
+} from './InnerContainerBox';
+
+export type UIAction = Omit & {
+ id: string;
+};
+
+export const ActionItem = ({
+ action,
+ index,
+ stateChanged,
+ onDelete,
+}: {
+ action: UIAction;
+ index: number;
+ stateChanged: (action: UIAction) => void;
+ onDelete: () => void;
+}) => {
+ const { id, action: actionName } = action;
+ const projectId = useRequiredPathParam('projectId');
+ const environments = useProjectEnvironments(projectId);
+ const { features } = useFeatureSearch(
+ mapValues(
+ {
+ project: `IS:${projectId}`,
+ },
+ (value) => (value ? `${value}` : undefined),
+ ),
+ {},
+ );
+ return (
+
+ 0}
+ show={THEN}
+ />
+
+
+ Action {index + 1}
+
+
+
+
+
+
+
+
+
+
+
+ stateChanged({
+ ...action,
+ action: selected,
+ })
+ }
+ fullWidth
+ />
+
+
+ ({
+ label: env.name,
+ key: env.name,
+ }))}
+ value={action.executionParams.environment as string}
+ onChange={(selected) =>
+ stateChanged({
+ ...action,
+ executionParams: {
+ ...action.executionParams,
+ environment: selected,
+ },
+ })
+ }
+ fullWidth
+ />
+
+
+ ({
+ label: feature.name,
+ key: feature.name,
+ }))}
+ value={action.executionParams.featureName as string}
+ onChange={(selected) =>
+ stateChanged({
+ ...action,
+ executionParams: {
+ ...action.executionParams,
+ featureName: selected,
+ },
+ })
+ }
+ fullWidth
+ />
+
+
+
+
+ );
+};
diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/FilterItem.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/FilterItem.tsx
new file mode 100644
index 0000000000..7f5271b6a8
--- /dev/null
+++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/FilterItem.tsx
@@ -0,0 +1,80 @@
+import { Badge, IconButton, Tooltip, styled } from '@mui/material';
+import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
+import { IActionFilter } from './useProjectActionsForm';
+import { Fragment } from 'react';
+import { Delete } from '@mui/icons-material';
+import Input from 'component/common/Input/Input';
+import {
+ BoxSeparator,
+ InnerBoxHeader,
+ Row,
+ StyledInnerBox,
+} from './InnerContainerBox';
+
+const StyledInput = styled(Input)(() => ({
+ width: '100%',
+}));
+
+const StyledBadge = styled(Badge)(({ theme }) => ({
+ color: 'primary',
+ margin: theme.spacing(1),
+}));
+
+export const FilterItem = ({
+ filter,
+ index,
+ stateChanged,
+ onDelete,
+}: {
+ filter: IActionFilter;
+ index: number;
+ stateChanged: (updatedFilter: IActionFilter) => void;
+ onDelete: () => void;
+}) => {
+ const { id, parameter, value } = filter;
+ return (
+
+ 0}
+ show={AND}
+ />
+
+
+ Filter {index + 1}
+
+
+
+
+
+
+
+
+
+
+ stateChanged({
+ id,
+ parameter: e.target.value,
+ value,
+ })
+ }
+ />
+ =
+
+ stateChanged({
+ id,
+ parameter,
+ value: e.target.value,
+ })
+ }
+ />
+
+
+
+ );
+};
diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/InnerContainerBox.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/InnerContainerBox.tsx
new file mode 100644
index 0000000000..d2fc1d85f6
--- /dev/null
+++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/InnerContainerBox.tsx
@@ -0,0 +1,57 @@
+import { Box, styled } from '@mui/material';
+
+export const StyledInnerBox = styled(Box)(({ theme }) => ({
+ display: 'flex',
+ flexDirection: 'column',
+ backgroundColor: theme.palette.background.default,
+ border: `1px solid ${theme.palette.divider}`,
+ padding: theme.spacing(2),
+ borderRadius: `${theme.shape.borderRadiusMedium}px`,
+}));
+
+export const InnerBoxHeader = styled('div')(({ theme }) => ({
+ marginLeft: 'auto',
+ whiteSpace: 'nowrap',
+ [theme.breakpoints.down('sm')]: {
+ display: 'none',
+ },
+}));
+
+// row for inner containers
+export const Row = styled('div')({
+ display: 'flex',
+ flexDirection: 'row',
+ width: '100%',
+});
+
+export const Col = styled('div')({
+ flex: 1,
+ margin: '0 4px',
+});
+
+export const BoxSeparator: React.FC = ({ children }) => {
+ const StyledBoxContent = styled('div')(({ theme }) => ({
+ padding: theme.spacing(0.75, 1),
+ color: theme.palette.text.primary,
+ fontSize: theme.fontSizes.smallerBody,
+ backgroundColor: theme.palette.seen.primary,
+ borderRadius: theme.shape.borderRadius,
+ position: 'absolute',
+ zIndex: theme.zIndex.fab,
+ top: '50%',
+ left: theme.spacing(2),
+ transform: 'translateY(-50%)',
+ lineHeight: 1,
+ }));
+ return (
+
+ {children}
+
+ );
+};
diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsForm.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsForm.tsx
index a85c1a9744..bad5c72d95 100644
--- a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsForm.tsx
+++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/ProjectActionsForm.tsx
@@ -1,11 +1,24 @@
-import { Alert, Link, styled } from '@mui/material';
+import { Alert, Box, Button, Link, styled } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';
import Input from 'component/common/Input/Input';
+import { Badge } from 'component/common/Badge/Badge';
import { FormSwitch } from 'component/common/FormSwitch/FormSwitch';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
-import { IAction, IActionSet } from 'interfaces/action';
-import { ProjectActionsFormErrors } from './useProjectActionsForm';
+import { IActionSet } from 'interfaces/action';
+import {
+ IActionFilter,
+ ProjectActionsFormErrors,
+} from './useProjectActionsForm';
import { useServiceAccounts } from 'hooks/api/getters/useServiceAccounts/useServiceAccounts';
+import { useIncomingWebhooks } from 'hooks/api/getters/useIncomingWebhooks/useIncomingWebhooks';
+import { v4 as uuidv4 } from 'uuid';
+import { useMemo } from 'react';
+import GeneralSelect, {} from 'component/common/GeneralSelect/GeneralSelect';
+import { Add } from '@mui/icons-material';
+import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
+import { Row } from './InnerContainerBox';
+import { ActionItem, UIAction } from './ActionItem';
+import { FilterItem } from './FilterItem';
const StyledServiceAccountAlert = styled(Alert)(({ theme }) => ({
marginBottom: theme.spacing(4),
@@ -27,30 +40,31 @@ const StyledInputDescription = styled('p')(({ theme }) => ({
},
}));
-const StyledInputSecondaryDescription = styled('p')(({ theme }) => ({
- color: theme.palette.text.secondary,
- marginBottom: theme.spacing(1),
-}));
-
-const StyledInput = styled(Input)(({ theme }) => ({
+const StyledInput = styled(Input)(() => ({
width: '100%',
- maxWidth: theme.spacing(50),
}));
-const StyledSecondarySection = styled('div')(({ theme }) => ({
- padding: theme.spacing(3),
- backgroundColor: theme.palette.background.elevation2,
+const StyledBadge = styled(Badge)(({ theme }) => ({
+ color: 'primary',
+ margin: 'auto',
+ marginBottom: theme.spacing(1.5),
+}));
+
+const StyledBox = styled(Box)(({ theme }) => ({
+ display: 'flex',
+ flexDirection: 'column',
+ backgroundColor: theme.palette.background.elevation1,
+ marginTop: theme.spacing(2),
+ padding: theme.spacing(2),
borderRadius: theme.shape.borderRadiusMedium,
- marginTop: theme.spacing(4),
- marginBottom: theme.spacing(2),
}));
-const StyledInlineContainer = styled('div')(({ theme }) => ({
- padding: theme.spacing(0, 4),
- '& > p:not(:first-of-type)': {
- marginTop: theme.spacing(2),
- },
-}));
+const Step = ({ name, children }: any) => (
+
+ {name}
+ {children}
+
+);
interface IProjectActionsFormProps {
action?: IActionSet;
@@ -60,12 +74,12 @@ interface IProjectActionsFormProps {
setName: React.Dispatch>;
sourceId: number;
setSourceId: React.Dispatch>;
- filters: Record;
- setFilters: React.Dispatch>>;
+ filters: IActionFilter[];
+ setFilters: React.Dispatch>;
actorId: number;
setActorId: React.Dispatch>;
- actions: IAction[];
- setActions: React.Dispatch>;
+ actions: UIAction[];
+ setActions: React.Dispatch>;
errors: ProjectActionsFormErrors;
validateName: (name: string) => boolean;
validated: boolean;
@@ -89,16 +103,83 @@ export const ProjectActionsForm = ({
validateName,
validated,
}: IProjectActionsFormProps) => {
- const { serviceAccounts } = useServiceAccounts();
+ const { serviceAccounts, loading: serviceAccountsLoading } =
+ useServiceAccounts();
+ const { incomingWebhooks, loading: incomingWebhooksLoading } =
+ useIncomingWebhooks();
const handleOnBlur = (callback: Function) => {
setTimeout(() => callback(), 300);
};
+ const addFilter = () => {
+ const id = uuidv4();
+ setFilters((filters) => [
+ ...filters,
+ {
+ id,
+ parameter: '',
+ value: '',
+ },
+ ]);
+ };
+
+ const updateInFilters = (updatedFilter: IActionFilter) => {
+ setFilters((filters) =>
+ filters.map((filter) =>
+ filter.id === updatedFilter.id ? updatedFilter : filter,
+ ),
+ );
+ };
+
+ const addAction = (projectId: string) => {
+ const id = uuidv4();
+ const action: UIAction = {
+ id,
+ action: '',
+ sortOrder:
+ actions
+ .map((a) => a.sortOrder)
+ .reduce((a, b) => Math.max(a, b), 0) + 1,
+ executionParams: {
+ project: projectId,
+ },
+ };
+ setActions([...actions, action]);
+ };
+
+ const updateInActions = (updatedAction: UIAction) => {
+ setActions((actions) =>
+ actions.map((action) =>
+ action.id === updatedAction.id ? updatedAction : action,
+ ),
+ );
+ };
+
+ const incomingWebhookOptions = useMemo(() => {
+ if (incomingWebhooksLoading) {
+ return [];
+ }
+
+ return incomingWebhooks.map((webhook) => ({
+ label: webhook.name,
+ key: `${webhook.id}`,
+ }));
+ }, [incomingWebhooksLoading, incomingWebhooks]);
+
+ const serviceAccountOptions = useMemo(() => {
+ if (serviceAccountsLoading) {
+ return [];
+ }
+
+ return serviceAccounts.map((sa) => ({
+ label: sa.name,
+ key: `${sa.id}`,
+ }));
+ }, [serviceAccountsLoading, serviceAccounts]);
+
const showErrors = validated && Object.values(errors).some(Boolean);
-
- // TODO: Need to add the remaining fields. Refer to the design
-
+ const projectId = useRequiredPathParam('projectId');
return (
handleOnBlur(() => validateName(e.target.value))}
autoComplete='off'
/>
+
+
+
+ Create incoming webhooks from
+
+ integrations section
+
+ .
+
+ {
+ setSourceId(parseInt(v));
+ }}
+ />
+
+
+
+ {filters.map((filter, index) => (
+
+ setFilters((filters) =>
+ filters.filter((f) => f.id !== filter.id),
+ )
+ }
+ />
+ ))}
+
+
+
+ }
+ onClick={addFilter}
+ variant='outlined'
+ color='primary'
+ >
+ Add filter
+
+
+
+
+
+
+ Create service accounts from
+
+ service accounts section
+
+ .
+
+ {
+ setActorId(parseInt(v));
+ }}
+ />
+
+ {actions.map((action, index) => (
+
+ setActions((actions) =>
+ actions.filter((a) => a.id !== action.id),
+ )
+ }
+ />
+ ))}
+
+
+ }
+ onClick={() => addAction(projectId)}
+ variant='outlined'
+ color='primary'
+ >
+ Add action
+
+
+
+
(
diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/useProjectActionsForm.ts b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/useProjectActionsForm.ts
index 021e24517e..53105d3e67 100644
--- a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/useProjectActionsForm.ts
+++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsForm/useProjectActionsForm.ts
@@ -1,14 +1,22 @@
import { useActions } from 'hooks/api/getters/useActions/useActions';
-import { IAction, IActionSet } from 'interfaces/action';
+import { IActionSet } from 'interfaces/action';
import { useEffect, useState } from 'react';
+import { UIAction } from './ActionItem';
+import { v4 as uuidv4 } from 'uuid';
-enum ErrorField {
+export enum ErrorField {
NAME = 'name',
TRIGGER = 'trigger',
ACTOR = 'actor',
ACTIONS = 'actions',
}
+export interface IActionFilter {
+ id: string;
+ parameter: string;
+ value: string;
+}
+
const DEFAULT_PROJECT_ACTIONS_FORM_ERRORS = {
[ErrorField.NAME]: undefined,
[ErrorField.TRIGGER]: undefined,
@@ -24,14 +32,38 @@ export const useProjectActionsForm = (action?: IActionSet) => {
const [enabled, setEnabled] = useState(false);
const [name, setName] = useState('');
const [sourceId, setSourceId] = useState(0);
- const [filters, setFilters] = useState>({});
+ const [filters, setFilters] = useState([]);
const [actorId, setActorId] = useState(0);
- const [actions, setActions] = useState([]);
+ const [actions, setActions] = useState([]);
const reloadForm = () => {
setEnabled(action?.enabled ?? true);
setName(action?.name || '');
setValidated(false);
+ if (action?.actorId) {
+ setActorId(action?.actorId);
+ }
+ if (action?.match) {
+ const { sourceId, payload } = action.match;
+ setSourceId(sourceId);
+ setFilters(
+ Object.entries(payload).map(([parameter, value]) => ({
+ id: uuidv4(),
+ parameter,
+ value: value as string,
+ })),
+ );
+ }
+ if (action?.actions) {
+ setActions(
+ action.actions.map((action) => ({
+ id: uuidv4(),
+ action: action.action,
+ sortOrder: action.sortOrder,
+ executionParams: action.executionParams,
+ })),
+ );
+ }
setErrors(DEFAULT_PROJECT_ACTIONS_FORM_ERRORS);
};
@@ -94,7 +126,7 @@ export const useProjectActionsForm = (action?: IActionSet) => {
return true;
};
- const validateActions = (actions: IAction[]) => {
+ const validateActions = (actions: UIAction[]) => {
if (actions.length === 0) {
setError(ErrorField.ACTIONS, 'At least one action is required.');
return false;
diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsModal.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsModal.tsx
index 545e110af6..9d955abe0f 100644
--- a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsModal.tsx
+++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsModal/ProjectActionsModal.tsx
@@ -83,10 +83,22 @@ export const ProjectActionsModal = ({
match: {
source: 'incoming-webhook',
sourceId,
- payload: filters,
+ payload: filters
+ .filter((f) => f.parameter.length > 0)
+ .reduce(
+ (acc, filter) => ({
+ ...acc,
+ [filter.parameter]: filter.value,
+ }),
+ {},
+ ),
},
actorId,
- actions,
+ actions: actions.map(({ action, sortOrder, executionParams }) => ({
+ action,
+ sortOrder,
+ executionParams,
+ })),
};
const formatApiCode = () => `curl --location --request ${
diff --git a/frontend/src/interfaces/action.ts b/frontend/src/interfaces/action.ts
index d521c81f06..2bb98bb26e 100644
--- a/frontend/src/interfaces/action.ts
+++ b/frontend/src/interfaces/action.ts
@@ -19,10 +19,7 @@ export interface IMatch {
}
export interface IAction {
- id: number;
action: string;
sortOrder: number;
executionParams: Record;
- createdAt: string;
- createdByUserId: number;
}