Skip to content

Commit

Permalink
feat(orchestrator): add workflow runs tab
Browse files Browse the repository at this point in the history
  • Loading branch information
LiorSoffer committed Dec 15, 2024
1 parent d6c3908 commit 8897e33
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 38 deletions.
5 changes: 5 additions & 0 deletions workspaces/orchestrator/.changeset/yellow-poets-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@red-hat-developer-hub/backstage-plugin-orchestrator': patch
---

add workflow tabs - details and runs
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,28 @@ import { Route, Routes } from 'react-router-dom';

import {
executeWorkflowRouteRef,
workflowDefinitionsRouteRef,
workflowInstanceRouteRef,
workflowRouteRef,
} from '../routes';
import { ExecuteWorkflowPage } from './ExecuteWorkflowPage/ExecuteWorkflowPage';
import { OrchestratorPage } from './OrchestratorPage';
import { WorkflowDefinitionViewerPage } from './WorkflowDefinitionViewerPage';
import { WorkflowInstancePage } from './WorkflowInstancePage';
import { WorkflowPage } from './WorkflowPage';

export const Router = () => {
return (
// relative to orchestrator/
<Routes>
<Route path="/*" element={<OrchestratorPage />} />
<Route
path={workflowInstanceRouteRef.path}
element={<WorkflowInstancePage />}
/>
<Route
path={workflowDefinitionsRouteRef.path}
element={<WorkflowDefinitionViewerPage />}
/>
<Route path={`${workflowRouteRef.path}/*`} element={<WorkflowPage />} />
<Route
path={executeWorkflowRouteRef.path}
element={<ExecuteWorkflowPage />}
/>
<Route
path={workflowInstanceRouteRef.path}
element={<WorkflowInstancePage />}
/>
</Routes>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,13 @@ import {

import { orchestratorApiRef } from '../../api';
import { usePermissionArrayDecision } from '../../hooks/usePermissionArray';
import {
executeWorkflowRouteRef,
workflowDefinitionsRouteRef,
} from '../../routes';
import { executeWorkflowRouteRef, workflowRouteRef } from '../../routes';
import { BaseOrchestratorPage } from '../BaseOrchestratorPage';
import { EditorViewKind, WorkflowEditor } from '../WorkflowEditor';
import WorkflowDefinitionDetailsCard from './WorkflowDefinitionDetailsCard';

export const WorkflowDefinitionViewerPage = () => {
const { workflowId, format } = useRouteRefParams(workflowDefinitionsRouteRef);
const { workflowId, format } = useRouteRefParams(workflowRouteRef);
const orchestratorApi = useApi(orchestratorApiRef);

const { loading: loadingPermission, allowed: canRun } =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ import {
} from '@red-hat-developer-hub/backstage-plugin-orchestrator-common';

import { orchestratorApiRef } from '../../api';
import { workflowDefinitionsRouteRef } from '../../routes';
import { workflowRouteRef } from '../../routes';
import { WorkflowEditorLanguageService } from './channel/WorkflowEditorLanguageService';
import { WorkflowEditorLanguageServiceChannelApiImpl } from './channel/WorkflowEditorLanguageServiceChannelApiImpl';

Expand Down Expand Up @@ -111,7 +111,7 @@ const RefForwardingWorkflowEditor: ForwardRefRenderFunction<
const [canRender, setCanRender] = useState(false);
const [ready, setReady] = useState(false);
const navigate = useNavigate();
const viewWorkflowLink = useRouteRef(workflowDefinitionsRouteRef);
const viewWorkflowLink = useRouteRef(workflowRouteRef);

const currentProcessInstance = useMemo(() => {
if (kind !== EditorViewKind.RUNTIME) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';

import { TabbedLayout } from '@backstage/core-components';
import { useRouteRefParams } from '@backstage/core-plugin-api';

import { workflowRouteRef, workflowRunsRouteRef } from '../routes';
import { BaseOrchestratorPage } from './BaseOrchestratorPage';
import { WorkflowDefinitionViewerPage } from './WorkflowDefinitionViewerPage';
import { WorkflowRunsTabContentFiltered } from './WorkflowRunsTabContentFiltered';

export const WorkflowPage = () => {
const { workflowId } = useRouteRefParams(workflowRouteRef);
return (
<BaseOrchestratorPage title={workflowId} noPadding>
<TabbedLayout>
<TabbedLayout.Route path="/" title="Workflow details">
<WorkflowDefinitionViewerPage />
</TabbedLayout.Route>
<TabbedLayout.Route
path={workflowRunsRouteRef.path}
title="Workflow runs"
>
<WorkflowRunsTabContentFiltered />
</TabbedLayout.Route>
</TabbedLayout>
</BaseOrchestratorPage>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { Grid } from '@material-ui/core';
import {
capitalize,
ellipsis,
ProcessInstanceState,
ProcessInstanceStatusDTO,
} from '@red-hat-developer-hub/backstage-plugin-orchestrator-common';

Expand All @@ -44,11 +45,11 @@ import { WorkflowRunDetail } from './WorkflowRunDetail';

const makeSelectItemsFromProcessInstanceValues = () =>
[
ProcessInstanceStatusDTO.Active,
ProcessInstanceStatusDTO.Error,
ProcessInstanceStatusDTO.Completed,
ProcessInstanceStatusDTO.Aborted,
ProcessInstanceStatusDTO.Suspended,
ProcessInstanceState.Active,
ProcessInstanceState.Error,
ProcessInstanceState.Completed,
ProcessInstanceState.Aborted,
ProcessInstanceState.Suspended,
].map(
(status): SelectItem => ({
label: capitalize(status),
Expand Down Expand Up @@ -116,7 +117,7 @@ export const WorkflowRunsTabContent = () => {
(value ?? []).filter(
(row: WorkflowRunDetail) =>
statusSelectorValue === Selector.AllItems ||
row.status === statusSelectorValue,
row.status?.toLocaleLowerCase('en-US') === statusSelectorValue,
),
[statusSelectorValue, value],
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useState } from 'react';

import {
ErrorPanel,
InfoCard,
Link,
SelectItem,
TableColumn,
} from '@backstage/core-components';
import {
useApi,
useRouteRef,
useRouteRefParams,
} from '@backstage/core-plugin-api';

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

import {
capitalize,
ellipsis,
ProcessInstanceState,
ProcessInstanceStatusDTO,
} from '@red-hat-developer-hub/backstage-plugin-orchestrator-common';

import { orchestratorApiRef } from '../api';
import { DEFAULT_TABLE_PAGE_SIZE, VALUE_UNAVAILABLE } from '../constants';
import usePolling from '../hooks/usePolling';
import { workflowInstanceRouteRef, workflowRouteRef } from '../routes';
import { Selector } from './Selector';
import OverrideBackstageTable from './ui/OverrideBackstageTable';
import { mapProcessInstanceToDetails } from './WorkflowInstancePageContent';
import { WorkflowInstanceStatusIndicator } from './WorkflowInstanceStatusIndicator';
import { WorkflowRunDetail } from './WorkflowRunDetail';

const makeSelectItemsFromProcessInstanceValues = () =>
[
ProcessInstanceState.Active,
ProcessInstanceState.Error,
ProcessInstanceState.Completed,
ProcessInstanceState.Aborted,
ProcessInstanceState.Suspended,
].map(
(status): SelectItem => ({
label: capitalize(status),
value: status,
}),
);

export const WorkflowRunsTabContentFiltered = () => {
const { workflowId } = useRouteRefParams(workflowRouteRef);

const orchestratorApi = useApi(orchestratorApiRef);
const workflowInstanceLink = useRouteRef(workflowInstanceRouteRef);
const [statusSelectorValue, setStatusSelectorValue] = useState<string>(
Selector.AllItems,
);

const fetchInstances = React.useCallback(async () => {
// TODO: use pagination with generic permission, skip (or use FE-only) for specific permissions
const instances = await orchestratorApi.listInstances({});
const clonedData: WorkflowRunDetail[] =
instances.data.items?.map(mapProcessInstanceToDetails) || [];
return clonedData.filter(item => item.workflowId === workflowId);
}, [orchestratorApi, workflowId]);

const { loading, error, value } = usePolling(fetchInstances);

const columns = React.useMemo(
(): TableColumn<WorkflowRunDetail>[] => [
{
title: 'ID',
field: 'id',
render: data => (
<Link to={workflowInstanceLink({ instanceId: data.id })}>
{ellipsis(data.id)}
</Link>
),
sorting: false,
},
{
title: 'Workflow name',
field: 'name',
},
{
title: 'Status',
field: 'status',
render: data => (
<WorkflowInstanceStatusIndicator
status={data.status as ProcessInstanceStatusDTO}
/>
),
},
{
title: 'Category',
field: 'category',
render: data => capitalize(data.category ?? VALUE_UNAVAILABLE),
},
{ title: 'Started', field: 'started', defaultSort: 'desc' },
{ title: 'Duration', field: 'duration' },
],
[workflowInstanceLink],
);

const statuses = React.useMemo(makeSelectItemsFromProcessInstanceValues, []);

const filteredData = React.useMemo(
() =>
(value ?? []).filter(
(row: WorkflowRunDetail) =>
statusSelectorValue === Selector.AllItems ||
row.status?.toLocaleLowerCase('en-US') === statusSelectorValue,
),
[statusSelectorValue, value],
);

const selectors = React.useMemo(
() => (
<Grid container alignItems="center">
<Grid item>
<Selector
label="Status"
items={statuses}
onChange={setStatusSelectorValue}
selected={statusSelectorValue}
/>
</Grid>
</Grid>
),
[statusSelectorValue, statuses],
);
const paging = (value?.length || 0) > DEFAULT_TABLE_PAGE_SIZE; // this behavior fits the backstage catalog table behavior https://github.com/backstage/backstage/blob/v1.14.0/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx#L228

return error ? (
<ErrorPanel error={error} />
) : (
<InfoCard noPadding title={selectors}>
<OverrideBackstageTable
title="Workflow Runs"
options={{
paging,
search: true,
pageSize: DEFAULT_TABLE_PAGE_SIZE,
}}
isLoading={loading}
columns={columns}
data={filteredData}
/>
</InfoCard>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,7 @@ import WorkflowOverviewFormatter, {
FormattedWorkflowOverview,
} from '../dataFormatters/WorkflowOverviewFormatter';
import { usePermissionArray } from '../hooks/usePermissionArray';
import {
executeWorkflowRouteRef,
workflowDefinitionsRouteRef,
} from '../routes';
import { executeWorkflowRouteRef, workflowRouteRef } from '../routes';
import OverrideBackstageTable from './ui/OverrideBackstageTable';
import { WorkflowInstanceStatusIndicator } from './WorkflowInstanceStatusIndicator';

Expand Down Expand Up @@ -98,7 +95,7 @@ const usePermittedToViewBatch = (

export const WorkflowsTable = ({ items }: WorkflowsTableProps) => {
const navigate = useNavigate();
const definitionLink = useRouteRef(workflowDefinitionsRouteRef);
const definitionLink = useRouteRef(workflowRouteRef);
const executeWorkflowLink = useRouteRef(executeWorkflowRouteRef);
const [data, setData] = useState<FormattedWorkflowOverview[]>([]);

Expand Down
30 changes: 20 additions & 10 deletions workspaces/orchestrator/plugins/orchestrator/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,40 @@
*/
import { createRouteRef, createSubRouteRef } from '@backstage/core-plugin-api';

// orchestrator page
export const orchestratorRootRouteRef = createRouteRef({
id: 'orchestrator',
});

export const workflowDefinitionsRouteRef = createSubRouteRef({
id: 'orchestrator/workflows',
parent: orchestratorRootRouteRef,
path: '/workflows/:format/:workflowId',
id: 'orchestrator', // display WorkflowsTabContent
});

export const workflowInstancesRouteRef = createSubRouteRef({
id: 'orchestrator/instances',
parent: orchestratorRootRouteRef,
path: '/instances',
path: '/instances', // display WorkflowRunsTabContent
});

// single instance page
export const workflowInstanceRouteRef = createSubRouteRef({
id: 'orchestrator/instances',
parent: orchestratorRootRouteRef,
path: '/instances/:instanceId',
path: '/instances/:instanceId', // display WorkflowInstancePage
});

// workflow page
export const workflowRouteRef = createSubRouteRef({
id: 'orchestrator/workflows',
parent: orchestratorRootRouteRef,
path: '/workflows/:format/:workflowId', // display WorkflowDefinitionViewerPage
});

export const workflowRunsRouteRef = createSubRouteRef({
id: 'orchestrator/workflows',
parent: orchestratorRootRouteRef,
path: '/runs', // display WorkflowRunsTabContentFiltered
});

// execute workflow page
export const executeWorkflowRouteRef = createSubRouteRef({
id: 'orchestrator/workflows/execute',
parent: orchestratorRootRouteRef,
path: '/workflows/:workflowId/execute',
path: '/workflows/:workflowId/execute', // diaplsy ExecuteWorkflowPage
});

0 comments on commit 8897e33

Please sign in to comment.