Skip to content

Commit

Permalink
feat(orchestrator): add auto refresh to workflow instance list and de…
Browse files Browse the repository at this point in the history
…tails pages (janus-idp#1081)

* feat(orchestrator): add auto refresh to workflow runs tab and to workflow execution results page

* move usePolling to hooks directory

* improve polling and add unit test for usePolling hook

* use swr for polling

* use existing version of swr
  • Loading branch information
batzionb authored Jan 24, 2024
1 parent bbbefbd commit fc30645
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 25 deletions.
4 changes: 4 additions & 0 deletions plugins/orchestrator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@
"react-json-view": "^1.21.3",
"react-moment": "^1.1.3",
"react-use": "^17.4.0",
"swr": "^2.0.0",
"uuid": "^9.0.1",
"vscode-languageserver-types": "^3.16.0"
},
"devDependencies": {
Expand All @@ -89,6 +91,8 @@
"@backstage/test-utils": "^1.4.4",
"@janus-idp/cli": "1.5.0",
"@storybook/react": "^7.5.3",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "8.0.1",
"@types/json-schema": "^7.0.12",
"css-loader": "^6.5.1",
"file-loader": "^5.0.2",
Expand Down
8 changes: 4 additions & 4 deletions plugins/orchestrator/src/__fixtures__/fakeProcessInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const fakeProcessInstance1: ProcessInstance = {
},
};

