Skip to content

Commit 79140ab

Browse files
[orchestrator] Backport orchestrator fixes and enhancements to 1.9 (#2666)
* feat(orchestrator): pre-populate Execute Workflow form from URL query params (#2570) * prepopulate workflow execution page form from URL query params Signed-off-by: Karthik <karthik.jk11@gmail.com> * support enum coercison for case-insenstive match and skip invalid values * add support for fields that are defined via '$ref' * add full support for json schema fields --------- Signed-off-by: Karthik <karthik.jk11@gmail.com> * fix(orchestrator-form-react): scope async validation to active step (#2602) * fix(orchestrator-form-react): scope async validation to active step Limit validate:url requests to the active step during multi-step navigation. Made-with: Cursor * fix(orchestrator-form-react): keep full formData for async validation Pass full formData to template evaluation while scoping uiSchema traversal. Made-with: Cursor * fix(orchestrator-form-react): preserve ui:hidden on objects with properties (#2653) * fix(orchestrator): honor json schema defaults in initial formData (#2654) * fix(orchestrator): honor json schema defaults Ensure extractStaticDefaults falls back to JSON Schema defaults when ui:props fetch:response:default is absent so initial formData includes schema defaults. Made-with: Cursor * chore(changeset): document schema default fix Add changeset for orchestrator form defaults update. Made-with: Cursor * fix(orchestrator): handle root defaults Avoid setting an empty key for root schema defaults and add tests to cover root default handling. Made-with: Cursor * fix(orchestrator): document and test defaults Clarify extractStaticDefaults precedence in docs and add test coverage for default handling. Made-with: Cursor * chore(orchestrator): update yarn.lock after backport Made-with: Cursor --------- Signed-off-by: Karthik <karthik.jk11@gmail.com> Co-authored-by: Karthik Jeeyar <karthik@redhat.com>
1 parent a78abf7 commit 79140ab

15 files changed

Lines changed: 1212 additions & 9 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-orchestrator-form-react': patch
3+
---
4+
5+
Include JSON Schema `default` values when computing initial form data so
6+
template expressions can resolve defaults before widgets render.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-orchestrator-form-react': patch
3+
---
4+
5+
Scope async validate:url calls to the active step in multi-step forms.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-orchestrator': patch
3+
---
4+
5+
prepopulate Execute Workflow form from URL query params
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-orchestrator-form-react': patch
3+
---
4+
5+
Fix `extractUiSchema` so `ui:*` directives on object schemas that define `properties` (for example `ui:hidden` on a `workflowParams` object) are copied into the generated UI schema.

workspaces/orchestrator/docs/user-interface.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,52 @@ Workflows can also be invoked from Backstage software templates using the `orche
2121
- Monitor workflow execution status
2222
- View workflow results and outputs
2323

24+
### Execute Workflow Form Prepopulation
25+
26+
The Execute Workflow page supports prepopulating form fields from URL query parameters. When the workflow schema defines input fields, any query parameter whose name matches a schema property path will be used to prepopulate the corresponding form field.
27+
28+
**Path format**
29+
30+
- For flat schemas, use the property name directly: `?language=English&name=John`
31+
- For nested (multi-step) schemas, use dot notation: `?firstStep.fooTheFirst=test` or `?provideInputs.language=English`
32+
- For fields inside `oneOf` or `anyOf` branches, use the same dot notation: `?mode.alphaValue=test`
33+
34+
**Schema support**
35+
36+
The prepopulation logic supports the full JSON Schema draft-07 spec, including:
37+
38+
- Fields defined via `$ref` in `$defs` or `definitions`
39+
- `oneOf` and `anyOf` — the correct branch is resolved from the provided data
40+
- Array fields — use comma-separated values: `?tags=foo,bar,baz`
41+
- Type coercion for numbers, integers, and booleans
42+
43+
**Schema constraints**
44+
45+
For fields with `enum` constraints in the schema, the query param value must match one of the allowed values. Case-insensitive matching is supported (e.g. `?language=english` maps to `English` when the enum is `['English', 'Spanish']`). Values that do not match any enum option are ignored and will not prepopulate the field.
46+
47+
Query parameters that do not match any schema property path are ignored and will not be merged into the form.
48+
49+
**Reserved parameters**
50+
51+
The following query parameters are reserved for navigation and are not used for form prepopulation:
52+
53+
- `targetEntity` — Used to associate the workflow run with a catalog entity
54+
- `instanceId` — Used when re-running or viewing a specific workflow instance
55+
56+
**Examples**
57+
58+
```
59+
/orchestrator/workflows/yamlgreet/execute?targetEntity=default:component:my-app&language=English&name=alice
60+
```
61+
62+
In this example, `targetEntity` is excluded (reserved), while `language` and `name` prepopulate the form when those fields exist in the workflow schema.
63+
64+
```
65+
/orchestrator/workflows/my-workflow/execute?language=English&mode.alphaValue=prefilled&tags=a,b,c
66+
```
67+
68+
This example prepopulates a flat field (`language`), a nested field inside an `oneOf` branch (`mode.alphaValue`), and an array field (`tags`).
69+
2470
### Entity Integration
2571

2672
- Workflow tabs on entity pages

workspaces/orchestrator/plugins/orchestrator-form-react/src/components/OrchestratorFormWrapper.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,22 @@ const FormComponent = (decoratorProps: FormDecoratorProps) => {
9191
let _extraErrors: ErrorSchema<JsonObject> | undefined = undefined;
9292
let _validationError: Error | undefined = undefined;
9393
const activeKey = getActiveKey();
94+
const shouldScopeExtraErrors =
95+
Boolean(activeKey) && Boolean(uiSchema?.[activeKey as string]);
96+
const extraErrorsFormData = (_formData ?? formData) as JsonObject;
97+
const extraErrorsUiSchema = shouldScopeExtraErrors
98+
? ({
99+
[activeKey as string]: uiSchema?.[activeKey as string],
100+
} as OrchestratorFormContextProps['uiSchema'])
101+
: uiSchema;
94102

95103
if (decoratorProps.getExtraErrors) {
96104
try {
97105
handleValidateStarted();
98-
_extraErrors = await decoratorProps.getExtraErrors(formData, uiSchema);
106+
_extraErrors = await decoratorProps.getExtraErrors(
107+
extraErrorsFormData,
108+
extraErrorsUiSchema,
109+
);
99110

100111
if (activeKey) {
101112
setExtraErrors(
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import type { JSONSchema7 } from 'json-schema';
18+
19+
import extractStaticDefaults from './extractStaticDefaults';
20+
21+
describe('extractStaticDefaults', () => {
22+
it('applies schema defaults when no fetch default', () => {
23+
const schema: JSONSchema7 = {
24+
type: 'object',
25+
properties: {
26+
name: { type: 'string', default: 'app' },
27+
},
28+
};
29+
30+
expect(extractStaticDefaults(schema)).toEqual({ name: 'app' });
31+
});
32+
33+
it('prefers fetch:response:default over schema default', () => {
34+
const schema: JSONSchema7 = {
35+
type: 'object',
36+
properties: {
37+
name: {
38+
type: 'string',
39+
default: 'schema',
40+
'ui:props': { 'fetch:response:default': 'fetch' },
41+
} as JSONSchema7 & Record<string, unknown>,
42+
},
43+
};
44+
45+
expect(extractStaticDefaults(schema)).toEqual({ name: 'fetch' });
46+
});
47+
48+
it('does not overwrite existing form data values', () => {
49+
const schema: JSONSchema7 = {
50+
type: 'object',
51+
properties: {
52+
name: { type: 'string', default: 'schema' },
53+
},
54+
};
55+
56+
expect(extractStaticDefaults(schema, { name: 'existing' })).toEqual({
57+
name: 'existing',
58+
});
59+
});
60+
61+
it('preserves falsy defaults', () => {
62+
const schema: JSONSchema7 = {
63+
type: 'object',
64+
properties: {
65+
enabled: { type: 'boolean', default: false },
66+
retries: { type: 'number', default: 0 },
67+
note: { type: 'string', default: '' },
68+
},
69+
};
70+
71+
expect(extractStaticDefaults(schema)).toEqual({
72+
enabled: false,
73+
retries: 0,
74+
note: '',
75+
});
76+
});
77+
78+
it('applies defaults from composed schemas', () => {
79+
const schema: JSONSchema7 = {
80+
type: 'object',
81+
allOf: [
82+
{
83+
type: 'object',
84+
properties: {
85+
name: { type: 'string', default: 'composed' },
86+
},
87+
},
88+
],
89+
};
90+
91+
expect(extractStaticDefaults(schema)).toEqual({ name: 'composed' });
92+
});
93+
94+
it('applies root default objects without creating empty key', () => {
95+
const schema: JSONSchema7 = {
96+
type: 'object',
97+
default: { foo: 'bar' },
98+
properties: {
99+
foo: { type: 'string' },
100+
},
101+
};
102+
103+
expect(extractStaticDefaults(schema)).toEqual({ foo: 'bar' });
104+
});
105+
106+
it('does not override existing data with root defaults', () => {
107+
const schema: JSONSchema7 = {
108+
type: 'object',
109+
default: { foo: 'bar' },
110+
properties: {
111+
foo: { type: 'string' },
112+
},
113+
};
114+
115+
expect(extractStaticDefaults(schema, { foo: 'existing' })).toEqual({
116+
foo: 'existing',
117+
});
118+
});
119+
});

workspaces/orchestrator/plugins/orchestrator-form-react/src/utils/extractStaticDefaults.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,19 @@ import type { JSONSchema7, JSONSchema7Definition } from 'json-schema';
2020
import get from 'lodash/get';
2121
import set from 'lodash/set';
2222

23+
const isPlainObject = (value: unknown): value is JsonObject => {
24+
return value !== null && typeof value === 'object' && !Array.isArray(value);
25+
};
26+
2327
/**
24-
* Extracts static default values from fetch:response:default properties in the schema.
28+
* Extracts static default values from the schema in priority order:
29+
* 1) ui:props.fetch:response:default
30+
* 2) JSON Schema default
31+
*
2532
* These values are applied to formData before widgets render, ensuring defaults
2633
* are available immediately without waiting for fetch operations.
2734
*
28-
* @param schema - The JSON Schema containing ui:props with fetch:response:default
35+
* @param schema - The JSON Schema containing ui:props and/or default values
2936
* @param existingFormData - Existing form data to preserve (won't be overwritten)
3037
* @returns An object containing the extracted default values merged with existing data
3138
*/
@@ -60,14 +67,26 @@ export function extractStaticDefaults(
6067

6168
// Extract fetch:response:default from ui:props
6269
const uiProps = (curSchema as Record<string, unknown>)['ui:props'];
70+
let staticDefault: unknown;
6371
if (uiProps && typeof uiProps === 'object') {
64-
const staticDefault = (uiProps as Record<string, unknown>)[
72+
staticDefault = (uiProps as Record<string, unknown>)[
6573
'fetch:response:default'
6674
];
67-
if (staticDefault !== undefined) {
68-
// Only set if not already in existing form data
69-
const existingValue = get(existingFormData, path);
70-
if (existingValue === undefined || existingValue === null) {
75+
}
76+
77+
if (staticDefault === undefined && 'default' in curSchema) {
78+
staticDefault = (curSchema as JSONSchema7).default;
79+
}
80+
81+
if (staticDefault !== undefined) {
82+
// Only set if not already in existing form data
83+
const existingValue = get(existingFormData, path);
84+
if (existingValue === undefined || existingValue === null) {
85+
if (path === '') {
86+
if (isPlainObject(staticDefault)) {
87+
Object.assign(defaults, staticDefault);
88+
}
89+
} else {
7190
set(defaults, path, staticDefault);
7291
}
7392
}

workspaces/orchestrator/plugins/orchestrator-form-react/src/utils/generateUiSchema.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,29 @@ describe('extract ui schema', () => {
6969
expect(uiSchema).toEqual({ name: { 'ui:autofocus': true } });
7070
});
7171

72+
it('extracts ui:hidden and other ui:* on object nodes that also have properties', () => {
73+
const mixedSchema: JSONSchema7 = {
74+
type: 'object',
75+
properties: {
76+
visibleField: {
77+
type: 'string',
78+
title: 'Visible',
79+
},
80+
workflowParams: {
81+
type: 'object',
82+
'ui:hidden': true,
83+
properties: {
84+
solutionName: { type: 'string', default: 'a' },
85+
solutionVersion: { type: 'string', default: '1.0' },
86+
},
87+
} as JSONSchema7,
88+
},
89+
};
90+
const uiSchema = generateUiSchema(mixedSchema, false);
91+
expect(uiSchema.workflowParams).toEqual({ 'ui:hidden': true });
92+
expect(uiSchema.visibleField).toMatchObject({ 'ui:autofocus': true });
93+
});
94+
7295
it('should extract from array', () => {
7396
const mixedSchema = {
7497
title: 'A list of tasks',

workspaces/orchestrator/plugins/orchestrator-form-react/src/utils/generateUiSchema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ function extractUiSchema(mixedSchema: JSONSchema7): UiSchema<JsonObject> {
106106
processObject(getSchemaDefinition(curSchema.$ref, rootSchema), path);
107107
} else if (curSchema.properties) {
108108
processObjectProperties(curSchema.properties, path);
109+
processLeafSchema(curSchema, path);
109110
} else if (curSchema.items) {
110111
processArraySchema(curSchema, path);
111112
} else {

0 commit comments

Comments
 (0)