diff --git a/workspaces/orchestrator/.changeset/yellow-poets-fold.md b/workspaces/orchestrator/.changeset/yellow-poets-fold.md new file mode 100644 index 000000000..2f0381ab7 --- /dev/null +++ b/workspaces/orchestrator/.changeset/yellow-poets-fold.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-orchestrator': patch +--- + +add workflow tabs - details and runs diff --git a/workspaces/orchestrator/plugins/orchestrator/src/components/Router.tsx b/workspaces/orchestrator/plugins/orchestrator/src/components/Router.tsx index db1b8908f..0f2c0b4da 100644 --- a/workspaces/orchestrator/plugins/orchestrator/src/components/Router.tsx +++ b/workspaces/orchestrator/plugins/orchestrator/src/components/Router.tsx @@ -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/ } /> - } - /> - } - /> + } /> } /> + } + /> ); }; diff --git a/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowDefinitionViewerPage/WorkflowDefinitionViewerPage.tsx b/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowDefinitionViewerPage/WorkflowDefinitionViewerPage.tsx index 30768389b..70e70e624 100644 --- a/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowDefinitionViewerPage/WorkflowDefinitionViewerPage.tsx +++ b/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowDefinitionViewerPage/WorkflowDefinitionViewerPage.tsx @@ -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 } = diff --git a/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowEditor/WorkflowEditor.tsx b/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowEditor/WorkflowEditor.tsx index 9fd6c3eb3..8b71157eb 100644 --- a/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowEditor/WorkflowEditor.tsx +++ b/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowEditor/WorkflowEditor.tsx @@ -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'; @@ -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) { diff --git a/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowPage.tsx b/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowPage.tsx new file mode 100644 index 000000000..642bdc41c --- /dev/null +++ b/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowPage.tsx @@ -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 ( + + + + + + + + + + + ); +}; diff --git a/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowRunsTabContent.tsx b/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowRunsTabContent.tsx index c634d919a..d9b2760a9 100644 --- a/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowRunsTabContent.tsx +++ b/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowRunsTabContent.tsx @@ -29,6 +29,7 @@ import { Grid } from '@material-ui/core'; import { capitalize, ellipsis, + ProcessInstanceState, ProcessInstanceStatusDTO, } from '@red-hat-developer-hub/backstage-plugin-orchestrator-common'; @@ -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), @@ -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], ); diff --git a/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowRunsTabContentFiltered.tsx b/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowRunsTabContentFiltered.tsx new file mode 100644 index 000000000..06b3b4bfd --- /dev/null +++ b/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowRunsTabContentFiltered.tsx @@ -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( + 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[] => [ + { + title: 'ID', + field: 'id', + render: data => ( + + {ellipsis(data.id)} + + ), + sorting: false, + }, + { + title: 'Workflow name', + field: 'name', + }, + { + title: 'Status', + field: 'status', + render: data => ( + + ), + }, + { + 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( + () => ( + + + + + + ), + [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 ? ( + + ) : ( + + + + ); +}; diff --git a/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowsTable.tsx b/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowsTable.tsx index 52c8c7e44..c7b4e457f 100644 --- a/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowsTable.tsx +++ b/workspaces/orchestrator/plugins/orchestrator/src/components/WorkflowsTable.tsx @@ -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'; @@ -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([]); diff --git a/workspaces/orchestrator/plugins/orchestrator/src/routes.ts b/workspaces/orchestrator/plugins/orchestrator/src/routes.ts index 2a10d7764..ab4af60e8 100644 --- a/workspaces/orchestrator/plugins/orchestrator/src/routes.ts +++ b/workspaces/orchestrator/plugins/orchestrator/src/routes.ts @@ -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 });