diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts
index 77bc4da8..c9f1a624 100644
--- a/packages/types/index.d.ts
+++ b/packages/types/index.d.ts
@@ -29,8 +29,10 @@ export interface IExecutionStep {
export interface IExecution {
id: string;
flowId: string;
+ flow: IFlow;
testRun: boolean;
executionSteps: IExecutionStep[];
+ createdAt: string;
}
export interface IStep {
diff --git a/packages/web/package.json b/packages/web/package.json
index 296709ed..7e19adde 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -22,7 +22,7 @@
"clipboard-copy": "^4.0.1",
"graphql": "^15.6.0",
"lodash.template": "^4.5.0",
- "luxon": "^2.2.0",
+ "luxon": "^2.3.1",
"notistack": "^2.0.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
diff --git a/packages/web/src/components/Drawer/index.tsx b/packages/web/src/components/Drawer/index.tsx
index 077c263e..282e44d5 100644
--- a/packages/web/src/components/Drawer/index.tsx
+++ b/packages/web/src/components/Drawer/index.tsx
@@ -6,6 +6,7 @@ import List from '@mui/material/List';
import Divider from '@mui/material/Divider';
import AppsIcon from '@mui/icons-material/Apps';
import SwapCallsIcon from '@mui/icons-material/SwapCalls';
+import HistoryIcon from '@mui/icons-material/History';
import ExploreIcon from '@mui/icons-material/Explore';
import useMediaQuery from '@mui/material/useMediaQuery';
@@ -54,6 +55,13 @@ export default function Drawer(props: SwipeableDrawerProps): React.ReactElement
onClick={closeOnClick}
/>
+ }
+ primary={formatMessage('drawer.executions')}
+ to={URLS.EXECUTIONS}
+ onClick={closeOnClick}
+ />
+
}
primary={formatMessage('drawer.explore')}
diff --git a/packages/web/src/components/ExecutionRow/index.tsx b/packages/web/src/components/ExecutionRow/index.tsx
new file mode 100644
index 00000000..004ef786
--- /dev/null
+++ b/packages/web/src/components/ExecutionRow/index.tsx
@@ -0,0 +1,50 @@
+import * as React from 'react';
+import { Link } from 'react-router-dom';
+import Card from '@mui/material/Card';
+import Box from '@mui/material/Box';
+import CardActionArea from '@mui/material/CardActionArea';
+import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
+import { DateTime } from 'luxon';
+
+import type { IExecution } from '@automatisch/types';
+import * as URLS from 'config/urls';
+import { CardContent, Typography } from './style';
+
+type ExecutionRowProps = {
+ execution: IExecution;
+}
+
+const getHumanlyDate = (timestamp: number) => DateTime.fromMillis(timestamp).toLocaleString(DateTime.DATETIME_MED);
+
+export default function ExecutionRow(props: ExecutionRowProps): React.ReactElement {
+ const { execution } = props;
+ const { flow } = execution;
+
+ return (
+
+
+
+
+
+
+ {flow.name}
+
+
+
+ {getHumanlyDate(parseInt(execution.createdAt, 10))}
+
+
+
+
+ theme.palette.primary.main }} />
+
+
+
+
+
+ );
+}
diff --git a/packages/web/src/components/ExecutionRow/style.ts b/packages/web/src/components/ExecutionRow/style.ts
new file mode 100644
index 00000000..c392df8b
--- /dev/null
+++ b/packages/web/src/components/ExecutionRow/style.ts
@@ -0,0 +1,24 @@
+import { styled } from '@mui/material/styles';
+import MuiCardContent from '@mui/material/CardContent';
+import MuiTypography from '@mui/material/Typography';
+
+export const CardContent = styled(MuiCardContent)(({ theme }) => ({
+ display: 'grid',
+ gridTemplateRows: 'auto',
+ gridTemplateColumns: '1fr auto',
+ gridColumnGap: theme.spacing(2),
+ alignItems: 'center',
+}));
+
+
+export const Typography = styled(MuiTypography)(() => ({
+ display: 'inline-block',
+ width: 500,
+ maxWidth: '70%',
+}));
+
+export const DesktopOnlyBreakline = styled('br')(({ theme }) => ({
+ [theme.breakpoints.down('sm')]: {
+ display: 'none',
+ }
+}));
diff --git a/packages/web/src/components/FlowRow/index.tsx b/packages/web/src/components/FlowRow/index.tsx
index fcfe73ee..32d50a95 100644
--- a/packages/web/src/components/FlowRow/index.tsx
+++ b/packages/web/src/components/FlowRow/index.tsx
@@ -21,7 +21,7 @@ export default function FlowRow(props: FlowRowProps): React.ReactElement {
-
+
{flow.name}
diff --git a/packages/web/src/components/FlowRow/style.ts b/packages/web/src/components/FlowRow/style.ts
index c392df8b..e6260438 100644
--- a/packages/web/src/components/FlowRow/style.ts
+++ b/packages/web/src/components/FlowRow/style.ts
@@ -13,7 +13,7 @@ export const CardContent = styled(MuiCardContent)(({ theme }) => ({
export const Typography = styled(MuiTypography)(() => ({
display: 'inline-block',
- width: 500,
+ width: '100%',
maxWidth: '70%',
}));
diff --git a/packages/web/src/config/urls.ts b/packages/web/src/config/urls.ts
index 9f608e4b..88ef32cb 100644
--- a/packages/web/src/config/urls.ts
+++ b/packages/web/src/config/urls.ts
@@ -1,5 +1,6 @@
export const CONNECTIONS = '/connections';
export const EXPLORE = '/explore';
+export const EXECUTIONS = '/executions';
export const LOGIN = '/login';
diff --git a/packages/web/src/graphql/cache.ts b/packages/web/src/graphql/cache.ts
index 75a4d25d..d30cffd0 100644
--- a/packages/web/src/graphql/cache.ts
+++ b/packages/web/src/graphql/cache.ts
@@ -29,7 +29,7 @@ const cache = new InMemoryCache({
}
}
}
- }
+ },
}
});
diff --git a/packages/web/src/graphql/pagination.ts b/packages/web/src/graphql/pagination.ts
new file mode 100644
index 00000000..ebbd4792
--- /dev/null
+++ b/packages/web/src/graphql/pagination.ts
@@ -0,0 +1,70 @@
+import type { FieldPolicy, Reference } from '@apollo/client';
+
+type KeyArgs = FieldPolicy["keyArgs"];
+
+export type TEdge = {
+ node: TNode;
+} | Reference;
+
+export type TPageInfo = {
+ currentPage: number;
+ totalPages: number;
+};
+
+export type TExisting = Readonly<{
+ edges: TEdge[];
+ pageInfo: TPageInfo;
+}>;
+
+export type TIncoming = {
+ edges: TEdge[];
+ pageInfo: TPageInfo;
+};
+
+export type CustomFieldPolicy = FieldPolicy<
+ TExisting | null,
+ TIncoming | null,
+ TIncoming | null
+>;
+
+
+const makeEmptyData = (): TExisting => {
+ return {
+ edges: [],
+ pageInfo: {
+ currentPage: 1,
+ totalPages: 1,
+ },
+ };
+};
+
+function offsetLimitPagination(
+ keyArgs: KeyArgs = false
+): CustomFieldPolicy {
+ return {
+ keyArgs,
+ merge(existing, incoming, { args }) {
+ if (!existing) {
+ existing = makeEmptyData();
+ }
+
+ if (!incoming || incoming === null) return existing;
+
+ const existingEdges = existing?.edges || [];
+ const incomingEdges = incoming.edges || []
+
+
+ if (args) {
+ const newEdges = [...existingEdges, ...incomingEdges];
+ return {
+ pageInfo: incoming.pageInfo,
+ edges: newEdges,
+ };
+ } else {
+ return existing;
+ }
+ },
+ };
+}
+
+export default offsetLimitPagination;
diff --git a/packages/web/src/graphql/queries/get-executions.ts b/packages/web/src/graphql/queries/get-executions.ts
new file mode 100644
index 00000000..1241ad35
--- /dev/null
+++ b/packages/web/src/graphql/queries/get-executions.ts
@@ -0,0 +1,25 @@
+import { gql } from '@apollo/client';
+
+export const GET_EXECUTIONS = gql`
+ query GetExecutions($limit: Int!, $offset: Int!) {
+ getExecutions(limit: $limit, offset: $offset) {
+ pageInfo {
+ currentPage
+ totalPages
+ }
+ edges {
+ node {
+ id
+ testRun
+ createdAt
+ updatedAt
+ flow {
+ id
+ name
+ active
+ }
+ }
+ }
+ }
+ }
+`;
diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json
index efb9f87d..4578cc75 100644
--- a/packages/web/src/locales/en.json
+++ b/packages/web/src/locales/en.json
@@ -4,6 +4,7 @@
"drawer.dashboard": "Dashboard",
"drawer.flows": "Flows",
"drawer.apps": "My Apps",
+ "drawer.executions": "Executions",
"drawer.explore": "Explore",
"app.connectionCount": "{count} connections",
"app.flowCount": "{count} flows",
@@ -35,5 +36,6 @@
"flowStep.triggerType": "Trigger",
"flowStep.actionType": "Action",
"flows.create": "Create flow",
- "flows.title": "Flows"
-}
\ No newline at end of file
+ "flows.title": "Flows",
+ "executions.title": "Executions"
+}
diff --git a/packages/web/src/pages/Executions/index.tsx b/packages/web/src/pages/Executions/index.tsx
new file mode 100644
index 00000000..4c766837
--- /dev/null
+++ b/packages/web/src/pages/Executions/index.tsx
@@ -0,0 +1,65 @@
+import * as React from 'react';
+import { Link, useSearchParams } from 'react-router-dom';
+import { useQuery } from '@apollo/client';
+import Box from '@mui/material/Box';
+import Grid from '@mui/material/Grid';
+import Pagination from '@mui/material/Pagination';
+import PaginationItem from '@mui/material/PaginationItem';
+import type { IExecution } from '@automatisch/types';
+
+import ExecutionRow from 'components/ExecutionRow';
+import Container from 'components/Container';
+import PageTitle from 'components/PageTitle';
+import useFormatMessage from 'hooks/useFormatMessage'
+import { GET_EXECUTIONS } from 'graphql/queries/get-executions';
+
+const EXECUTION_PER_PAGE = 10;
+
+const getLimitAndOffset = (page: number) => ({
+ limit: EXECUTION_PER_PAGE,
+ offset: (page - 1) * EXECUTION_PER_PAGE,
+});
+
+export default function Executions(): React.ReactElement {
+ const formatMessage = useFormatMessage();
+ const [searchParams, setSearchParams] = useSearchParams();
+ const page = parseInt(searchParams.get('page') || '', 10) || 1;
+
+ const { data, refetch } = useQuery(GET_EXECUTIONS, { variables: getLimitAndOffset(page), fetchPolicy: 'cache-and-network' });
+ const getExecutions = data?.getExecutions || {};
+ const { pageInfo, edges } = getExecutions;
+
+ React.useEffect(() => {
+ refetch(getLimitAndOffset(page));
+ }, [refetch, page])
+
+ const executions: IExecution[] = edges?.map(({ node }: { node: IExecution }) => node);
+
+ return (
+
+
+
+
+ {formatMessage('executions.title')}
+
+
+
+ {executions?.map((execution) => ())}
+
+ {pageInfo && pageInfo.totalPages > 1 && setSearchParams({ page: page.toString() })}
+ renderItem={(item) => (
+
+ )}
+ />}
+
+
+ );
+};
diff --git a/packages/web/src/routes.tsx b/packages/web/src/routes.tsx
index 48fdd132..1a0c3fca 100644
--- a/packages/web/src/routes.tsx
+++ b/packages/web/src/routes.tsx
@@ -4,6 +4,7 @@ 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 Flow from 'pages/Flow';
import Explore from 'pages/Explore';
import Login from 'pages/Login';
@@ -12,6 +13,8 @@ import * as URLS from 'config/urls';
export default (
+ } />
+
} />
} />
diff --git a/yarn.lock b/yarn.lock
index 2c52c00e..b13f606f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -12052,10 +12052,10 @@ lru-cache@^7.3.1:
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.4.0.tgz#2830a779b483e9723e20f26fa5278463c50599d8"
integrity sha512-YOfuyWa/Ee+PXbDm40j9WXyJrzQUynVbgn4Km643UYcWNcrSfRkKL0WaiUcxcIbkXcVTgNpDqSnPXntWXT75cw==
-luxon@^2.2.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.3.0.tgz#bf16a7e642513c2a20a6230a6a41b0ab446d0045"
- integrity sha512-gv6jZCV+gGIrVKhO90yrsn8qXPKD8HYZJtrUDSfEbow8Tkw84T9OnCyJhWvnJIaIF/tBuiAjZuQHUt1LddX2mg==
+luxon@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.3.1.tgz#f276b1b53fd9a740a60e666a541a7f6dbed4155a"
+ integrity sha512-I8vnjOmhXsMSlNMZlMkSOvgrxKJl0uOsEzdGgGNZuZPaS9KlefpE9KV95QFftlJSC+1UyCC9/I69R02cz/zcCA==
lz-string@^1.4.4:
version "1.4.4"