From 757dd4470b7b7dd8bb2b24afb267a54a1b71b1e4 Mon Sep 17 00:00:00 2001 From: Bat-Zion Rotman Date: Thu, 15 Aug 2024 22:10:11 +0300 Subject: [PATCH] feat(orchestrator): provide API for overriding workflow execution form properties (#2054) * feat(orchestrator): provide API for overriding workflow execution form properties * Update plugins/orchestrator-form-api/README.md fix typo --------- Co-authored-by: Nick Boldt --- plugins/orchestrator-form-api/.eslintrc.js | 1 + plugins/orchestrator-form-api/README.md | 87 +++++++++++++++++++ plugins/orchestrator-form-api/package.json | 45 ++++++++++ plugins/orchestrator-form-api/src/api.ts | 20 +++++ plugins/orchestrator-form-api/src/index.ts | 3 + plugins/orchestrator-form-api/tsconfig.json | 9 ++ plugins/orchestrator-form-api/turbo.json | 9 ++ plugins/orchestrator-form-react/.eslintrc.js | 1 + plugins/orchestrator-form-react/README.md | 3 + plugins/orchestrator-form-react/package.json | 55 ++++++++++++ .../src/DefaultFormApi.tsx | 23 +++++ .../src/components/OrchestratorForm.tsx} | 43 +++++---- .../src/components/SubmitButton.tsx | 0 .../src/components/index.ts | 2 + plugins/orchestrator-form-react/src/index.ts | 11 +++ plugins/orchestrator-form-react/tsconfig.json | 9 ++ plugins/orchestrator-form-react/turbo.json | 9 ++ plugins/orchestrator/package.json | 17 ++-- .../ExecuteWorkflowPage.tsx | 4 +- .../ExecuteWorkflowPage/JsonTextAreaForm.tsx | 2 +- 20 files changed, 325 insertions(+), 28 deletions(-) create mode 100644 plugins/orchestrator-form-api/.eslintrc.js create mode 100644 plugins/orchestrator-form-api/README.md create mode 100644 plugins/orchestrator-form-api/package.json create mode 100644 plugins/orchestrator-form-api/src/api.ts create mode 100644 plugins/orchestrator-form-api/src/index.ts create mode 100644 plugins/orchestrator-form-api/tsconfig.json create mode 100644 plugins/orchestrator-form-api/turbo.json create mode 100644 plugins/orchestrator-form-react/.eslintrc.js create mode 100644 plugins/orchestrator-form-react/README.md create mode 100644 plugins/orchestrator-form-react/package.json create mode 100644 plugins/orchestrator-form-react/src/DefaultFormApi.tsx rename plugins/{orchestrator/src/components/ExecuteWorkflowPage/StepperForm.tsx => orchestrator-form-react/src/components/OrchestratorForm.tsx} (82%) rename plugins/{orchestrator => orchestrator-form-react}/src/components/SubmitButton.tsx (100%) create mode 100644 plugins/orchestrator-form-react/src/components/index.ts create mode 100644 plugins/orchestrator-form-react/src/index.ts create mode 100644 plugins/orchestrator-form-react/tsconfig.json create mode 100644 plugins/orchestrator-form-react/turbo.json diff --git a/plugins/orchestrator-form-api/.eslintrc.js b/plugins/orchestrator-form-api/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/orchestrator-form-api/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/orchestrator-form-api/README.md b/plugins/orchestrator-form-api/README.md new file mode 100644 index 0000000000..6877bc7b85 --- /dev/null +++ b/plugins/orchestrator-form-api/README.md @@ -0,0 +1,87 @@ +# @janus-idp/backstage-plugin-orchestrator-form-api + +### Overview + +This library offers the flexibility to completely override all [properties](https://rjsf-team.github.io/react-jsonschema-form/docs/api-reference/form-props) of the `react-jsonschema-form` workflow execution form component. It allows customers to provide a custom decorator for the form component, which can be defined by implementing a dynamic frontend plugin. This decorator enables users to: + +- **Dynamic Validations:** Override the `extraErrors` property to implement dynamic validation logic. +- **Custom Components:** Replace default components by overriding the `widgets` property. +- **Correlated Field Values:** Implement complex inter-field dependencies by overriding the `onChange` property. +- **Additional Customizations:** Make other necessary adjustments by overriding additional properties. + +The decorator will be provided through a factory method that leverages a [Backstage utility API](https://backstage.io/docs/api/utility-apis) offered by the orchestrator. + +### Interface Provided in this package + +```typescript +export type FormDecorator = ( + FormComponent: React.ComponentType>, +) => React.ComponentType; + +export interface FormExtensionsApi { + getFormDecorator(): FormDecorator; +} +``` + +### Example API Implementation + +```typescript +class CustomFormExtensionsApi implements FormExtensionsApi { + getFormDecorator() { + return (FormComponent: React.ComponentType>) => { + const widgets = { + color1: ColorWidget + }; + return () => ; + }; + } +} +``` + +### Plugin Creation Example + +```typescript +export const testFactoryPlugin = createPlugin({ + id: 'testfactory', + routes: { + root: rootRouteRef, + }, + apis: [ + createApiFactory({ + api: formExtensionsApiRef, + deps: {}, + factory() { + return new CustomApi(); + }, + }), + ], +}); +``` + +### Schema example for above plugin + +```typescript +{ + "title": "Product", + "type": "object", + "properties": { + "name": { + "type": "string", + "title": "Product Name" + }, + "color": { + "type": "string", + "title": "Product Color", + "description": "The color of the product", + "ui:widget": "color1" + } + }, + "required": ["name", "color"] +} +``` + +### Additional Details + +The workflow execution schema adheres to the [json-schema](https://json-schema.org/) format, which allows for extending the schema with custom properties beyond the official specification. This flexibility enables the inclusion of additional [UiSchema](https://rjsf-team.github.io/react-jsonschema-form/docs/api-reference/uiSchema/) fields directly within the schema, as demonstrated in the example above. + +The orchestrator plugin handles the separation of UI schema fields from the main schema. By default, it also organizes the form into wizard steps based on an additional hierarchical structure within the JSON schema. This behavior is built into the orchestrator plugin but can be customized or overridden using the provided decorator. diff --git a/plugins/orchestrator-form-api/package.json b/plugins/orchestrator-form-api/package.json new file mode 100644 index 0000000000..3b79a8edc0 --- /dev/null +++ b/plugins/orchestrator-form-api/package.json @@ -0,0 +1,45 @@ +{ + "name": "@janus-idp/backstage-plugin-orchestrator-form-api", + "description": "library for orchestrator form api, enabling creating a factory to extend the workflow execution form", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "web-library" + }, + "sideEffects": false, + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "tsc": "tsc" + }, + "dependencies": { + "@backstage/core-plugin-api": "^1.9.3", + "@backstage/types": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0", + "@rjsf/core": "^5.17.1" + }, + "devDependencies": { + "@backstage/cli": "0.26.11", + "@types/json-schema": "^7.0.15" + }, + "files": [ + "dist" + ], + "maintainers": [ + "@janus-idp/maintainers-plugins", + "@janus-idp/orchestrator-codeowners" + ] +} diff --git a/plugins/orchestrator-form-api/src/api.ts b/plugins/orchestrator-form-api/src/api.ts new file mode 100644 index 0000000000..e284a94242 --- /dev/null +++ b/plugins/orchestrator-form-api/src/api.ts @@ -0,0 +1,20 @@ +import { createApiRef } from '@backstage/core-plugin-api'; +import { JsonObject } from '@backstage/types'; + +import { FormProps } from '@rjsf/core'; +// eslint-disable-next-line @backstage/no-undeclared-imports +import { JSONSchema7 } from 'json-schema'; + +export type OrchestratorFormDecorator = ( + FormComponent: React.ComponentType< + Partial> + >, +) => React.ComponentType; + +export interface OrchestratorFormApi { + getFormDecorator(): OrchestratorFormDecorator; +} + +export const orchestratorFormApiRef = createApiRef({ + id: 'plugin.orchestrator.form', +}); diff --git a/plugins/orchestrator-form-api/src/index.ts b/plugins/orchestrator-form-api/src/index.ts new file mode 100644 index 0000000000..d1f668b0e3 --- /dev/null +++ b/plugins/orchestrator-form-api/src/index.ts @@ -0,0 +1,3 @@ +export { orchestratorFormApiRef } from './api'; +export type { OrchestratorFormApi } from './api'; +export type { OrchestratorFormDecorator } from './api'; diff --git a/plugins/orchestrator-form-api/tsconfig.json b/plugins/orchestrator-form-api/tsconfig.json new file mode 100644 index 0000000000..e036e04dce --- /dev/null +++ b/plugins/orchestrator-form-api/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@backstage/cli/config/tsconfig.json", + "include": ["src"], + "exclude": ["node_modules"], + "compilerOptions": { + "outDir": "../../dist-types/plugins/orchestrator-form-api", + "rootDir": "." + } +} diff --git a/plugins/orchestrator-form-api/turbo.json b/plugins/orchestrator-form-api/turbo.json new file mode 100644 index 0000000000..9f8d875cfc --- /dev/null +++ b/plugins/orchestrator-form-api/turbo.json @@ -0,0 +1,9 @@ +{ + "extends": ["//"], + "pipeline": { + "tsc": { + "outputs": ["../../dist-types/plugins/orchestrator-form-react/**"], + "dependsOn": ["^tsc"] + } + } +} diff --git a/plugins/orchestrator-form-react/.eslintrc.js b/plugins/orchestrator-form-react/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/orchestrator-form-react/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/orchestrator-form-react/README.md b/plugins/orchestrator-form-react/README.md new file mode 100644 index 0000000000..18ced4033e --- /dev/null +++ b/plugins/orchestrator-form-react/README.md @@ -0,0 +1,3 @@ +# backstage-plugin-orchestrator-form-react + +This library provides the form component used in the workflow execution form. It is decoupled from the orchestrator plugin to allow plugins implementing the OrchestratorFormApi to test their behavior independently. diff --git a/plugins/orchestrator-form-react/package.json b/plugins/orchestrator-form-react/package.json new file mode 100644 index 0000000000..eeaa8f50b0 --- /dev/null +++ b/plugins/orchestrator-form-react/package.json @@ -0,0 +1,55 @@ +{ + "name": "@janus-idp/backstage-plugin-orchestrator-form-react", + "description": "Web library for the orchestrator-form plugin", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "web-library" + }, + "sideEffects": false, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "tsc": "tsc" + }, + "dependencies": { + "@backstage/core-components": "^0.14.9", + "@backstage/core-plugin-api": "^1.9.3", + "@backstage/types": "^1.1.1", + "@janus-idp/backstage-plugin-orchestrator-common": "^1.14.0", + "@janus-idp/backstage-plugin-orchestrator-form-api": "^0.1.0", + "json-schema": "^0.4.0" + }, + "peerDependencies": { + "@material-ui/core": "^4.12.4", + "@rjsf/core": "^5.17.1", + "@rjsf/material-ui": "^5.17.1", + "@rjsf/utils": "^5.17.1", + "@rjsf/validator-ajv8": "^5.17.1", + "react": "^16.13.1 || ^17.0.0 || ^18.0.0" + }, + "devDependencies": { + "@backstage/cli": "0.26.11", + "@types/json-schema": "^7.0.15" + }, + "files": [ + "dist" + ], + "maintainers": [ + "@janus-idp/maintainers-plugins", + "@janus-idp/orchestrator-codeowners" + ] +} diff --git a/plugins/orchestrator-form-react/src/DefaultFormApi.tsx b/plugins/orchestrator-form-react/src/DefaultFormApi.tsx new file mode 100644 index 0000000000..b3dd08b0a8 --- /dev/null +++ b/plugins/orchestrator-form-react/src/DefaultFormApi.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +import { JsonObject } from '@backstage/types'; + +import { FormProps } from '@rjsf/core'; +import { JSONSchema7 } from 'json-schema'; + +import { + OrchestratorFormApi, + OrchestratorFormDecorator, +} from '@janus-idp/backstage-plugin-orchestrator-form-api'; + +class DefaultFormApi implements OrchestratorFormApi { + getFormDecorator(): OrchestratorFormDecorator { + return ( + FormComponent: React.ComponentType< + Partial> + >, + ) => FormComponent; + } +} + +export const defaultFormExtensionsApi = new DefaultFormApi(); diff --git a/plugins/orchestrator/src/components/ExecuteWorkflowPage/StepperForm.tsx b/plugins/orchestrator-form-react/src/components/OrchestratorForm.tsx similarity index 82% rename from plugins/orchestrator/src/components/ExecuteWorkflowPage/StepperForm.tsx rename to plugins/orchestrator-form-react/src/components/OrchestratorForm.tsx index e0582695ad..6533b00ceb 100644 --- a/plugins/orchestrator/src/components/ExecuteWorkflowPage/StepperForm.tsx +++ b/plugins/orchestrator-form-react/src/components/OrchestratorForm.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Content, StructuredMetadataTable } from '@backstage/core-components'; +import { useApiHolder } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; import { @@ -19,8 +20,10 @@ import { RJSFSchema, UiSchema } from '@rjsf/utils'; import validator from '@rjsf/validator-ajv8'; import { WorkflowInputSchemaStep } from '@janus-idp/backstage-plugin-orchestrator-common'; +import { orchestratorFormApiRef } from '@janus-idp/backstage-plugin-orchestrator-form-api'; -import SubmitButton from '../SubmitButton'; +import { defaultFormExtensionsApi } from '../DefaultFormApi'; +import SubmitButton from './SubmitButton'; const Form = withTheme(MuiTheme); @@ -94,6 +97,9 @@ const FormWrapper = ({ }: Pick, 'onSubmit' | 'children'> & { step: WorkflowInputSchemaStep; }) => { + const formApi = + useApiHolder().get(orchestratorFormApiRef) || defaultFormExtensionsApi; + const withFormExtensions = formApi.getFormDecorator(); const firstKey = Object.keys(step.schema.properties ?? {})[0]; const uiSchema = React.useMemo(() => { const res: UiSchema = firstKey @@ -105,22 +111,27 @@ const FormWrapper = ({ return res; }, [firstKey, step.readonlyKeys]); - return ( -
- {children} -
- ); + const schema = { ...step.schema, title: '' }; // the title is in the step + + const FormComponent = (props: Partial) => { + return ( +
+ {children} +
+ ); + }; + const NewComponent = withFormExtensions(FormComponent); + return ; }; -const StepperForm = ({ +const OrchestratorForm = ({ isComposedSchema, steps: inputSteps, handleExecute, @@ -194,4 +205,4 @@ const StepperForm = ({ ); }; -export default StepperForm; +export default OrchestratorForm; diff --git a/plugins/orchestrator/src/components/SubmitButton.tsx b/plugins/orchestrator-form-react/src/components/SubmitButton.tsx similarity index 100% rename from plugins/orchestrator/src/components/SubmitButton.tsx rename to plugins/orchestrator-form-react/src/components/SubmitButton.tsx diff --git a/plugins/orchestrator-form-react/src/components/index.ts b/plugins/orchestrator-form-react/src/components/index.ts new file mode 100644 index 0000000000..dbeaa645c5 --- /dev/null +++ b/plugins/orchestrator-form-react/src/components/index.ts @@ -0,0 +1,2 @@ +export { default as OrchestratorForm } from './OrchestratorForm'; +export { default as SubmitButton } from './SubmitButton'; diff --git a/plugins/orchestrator-form-react/src/index.ts b/plugins/orchestrator-form-react/src/index.ts new file mode 100644 index 0000000000..3b6a26ca2f --- /dev/null +++ b/plugins/orchestrator-form-react/src/index.ts @@ -0,0 +1,11 @@ +/***/ +/** + * Web library for the orchestrator-form plugin. + * + * @packageDocumentation + */ + +// In this package you might for example export components or hooks +// that are useful to other plugins or modules. + +export * from './components'; diff --git a/plugins/orchestrator-form-react/tsconfig.json b/plugins/orchestrator-form-react/tsconfig.json new file mode 100644 index 0000000000..d94e805db0 --- /dev/null +++ b/plugins/orchestrator-form-react/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@backstage/cli/config/tsconfig.json", + "include": ["src"], + "exclude": ["node_modules"], + "compilerOptions": { + "outDir": "../../dist-types/plugins/orchestrator-form-react", + "rootDir": "." + } +} diff --git a/plugins/orchestrator-form-react/turbo.json b/plugins/orchestrator-form-react/turbo.json new file mode 100644 index 0000000000..9f8d875cfc --- /dev/null +++ b/plugins/orchestrator-form-react/turbo.json @@ -0,0 +1,9 @@ +{ + "extends": ["//"], + "pipeline": { + "tsc": { + "outputs": ["../../dist-types/plugins/orchestrator-form-react/**"], + "dependsOn": ["^tsc"] + } + } +} diff --git a/plugins/orchestrator/package.json b/plugins/orchestrator/package.json index dec8218a5b..d67db896ca 100644 --- a/plugins/orchestrator/package.json +++ b/plugins/orchestrator/package.json @@ -59,6 +59,8 @@ "@backstage/plugin-permission-react": "^0.4.24", "@backstage/types": "^1.1.1", "@janus-idp/backstage-plugin-orchestrator-common": "1.14.0", + "@janus-idp/backstage-plugin-orchestrator-form-api": "^0.1.0", + "@janus-idp/backstage-plugin-orchestrator-form-react": "^0.1.0", "@kie-tools-core/editor": "^0.32.0", "@kie-tools-core/notifications": "^0.32.0", "@kie-tools-core/react-hooks": "^0.32.0", @@ -80,25 +82,22 @@ "@backstage/cli": "0.26.11", "@backstage/dev-utils": "1.0.36", "@backstage/test-utils": "1.5.9", + "@backstage/types": "^1.1.1", "@janus-idp/cli": "1.13.0", "@redhat-developer/red-hat-developer-hub-theme": "0.2.0", "@storybook/preview-api": "7.6.20", "@storybook/react": "7.6.20", - "@testing-library/react": "14.3.1" + "@testing-library/react": "14.3.1", + "@types/json-schema": "^7.0.15" }, "peerDependencies": { - "react": "^16.13.1 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0", - "react-router-dom": "^6.0.0", "@material-ui/core": "^4.12.4", "@material-ui/icons": "^4.11.3", "@material-ui/lab": "^4.0.0-alpha.45", - "@monaco-editor/react": "^4.6.0", "@mui/icons-material": "^5.15.8", - "@rjsf/core": "^5.17.1", - "@rjsf/material-ui": "^5.17.1", - "@rjsf/utils": "^5.17.1", - "@rjsf/validator-ajv8": "^5.17.1" + "react": "^16.13.1 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0", + "react-router-dom": "^6.0.0" }, "scalprum": { "name": "janus-idp.backstage-plugin-orchestrator", diff --git a/plugins/orchestrator/src/components/ExecuteWorkflowPage/ExecuteWorkflowPage.tsx b/plugins/orchestrator/src/components/ExecuteWorkflowPage/ExecuteWorkflowPage.tsx index 1b34dda596..8b9a0f5e46 100644 --- a/plugins/orchestrator/src/components/ExecuteWorkflowPage/ExecuteWorkflowPage.tsx +++ b/plugins/orchestrator/src/components/ExecuteWorkflowPage/ExecuteWorkflowPage.tsx @@ -23,6 +23,7 @@ import { QUERY_PARAM_INSTANCE_STATE, WorkflowInputSchemaResponse, } from '@janus-idp/backstage-plugin-orchestrator-common'; +import { OrchestratorForm } from '@janus-idp/backstage-plugin-orchestrator-form-react'; import { orchestratorApiRef } from '../../api'; import { @@ -32,7 +33,6 @@ import { import { getErrorObject } from '../../utils/ErrorUtils'; import { BaseOrchestratorPage } from '../BaseOrchestratorPage'; import JsonTextAreaForm from './JsonTextAreaForm'; -import StepperForm from './StepperForm'; export const ExecuteWorkflowPage = () => { const orchestratorApi = useApi(orchestratorApiRef); @@ -162,7 +162,7 @@ export const ExecuteWorkflowPage = () => { {schemaResponse.schemaSteps.length > 0 ? ( -