Skip to content

Commit 3c71547

Browse files
authored
chore(orchestrator): add 'Dynamic course select' example workflow (#774)
Signed-off-by: Marek Libra <marek.libra@gmail.com>
1 parent a6a0262 commit 3c71547

4 files changed

Lines changed: 282 additions & 0 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
id: dynamic-course-select
2+
version: '1.0'
3+
specVersion: '0.8'
4+
name: Dynamic Course Select
5+
description: Simple workflow demonstrating basics of using the orchestrator-form-widgets library. Requires corresponding HTTP endpoints to be up and proxy to be configured.
6+
start: Start
7+
dataInputSchema: schemas/dynamic-course-select__main-schema.json
8+
functions:
9+
- name: PrintSuccessData
10+
type: expression
11+
operation: '{
12+
"result": {
13+
"message": "The workflow finished successfully.",
14+
"outputs": [
15+
{
16+
"key": "resourceOne",
17+
"value": $WORKFLOW.instanceId
18+
},
19+
{
20+
"key": "studentName",
21+
"value": .studentName
22+
},
23+
{
24+
"key": "courseName",
25+
"value": .courseName
26+
},
27+
{
28+
"key": "room",
29+
"value": .courseDetails.room
30+
},
31+
{
32+
"key": "requestCertificate",
33+
"value": .courseDetails.requestCertificate
34+
}
35+
]
36+
}
37+
}'
38+
states:
39+
- name: Start
40+
type: operation
41+
actions:
42+
- name: Fill progress data
43+
functionRef: PrintSuccessData
44+
end: true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"$id": "classpath:/schemas/dynamic-course-select__main-schema.json",
3+
"title": "Data Input Schema",
4+
"$schema": "http://json-schema.org/draft-07/schema#",
5+
"type": "object",
6+
"properties": {
7+
"studentName": {
8+
"type": "string",
9+
"title": "Student name (standard HTML component)"
10+
},
11+
"courseName": {
12+
"type": "string",
13+
"title": "Course name (ActiveTextInput component with autocomplete, fetches default data, retriggered by studentName, a change triggers a SchemaUpdater), try to enter 'complexCourse'",
14+
"ui:widget": "ActiveTextInput",
15+
"ui:props": {
16+
"fetch:url": "http://localhost:7007/api/proxy/mytesthttpserver/courses?studentname=$${{current.studentName}}",
17+
"fetch:response:value": "mycourses.mydefault",
18+
"fetch:response:autocomplete": "listofcourses.all",
19+
"fetch:retrigger": ["current.studentName"],
20+
"fetch:method": "POST",
21+
"fetch:body": {
22+
"requesterName": "Mr./Mrs. $${{identityApi.displayName}}"
23+
},
24+
"fetch:headers": {
25+
"githubNameNotUsedJustShowing": "${{githubAuthApi.profileName}}"
26+
}
27+
}
28+
},
29+
"courseDetails": {
30+
"type": "object",
31+
"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.",
32+
"ui:widget": "hidden"
33+
},
34+
"mySchemaUpdater": {
35+
"type": "string",
36+
"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.",
37+
"ui:widget": "SchemaUpdater",
38+
"ui:props": {
39+
"fetch:url": "http://localhost:7007/api/proxy/mytesthttpserver/coursedetailsschema?coursename=$${{current.courseName}}",
40+
"fetch:retrigger": ["current.courseName"]
41+
}
42+
}
43+
},
44+
"required": ["studentName", "courseName"]
45+
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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+
const express = require('express');
18+
19+
const app = express();
20+
app.disable('x-powered-by');
21+
22+
const port = 12345;
23+
app.use(express.json());
24+
25+
const logRequest = req => {
26+
// eslint-disable-next-line no-console
27+
console.log('request: ', {
28+
originalUrl: req.originalUrl,
29+
method: req.method,
30+
query: req.query,
31+
headers: req.headers,
32+
body: req.body,
33+
});
34+
};
35+
36+
app.get('/', (_, res) => {
37+
res.send(
38+
'Hello World from HTTP test server providing endpoints for the "Dynamic course select" workflow',
39+
);
40+
});
41+
42+
app.post('/courses', (req, res) => {
43+
logRequest(req);
44+
45+
const requesterName = req.body?.requesterName;
46+
const studentName = req.query?.studentname;
47+
48+
const optionsForAutocomplete = [
49+
'one course',
50+
'another course',
51+
'complexCourse',
52+
`a course just for ${requesterName}`,
53+
];
54+
if (studentName && studentName !== '___undefined___') {
55+
optionsForAutocomplete.push(`I want to be a course for ${studentName}`);
56+
}
57+
58+
const result = {
59+
// Use whatever structure here, just make sure the selectors in the data input schema picks correct values and types
60+
mycourses: { mydefault: optionsForAutocomplete[0] },
61+
listofcourses: { all: optionsForAutocomplete },
62+
};
63+
64+
// HTTP 200
65+
res.send(JSON.stringify(result));
66+
});
67+
68+
app.get('/coursedetailsschema', (req, res) => {
69+
logRequest(req);
70+
71+
const courseName = req.query?.coursename;
72+
if (!courseName || courseName === '___undefined___') {
73+
// Not enough data yet
74+
res.send(
75+
JSON.stringify({
76+
/* replace nothing */
77+
}),
78+
);
79+
}
80+
81+
const fields = {
82+
room: {
83+
type: 'string',
84+
title:
85+
'Room (backed by ActiveTextInput, fetches default value, validates externally the data but does not have autocomplete)',
86+
'ui:widget': 'ActiveTextInput',
87+
'ui:props': {
88+
'fetch:url':
89+
'http://localhost:7007/api/proxy/mytesthttpserver/rooms?coursename=$${{current.courseName}}',
90+
'fetch:response:value': 'room.mydefault',
91+
'fetch:retrigger': ['current.courseName'],
92+
'fetch:method': 'GET',
93+
'validate:url':
94+
'http://localhost:7007/api/proxy/mytesthttpserver/validateroom',
95+
'validate:method': 'POST',
96+
'validate:body': {
97+
field: 'courseDetails.room',
98+
value: '$${{current.courseDetails.room}}',
99+
courseName: '$${{current.courseName}}',
100+
},
101+
},
102+
},
103+
};
104+
105+
if (courseName === 'complexCourse') {
106+
fields.requestCertificate = {
107+
type: 'boolean',
108+
title: 'Receive a certificate',
109+
'ui:widget': 'radio',
110+
};
111+
}
112+
113+
const courseDetailsSchema = {
114+
// The 'courseDetails' name matches the placeholder in the data input schema
115+
courseDetails: {
116+
type: 'object',
117+
title: `Course details for "${courseName}"`,
118+
properties: fields,
119+
},
120+
};
121+
122+
// HTTP 200
123+
res.send(JSON.stringify(courseDetailsSchema));
124+
});
125+
126+
app.get('/rooms', (req, res) => {
127+
logRequest(req);
128+
129+
// const courseName = req.query?.coursename;
130+
131+
const result = {
132+
room: { mydefault: 'Dynamically fetched default room name' },
133+
};
134+
135+
// HTTP 200
136+
res.send(JSON.stringify(result));
137+
});
138+
139+
app.post('/validateroom', (req, res) => {
140+
logRequest(req);
141+
142+
const field = req.body?.field;
143+
const value = req.body?.value;
144+
const courseName = req.body?.courseName;
145+
146+
if (field === 'courseDetails.room') {
147+
if (!value || value.length < 5) {
148+
// any 4xx or 5xx is fine here
149+
res.status(422);
150+
res.send({
151+
[field]: [
152+
'The field must be 5 or more characters long.',
153+
`This is something specific for ${courseName} courseName.`,
154+
],
155+
});
156+
157+
return;
158+
}
159+
}
160+
161+
// The HTTP 200 is important here. The response content "Valid" is not required, just a courtesy.
162+
res.status(200);
163+
res.send('Valid');
164+
});
165+
166+
app.get('/coursedetailsschema', (req, res) => {
167+
logRequest(req);
168+
169+
const courseName = req.query?.coursename;
170+
171+
let mydefault = 'My default room';
172+
if (courseName && courseName !== '___undefined___') {
173+
mydefault += ` for "${courseName}" course`;
174+
}
175+
176+
const response = {
177+
// The structure matches the "fetch:response:value" selector from "/coursedetailsschema" endpoint
178+
room: {
179+
mydefault,
180+
},
181+
};
182+
183+
// HTTP 200
184+
res.send(JSON.stringify(response));
185+
});
186+
187+
app.listen(port, () => {
188+
// eslint-disable-next-line no-console
189+
console.info(
190+
`Simple HTTP server for orchestrator-form-widgets development only. Provides endpoints for the "Dynamic course select" example workflow. Listening on ${port} port.`,
191+
);
192+
});

workspaces/orchestrator/plugins/orchestrator-form-widgets/http-workflow-dev-server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"main": "index.ts",
66
"scripts": {
77
"start": "node ./index.js",
8+
"start-courses": "node ./httpServer.dynamic.course.select.js",
89
"prettier:check": "prettier --ignore-unknown --check .",
910
"prettier:fix": "prettier --ignore-unknown --write .",
1011
"update-running-workflow": "cp -r ./exampleWorkflows/* ../../../packages/backend/.devModeTemp/repository/workflows/"

0 commit comments

Comments
 (0)