From 3839bcc365b9b711a3b04652a6f0943c74cf9efa Mon Sep 17 00:00:00 2001 From: Jonathan Kilzi Date: Tue, 28 Nov 2023 18:21:23 +0200 Subject: [PATCH] feat(orchestrator): orchestrator plugin entry page & workflows table (#31) https://issues.redhat.com/browse/FLPATH-686 https://issues.redhat.com/browse/FLPATH-682 --- plugins/orchestrator/package.json | 4 +- .../src/__fixtures__/fakeProcessInstance.ts | 17 ++ .../src/__fixtures__/fakeWorkflowItem.ts | 246 ++++++++++++++++++ .../src/api/MockOrchestratorClient.ts | 128 +++++++++ .../orchestrator/src/components/Router.tsx | 2 + .../components/next/BaseOrchestratorPage.tsx | 21 ++ .../next/OrchestratorPage.stories.tsx | 60 +++++ .../src/components/next/OrchestratorPage.tsx | 76 ++++++ .../next/WorkflowsTable.stories.tsx | 58 +++++ .../src/components/next/WorkflowsTable.tsx | 197 ++++++++++++++ plugins/orchestrator/src/routes.ts | 6 + yarn.lock | 38 +++ 12 files changed, 852 insertions(+), 1 deletion(-) create mode 100644 plugins/orchestrator/src/__fixtures__/fakeProcessInstance.ts create mode 100644 plugins/orchestrator/src/__fixtures__/fakeWorkflowItem.ts create mode 100644 plugins/orchestrator/src/api/MockOrchestratorClient.ts create mode 100644 plugins/orchestrator/src/components/next/BaseOrchestratorPage.tsx create mode 100644 plugins/orchestrator/src/components/next/OrchestratorPage.stories.tsx create mode 100644 plugins/orchestrator/src/components/next/OrchestratorPage.tsx create mode 100644 plugins/orchestrator/src/components/next/WorkflowsTable.stories.tsx create mode 100644 plugins/orchestrator/src/components/next/WorkflowsTable.tsx diff --git a/plugins/orchestrator/package.json b/plugins/orchestrator/package.json index f13d85fe97..59c87297fa 100644 --- a/plugins/orchestrator/package.json +++ b/plugins/orchestrator/package.json @@ -59,9 +59,9 @@ "@rjsf/utils": "5.7.3", "@rjsf/validator-ajv8": "5.7.3", "json-schema": "^0.4.0", + "moment": "^2.29.0", "react-hook-form": "^7.45.1", "react-json-view": "^1.21.3", - "moment": "^2.29.0", "react-moment": "^1.1.3", "react-use": "^17.4.0", "vscode-languageserver-types": "^3.16.0" @@ -69,7 +69,9 @@ "devDependencies": { "@backstage/cli": "0.23.0", "@backstage/dev-utils": "1.0.22", + "@backstage/test-utils": "^1.4.4", "@janus-idp/cli": "1.4.0", + "@storybook/react": "^7.5.3", "@types/json-schema": "^7.0.12" }, "peerDependencies": { diff --git a/plugins/orchestrator/src/__fixtures__/fakeProcessInstance.ts b/plugins/orchestrator/src/__fixtures__/fakeProcessInstance.ts new file mode 100644 index 0000000000..7018b03d65 --- /dev/null +++ b/plugins/orchestrator/src/__fixtures__/fakeProcessInstance.ts @@ -0,0 +1,17 @@ +import { + ProcessInstance, + ProcessInstanceState, +} from '@janus-idp/backstage-plugin-orchestrator-common'; + +export const fakeProcessInstance: ProcessInstance = { + id: '12f767c1-9002-43af-9515-62a72d0eafb2', + processName: 'Workflow name', + processId: 'workflow_actions', + state: ProcessInstanceState.Error, + start: new Date('2023-11-16T10:50:34.346Z'), + lastUpdate: new Date('2023-11-16T10:50:34.5Z'), + nodes: [], + variables: {}, + end: new Date('2023-11-16T10:50:34.5Z'), + endpoint: '', +}; diff --git a/plugins/orchestrator/src/__fixtures__/fakeWorkflowItem.ts b/plugins/orchestrator/src/__fixtures__/fakeWorkflowItem.ts new file mode 100644 index 0000000000..ef2b329735 --- /dev/null +++ b/plugins/orchestrator/src/__fixtures__/fakeWorkflowItem.ts @@ -0,0 +1,246 @@ +import { WorkflowItem } from '@janus-idp/backstage-plugin-orchestrator-common'; + +export const fakeWorkflowItem: WorkflowItem = { + uri: 'quarkus-backend-workflow-ci-switch.sw.yaml', + definition: { + id: 'quarkus-backend-workflow-ci-switch', + version: '1.0', + specVersion: '0.8', + name: '[WF] Create a starter Quarkus Backend application with a CI pipeline - CI Switch', + description: + '[WF] Create a starter Quarkus Backend application with a CI pipeline - CI Switch', + functions: [ + { + name: 'runActionTemplateFetch', + operation: 'specs/actions-openapi.json#fetch:template', + }, + { + name: 'runActionPublishGithub', + operation: 'specs/actions-openapi.json#publish:github', + }, + { + name: 'runActionCatalogRegister', + operation: 'specs/actions-openapi.json#catalog:register', + }, + ], + start: 'Generating the Source Code Component', + states: [ + { + name: 'Generating the Source Code Component', + type: 'operation', + actionMode: 'sequential', + actions: [ + { + name: 'Fetch Template Action - Source Code', + functionRef: { + refName: 'runActionTemplateFetch', + arguments: { + url: 'https://github.com/janus-idp/software-templates/tree/main/templates/github/quarkus-backend/skeleton', + values: { + githubOrg: '.githubOrg', + repoName: '.repoName', + owner: '.owner', + system: '.system', + applicationType: 'api', + description: '.description', + namespace: '.namespace', + imageUrl: 'imageUrl', + imageBuilder: '.imageBuilder', + imageRepository: '.imageRepository', + port: '.port', + ci: '.ci', + groupId: '.groupId', + artifactId: '.artifactId', + javaPackageName: '.javaPackageName', + }, + }, + }, + actionDataFilter: { + toStateData: '.actionFetchTemplateSourceCodeResult', + }, + }, + ], + transition: 'Generating the CI Component', + }, + { + name: 'Generating the CI Component', + type: 'switch', + dataConditions: [ + { + condition: '${ .ci == "github" }', + transition: 'Generating the CI Component - GitHub', + }, + { + condition: '${ .ci == "tekton" }', + transition: 'Generating the CI Component - Tekton', + }, + ], + defaultCondition: { + transition: 'Generating the CI Component - GitHub', + }, + }, + { + name: 'Generating the CI Component - GitHub', + type: 'operation', + actionMode: 'sequential', + actions: [ + { + name: 'Run Template Fetch Action - CI - GitHub', + functionRef: { + refName: 'runActionTemplateFetch', + arguments: { + url: 'https://github.com/janus-idp/software-templates/tree/main/skeletons/github-actions', + copyWithoutTemplating: ['".github/workflows/"'], + values: { + githubOrg: '.githubOrg', + repoName: '.repoName', + owner: '.owner', + system: '.system', + applicationType: 'api', + description: '.description', + namespace: '.namespace', + imageUrl: 'imageUrl', + imageBuilder: '.imageBuilder', + imageRepository: '.imageRepository', + port: '.port', + ci: '.ci', + groupId: '.groupId', + artifactId: '.artifactId', + javaPackageName: '.javaPackageName', + }, + }, + }, + actionDataFilter: { + toStateData: '.actionTemplateFetchCIResult', + }, + }, + ], + transition: 'Generating the Catalog Info Component', + }, + { + name: 'Generating the CI Component - Tekton', + type: 'operation', + actionMode: 'sequential', + actions: [ + { + name: 'Run Template Fetch Action - CI - Tekton', + functionRef: { + refName: 'runActionTemplateFetch', + arguments: { + url: 'https://github.com/janus-idp/software-templates/tree/main/skeletons/tekton', + copyWithoutTemplating: ['".tekton/workflows/"'], + values: { + githubOrg: '.githubOrg', + repoName: '.repoName', + owner: '.owner', + system: '.system', + applicationType: 'api', + description: '.description', + namespace: '.namespace', + imageUrl: 'imageUrl', + imageBuilder: '.imageBuilder', + imageRepository: '.imageRepository', + port: '.port', + ci: '.ci', + groupId: '.groupId', + artifactId: '.artifactId', + javaPackageName: '.javaPackageName', + }, + }, + }, + actionDataFilter: { + toStateData: '.actionTemplateFetchCIResult', + }, + }, + ], + transition: 'Generating the Catalog Info Component', + }, + { + name: 'Generating the Catalog Info Component', + type: 'operation', + actions: [ + { + name: 'Fetch Template Action - Catalog Info', + functionRef: { + refName: 'runActionTemplateFetch', + arguments: { + url: 'https://github.com/janus-idp/software-templates/tree/main/skeletons/catalog-info', + values: { + githubOrg: '.githubOrg', + repoName: '.repoName', + owner: '.owner', + system: '.system', + applicationType: 'api', + description: '.description', + namespace: '.namespace', + imageUrl: 'imageUrl', + imageBuilder: '.imageBuilder', + imageRepository: '.imageRepository', + port: '.port', + ci: '.ci', + groupId: '.groupId', + artifactId: '.artifactId', + javaPackageName: '.javaPackageName', + }, + }, + }, + actionDataFilter: { + toStateData: '.actionFetchTemplateCatalogInfoResult', + }, + }, + ], + transition: 'Publishing to the Source Code Repository', + }, + { + name: 'Publishing to the Source Code Repository', + type: 'operation', + actionMode: 'sequential', + actions: [ + { + name: 'Publish Github Action', + functionRef: { + refName: 'runActionPublishGithub', + arguments: { + allowedHosts: ['"github.com"'], + description: 'Workflow Action', + repoUrl: + '"github.com?owner=" + .githubOrg + "&repo=" + .repoName', + defaultBranch: '.defaultBranch', + gitCommitMessage: '.gitCommitMessage', + allowAutoMerge: true, + allowRebaseMerge: true, + }, + }, + actionDataFilter: { + toStateData: '.actionPublishResult', + }, + }, + ], + transition: 'Registering the Catalog Info Component', + }, + { + name: 'Registering the Catalog Info Component', + type: 'operation', + actionMode: 'sequential', + actions: [ + { + name: 'Catalog Register Action', + functionRef: { + refName: 'runActionCatalogRegister', + arguments: { + repoContentsUrl: '.actionPublishResult.repoContentsUrl', + catalogInfoPath: '"/catalog-info.yaml"', + }, + }, + actionDataFilter: { + toStateData: '.actionCatalogRegisterResult', + }, + }, + ], + end: true, + }, + ], + dataInputSchema: + 'schemas/quarkus-backend-workflow-ci-switch__main_schema.json', + }, +}; diff --git a/plugins/orchestrator/src/api/MockOrchestratorClient.ts b/plugins/orchestrator/src/api/MockOrchestratorClient.ts new file mode 100644 index 0000000000..17af50031f --- /dev/null +++ b/plugins/orchestrator/src/api/MockOrchestratorClient.ts @@ -0,0 +1,128 @@ +import { JsonValue } from '@backstage/types'; + +import { + Job, + ProcessInstance, + WorkflowDataInputSchemaResponse, + WorkflowExecutionResponse, + WorkflowItem, + WorkflowListResult, + WorkflowSpecFile, +} from '@janus-idp/backstage-plugin-orchestrator-common'; + +import { OrchestratorApi } from './api'; + +export interface MockOrchestratorApiData { + createWorkflowDefinitionResponse: ReturnType< + OrchestratorApi['createWorkflowDefinition'] + >; + deleteWorkflowDefinitionResponse: ReturnType< + OrchestratorApi['deleteWorkflowDefinition'] + >; + executeWorkflowResponse: ReturnType; + getInstanceResponse: ReturnType; + getInstancesResponse: ReturnType; + getInstanceJobsResponse: ReturnType; + getSpecsResponse: ReturnType; + getWorkflowResponse: ReturnType; + getWorkflowDataInputSchemaResponse: ReturnType< + OrchestratorApi['getWorkflowDataInputSchema'] + >; + listWorkflowsResponse: ReturnType; +} + +export class MockOrchestratorClient implements OrchestratorApi { + private _mockData: Partial; + + constructor(mockData: Partial = {}) { + this._mockData = mockData; + } + + createWorkflowDefinition( + _uri: string, + _content?: string, + ): Promise { + if (!this._mockData.createWorkflowDefinitionResponse) { + throw new Error(`[createWorkflowDefinition]: No mock data available`); + } + + return Promise.resolve(this._mockData.createWorkflowDefinitionResponse); + } + + deleteWorkflowDefinition(_workflowId: string): Promise { + if (!this._mockData.deleteWorkflowDefinitionResponse) { + throw new Error(`[deleteWorkflowDefinition]: No mock data available`); + } + + return Promise.resolve(this._mockData.deleteWorkflowDefinitionResponse); + } + + executeWorkflow(_args: { + workflowId: string; + parameters: Record; + }): Promise { + if (!this._mockData.executeWorkflowResponse) { + throw new Error(`[executeWorkflow]: No mock data available`); + } + + return Promise.resolve(this._mockData.executeWorkflowResponse); + } + + getInstance(_instanceId: string): Promise { + if (!this._mockData.getInstanceResponse) { + throw new Error(`[getInstance]: No mock data available`); + } + + return Promise.resolve(this._mockData.getInstanceResponse); + } + + getInstanceJobs(_instanceId: string): Promise { + if (!this._mockData.getInstanceJobsResponse) { + throw new Error(`[getInstanceJobs]: No mock data available`); + } + + return Promise.resolve(this._mockData.getInstanceJobsResponse); + } + + getInstances(): Promise { + if (!this._mockData.getInstancesResponse) { + throw new Error(`[getInstances]: No mock data available`); + } + + return Promise.resolve(this._mockData.getInstancesResponse); + } + + getSpecs(): Promise { + if (!this._mockData.getSpecsResponse) { + throw new Error(`[getSpecs]: No mock data available`); + } + + return Promise.resolve(this._mockData.getSpecsResponse); + } + + getWorkflow(_workflowId: string): Promise { + if (!this._mockData.getWorkflowResponse) { + throw new Error(`[getWorkflow]: No mock data available`); + } + + return Promise.resolve(this._mockData.getWorkflowResponse); + } + + getWorkflowDataInputSchema( + _workflowId: string, + ): Promise { + if (!this._mockData.getWorkflowDataInputSchemaResponse) { + throw new Error(`[getWorkflowDataInputSchema]: No mock data available`); + } + + return Promise.resolve(this._mockData.getWorkflowDataInputSchemaResponse); + } + + listWorkflows(): Promise { + if (!this._mockData.listWorkflowsResponse) { + throw new Error(`[listWorkflows]: No mock data available`); + } + + return Promise.resolve(this._mockData.listWorkflowsResponse); + } +} diff --git a/plugins/orchestrator/src/components/Router.tsx b/plugins/orchestrator/src/components/Router.tsx index 09b6ab2087..0cd7d9569e 100644 --- a/plugins/orchestrator/src/components/Router.tsx +++ b/plugins/orchestrator/src/components/Router.tsx @@ -13,6 +13,7 @@ import { import { CreateWorkflowPage } from './CreateWorkflowPage'; import { ExecuteWorkflowPage } from './ExecuteWorkflowPage'; import { NewWorkflowViewerPage } from './NewWorkflowViewerPage'; +import { OrchestratorPage as OrchestratorPageNext } from './next/OrchestratorPage'; import { OrchestratorPage } from './OrchestratorPage'; import { WorkflowDefinitionViewerPage } from './WorkflowDefinitionViewerPage'; import { WorkflowInstancesViewerPage } from './WorkflownstancesViewerPage'; @@ -46,6 +47,7 @@ export const Router = () => { path={executeWorkflowRouteRef.path} element={} /> + } /> ); }; diff --git a/plugins/orchestrator/src/components/next/BaseOrchestratorPage.tsx b/plugins/orchestrator/src/components/next/BaseOrchestratorPage.tsx new file mode 100644 index 0000000000..424891a32b --- /dev/null +++ b/plugins/orchestrator/src/components/next/BaseOrchestratorPage.tsx @@ -0,0 +1,21 @@ +import React, { PropsWithChildren } from 'react'; + +import { Content, Header, Page } from '@backstage/core-components'; + +export interface BaseOrchestratorProps { + title?: string; + subtitle?: string; +} + +export const BaseOrchestratorPage = ({ + title, + subtitle, + children, +}: PropsWithChildren) => { + return ( + +
+ {children} + + ); +}; diff --git a/plugins/orchestrator/src/components/next/OrchestratorPage.stories.tsx b/plugins/orchestrator/src/components/next/OrchestratorPage.stories.tsx new file mode 100644 index 0000000000..28a8b67042 --- /dev/null +++ b/plugins/orchestrator/src/components/next/OrchestratorPage.stories.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { Route, Routes } from 'react-router-dom'; + +import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils'; + +import { Meta, StoryObj } from '@storybook/react'; + +import { fakeProcessInstance } from '../../__fixtures__/fakeProcessInstance'; +import { fakeWorkflowItem } from '../../__fixtures__/fakeWorkflowItem'; +import { orchestratorApiRef } from '../../api'; +import { MockOrchestratorClient } from '../../api/MockOrchestratorClient'; +import { orchestratorRootRouteRef } from '../../routes'; +import { OrchestratorPage } from './OrchestratorPage'; + +const meta = { + title: 'Orchestrator/next', + component: OrchestratorPage, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/** This component is used in order to correctly render nested components using the `TabbedLayout.Route` component. */ +const TestRouter: React.FC> = ({ children }) => ( + + {children}} /> + +); + +export const OrchestratorPageStory: Story = { + name: 'OrchestratorPage', + render: args => + wrapInTestApp( + + + + + , + { + mountedRoutes: { + '/orchestrator': orchestratorRootRouteRef, + }, + }, + ), +}; diff --git a/plugins/orchestrator/src/components/next/OrchestratorPage.tsx b/plugins/orchestrator/src/components/next/OrchestratorPage.tsx new file mode 100644 index 0000000000..0617b79a0d --- /dev/null +++ b/plugins/orchestrator/src/components/next/OrchestratorPage.tsx @@ -0,0 +1,76 @@ +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 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 { BaseOrchestratorPage } from './BaseOrchestratorPage'; +import { WorkflowsTable } from './WorkflowsTable'; + +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 ( + + + + <> + {loading ? : null} + {error ? : null} + {isReady ? ( + <> + + + + + + + + + + + + ) : null} + + + + + This is the "Workflows run (aka instances)" tab content. + + + + + ); +}; diff --git a/plugins/orchestrator/src/components/next/WorkflowsTable.stories.tsx b/plugins/orchestrator/src/components/next/WorkflowsTable.stories.tsx new file mode 100644 index 0000000000..486464f831 --- /dev/null +++ b/plugins/orchestrator/src/components/next/WorkflowsTable.stories.tsx @@ -0,0 +1,58 @@ +import React from 'react'; + +import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils'; + +import { Meta, StoryObj } from '@storybook/react'; + +import { fakeProcessInstance } from '../../__fixtures__/fakeProcessInstance'; +import { fakeWorkflowItem } from '../../__fixtures__/fakeWorkflowItem'; +import { orchestratorApiRef } from '../../api'; +import { MockOrchestratorClient } from '../../api/MockOrchestratorClient'; +import { orchestratorRootRouteRef } from '../../routes'; +import { WorkflowsTable } from './WorkflowsTable'; + +const meta = { + title: 'Orchestrator/next', + component: WorkflowsTable, + decorators: [ + Story => + wrapInTestApp( + + + , + { + mountedRoutes: { + '/orchestrator/next': orchestratorRootRouteRef, + }, + }, + ), + ], + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const WorkflowsTableStory: Story = { + name: 'WorkflowsTable', + args: { + items: [], + }, +}; diff --git a/plugins/orchestrator/src/components/next/WorkflowsTable.tsx b/plugins/orchestrator/src/components/next/WorkflowsTable.tsx new file mode 100644 index 0000000000..2c2c3fff18 --- /dev/null +++ b/plugins/orchestrator/src/components/next/WorkflowsTable.tsx @@ -0,0 +1,197 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { Table, TableColumn, TableProps } from '@backstage/core-components'; +import { useApi, useRouteRef } from '@backstage/core-plugin-api'; + +import DeleteForever from '@material-ui/icons/DeleteForever'; +import Edit from '@material-ui/icons/Edit'; +import Pageview from '@material-ui/icons/Pageview'; +import PlayArrow from '@material-ui/icons/PlayArrow'; +import moment from 'moment'; + +import { + ASSESSMENT_WORKFLOW_TYPE, + extractWorkflowFormatFromUri, + WorkflowCategory, + WorkflowItem, +} from '@janus-idp/backstage-plugin-orchestrator-common'; + +import { orchestratorApiRef } from '../../api'; +import { + editWorkflowRouteRef, + executeWorkflowRouteRef, + workflowDefinitionsRouteRef, +} from '../../routes'; + +export interface WorkflowsTableProps { + items: WorkflowItem[]; +} + +export const WorkflowsTable = ({ items }: WorkflowsTableProps) => { + const orchestratorApi = useApi(orchestratorApiRef); + const navigate = useNavigate(); + const definitionLink = useRouteRef(workflowDefinitionsRouteRef); + const executeWorkflowLink = useRouteRef(executeWorkflowRouteRef); + const editLink = useRouteRef(editWorkflowRouteRef); + const [data, setData] = useState([]); + + interface Row { + id: string; + name: string; + lastRun: string; + lastRunStatus: string; + type: WorkflowCategory; + components: string; + format: string; + } + + const columns: TableColumn[] = [ + { title: 'Name', field: 'name' }, + { title: 'Last run', field: 'lastRun' }, + { title: 'Last run status', field: 'lastRunStatus' }, + { title: 'Type', field: 'type' }, + { title: 'Components', field: 'components' }, + ]; + + const initTableState = useMemo(() => { + const assessmentExist = !!items.find( + item => + item.definition.annotations?.find( + annotation => annotation === ASSESSMENT_WORKFLOW_TYPE, + ), + ); + return { + filtersOpen: true, + filters: assessmentExist + ? { Type: WorkflowCategory.ASSESSMENT } + : undefined, + }; + }, [items]); + + const getInitialState = useMemo(() => { + return items.map(item => { + return { + id: item.definition.id, + name: item.definition.name ?? '', + lastRun: '', + lastRunStatus: '', + type: item.definition.annotations?.find( + annotation => annotation === ASSESSMENT_WORKFLOW_TYPE, + ) + ? WorkflowCategory.ASSESSMENT + : WorkflowCategory.INFRASTRUCTURE, + components: '---', + format: extractWorkflowFormatFromUri(item.uri), + }; + }); + }, [items]); + + const loadFromInstances = useCallback( + (initData: Row[]) => { + orchestratorApi.getInstances().then(instances => { + const clonedData: Row[] = []; + for (const init_row of initData) { + const row = { ...init_row }; + const instancesById = instances.filter( + instance => instance.processId === row.id, + ); + const lastRunInstance = instancesById.at(-1); + if (lastRunInstance) { + row.lastRun = moment(lastRunInstance.start?.toString()).format( + 'MMMM DD, YYYY', + ); + row.lastRunStatus = lastRunInstance.state; + row.components = instancesById.length.toString(); + } + clonedData.push(row); + } + setData(clonedData); + }); + }, + [orchestratorApi], + ); + + useEffect(() => { + const initData = getInitialState; + setData(initData); + loadFromInstances(initData); + }, [getInitialState, loadFromInstances]); + + const doView = useCallback( + (rowData: Row) => { + navigate( + definitionLink({ workflowId: rowData.id, format: rowData.format }), + ); + }, + [definitionLink, navigate], + ); + + const doExecute = useCallback( + (rowData: Row) => { + navigate(executeWorkflowLink({ workflowId: rowData.id })); + }, + [executeWorkflowLink, navigate], + ); + + const doEdit = useCallback( + (rowData: Row) => { + navigate( + editLink({ workflowId: `${rowData.id}`, format: rowData.format }), + ); + }, + [editLink, navigate], + ); + + const doDelete = useCallback( + (rowData: Row) => { + // Lazy use of window.confirm vs writing a popup. + if ( + // eslint-disable-next-line no-alert + window.confirm( + `Please confirm you want to delete '${rowData.id}' permanently.`, + ) + ) { + orchestratorApi.deleteWorkflowDefinition(rowData.id); + } + }, + [orchestratorApi], + ); + + const actions: TableProps['actions'] = React.useMemo( + () => [ + { + icon: PlayArrow, + tooltip: 'Execute', + onClick: (_, rowData) => doExecute(rowData as Row), + }, + { + icon: Pageview, + tooltip: 'View', + onClick: (_, rowData) => doView(rowData as Row), + }, + { + icon: Edit, + tooltip: 'Edit', + onClick: (_, rowData) => doEdit(rowData as Row), + }, + { + icon: DeleteForever, + tooltip: 'Delete', + onClick: (_, rowData) => doDelete(rowData as Row), + }, + ], + [doDelete, doEdit, doExecute, doView], + ); + + return ( + + ); +}; diff --git a/plugins/orchestrator/src/routes.ts b/plugins/orchestrator/src/routes.ts index b4e143cfba..4933604c40 100644 --- a/plugins/orchestrator/src/routes.ts +++ b/plugins/orchestrator/src/routes.ts @@ -45,3 +45,9 @@ export const executeWorkflowRouteRef = createSubRouteRef({ parent: orchestratorRootRouteRef, path: '/workflows/:workflowId/execute', }); + +export const orchestratorRootNextRouteRef = createSubRouteRef({ + id: 'orchestrator/next', + parent: orchestratorRootRouteRef, + path: '/next', +}); diff --git a/yarn.lock b/yarn.lock index 370dbe3bbf..a9920dc61b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3464,6 +3464,25 @@ zen-observable "^0.10.0" zod "^3.21.4" +"@backstage/core-app-api@^1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@backstage/core-app-api/-/core-app-api-1.11.1.tgz#21584a43cf7bc80171b8f03a3c0db80b1adef424" + integrity sha512-BtYilKoiAnyPjTBZ/LuBIU+nopuleAG/KqfDB36cQ5zAC5cNd2xpXw9NlCQcxJnMV1MjsiqHrfK2lb1S1owXpQ== + dependencies: + "@backstage/config" "^1.1.1" + "@backstage/core-plugin-api" "^1.8.0" + "@backstage/types" "^1.1.1" + "@backstage/version-bridge" "^1.0.7" + "@types/prop-types" "^15.7.3" + "@types/react" "^16.13.1 || ^17.0.0" + history "^5.0.0" + i18next "^22.4.15" + lodash "^4.17.21" + prop-types "^15.7.2" + react-use "^17.2.4" + zen-observable "^0.10.0" + zod "^3.21.4" + "@backstage/core-components@0.13.6", "@backstage/core-components@^0.13.6": version "0.13.6" resolved "https://registry.yarnpkg.com/@backstage/core-components/-/core-components-0.13.6.tgz#c34b9ec741c993db7a63634bc25b9d23e4939e6b" @@ -5167,6 +5186,25 @@ i18next "^22.4.15" zen-observable "^0.10.0" +"@backstage/test-utils@^1.4.4": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@backstage/test-utils/-/test-utils-1.4.5.tgz#c6dd719aff8d41233786026171a98f34c03eb244" + integrity sha512-79ZYGh2gNJRltQ9Mv60P8CZ1IDAQ/lY50ihlLUUohVXXYept3m53z4r68FwRVZl1/XwmXgsdh19beBrrxOA8tQ== + dependencies: + "@backstage/config" "^1.1.1" + "@backstage/core-app-api" "^1.11.1" + "@backstage/core-plugin-api" "^1.8.0" + "@backstage/plugin-permission-common" "^0.7.10" + "@backstage/plugin-permission-react" "^0.4.17" + "@backstage/theme" "^0.4.4" + "@backstage/types" "^1.1.1" + "@material-ui/core" "^4.12.2" + "@material-ui/icons" "^4.9.1" + "@types/react" "^16.13.1 || ^17.0.0" + cross-fetch "^4.0.0" + i18next "^22.4.15" + zen-observable "^0.10.0" + "@backstage/theme@0.4.3", "@backstage/theme@^0.4.3": version "0.4.3" resolved "https://registry.yarnpkg.com/@backstage/theme/-/theme-0.4.3.tgz#58901531da6b2d9b3058f532ee15c866a06ab9af"