Skip to content

Commit f5e85c5

Browse files
authored
feat(orchestrator): add support for selectors in SchemaUpdater (#1006)
Signed-off-by: Marek Libra <marek.libra@gmail.com>
1 parent 112d44f commit f5e85c5

4 files changed

Lines changed: 129 additions & 25 deletions

File tree

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-widgets': patch
3+
---
4+
5+
Add support for selectors in SchemaUpdater. A complex response can be narrowed by the selector to produce the object structure as desired by the SchemaUpdater.

workspaces/orchestrator/docs/orchestratorFormWidgets.md

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ A headless widget used for fetching snippets of JSON schema and dynamically upda
3939

4040
Thanks to this component, complex subparts of the form can be changed based on data entered in other fields by the user.
4141

42-
Example of use in workflow's input data schema:
42+
### Example of the SchemaUpdater use in workflow's input data schema
4343

4444
```json
4545
{
@@ -67,6 +67,8 @@ Example of use in workflow's input data schema:
6767
}
6868
```
6969

70+
### Expected response for the SchemaUpdater
71+
7072
The response of `fetch:url` endpoint is expected to be a JSON document conforming structure defined by the `SchemaChunksResponse` type.
7173

7274
Considering the data-input schema structure above, the response can look like:
@@ -86,10 +88,15 @@ Considering the data-input schema structure above, the response can look like:
8688
}
8789
```
8890

89-
Please note: The response must be a single JSON object whose property names correspond to the identifiers defined in the data-input JSON Schema.
91+
Please note: The response must be
92+
93+
- a single JSON object
94+
- whose property names correspond to the identifiers defined in the data-input JSON Schema
95+
- and values are valid replacements for the UI schema.
9096

91-
A provided snipped can be of `"type": "object"` and so inject/replace fields for a complex data structure.
92-
Additional `SchemaUpdater` widgets can be instantiated this way as well.
97+
A provided snipped can be of `"type": "object"` and so inject/replace fields for a complex data structure, so the use is not limited to just a single string or numeric properties.
98+
99+
**Additional `SchemaUpdater` widgets can be instantiated this way as well.**
93100

94101
The `SchemaUpdater` widget scans for the identifiers, the top-level property names in the response, and replaces any matching ones with the corresponding values from the response.
95102
Identifiers that do not exist in the current schema are ignored.
@@ -100,14 +107,71 @@ You can instantiate multiple `SchemaUpdater` widgets simultaneously. It is up to
100107

101108
It is highly recommended that endpoints are implemented as stateless and free from side effects, consistently returning the same response for identical input sets.
102109

103-
### SchmeaUpdater widget ui:props
110+
### Using selector to narrow complex response in SchemaUpdater
111+
112+
As stated above, the `SchemaUpdater` expects a single object of the desired structure as its input.
113+
114+
If the response does not meet that condition, meaning it contains additional data or the structure is malformed, the `fetch:response:value` selector can be used to pick-up a single object in the desired format.
115+
116+
Example complex HTTP response:
117+
118+
```json
119+
{
120+
"foo": "bar",
121+
"prop1": {
122+
"subprop": "a lot of complex but useless stuff"
123+
},
124+
"mydataroot": {
125+
"mydata": {
126+
"sendCertificatesAs": {
127+
"type": "string",
128+
"title": "Send certificates via",
129+
"ui:widget": "ActiveText",
130+
"ui:props": {
131+
"ui:variant": "caption",
132+
"ui:text": "This course does not provide certificate"
133+
}
134+
}
135+
}
136+
}
137+
}
138+
```
139+
140+
For the schema:
141+
142+
```json
143+
{
144+
"properties": {
145+
...
146+
"sendCertificatesAs": {
147+
"type": "object",
148+
"title": "This title will never be displayed. Will be managed by the 'mySchemaUpdaterForCertificates'.",
149+
"ui:widget": "hidden"
150+
},
151+
"mySchemaUpdaterForCertificates": {
152+
"type": "string",
153+
"title": "This title will never be displayed.",
154+
"ui:widget": "SchemaUpdater",
155+
"ui:props": {
156+
"fetch:url": "$${{backend.baseUrl}}/api/proxy/mytesthttpserver/certificatesschema",
157+
"fetch:response:value": "mydataroot.mydata",
158+
...
159+
}
160+
},
161+
...
162+
}
163+
}
164+
```
165+
166+
### SchemaUpdater widget ui:props
104167

105168
The widget supports following `ui:props`:
106169

107170
- fetch:url
108171
- fetch:headers
109172
- fetch:method
110173
- fetch:body
174+
- fetch:response:value
111175
- fetch:retrigger
112176

113177
[Check mode details](#content-of-uiprops)

workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@
1414
* limitations under the License.
1515
*/
1616
import jsonata from 'jsonata';
17-
import { JsonObject } from '@backstage/types';
17+
import { JsonObject, JsonValue } from '@backstage/types';
18+
19+
export function isJsonObject(value?: JsonValue): value is JsonObject {
20+
return typeof value === 'object' && value !== null && !Array.isArray(value);
21+
}
1822

1923
export const applySelectorArray = async (
2024
data: JsonObject,
@@ -28,7 +32,7 @@ export const applySelectorArray = async (
2832
}
2933

3034
throw new Error(
31-
`Unexpected result of "${selector}" selector, expected string[] type. Value "${value}"`,
35+
`Unexpected result of "${selector}" selector, expected string[] type. Value "${JSON.stringify(value)}"`,
3236
);
3337
};
3438

@@ -44,6 +48,22 @@ export const applySelectorString = async (
4448
}
4549

4650
throw new Error(
47-
`Unexpected result of "${selector}" selector, expected string type. Value "${value}"`,
51+
`Unexpected result of "${selector}" selector, expected string type. Value "${JSON.stringify(value)}"`,
52+
);
53+
};
54+
55+
export const applySelectorObject = async (
56+
data: JsonObject,
57+
selector: string,
58+
): Promise<JsonObject> => {
59+
const expression = jsonata(selector);
60+
const value = await expression.evaluate(data);
61+
62+
if (isJsonObject(value)) {
63+
return value;
64+
}
65+
66+
throw new Error(
67+
`Unexpected result of "${selector}" selector, expected object type. Value "${JSON.stringify(value)}"`,
4868
);
4969
};

workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/SchemaUpdater.tsx

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
useRetriggerEvaluate,
2828
useTemplateUnitEvaluator,
2929
useFetch,
30+
applySelectorObject,
3031
} from '../utils';
3132
import { ErrorText } from './ErrorText';
3233
import { UiProps } from '../uiPropTypes';
@@ -47,6 +48,8 @@ export const SchemaUpdater: Widget<
4748
() => (props.options?.props ?? {}) as UiProps,
4849
[props.options?.props],
4950
);
51+
const valueSelector = uiProps['fetch:response:value']?.toString();
52+
5053
const [localError, setLocalError] = useState<string>();
5154

5255
const retrigger = useRetriggerEvaluate(
@@ -68,27 +71,39 @@ export const SchemaUpdater: Widget<
6871
return;
6972
}
7073

71-
const typedData = data as unknown as SchemaChunksResponse;
74+
const doItAsync = async () => {
75+
let typedData: SchemaChunksResponse =
76+
data as unknown as SchemaChunksResponse;
77+
if (valueSelector) {
78+
typedData = (await applySelectorObject(
79+
data,
80+
valueSelector,
81+
)) as unknown as SchemaChunksResponse;
82+
}
83+
84+
// validate received response before updating
85+
Object.keys(typedData).forEach(key => {
86+
if (!typedData[key]?.type) {
87+
// eslint-disable-next-line no-console
88+
console.error('JSON response malformed: ', typedData);
89+
setLocalError(
90+
`JSON response malformed for SchemaUpdater, missing "type" field for "${key}" key.`,
91+
);
92+
}
93+
});
7294

73-
// validate received response before updating
74-
Object.keys(typedData).forEach(key => {
75-
if (!typedData[key]?.type) {
95+
try {
96+
updateSchema(typedData);
97+
} catch (err) {
98+
// eslint-disable-next-line no-console
99+
console.error('Error when updating schema', props.id, err);
76100
setLocalError(
77-
`JSON response malformed for SchemaUpdater, missing "type" field for "${key}" key.`,
101+
`Failed to update schema update by the ${props.id} SchemaUpdater`,
78102
);
79103
}
80-
});
81-
82-
try {
83-
updateSchema(typedData);
84-
} catch (err) {
85-
// eslint-disable-next-line no-console
86-
console.error('Error when updating schema', props.id, err);
87-
setLocalError(
88-
`Failed to update schema update by the ${props.id} SchemaUpdater`,
89-
);
90-
}
91-
}, [data, props.id, updateSchema]);
104+
};
105+
doItAsync();
106+
}, [data, props.id, updateSchema, valueSelector]);
92107

93108
if (localError ?? error) {
94109
return <ErrorText text={localError ?? error ?? ''} id={id} />;

0 commit comments

Comments
 (0)