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"