diff --git a/packages/backend/src/graphql/queries/get-execution-steps.ts b/packages/backend/src/graphql/queries/get-execution-steps.ts index 7c419627..33fdd3f5 100644 --- a/packages/backend/src/graphql/queries/get-execution-steps.ts +++ b/packages/backend/src/graphql/queries/get-execution-steps.ts @@ -19,7 +19,8 @@ const getExecutionSteps = async ( const executionSteps = execution .$relatedQuery('executionSteps') - .orderBy('created_at', 'desc'); + .withGraphFetched('step') + .orderBy('created_at', 'asc'); return paginate(executionSteps, params.limit, params.offset); }; diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 34c04984..a502fd83 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -163,9 +163,12 @@ type ExecutionStep { id: String executionId: String stepId: String + step: Step status: String dataIn: JSONObject dataOut: JSONObject + createdAt: String + updatedAt: String } type Field { diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index ba814e5b..1e26b71d 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -20,10 +20,12 @@ export interface IConnection { export interface IExecutionStep { id: string; executionId: string; - stepId: string; + stepId: IStep["id"]; + step: IStep; dataIn: IJSONObject; dataOut: IJSONObject; status: string; + createdAt: string; } export interface IExecution { diff --git a/packages/web/package.json b/packages/web/package.json index 7e19adde..4d5ed509 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "description": "> TODO: description", "dependencies": { + "@alenaksu/json-viewer": "^1.0.0", "@apollo/client": "^3.4.15", "@automatisch/types": "0.1.0", "@emotion/react": "^11.4.1", diff --git a/packages/web/src/components/AppConnectionRow/index.tsx b/packages/web/src/components/AppConnectionRow/index.tsx index 412b4d98..d7d8d08c 100644 --- a/packages/web/src/components/AppConnectionRow/index.tsx +++ b/packages/web/src/components/AppConnectionRow/index.tsx @@ -11,15 +11,15 @@ import ErrorIcon from '@mui/icons-material/Error'; import { useSnackbar } from 'notistack'; import { DateTime } from 'luxon'; +import type { IConnection } from '@automatisch/types'; import { DELETE_CONNECTION } from 'graphql/mutations/delete-connection'; import { TEST_CONNECTION } from 'graphql/queries/test-connection'; import ConnectionContextMenu from 'components/AppConnectionContextMenu'; import useFormatMessage from 'hooks/useFormatMessage'; -import type { Connection } from 'types/connection'; import { CardContent, Typography } from './style'; type AppConnectionRowProps = { - connection: Connection; + connection: IConnection; } const countTranslation = (value: React.ReactNode) => ( diff --git a/packages/web/src/components/AppConnections/index.tsx b/packages/web/src/components/AppConnections/index.tsx index 42fc945f..82e3ffa8 100644 --- a/packages/web/src/components/AppConnections/index.tsx +++ b/packages/web/src/components/AppConnections/index.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; import { useQuery } from '@apollo/client'; +import type { IConnection } from '@automatisch/types'; import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections'; import AppConnectionRow from 'components/AppConnectionRow'; import NoResultFound from 'components/NoResultFound'; import useFormatMessage from 'hooks/useFormatMessage'; -import type { Connection } from 'types/connection'; import * as URLS from 'config/urls'; type AppConnectionsProps = { @@ -16,7 +16,7 @@ export default function AppConnections(props: AppConnectionsProps): React.ReactE const { appKey } = props; const formatMessage = useFormatMessage(); const { data } = useQuery(GET_APP_CONNECTIONS, { variables: { key: appKey } }); - const appConnections: Connection[] = data?.getApp?.connections || []; + const appConnections: IConnection[] = data?.getApp?.connections || []; const hasConnections = appConnections?.length; @@ -31,7 +31,7 @@ export default function AppConnections(props: AppConnectionsProps): React.ReactE return ( <> - {appConnections.map((appConnection: Connection) => ( + {appConnections.map((appConnection: IConnection) => ( ))} diff --git a/packages/web/src/components/ExecutionRow/index.tsx b/packages/web/src/components/ExecutionRow/index.tsx index 004ef786..01db1847 100644 --- a/packages/web/src/components/ExecutionRow/index.tsx +++ b/packages/web/src/components/ExecutionRow/index.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { Link } from 'react-router-dom'; import Card from '@mui/material/Card'; import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; import CardActionArea from '@mui/material/CardActionArea'; import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; import { DateTime } from 'luxon'; @@ -21,23 +22,23 @@ export default function ExecutionRow(props: ExecutionRowProps): React.ReactEleme const { flow } = execution; return ( - + - {flow.name} - + {getHumanlyDate(parseInt(execution.createdAt, 10))} - + theme.palette.primary.main }} /> diff --git a/packages/web/src/components/ExecutionStep/index.tsx b/packages/web/src/components/ExecutionStep/index.tsx new file mode 100644 index 00000000..d7d2bb2a --- /dev/null +++ b/packages/web/src/components/ExecutionStep/index.tsx @@ -0,0 +1,95 @@ +import * as React from 'react'; +import { useQuery } from '@apollo/client'; +import Stack from '@mui/material/Stack'; +import ErrorIcon from '@mui/icons-material/Error'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; +import type { IApp, IJSONObject, IExecutionStep, IStep } from '@automatisch/types'; + +import TabPanel from 'components/TabPanel'; +import JSONViewer from 'components/JSONViewer'; +import AppIcon from 'components/AppIcon'; +import { GET_APPS } from 'graphql/queries/get-apps'; +import useFormatMessage from 'hooks/useFormatMessage'; +import { AppIconWrapper, AppIconStatusIconWrapper, Content, Header, Wrapper } from './style'; + +type ExecutionStepProps = { + collapsed?: boolean; + step: IStep; + index?: number; + executionStep: IExecutionStep; +} + +const validIcon = ; +const errorIcon = ; + +export default function ExecutionStep(props: ExecutionStepProps): React.ReactElement | null { + const { executionStep, index, } = props; + const [activeTabIndex, setActiveTabIndex] = React.useState(0); + const step: IStep = executionStep.step; + const isTrigger = step.type === 'trigger'; + const formatMessage = useFormatMessage(); + const { data } = useQuery(GET_APPS, { variables: { onlyWithTriggers: isTrigger }}); + const apps: IApp[] = data?.getApps; + const app = apps?.find((currentApp: IApp) => currentApp.key === step.appKey); + + if (!apps) return null; + + const validationStatusIcon = executionStep.status === 'success' ? validIcon : errorIcon; + + return ( + +
+ + + + + + {validationStatusIcon} + + + +
+ + { + isTrigger ? + formatMessage('flowStep.triggerType') : + formatMessage('flowStep.actionType') + } + + + + {step.position}. {app?.name} + +
+
+
+ + + + setActiveTabIndex(tabIndex)}> + + + + + + + + + + + + + + + + + + + +
+ ) +}; diff --git a/packages/web/src/components/ExecutionStep/style.ts b/packages/web/src/components/ExecutionStep/style.ts new file mode 100644 index 00000000..7a7ffaf3 --- /dev/null +++ b/packages/web/src/components/ExecutionStep/style.ts @@ -0,0 +1,42 @@ +import { styled, alpha } from '@mui/material/styles'; +import Card from '@mui/material/Card'; + +export const AppIconWrapper = styled('div')` + position: relative; +`; + +export const AppIconStatusIconWrapper = styled('span')` + position: absolute; + right: 0; + top: 0; + transform: translate(50%, -50%); + display: inline-flex; + + svg { + // to make it distinguishable over an app icon + background: white; + border-radius: 100%; + overflow: hidden; + } +`; + +export const Wrapper = styled(Card)` + width: 100%; + overflow: unset; +`; + +type HeaderProps = { + collapsed?: boolean; +} + +export const Header = styled('div', { shouldForwardProp: prop => prop !== 'collapsed' })` + padding: ${({ theme }) => theme.spacing(2)}; + cursor: ${({ collapsed }) => collapsed ? 'pointer' : 'unset'}; +`; + +export const Content = styled('div')` + border: 1px solid ${({ theme }) => alpha(theme.palette.divider, .8)}; + border-left: none; + border-right: none; + padding: ${({ theme }) => theme.spacing(2, 0)}; +`; diff --git a/packages/web/src/components/FlowRow/index.tsx b/packages/web/src/components/FlowRow/index.tsx index 32d50a95..b220cb90 100644 --- a/packages/web/src/components/FlowRow/index.tsx +++ b/packages/web/src/components/FlowRow/index.tsx @@ -17,7 +17,7 @@ export default function FlowRow(props: FlowRowProps): React.ReactElement { const { flow } = props; return ( - + diff --git a/packages/web/src/components/FlowStep/index.tsx b/packages/web/src/components/FlowStep/index.tsx index 7d1524bb..a3662943 100644 --- a/packages/web/src/components/FlowStep/index.tsx +++ b/packages/web/src/components/FlowStep/index.tsx @@ -144,7 +144,7 @@ export default function FlowStep( - {index}. {app?.name} + {step.position}. {app?.name} diff --git a/packages/web/src/components/JSONViewer/index.tsx b/packages/web/src/components/JSONViewer/index.tsx new file mode 100644 index 00000000..34d3e566 --- /dev/null +++ b/packages/web/src/components/JSONViewer/index.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import '@alenaksu/json-viewer'; + +import type { IJSONObject } from '@automatisch/types'; +import { jsonViewerStyles } from './style'; + +type JSONViewerProps = { + data: IJSONObject; +} +function JSONViewer(props: JSONViewerProps) { + const { data } = props; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const viewerRef = React.useRef(null); + + React.useEffect(() => { + if (viewerRef.current){ + viewerRef.current.data = data; + } + }, [data]); + + return ( +
+ {jsonViewerStyles} + + +
+ ); +} + + +export default JSONViewer; diff --git a/packages/web/src/components/JSONViewer/style.tsx b/packages/web/src/components/JSONViewer/style.tsx new file mode 100644 index 00000000..0e3d5ad9 --- /dev/null +++ b/packages/web/src/components/JSONViewer/style.tsx @@ -0,0 +1,29 @@ +import GlobalStyles from '@mui/material/GlobalStyles'; + +export const jsonViewerStyles = ( ({ + 'json-viewer': { + '--background-color': 'transparent', + '--font-family': 'monaco, Consolas, Lucida Console, monospace', + '--font-size': '1rem', + '--indent-size': '1.5em', + '--indentguide-size': '1px', + '--indentguide-style': 'solid', + '--indentguide-color': theme.palette.text.primary, + '--indentguide-color-active': '#666', + '--indentguide': 'var(--indentguide-size) var(--indentguide-style) var(--indentguide-color)', + '--indentguide-active': 'var(--indentguide-size) var(--indentguide-style) var(--indentguide-color-active)', + + /* Types colors */ + '--string-color': theme.palette.text.secondary, + '--number-color': theme.palette.text.primary, + '--boolean-color': theme.palette.text.primary, + '--null-color': theme.palette.text.primary, + '--property-color': theme.palette.text.primary, + + /* Collapsed node preview */ + '--preview-color': theme.palette.text.primary, + + /* Search highlight color */ + '--highlight-color': '#6fb3d2', + } +})} />) diff --git a/packages/web/src/components/TestSubstep/index.tsx b/packages/web/src/components/TestSubstep/index.tsx index 66da2cb7..1738eef3 100644 --- a/packages/web/src/components/TestSubstep/index.tsx +++ b/packages/web/src/components/TestSubstep/index.tsx @@ -4,6 +4,7 @@ import Collapse from '@mui/material/Collapse'; import ListItem from '@mui/material/ListItem'; import Button from '@mui/material/Button'; +import JSONViewer from 'components/JSONViewer'; import { EXECUTE_FLOW } from 'graphql/mutations/execute-flow'; import FlowSubstepTitle from 'components/FlowSubstepTitle'; import type { IStep, ISubstep } from '@automatisch/types'; @@ -58,9 +59,7 @@ function TestSubstep(props: TestSubstepProps): React.ReactElement { {response && ( -
-              {JSON.stringify(response, null, 2)}
-            
+ )}