diff --git a/frontend/src/component/common/SidePanelList/SidePanelList.tsx b/frontend/src/component/common/SidePanelList/SidePanelList.tsx
index 0d986538b6..67af7b75db 100644
--- a/frontend/src/component/common/SidePanelList/SidePanelList.tsx
+++ b/frontend/src/component/common/SidePanelList/SidePanelList.tsx
@@ -21,13 +21,14 @@ const StyledSidePanelHalf = styled('div')({
});
const StyledSidePanelHalfLeft = styled(StyledSidePanelHalf, {
- shouldForwardProp: (prop) => prop !== 'height',
-})<{ height?: number }>(({ theme, height }) => ({
+ shouldForwardProp: (prop) => prop !== 'height' && prop !== 'maxWidth',
+})<{ height?: number; maxWidth?: number }>(({ theme, height, maxWidth }) => ({
border: `1px solid ${theme.palette.divider}`,
borderTop: 0,
borderBottomLeftRadius: theme.shape.borderRadiusMedium,
overflow: 'auto',
...(height && { height }),
+ ...(maxWidth && { maxWidth }),
}));
const StyledSidePanelHalfRight = styled(StyledSidePanelHalf)(({ theme }) => ({
@@ -43,12 +44,14 @@ export const StyledSidePanelListColumn = styled('div', {
shouldForwardProp: (prop) => prop !== 'maxWidth' && prop !== 'align',
})<{ maxWidth?: number; align?: ColumnAlignment }>(
({ theme, maxWidth, align = 'start' }) => ({
+ display: 'flex',
flex: 1,
padding: theme.spacing(2),
fontSize: theme.fontSizes.smallBody,
justifyContent: align,
...(maxWidth && { maxWidth }),
textAlign: align,
+ alignItems: 'center',
}),
);
@@ -64,6 +67,7 @@ interface ISidePanelListProps {
columns: SidePanelListColumn[];
sidePanelHeader: string;
renderContent: (item: T) => ReactNode;
+ renderItem?: (item: T, children: ReactNode) => ReactNode;
height?: number;
listEnd?: ReactNode;
}
@@ -73,6 +77,7 @@ export const SidePanelList = ({
columns,
sidePanelHeader,
renderContent,
+ renderItem = (_, children) => children,
height,
listEnd,
}: ISidePanelListProps) => {
@@ -83,34 +88,44 @@ export const SidePanelList = ({
}
const activeItem = selectedItem || items[0];
+ const leftPanelMaxWidth = columns.every(({ maxWidth }) => Boolean(maxWidth))
+ ? columns.reduce((acc, { maxWidth }) => acc + (maxWidth || 0), 0)
+ : undefined;
return (
-
- {items.map((item) => (
- setSelectedItem(item)}
- >
- {columns.map(
- ({ header, maxWidth, align, cell }) => (
-
- {cell(item)}
-
- ),
- )}
-
- ))}
+
+ {items.map((item) =>
+ renderItem(
+ item,
+ setSelectedItem(item)}
+ >
+ {columns.map(
+ ({ header, maxWidth, align, cell }) => (
+
+ {cell(item)}
+
+ ),
+ )}
+ ,
+ ),
+ )}
{listEnd}
diff --git a/frontend/src/component/common/SidePanelList/SidePanelListHeader.tsx b/frontend/src/component/common/SidePanelList/SidePanelListHeader.tsx
index 66777524b2..bf60881fce 100644
--- a/frontend/src/component/common/SidePanelList/SidePanelListHeader.tsx
+++ b/frontend/src/component/common/SidePanelList/SidePanelListHeader.tsx
@@ -13,22 +13,27 @@ const StyledHeader = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.table.headerBackground,
}));
-const StyledHeaderHalf = styled('div')({
+const StyledHeaderHalf = styled('div', {
+ shouldForwardProp: (prop) => prop !== 'maxWidth',
+})<{ maxWidth?: number }>(({ maxWidth }) => ({
display: 'flex',
flex: 1,
-});
+ ...(maxWidth && { maxWidth }),
+}));
interface ISidePanelListHeaderProps {
columns: SidePanelListColumn[];
sidePanelHeader: string;
+ leftPanelMaxWidth?: number;
}
export const SidePanelListHeader = ({
columns,
sidePanelHeader,
+ leftPanelMaxWidth,
}: ISidePanelListHeaderProps) => (
-
+
{columns.map(({ header, maxWidth, align }) => (
{
+interface ISidePanelListItemProps {
selected: boolean;
onClick: () => void;
children: ReactNode;
}
-export const SidePanelListItem = ({
+export const SidePanelListItem = ({
selected,
onClick,
children,
-}: ISidePanelListItemProps) => (
+}: ISidePanelListItemProps) => (
{children}
diff --git a/frontend/src/component/incomingWebhooks/IncomingWebhooksEvents/IncomingWebhooksEventsModal.tsx b/frontend/src/component/incomingWebhooks/IncomingWebhooksEvents/IncomingWebhooksEventsModal.tsx
index 3bb7ac0a53..b7c99c157d 100644
--- a/frontend/src/component/incomingWebhooks/IncomingWebhooksEvents/IncomingWebhooksEventsModal.tsx
+++ b/frontend/src/component/incomingWebhooks/IncomingWebhooksEvents/IncomingWebhooksEventsModal.tsx
@@ -121,6 +121,7 @@ export const IncomingWebhooksEventsModal = ({
columns={[
{
header: 'Date',
+ maxWidth: 180,
cell: (event) =>
formatDateYMDHMS(
event.createdAt,
@@ -129,6 +130,7 @@ export const IncomingWebhooksEventsModal = ({
},
{
header: 'Token',
+ maxWidth: 350,
cell: (event) => event.tokenName,
},
]}
diff --git a/frontend/src/component/incomingWebhooks/IncomingWebhooksModal/IncomingWebhooksModal.tsx b/frontend/src/component/incomingWebhooks/IncomingWebhooksModal/IncomingWebhooksModal.tsx
index 375f136957..a9dd8ee0e2 100644
--- a/frontend/src/component/incomingWebhooks/IncomingWebhooksModal/IncomingWebhooksModal.tsx
+++ b/frontend/src/component/incomingWebhooks/IncomingWebhooksModal/IncomingWebhooksModal.tsx
@@ -17,6 +17,7 @@ import {
TokenGeneration,
useIncomingWebhooksForm,
} from './IncomingWebhooksForm/useIncomingWebhooksForm';
+import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
const StyledHeader = styled('div')(({ theme }) => ({
display: 'flex',
@@ -158,7 +159,10 @@ export const IncomingWebhooksModal = ({
>
{title}
- View events
+ View events}
+ />
({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: theme.spacing(2),
+ padding: theme.spacing(2),
+}));
+
+export const ProjectActionsEventsDetails = ({
+ state,
+ actionSet: { actions },
+ observableEvent,
+}: IActionSetEvent) => {
+ const stateText =
+ state === 'failed'
+ ? `${
+ actions.filter(({ state }) => state !== 'success').length
+ } out of ${actions.length} actions were not successfully executed`
+ : 'All actions were successfully executed';
+
+ return (
+
+
+ {stateText}
+
+
+ {actions.map((action, i) => (
+
+ Action {i + 1}
+
+ ))}
+
+ );
+};
diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails.tsx/ProjectActionsEventsDetailsAction.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails.tsx/ProjectActionsEventsDetailsAction.tsx
new file mode 100644
index 0000000000..0c55f57944
--- /dev/null
+++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails.tsx/ProjectActionsEventsDetailsAction.tsx
@@ -0,0 +1,110 @@
+import { CheckCircleOutline, ErrorOutline } from '@mui/icons-material';
+import { Alert, CircularProgress, Divider, styled } from '@mui/material';
+import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
+import { IActionEvent } from 'interfaces/action';
+import { ReactNode } from 'react';
+
+const StyledAction = styled('div', {
+ shouldForwardProp: (prop) => prop !== 'state',
+})<{ state?: IActionEvent['state'] }>(({ theme, state }) => ({
+ padding: theme.spacing(2),
+ border: `1px solid ${theme.palette.divider}`,
+ borderRadius: theme.shape.borderRadiusMedium,
+ ...(state === 'not started' && {
+ backgroundColor: theme.palette.background.elevation1,
+ }),
+}));
+
+const StyledHeader = styled('div')({
+ display: 'flex',
+ flexDirection: 'column',
+});
+
+const StyledHeaderRow = styled('div')({
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ width: '100%',
+});
+
+const StyledHeaderState = styled('div')(({ theme }) => ({
+ display: 'flex',
+ alignItems: 'center',
+ fontSize: theme.fontSizes.smallBody,
+ gap: theme.spacing(2),
+}));
+
+export const StyledSuccessIcon = styled(CheckCircleOutline)(({ theme }) => ({
+ color: theme.palette.success.main,
+}));
+
+export const StyledFailedIcon = styled(ErrorOutline)(({ theme }) => ({
+ color: theme.palette.error.main,
+}));
+
+const StyledAlert = styled(Alert)(({ theme }) => ({
+ marginTop: theme.spacing(2),
+}));
+
+const StyledDivider = styled(Divider)(({ theme }) => ({
+ margin: theme.spacing(2, 0),
+}));
+
+const StyledActionBody = styled('div')(({ theme }) => ({
+ fontSize: theme.fontSizes.smallBody,
+}));
+
+const StyledActionLabel = styled('p')(({ theme }) => ({
+ fontWeight: theme.fontWeight.bold,
+ marginBottom: theme.spacing(0.5),
+}));
+
+const StyledPropertyLabel = styled('span')(({ theme }) => ({
+ color: theme.palette.text.secondary,
+}));
+
+interface IProjectActionsEventsDetailsActionProps {
+ action: IActionEvent;
+ children: ReactNode;
+}
+
+export const ProjectActionsEventsDetailsAction = ({
+ action: { state, details, action, executionParams },
+ children,
+}: IProjectActionsEventsDetailsActionProps) => {
+ const actionState =
+ state === 'success' ? (
+
+ ) : state === 'failed' ? (
+
+ ) : state === 'started' ? (
+
+ ) : (
+ Not started
+ );
+
+ return (
+
+
+
+ {children}
+ {actionState}
+
+ {details}}
+ />
+
+
+
+ {action}
+ {Object.entries(executionParams).map(([property, value]) => (
+
+ {property}:{' '}
+ {value}
+
+ ))}
+
+
+ );
+};
diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails.tsx/ProjectActionsEventsDetailsSource/ProjectActionsEventsDetailsSource.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails.tsx/ProjectActionsEventsDetailsSource/ProjectActionsEventsDetailsSource.tsx
new file mode 100644
index 0000000000..fbe2a04c7d
--- /dev/null
+++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails.tsx/ProjectActionsEventsDetailsSource/ProjectActionsEventsDetailsSource.tsx
@@ -0,0 +1,22 @@
+import { IObservableEvent } from 'interfaces/action';
+import { ProjectActionsEventsDetailsSourceIncomingWebhook } from './ProjectActionsEventsDetailsSourceIncomingWebhook';
+
+interface IProjectActionsEventsDetailsSourceProps {
+ observableEvent: IObservableEvent;
+}
+
+export const ProjectActionsEventsDetailsSource = ({
+ observableEvent,
+}: IProjectActionsEventsDetailsSourceProps) => {
+ const { source } = observableEvent;
+
+ if (source === 'incoming-webhook') {
+ return (
+
+ );
+ }
+
+ return null;
+};
diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails.tsx/ProjectActionsEventsDetailsSource/ProjectActionsEventsDetailsSourceIncomingWebhook.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails.tsx/ProjectActionsEventsDetailsSource/ProjectActionsEventsDetailsSourceIncomingWebhook.tsx
new file mode 100644
index 0000000000..50e37d3589
--- /dev/null
+++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails.tsx/ProjectActionsEventsDetailsSource/ProjectActionsEventsDetailsSourceIncomingWebhook.tsx
@@ -0,0 +1,75 @@
+import { ExpandMore } from '@mui/icons-material';
+import {
+ Accordion,
+ AccordionDetails,
+ AccordionSummary,
+ IconButton,
+ styled,
+} from '@mui/material';
+import { useIncomingWebhooks } from 'hooks/api/getters/useIncomingWebhooks/useIncomingWebhooks';
+import { IObservableEvent } from 'interfaces/action';
+import { Suspense, lazy, useMemo } from 'react';
+import { Link } from 'react-router-dom';
+
+const LazyReactJSONEditor = lazy(
+ () => import('component/common/ReactJSONEditor/ReactJSONEditor'),
+);
+
+const StyledAccordion = styled(Accordion)(({ theme }) => ({
+ boxShadow: 'none',
+ border: `1px solid ${theme.palette.divider}`,
+ borderRadius: theme.shape.borderRadiusMedium,
+ '&:before': {
+ display: 'none',
+ },
+}));
+
+const StyledLink = styled(Link)(({ theme }) => ({
+ marginLeft: theme.spacing(1),
+}));
+
+interface IProjectActionsEventsDetailsSourceIncomingWebhookProps {
+ observableEvent: IObservableEvent;
+}
+
+export const ProjectActionsEventsDetailsSourceIncomingWebhook = ({
+ observableEvent,
+}: IProjectActionsEventsDetailsSourceIncomingWebhookProps) => {
+ const { incomingWebhooks } = useIncomingWebhooks();
+
+ const incomingWebhookName = useMemo(() => {
+ const incomingWebhook = incomingWebhooks.find(
+ (incomingWebhook) =>
+ incomingWebhook.id === observableEvent.sourceId,
+ );
+
+ return incomingWebhook?.name;
+ }, [incomingWebhooks, observableEvent.sourceId]);
+
+ return (
+
+
+
+
+ }
+ >
+ Incoming webhook:
+
+ {incomingWebhookName}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsModal.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsModal.tsx
new file mode 100644
index 0000000000..cc72039c7b
--- /dev/null
+++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsModal.tsx
@@ -0,0 +1,169 @@
+import { Button, Link, styled } from '@mui/material';
+import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
+import { IActionSet } from 'interfaces/action';
+import { useActionEvents } from 'hooks/api/getters/useActionEvents/useActionEvents';
+import FormTemplate from 'component/common/FormTemplate/FormTemplate';
+import { SidePanelList } from 'component/common/SidePanelList/SidePanelList';
+import { formatDateYMDHMS } from 'utils/formatDate';
+import { useLocationSettings } from 'hooks/useLocationSettings';
+import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
+import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
+import { ProjectActionsEventsStateCell } from './ProjectActionsEventsStateCell';
+import { ProjectActionsEventsDetails } from './ProjectActionsEventsDetails.tsx/ProjectActionsEventsDetails';
+
+const StyledHeader = styled('div')(({ theme }) => ({
+ display: 'flex',
+ flexDirection: 'column',
+ marginBottom: theme.fontSizes.mainHeader,
+}));
+
+const StyledHeaderRow = styled('div')({
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ width: '100%',
+});
+
+const StyledTitle = styled('h1')({
+ fontWeight: 'normal',
+});
+
+const StyledForm = styled('form')({
+ display: 'flex',
+ flexDirection: 'column',
+ height: '100%',
+});
+
+const StyledFailedItemWrapper = styled('div')(({ theme }) => ({
+ backgroundColor: theme.palette.error.light,
+}));
+
+const StyledButtonContainer = styled('div')(({ theme }) => ({
+ marginTop: 'auto',
+ display: 'flex',
+ justifyContent: 'flex-end',
+ paddingTop: theme.spacing(4),
+}));
+
+interface IProjectActionsEventsModalProps {
+ action?: IActionSet;
+ open: boolean;
+ setOpen: React.Dispatch>;
+ onOpenConfiguration: () => void;
+}
+
+export const ProjectActionsEventsModal = ({
+ action,
+ open,
+ setOpen,
+ onOpenConfiguration,
+}: IProjectActionsEventsModalProps) => {
+ const projectId = useRequiredPathParam('projectId');
+ const { locationSettings } = useLocationSettings();
+ const { actionEvents, hasMore, loadMore, loading } = useActionEvents(
+ action?.id,
+ projectId,
+ 20,
+ {
+ refreshInterval: 5000,
+ },
+ );
+
+ if (!action) {
+ return null;
+ }
+
+ const title = `Events: ${action.name}`;
+
+ return (
+ {
+ setOpen(false);
+ }}
+ label={title}
+ >
+
+
+
+ {title}
+
+ View configuration
+
+
+
+
+
+ formatDateYMDHMS(
+ createdAt,
+ locationSettings?.locale,
+ ),
+ },
+ ]}
+ sidePanelHeader='Details'
+ renderContent={ProjectActionsEventsDetails}
+ renderItem={({ id, state }, children) => {
+ if (state === 'failed') {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return children;
+ }}
+ listEnd={
+
+ Load more
+
+ }
+ />
+ }
+ />
+
+ No events have been registered for this action
+ set.
+
+ }
+ />
+
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsStateCell.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsStateCell.tsx
new file mode 100644
index 0000000000..74006a84fd
--- /dev/null
+++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsStateCell.tsx
@@ -0,0 +1,23 @@
+import { CircularProgress, styled } from '@mui/material';
+import { CheckCircle, Error as ErrorIcon } from '@mui/icons-material';
+import { IActionSetEvent } from 'interfaces/action';
+
+export const StyledSuccessIcon = styled(CheckCircle)(({ theme }) => ({
+ color: theme.palette.success.main,
+}));
+
+export const StyledFailedIcon = styled(ErrorIcon)(({ theme }) => ({
+ color: theme.palette.error.main,
+}));
+
+export const ProjectActionsEventsStateCell = ({ state }: IActionSetEvent) => {
+ if (state === 'success') {
+ return ;
+ }
+
+ if (state === 'failed') {
+ return ;
+ }
+
+ return ;
+};
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 541e3ad2d7..1920e94c78 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
@@ -1,5 +1,5 @@
import { FormEvent, useEffect } from 'react';
-import { Button, styled } from '@mui/material';
+import { Button, Link, styled } from '@mui/material';
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
@@ -14,6 +14,19 @@ import {
import { ProjectActionsForm } from './ProjectActionsForm/ProjectActionsForm';
import { useProjectActionsForm } from './ProjectActionsForm/useProjectActionsForm';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
+import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
+
+const StyledHeader = styled('div')(({ theme }) => ({
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ width: '100%',
+ marginBottom: theme.fontSizes.mainHeader,
+}));
+
+const StyledTitle = styled('h1')({
+ fontWeight: 'normal',
+});
const StyledForm = styled('form')(() => ({
display: 'flex',
@@ -36,12 +49,14 @@ interface IProjectActionsModalProps {
action?: IActionSet;
open: boolean;
setOpen: React.Dispatch>;
+ onOpenEvents: () => void;
}
export const ProjectActionsModal = ({
action,
open,
setOpen,
+ onOpenEvents,
}: IProjectActionsModalProps) => {
const projectId = useRequiredPathParam('projectId');
const { refetch } = useActions(projectId);
@@ -142,12 +157,18 @@ export const ProjectActionsModal = ({
+
+ {title}
+ View events}
+ />
+
{
@@ -182,6 +184,10 @@ export const ProjectActionsTable = ({
}: { row: { original: IActionSet } }) => (
{
+ setSelectedAction(action);
+ setEventsModalOpen(true);
+ }}
onEdit={() => {
setSelectedAction(action);
setModalOpen(true);
@@ -255,6 +261,19 @@ export const ProjectActionsTable = ({
action={selectedAction}
open={modalOpen}
setOpen={setModalOpen}
+ onOpenEvents={() => {
+ setModalOpen(false);
+ setEventsModalOpen(true);
+ }}
+ />
+ {
+ setEventsModalOpen(false);
+ setModalOpen(true);
+ }}
/>
void;
onEdit: (event: React.SyntheticEvent) => void;
onDelete: (event: React.SyntheticEvent) => void;
}
export const ProjectActionsTableActionsCell = ({
actionId,
+ onOpenEvents,
onEdit,
onDelete,
}: IProjectActionsTableActionsCellProps) => {
@@ -80,6 +82,24 @@ export const ProjectActionsTableActionsCell = ({
}}
>
+
+ {({ hasAccess }) => (
+
+ )}
+
{({ hasAccess }) => (