Skip to content

Commit

Permalink
fix(orchestrator): fix issues with extendable workflow execution form (
Browse files Browse the repository at this point in the history
  • Loading branch information
batzionb authored Nov 4, 2024
1 parent 9da7728 commit 67f466a
Show file tree
Hide file tree
Showing 20 changed files with 301 additions and 147 deletions.
12 changes: 12 additions & 0 deletions .changeset/silent-glasses-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@janus-idp/backstage-plugin-orchestrator-form-react": patch
"@janus-idp/backstage-plugin-orchestrator-form-api": patch
---

Resolved the following issues:

1. enabled validation using customValidate, and replaced extraErrors with getExtraErrors, since extraErrors is supposed to be populated when running onSubmit, and that isn't exposed to the user. Added busy handling while calling getExtraErrors.
2. moved FormComponent to a separate component, to avoid buggy behavior and code smells with component generated in a different component.
3. update formData on each change instead of when moving to next step, to avoid data being cleared.
4. fix bug in validator - it only worked in first step, because of issue in @rjsf form
5. removed unnecessary package json-schema that was used just for lint error, and fixed the root cause of lint error when importing types from @types/json-schema
21 changes: 17 additions & 4 deletions plugins/orchestrator-form-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

This library offers the flexibility to override a selected list of [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 in a backstage plugin. This decorator enables users to:

- **Dynamic Validations:** Override the `extraErrors` property to implement dynamic validation logic.
- **Custom Validations:** Two types of custom validations can be added on top of the JSON schema validation provided by default:
- Synchronous validation through the `customValidate` property
- Asynchronous validation through the `getExtraErrors` property. Handles validations that require backend calls.
- **Custom Components:** Replace default components by overriding the `widgets` property.
- **Correlated Field Values:** Implement complex inter-field dependencies by overriding the `onChange` and the `formData` properties.

Expand All @@ -13,8 +15,17 @@ The decorator will be provided through a factory method that leverages a [Backst
### Interface Provided in this package

```typescript
export type FormDecoratorProps = Pick<
FormProps<JsonObject, JSONSchema7>,
'formData' | 'formContext' | 'widgets' | 'onChange' | 'customValidate'
> & {
getExtraErrors?: (
formData: JsonObject,
) => Promise<ErrorSchema<JsonObject>> | undefined;
};

export type FormDecorator = (
FormComponent: React.ComponentType<Partial<FormProps>>,
FormComponent: React.ComponentType<FormDecoratorProps>,
) => React.ComponentType;

export interface FormExtensionsApi {
Expand All @@ -27,7 +38,7 @@ export interface FormExtensionsApi {
```typescript
class CustomFormExtensionsApi implements FormExtensionsApi {
getFormDecorator(schema: JSONSchema7) {
return (FormComponent: React.ComponentType<Partial<FormProps>>) => {
return (FormComponent: React.ComponentType<FormDecoratorProps>>) => {
const widgets = {CountryWidget};
return () => <FormComponent widgets={widgets} />;
};
Expand Down Expand Up @@ -76,7 +87,7 @@ export const testFactoryPlugin = createPlugin({

### dynamic plugin configuration

add the following to app-config.local.yaml for integrating a dynamic plugin
add the following to app-config.local.yaml for integrating the dynamic plugin.

```yaml
dynamicPlugins:
Expand All @@ -91,3 +102,5 @@ dynamicPlugins:
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. It also organizes the form into wizard steps based on an additional hierarchical structure within the JSON schema.
Full plugin example is available [here](https://github.com/parodos-dev/extended-form-example-plugin).
3 changes: 2 additions & 1 deletion plugins/orchestrator-form-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"dependencies": {
"@backstage/core-plugin-api": "^1.10.0",
"@backstage/types": "^1.1.1",
"@rjsf/core": "^5.21.2"
"@rjsf/core": "^5.21.2",
"@rjsf/utils": "^5.21.2"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0 || ^18.0.0"
Expand Down
42 changes: 33 additions & 9 deletions plugins/orchestrator-form-api/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,46 @@ 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';
import { ErrorSchema, UiSchema } from '@rjsf/utils';
import type { JSONSchema7 } from 'json-schema';

export type FormDecoratorProps = Partial<
Pick<
FormProps<JsonObject, JSONSchema7>,
'formData' | 'formContext' | 'widgets' | 'onChange' | 'extraErrors'
>
>;
/**
* Type definition for properties passed to a form decorator component.
* This interface extends selected fields from `FormProps` provided by `react-jsonschema-form`,
* with additional custom functionality.
*
* @see {@link https://rjsf-team.github.io/react-jsonschema-form/docs/api-reference/form-props|RJSF Form Props Documentation}
*
* Core properties include:
* - formData: The form's current data
* - formContext: Contextual data shared across form components
* - widgets: Custom widget components for form fields
* - onChange: Handler for form data changes
* - customValidate: Custom validation function
*
* Additional properties:
* - getExtraErrors: Async function to fetch additional validation errors.
* This replaces the static 'extraErrors' prop from react-jsonschema-form, which can't be used as is, since onSubmit isn't exposed.
* The orchestrator form component will call getExtraErrors when running onSubmit.
*/
export type FormDecoratorProps = Pick<
FormProps<JsonObject, JSONSchema7>,
'formData' | 'formContext' | 'widgets' | 'onChange' | 'customValidate'
> & {
getExtraErrors?: (
formData: JsonObject,
) => Promise<ErrorSchema<JsonObject>> | undefined;
};

export type OrchestratorFormDecorator = (
FormComponent: React.ComponentType<FormDecoratorProps>,
) => React.ComponentType;

export interface OrchestratorFormApi {
getFormDecorator(schema: JSONSchema7): OrchestratorFormDecorator;
getFormDecorator(
schema: JSONSchema7,
uiSchema: UiSchema<JsonObject, JSONSchema7>,
): OrchestratorFormDecorator;
}

export const orchestratorFormApiRef = createApiRef<OrchestratorFormApi>({
Expand Down
1 change: 0 additions & 1 deletion plugins/orchestrator-form-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
"@rjsf/material-ui": "^5.21.2",
"@rjsf/utils": "^5.21.2",
"@rjsf/validator-ajv8": "^5.21.2",
"json-schema": "^0.4.0",
"json-schema-library": "^9.0.0",
"lodash": "^4.17.21"
},
Expand Down
2 changes: 1 addition & 1 deletion plugins/orchestrator-form-react/src/DefaultFormApi.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';

import { JSONSchema7 } from 'json-schema';
import type { JSONSchema7 } from 'json-schema';

import {
FormDecoratorProps,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, { Fragment } from 'react';
import { JsonObject } from '@backstage/types';

import { UiSchema } from '@rjsf/utils';
import { JSONSchema7 } from 'json-schema';
import type { JSONSchema7 } from 'json-schema';

import generateUiSchema from '../utils/generateUiSchema';
import { StepperContextProvider } from '../utils/StepperContext';
Expand All @@ -25,12 +25,12 @@ const getNumSteps = (schema: JSONSchema7): number | undefined => {
const SingleStepForm = ({
schema,
formData,
onSubmit,
onChange,
uiSchema,
}: {
schema: JSONSchema7;
formData: JsonObject;
onSubmit: (formData: JsonObject) => void;
onChange: (formData: JsonObject) => void;
uiSchema: UiSchema<JsonObject>;
}) => {
const steps = React.useMemo<OrchestratorFormStep[]>(() => {
Expand All @@ -42,15 +42,15 @@ const SingleStepForm = ({
<OrchestratorFormWrapper
schema={{ ...schema, title: '' }}
formData={formData}
onSubmit={onSubmit}
onChange={onChange}
uiSchema={uiSchema}
>
<OrchestratorFormToolbar />
</OrchestratorFormWrapper>
),
},
];
}, [schema, formData, onSubmit, uiSchema]);
}, [schema, formData, onChange, uiSchema]);
return <OrchestratorFormStepper steps={steps} />;
};

Expand All @@ -59,7 +59,7 @@ type OrchestratorFormProps = {
isExecuting: boolean;
handleExecute: (parameters: JsonObject) => Promise<void>;
data?: JsonObject;
isDataReadonly: boolean;
isDataReadonly?: boolean;
};

const OrchestratorForm = ({
Expand All @@ -80,7 +80,7 @@ const OrchestratorForm = ({
handleExecute(formData || {});
}, [formData, handleExecute]);

const onSubmit = React.useCallback(
const onChange = React.useCallback(
(_formData: JsonObject) => {
setFormData(_formData);
},
Expand Down Expand Up @@ -114,15 +114,15 @@ const OrchestratorForm = ({
schema={schema}
numStepsInMultiStepSchema={numStepsInMultiStepSchema}
formData={formData}
onSubmit={onSubmit}
onChange={onChange}
uiSchema={uiSchema}
>
<Fragment />
</OrchestratorFormWrapper> // it is required to pass the fragment so rjsf won't generate a Submit button
) : (
<SingleStepForm
schema={schema}
onSubmit={onSubmit}
onChange={onChange}
formData={formData}
uiSchema={uiSchema}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '@material-ui/core';

import { useStepperContext } from '../utils/StepperContext';
import SubmitButton from './SubmitButton';

const useStyles = makeStyles(theme => ({
// Hotfix: this should be fixed in the theme
Expand Down Expand Up @@ -85,16 +86,14 @@ const OrchestratorFormStepper = ({
};

export const OrchestratorFormToolbar = () => {
const { activeStep, handleBack } = useStepperContext();
const { activeStep, handleBack, isValidating } = useStepperContext();
const styles = useStyles();
return (
<div className={styles.footer}>
<Button disabled={activeStep === 0} onClick={handleBack}>
Back
</Button>
<Button variant="contained" color="primary" type="submit">
Next
</Button>
<SubmitButton submitting={isValidating}>Next</SubmitButton>
</div>
);
};
Expand Down
Loading

0 comments on commit 67f466a

Please sign in to comment.