export const fakeProcessInstance2: ProcessInstance = {
export const fakeCompletedInstance: ProcessInstance = {
id: `12f767c1-9002-43af-9515-62a72d0eaf${id++}`,
processName: fakeWorkflowOverviewList[1].name,
processId: fakeWorkflowOverviewList[1].workflowId,
Expand All @@ -65,7 +65,7 @@ export const fakeProcessInstance2: ProcessInstance = {
description: 'test description 2',
};

export const fakeProcessInstance3: ProcessInstance = {
export const fakeActiveInstance: ProcessInstance = {
id: `12f767c1-9002-43af-9515-62a72d0eaf${id++}`,
processName: fakeWorkflowOverviewList[2].name,
processId: fakeWorkflowOverviewList[2].workflowId,
Expand Down Expand Up @@ -99,7 +99,7 @@ export const fakeProcessInstance4: ProcessInstance = {

export const fakeProcessInstances = [
fakeProcessInstance1,
fakeProcessInstance2,
fakeProcessInstance3,
fakeCompletedInstance,
fakeActiveInstance,
fakeProcessInstance4,
];
4 changes: 2 additions & 2 deletions plugins/orchestrator/src/api/MockOrchestratorClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface MockOrchestratorApiData {
OrchestratorApi['deleteWorkflowDefinition']
>;
executeWorkflowResponse: () => ReturnType<OrchestratorApi['executeWorkflow']>;
getInstanceResponse: ReturnType<OrchestratorApi['getInstance']>;
getInstanceResponse: () => ReturnType<OrchestratorApi['getInstance']>;
getInstancesResponse: ReturnType<OrchestratorApi['getInstances']>;
getInstanceJobsResponse: ReturnType<OrchestratorApi['getInstanceJobs']>;
getSpecsResponse: ReturnType<OrchestratorApi['getSpecs']>;
Expand Down Expand Up @@ -94,7 +94,7 @@ export class MockOrchestratorClient implements OrchestratorApi {
throw new Error(`[getInstance]: No mock data available`);
}

return Promise.resolve(this._mockData.getInstanceResponse);
return Promise.resolve(this._mockData.getInstanceResponse());
}

getInstanceJobs(_instanceId: string): Promise<Job[]> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import {
WorkflowSpecFile,
} from '@janus-idp/backstage-plugin-orchestrator-common';

import { fakeProcessInstances } from '../__fixtures__/fakeProcessInstance';
import {
fakeActiveInstance,
fakeCompletedInstance,
fakeProcessInstances,
} from '../__fixtures__/fakeProcessInstance';
import { fakeWorkflowItem } from '../__fixtures__/fakeWorkflowItem';
import { fakeWorkflowSpecs } from '../__fixtures__/fakeWorkflowSpecs';
import { orchestratorApiRef } from '../api';
Expand All @@ -25,6 +29,7 @@ const delay = (timeMs: number) => {
};

const getFakeProcessInstance = async (
context: { responseCounter?: number },
instanceId?: string,
): Promise<ProcessInstance> => {
if (instanceId === '__loading__') {
Expand All @@ -36,6 +41,22 @@ const getFakeProcessInstance = async (
return fakeProcessInstances[0];
}

if (instanceId === '__auto_refresh__') {
const ret = !context.responseCounter
? Promise.resolve(fakeActiveInstance)
: Promise.resolve(fakeCompletedInstance);
context.responseCounter = (context.responseCounter || 0) + 1;
return ret;
}

if (instanceId === '__auto_refresh_three_errors__') {
const ret = !context.responseCounter
? Promise.resolve(fakeActiveInstance)
: Promise.reject(new Error('Failed to fetch'));
context.responseCounter = (context.responseCounter || 0) + 1;
return ret;
}

throw new Error('This is an example error for non existing instance');
};

Expand Down Expand Up @@ -70,9 +91,12 @@ const meta = {
getWorkflowResponse: getFakeWorkflowItem(
context.args.instanceId,
),
getInstanceResponse: getFakeProcessInstance(
context.args.instanceId,
),
getInstanceResponse: () => {
return getFakeProcessInstance(
context as { responseCounter?: number },
context.args.instanceId,
);
},
}),
],
]}
Expand Down Expand Up @@ -114,3 +138,17 @@ export const ErrorStory: Story = {
instanceId: '__non_existing_id__',
},
};

export const AutoRefreshErrors: Story = {
name: 'Auto refresh',
args: {
instanceId: '__auto_refresh__',
},
};

export const AutoRefresh3Errors: Story = {
name: 'Auto refresh three errors',
args: {
instanceId: '__auto_refresh_three_errors__',
},
};
32 changes: 20 additions & 12 deletions plugins/orchestrator/src/components/WorkflowInstancePage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react';
import { useAsyncRetry } from 'react-use';

import {
ContentHeader,
Expand All @@ -10,7 +9,11 @@ import { useApi, useRouteRefParams } from '@backstage/core-plugin-api';

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

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

import { orchestratorApiRef } from '../api';
import { SHORT_REFRESH_INTERVAL } from '../constants';
import usePolling from '../hooks/usePolling';
import { workflowInstanceRouteRef } from '../routes';
import { isNonNullable } from '../utils/TypeGuards';
import { BaseOrchestratorPage } from './BaseOrchestratorPage';
Expand All @@ -26,14 +29,19 @@ export const WorkflowInstancePage = ({
workflowInstanceRouteRef,
);

const { loading, error, value, retry } = useAsyncRetry(async () => {
if (!instanceId && !queryInstanceId) {
return undefined;
}
return await orchestratorApi.getInstance(instanceId || queryInstanceId);
}, [orchestratorApi, queryInstanceId]);

const isReady = React.useMemo(() => !loading && !error, [loading, error]);
const { loading, error, value, restart } = usePolling<
ProcessInstance | undefined
>(
async () => {
if (!instanceId && !queryInstanceId) {
return undefined;
}
return await orchestratorApi.getInstance(instanceId || queryInstanceId);
},
SHORT_REFRESH_INTERVAL,
(curValue: ProcessInstance | undefined) =>
!!curValue && curValue.state === 'ACTIVE',
);

const handleAbort = React.useCallback(async () => {
if (value) {
Expand All @@ -45,7 +53,7 @@ export const WorkflowInstancePage = ({
if (yes) {
try {
await orchestratorApi.abortWorkflow(value.id);
retry();
restart();
} catch (e) {
// eslint-disable-next-line no-alert
window.alert(
Expand All @@ -56,7 +64,7 @@ export const WorkflowInstancePage = ({
}
}
}
}, [orchestratorApi, retry, value]);
}, [orchestratorApi, restart, value]);

return (
<BaseOrchestratorPage
Expand All @@ -66,7 +74,7 @@ export const WorkflowInstancePage = ({
>
{loading ? <Progress /> : null}
{error ? <ResponseErrorPanel error={error} /> : null}
{isReady && isNonNullable(value) ? (
{!loading && isNonNullable(value) ? (
<>
<ContentHeader title="">
<Grid container item justifyContent="flex-end" spacing={1}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { useState } from 'react';
import { useAsync } from 'react-use';

import {
ErrorPanel,
Expand All @@ -20,6 +19,7 @@ import {

import { orchestratorApiRef } from '../api';
import { VALUE_UNAVAILABLE } from '../constants';
import usePolling from '../hooks/usePolling';
import { workflowInstanceRouteRef } from '../routes';
import { capitalize, ellipsis } from '../utils/StringUtils';
import { Selector } from './Selector';
Expand Down Expand Up @@ -48,14 +48,14 @@ export const WorkflowRunsTabContent = () => {
Selector.AllItems,
);

const { loading, error, value } = useAsync(async () => {
const { loading, error, value } = usePolling(async () => {
const instances = await orchestratorApi.getInstances();
const clonedData: WorkflowRunDetail[] = instances.map(
mapProcessInstanceToDetails,
);

return clonedData;
}, [orchestratorApi]);
});

const columns = React.useMemo(
(): TableColumn<WorkflowRunDetail>[] => [
Expand Down
2 changes: 2 additions & 0 deletions plugins/orchestrator/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export const VALUE_UNAVAILABLE = '---' as const;
export const SHORT_REFRESH_INTERVAL = 5000;
export const LONG_REFRESH_INTERVAL = 15000;
Loading

0 comments on commit fc30645

Please sign in to comment.