feat: add single execution page

This commit is contained in:
Ali BARIN
2022-03-16 17:37:06 +01:00
parent c9bf7c9e21
commit f11f523b30
24 changed files with 372 additions and 36 deletions

View File

@@ -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);
};

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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",

View File

@@ -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) => (

View File

@@ -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) => (
<AppConnectionRow key={appConnection.id} connection={appConnection} />
))}
</>

View File

@@ -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 (
<Link to={URLS.FLOW(flow.id.toString())}>
<Link to={URLS.EXECUTION(execution.id)}>
<Card sx={{ mb: 1 }}>
<CardActionArea>
<CardContent>
<Box
display="flex"
flex={1}
flexDirection="column"
<Stack
justifyContent="center"
alignItems="flex-start"
spacing={1}
>
<Typography variant="h6" noWrap>
{flow.name}
</Typography>
<Typography variant="subtitle1" noWrap>
<Typography variant="caption" noWrap>
{getHumanlyDate(parseInt(execution.createdAt, 10))}
</Typography>
</Box>
</Stack>
<Box>
<ArrowForwardIosIcon sx={{ color: (theme) => theme.palette.primary.main }} />

View File

@@ -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 = <CheckCircleIcon color="success" />;
const errorIcon = <ErrorIcon color="error" />;
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 (
<Wrapper elevation={1}>
<Header>
<Stack direction="row" alignItems="center" gap={2}>
<AppIconWrapper>
<AppIcon url={app?.iconUrl} name={app?.name} />
<AppIconStatusIconWrapper>
{validationStatusIcon}
</AppIconStatusIconWrapper>
</AppIconWrapper>
<div>
<Typography variant="caption">
{
isTrigger ?
formatMessage('flowStep.triggerType') :
formatMessage('flowStep.actionType')
}
</Typography>
<Typography variant="body2">
{step.position}. {app?.name}
</Typography>
</div>
</Stack>
</Header>
<Content sx={{ px: 2 }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={activeTabIndex} onChange={(event, tabIndex) => setActiveTabIndex(tabIndex)}>
<Tab label="Data in" />
<Tab label="Data out" />
<Tab label="Execution step" />
</Tabs>
</Box>
<TabPanel value={activeTabIndex} index={0}>
<JSONViewer data={executionStep.dataIn} />
</TabPanel>
<TabPanel value={activeTabIndex} index={1}>
<JSONViewer data={executionStep.dataOut} />
</TabPanel>
<TabPanel value={activeTabIndex} index={2}>
<JSONViewer data={(executionStep as unknown) as IJSONObject} />
</TabPanel>
</Content>
</Wrapper>
)
};

View File

@@ -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' })<HeaderProps>`
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)};
`;

View File

@@ -17,7 +17,7 @@ export default function FlowRow(props: FlowRowProps): React.ReactElement {
const { flow } = props;
return (
<Link to={URLS.FLOW(flow.id.toString())}>
<Link to={URLS.FLOW(flow.id)}>
<Card sx={{ mb: 1 }}>
<CardActionArea>
<CardContent>

View File

@@ -144,7 +144,7 @@ export default function FlowStep(
</Typography>
<Typography variant="body2">
{index}. {app?.name}
{step.position}. {app?.name}
</Typography>
</div>

View File

@@ -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<any>(null);
React.useEffect(() => {
if (viewerRef.current){
viewerRef.current.data = data;
}
}, [data]);
return (
<div>
{jsonViewerStyles}
<json-viewer ref={viewerRef} />
</div>
);
}
export default JSONViewer;

View File

@@ -0,0 +1,29 @@
import GlobalStyles from '@mui/material/GlobalStyles';
export const jsonViewerStyles = (<GlobalStyles styles={(theme) => ({
'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',
}
})} />)

View File

@@ -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 {
<ListItem sx={{ pt: 2, pb: 3, flexDirection: 'column', alignItems: 'flex-start' }}>
{response && (
<pre style={{ whiteSpace: 'pre-wrap', }}>
{JSON.stringify(response, null, 2)}
</pre>
<JSONViewer data={response} />
)}
<Button

View File

@@ -1,6 +1,8 @@
export const CONNECTIONS = '/connections';
export const EXPLORE = '/explore';
export const EXECUTIONS = '/executions';
export const EXECUTION_PATTERN = '/executions/:executionId';
export const EXECUTION = (executionId: string): string => `/executions/${executionId}`;
export const LOGIN = '/login';

View File

@@ -1,4 +1,5 @@
import { InMemoryCache } from '@apollo/client';
import offsetLimitPagination from './pagination';
const cache = new InMemoryCache({
typePolicies: {
@@ -30,6 +31,11 @@ const cache = new InMemoryCache({
}
}
},
Query: {
fields: {
getExecutionSteps: offsetLimitPagination(['executionId', 'limit']),
}
}
}
});

View File

@@ -0,0 +1,30 @@
import { gql } from '@apollo/client';
export const GET_EXECUTION_STEPS = gql`
query GetExecutionSteps($executionId: String!, $limit: Int!, $offset: Int!) {
getExecutionSteps(executionId: $executionId, limit: $limit, offset: $offset) {
pageInfo {
currentPage
totalPages
}
edges {
node {
id
executionId
status
dataIn
dataOut
createdAt
updatedAt
step {
id
appKey
type
status
position
}
}
}
}
}
`;

View File

@@ -12,6 +12,7 @@ export const GET_FLOW = gql`
key
appKey
status
position
connection {
id
verified

View File

@@ -0,0 +1,52 @@
import * as React from 'react';
import { useParams } from 'react-router-dom';
import { useQuery } from '@apollo/client';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import type { IExecutionStep } from '@automatisch/types';
import ExecutionStep from 'components/ExecutionStep';
import Container from 'components/Container';
import { GET_EXECUTION_STEPS } from 'graphql/queries/get-execution-steps';
type ExecutionParams = {
executionId: string;
};
const EXECUTION_PER_PAGE = 5;
const getLimitAndOffset = (page: number) => ({
limit: EXECUTION_PER_PAGE,
offset: (page - 1) * EXECUTION_PER_PAGE,
});
export default function Execution(): React.ReactElement {
const { executionId } = useParams() as ExecutionParams;
const { data, fetchMore } = useQuery(GET_EXECUTION_STEPS, { variables: { executionId, ...getLimitAndOffset(1) } });
const { edges, pageInfo } = data?.getExecutionSteps || {};
const executionSteps: IExecutionStep[] = edges?.map((edge: { node: IExecutionStep }) => edge.node);
React.useEffect(() => {
if (pageInfo?.currentPage < pageInfo?.totalPages) {
fetchMore({
variables: {
executionId,
...getLimitAndOffset(pageInfo.currentPage + 1),
}
});
}
}, [executionId, fetchMore, pageInfo]);
return (
<Box sx={{ py: 3 }}>
<Container>
<Grid container item sx={{ mb: [2, 5] }} columnSpacing={1.5} rowGap={3}>
{executionSteps?.map((executionStep) => (
<ExecutionStep key={executionStep.id} executionStep={executionStep} step={executionStep.step} />
))}
</Grid>
</Container>
</Box>
);
};

View File

@@ -5,15 +5,14 @@ import Grid from '@mui/material/Grid';
import Container from 'components/Container';
type ApplicationParams = {
type FlowParams = {
flowId: string;
};
export default function Flow(): React.ReactElement {
const { flowId } = useParams() as ApplicationParams;
const { flowId } = useParams() as FlowParams;
return (
<>
<Box sx={{ py: 3 }}>
<Container>
<Grid container>
@@ -23,6 +22,5 @@ export default function Flow(): React.ReactElement {
</Grid>
</Container>
</Box>
</>
);
};

View File

@@ -1 +1,8 @@
/// <reference types="react-scripts" />
declare namespace JSX {
interface IntrinsicElements {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
"json-viewer": any;
}
}

View File

@@ -3,8 +3,9 @@ import Layout from 'components/Layout';
import PublicLayout from 'components/PublicLayout';
import Applications from 'pages/Applications';
import Application from 'pages/Application';
import Flows from 'pages/Flows';
import Executions from 'pages/Executions';
import Execution from 'pages/Execution';
import Flows from 'pages/Flows';
import Flow from 'pages/Flow';
import Explore from 'pages/Explore';
import Login from 'pages/Login';
@@ -15,9 +16,11 @@ export default (
<Routes>
<Route path={URLS.EXECUTIONS} element={<Layout><Executions /></Layout>} />
<Route path={URLS.EXECUTION_PATTERN} element={<Layout><Execution /></Layout>} />
<Route path={URLS.FLOWS} element={<Layout><Flows /></Layout>} />
<Route path={`${URLS.FLOW_PATTERN}/*`} element={<Layout><Flow /></Layout>} />
<Route path={URLS.FLOW_PATTERN} element={<Layout><Flow /></Layout>} />
<Route path={`${URLS.APPS}/*`} element={<Layout><Applications /></Layout>} />

View File

@@ -1,3 +0,0 @@
import type { IConnection } from '@automatisch/types';
export type Connection = IConnection;

View File

@@ -2,6 +2,13 @@
# yarn lockfile v1
"@alenaksu/json-viewer@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@alenaksu/json-viewer/-/json-viewer-1.0.0.tgz#4451d80f581f4eb4e0b4f030441c56ffb5420869"
integrity sha512-eG4vPLGrjAkx3qyM5ub89POG/ySbXu46tz8ANzdFw9JnZaTrKXBwRxTo6zgC+6T5InXU5wpLvecPe/t+1vwlqQ==
dependencies:
lit "^2.2.0"
"@algolia/autocomplete-core@1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.5.0.tgz#6c91c9de7748e9c103846828a58dfe92bd4d6689"
@@ -3112,6 +3119,11 @@
npmlog "^4.1.2"
write-file-atomic "^3.0.3"
"@lit/reactive-element@^1.3.0":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.3.1.tgz#3021ad0fa30a75a41212c5e7f1f169c5762ef8bb"
integrity sha512-nOJARIr3pReqK3hfFCSW2Zg/kFcFsSAlIE7z4a0C9D2dPrgD/YSn3ZP2ET/rxKB65SXyG7jJbkynBRm+tGlacw==
"@mapbox/node-pre-gyp@^1.0.0":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.8.tgz#32abc8a5c624bc4e46c43d84dfb8b26d33a96f58"
@@ -11991,6 +12003,30 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
lit-element@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.2.0.tgz#9c981c55dfd9a8f124dc863edb62cc529d434db7"
integrity sha512-HbE7yt2SnUtg5DCrWt028oaU4D5F4k/1cntAFHTkzY8ZIa8N0Wmu92PxSxucsQSOXlODFrICkQ5x/tEshKi13g==
dependencies:
"@lit/reactive-element" "^1.3.0"
lit-html "^2.2.0"
lit-html@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.2.1.tgz#762f112a8b54eaf0bbae3f516de935a25dcc12d1"
integrity sha512-AiJ/Rs0awjICs2FioTnHSh+Np5dhYSkyRczKy3wKjp8qjLhr1Ov+GiHrUQNdX8ou1LMuznpIME990AZsa/tR8g==
dependencies:
"@types/trusted-types" "^2.0.2"
lit@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/lit/-/lit-2.2.1.tgz#4b679e1d8cb6c7977b64921b1ea3eca7850ca1dd"
integrity sha512-dSe++R50JqrvNGXmI9OE13de1z5U/Y3J2dTm/9GC86vedI8ILoR8ZGnxfThFpvQ9m0lR0qRnIR4IiKj/jDCfYw==
dependencies:
"@lit/reactive-element" "^1.3.0"
lit-element "^3.2.0"
lit-html "^2.2.0"
load-json-file@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"