From 08c80517a6f87d8efabbd0ccb0953848ddfe561f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Mon, 26 Feb 2024 18:59:25 +0000 Subject: [PATCH 1/3] chore: action events UI --- .../common/SidePanelList/SidePanelList.tsx | 59 +++--- .../SidePanelList/SidePanelListHeader.tsx | 11 +- .../SidePanelList/SidePanelListItem.tsx | 6 +- .../IncomingWebhooksEventsModal.tsx | 2 + .../ProjectActionsEventsDetails.tsx | 42 +++++ .../ProjectActionsEventsDetailsAction.tsx | 110 ++++++++++++ .../ProjectActionsEventsModal.tsx | 169 ++++++++++++++++++ .../ProjectActionsEventsStateCell.tsx | 23 +++ .../ProjectActionsModal.tsx | 21 ++- .../ProjectActionsTable.tsx | 19 ++ .../ProjectActionsTableActionsCell.tsx | 22 ++- .../useActionEvents/useActionEvents.ts | 83 +++++++++ frontend/src/interfaces/action.ts | 34 ++++ frontend/src/interfaces/incomingWebhook.ts | 6 +- 14 files changed, 573 insertions(+), 34 deletions(-) create mode 100644 frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails.tsx/ProjectActionsEventsDetails.tsx create mode 100644 frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails.tsx/ProjectActionsEventsDetailsAction.tsx create mode 100644 frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsModal.tsx create mode 100644 frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsStateCell.tsx create mode 100644 frontend/src/hooks/api/getters/useActionEvents/useActionEvents.ts diff --git a/frontend/src/component/common/SidePanelList/SidePanelList.tsx b/frontend/src/component/common/SidePanelList/SidePanelList.tsx index 0d986538b6ea..67af7b75db13 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 66777524b280..bf60881fceac 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 3bb7ac0a534b..b7c99c157d17 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/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails.tsx/ProjectActionsEventsDetails.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails.tsx/ProjectActionsEventsDetails.tsx new file mode 100644 index 000000000000..453b562006e8 --- /dev/null +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails.tsx/ProjectActionsEventsDetails.tsx @@ -0,0 +1,42 @@ +import { Alert, styled } from '@mui/material'; +import { IActionSetEvent } from 'interfaces/action'; +import { ProjectActionsEventsDetailsAction } from './ProjectActionsEventsDetailsAction'; + +const StyledDetails = styled('div')(({ theme }) => ({ + 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 000000000000..0c55f57944c2 --- /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/ProjectActionsEventsModal.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsModal.tsx new file mode 100644 index 000000000000..ce3e57f9c7c2 --- /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={({ 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 000000000000..74006a84fd14 --- /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 541e3ad2d7f8..e3b199ec9cf6 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'; @@ -15,6 +15,18 @@ import { ProjectActionsForm } from './ProjectActionsForm/ProjectActionsForm'; import { useProjectActionsForm } from './ProjectActionsForm/useProjectActionsForm'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; +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', flexDirection: 'column', @@ -36,12 +48,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 +156,15 @@ 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 }) => ( + + + + + + + View events + + + + )} + {({ hasAccess }) => ( { + const response = await fetch(url); + await handleErrorResponses('Action events')(response); + return response.json(); +}; + +export const useActionEvents = ( + actionSetId?: number, + projectId?: string, + limit = 50, + options: SWRInfiniteConfiguration = {}, +) => { + const { isEnterprise } = useUiConfig(); + const automatedActionsEnabled = useUiFlag('automatedActions'); + + const getKey: SWRInfiniteKeyLoader = ( + pageIndex: number, + previousPageData: ActionEventsResponse, + ) => { + // Does not meet conditions + if ( + !actionSetId || + !projectId || + !isEnterprise || + !automatedActionsEnabled + ) + return null; + + // Reached the end + if (previousPageData && !previousPageData.actionSetEvents.length) + return null; + + return formatApiPath( + `api/admin/projects/${projectId}/actions/${actionSetId}/events?limit=${limit}&offset=${ + pageIndex * limit + }`, + ); + }; + + const { data, error, size, setSize, mutate } = + useSWRInfinite(getKey, fetcher, { + ...options, + revalidateAll: true, + }); + + const actionEvents = data + ? data.flatMap(({ actionSetEvents }) => actionSetEvents) + : []; + + const isLoadingInitialData = !data && !error; + const isLoadingMore = size > 0 && !data?.[size - 1]; + const loading = isLoadingInitialData || isLoadingMore; + + const hasMore = data?.[size - 1]?.actionSetEvents.length === limit; + + const loadMore = () => { + if (loading || !hasMore) return; + setSize(size + 1); + }; + + return { + actionEvents, + hasMore, + loadMore, + loading, + refetch: () => mutate(), + error, + }; +}; diff --git a/frontend/src/interfaces/action.ts b/frontend/src/interfaces/action.ts index d521c81f06ba..532a1d04f7d7 100644 --- a/frontend/src/interfaces/action.ts +++ b/frontend/src/interfaces/action.ts @@ -26,3 +26,37 @@ export interface IAction { createdAt: string; createdByUserId: number; } + +export type ObservableEventSource = 'incoming-webhook'; + +interface IObservableEvent { + id: number; + source: ObservableEventSource; + sourceId: number; + createdAt: string; + createdByIncomingWebhookTokenId: number; + payload: Record; +} + +type ActionSetState = 'started' | 'success' | 'failed'; + +type ActionState = ActionSetState | 'not started'; + +export interface IActionEvent extends IAction { + state: ActionState; + details?: string; +} + +interface IActionSetEventActionSet extends IActionSet { + actions: IActionEvent[]; +} + +export interface IActionSetEvent { + id: number; + actionSetId: number; + observableEventId: number; + createdAt: string; + state: ActionSetState; + observableEvent: IObservableEvent; + actionSet: IActionSetEventActionSet; +} diff --git a/frontend/src/interfaces/incomingWebhook.ts b/frontend/src/interfaces/incomingWebhook.ts index cc02b0015882..4a8623731e54 100644 --- a/frontend/src/interfaces/incomingWebhook.ts +++ b/frontend/src/interfaces/incomingWebhook.ts @@ -1,3 +1,5 @@ +import { ObservableEventSource } from './action'; + export interface IIncomingWebhook { id: number; enabled: boolean; @@ -16,13 +18,11 @@ export interface IIncomingWebhookToken { createdByUserId: number; } -type EventSource = 'incoming-webhook'; - export interface IIncomingWebhookEvent { id: number; payload: Record; createdAt: string; - source: EventSource; + source: ObservableEventSource; sourceId: number; tokenName: string; } From c891e01e5f66305d5d227165916c700766260d1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Tue, 27 Feb 2024 09:32:06 +0000 Subject: [PATCH 2/3] chore: action events UI development --- .../ProjectActionsEventsDetails.tsx | 5 +- .../ProjectActionsEventsDetailsSource.tsx | 22 ++++++ ...ionsEventsDetailsSourceIncomingWebhook.tsx | 75 +++++++++++++++++++ .../ProjectActionsEventsModal.tsx | 4 +- frontend/src/interfaces/action.ts | 2 +- 5 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails.tsx/ProjectActionsEventsDetailsSource/ProjectActionsEventsDetailsSource.tsx create mode 100644 frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails.tsx/ProjectActionsEventsDetailsSource/ProjectActionsEventsDetailsSourceIncomingWebhook.tsx diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails.tsx/ProjectActionsEventsDetails.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails.tsx/ProjectActionsEventsDetails.tsx index 453b562006e8..c1ef8af68182 100644 --- a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails.tsx/ProjectActionsEventsDetails.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsDetails.tsx/ProjectActionsEventsDetails.tsx @@ -1,6 +1,7 @@ import { Alert, styled } from '@mui/material'; import { IActionSetEvent } from 'interfaces/action'; import { ProjectActionsEventsDetailsAction } from './ProjectActionsEventsDetailsAction'; +import { ProjectActionsEventsDetailsSource } from './ProjectActionsEventsDetailsSource/ProjectActionsEventsDetailsSource'; const StyledDetails = styled('div')(({ theme }) => ({ display: 'flex', @@ -26,9 +27,9 @@ export const ProjectActionsEventsDetails = ({ {stateText} - {/* */} + /> {actions.map((action, i) => ( { + 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 000000000000..50e37d3589a7 --- /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 index ce3e57f9c7c2..cc72039c7b00 100644 --- a/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsModal.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectActions/ProjectActionsTable/ProjectActionsEventsModal/ProjectActionsEventsModal.tsx @@ -122,10 +122,10 @@ export const ProjectActionsEventsModal = ({ ]} sidePanelHeader='Details' renderContent={ProjectActionsEventsDetails} - renderItem={({ state }, children) => { + renderItem={({ id, state }, children) => { if (state === 'failed') { return ( - + {children} ); diff --git a/frontend/src/interfaces/action.ts b/frontend/src/interfaces/action.ts index 532a1d04f7d7..bb0094f70b1a 100644 --- a/frontend/src/interfaces/action.ts +++ b/frontend/src/interfaces/action.ts @@ -29,7 +29,7 @@ export interface IAction { export type ObservableEventSource = 'incoming-webhook'; -interface IObservableEvent { +export interface IObservableEvent { id: number; source: ObservableEventSource; sourceId: number; From c52a0d29845b4b2cf74e12e7ca69290fb61f7d50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Tue, 27 Feb 2024 10:53:21 +0000 Subject: [PATCH 3/3] fix: only show events link if editing --- .../IncomingWebhooksModal/IncomingWebhooksModal.tsx | 6 +++++- .../ProjectActionsModal/ProjectActionsModal.tsx | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/src/component/incomingWebhooks/IncomingWebhooksModal/IncomingWebhooksModal.tsx b/frontend/src/component/incomingWebhooks/IncomingWebhooksModal/IncomingWebhooksModal.tsx index 375f1369571d..a9dd8ee0e237 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', @@ -163,7 +164,10 @@ export const ProjectActionsModal = ({ > {title} - View events + View events} + />