diff --git a/plugins/orchestrator-backend/__fixtures__/mockComposedGreetingWorfklow.ts b/plugins/orchestrator-backend/__fixtures__/mockComposedGreetingWorfklow.ts new file mode 100644 index 0000000000..9bd39eb899 --- /dev/null +++ b/plugins/orchestrator-backend/__fixtures__/mockComposedGreetingWorfklow.ts @@ -0,0 +1,132 @@ +import { JsonObject } from '@backstage/types'; + +import { JSONSchema7 } from 'json-schema'; + +import { WorkflowItem } from '@janus-idp/backstage-plugin-orchestrator-common'; + +const schema = { + $id: 'classpath:/schemas/yamlgreet__main-schema.json', + title: 'Data Input Schema', + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + properties: { + language: { + type: 'object', + properties: { + language: { + title: 'Language', + description: 'Language to greet', + type: 'string', + enum: ['English', 'Spanish'], + default: 'English', + }, + }, + title: 'Language', + }, + name: { + type: 'object', + properties: { + name: { + title: 'Name', + description: 'Name of the person', + type: 'string', + default: 'John Doe', + }, + }, + }, + }, + required: ['name'], +} as JSONSchema7; + +const workflowItem = { + uri: 'yamlgreet.sw.yaml', + definition: { + id: 'yamlgreet', + version: '1.0', + specVersion: '0.8', + name: 'Greeting workflow', + description: 'YAML based greeting workflow', + dataInputSchema: 'schemas/yamlgreet__main-schema.json', + start: 'ChooseOnLanguage', + functions: [ + { + name: 'greetFunction', + type: 'custom', + operation: 'sysout', + }, + ], + states: [ + { + name: 'ChooseOnLanguage', + type: 'switch', + dataConditions: [ + { + condition: '${ .language.language == "English" }', + transition: 'GreetInEnglish', + }, + { + condition: '${ .language.language == "Spanish" }', + transition: 'GreetInSpanish', + }, + ], + defaultCondition: { + transition: 'GreetInEnglish', + }, + }, + { + name: 'GreetInEnglish', + type: 'inject', + data: { + greeting: 'Hello from YAML Workflow, ', + }, + transition: 'GreetPerson', + }, + { + name: 'GreetInSpanish', + type: 'inject', + data: { + greeting: 'Saludos desde YAML Workflow, ', + }, + transition: 'GreetPerson', + }, + { + name: 'GreetPerson', + type: 'operation', + actions: [ + { + name: 'greetAction', + functionRef: { + refName: 'greetFunction', + arguments: { + message: '.greeting+.name.name', + }, + }, + }, + ], + end: { + terminate: true, + }, + }, + ], + }, +} as WorkflowItem; + +const variables = { + workflowdata: { + name: { + name: 'John Doe', + }, + language: { + language: 'Spanish', + }, + greeting: 'hello', + }, +}; + +const mockData: { + schema: JSONSchema7; + workflowItem: WorkflowItem; + variables: JsonObject; +} = { schema, workflowItem, variables }; + +export default mockData; diff --git a/plugins/orchestrator-backend/__fixtures__/mockGreetingWorkflowData.ts b/plugins/orchestrator-backend/__fixtures__/mockGreetingWorkflowData.ts new file mode 100644 index 0000000000..a4ff9c2ae5 --- /dev/null +++ b/plugins/orchestrator-backend/__fixtures__/mockGreetingWorkflowData.ts @@ -0,0 +1,117 @@ +import { JsonObject } from '@backstage/types'; + +import { JSONSchema7 } from 'json-schema'; + +import { WorkflowItem } from '@janus-idp/backstage-plugin-orchestrator-common'; + +const schema = { + $id: 'classpath:/schemas/yamlgreet__main-schema.json', + title: 'Data Input Schema', + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + properties: { + language: { + title: 'Language', + description: 'Language to greet', + type: 'string', + enum: ['English', 'Spanish'], + default: 'English', + }, + name: { + title: 'Name', + description: 'Name of the person', + type: 'string', + default: 'John Doe', + }, + }, + required: ['name'], +} as JSONSchema7; + +const workflowItem = { + uri: 'yamlgreet.sw.yaml', + definition: { + id: 'yamlgreet', + version: '1.0', + specVersion: '0.8', + name: 'Greeting workflow', + description: 'YAML based greeting workflow', + dataInputSchema: 'schemas/yamlgreet__main-schema.json', + start: 'ChooseOnLanguage', + functions: [ + { + name: 'greetFunction', + type: 'custom', + operation: 'sysout', + }, + ], + states: [ + { + name: 'ChooseOnLanguage', + type: 'switch', + dataConditions: [ + { + condition: '${ .language == "English" }', + transition: 'GreetInEnglish', + }, + { + condition: '${ .language == "Spanish" }', + transition: 'GreetInSpanish', + }, + ], + defaultCondition: { + transition: 'GreetInEnglish', + }, + }, + { + name: 'GreetInEnglish', + type: 'inject', + data: { + greeting: 'Hello from YAML Workflow, ', + }, + transition: 'GreetPerson', + }, + { + name: 'GreetInSpanish', + type: 'inject', + data: { + greeting: 'Saludos desde YAML Workflow, ', + }, + transition: 'GreetPerson', + }, + { + name: 'GreetPerson', + type: 'operation', + actions: [ + { + name: 'greetAction', + functionRef: { + refName: 'greetFunction', + arguments: { + message: '.greeting+.name', + }, + }, + }, + ], + end: { + terminate: true, + }, + }, + ], + }, +} as WorkflowItem; + +const variables = { + workflowdata: { + name: 'John Doe', + greeting: 'Saludos desde YAML Workflow, ', + language: 'Spanish', + }, +}; + +const mockData: { + schema: JSONSchema7; + workflowItem: WorkflowItem; + variables: JsonObject; +} = { schema, workflowItem, variables }; + +export default mockData; diff --git a/plugins/orchestrator-backend/__fixtures__/mockSpringBootWorkflowData.ts b/plugins/orchestrator-backend/__fixtures__/mockSpringBootWorkflowData.ts new file mode 100644 index 0000000000..033581c8f1 --- /dev/null +++ b/plugins/orchestrator-backend/__fixtures__/mockSpringBootWorkflowData.ts @@ -0,0 +1,583 @@ +import { JSONSchema7 } from 'json-schema'; + +import { WorkflowItem } from '@janus-idp/backstage-plugin-orchestrator-common'; + +const schema = { + $id: 'classpath:/schemas/spring-boot-backend__main-schema.json', + title: 'Data input schema', + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + properties: { + newComponent: { + $ref: '#/$defs/Provide information about the new component_0', + type: 'object', + }, + javaMetadata: { + $ref: '#/$defs/Provide information about the Java metadata_1', + type: 'object', + }, + ciMethod: { + $ref: '#/$defs/Provide information about the CI method_2', + type: 'object', + }, + }, + $defs: { + 'Provide information about the CI method_2': { + $id: 'classpath:/schemas/spring-boot-backend__ref-schema__CI_Method.json', + title: 'Provide information about the CI method', + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + properties: { + ci: { + title: 'CI Method', + type: 'string', + default: 'github', + oneOf: [ + { + const: 'github', + title: 'GitHub Action', + }, + { + const: 'tekton', + title: 'Tekton', + }, + ], + }, + }, + allOf: [ + { + if: { + properties: { + ci: { + const: 'github', + }, + }, + }, + }, + { + if: { + properties: { + ci: { + const: 'tekton', + }, + }, + }, + then: { + properties: { + imageRepository: { + title: 'Image Registry', + description: 'The registry to use', + type: 'string', + default: 'quay.io', + oneOf: [ + { + const: 'quay.io', + title: 'Quay', + }, + { + const: 'image-registry.openshift-image-registry.svc:5000', + title: 'Internal OpenShift Registry', + }, + ], + }, + imageUrl: { + title: 'Image URL', + description: + 'The Quay.io or OpenShift Image URL //', + type: 'string', + }, + namespace: { + title: 'Namespace', + description: 'The namespace for deploying resources', + type: 'string', + }, + }, + required: ['namespace', 'imageUrl', 'imageRepository'], + }, + }, + ], + }, + 'Provide information about the Java metadata_1': { + $id: 'classpath:/schemas/spring-boot-backend__ref-schema__Java_Metadata.json', + title: 'Provide information about the Java metadata', + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + properties: { + groupId: { + title: 'Group ID', + description: 'Maven Group ID eg (io.janus)', + type: 'string', + default: 'io.janus', + }, + artifactId: { + title: 'Artifact ID', + description: 'Maven Artifact ID', + type: 'string', + default: 'spring-boot-app', + }, + javaPackageName: { + title: 'Java Package Namespace', + description: + 'Name for the Java Package (ensure to use the / character as this is used for folder structure) should match Group ID and Artifact ID', + type: 'string', + default: 'io/janus/spring-boot-app', + }, + version: { + title: 'Version', + description: 'Maven Artifact Version', + type: 'string', + default: '1.0.0-SNAPSHOT', + }, + }, + required: ['groupId', 'artifactId', 'javaPackageName', 'version'], + }, + 'Provide information about the new component_0': { + $id: 'classpath:/schemas/spring-boot-backend__ref-schema__New_Component.json', + title: 'Provide information about the new component', + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + properties: { + orgName: { + title: 'Organization Name', + description: 'Organization name', + type: 'string', + }, + repoName: { + title: 'Repository Name', + description: 'Repository name', + type: 'string', + }, + description: { + title: 'Description', + description: 'Help others understand what this component is for', + type: 'string', + }, + owner: { + title: 'Owner', + description: 'An entity from the catalog', + type: 'string', + }, + system: { + title: 'System', + description: 'An entity from the catalog', + type: 'string', + }, + port: { + title: 'Port', + description: 'Override the port exposed for the application', + type: 'number', + default: 8080, + }, + }, + required: ['orgName', 'repoName', 'owner', 'system', 'port'], + }, + }, +} as JSONSchema7; + +const workflowItem = { + uri: 'spring-boot-backend.sw.yaml', + definition: { + id: 'spring-boot-backend', + version: '1.0', + specVersion: '0.8', + name: 'Spring Boot Backend application', + description: + 'Create a starter Spring Boot backend application with a CI pipeline', + dataInputSchema: 'schemas/spring-boot-backend__main-schema.json', + functions: [ + { + name: 'runActionFetchTemplate', + 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', + }, + { + name: 'fs:delete', + operation: 'specs/actions-openapi.json#fs:delete', + }, + { + name: 'sysout', + type: 'custom', + operation: 'sysout', + }, + ], + errors: [ + { + name: 'Error on Action', + code: 'java.lang.RuntimeException', + }, + ], + 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: 'runActionFetchTemplate', + arguments: { + url: 'https://github.com/janus-idp/software-templates/tree/main/templates/github/spring-boot-backend/skeleton', + values: { + orgName: '.newComponent.orgName', + repoName: '.newComponent.repoName', + owner: '.newComponent.owner', + system: '.newComponent.system', + applicationType: 'api', + description: '.newComponent.description', + namespace: '.ciMethod.namespace', + port: '.newComponent.port', + ci: '.ciMethod.ci', + sourceControl: 'github.com', + groupId: '.javaMetadata.groupId', + artifactId: '.javaMetadata.artifactId', + javaPackageName: '.javaMetadata.javaPackageName', + version: '.javaMetadata.version', + }, + }, + }, + actionDataFilter: { + toStateData: '.actionFetchTemplateSourceCodeResult', + }, + }, + ], + onErrors: [ + { + errorRef: 'Error on Action', + transition: 'Handle Error', + }, + ], + compensatedBy: 'Clear File System - Source Code', + transition: 'Generating the CI Component', + }, + { + name: 'Generating the CI Component', + type: 'switch', + dataConditions: [ + { + condition: '${ .ciMethod.ci == "github" }', + transition: 'Generating the CI Component - GitHub', + }, + { + condition: '${ .ciMethod.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: 'runActionFetchTemplate', + arguments: { + url: 'https://github.com/janus-idp/software-templates/tree/main/skeletons/github-actions', + copyWithoutTemplating: ['".github/workflows/"'], + values: { + orgName: '.newComponent.orgName', + repoName: '.newComponent.repoName', + owner: '.newComponent.owner', + system: '.newComponent.system', + applicationType: 'api', + description: '.newComponent.description', + namespace: '.ciMethod.namespace', + port: '.newComponent.port', + ci: '.ciMethod.ci', + sourceControl: 'github.com', + groupId: '.javaMetadata.groupId', + artifactId: '.javaMetadata.artifactId', + javaPackageName: '.javaMetadata.javaPackageName', + version: '.javaMetadata.version', + }, + }, + }, + actionDataFilter: { + toStateData: '.actionTemplateFetchCIResult', + }, + }, + ], + onErrors: [ + { + errorRef: 'Error on Action', + transition: 'Handle Error', + }, + ], + compensatedBy: 'Clear File System - CI', + 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: 'runActionFetchTemplate', + arguments: { + url: 'https://github.com/janus-idp/software-templates/tree/main/skeletons/tekton', + copyWithoutTemplating: ['".github/workflows/"'], + values: { + orgName: '.newComponent.orgName', + repoName: '.newComponent.repoName', + owner: '.newComponent.owner', + system: '.newComponent.system', + applicationType: 'api', + description: '.newComponent.description', + namespace: '.ciMethod.namespace', + imageUrl: '.imageUrl', + imageRepository: '.imageRepository', + imageBuilder: 's2i-go', + port: '.newComponent.port', + ci: '.ciMethod.ci', + sourceControl: 'github.com', + groupId: '.javaMetadata.groupId', + artifactId: '.javaMetadata.artifactId', + javaPackageName: '.javaMetadata.javaPackageName', + version: '.javaMetadata.version', + }, + }, + }, + actionDataFilter: { + toStateData: '.actionTemplateFetchCIResult', + }, + }, + ], + onErrors: [ + { + errorRef: 'Error on Action', + transition: 'Handle Error', + }, + ], + compensatedBy: 'Clear File System - CI', + transition: 'Generating the Catalog Info Component', + }, + { + name: 'Generating the Catalog Info Component', + type: 'operation', + actions: [ + { + name: 'Fetch Template Action - Catalog Info', + functionRef: { + refName: 'runActionFetchTemplate', + arguments: { + url: 'https://github.com/janus-idp/software-templates/tree/main/skeletons/catalog-info', + values: { + orgName: '.newComponent.orgName', + repoName: '.newComponent.repoName', + owner: '.newComponent.owner', + system: '.newComponent.system', + applicationType: 'api', + description: '.newComponent.description', + namespace: '.ciMethod.namespace', + imageUrl: '.ciMethod.imageUrl', + imageRepository: '.ciMethod.imageRepository', + imageBuilder: 's2i-go', + port: '.newComponent.port', + ci: '.ciMethod.ci', + sourceControl: 'github.com', + groupId: '.javaMetadata.groupId', + artifactId: '.javaMetadata.artifactId', + javaPackageName: '.javaMetadata.javaPackageName', + version: '.javaMetadata.version', + }, + }, + }, + actionDataFilter: { + toStateData: '.actionFetchTemplateCatalogInfoResult', + }, + }, + ], + onErrors: [ + { + errorRef: 'Error on Action', + transition: 'Handle Error', + }, + ], + compensatedBy: 'Clear File System - Catalog', + transition: 'Publishing to the Source Code Repository', + }, + { + name: 'Publishing to the Source Code Repository', + type: 'operation', + actionMode: 'sequential', + actions: [ + { + name: 'Publish Github', + functionRef: { + refName: 'runActionPublishGithub', + arguments: { + allowedHosts: ['"github.com"'], + description: 'Workflow Action', + repoUrl: + '"github.com?owner=" + .newComponent.orgName + "&repo=" + .newComponent.repoName', + defaultBranch: 'main', + gitCommitMessage: 'Initial commit', + allowAutoMerge: true, + allowRebaseMerge: true, + }, + }, + actionDataFilter: { + toStateData: '.actionPublishResult', + }, + }, + ], + onErrors: [ + { + errorRef: 'Error on Action', + transition: 'Handle Error', + }, + ], + compensatedBy: 'Remove Source Code Repository', + 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', + }, + }, + ], + onErrors: [ + { + errorRef: 'Error on Action', + transition: 'Handle Error', + }, + ], + compensatedBy: 'Remove Catalog Info Component', + end: true, + }, + { + name: 'Handle Error', + type: 'operation', + actions: [ + { + name: 'Error Action', + functionRef: { + refName: 'sysout', + arguments: { + message: 'Error on workflow, triggering compensations', + }, + }, + }, + ], + end: { + compensate: true, + }, + }, + { + name: 'Clear File System - Source Code', + type: 'operation', + usedForCompensation: true, + actions: [ + { + name: 'Clear FS Action', + functionRef: { + refName: 'fs:delete', + arguments: { + files: ['./'], + }, + }, + }, + ], + }, + { + name: 'Clear File System - CI', + type: 'operation', + usedForCompensation: true, + actions: [ + { + name: 'Clear FS Action', + functionRef: { + refName: 'fs:delete', + arguments: { + files: ['./'], + }, + }, + }, + ], + }, + { + name: 'Clear File System - Catalog', + type: 'operation', + usedForCompensation: true, + actions: [ + { + name: 'Clear FS Action', + functionRef: { + refName: 'fs:delete', + arguments: { + files: ['./'], + }, + }, + }, + ], + }, + { + name: 'Remove Source Code Repository', + type: 'operation', + usedForCompensation: true, + actions: [ + { + name: 'Remove Source Code Repository', + functionRef: { + refName: 'sysout', + arguments: { + message: 'Remove Source Code Repository', + }, + }, + }, + ], + }, + { + name: 'Remove Catalog Info Component', + type: 'operation', + usedForCompensation: true, + actions: [ + { + name: 'Remove Catalog Info Component', + functionRef: { + refName: 'sysout', + arguments: { + message: 'Remove Catalog Info Component', + }, + }, + }, + ], + }, + ], + }, +} as WorkflowItem; + +const mockData: { + schema: JSONSchema7; + workflowItem: WorkflowItem; +} = { schema, workflowItem }; + +export default mockData; diff --git a/plugins/orchestrator-backend/src/service/DataInputSchemaService.test.ts b/plugins/orchestrator-backend/src/service/DataInputSchemaService.test.ts new file mode 100644 index 0000000000..9f05145db3 --- /dev/null +++ b/plugins/orchestrator-backend/src/service/DataInputSchemaService.test.ts @@ -0,0 +1,130 @@ +import { createLogger, format, transports } from 'winston'; + +import mockComposedGreetingWorkflowData from '../../__fixtures__/mockComposedGreetingWorfklow'; +import mockGreetingWorkflowData from '../../__fixtures__/mockGreetingWorkflowData'; +import mockSpringBootWorkflowData from '../../__fixtures__/mockSpringBootWorkflowData'; +import { DataInputSchemaService } from './DataInputSchemaService'; + +const logger = createLogger({ + level: 'info', + format: format.json(), + transports: [ + new transports.Console(), + new transports.File({ filename: 'logfile.log' }), + ], +}); +const service = new DataInputSchemaService(logger, undefined); + +describe('workflow input schema response', () => { + it('schema with refs should return multiple steps', () => { + const response = service.getWorkflowInputSchemaResponse( + mockSpringBootWorkflowData.workflowItem, + mockSpringBootWorkflowData.schema, + ); + expect(response.isComposedSchema).toEqual(true); + expect(response.schemaSteps?.map(step => step.title)).toEqual([ + 'Provide information about the new component', + 'Provide information about the Java metadata', + 'Provide information about the CI method', + ]); + expect(response.schemaSteps?.map(step => step.key)).toEqual([ + 'newComponent', + 'javaMetadata', + 'ciMethod', + ]); + }); + + it('schema with two layers without refs should return a schema parse error', () => { + const response = service.getWorkflowInputSchemaResponse( + mockSpringBootWorkflowData.workflowItem, + { ...mockSpringBootWorkflowData.schema, $defs: undefined }, + ); + expect(response.isComposedSchema).toEqual(false); + expect(response.schemaSteps.length).toEqual(0); + expect(response.schemaParseError).toEqual( + 'schema contains invalid ref #/$defs/Provide information about the new component_0', + ); + }); + + it('none composed schema should return isComposedSchema false and one step', () => { + const response = service.getWorkflowInputSchemaResponse( + mockGreetingWorkflowData.workflowItem, + mockGreetingWorkflowData.schema, + ); + expect(response.isComposedSchema).toEqual(false); + expect(response.schemaSteps[0].key).toEqual('DUMMY_KEY_FOR_SINGLE_SCHEMA'); + expect(response.schemaSteps[0].title).toEqual('Data Input Schema'); + }); + + it('composed schema also wihtout refs should return multiple steps', () => { + const response = service.getWorkflowInputSchemaResponse( + mockComposedGreetingWorkflowData.workflowItem, + mockComposedGreetingWorkflowData.schema, + ); + expect(response.isComposedSchema).toEqual(true); + expect(response.schemaSteps[0].key).toEqual('language'); + expect(response.schemaSteps[0].title).toEqual('Language'); + expect(response.schemaSteps[1].key).toEqual('name'); + expect(response.schemaSteps[1].title).toEqual('name'); + }); + + it('a schema without properties should return a schema parse error', () => { + const response = service.getWorkflowInputSchemaResponse( + mockComposedGreetingWorkflowData.workflowItem, + { title: 'A' }, + ); + expect(response.isComposedSchema).toEqual(false); + expect(response.schemaSteps.length).toEqual(0); + expect(response.schemaParseError).toEqual( + 'the provided schema does not contain valid properties', + ); + }); + + it('using initial variables should return data for each step', () => { + const response = service.getWorkflowInputSchemaResponse( + mockGreetingWorkflowData.workflowItem, + mockGreetingWorkflowData.schema, + mockGreetingWorkflowData.variables, + ); + expect(response.isComposedSchema).toEqual(false); + expect(response.schemaSteps[0].data).toEqual({ + language: 'Spanish', + name: 'John Doe', + }); + expect(response.schemaSteps[0].readonlyKeys).toEqual([]); + }); + + it('using initial variables on composed schema should return data for each step', () => { + const response = service.getWorkflowInputSchemaResponse( + mockComposedGreetingWorkflowData.workflowItem, + mockComposedGreetingWorkflowData.schema, + mockComposedGreetingWorkflowData.variables, + ); + expect(response.isComposedSchema).toEqual(true); + expect(response.schemaSteps[0].data).toEqual({ language: 'Spanish' }); + expect(response.schemaSteps[1].data).toEqual({ name: 'John Doe' }); + expect(response.schemaSteps[0].readonlyKeys).toEqual([]); + }); + + it('using initial assessment variables should return read only keys', () => { + const response = service.getWorkflowInputSchemaResponse( + mockGreetingWorkflowData.workflowItem, + mockGreetingWorkflowData.schema, + undefined, + { + workflowdata: { + language: 'Spanish', + name: 'John Doe', + greeting: 'hello', + waitOrError: 'Error', + }, + }, + ); + expect(response.isComposedSchema).toEqual(false); + expect(response.schemaSteps[0].data).toEqual({ + language: 'Spanish', + name: 'John Doe', + }); + expect(response.schemaSteps[0].readonlyKeys).toEqual(['language', 'name']); + }); +}); diff --git a/plugins/orchestrator-backend/src/service/DataInputSchemaService.ts b/plugins/orchestrator-backend/src/service/DataInputSchemaService.ts index dcf936ca75..023117cb27 100644 --- a/plugins/orchestrator-backend/src/service/DataInputSchemaService.ts +++ b/plugins/orchestrator-backend/src/service/DataInputSchemaService.ts @@ -16,7 +16,19 @@ import { JSONSchema4, JSONSchema7 } from 'json-schema'; import { OpenAPIV3 } from 'openapi-types'; import { Logger } from 'winston'; -import { WorkflowDefinition } from '@janus-idp/backstage-plugin-orchestrator-common'; +import { + ComposedSchema, + isComposedSchema, + isJsonObjectSchema, + JsonObjectSchema, + ProcessInstanceVariables, + WorkflowDefinition, + WorkflowInputSchemaResponse, + WorkflowInputSchemaStep, + WorkflowItem, +} from '@janus-idp/backstage-plugin-orchestrator-common'; + +import { WORKFLOW_DATA_KEY } from './constants'; type OpenApiSchemaProperties = { [k: string]: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject; @@ -83,6 +95,8 @@ interface WorkflowFunction { const JSON_SCHEMA_VERSION = 'http://json-schema.org/draft-04/schema#'; const FETCH_TEMPLATE_ACTION_OPERATION_ID = 'fetch:template'; +const SINGLE_SCHEMA_TITLE = 'workflow input data'; +const SINGLE_SCHEMA_KEY = 'DUMMY_KEY_FOR_SINGLE_SCHEMA'; const Regex = { VALUES_IN_SKELETON: /\{\{[%-]?\s*values\.(\w+)\s*[%-]?}}/gi, @@ -1170,7 +1184,6 @@ export class DataInputSchemaService { inputVariableSet.add(args.variable); } } - function traverseObject(currentObj: any, currentPath: string) { for (const key in currentObj) { if (currentObj.hasOwnProperty(key)) { @@ -1195,88 +1208,129 @@ export class DataInputSchemaService { return inputVariableSet; } - public parseComposition(inputSchema: JSONSchema7): JSONSchema7[] { - if (!inputSchema.properties) { - return []; - } - - const refPaths = Object.values(inputSchema.properties) - .map(p => (p as JSONSchema7).$ref) - .filter((r): r is string => r !== undefined); - - if (!refPaths.length) { - return [inputSchema]; - } - - return refPaths - .map(r => this.findReferencedSchema({ rootSchema: inputSchema, ref: r })) - .filter((r): r is JSONSchema7 => r !== undefined); + public extractInitialStateFromWorkflowData( + workflowData: JsonObject, + schemaProperties: JsonObjectSchema['properties'], + ): JsonObject { + return Object.keys(schemaProperties) + .filter(k => k in workflowData) + .reduce((result, k) => { + if (!workflowData[k]) { + return result; + } + result[k] = workflowData[k]; + return result; + }, {} as JsonObject); } - private findReferencedSchema(args: { - rootSchema: JSONSchema7; - ref: string; - }): JSONSchema7 | undefined { - const pathParts = args.ref - .split('/') - .filter(part => !['#', ''].includes(part)); + private findReferencedSchema( + rootSchema: JSONSchema7, + ref: string, + ): JSONSchema7 { + const pathParts = ref.split('/').filter(part => !['#', ''].includes(part)); - let current: any = args.rootSchema; + let current: any = rootSchema; for (const part of pathParts) { current = current?.[part]; if (current === undefined) { - return undefined; + throw new Error(`schema contains invalid ref ${ref}`); } } - return current; } - public extractInitialStateFromWorkflowData(args: { - workflowData: JsonObject; - schemas: JSONSchema7[]; - }): JsonObject[] { - return args.schemas.map(s => { - const mergedProperties = this.mergeValuesByObjectKey( - s as JsonObject, - 'properties', - ); - return Object.keys(mergedProperties) - .filter(k => k in args.workflowData) - .reduce((result, k) => { - result[k] = args.workflowData[k]; - return result; - }, {} as JsonObject); + public resolveRefs(schema: JsonObjectSchema): JsonObjectSchema { + const resolvedSchemaProperties = Object.entries(schema.properties).reduce< + JsonObjectSchema['properties'] + >( + (prev, [key, curSchema]) => ({ + ...prev, + [key]: curSchema.$ref + ? this.findReferencedSchema(schema, curSchema.$ref) + : curSchema, + }), + {}, + ); + return { + ...schema, + properties: resolvedSchemaProperties, + }; + } + + private getInputSchemaSteps( + schema: ComposedSchema, + isAssessment: boolean, + workflowData?: JsonObject, + ): WorkflowInputSchemaStep[] { + return Object.entries(schema.properties).map(([key, curSchema]) => { + const data = (workflowData?.[key] as JsonObject) ?? {}; + return { + title: curSchema.title || key, + key, + schema: curSchema, + data, + readonlyKeys: isAssessment ? Object.keys(data) : [], + }; }); } - private mergeValuesByObjectKey( - jsonObj: JsonObject, - targetKey: string, - ): JsonObject { - let result: JsonObject = {}; + public getWorkflowInputSchemaResponse( + workflowItem: WorkflowItem, + inputSchema: JSONSchema7, + instanceVariables?: ProcessInstanceVariables, + assessmentInstanceVariables?: ProcessInstanceVariables, + ): WorkflowInputSchemaResponse { + const variables = instanceVariables ?? assessmentInstanceVariables; + const isAssessment = !!assessmentInstanceVariables; + const workflowData = variables + ? (variables[WORKFLOW_DATA_KEY] as JsonObject) + : undefined; + + const res: WorkflowInputSchemaResponse = { + workflowItem, + isComposedSchema: false, + schemaSteps: [], + }; + if (!isJsonObjectSchema(inputSchema)) { + return { + ...res, + schemaParseError: + 'the provided schema does not contain valid properties', + }; + } + try { + const resolvedSchema = this.resolveRefs(inputSchema); - function traverse(currentObj: JsonObject) { - for (const key of Object.keys(currentObj)) { - const child = currentObj[key]; - if (!child || typeof child !== 'object') { - continue; - } - if (Array.isArray(child)) { - child - .filter(c => typeof c === 'object') - .forEach(c => { - traverse(c as JsonObject); - }); - } else if (key === targetKey) { - result = { ...result, ...child }; - } else { - traverse(child); - } + if (isComposedSchema(resolvedSchema)) { + res.schemaSteps = this.getInputSchemaSteps( + resolvedSchema, + isAssessment, + workflowData, + ); + res.isComposedSchema = true; + } else { + const data = workflowData + ? this.extractInitialStateFromWorkflowData( + workflowData, + resolvedSchema.properties, + ) + : {}; + res.schemaSteps = [ + { + schema: resolvedSchema, + title: resolvedSchema.title ?? SINGLE_SCHEMA_TITLE, + key: SINGLE_SCHEMA_KEY, + data, + readonlyKeys: isAssessment ? Object.keys(data) : [], + }, + ]; } + } catch (err) { + res.schemaParseError = + typeof err === 'object' && (err as { message: string }).message + ? (err as { message: string }).message + : 'unexpected parsing error'; } - - traverse(jsonObj); - return result; + return res; } } diff --git a/plugins/orchestrator-backend/src/service/router.ts b/plugins/orchestrator-backend/src/service/router.ts index ae5671792a..fae938c281 100644 --- a/plugins/orchestrator-backend/src/service/router.ts +++ b/plugins/orchestrator-backend/src/service/router.ts @@ -18,9 +18,9 @@ import { QUERY_PARAM_INCLUDE_ASSESSMENT, QUERY_PARAM_INSTANCE_ID, QUERY_PARAM_URI, - WorkflowDataInputSchemaResponse, WorkflowDefinition, WorkflowInfo, + WorkflowInputSchemaResponse, WorkflowItem, WorkflowListResult, } from '@janus-idp/backstage-plugin-orchestrator-common'; @@ -30,7 +30,6 @@ import { ApiResponseBuilder } from '../types/apiResponse'; import { getWorkflowOverviewV1 } from './api/v1'; import { getWorkflowOverviewV2 } from './api/v2'; import { CloudEventService } from './CloudEventService'; -import { WORKFLOW_DATA_KEY } from './constants'; import { DataIndexService } from './DataIndexService'; import { DataInputSchemaService } from './DataInputSchemaService'; import { JiraEvent, JiraService } from './JiraService'; @@ -403,13 +402,10 @@ function setupInternalRoutes( const workflowItem: WorkflowItem = { uri, definition }; - const response: WorkflowDataInputSchemaResponse = { + const response: WorkflowInputSchemaResponse = { workflowItem, - schemas: [], - initialState: { - values: [], - readonlyKeys: [], - }, + schemaSteps: [], + isComposedSchema: false, }; if (!definition.dataInputSchema) { @@ -423,71 +419,39 @@ function setupInternalRoutes( ); if (!workflowInfo) { - res.status(500).send(`Couldn't fetch workflow info ${workflowId}`); + res.status(500).send(`couldn't fetch workflow info ${workflowId}`); return; } if (!workflowInfo.inputSchema) { res .status(500) - .send(`Couldn't fetch workflow input schema ${workflowId}`); + .send(`failed to retreive schema ${definition.dataInputSchema}`); return; } - const schemas = services.dataInputSchemaService.parseComposition( - workflowInfo.inputSchema, - ); - const instanceVariables = instanceId ? await services.dataIndexService.fetchProcessInstanceVariables( instanceId, ) : undefined; - const instanceWorkflowData = instanceVariables?.[WORKFLOW_DATA_KEY]; - let initialState: JsonObject[] = []; - let readonlyKeys: string[] = []; - - if (instanceWorkflowData) { - initialState = - services.dataInputSchemaService.extractInitialStateFromWorkflowData({ - workflowData: instanceWorkflowData as JsonObject, - schemas, - }); - } - const assessmentInstanceVariables = assessmentInstanceId ? await services.dataIndexService.fetchProcessInstanceVariables( assessmentInstanceId, ) : undefined; - const assessmentInstanceWorkflowData = - assessmentInstanceVariables?.[WORKFLOW_DATA_KEY]; - - if (assessmentInstanceWorkflowData) { - const assessmentInstanceInitialState = - services.dataInputSchemaService.extractInitialStateFromWorkflowData({ - workflowData: assessmentInstanceWorkflowData as JsonObject, - schemas, - }); - - if (initialState.length === 0) { - initialState = assessmentInstanceInitialState; - } - - readonlyKeys = assessmentInstanceInitialState - .map(item => Object.keys(item).filter(key => item[key] !== undefined)) - .flat(); - } - - response.schemas = schemas; - response.initialState = { - values: initialState, - readonlyKeys, - }; - - res.status(200).json(response); + res + .status(200) + .json( + services.dataInputSchemaService.getWorkflowInputSchemaResponse( + workflowItem, + workflowInfo.inputSchema, + instanceVariables, + assessmentInstanceVariables, + ), + ); }); router.delete('/workflows/:workflowId', async (req, res) => { diff --git a/plugins/orchestrator-common/src/types.ts b/plugins/orchestrator-common/src/types.ts index 0ba23d760c..9fc222652e 100644 --- a/plugins/orchestrator-common/src/types.ts +++ b/plugins/orchestrator-common/src/types.ts @@ -1,7 +1,7 @@ import { JsonObject } from '@backstage/types'; import { Specification } from '@severlessworkflow/sdk-typescript'; -import { JSONSchema7 } from 'json-schema'; +import { JSONSchema7, JSONSchema7Definition } from 'json-schema'; import { OpenAPIV3 } from 'openapi-types'; import { ProcessInstance, ProcessInstanceStateValues } from './models'; @@ -56,15 +56,48 @@ export interface WorkflowSpecFile { content: OpenAPIV3.Document; } -export interface DataInputSchemaInitialState { - values: JsonObject[]; +export type WorkflowInputSchemaStep = { + schema: JsonObjectSchema; + title: string; + key: string; + data: JsonObject; readonlyKeys: string[]; -} +}; + +export type JsonObjectSchema = Omit & { + properties: { [key: string]: JSONSchema7 }; +}; + +export type ComposedSchema = Omit & { + properties: { + [key: string]: Omit & { + properties: { [key: string]: JsonObjectSchema }; + }; + }; +}; -export interface WorkflowDataInputSchemaResponse { +export const isJsonObjectSchema = ( + schema: JSONSchema7 | JsonObjectSchema | JSONSchema7Definition, +): schema is JsonObjectSchema => + typeof schema === 'object' && + !!schema.properties && + Object.values(schema.properties).filter( + curSchema => typeof curSchema !== 'object', + ).length === 0; + +export const isComposedSchema = ( + schema: JSONSchema7 | ComposedSchema, +): schema is ComposedSchema => + !!schema.properties && + Object.values(schema.properties).filter( + curSchema => !isJsonObjectSchema(curSchema), + ).length === 0; + +export interface WorkflowInputSchemaResponse { workflowItem: WorkflowItem; - schemas: JSONSchema7[]; - initialState: DataInputSchemaInitialState; + schemaSteps: WorkflowInputSchemaStep[]; + isComposedSchema: boolean; + schemaParseError?: string; } export interface WorkflowExecutionResponse { diff --git a/plugins/orchestrator/src/__fixtures__/fakeWorkflowDataInputSchemaDifferentTypes.ts b/plugins/orchestrator/src/__fixtures__/fakeWorkflowDataInputSchemaDifferentTypes.ts index 3a0b16ef2d..9f02e70ec4 100644 --- a/plugins/orchestrator/src/__fixtures__/fakeWorkflowDataInputSchemaDifferentTypes.ts +++ b/plugins/orchestrator/src/__fixtures__/fakeWorkflowDataInputSchemaDifferentTypes.ts @@ -1,82 +1,83 @@ -import { WorkflowDataInputSchemaResponse } from '@janus-idp/backstage-plugin-orchestrator-common'; +import { WorkflowInputSchemaResponse } from '@janus-idp/backstage-plugin-orchestrator-common'; -export const fakeDataInputSchemaDifferentTypes: WorkflowDataInputSchemaResponse = - { - workflowItem: { - uri: 'yamlgreet.sw.yaml', - definition: { - id: 'yamlgreet', - version: '1.0', - specVersion: '0.8', - name: 'Greeting workflow', - description: 'YAML based greeting workflow', - dataInputSchema: 'schemas/yamlgreet__main_schema.json', - start: 'ChooseOnLanguage', - functions: [ - { - name: 'greetFunction', - type: 'custom', - operation: 'sysout', - }, - ], - states: [ - { - name: 'ChooseOnLanguage', - type: 'switch', - dataConditions: [ - { - condition: '${ .language == "English" }', - transition: 'GreetInEnglish', - }, - { - condition: '${ .language == "Spanish" }', - transition: 'GreetInSpanish', - }, - ], - defaultCondition: { +export const fakeDataInputSchemaDifferentTypes: WorkflowInputSchemaResponse = { + workflowItem: { + uri: 'yamlgreet.sw.yaml', + definition: { + id: 'yamlgreet', + version: '1.0', + specVersion: '0.8', + name: 'Greeting workflow', + description: 'YAML based greeting workflow', + dataInputSchema: 'schemas/yamlgreet__main_schema.json', + start: 'ChooseOnLanguage', + functions: [ + { + name: 'greetFunction', + type: 'custom', + operation: 'sysout', + }, + ], + states: [ + { + name: 'ChooseOnLanguage', + type: 'switch', + dataConditions: [ + { + condition: '${ .language == "English" }', transition: 'GreetInEnglish', }, - }, - { - name: 'GreetInEnglish', - type: 'inject', - data: { - greeting: 'Hello from YAML Workflow, ', + { + condition: '${ .language == "Spanish" }', + transition: 'GreetInSpanish', }, - transition: 'GreetPerson', + ], + defaultCondition: { + transition: 'GreetInEnglish', }, - { - name: 'GreetInSpanish', - type: 'inject', - data: { - greeting: 'Saludos desde YAML Workflow, ', - }, - transition: 'GreetPerson', + }, + { + name: 'GreetInEnglish', + type: 'inject', + data: { + greeting: 'Hello from YAML Workflow, ', + }, + transition: 'GreetPerson', + }, + { + name: 'GreetInSpanish', + type: 'inject', + data: { + greeting: 'Saludos desde YAML Workflow, ', }, - { - name: 'GreetPerson', - type: 'operation', - actions: [ - { - name: 'greetAction', - functionRef: { - refName: 'greetFunction', - arguments: { - message: '.greeting+.name', - }, + transition: 'GreetPerson', + }, + { + name: 'GreetPerson', + type: 'operation', + actions: [ + { + name: 'greetAction', + functionRef: { + refName: 'greetFunction', + arguments: { + message: '.greeting+.name', }, }, - ], - end: { - terminate: true, }, + ], + end: { + terminate: true, }, - ], - }, + }, + ], }, - schemas: [ - { - title: 'Boolean field', + }, + isComposedSchema: true, + schemaSteps: [ + { + title: 'Boolean field', + schema: { type: 'object', properties: { default: { @@ -86,9 +87,15 @@ export const fakeDataInputSchemaDifferentTypes: WorkflowDataInputSchemaResponse }, }, }, - { - title: 'String formats', + key: 'booleanfield', + readonlyKeys: [], + data: {}, + }, + { + title: 'String formats', + schema: { type: 'object', + properties: { email: { type: 'string', @@ -100,7 +107,16 @@ export const fakeDataInputSchemaDifferentTypes: WorkflowDataInputSchemaResponse }, }, }, - { + data: {}, + readonlyKeys: [], + key: 'string-formats', + }, + { + readonlyKeys: [], + key: 'select', + title: 'Select', + data: {}, + schema: { title: 'Select', type: 'object', properties: { @@ -111,8 +127,13 @@ export const fakeDataInputSchemaDifferentTypes: WorkflowDataInputSchemaResponse }, }, }, - { - title: 'Date and time widgets', + }, + { + readonlyKeys: [], + key: 'dateandtime', + title: 'Date and time', + data: {}, + schema: { type: 'object', properties: { datetime: { @@ -129,8 +150,11 @@ export const fakeDataInputSchemaDifferentTypes: WorkflowDataInputSchemaResponse }, }, }, - { - title: 'Array', + }, + { + key: 'array', + title: 'Array', + schema: { type: 'object', required: ['title'], properties: { @@ -165,6 +189,8 @@ export const fakeDataInputSchemaDifferentTypes: WorkflowDataInputSchemaResponse }, }, }, - ], - initialState: { values: [], readonlyKeys: [] }, - }; + data: { title: 'my task list' }, + readonlyKeys: ['title'], + }, + ], +}; diff --git a/plugins/orchestrator/src/__fixtures__/fakeWorkflowDataInputSchemaResponseMultiStep.ts b/plugins/orchestrator/src/__fixtures__/fakeWorkflowDataInputSchemaResponseMultiStep.ts index 6f9e13d622..f4074dd708 100644 --- a/plugins/orchestrator/src/__fixtures__/fakeWorkflowDataInputSchemaResponseMultiStep.ts +++ b/plugins/orchestrator/src/__fixtures__/fakeWorkflowDataInputSchemaResponseMultiStep.ts @@ -1,25 +1,25 @@ -import { WorkflowDataInputSchemaResponse } from '@janus-idp/backstage-plugin-orchestrator-common'; +import { WorkflowInputSchemaResponse } from '@janus-idp/backstage-plugin-orchestrator-common'; -export const fakeDataInputSchemaMultiStepResponse: WorkflowDataInputSchemaResponse = +export const fakeDataInputSchemaMultiStepResponse: WorkflowInputSchemaResponse = { workflowItem: { - uri: 'quarkus-backend.sw.yaml', + uri: 'ansible-job-template.sw.yaml', definition: { - id: 'quarkus-backend', + id: 'ansible-job-template', version: '1.0', specVersion: '0.8', - name: 'Quarkus Backend application', + name: 'Ansible Job Template', description: - 'Create a starter Quarkus backend application with a CI pipeline', - dataInputSchema: 'schemas/quarkus-backend__main-schema.json', + 'Define an Ansible Job Template within Ansible Automation Platform', + dataInputSchema: 'schemas/ansible-job-template__main-schema.json', functions: [ { name: 'runActionFetchTemplate', operation: 'specs/actions-openapi.json#fetch:template', }, { - name: 'runActionPublishGithub', - operation: 'specs/actions-openapi.json#publish:github', + name: 'runActionGitHubRepoPush', + operation: 'specs/actions-openapi.json#github:repo:push', }, { name: 'runActionCatalogRegister', @@ -41,193 +41,57 @@ export const fakeDataInputSchemaMultiStepResponse: WorkflowDataInputSchemaRespon code: 'java.lang.RuntimeException', }, ], - start: 'Generating the Source Code Component', + start: 'Code and Catalog generation', states: [ { - name: 'Generating the Source Code Component', - type: 'operation', - actionMode: 'sequential', - actions: [ - { - name: 'Fetch Template Action - Source Code', - functionRef: { - refName: 'runActionFetchTemplate', - arguments: { - url: 'https://github.com/janus-idp/software-templates/tree/main/templates/github/quarkus-backend/skeleton', - values: { - orgName: '.orgName', - repoName: '.repoName', - owner: '.owner', - system: '.system', - applicationType: 'api', - description: '.description', - namespace: '.namespace', - port: '.port', - ci: '.ci', - sourceControl: 'github.com', - groupId: '.groupId', - artifactId: '.artifactId', - javaPackageName: '.javaPackageName', - version: '.version', - }, - }, - }, - actionDataFilter: { - toStateData: '.actionFetchTemplateSourceCodeResult', - }, - }, - ], - onErrors: [ - { - errorRef: 'Error on Action', - transition: 'Handle Error', - }, - ], - compensatedBy: 'Clear File System - Source Code', - 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: 'runActionFetchTemplate', - arguments: { - url: 'https://github.com/janus-idp/software-templates/tree/main/skeletons/github-actions', - copyWithoutTemplating: ['".github/workflows/"'], - values: { - orgName: '.orgName', - repoName: '.repoName', - owner: '.owner', - system: '.system', - applicationType: 'api', - description: '.description', - namespace: '.namespace', - port: '.port', - ci: '.ci', - sourceControl: 'github.com', - groupId: '.groupId', - artifactId: '.artifactId', - javaPackageName: '.javaPackageName', - version: '.version', - }, - }, - }, - actionDataFilter: { - toStateData: '.actionTemplateFetchCIResult', - }, - }, - ], - onErrors: [ + name: 'Code and Catalog generation', + type: 'parallel', + branches: [ { - errorRef: 'Error on Action', - transition: 'Handle Error', - }, - ], - compensatedBy: 'Clear File System - CI', - 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: 'runActionFetchTemplate', - arguments: { - url: 'https://github.com/janus-idp/software-templates/tree/main/skeletons/tekton', - copyWithoutTemplating: ['".github/workflows/"'], - values: { - orgName: '.orgName', - repoName: '.repoName', - owner: '.owner', - system: '.system', - applicationType: 'api', - description: '.description', - namespace: '.namespace', - imageUrl: '.imageUrl', - imageRepository: '.imageRepository', - imageBuilder: 's2i-java', - port: '.port', - ci: '.ci', - sourceControl: 'github.com', - groupId: '.groupId', - artifactId: '.artifactId', - javaPackageName: '.javaPackageName', - version: '.version', + name: 'Generating the Ansible Job component', + actions: [ + { + name: 'Run Template Fetch Action', + functionRef: { + refName: 'runActionFetchTemplate', + arguments: { + id: '$WORKFLOW.instanceId', + url: 'https://github.com/janus-idp/software-templates/tree/main/templates/github/launch-ansible-job/skeleton', + values: { + name: '${.ansibleJobDefinition.name}', + jobTemplate: '${.ansibleJobDefinition.jobTemplate}', + component_id: '${.ansibleJobDefinition.name}', + namespace: '${.ansibleJobDefinition.namespace}', + connection_secret: + '${.ansibleJobDefinition.connectionSecret}', + description: '${.ansibleJobDefinition.description}', + extra_vars: '${.ansibleJobDefinition.extraVars}', + }, + }, }, }, - }, - actionDataFilter: { - toStateData: '.actionTemplateFetchCIResult', - }, + ], }, - ], - onErrors: [ { - errorRef: 'Error on Action', - transition: 'Handle Error', - }, - ], - compensatedBy: 'Clear File System - CI', - transition: 'Generating the Catalog Info Component', - }, - { - name: 'Generating the Catalog Info Component', - type: 'operation', - actions: [ - { - name: 'Fetch Template Action - Catalog Info', - functionRef: { - refName: 'runActionFetchTemplate', - arguments: { - url: 'https://github.com/janus-idp/software-templates/tree/main/skeletons/catalog-info', - values: { - orgName: '.orgName', - repoName: '.repoName', - owner: '.owner', - system: '.system', - applicationType: 'api', - description: '.description', - namespace: '.namespace', - imageUrl: 'imageUrl', - imageRepository: '.imageRepository', - imageBuilder: 's2i-go', - port: '.port', - ci: '.ci', - sourceControl: 'github.com', - groupId: '.groupId', - artifactId: '.artifactId', - javaPackageName: '.javaPackageName', - version: '.version', + name: 'Generating the Catalog Info Component', + actions: [ + { + functionRef: { + refName: 'runActionFetchTemplate', + arguments: { + id: '$WORKFLOW.instanceId', + url: 'https://github.com/janus-idp/software-templates/tree/main/skeletons/catalog-info', + values: { + githubOrg: '${.repositoryInfo.githubOrg}', + repoName: '${.repositoryInfo.repoName}', + owner: '${.repositoryInfo.owner}', + applicationType: 'api', + description: '${.ansibleJobDefinition.description}', + }, + }, }, }, - }, - actionDataFilter: { - toStateData: '.actionFetchTemplateCatalogInfoResult', - }, + ], }, ], onErrors: [ @@ -236,7 +100,7 @@ export const fakeDataInputSchemaMultiStepResponse: WorkflowDataInputSchemaRespon transition: 'Handle Error', }, ], - compensatedBy: 'Clear File System - Catalog', + compensatedBy: 'Clear Code and Catalog generation', transition: 'Publishing to the Source Code Repository', }, { @@ -247,20 +111,21 @@ export const fakeDataInputSchemaMultiStepResponse: WorkflowDataInputSchemaRespon { name: 'Publish Github', functionRef: { - refName: 'runActionPublishGithub', + refName: 'runActionGitHubRepoPush', arguments: { - allowedHosts: ['"github.com"'], + id: '$WORKFLOW.instanceId', + title: '.ansibleJobDefinition.name + "-job"', description: 'Workflow Action', repoUrl: - '"github.com?owner=" + .orgName + "&repo=" + .repoName', + '"github.com?owner=" + .repositoryInfo.githubOrg + "&repo=" + .repositoryInfo.repoName', defaultBranch: 'main', gitCommitMessage: 'Initial commit', - allowAutoMerge: true, - allowRebaseMerge: true, + protectDefaultBranch: false, + protectEnforceAdmins: false, }, }, actionDataFilter: { - toStateData: '.actionPublishResult', + results: '.actionPublishResult', }, }, ], @@ -283,6 +148,7 @@ export const fakeDataInputSchemaMultiStepResponse: WorkflowDataInputSchemaRespon functionRef: { refName: 'runActionCatalogRegister', arguments: { + id: '$WORKFLOW.instanceId', repoContentsUrl: '.actionPublishResult.repoContentsUrl', catalogInfoPath: '"/catalog-info.yaml"', }, @@ -302,41 +168,7 @@ export const fakeDataInputSchemaMultiStepResponse: WorkflowDataInputSchemaRespon end: true, }, { - name: 'Handle Error', - type: 'operation', - actions: [ - { - name: 'Error Action', - functionRef: { - refName: 'sysout', - arguments: { - message: 'Error on workflow, triggering compensations', - }, - }, - }, - ], - end: { - compensate: true, - }, - }, - { - name: 'Clear File System - Source Code', - type: 'operation', - usedForCompensation: true, - actions: [ - { - name: 'Clear FS Action', - functionRef: { - refName: 'fs:delete', - arguments: { - files: ['./'], - }, - }, - }, - ], - }, - { - name: 'Clear File System - CI', + name: 'Clear Code and Catalog generation', type: 'operation', usedForCompensation: true, actions: [ @@ -352,206 +184,149 @@ export const fakeDataInputSchemaMultiStepResponse: WorkflowDataInputSchemaRespon ], }, { - name: 'Clear File System - Catalog', + name: 'Remove Source Code Repository', type: 'operation', usedForCompensation: true, actions: [ { - name: 'Clear FS Action', + name: 'Remove Source Code Repository', functionRef: { - refName: 'fs:delete', + refName: 'sysout', arguments: { - files: ['./'], + message: 'Remove Source Code Repository', }, }, }, ], }, { - name: 'Remove Source Code Repository', + name: 'Remove Catalog Info Component', type: 'operation', usedForCompensation: true, actions: [ { - name: 'Remove Source Code Repository', + name: 'Remove Catalog Info Component', functionRef: { refName: 'sysout', arguments: { - message: 'Remove Source Code Repository', + message: 'Remove Catalog Info Component', }, }, }, ], }, { - name: 'Remove Catalog Info Component', + name: 'Handle Error', type: 'operation', - usedForCompensation: true, actions: [ { - name: 'Remove Catalog Info Component', + name: 'Error Action', functionRef: { refName: 'sysout', arguments: { - message: 'Remove Catalog Info Component', + message: 'Error on workflow, triggering compensations', }, }, }, ], + end: { + compensate: true, + }, }, ], }, }, - schemas: [ + isComposedSchema: true, + schemaSteps: [ { - $id: 'classpath:/schemas/quarkus-backend__ref-schema__New_Component.json', - title: 'Provide information about the new component', - $schema: 'http://json-schema.org/draft-07/schema#', - type: 'object', - properties: { - orgName: { - title: 'Organization Name', - description: 'Organization name', - type: 'string', - }, - repoName: { - title: 'Repository Name', - description: 'Repository name', - type: 'string', - }, - description: { - title: 'Description', - description: 'Help others understand what this component is for', - type: 'string', - }, - owner: { - title: 'Owner', - description: 'An entity from the catalog', - type: 'string', - }, - system: { - title: 'System', - description: 'An entity from the catalog', - type: 'string', - }, - port: { - title: 'Port', - description: 'Override the port exposed for the application', - type: 'number', - default: 8080, - }, - }, - required: ['orgName', 'repoName', 'owner', 'system', 'port'], - }, - { - $id: 'classpath:/schemas/quarkus-backend__ref-schema__Java_Metadata.json', - title: 'Provide information about the Java metadata', - $schema: 'http://json-schema.org/draft-07/schema#', - type: 'object', - properties: { - groupId: { - title: 'Group ID', - description: 'Maven Group ID eg (io.janus)', - type: 'string', - default: 'io.janus', - }, - artifactId: { - title: 'Artifact ID', - description: 'Maven Artifact ID', - type: 'string', - default: 'quarkusapp', - }, - javaPackageName: { - title: 'Java Package Namespace', - description: - 'Name for the Java Package (ensure to use the / character as this is used for folder structure) should match Group ID and Artifact ID', - type: 'string', - default: 'io/janus/quarkusapp', - }, - version: { - title: 'Version', - description: 'Maven Artifact Version', - type: 'string', - default: '1.0.0-SNAPSHOT', + title: 'Provide information about the GitHub location', + key: 'repositoryInfo', + schema: { + $id: 'classpath:/schemas/ansible-job-template__ref-schema__GitHub_Repository_Info.json', + title: 'Provide information about the GitHub location', + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + properties: { + githubOrg: { + title: 'Organization Name', + description: 'GitHub organization name', + type: 'string', + }, + owner: { + title: 'Owner', + description: 'An entity from the catalog', + type: 'string', + }, + repoName: { + title: 'Repository Name', + description: 'GitHub repository name', + type: 'string', + }, + system: { + title: 'System', + description: 'An entity from the catalog', + type: 'string', + default: 'system:janus-idp', + }, }, + required: ['githubOrg', 'owner', 'repoName', 'system'], }, - required: ['groupId', 'artifactId', 'javaPackageName', 'version'], + data: {}, + readonlyKeys: [], }, { - $id: 'classpath:/schemas/quarkus-backend__ref-schema__CI_Method.json', - title: 'Provide information about the CI method', - $schema: 'http://json-schema.org/draft-07/schema#', - type: 'object', - properties: { - ci: { - title: 'CI Method', - type: 'string', - default: 'github', - oneOf: [ - { - const: 'github', - title: 'GitHub Action', - }, - { - const: 'tekton', - title: 'Tekton', - }, - ], - }, - }, - allOf: [ - { - if: { - properties: { - ci: { - const: 'github', - }, - }, + title: 'Ansible Job Definition', + key: 'ansibleJobDefinition', + schema: { + $id: 'classpath:/schemas/ansible-job-template__ref-schema__Ansible_Job_Definition.json', + title: 'Ansible Job Definition', + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + properties: { + name: { + title: 'Name of the Ansible Job', + description: 'A unique name for the Ansible Job', + type: 'string', }, - }, - { - if: { - properties: { - ci: { - const: 'tekton', - }, - }, + jobTemplate: { + title: 'Name of the Job Template to launch', + description: 'Specify a job template to launch', + type: 'string', }, - then: { - properties: { - imageRepository: { - title: 'Image Registry', - description: 'The registry to use', - type: 'string', - default: 'quay.io', - oneOf: [ - { - const: 'quay.io', - title: 'Quay', - }, - { - const: 'image-registry.openshift-image-registry.svc:5000', - title: 'Internal OpenShift Registry', - }, - ], - }, - imageUrl: { - title: 'Image URL', - description: - 'The Quay.io or OpenShift Image URL //', - type: 'string', - }, - namespace: { - title: 'Namespace', - description: 'The namespace for deploying resources', - type: 'string', - }, - }, - required: ['namespace', 'imageUrl', 'imageRepository'], + description: { + title: 'Description', + description: 'Provide a description of the Job to be launched', + type: 'string', + }, + namespace: { + title: 'Namespace', + description: 'Specify the namespace to launch the job', + type: 'string', + default: 'aap', + }, + connectionSecret: { + title: 'Connection Secret', + description: 'Specify the connection secret to use for the job', + type: 'string', + default: 'aapaccess', + }, + extraVars: { + title: 'Extra Vars', + description: 'Specify any extra vars to pass to the job', + type: 'string', + default: '{}', }, }, - ], + required: [ + 'name', + 'jobTemplate', + 'description', + 'namespace', + 'connectionSecret', + ], + }, + data: {}, + readonlyKeys: [], }, ], - initialState: { values: [], readonlyKeys: [] }, }; diff --git a/plugins/orchestrator/src/__fixtures__/fakeWorkflowDataInputSchemaResponseMultiStepInitialState.ts b/plugins/orchestrator/src/__fixtures__/fakeWorkflowDataInputSchemaResponseMultiStepInitialState.ts index 94d6717b83..efdad9e212 100644 --- a/plugins/orchestrator/src/__fixtures__/fakeWorkflowDataInputSchemaResponseMultiStepInitialState.ts +++ b/plugins/orchestrator/src/__fixtures__/fakeWorkflowDataInputSchemaResponseMultiStepInitialState.ts @@ -1,6 +1,6 @@ -import { WorkflowDataInputSchemaResponse } from '@janus-idp/backstage-plugin-orchestrator-common'; +import { WorkflowInputSchemaResponse } from '@janus-idp/backstage-plugin-orchestrator-common'; -export const fakeDataInputSchemaMultiStepInitialStateResponse: WorkflowDataInputSchemaResponse = +export const fakeDataInputSchemaMultiStepInitialStateResponse: WorkflowInputSchemaResponse = { workflowItem: { uri: 'quarkus-backend.sw.yaml', @@ -402,167 +402,177 @@ export const fakeDataInputSchemaMultiStepInitialStateResponse: WorkflowDataInput ], }, }, - schemas: [ + schemaSteps: [ { - $id: 'classpath:/schemas/quarkus-backend__ref-schema__New_Component.json', + key: 'newcomponent', + readonlyKeys: ['system'], title: 'Provide information about the new component', - $schema: 'http://json-schema.org/draft-07/schema#', - type: 'object', - properties: { - orgName: { - title: 'Organization Name', - description: 'Organization name', - type: 'string', - }, - repoName: { - title: 'Repository Name', - description: 'Repository name', - type: 'string', - }, - description: { - title: 'Description', - description: 'Help others understand what this component is for', - type: 'string', - }, - owner: { - title: 'Owner', - description: 'An entity from the catalog', - type: 'string', - }, - system: { - title: 'System', - description: 'An entity from the catalog', - type: 'string', - }, - port: { - title: 'Port', - description: 'Override the port exposed for the application', - type: 'number', - default: 8080, + data: { + system: 'system', + }, + schema: { + $id: 'classpath:/schemas/quarkus-backend__ref-schema__New_Component.json', + title: 'Provide information about the new component', + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + properties: { + orgName: { + title: 'Organization Name', + description: 'Organization name', + type: 'string', + }, + repoName: { + title: 'Repository Name', + description: 'Repository name', + type: 'string', + }, + description: { + title: 'Description', + description: 'Help others understand what this component is for', + type: 'string', + }, + owner: { + title: 'Owner', + description: 'An entity from the catalog', + type: 'string', + }, + system: { + title: 'System', + description: 'An entity from the catalog', + type: 'string', + }, + port: { + title: 'Port', + description: 'Override the port exposed for the application', + type: 'number', + default: 8080, + }, }, + required: ['orgName', 'repoName', 'owner', 'system', 'port'], }, - required: ['orgName', 'repoName', 'owner', 'system', 'port'], }, { - $id: 'classpath:/schemas/quarkus-backend__ref-schema__Java_Metadata.json', - title: 'Provide information about the Java metadata', - $schema: 'http://json-schema.org/draft-07/schema#', - type: 'object', - properties: { - groupId: { - title: 'Group ID', - description: 'Maven Group ID eg (io.janus)', - type: 'string', - default: 'io.janus', - }, - artifactId: { - title: 'Artifact ID', - description: 'Maven Artifact ID', - type: 'string', - default: 'quarkusapp', - }, - javaPackageName: { - title: 'Java Package Namespace', - description: - 'Name for the Java Package (ensure to use the / character as this is used for folder structure) should match Group ID and Artifact ID', - type: 'string', - default: 'io/janus/quarkusapp', - }, - version: { - title: 'Version', - description: 'Maven Artifact Version', - type: 'string', - default: '1.0.0-SNAPSHOT', + schema: { + $id: 'classpath:/schemas/quarkus-backend__ref-schema__Java_Metadata.json', + title: 'Provide information about the Java metadata', + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + properties: { + groupId: { + title: 'Group ID', + description: 'Maven Group ID eg (io.janus)', + type: 'string', + default: 'io.janus', + }, + artifactId: { + title: 'Artifact ID', + description: 'Maven Artifact ID', + type: 'string', + default: 'quarkusapp', + }, + javaPackageName: { + title: 'Java Package Namespace', + description: + 'Name for the Java Package (ensure to use the / character as this is used for folder structure) should match Group ID and Artifact ID', + type: 'string', + default: 'io/janus/quarkusapp', + }, + version: { + title: 'Version', + description: 'Maven Artifact Version', + type: 'string', + default: '1.0.0-SNAPSHOT', + }, }, + required: ['groupId', 'artifactId', 'javaPackageName', 'version'], }, - required: ['groupId', 'artifactId', 'javaPackageName', 'version'], + title: 'Provide information about the Java metadata', + key: 'javametadata', + data: {}, + readonlyKeys: [], }, { - $id: 'classpath:/schemas/quarkus-backend__ref-schema__CI_Method.json', - title: 'Provide information about the CI method', - $schema: 'http://json-schema.org/draft-07/schema#', - type: 'object', - properties: { - ci: { - title: 'CI Method', - type: 'string', - default: 'github', - oneOf: [ - { - const: 'github', - title: 'GitHub Action', - }, - { - const: 'tekton', - title: 'Tekton', - }, - ], - }, - }, - allOf: [ - { - if: { - properties: { - ci: { + schema: { + $id: 'classpath:/schemas/quarkus-backend__ref-schema__CI_Method.json', + title: 'Provide information about the CI method', + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + properties: { + ci: { + title: 'CI Method', + type: 'string', + default: 'github', + oneOf: [ + { const: 'github', + title: 'GitHub Action', }, - }, + { + const: 'tekton', + title: 'Tekton', + }, + ], }, }, - { - if: { - properties: { - ci: { - const: 'tekton', + allOf: [ + { + if: { + properties: { + ci: { + const: 'github', + }, }, }, }, - then: { - properties: { - imageRepository: { - title: 'Image Registry', - description: 'The registry to use', - type: 'string', - default: 'quay.io', - oneOf: [ - { - const: 'quay.io', - title: 'Quay', - }, - { - const: 'image-registry.openshift-image-registry.svc:5000', - title: 'Internal OpenShift Registry', - }, - ], - }, - imageUrl: { - title: 'Image URL', - description: - 'The Quay.io or OpenShift Image URL //', - type: 'string', + { + if: { + properties: { + ci: { + const: 'tekton', + }, }, - namespace: { - title: 'Namespace', - description: 'The namespace for deploying resources', - type: 'string', + }, + then: { + properties: { + imageRepository: { + title: 'Image Registry', + description: 'The registry to use', + type: 'string', + default: 'quay.io', + oneOf: [ + { + const: 'quay.io', + title: 'Quay', + }, + { + const: + 'image-registry.openshift-image-registry.svc:5000', + title: 'Internal OpenShift Registry', + }, + ], + }, + imageUrl: { + title: 'Image URL', + description: + 'The Quay.io or OpenShift Image URL //', + type: 'string', + }, + namespace: { + title: 'Namespace', + description: 'The namespace for deploying resources', + type: 'string', + }, }, + required: ['namespace', 'imageUrl', 'imageRepository'], }, - required: ['namespace', 'imageUrl', 'imageRepository'], }, - }, - ], + ], + }, + data: { ci: 'tekton' }, + key: 'ci', + title: 'Ci', + readonlyKeys: [], }, ], - initialState: { - values: [ - { - orgName: 'Org name', - repoName: 'Repo name', - description: 'Description', - owner: 'owner', - system: 'system', - }, - ], - readonlyKeys: ['orgName', 'system'], - }, + isComposedSchema: true, }; diff --git a/plugins/orchestrator/src/__fixtures__/fakeWorkflowDataInputSchemaResponse.ts b/plugins/orchestrator/src/__fixtures__/fakeWorkflowInputSchemaResponse.ts similarity index 70% rename from plugins/orchestrator/src/__fixtures__/fakeWorkflowDataInputSchemaResponse.ts rename to plugins/orchestrator/src/__fixtures__/fakeWorkflowInputSchemaResponse.ts index 8ecdbf307f..ff20633c19 100644 --- a/plugins/orchestrator/src/__fixtures__/fakeWorkflowDataInputSchemaResponse.ts +++ b/plugins/orchestrator/src/__fixtures__/fakeWorkflowInputSchemaResponse.ts @@ -1,6 +1,6 @@ -import { WorkflowDataInputSchemaResponse } from '@janus-idp/backstage-plugin-orchestrator-common'; +import { WorkflowInputSchemaResponse } from '@janus-idp/backstage-plugin-orchestrator-common'; -export const fakeDataInputSchemaResponse: WorkflowDataInputSchemaResponse = { +export const fakeDataInputSchemaResponse: WorkflowInputSchemaResponse = { workflowItem: { uri: 'yamlgreet.sw.yaml', definition: { @@ -73,23 +73,29 @@ export const fakeDataInputSchemaResponse: WorkflowDataInputSchemaResponse = { ], }, }, - schemas: [ + schemaSteps: [ { - $id: 'classpath:/schemas/yamlgreet__sub_schema__Additional_input_data.json', + readonlyKeys: [], + data: {}, + key: 'yamlgreet', title: 'yamlgreet: Additional input data', - $schema: 'http://json-schema.org/draft-07/schema#', - type: 'object', - required: ['language'], - properties: { - language: { - title: 'language', - type: 'string', - pattern: 'Spanish|English', - description: 'Extracted from the Workflow definition', - default: 'English', + schema: { + $id: 'classpath:/schemas/yamlgreet__sub_schema__Additional_input_data.json', + title: 'yamlgreet: Additional input data', + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + required: ['language'], + properties: { + language: { + title: 'language', + type: 'string', + pattern: 'Spanish|English', + description: 'Extracted from the Workflow definition', + default: 'English', + }, }, }, }, ], - initialState: { values: [], readonlyKeys: [] }, + isComposedSchema: false, }; diff --git a/plugins/orchestrator/src/api/MockOrchestratorClient.ts b/plugins/orchestrator/src/api/MockOrchestratorClient.ts index fba42647cc..cf339af02f 100644 --- a/plugins/orchestrator/src/api/MockOrchestratorClient.ts +++ b/plugins/orchestrator/src/api/MockOrchestratorClient.ts @@ -4,8 +4,8 @@ import { AssessedProcessInstance, Job, ProcessInstance, - WorkflowDataInputSchemaResponse, WorkflowExecutionResponse, + WorkflowInputSchemaResponse, WorkflowItem, WorkflowListResult, WorkflowOverview, @@ -149,7 +149,7 @@ export class MockOrchestratorClient implements OrchestratorApi { workflowId: string; instanceId?: string; assessmentInstanceId?: string; - }): Promise { + }): Promise { if ( !hasOwnProp(this._mockData, 'getWorkflowDataInputSchemaResponse') || !isNonNullable(this._mockData.getWorkflowDataInputSchemaResponse) diff --git a/plugins/orchestrator/src/api/OrchestratorClient.ts b/plugins/orchestrator/src/api/OrchestratorClient.ts index abe54bcbfa..636452977e 100644 --- a/plugins/orchestrator/src/api/OrchestratorClient.ts +++ b/plugins/orchestrator/src/api/OrchestratorClient.ts @@ -11,8 +11,8 @@ import { QUERY_PARAM_INCLUDE_ASSESSMENT, QUERY_PARAM_INSTANCE_ID, QUERY_PARAM_URI, - WorkflowDataInputSchemaResponse, WorkflowExecutionResponse, + WorkflowInputSchemaResponse, WorkflowItem, WorkflowListResult, WorkflowOverview, @@ -141,7 +141,7 @@ export class OrchestratorClient implements OrchestratorApi { workflowId: string; instanceId?: string; assessmentInstanceId?: string; - }): Promise { + }): Promise { const baseUrl = await this.getBaseUrl(); const endpoint = `${baseUrl}/workflows/${args.workflowId}/inputSchema`; const urlToFetch = buildUrl(endpoint, { diff --git a/plugins/orchestrator/src/api/api.ts b/plugins/orchestrator/src/api/api.ts index 695d4aaffc..84a14449bb 100644 --- a/plugins/orchestrator/src/api/api.ts +++ b/plugins/orchestrator/src/api/api.ts @@ -5,8 +5,8 @@ import { AssessedProcessInstance, Job, ProcessInstance, - WorkflowDataInputSchemaResponse, WorkflowExecutionResponse, + WorkflowInputSchemaResponse, WorkflowItem, WorkflowListResult, WorkflowOverview, @@ -42,7 +42,7 @@ export interface OrchestratorApi { workflowId: string; instanceId?: string; assessmentInstanceId?: string; - }): Promise; + }): Promise; createWorkflowDefinition( uri: string, diff --git a/plugins/orchestrator/src/components/ExecuteWorkflowPage/ExecuteWorkflowPage.stories.tsx b/plugins/orchestrator/src/components/ExecuteWorkflowPage/ExecuteWorkflowPage.stories.tsx index fbd5494525..ac8ee65a20 100644 --- a/plugins/orchestrator/src/components/ExecuteWorkflowPage/ExecuteWorkflowPage.stories.tsx +++ b/plugins/orchestrator/src/components/ExecuteWorkflowPage/ExecuteWorkflowPage.stories.tsx @@ -4,12 +4,13 @@ import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils'; import { Meta, StoryObj } from '@storybook/react'; -import { WorkflowDataInputSchemaResponse } from '@janus-idp/backstage-plugin-orchestrator-common'; +import { WorkflowInputSchemaResponse } from '@janus-idp/backstage-plugin-orchestrator-common'; import { fakeDataInputSchemaDifferentTypes } from '../../__fixtures__/fakeWorkflowDataInputSchemaDifferentTypes'; -import { fakeDataInputSchemaResponse } from '../../__fixtures__/fakeWorkflowDataInputSchemaResponse'; import { fakeDataInputSchemaMultiStepResponse } from '../../__fixtures__/fakeWorkflowDataInputSchemaResponseMultiStep'; import { fakeDataInputSchemaMultiStepInitialStateResponse } from '../../__fixtures__/fakeWorkflowDataInputSchemaResponseMultiStepInitialState'; +import { fakeDataInputSchemaResponse } from '../../__fixtures__/fakeWorkflowInputSchemaResponse'; +import { fakeWorkflowItem } from '../../__fixtures__/fakeWorkflowItem'; import { orchestratorApiRef } from '../../api'; import { MockOrchestratorClient } from '../../api/MockOrchestratorClient'; import { orchestratorRootRouteRef } from '../../routes'; @@ -23,7 +24,7 @@ const meta = { _, context?: { args?: { - schemaResponse?: () => Promise; + schemaResponse?: () => Promise; }; }, ) => @@ -91,8 +92,9 @@ export const ExecuteWorkflowPageNoSchemaStory: Story = { name: 'No schema', args: { schemaResponse: () => ({ - ...fakeDataInputSchemaResponse, - schemas: [], + workflowItem: fakeWorkflowItem, + isComposedSchema: false, + schemaSteps: [], }), }, }; diff --git a/plugins/orchestrator/src/components/ExecuteWorkflowPage/ExecuteWorkflowPage.tsx b/plugins/orchestrator/src/components/ExecuteWorkflowPage/ExecuteWorkflowPage.tsx index a07c500ae2..9ce9d33292 100644 --- a/plugins/orchestrator/src/components/ExecuteWorkflowPage/ExecuteWorkflowPage.tsx +++ b/plugins/orchestrator/src/components/ExecuteWorkflowPage/ExecuteWorkflowPage.tsx @@ -20,7 +20,7 @@ import { Grid } from '@material-ui/core'; import { QUERY_PARAM_ASSESSMENT_INSTANCE_ID, QUERY_PARAM_INSTANCE_ID, - WorkflowDataInputSchemaResponse, + WorkflowInputSchemaResponse, } from '@janus-idp/backstage-plugin-orchestrator-common'; import { orchestratorApiRef } from '../../api'; @@ -49,7 +49,7 @@ export const ExecuteWorkflowPage = () => { loading, error: responseError, } = useAsync( - async (): Promise => + async (): Promise => await orchestratorApi.getWorkflowDataInputSchema({ workflowId, instanceId, @@ -107,12 +107,23 @@ export const ExecuteWorkflowPage = () => { )} + {schemaResponse.schemaParseError && ( + + + + )} - {schemaResponse.schemas.length > 0 ? ( + {schemaResponse.schemaSteps.length > 0 ? ( diff --git a/plugins/orchestrator/src/components/ExecuteWorkflowPage/StepperForm.tsx b/plugins/orchestrator/src/components/ExecuteWorkflowPage/StepperForm.tsx index 38ec2aa8d6..5a2eb1ad8b 100644 --- a/plugins/orchestrator/src/components/ExecuteWorkflowPage/StepperForm.tsx +++ b/plugins/orchestrator/src/components/ExecuteWorkflowPage/StepperForm.tsx @@ -17,40 +17,55 @@ import { FormProps, withTheme } from '@rjsf/core-v5'; import { Theme as MuiTheme } from '@rjsf/material-ui-v5'; import { UiSchema } from '@rjsf/utils'; import validator from '@rjsf/validator-ajv8'; -import { JSONSchema7 } from 'json-schema'; -import { DataInputSchemaInitialState } from '@janus-idp/backstage-plugin-orchestrator-common'; +import { WorkflowInputSchemaStep } from '@janus-idp/backstage-plugin-orchestrator-common'; import SubmitButton from '../SubmitButton'; const MuiForm = withTheme(MuiTheme); +const getCombinedData = ( + steps: WorkflowInputSchemaStep[], + isComposedSchema: boolean, +): JsonObject => { + if (!isComposedSchema) { + return steps[0].data; + } + return steps.reduce( + (prev, { key, data }) => ({ ...prev, [key]: data }), + {}, + ); +}; + const ReviewStep = ({ busy, - formDataObjects, + steps, + isComposedSchema, handleBack, handleReset, handleExecute, }: { busy: boolean; - formDataObjects: JsonObject[]; + steps: WorkflowInputSchemaStep[]; + isComposedSchema: boolean; handleBack: () => void; handleReset: () => void; - handleExecute: () => void; + handleExecute: (getParameters: () => JsonObject) => Promise; }) => { - const combinedFormData = React.useMemo( - () => - formDataObjects.reduce( - (prev, cur) => ({ ...prev, ...cur }), - {}, - ), - [formDataObjects], - ); + const displayData: JsonObject = React.useMemo(() => { + if (!isComposedSchema) { + return steps[0].data; + } + return steps.reduce( + (prev, { title, data }) => ({ ...prev, [title]: data }), + {}, + ); + }, [steps, isComposedSchema]); return ( Review and run - + + handleExecute(() => getCombinedData(steps, isComposedSchema)) + } submitting={busy} focusOnMount > @@ -71,28 +88,32 @@ const ReviewStep = ({ }; const FormWrapper = ({ - formData, - schema, - uiSchema = {}, + step, onSubmit, children, -}: Pick< - FormProps, - 'formData' | 'schema' | 'uiSchema' | 'onSubmit' | 'children' ->) => { - const firstKey = Object.keys(schema?.properties ?? {})[0]; - uiSchema[firstKey] = firstKey - ? { ...uiSchema[firstKey], 'ui:autofocus': 'true' } - : uiSchema[firstKey]; +}: Pick, 'onSubmit' | 'children'> & { + step: WorkflowInputSchemaStep; +}) => { + const firstKey = Object.keys(step.schema.properties ?? {})[0]; + const uiSchema = React.useMemo(() => { + const res: UiSchema = firstKey + ? { [firstKey]: { 'ui:autofocus': 'true' } } + : {}; + for (const key of step.readonlyKeys) { + res[key] = { 'ui:disabled': 'true' }; + } + return res; + }, [firstKey, step.readonlyKeys]); + return ( {children} @@ -100,75 +121,45 @@ const FormWrapper = ({ }; const StepperForm = ({ - refSchemas, - initialState, + isComposedSchema, + steps: inputSteps, handleExecute, isExecuting, }: { - refSchemas: JSONSchema7[]; - initialState: DataInputSchemaInitialState; + isComposedSchema: boolean; + steps: WorkflowInputSchemaStep[]; handleExecute: (getParameters: () => JsonObject) => Promise; isExecuting: boolean; }) => { const [activeStep, setActiveStep] = React.useState(0); const handleBack = () => setActiveStep(activeStep - 1); - const [formDataObjects, setFormDataObjects] = React.useState( - initialState.values, - ); - - const getFormData = () => - formDataObjects.reduce( - (prev, curFormObject) => ({ ...prev, ...curFormObject }), - {}, - ); - - const [uiSchema, setUiSchema] = React.useState< - UiSchema | undefined - >(() => { - if (!initialState.readonlyKeys) { - return undefined; - } - - return initialState.readonlyKeys.reduce>( - (obj, key) => ({ - ...obj, - [key]: { ...obj[key], 'ui:disabled': 'true' }, - }), - {}, - ); - }); - - const resetFormDataObjects = React.useCallback(() => { - setFormDataObjects( - refSchemas.reduce(prev => [...prev, {}], []), - ); - setUiSchema(undefined); - }, [refSchemas]); - + const [steps, setSteps] = React.useState([...inputSteps]); return ( <> - {refSchemas.map((schema, index) => ( - + {steps?.map((step, index) => ( + - {schema.title} + {step.title} { - const newDataObjects = [...formDataObjects]; - newDataObjects.splice(index, 1, e.formData ?? {}); - setFormDataObjects(newDataObjects); + const newStep: WorkflowInputSchemaStep = { + ...step, + data: e.formData ?? {}, + }; + const newSteps = [...steps]; + newSteps.splice(index, 1, newStep); + setSteps(newSteps); setActiveStep(activeStep + 1); }} > @@ -183,16 +174,17 @@ const StepperForm = ({ ))} - {activeStep === refSchemas.length && ( + {activeStep === steps.length && ( { - resetFormDataObjects(); + setSteps([...inputSteps]); setActiveStep(0); }} + handleExecute={handleExecute} busy={isExecuting} - handleExecute={() => handleExecute(() => getFormData())} /> )}