Skip to content

Commit

Permalink
feat(orchestrator): add Workflow Run List (janus-idp#30)
Browse files Browse the repository at this point in the history
* feat(orchestrator): add Workflow Run List

FLPATH-693

A component listing running workflows is added.

* chore: move loading logic out of the OrchestratorPage
  • Loading branch information
mareklibra authored and caponetto committed Jan 16, 2024
1 parent d1d3cf5 commit 8748d8b
Show file tree
Hide file tree
Showing 13 changed files with 616 additions and 68 deletions.
75 changes: 67 additions & 8 deletions plugins/orchestrator/src/__fixtures__/fakeProcessInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,74 @@ import {
ProcessInstanceState,
} from '@janus-idp/backstage-plugin-orchestrator-common';

export const fakeProcessInstance: ProcessInstance = {
id: '12f767c1-9002-43af-9515-62a72d0eafb2',
processName: 'Workflow name',
processId: 'workflow_actions',
import { fakeWorkflowItem } from './fakeWorkflowItem';

let id = 10;
const baseDate = new Date('2023-11-16T10:50:34.346Z');
const HOUR = 60 * 60 * 1000;
const DAY = 24 * HOUR;

export const fakeProcessInstance1: ProcessInstance = {
id: `12f767c1-9002-43af-9515-62a72d0eaf${id++}`,
processName: fakeWorkflowItem.definition.name,
processId: fakeWorkflowItem.definition.id,
state: ProcessInstanceState.Error,
start: new Date('2023-11-16T10:50:34.346Z'),
lastUpdate: new Date('2023-11-16T10:50:34.5Z'),
start: baseDate,
end: new Date(baseDate.getTime() + 13 * HOUR),
lastUpdate: new Date(baseDate.getTime() + DAY),
nodes: [],
variables: {},
endpoint: 'enpoint/foo',
serviceUrl: 'service/bar',
source: 'my-source',
};

export const fakeProcessInstance2: ProcessInstance = {
id: `12f767c1-9002-43af-9515-62a72d0eaf${id++}`,
processName: fakeWorkflowItem.definition.name,
processId: fakeWorkflowItem.definition.id,
state: ProcessInstanceState.Completed,
start: new Date(baseDate.getTime() + HOUR),
end: new Date(baseDate.getTime() + DAY),
lastUpdate: new Date(baseDate.getTime() + DAY),
nodes: [],
variables: {},
end: new Date('2023-11-16T10:50:34.5Z'),
endpoint: '',
endpoint: 'enpoint/foo',
serviceUrl: 'service/bar',
source: 'my-source',
};

export const fakeProcessInstance3: ProcessInstance = {
id: `12f767c1-9002-43af-9515-62a72d0eaf${id++}`,
processName: fakeWorkflowItem.definition.name,
processId: fakeWorkflowItem.definition.id,
state: ProcessInstanceState.Active,
start: new Date(baseDate.getTime() + 2 * HOUR),
lastUpdate: new Date(baseDate.getTime() + DAY),
nodes: [],
variables: {},
endpoint: 'enpoint/foo',
serviceUrl: 'service/bar',
source: 'my-source',
};

export const fakeProcessInstance4: ProcessInstance = {
id: `12f767c1-9002-43af-9515-62a72d0eaf${id++}`,
processName: fakeWorkflowItem.definition.name,
processId: fakeWorkflowItem.definition.id,
state: ProcessInstanceState.Suspended,
start: baseDate,
lastUpdate: new Date(baseDate.getTime() + 2 * DAY),
nodes: [],
variables: {},
endpoint: 'enpoint/foo',
serviceUrl: 'service/bar',
source: 'my-source',
};

export const fakeProcessInstances = [
fakeProcessInstance1,
fakeProcessInstance2,
fakeProcessInstance3,
fakeProcessInstance4,
];
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils';

import { Meta, StoryObj } from '@storybook/react';

import { fakeProcessInstance } from '../../__fixtures__/fakeProcessInstance';
import { fakeProcessInstances } from '../../__fixtures__/fakeProcessInstance';
import { fakeWorkflowItem } from '../../__fixtures__/fakeWorkflowItem';
import { orchestratorApiRef } from '../../api';
import { MockOrchestratorClient } from '../../api/MockOrchestratorClient';
Expand Down Expand Up @@ -37,7 +37,7 @@ export const OrchestratorPageStory: Story = {
[
orchestratorApiRef,
new MockOrchestratorClient({
getInstancesResponse: Promise.resolve([fakeProcessInstance]),
getInstancesResponse: Promise.resolve(fakeProcessInstances),
listWorkflowsResponse: Promise.resolve({
limit: 0,
offset: 0,
Expand Down
62 changes: 6 additions & 56 deletions plugins/orchestrator/src/components/next/OrchestratorPage.tsx
Original file line number Diff line number Diff line change
@@ -1,74 +1,24 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { useAsync } from 'react-use';

import {
Content,
Progress,
ResponseErrorPanel,
TabbedLayout,
} from '@backstage/core-components';
import { useApi, useRouteRef } from '@backstage/core-plugin-api';
import { TabbedLayout } from '@backstage/core-components';

import Button from '@material-ui/core/Button/Button';
import Grid from '@material-ui/core/Grid/Grid';

import { WorkflowItem } from '@janus-idp/backstage-plugin-orchestrator-common';

import { orchestratorApiRef } from '../../api';
import { newWorkflowRef, workflowInstancesRouteRef } from '../../routes';
import { workflowInstancesRouteRef } from '../../routes';
import { BaseOrchestratorPage } from './BaseOrchestratorPage';
import { WorkflowsTable } from './WorkflowsTable';
import { WorkflowRunListContent } from './WorkflowRunListContent';
import { WorkflowsTableContent } from './WorkflowsTableContent';

export const OrchestratorPage = () => {
const orchestratorApi = useApi(orchestratorApiRef);
const navigate = useNavigate();
const newWorkflowLink = useRouteRef(newWorkflowRef);
const { value, error, loading } = useAsync(async (): Promise<
WorkflowItem[]
> => {
const data = await orchestratorApi.listWorkflows();
return data.items;
}, []);

const isReady = React.useMemo(() => !loading && !error, [loading, error]);

return (
<BaseOrchestratorPage title="Workflow Orchestrator">
<TabbedLayout>
<TabbedLayout.Route path="/" title="Workflows">
<>
{loading ? <Progress /> : null}
{error ? <ResponseErrorPanel error={error} /> : null}
{isReady ? (
<>
<Grid container direction="row-reverse">
<Grid item>
<Button
variant="contained"
color="primary"
onClick={() => navigate(newWorkflowLink())}
>
Create new
</Button>
</Grid>
</Grid>
<Grid container direction="column">
<Grid item>
<WorkflowsTable items={value ?? []} />
</Grid>
</Grid>
</>
) : null}
</>
<WorkflowsTableContent />
</TabbedLayout.Route>
<TabbedLayout.Route
path={workflowInstancesRouteRef.path}
title="Workflow runs"
>
<Content>
This is the "Workflows run (aka instances)" tab content.
</Content>
<WorkflowRunListContent />
</TabbedLayout.Route>
</TabbedLayout>
</BaseOrchestratorPage>
Expand Down
44 changes: 44 additions & 0 deletions plugins/orchestrator/src/components/next/ProcessInstanceStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';

import { makeStyles } from '@material-ui/core';
import DotIcon from '@material-ui/icons/FiberManualRecord';

import { ProcessInstanceState } from '@janus-idp/backstage-plugin-orchestrator-common';

import { humanizeProcessInstanceState } from './utils';

const useStyles = makeStyles(theme => ({
root: {},
icon: { fontSize: '0.75rem' },
[ProcessInstanceState.Active.toLowerCase()]: {
color: theme.palette.primary.main,
},
[ProcessInstanceState.Aborted.toLowerCase()]: {
color: theme.palette.grey[400],
},
[ProcessInstanceState.Completed.toLowerCase()]: {
color: theme.palette.success.main,
},
[ProcessInstanceState.Error.toLowerCase()]: {
color: theme.palette.error.main,
},
[ProcessInstanceState.Suspended.toLowerCase()]: {
color: theme.palette.warning.main,
},
}));

export const ProcessInstanceStatus = ({ status }: { status: string }) => {
const styles = useStyles();
const colorStyle = styles[status.toLowerCase()];

const icon = colorStyle && (
<DotIcon className={`${styles.icon} ${colorStyle}`} />
);

// TODO(mlibra): Show progress, i.e.: (2/5)
return (
<div className={styles.root}>
{icon} {humanizeProcessInstanceState(status)}
</div>
);
};
82 changes: 82 additions & 0 deletions plugins/orchestrator/src/components/next/StatusSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from 'react';

import { Select, SelectedItems, SelectItem } from '@backstage/core-components';

import { makeStyles, Typography } from '@material-ui/core';

import { ProcessInstanceState } from '@janus-idp/backstage-plugin-orchestrator-common';

import { humanizeProcessInstanceState } from './utils';

const useStyles = makeStyles(theme => ({
root: {
display: 'flex',
alignItems: 'baseline',
'& label + div': {
marginTop: '0px',
},
'& select': {
width: '7rem',
},
},
label: {
color: theme.palette.text.primary,
fontSize: theme.typography.fontSize,
paddingRight: '0.5rem',
fontWeight: 'bold',
},
}));

const ALL = '___all___';

export type StatusSelectorProps = {
onChange: (item?: string) => void;
value?: string;
includeAll?: boolean;
};

export const StatusSelector = ({
onChange,
value,
includeAll = true,
}: StatusSelectorProps) => {
const styles = useStyles();

let statuses: SelectItem[] = [];
if (includeAll) {
statuses = [{ label: 'All', value: ALL }];
}
statuses = [
...statuses,
...[
ProcessInstanceState.Active,
ProcessInstanceState.Completed,
ProcessInstanceState.Error,
ProcessInstanceState.Aborted,
ProcessInstanceState.Suspended,
].map(status => ({
label: humanizeProcessInstanceState(status),
value: status,
})),
];

const selected = statuses?.find(item => item.value === value)?.value;

const handleOnChange = (item: SelectedItems) => {
onChange(item === ALL ? undefined : (item as string));
};

return (
<div className={styles.root}>
<Typography className={styles.label}>Status</Typography>
<Select
onChange={handleOnChange}
items={statuses}
selected={selected}
margin="dense"
native
label=""
/>
</div>
);
};
38 changes: 38 additions & 0 deletions plugins/orchestrator/src/components/next/TableExpandCollapse.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';

import { IconButton, Tooltip } from '@material-ui/core';
import Collapse from '@material-ui/icons/UnfoldLess';
import Expand from '@material-ui/icons/UnfoldMore';

type TableExpandCollapseProps = {
isExpanded: boolean;
setIsExpanded: (value: boolean) => void;
};

export const TableExpandCollapse = ({
isExpanded,
setIsExpanded,
}: TableExpandCollapseProps) => {
const handleExpandCollaspse = () => {
setIsExpanded(!isExpanded);
};

return (
<>
<Tooltip title="Collapse all" placement="top">
<span>
<IconButton onClick={handleExpandCollaspse} disabled={!isExpanded}>
<Collapse />
</IconButton>
</span>
</Tooltip>
<Tooltip title="Expand all" placement="top">
<span>
<IconButton onClick={handleExpandCollaspse} disabled={isExpanded}>
<Expand />
</IconButton>
</span>
</Tooltip>
</>
);
};
Loading

0 comments on commit 8748d8b

Please sign in to comment.