Skip to content

Commit

Permalink
feat(orchestrator): provide API for overriding workflow execution for…
Browse files Browse the repository at this point in the history
…m properties
  • Loading branch information
batzionb committed Aug 14, 2024
1 parent 231c102 commit c2fb8bc
Show file tree
Hide file tree
Showing 20 changed files with 327 additions and 30 deletions.
1 change: 1 addition & 0 deletions plugins/orchestrator-form-api/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
87 changes: 87 additions & 0 deletions plugins/orchestrator-form-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# @janus-idp/backstage-plugin-orchestrator-form-api-react

### 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<Partial<FormProps>>,
) => React.ComponentType;

export interface FormExtensionsApi {
getFormDecorator(): FormDecorator;
}
```

### Example API Implementation

```typescript
class CustomFormExtensionsApi implements FormExtensionsApi {
getFormDecorator() {
return (FormComponent: React.ComponentType<Partial<FormProps>>) => {
const widgets = {
color1: ColorWidget
};
return () => <FormComponent widgets={widgets} />;
};
}
}
```

### 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.
45 changes: 45 additions & 0 deletions plugins/orchestrator-form-api/package.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
20 changes: 20 additions & 0 deletions plugins/orchestrator-form-api/src/api.ts
Original file line number Diff line number Diff line change
@@ -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<FormProps<JsonObject, JSONSchema7>>
>,
) => React.ComponentType;

export interface OrchestratorFormApi {
getFormDecorator(): OrchestratorFormDecorator;
}

export const orchestratorFormApiRef = createApiRef<OrchestratorFormApi>({
id: 'plugin.orchestrator.form',
});
3 changes: 3 additions & 0 deletions plugins/orchestrator-form-api/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { orchestratorFormApiRef } from './api';
export type { OrchestratorFormApi } from './api';
export type { OrchestratorFormDecorator } from './api';
9 changes: 9 additions & 0 deletions plugins/orchestrator-form-api/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "@backstage/cli/config/tsconfig.json",
"include": ["src"],
"exclude": ["node_modules"],
"compilerOptions": {
"outDir": "../../dist-types/plugins/orchestrator-form-api",
"rootDir": "."
}
}
9 changes: 9 additions & 0 deletions plugins/orchestrator-form-api/turbo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": ["//"],
"pipeline": {
"tsc": {
"outputs": ["../../dist-types/plugins/orchestrator-form-react/**"],
"dependsOn": ["^tsc"]
}
}
}
1 change: 1 addition & 0 deletions plugins/orchestrator-form-react/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
3 changes: 3 additions & 0 deletions plugins/orchestrator-form-react/README.md
Original file line number Diff line number Diff line change
@@ -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.
55 changes: 55 additions & 0 deletions plugins/orchestrator-form-react/package.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
23 changes: 23 additions & 0 deletions plugins/orchestrator-form-react/src/DefaultFormApi.tsx
Original file line number Diff line number Diff line change
@@ -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<FormProps<JsonObject, JSONSchema7>>
>,
) => FormComponent;
}
}

export const defaultFormExtensionsApi = new DefaultFormApi();
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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);

Expand Down Expand Up @@ -94,6 +97,9 @@ const FormWrapper = ({
}: Pick<FormProps<JsonObject>, '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<any, RJSFSchema, any> = firstKey
Expand All @@ -105,22 +111,27 @@ const FormWrapper = ({
return res;
}, [firstKey, step.readonlyKeys]);

return (
<Form
validator={validator}
showErrorList={false}
noHtml5Validate
formData={step.data}
schema={{ ...step.schema, title: '' }} // title is in step
onSubmit={onSubmit}
uiSchema={uiSchema}
>
{children}
</Form>
);
const schema = { ...step.schema, title: '' }; // the title is in the step

const FormComponent = (props: Partial<FormProps>) => {
return (
<Form
validator={props.validator || validator}
schema={props.schema || schema}
uiSchema={props.uiSchema || uiSchema}
noHtml5Validate={props.noHtml5Validate || true}
onSubmit={props.onSubmit || onSubmit}
{...props}
>
{children}
</Form>
);
};
const NewComponent = withFormExtensions(FormComponent);
return <NewComponent />;
};

const StepperForm = ({
const OrchestratorForm = ({
isComposedSchema,
steps: inputSteps,
handleExecute,
Expand Down Expand Up @@ -194,4 +205,4 @@ const StepperForm = ({
);
};

export default StepperForm;
export default OrchestratorForm;
2 changes: 2 additions & 0 deletions plugins/orchestrator-form-react/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as OrchestratorForm } from './OrchestratorForm';
export { default as SubmitButton } from './SubmitButton';
11 changes: 11 additions & 0 deletions plugins/orchestrator-form-react/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
9 changes: 9 additions & 0 deletions plugins/orchestrator-form-react/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "@backstage/cli/config/tsconfig.json",
"include": ["src"],
"exclude": ["node_modules"],
"compilerOptions": {
"outDir": "../../dist-types/plugins/orchestrator-form-react",
"rootDir": "."
}
}
9 changes: 9 additions & 0 deletions plugins/orchestrator-form-react/turbo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": ["//"],
"pipeline": {
"tsc": {
"outputs": ["../../dist-types/plugins/orchestrator-form-react/**"],
"dependsOn": ["^tsc"]
}
}
}
Loading

0 comments on commit c2fb8bc

Please sign in to comment.