@oneuptime/common 10.0.61 → 10.0.63

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.
Files changed (34) hide show
  1. package/Server/API/TelemetryAPI.ts +36 -0
  2. package/Server/Services/ProfileAggregationService.ts +34 -2
  3. package/Server/Types/Workflow/ComponentCode.ts +18 -0
  4. package/Server/Types/Workflow/Components/Index.ts +2 -0
  5. package/Server/Types/Workflow/Components/Workflow.ts +129 -0
  6. package/Server/Types/Workflow/TriggerCode.ts +7 -0
  7. package/Server/Types/Workflow/Workflow.ts +7 -0
  8. package/Types/Workflow/Component.ts +1 -0
  9. package/Types/Workflow/ComponentID.ts +1 -0
  10. package/Types/Workflow/Components/Workflow.ts +26 -7
  11. package/UI/Components/Workflow/ArgumentsForm.tsx +127 -9
  12. package/UI/Components/Workflow/Utils.ts +11 -0
  13. package/build/dist/Server/API/TelemetryAPI.js +23 -5
  14. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  15. package/build/dist/Server/Services/ProfileAggregationService.js +17 -3
  16. package/build/dist/Server/Services/ProfileAggregationService.js.map +1 -1
  17. package/build/dist/Server/Types/Workflow/ComponentCode.js +5 -0
  18. package/build/dist/Server/Types/Workflow/ComponentCode.js.map +1 -1
  19. package/build/dist/Server/Types/Workflow/Components/Index.js +2 -0
  20. package/build/dist/Server/Types/Workflow/Components/Index.js.map +1 -1
  21. package/build/dist/Server/Types/Workflow/Components/Workflow.js +105 -0
  22. package/build/dist/Server/Types/Workflow/Components/Workflow.js.map +1 -0
  23. package/build/dist/Server/Types/Workflow/TriggerCode.js.map +1 -1
  24. package/build/dist/Types/Workflow/Component.js +1 -0
  25. package/build/dist/Types/Workflow/Component.js.map +1 -1
  26. package/build/dist/Types/Workflow/ComponentID.js +1 -0
  27. package/build/dist/Types/Workflow/ComponentID.js.map +1 -1
  28. package/build/dist/Types/Workflow/Components/Workflow.js +22 -7
  29. package/build/dist/Types/Workflow/Components/Workflow.js.map +1 -1
  30. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +98 -5
  31. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  32. package/build/dist/UI/Components/Workflow/Utils.js +10 -0
  33. package/build/dist/UI/Components/Workflow/Utils.js.map +1 -1
  34. 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<FlamegraphRequest, "serviceIds" | "profileType">,
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
- if (request.profileType) {
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
  }
@@ -27,6 +27,7 @@ export enum ComponentInputType {
27
27
  Operator = "Operator",
28
28
  Markdown = "Markdown",
29
29
  ValueType = "Value Type",
30
+ WorkflowSelect = "Workflow Select",
30
31
  }
31
32
 
32
33
  export enum ComponentType {
@@ -18,6 +18,7 @@ enum ComponentID {
18
18
  ApiPatch = "api-patch",
19
19
  SendEmail = "send-email",
20
20
  IfElse = "if-else",
21
+ WorkflowRun = "workflow-run",
21
22
  }
22
23
 
23
24
  export default ComponentID;
@@ -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: "workflow-run",
10
+ id: ComponentID.WorkflowRun,
10
11
  title: "Execute Workflow",
11
12
  category: "Utils",
12
- description: "Execute another workflow",
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.AnyValue,
18
- name: "Value",
19
- description: "Value to pass to another workflow",
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: "value",
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 { Argument, NodeDataProp } from "../../../Types/Workflow/Component";
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
- footerElement: (
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
- ...componentInputTypeToFormFieldType(
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,