Skip to content

Commit 19c04f7

Browse files
authored
feat(orchestrator): added StaticText widget to form widgets library (#786)
* feat(orchestrator): added StaticText widget to form widgets library * fix test failure * code review fixes
1 parent 4413eef commit 19c04f7

19 files changed

Lines changed: 459 additions & 52 deletions

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"devDependencies": {
1818
"@backstage/cli": "^0.27.0",
1919
"@backstage/cli-node": "^0.2.2",
20+
"@backstage/types": "^1.2.1",
2021
"@changesets/parse": "^0.4.0",
2122
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
2223
"@manypkg/get-packages": "^2.2.2",
@@ -32,6 +33,7 @@
3233
"fs-extra": "11.3.0",
3334
"husky": "^9.0.11",
3435
"lint-staged": "^15.2.2",
36+
"lodash": "^4.17.21",
3537
"lodash.escaperegexp": "^4.1.2",
3638
"node-fetch": "^2.6.7",
3739
"prettier": "^3.4.2"

workspaces/orchestrator/plugins/orchestrator-form-widgets/http-workflow-dev-server/exampleWorkflows/schemas/dynamic-course-select__main-schema.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,28 @@
3131
"title": "This title will never be displayed. This 'courseDetails' property is just a placeholder to be replaced by the 'mySchemaUpdater' based on the fetched response. Will contain complex data later.",
3232
"ui:widget": "hidden"
3333
},
34+
"suggestedCourses": {
35+
"type": "string",
36+
"title": "Suggested Additional Courses",
37+
"ui:widget": "ActiveText",
38+
"ui:props": {
39+
"ui:variant": "h4",
40+
"fetch:url": "http://localhost:7007/api/proxy/mytesthttpserver/suggested-courses?coursename=$${{current.courseName}}",
41+
"fetch:method": "GET",
42+
"fetch:response:suggestions": "suggestions",
43+
"fetch:retrigger": ["current.studentName", "current.courseName"],
44+
"ui:text": "You might also be interested in: $${{fetch:response:suggestions}}"
45+
}
46+
},
47+
"staticMessage": {
48+
"type": "string",
49+
"title": "Suggested Additional Courses static",
50+
"ui:widget": "ActiveText",
51+
"ui:props": {
52+
"ui:variant": "subtitle1",
53+
"ui:text": "You might also be interested in other stuff"
54+
}
55+
},
3456
"mySchemaUpdater": {
3557
"type": "string",
3658
"title": "This title will never be displayed. The 'type' is irrelevant. There can be multiple SchemaUpdater instances, if you like. They can even be dynamically supplied by one of them.",

workspaces/orchestrator/plugins/orchestrator-form-widgets/http-workflow-dev-server/exampleWorkflows/schemas/dynamic_schema__main-schema.json

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,6 @@
1212
"type": "string",
1313
"title": "Standard input field. Write something to see it in the autocomplete field."
1414
},
15-
"myActiveText": {
16-
"type": "string",
17-
"title": "Example of ActiveTextInput widget with autocomplete",
18-
"ui:widget": "ActiveTextInput",
19-
"ui:props": {
20-
"fetch:url": "http://localhost:7007/api/proxy/mytesthttpserver/activeTextWhisperer?mydata=$${{current.firstStep.fooTheFirst}}",
21-
"fetch:response:value": "myresult.foo.default",
22-
"fetch:response:autocomplete": "bar.something.myautocompleteoptions",
23-
"fetch:retrigger": ["current.firstStep.fooTheFirst"]
24-
}
25-
},
2615
"mySimpleActiveText": {
2716
"type": "string",
2817
"title": "Example of simple ActiveTextInput with validation",
@@ -49,6 +38,18 @@
4938
}
5039
}
5140
},
41+
"myActiveText": {
42+
"type": "string",
43+
"title": "Example of ActiveText with fetch",
44+
"ui:widget": "ActiveText",
45+
"ui:props": {
46+
"ui:text": "Example demonstration text. fooTheFirst: $${{current.firstStep.fooTheFirst}}. response:example: $${{fetch:response:example}}",
47+
"ui:variant": "p1",
48+
"fetch:url": "http://localhost:7007/api/proxy/mytesthttpserver/activeTexts",
49+
"fetch:response:example": "example.text0",
50+
"fetch:retrigger": ["current.firstStep.fooTheFirst"]
51+
}
52+
},
5253
"placeholderOne": {
5354
"type": "string",
5455
"title": "This field and its title are visible until replaced by any SchemaUpdater."

workspaces/orchestrator/plugins/orchestrator-form-widgets/http-workflow-dev-server/httpServer.dynamic.course.select.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* you may not use this file except in compliance with the License.
66
* You may obtain a copy of the License at
77
*
8-
* http://www.apache.org/licenses/LICENSE-2.0
8+
* http://www.apache.org/licenses/LICENSE-2.0
99
*
1010
* Unless required by applicable law or agreed to in writing, software
1111
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -184,6 +184,30 @@ app.get('/coursedetailsschema', (req, res) => {
184184
res.send(JSON.stringify(response));
185185
});
186186

187+
app.get('/suggested-courses', (req, res) => {
188+
logRequest(req);
189+
190+
const courseName = req.query?.coursename;
191+
192+
let suggestions = [];
193+
if (courseName === 'one course') {
194+
suggestions = ['Related Course A', 'Another Related Course'];
195+
} else if (courseName === 'another course') {
196+
suggestions = ['One More Course', 'And Another One'];
197+
} else if (courseName === 'complexCourse') {
198+
suggestions = ['Advanced Topics', 'Master Class'];
199+
} else {
200+
suggestions = ['Consider these too!', 'Explore other options'];
201+
}
202+
203+
const response = {
204+
suggestions: suggestions.join(', '),
205+
};
206+
207+
// HTTP 200
208+
res.send(JSON.stringify(response));
209+
});
210+
187211
app.listen(port, () => {
188212
// eslint-disable-next-line no-console
189213
console.info(

workspaces/orchestrator/plugins/orchestrator-form-widgets/http-workflow-dev-server/index.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,18 @@ app.get('/activeTextWhisperer', (req, res) => {
135135
res.send(JSON.stringify(result));
136136
});
137137

138+
app.get('/activeTexts', (req, res) => {
139+
logRequest(req);
140+
res.send(
141+
JSON.stringify({
142+
example: {
143+
text0: 'Text 0',
144+
text1: 'Text 1',
145+
},
146+
}),
147+
);
148+
});
149+
138150
app.listen(port, () => {
139151
// eslint-disable-next-line no-console
140152
console.info(

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
import { FormValidation } from '@rjsf/utils';
2424
import { JsonObject } from '@backstage/types';
2525

26-
import { SchemaUpdater, ActiveTextInput } from './widgets';
26+
import { SchemaUpdater, ActiveTextInput, ActiveText } from './widgets';
2727
import { useGetExtraErrors } from './utils';
2828

2929
const customValidate = (
@@ -38,6 +38,7 @@ const customValidate = (
3838
const widgets = {
3939
SchemaUpdater,
4040
ActiveTextInput,
41+
ActiveText,
4142
};
4243

4344
export class FormWidgetsApi implements OrchestratorFormApi {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
import { JsonValue } from '@backstage/types/index';
17+
import { Variant } from '@material-ui/core/styles/createTypography';
18+
19+
export type UiProps = {
20+
'ui:variant'?: Variant;
21+
'ui:text'?: string;
22+
'fetch:url'?: string;
23+
'fetch:method'?: 'GET' | 'POST';
24+
'fetch:headers'?: Record<string, string>;
25+
'fetch:body'?: Record<string, JsonValue>;
26+
'fetch:retrigger'?: string[];
27+
[key: `fetch:response:${string}`]: string;
28+
};
29+
30+
export const isFetchResponseKey = (
31+
key: string,
32+
): key is `fetch:response:${string}` => {
33+
return key.startsWith('fetch:response:');
34+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export const getErrorMessage = (prefix: string, error: unknown): string => {
2+
if (!error) {
3+
return prefix;
4+
}
5+
if (error instanceof Error && error.message) {
6+
return `${prefix}: ${error.message}`;
7+
}
8+
const errorString = String(error);
9+
return errorString ? `${prefix}: ${errorString}` : prefix;
10+
};

workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/evaluateTemplate.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ describe('evaluate template', () => {
3333

3434
it('fails on incorrect input', async () => {
3535
const cases = [
36-
{ input: '', throws: 'Template can be a string only, key: myKey' },
3736
{ input: undefined, throws: 'Template can be a string only, key: myKey' },
3837
{ input: '$${{}}', throws: 'Template unit can not be empty' },
3938
{ input: '$${{foo', throws: 'Template unit is not closed by }}' },

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

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,29 @@
1717
import { useEffect, useState } from 'react';
1818
import { JsonObject, JsonValue } from '@backstage/types';
1919
import { useTemplateUnitEvaluator } from './useTemplateUnitEvaluator';
20+
import { UiProps } from '../uiPropTypes';
2021

2122
export type evaluateTemplateProps = {
2223
template?: JsonValue;
2324
key: string;
2425
unitEvaluator: (
2526
unit: string,
2627
formData: JsonObject,
28+
responseData?: JsonObject,
29+
uiProps?: UiProps,
2730
) => Promise<JsonValue | undefined>;
2831
formData: JsonObject;
32+
responseData?: JsonObject;
33+
uiProps?: UiProps;
2934
};
3035

3136
export const evaluateTemplate = async (
3237
props: evaluateTemplateProps,
3338
): Promise<string> => {
34-
const { template, key, unitEvaluator, formData } = props;
39+
const { template, key, unitEvaluator, formData, responseData, uiProps } =
40+
props;
3541

36-
if (!template || typeof template !== 'string') {
42+
if (template === undefined || typeof template !== 'string') {
3743
throw new Error(`Template can be a string only, key: ${key}`);
3844
}
3945

@@ -51,6 +57,8 @@ export const evaluateTemplate = async (
5157
let evaluatedUnit = await unitEvaluator(
5258
template.substring(startIndex + 4, stopIndex),
5359
formData,
60+
responseData,
61+
uiProps,
5462
);
5563
if (evaluatedUnit === undefined) {
5664
evaluatedUnit = '___undefined___';
@@ -84,12 +92,7 @@ export const useEvaluateTemplate = ({
8492
const [evaluated, setEvaluated] = useState<string>();
8593

8694
useEffect(() => {
87-
evaluateTemplate({
88-
template,
89-
key,
90-
unitEvaluator,
91-
formData,
92-
})
95+
evaluateTemplate({ template, key, unitEvaluator, formData })
9396
.then(setEvaluated)
9497
.catch(reason => setError(reason.toString()));
9598
}, [template, unitEvaluator, formData, key, setError]);

0 commit comments

Comments
 (0)