@oneuptime/common 10.0.61 → 10.0.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Server/API/TelemetryAPI.ts +36 -0
- package/Server/Services/ProfileAggregationService.ts +34 -2
- package/Server/Types/Workflow/ComponentCode.ts +18 -0
- package/Server/Types/Workflow/Components/Index.ts +2 -0
- package/Server/Types/Workflow/Components/Workflow.ts +129 -0
- package/Server/Types/Workflow/TriggerCode.ts +7 -0
- package/Server/Types/Workflow/Workflow.ts +7 -0
- package/Types/Workflow/Component.ts +1 -0
- package/Types/Workflow/ComponentID.ts +1 -0
- package/Types/Workflow/Components/Workflow.ts +26 -7
- package/UI/Components/Workflow/ArgumentsForm.tsx +127 -9
- package/UI/Components/Workflow/Utils.ts +11 -0
- package/build/dist/Server/API/TelemetryAPI.js +23 -5
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Services/ProfileAggregationService.js +17 -3
- package/build/dist/Server/Services/ProfileAggregationService.js.map +1 -1
- package/build/dist/Server/Types/Workflow/ComponentCode.js +5 -0
- package/build/dist/Server/Types/Workflow/ComponentCode.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Index.js +2 -0
- package/build/dist/Server/Types/Workflow/Components/Index.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Workflow.js +105 -0
- package/build/dist/Server/Types/Workflow/Components/Workflow.js.map +1 -0
- package/build/dist/Server/Types/Workflow/TriggerCode.js.map +1 -1
- package/build/dist/Types/Workflow/Component.js +1 -0
- package/build/dist/Types/Workflow/Component.js.map +1 -1
- package/build/dist/Types/Workflow/ComponentID.js +1 -0
- package/build/dist/Types/Workflow/ComponentID.js.map +1 -1
- package/build/dist/Types/Workflow/Components/Workflow.js +22 -7
- package/build/dist/Types/Workflow/Components/Workflow.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js +98 -5
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/Utils.js +10 -0
- package/build/dist/UI/Components/Workflow/Utils.js.map +1 -1
- package/package.json +1 -1
|
@@ -1208,6 +1208,16 @@ router.post(
|
|
|
1208
1208
|
? (body["profileType"] as string)
|
|
1209
1209
|
: undefined;
|
|
1210
1210
|
|
|
1211
|
+
const profileTypes: Array<string> | undefined = Array.isArray(
|
|
1212
|
+
body["profileTypes"],
|
|
1213
|
+
)
|
|
1214
|
+
? (body["profileTypes"] as Array<string>).filter(
|
|
1215
|
+
(t: unknown): t is string => {
|
|
1216
|
+
return typeof t === "string" && t.length > 0;
|
|
1217
|
+
},
|
|
1218
|
+
)
|
|
1219
|
+
: undefined;
|
|
1220
|
+
|
|
1211
1221
|
if (!profileId && !startTime) {
|
|
1212
1222
|
return Response.sendErrorResponse(
|
|
1213
1223
|
req,
|
|
@@ -1225,6 +1235,8 @@ router.post(
|
|
|
1225
1235
|
...(endTime !== undefined && { endTime }),
|
|
1226
1236
|
...(serviceIds !== undefined && { serviceIds }),
|
|
1227
1237
|
...(profileType !== undefined && { profileType }),
|
|
1238
|
+
...(profileTypes !== undefined &&
|
|
1239
|
+
profileTypes.length > 0 && { profileTypes }),
|
|
1228
1240
|
};
|
|
1229
1241
|
|
|
1230
1242
|
const flamegraph: ProfileFlamegraphNode =
|
|
@@ -1281,6 +1293,16 @@ router.post(
|
|
|
1281
1293
|
? (body["profileType"] as string)
|
|
1282
1294
|
: undefined;
|
|
1283
1295
|
|
|
1296
|
+
const profileTypes: Array<string> | undefined = Array.isArray(
|
|
1297
|
+
body["profileTypes"],
|
|
1298
|
+
)
|
|
1299
|
+
? (body["profileTypes"] as Array<string>).filter(
|
|
1300
|
+
(t: unknown): t is string => {
|
|
1301
|
+
return typeof t === "string" && t.length > 0;
|
|
1302
|
+
},
|
|
1303
|
+
)
|
|
1304
|
+
: undefined;
|
|
1305
|
+
|
|
1284
1306
|
const limit: number | undefined = body["limit"]
|
|
1285
1307
|
? (body["limit"] as number)
|
|
1286
1308
|
: undefined;
|
|
@@ -1296,6 +1318,8 @@ router.post(
|
|
|
1296
1318
|
endTime,
|
|
1297
1319
|
...(serviceIds !== undefined && { serviceIds }),
|
|
1298
1320
|
...(profileType !== undefined && { profileType }),
|
|
1321
|
+
...(profileTypes !== undefined &&
|
|
1322
|
+
profileTypes.length > 0 && { profileTypes }),
|
|
1299
1323
|
...(limit !== undefined && { limit }),
|
|
1300
1324
|
...(sortBy !== undefined && { sortBy }),
|
|
1301
1325
|
};
|
|
@@ -1506,6 +1530,16 @@ router.post(
|
|
|
1506
1530
|
? (body["profileType"] as string)
|
|
1507
1531
|
: undefined;
|
|
1508
1532
|
|
|
1533
|
+
const profileTypes: Array<string> | undefined = Array.isArray(
|
|
1534
|
+
body["profileTypes"],
|
|
1535
|
+
)
|
|
1536
|
+
? (body["profileTypes"] as Array<string>).filter(
|
|
1537
|
+
(t: unknown): t is string => {
|
|
1538
|
+
return typeof t === "string" && t.length > 0;
|
|
1539
|
+
},
|
|
1540
|
+
)
|
|
1541
|
+
: undefined;
|
|
1542
|
+
|
|
1509
1543
|
const request: DiffFlamegraphRequest = {
|
|
1510
1544
|
projectId: databaseProps.tenantId,
|
|
1511
1545
|
baselineStartTime,
|
|
@@ -1514,6 +1548,8 @@ router.post(
|
|
|
1514
1548
|
comparisonEndTime,
|
|
1515
1549
|
...(serviceIds !== undefined && { serviceIds }),
|
|
1516
1550
|
...(profileType !== undefined && { profileType }),
|
|
1551
|
+
...(profileTypes !== undefined &&
|
|
1552
|
+
profileTypes.length > 0 && { profileTypes }),
|
|
1517
1553
|
};
|
|
1518
1554
|
|
|
1519
1555
|
const diffFlamegraph: DiffFlamegraphNode =
|
|
@@ -26,7 +26,17 @@ export interface FlamegraphRequest {
|
|
|
26
26
|
startTime?: Date;
|
|
27
27
|
endTime?: Date;
|
|
28
28
|
serviceIds?: Array<ObjectID>;
|
|
29
|
+
/**
|
|
30
|
+
* Single profile type to filter on. Kept for backwards compat. When
|
|
31
|
+
* `profileTypes` is also supplied, `profileTypes` wins.
|
|
32
|
+
*/
|
|
29
33
|
profileType?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Multiple raw profile-type strings to OR together. The UI maps a
|
|
36
|
+
* user-facing category (e.g. "CPU") to all the raw type strings real
|
|
37
|
+
* agents actually emit (e.g. ["cpu", "samples"]).
|
|
38
|
+
*/
|
|
39
|
+
profileTypes?: Array<string>;
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
export interface FunctionListItem {
|
|
@@ -44,6 +54,7 @@ export interface FunctionListRequest {
|
|
|
44
54
|
endTime: Date;
|
|
45
55
|
serviceIds?: Array<ObjectID>;
|
|
46
56
|
profileType?: string;
|
|
57
|
+
profileTypes?: Array<string>;
|
|
47
58
|
limit?: number;
|
|
48
59
|
sortBy?: "selfValue" | "totalValue" | "sampleCount";
|
|
49
60
|
}
|
|
@@ -56,6 +67,7 @@ export interface DiffFlamegraphRequest {
|
|
|
56
67
|
comparisonEndTime: Date;
|
|
57
68
|
serviceIds?: Array<ObjectID>;
|
|
58
69
|
profileType?: string;
|
|
70
|
+
profileTypes?: Array<string>;
|
|
59
71
|
}
|
|
60
72
|
|
|
61
73
|
export interface DiffFlamegraphNode {
|
|
@@ -302,6 +314,9 @@ export class ProfileAggregationService {
|
|
|
302
314
|
...(request.profileType !== undefined && {
|
|
303
315
|
profileType: request.profileType,
|
|
304
316
|
}),
|
|
317
|
+
...(request.profileTypes !== undefined && {
|
|
318
|
+
profileTypes: request.profileTypes,
|
|
319
|
+
}),
|
|
305
320
|
});
|
|
306
321
|
|
|
307
322
|
const comparisonTree: ProfileFlamegraphNode =
|
|
@@ -315,6 +330,9 @@ export class ProfileAggregationService {
|
|
|
315
330
|
...(request.profileType !== undefined && {
|
|
316
331
|
profileType: request.profileType,
|
|
317
332
|
}),
|
|
333
|
+
...(request.profileTypes !== undefined && {
|
|
334
|
+
profileTypes: request.profileTypes,
|
|
335
|
+
}),
|
|
318
336
|
});
|
|
319
337
|
|
|
320
338
|
return ProfileAggregationService.mergeDiffTrees(
|
|
@@ -490,7 +508,10 @@ export class ProfileAggregationService {
|
|
|
490
508
|
|
|
491
509
|
private static appendCommonFilters(
|
|
492
510
|
statement: Statement,
|
|
493
|
-
request: Pick<
|
|
511
|
+
request: Pick<
|
|
512
|
+
FlamegraphRequest,
|
|
513
|
+
"serviceIds" | "profileType" | "profileTypes"
|
|
514
|
+
>,
|
|
494
515
|
): void {
|
|
495
516
|
if (request.serviceIds && request.serviceIds.length > 0) {
|
|
496
517
|
statement.append(
|
|
@@ -505,7 +526,18 @@ export class ProfileAggregationService {
|
|
|
505
526
|
);
|
|
506
527
|
}
|
|
507
528
|
|
|
508
|
-
|
|
529
|
+
/*
|
|
530
|
+
* profileTypes (array) wins over profileType (single) so the UI can
|
|
531
|
+
* OR together every raw type string in a category.
|
|
532
|
+
*/
|
|
533
|
+
if (request.profileTypes && request.profileTypes.length > 0) {
|
|
534
|
+
statement.append(
|
|
535
|
+
SQL` AND profileType IN (${{
|
|
536
|
+
type: TableColumnType.Text,
|
|
537
|
+
value: new Includes(request.profileTypes),
|
|
538
|
+
}})`,
|
|
539
|
+
);
|
|
540
|
+
} else if (request.profileType) {
|
|
509
541
|
statement.append(
|
|
510
542
|
SQL` AND profileType = ${{
|
|
511
543
|
type: TableColumnType.Text,
|
|
@@ -9,12 +9,30 @@ import ObjectID from "../../../Types/ObjectID";
|
|
|
9
9
|
import ComponentMetadata, { Port } from "../../../Types/Workflow/Component";
|
|
10
10
|
import CaptureSpan from "../../Utils/Telemetry/CaptureSpan";
|
|
11
11
|
|
|
12
|
+
export interface ExecuteChildWorkflow {
|
|
13
|
+
workflowId: ObjectID;
|
|
14
|
+
returnValues: JSONObject;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Maximum depth of nested workflow invocations (Execute Workflow component).
|
|
19
|
+
* Catches pathological fanout even when no direct cycle exists.
|
|
20
|
+
*/
|
|
21
|
+
export const MAX_WORKFLOW_CALL_DEPTH: number = 10;
|
|
22
|
+
|
|
12
23
|
export interface RunOptions {
|
|
13
24
|
log: (item: string | JSONObject | Error | JSONArray | JSONValue) => void;
|
|
14
25
|
workflowLogId: ObjectID;
|
|
15
26
|
workflowId: ObjectID;
|
|
16
27
|
projectId: ObjectID;
|
|
17
28
|
onError: (exception: Exception) => Exception;
|
|
29
|
+
/**
|
|
30
|
+
* Fire-and-forget trigger for another workflow in the same project.
|
|
31
|
+
* Enqueues the target workflow with the given `returnValues` as its arguments
|
|
32
|
+
* (the payload a Manual trigger in the child workflow will receive on its
|
|
33
|
+
* output port).
|
|
34
|
+
*/
|
|
35
|
+
executeWorkflow: (executeWorkflow: ExecuteChildWorkflow) => Promise<void>;
|
|
18
36
|
}
|
|
19
37
|
|
|
20
38
|
export interface RunReturnType {
|
|
@@ -30,6 +30,7 @@ import Schedule from "./Schedule";
|
|
|
30
30
|
import SlackSendMessageToChannel from "./Slack/SendMessageToChannel";
|
|
31
31
|
import TelegramSendMessageToChat from "./Telegram/SendMessageToChat";
|
|
32
32
|
import WebhookTrigger from "./Webhook";
|
|
33
|
+
import ExecuteWorkflow from "./Workflow";
|
|
33
34
|
import BaseModel from "../../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
34
35
|
import Dictionary from "../../../../Types/Dictionary";
|
|
35
36
|
import Text from "../../../../Types/Text";
|
|
@@ -57,6 +58,7 @@ const Components: Dictionary<ComponentCode> = {
|
|
|
57
58
|
[ComponentID.ApiPut]: new ApiPut(),
|
|
58
59
|
[ComponentID.SendEmail]: new Email(),
|
|
59
60
|
[ComponentID.IfElse]: new IfElse(),
|
|
61
|
+
[ComponentID.WorkflowRun]: new ExecuteWorkflow(),
|
|
60
62
|
};
|
|
61
63
|
|
|
62
64
|
for (const baseModelService of Services) {
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import ComponentCode, { RunOptions, RunReturnType } from "../ComponentCode";
|
|
2
|
+
import BadDataException from "../../../../Types/Exception/BadDataException";
|
|
3
|
+
import { JSONObject } from "../../../../Types/JSON";
|
|
4
|
+
import ObjectID from "../../../../Types/ObjectID";
|
|
5
|
+
import ComponentMetadata, { Port } from "../../../../Types/Workflow/Component";
|
|
6
|
+
import ComponentID from "../../../../Types/Workflow/ComponentID";
|
|
7
|
+
import WorkflowComponents from "../../../../Types/Workflow/Components/Workflow";
|
|
8
|
+
import CaptureSpan from "../../../Utils/Telemetry/CaptureSpan";
|
|
9
|
+
|
|
10
|
+
export default class ExecuteWorkflow extends ComponentCode {
|
|
11
|
+
public constructor() {
|
|
12
|
+
super();
|
|
13
|
+
|
|
14
|
+
const Component: ComponentMetadata | undefined = WorkflowComponents.find(
|
|
15
|
+
(i: ComponentMetadata) => {
|
|
16
|
+
return i.id === ComponentID.WorkflowRun;
|
|
17
|
+
},
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
if (!Component) {
|
|
21
|
+
throw new BadDataException("Execute Workflow component not found.");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.setMetadata(Component);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@CaptureSpan()
|
|
28
|
+
public override async run(
|
|
29
|
+
args: JSONObject,
|
|
30
|
+
options: RunOptions,
|
|
31
|
+
): Promise<RunReturnType> {
|
|
32
|
+
const successPort: Port | undefined = this.getMetadata().outPorts.find(
|
|
33
|
+
(p: Port) => {
|
|
34
|
+
return p.id === "out";
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const errorPort: Port | undefined = this.getMetadata().outPorts.find(
|
|
39
|
+
(p: Port) => {
|
|
40
|
+
return p.id === "error";
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (!successPort) {
|
|
45
|
+
throw options.onError(new BadDataException("Out port not found"));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!errorPort) {
|
|
49
|
+
throw options.onError(new BadDataException("Error port not found"));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const workflowIdString: string | undefined = args["workflowId"] as
|
|
54
|
+
| string
|
|
55
|
+
| undefined;
|
|
56
|
+
|
|
57
|
+
if (!workflowIdString) {
|
|
58
|
+
throw new BadDataException("Workflow ID is required.");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!ObjectID.isValidUUID(workflowIdString)) {
|
|
62
|
+
throw new BadDataException(
|
|
63
|
+
"Workflow ID is not a valid ObjectID: " + workflowIdString,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const targetWorkflowId: ObjectID = new ObjectID(workflowIdString);
|
|
68
|
+
|
|
69
|
+
// Prevent a workflow from triggering itself — obvious infinite loop.
|
|
70
|
+
if (targetWorkflowId.toString() === options.workflowId.toString()) {
|
|
71
|
+
throw new BadDataException(
|
|
72
|
+
"A workflow cannot execute itself. Use a different workflow ID.",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let payload: JSONObject = {};
|
|
77
|
+
const rawArgs: unknown = args["arguments"];
|
|
78
|
+
|
|
79
|
+
if (rawArgs) {
|
|
80
|
+
if (typeof rawArgs === "object") {
|
|
81
|
+
payload = rawArgs as JSONObject;
|
|
82
|
+
} else if (typeof rawArgs === "string") {
|
|
83
|
+
try {
|
|
84
|
+
payload = JSON.parse(rawArgs) as JSONObject;
|
|
85
|
+
} catch (err: unknown) {
|
|
86
|
+
throw new BadDataException(
|
|
87
|
+
"Arguments must be valid JSON: " +
|
|
88
|
+
(err instanceof Error ? err.message : String(err)),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
throw new BadDataException(
|
|
93
|
+
"Arguments must be a JSON object or a JSON string.",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
options.log("Enqueuing child workflow " + targetWorkflowId.toString());
|
|
99
|
+
options.log("Payload:");
|
|
100
|
+
options.log(payload);
|
|
101
|
+
|
|
102
|
+
await options.executeWorkflow({
|
|
103
|
+
workflowId: targetWorkflowId,
|
|
104
|
+
returnValues: payload,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
options.log(
|
|
108
|
+
"Child workflow " +
|
|
109
|
+
targetWorkflowId.toString() +
|
|
110
|
+
" enqueued successfully.",
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
returnValues: {},
|
|
115
|
+
executePort: successPort,
|
|
116
|
+
};
|
|
117
|
+
} catch (err: unknown) {
|
|
118
|
+
const message: string = err instanceof Error ? err.message : String(err);
|
|
119
|
+
options.log("Failed to execute child workflow: " + message);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
returnValues: {
|
|
123
|
+
error: message,
|
|
124
|
+
},
|
|
125
|
+
executePort: errorPort,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -13,6 +13,13 @@ import CaptureSpan from "../../Utils/Telemetry/CaptureSpan";
|
|
|
13
13
|
export interface ExecuteWorkflowType {
|
|
14
14
|
workflowId: ObjectID;
|
|
15
15
|
returnValues: JSONObject;
|
|
16
|
+
/**
|
|
17
|
+
* Chain of ancestor workflow IDs (oldest first). Set when one workflow
|
|
18
|
+
* invokes another via the Execute Workflow component so downstream runs
|
|
19
|
+
* can detect cycles. Top-level triggers (webhook, manual API, schedule)
|
|
20
|
+
* should leave this undefined.
|
|
21
|
+
*/
|
|
22
|
+
callChain?: Array<string>;
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
export interface InitProps {
|
|
@@ -6,4 +6,11 @@ export interface RunProps {
|
|
|
6
6
|
workflowId: ObjectID;
|
|
7
7
|
workflowLogId: ObjectID | null;
|
|
8
8
|
timeout: number;
|
|
9
|
+
/**
|
|
10
|
+
* Chain of ancestor workflow IDs that led to this run, oldest first.
|
|
11
|
+
* Empty / undefined for top-level runs. Used by the Execute Workflow
|
|
12
|
+
* component to detect cycles (A -> B -> A) and enforce a max recursion
|
|
13
|
+
* depth across workflow boundaries.
|
|
14
|
+
*/
|
|
15
|
+
callChain?: Array<string>;
|
|
9
16
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import IconProp from "../../Icon/IconProp";
|
|
2
|
+
import ComponentID from "../ComponentID";
|
|
2
3
|
import ComponentMetadata, {
|
|
3
4
|
ComponentInputType,
|
|
4
5
|
ComponentType,
|
|
@@ -6,19 +7,31 @@ import ComponentMetadata, {
|
|
|
6
7
|
|
|
7
8
|
const components: Array<ComponentMetadata> = [
|
|
8
9
|
{
|
|
9
|
-
id:
|
|
10
|
+
id: ComponentID.WorkflowRun,
|
|
10
11
|
title: "Execute Workflow",
|
|
11
12
|
category: "Utils",
|
|
12
|
-
description:
|
|
13
|
+
description:
|
|
14
|
+
"Execute another workflow in the same project (fire-and-forget)",
|
|
13
15
|
iconProp: IconProp.Workflow,
|
|
14
16
|
componentType: ComponentType.Component,
|
|
15
17
|
arguments: [
|
|
16
18
|
{
|
|
17
|
-
type: ComponentInputType.
|
|
18
|
-
name: "
|
|
19
|
-
description:
|
|
19
|
+
type: ComponentInputType.WorkflowSelect,
|
|
20
|
+
name: "Workflow",
|
|
21
|
+
description:
|
|
22
|
+
"Pick the workflow to execute. The workflow must be in the same project and be enabled. It must have a Manual trigger to receive the arguments passed below.",
|
|
23
|
+
required: true,
|
|
24
|
+
id: "workflowId",
|
|
25
|
+
placeholder: "Select a workflow",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
type: ComponentInputType.JSON,
|
|
29
|
+
name: "Arguments",
|
|
30
|
+
description:
|
|
31
|
+
"JSON payload to pass to the target workflow. The target workflow's Manual trigger will emit this object on its output port.",
|
|
20
32
|
required: false,
|
|
21
|
-
id: "
|
|
33
|
+
id: "arguments",
|
|
34
|
+
placeholder: '{ "key": "value" }',
|
|
22
35
|
},
|
|
23
36
|
],
|
|
24
37
|
returnValues: [],
|
|
@@ -34,9 +47,15 @@ const components: Array<ComponentMetadata> = [
|
|
|
34
47
|
{
|
|
35
48
|
title: "Out",
|
|
36
49
|
description:
|
|
37
|
-
"Connect to this port if you want other components to execute after the workflow is triggered",
|
|
50
|
+
"Connect to this port if you want other components to execute after the workflow is triggered. This component is fire-and-forget — it does not wait for the child workflow to finish.",
|
|
38
51
|
id: "out",
|
|
39
52
|
},
|
|
53
|
+
{
|
|
54
|
+
title: "Error",
|
|
55
|
+
description:
|
|
56
|
+
"Executes if the child workflow could not be enqueued (e.g. not found, disabled, or in a different project).",
|
|
57
|
+
id: "error",
|
|
58
|
+
},
|
|
40
59
|
],
|
|
41
60
|
},
|
|
42
61
|
];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import ComponentLoader from "../ComponentLoader/ComponentLoader";
|
|
1
2
|
import ErrorMessage from "../ErrorMessage/ErrorMessage";
|
|
2
3
|
import BasicForm, { FormProps } from "../Forms/BasicForm";
|
|
3
4
|
import FormValues from "../Forms/Types/FormValues";
|
|
@@ -7,7 +8,15 @@ import VariableModal from "./VariableModal";
|
|
|
7
8
|
import Dictionary from "../../../Types/Dictionary";
|
|
8
9
|
import { JSONObject } from "../../../Types/JSON";
|
|
9
10
|
import ObjectID from "../../../Types/ObjectID";
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
Argument,
|
|
13
|
+
ComponentInputType,
|
|
14
|
+
NodeDataProp,
|
|
15
|
+
} from "../../../Types/Workflow/Component";
|
|
16
|
+
import { DropdownOption } from "../Dropdown/Dropdown";
|
|
17
|
+
import { LIMIT_PER_PROJECT } from "../../../Types/Database/LimitMax";
|
|
18
|
+
import ModelAPI, { ListResult } from "../../Utils/ModelAPI/ModelAPI";
|
|
19
|
+
import Workflow from "../../../Models/DatabaseModels/Workflow";
|
|
11
20
|
import React, {
|
|
12
21
|
FunctionComponent,
|
|
13
22
|
ReactElement,
|
|
@@ -40,6 +49,88 @@ const ArgumentsForm: FunctionComponent<ComponentProps> = (
|
|
|
40
49
|
|
|
41
50
|
const [selectedArgId, setSelectedArgId] = useState<string>("");
|
|
42
51
|
|
|
52
|
+
/*
|
|
53
|
+
* Workflows in the current project, used to populate dropdowns for any
|
|
54
|
+
* argument of type WorkflowSelect (e.g. the "Workflow" field on the
|
|
55
|
+
* Execute Workflow component). Empty until the fetch completes.
|
|
56
|
+
*/
|
|
57
|
+
const [workflowDropdownOptions, setWorkflowDropdownOptions] = useState<
|
|
58
|
+
Array<DropdownOption>
|
|
59
|
+
>([]);
|
|
60
|
+
const [isLoadingWorkflows, setIsLoadingWorkflows] = useState<boolean>(false);
|
|
61
|
+
|
|
62
|
+
const hasWorkflowSelectArg: boolean = Boolean(
|
|
63
|
+
component.metadata.arguments?.some((arg: Argument) => {
|
|
64
|
+
return arg.type === ComponentInputType.WorkflowSelect;
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!hasWorkflowSelectArg) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let cancelled: boolean = false;
|
|
74
|
+
setIsLoadingWorkflows(true);
|
|
75
|
+
|
|
76
|
+
const loadWorkflows: () => Promise<void> = async (): Promise<void> => {
|
|
77
|
+
try {
|
|
78
|
+
const result: ListResult<Workflow> = await ModelAPI.getList<Workflow>({
|
|
79
|
+
modelType: Workflow,
|
|
80
|
+
query: {},
|
|
81
|
+
limit: LIMIT_PER_PROJECT,
|
|
82
|
+
skip: 0,
|
|
83
|
+
select: {
|
|
84
|
+
_id: true,
|
|
85
|
+
name: true,
|
|
86
|
+
},
|
|
87
|
+
sort: {
|
|
88
|
+
name: "Ascending" as any,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (cancelled) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const currentWorkflowIdStr: string = props.workflowId.toString();
|
|
97
|
+
|
|
98
|
+
const options: Array<DropdownOption> = result.data
|
|
99
|
+
.filter((wf: Workflow) => {
|
|
100
|
+
// Exclude the current workflow — can't pick yourself.
|
|
101
|
+
return wf._id?.toString() !== currentWorkflowIdStr;
|
|
102
|
+
})
|
|
103
|
+
.map((wf: Workflow) => {
|
|
104
|
+
return {
|
|
105
|
+
label: (wf.name as string) || (wf._id?.toString() ?? ""),
|
|
106
|
+
value: wf._id?.toString() ?? "",
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
setWorkflowDropdownOptions(options);
|
|
111
|
+
} catch {
|
|
112
|
+
/*
|
|
113
|
+
* Swallow: the dropdown will simply be empty and the user can try
|
|
114
|
+
* again by re-opening the settings panel.
|
|
115
|
+
*/
|
|
116
|
+
if (!cancelled) {
|
|
117
|
+
setWorkflowDropdownOptions([]);
|
|
118
|
+
}
|
|
119
|
+
} finally {
|
|
120
|
+
if (!cancelled) {
|
|
121
|
+
setIsLoadingWorkflows(false);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
void loadWorkflows();
|
|
127
|
+
|
|
128
|
+
return () => {
|
|
129
|
+
cancelled = true;
|
|
130
|
+
};
|
|
131
|
+
// Only re-fetch when the component in the settings panel changes identity.
|
|
132
|
+
}, [component.id, hasWorkflowSelectArg]);
|
|
133
|
+
|
|
43
134
|
useEffect(() => {
|
|
44
135
|
props.onHasFormValidationErrors(hasFormValidationErrors);
|
|
45
136
|
}, [hasFormValidationErrors]);
|
|
@@ -61,8 +152,15 @@ const ArgumentsForm: FunctionComponent<ComponentProps> = (
|
|
|
61
152
|
message={"This component does not take any arguments."}
|
|
62
153
|
/>
|
|
63
154
|
)}
|
|
155
|
+
{/*
|
|
156
|
+
If any argument is a WorkflowSelect and we're still fetching the
|
|
157
|
+
list of workflows, show a loader instead of the form. Otherwise
|
|
158
|
+
the user briefly sees an empty dropdown which is confusing.
|
|
159
|
+
*/}
|
|
160
|
+
{hasWorkflowSelectArg && isLoadingWorkflows && <ComponentLoader />}
|
|
64
161
|
{component.metadata.arguments &&
|
|
65
|
-
component.metadata.arguments.length > 0 &&
|
|
162
|
+
component.metadata.arguments.length > 0 &&
|
|
163
|
+
!(hasWorkflowSelectArg && isLoadingWorkflows) && (
|
|
66
164
|
<BasicForm
|
|
67
165
|
hideSubmitButton={true}
|
|
68
166
|
ref={formRef}
|
|
@@ -89,9 +187,34 @@ const ArgumentsForm: FunctionComponent<ComponentProps> = (
|
|
|
89
187
|
fields={
|
|
90
188
|
component.metadata.arguments &&
|
|
91
189
|
component.metadata.arguments.map((arg: Argument) => {
|
|
190
|
+
const isWorkflowSelect: boolean =
|
|
191
|
+
arg.type === ComponentInputType.WorkflowSelect;
|
|
192
|
+
|
|
193
|
+
const baseField: {
|
|
194
|
+
fieldType: import("../Forms/Types/FormFieldSchemaType").default;
|
|
195
|
+
dropdownOptions?: Array<DropdownOption> | undefined;
|
|
196
|
+
} = componentInputTypeToFormFieldType(
|
|
197
|
+
arg.type,
|
|
198
|
+
component.arguments && component.arguments[arg.id]
|
|
199
|
+
? component.arguments[arg.id]
|
|
200
|
+
: null,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
/*
|
|
204
|
+
* For WorkflowSelect, inject the dynamically fetched list
|
|
205
|
+
* of workflows as dropdown options.
|
|
206
|
+
*/
|
|
207
|
+
if (isWorkflowSelect) {
|
|
208
|
+
baseField.dropdownOptions = workflowDropdownOptions;
|
|
209
|
+
}
|
|
210
|
+
|
|
92
211
|
return {
|
|
93
212
|
title: `${arg.name}`,
|
|
94
|
-
|
|
213
|
+
/*
|
|
214
|
+
* WorkflowSelect has no "pick from component/variable"
|
|
215
|
+
* footer — it's a bound dropdown, not a free-text field.
|
|
216
|
+
*/
|
|
217
|
+
footerElement: isWorkflowSelect ? undefined : (
|
|
95
218
|
<div className="text-gray-500">
|
|
96
219
|
<p className="text-sm">
|
|
97
220
|
Pick this value from other{" "}
|
|
@@ -125,12 +248,7 @@ const ArgumentsForm: FunctionComponent<ComponentProps> = (
|
|
|
125
248
|
},
|
|
126
249
|
required: arg.required,
|
|
127
250
|
placeholder: arg.placeholder,
|
|
128
|
-
...
|
|
129
|
-
arg.type,
|
|
130
|
-
component.arguments && component.arguments[arg.id]
|
|
131
|
-
? component.arguments[arg.id]
|
|
132
|
-
: null,
|
|
133
|
-
),
|
|
251
|
+
...baseField,
|
|
134
252
|
};
|
|
135
253
|
})
|
|
136
254
|
}
|
|
@@ -233,6 +233,17 @@ export const componentInputTypeToFormFieldType: ComponentInputTypeToFormFieldTyp
|
|
|
233
233
|
};
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
if (componentInputType === ComponentInputType.WorkflowSelect) {
|
|
237
|
+
/*
|
|
238
|
+
* Dropdown options are injected at render time by ArgumentsForm,
|
|
239
|
+
* which fetches the list of workflows in the current project.
|
|
240
|
+
*/
|
|
241
|
+
return {
|
|
242
|
+
fieldType: FormFieldSchemaType.Dropdown,
|
|
243
|
+
dropdownOptions: [],
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
236
247
|
if (componentInputType === ComponentInputType.ValueType) {
|
|
237
248
|
return {
|
|
238
249
|
fieldType: FormFieldSchemaType.Dropdown,
|