From 7b2bd1bb094cb02a4afe484d6b44cfe161248d27 Mon Sep 17 00:00:00 2001 From: richard wang <58698556+RichardW98@users.noreply.github.com> Date: Tue, 28 Nov 2023 11:10:23 -0500 Subject: [PATCH] feat(orchestrator): handle assessment workflow and make optional the workflowsSource config (#35) * feat: issue FLPATH-591 - Add assessment workflow type and display outputs (#21) * Configure for assessment swf * filter assessment on workflow definition page * workflow columns * workflow columns * workflow columns * workflow filter * feat: Render assessment results supporting dynamic categories * issue FLPATH-657: workflow label * Revert "Configure for assessment swf" This reverts commit b4048e11939bf86dd881536513d8ea4764999d0b. * Revert sonata service port * Add key for workflow options category * Fix workflow execution from choose btn * Fix review comments * address comments * Fix review comment to switch to useRouteRef * fix review comments * fix review comments --------- Co-authored-by: richardwang98 * feat: make optional the workflowsSource config (#25) --------- Co-authored-by: anludke --- plugins/orchestrator-backend/config.d.ts | 2 +- .../provider/OrchestratorEntityProvider.ts | 9 +- .../src/service/SonataFlowService.ts | 13 +- .../src/service/WorkflowService.ts | 7 +- plugins/orchestrator-common/src/constants.ts | 5 +- plugins/orchestrator-common/src/types.ts | 5 + plugins/orchestrator/README.md | 3 + plugins/orchestrator/package.json | 1 + .../OrchestratorPage/OrchestratorPage.tsx | 2 +- .../OrchestratorScaffolderTemplateCard.tsx | 2 +- .../WorkflowDefinitionsListComponent.tsx | 91 ++++++++++++-- .../AssessmentResultViewer.tsx | 112 ++++++++++++++++++ .../WorkflowInstancesViewerPage.tsx | 4 + 13 files changed, 234 insertions(+), 22 deletions(-) create mode 100644 plugins/orchestrator/src/components/WorkflownstancesViewerPage/AssessmentResultViewer.tsx diff --git a/plugins/orchestrator-backend/config.d.ts b/plugins/orchestrator-backend/config.d.ts index 4d50069375..eeeb932fea 100644 --- a/plugins/orchestrator-backend/config.d.ts +++ b/plugins/orchestrator-backend/config.d.ts @@ -23,7 +23,7 @@ export interface Config { /** * Workflows definitions source configurations */ - workflowsSource: + workflowsSource?: | { /** * Remote git repository where workflows definitions are stored diff --git a/plugins/orchestrator-backend/src/provider/OrchestratorEntityProvider.ts b/plugins/orchestrator-backend/src/provider/OrchestratorEntityProvider.ts index 21a72df36d..ee8f2ac577 100644 --- a/plugins/orchestrator-backend/src/provider/OrchestratorEntityProvider.ts +++ b/plugins/orchestrator-backend/src/provider/OrchestratorEntityProvider.ts @@ -18,10 +18,12 @@ import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { Logger } from 'winston'; import { + ASSESSMENT_WORKFLOW_TYPE, default_catalog_environment, default_catalog_owner, orchestrator_service_ready_topic, workflow_type, + WorkflowCategory, WorkflowItem, } from '@janus-idp/backstage-plugin-orchestrator-common'; @@ -154,6 +156,11 @@ export class OrchestratorEntityProvider ): TemplateEntityV1beta3[] { return items.map(i => { const sanitizedId = i.definition.id.replace(/ /g, '_'); + const category: WorkflowCategory = i.definition.annotations?.find( + annotation => annotation === ASSESSMENT_WORKFLOW_TYPE, + ) + ? WorkflowCategory.ASSESSMENT + : WorkflowCategory.INFRASTRUCTURE; return { apiVersion: 'scaffolder.backstage.io/v1beta3', kind: 'Template', @@ -161,7 +168,7 @@ export class OrchestratorEntityProvider name: sanitizedId, title: i.definition.name, description: i.definition.description, - tags: [workflow_type], + tags: [category], annotations: { [ANNOTATION_LOCATION]: `url:${this.sonataFlowServiceUrl}`, [ANNOTATION_ORIGIN_LOCATION]: `url:${this.sonataFlowServiceUrl}`, diff --git a/plugins/orchestrator-backend/src/service/SonataFlowService.ts b/plugins/orchestrator-backend/src/service/SonataFlowService.ts index 60b851b92a..5067a0e36f 100644 --- a/plugins/orchestrator-backend/src/service/SonataFlowService.ts +++ b/plugins/orchestrator-backend/src/service/SonataFlowService.ts @@ -57,6 +57,10 @@ export class SonataFlowService { this.connection = this.extractConnectionConfig(config); } + public get autoStart(): boolean { + return this.connection.autoStart; + } + public get url(): string { if (!this.connection.port) { return this.connection.host; @@ -187,7 +191,7 @@ export class SonataFlowService { public async fetchProcessInstances(): Promise { const graphQlQuery = - '{ ProcessInstances (where: {processId: {isNull: false} } ) { id, processName, processId, state, start, lastUpdate, end, nodes { id }, variables, parentProcessInstance {id, processName, businessKey} } }'; + '{ ProcessInstances ( orderBy: { start: ASC }, where: {processId: {isNull: false} } ) { id, processName, processId, state, start, lastUpdate, end, nodes { id }, variables, parentProcessInstance {id, processName, businessKey} } }'; try { const response = await executeWithRetry(() => @@ -371,9 +375,10 @@ export class SonataFlowService { 'orchestrator.sonataFlowService.port', ); - const resourcesPath = config.getString( - 'orchestrator.sonataFlowService.workflowsSource.localPath', - ); + const resourcesPath = + config.getOptionalString( + 'orchestrator.sonataFlowService.workflowsSource.localPath', + ) ?? ''; const containerImage = config.getOptionalString('orchestrator.sonataFlowService.container') ?? diff --git a/plugins/orchestrator-backend/src/service/WorkflowService.ts b/plugins/orchestrator-backend/src/service/WorkflowService.ts index 8aeb57173b..768382e0d7 100644 --- a/plugins/orchestrator-backend/src/service/WorkflowService.ts +++ b/plugins/orchestrator-backend/src/service/WorkflowService.ts @@ -42,9 +42,10 @@ export class WorkflowService { this.sonataFlowService = sonataFlowService; this.logger = logger; this.githubService = new GitService(logger, config); - this.repoURL = config.getString( - 'orchestrator.sonataFlowService.workflowsSource.gitRepositoryUrl', - ); + this.repoURL = + config.getOptionalString( + 'orchestrator.sonataFlowService.workflowsSource.gitRepositoryUrl', + ) ?? ''; this.autoPush = config.getOptionalBoolean( 'orchestrator.sonataFlowService.workflowsSource.autoPush', diff --git a/plugins/orchestrator-common/src/constants.ts b/plugins/orchestrator-common/src/constants.ts index 10f3b04117..58562ff937 100644 --- a/plugins/orchestrator-common/src/constants.ts +++ b/plugins/orchestrator-common/src/constants.ts @@ -64,8 +64,11 @@ export const workflow_yaml_sample: WorkflowSample = { export const default_sonataflow_container_image = 'quay.io/kiegroup/kogito-swf-devmode-nightly:main-2023-08-30'; export const default_sonataflow_persistance_path = '/home/kogito/persistence'; -export const default_catalog_owner = 'infrastructure'; +export const default_catalog_owner = 'orchestrator'; export const default_catalog_environment = 'development'; export const default_editor_path = 'https://start.kubesmarts.org'; export const default_workflows_path = 'workflows'; + +export const ASSESSMENT_WORKFLOW_TYPE = 'workflow-type/assessment'; +export const INFRASTRUCTURE_WORKFLOW_TYPE = 'workflow-type/infrastructure'; diff --git a/plugins/orchestrator-common/src/types.ts b/plugins/orchestrator-common/src/types.ts index b3285127d2..0a989f10eb 100644 --- a/plugins/orchestrator-common/src/types.ts +++ b/plugins/orchestrator-common/src/types.ts @@ -60,3 +60,8 @@ export interface WorkflowDataInputSchemaResponse { export interface WorkflowExecutionResponse { id: string; } + +export enum WorkflowCategory { + ASSESSMENT = 'assessment', + INFRASTRUCTURE = 'infrastructure', +} diff --git a/plugins/orchestrator/README.md b/plugins/orchestrator/README.md index 7f1d507f29..eeddd9396e 100644 --- a/plugins/orchestrator/README.md +++ b/plugins/orchestrator/README.md @@ -65,12 +65,15 @@ orchestrator: sonataFlowService: baseUrl: http://localhost port: 8899 + autoStart: true workflowsSource: gitRepositoryUrl: https://github.com/tiagodolphine/backstage-orchestrator-workflows localPath: /tmp/orchestrator/repository autoPush: true ``` +when interacting with an existing Sonataflow backend service from `baseUrl` and `port`, `autoStart` needs to be unset or set to `false`, also the section of `workflowSource` can be neglect. + For more information about the configuration options, including other optional properties, see the [config.d.ts](../orchestrator-common/config.d.ts) file. - Although optional, you may also want to set up the `GITHUB_TOKEN` environment variable to allow the Orchestrator to access the GitHub API. diff --git a/plugins/orchestrator/package.json b/plugins/orchestrator/package.json index 5cee3956ba..f215f9ec23 100644 --- a/plugins/orchestrator/package.json +++ b/plugins/orchestrator/package.json @@ -61,6 +61,7 @@ "json-schema": "^0.4.0", "react-hook-form": "^7.45.1", "react-json-view": "^1.21.3", + "moment": "^2.29.4", "react-moment": "^1.1.3", "react-use": "^17.4.0", "vscode-languageserver-types": "^3.16.0" diff --git a/plugins/orchestrator/src/components/OrchestratorPage/OrchestratorPage.tsx b/plugins/orchestrator/src/components/OrchestratorPage/OrchestratorPage.tsx index 8e2fbee886..4a5bf98694 100644 --- a/plugins/orchestrator/src/components/OrchestratorPage/OrchestratorPage.tsx +++ b/plugins/orchestrator/src/components/OrchestratorPage/OrchestratorPage.tsx @@ -20,7 +20,7 @@ import { orchestratorApiRef } from '../../api'; import { newWorkflowRef, workflowInstancesRouteRef } from '../../routes'; import { BaseOrchestratorPage } from '../BaseOrchestratorPage/BaseOrchestratorPage'; import { OrchestratorSupportButton } from '../OrchestratorSupportButton/OrchestratorSupportButton'; -import { WorkflowsTable } from '../WorkflowDefinitionsListComponent/WorkflowDefinitionsListComponent'; +import { WorkflowsTable } from '../WorkflowDefinitionsListComponent'; export const OrchestratorPage = () => { const orchestratorApi = useApi(orchestratorApiRef); diff --git a/plugins/orchestrator/src/components/ScaffolderTemplateCard/OrchestratorScaffolderTemplateCard.tsx b/plugins/orchestrator/src/components/ScaffolderTemplateCard/OrchestratorScaffolderTemplateCard.tsx index ef486c11ff..d4eb43b3f2 100644 --- a/plugins/orchestrator/src/components/ScaffolderTemplateCard/OrchestratorScaffolderTemplateCard.tsx +++ b/plugins/orchestrator/src/components/ScaffolderTemplateCard/OrchestratorScaffolderTemplateCard.tsx @@ -21,7 +21,7 @@ export const OrchestratorScaffolderTemplateCard = ( const onSelectedExtended = useCallback( (template: TemplateEntityV1beta3) => { - const isWorkflow = template.metadata.tags?.includes(workflow_type); + const isWorkflow = template.spec.type === workflow_type; if (!isWorkflow) { onSelected?.(template); diff --git a/plugins/orchestrator/src/components/WorkflowDefinitionsListComponent/WorkflowDefinitionsListComponent.tsx b/plugins/orchestrator/src/components/WorkflowDefinitionsListComponent/WorkflowDefinitionsListComponent.tsx index ccc5bf3b78..23a2e16f2d 100644 --- a/plugins/orchestrator/src/components/WorkflowDefinitionsListComponent/WorkflowDefinitionsListComponent.tsx +++ b/plugins/orchestrator/src/components/WorkflowDefinitionsListComponent/WorkflowDefinitionsListComponent.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Table, TableColumn } from '@backstage/core-components'; @@ -8,9 +8,12 @@ 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'; @@ -27,26 +30,93 @@ type WorkflowsTableProps = { 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' }]; - const data: Row[] = items.map(item => { + 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 { - id: item.definition.id, - name: item.definition.name ?? '', - format: extractWorkflowFormatFromUri(item.uri), + 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) => { @@ -90,10 +160,11 @@ export const WorkflowsTable = ({ items }: WorkflowsTableProps) => { return ( | string | undefined; +} + +interface WorkflowOption { + id: string; + name: string; +} + +export const AssessmentResultViewer = (props: AssessmentResultViewerProps) => { + const { result } = props; + + const jsonSource = useMemo(() => { + if (!result) { + return undefined; + } + if (typeof result === 'string') { + return JSON.parse(result); + } + return result; + }, [result]); + + const executeWorkflowLink = useRouteRef(executeWorkflowRouteRef); + + const keyToTitle = (key: string) => { + const title = key.replace(/([a-z])([A-Z])/g, '$1 $2'); + return title.charAt(0).toUpperCase() + title.slice(1); + }; + + const accordionProps = (items: WorkflowOption | WorkflowOption[]) => { + if (!Array.isArray(items)) return { expanded: true }; + if (Array.isArray(items) && items.length === 0) return { disabled: true }; + return {}; + }; + + const workflowLinks = (items: WorkflowOption | WorkflowOption[]) => { + if (!Array.isArray(items)) { + const workflowOption: WorkflowOption = items; + return ( + <> + + {workflowOption.name} + +    + + + ); + } + return items.map(item => { + const workflowOption: WorkflowOption = item; + return ( + <> + + {workflowOption.name} + +
+ + ); + }); + }; + + const workflowOptions = ( + category: string, + items: WorkflowOption | WorkflowOption[], + ) => { + return ( + + } + aria-controls="panel1a-content" + id="panel1a-header" + > + {keyToTitle(category)} + + + {workflowLinks(items)} + + + ); + }; + + const assessmentOutput = (output: any) => { + if ( + output === undefined || + output?.workflowdata?.workflowOptions === undefined + ) + return null; + const rows = Object.entries(output.workflowdata.workflowOptions).map( + ([key, value]) => + workflowOptions(key, value as WorkflowOption | WorkflowOption[]), + ); + return {rows}; + }; + return assessmentOutput(jsonSource); +}; diff --git a/plugins/orchestrator/src/components/WorkflownstancesViewerPage/WorkflowInstancesViewerPage.tsx b/plugins/orchestrator/src/components/WorkflownstancesViewerPage/WorkflowInstancesViewerPage.tsx index 7f61103317..70b325c78c 100644 --- a/plugins/orchestrator/src/components/WorkflownstancesViewerPage/WorkflowInstancesViewerPage.tsx +++ b/plugins/orchestrator/src/components/WorkflownstancesViewerPage/WorkflowInstancesViewerPage.tsx @@ -8,6 +8,7 @@ import { ProcessInstance } from '@janus-idp/backstage-plugin-orchestrator-common import { BaseOrchestratorPage } from '../BaseOrchestratorPage/BaseOrchestratorPage'; import { OrchestratorSupportButton } from '../OrchestratorSupportButton/OrchestratorSupportButton'; +import { AssessmentResultViewer } from './AssessmentResultViewer'; import { ProcessDetailsViewer } from './ProcessDetailsViewer'; import { ProcessGraphViewer } from './ProcessGraphViewer'; import { ProcessInstancesTable } from './ProcessInstancesTable'; @@ -41,6 +42,9 @@ export const WorkflowInstancesViewerPage = () => { + + +