@oneuptime/common 12.0.7 → 12.0.8
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/Models/DatabaseModels/TelemetryEntity.ts +45 -0
- package/Models/DatabaseModels/TelemetryEntityRelationship.ts +26 -0
- package/Models/DatabaseModels/WorkflowLog.ts +38 -0
- package/Server/API/UserAPI.ts +16 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.ts +41 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.ts +17 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/MasterAdminAuthorization.ts +11 -6
- package/Server/Services/TelemetryEntityRelationshipService.ts +3 -0
- package/Server/Services/TelemetryEntityService.ts +114 -1
- package/Server/Types/Workflow/Components/API/Utils.ts +44 -1
- package/Server/Types/Workflow/TriggerCode.ts +12 -0
- package/Server/Types/Workflow/Workflow.ts +5 -0
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +689 -0
- package/Server/Utils/VM/VMAPI.ts +56 -8
- package/Tests/App/Dashboard/MonitorCriteriaAttributeFilter.test.tsx +574 -0
- package/Tests/Server/API/UserProjectsAPI.test.ts +478 -4
- package/Tests/Server/Services/TelemetryEntityManualCreate.test.ts +248 -0
- package/Tests/Server/Types/Workflow/Components/ApiComponentHeaders.test.ts +192 -0
- package/Tests/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.test.ts +152 -0
- package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +322 -0
- package/Tests/Server/Utils/VM/VMAPISubstitution.test.ts +243 -0
- package/Tests/Types/SerializableObjectDictionaryImportCycle.test.ts +197 -0
- package/Tests/Types/Telemetry/EntityTypeGroups.test.ts +140 -0
- package/Tests/Types/Workflow/StepTrace.test.ts +235 -0
- package/Tests/Types/Workflow/TemplateSyntax.test.ts +379 -0
- package/Tests/Types/Workflow/Templates.test.ts +403 -0
- package/Tests/UI/Components/CustomTimeRangeModal.test.tsx +459 -0
- package/Tests/UI/Components/DictionaryAttributeFilterRow.test.tsx +477 -0
- package/Tests/UI/Components/Forms/AnchoredFieldPopupKeyboard.test.tsx +382 -0
- package/Tests/UI/Components/Forms/ColorPickerPicking.test.tsx +569 -0
- package/Tests/UI/Components/Forms/ValidationJSON.test.ts +241 -0
- package/Tests/UI/Components/LogTimeRangePicker.test.tsx +42 -0
- package/Tests/UI/Components/ModalPortalDismissal.test.tsx +90 -0
- package/Tests/UI/Components/TelemetryTimeRangePicker.test.tsx +49 -7
- package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +325 -0
- package/Tests/UI/Components/Workflow/GraphLint.test.ts +821 -0
- package/Tests/UI/Components/Workflow/ModelColumnEditor.test.ts +287 -0
- package/Tests/UI/Components/Workflow/ModelColumnEditorServerContract.test.ts +185 -0
- package/Tests/UI/Components/Workflow/ModelSchema.test.ts +174 -0
- package/Tests/UI/Components/Workflow/RunStatusWatcher.test.ts +152 -0
- package/Tests/UI/Components/Workflow/Utils.test.ts +192 -0
- package/Tests/UI/Monitor/MonitorStepCriteriaView.test.tsx +432 -0
- package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +514 -0
- package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +240 -0
- package/Tests/Utils/Schema/AnalyticsModelSchema.test.ts +469 -0
- package/Tests/Utils/Schema/ModelSchema.test.ts +268 -0
- package/Tests/Utils/Telemetry/EntityKeyNonTelemetry.test.ts +193 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +31 -0
- package/Types/SerializableObjectDictionary.ts +133 -39
- package/Types/Telemetry/EntitySource.ts +40 -0
- package/Types/Telemetry/EntityType.ts +27 -0
- package/Types/Telemetry/EntityTypeGroups.ts +65 -0
- package/Types/Workflow/Component.ts +29 -0
- package/Types/Workflow/Components/API.ts +35 -0
- package/Types/Workflow/Components/BaseModel.ts +7 -3
- package/Types/Workflow/StepTrace.ts +181 -0
- package/Types/Workflow/TemplateSyntax.ts +499 -0
- package/Types/Workflow/Templates.ts +316 -0
- package/UI/Components/Date/CustomTimeRangeModal.tsx +278 -0
- package/UI/Components/Date/TimeRangePickerDropdown.tsx +250 -0
- package/UI/Components/Dictionary/Dictionary.tsx +23 -6
- package/UI/Components/Forms/Fields/ColorPicker.tsx +66 -10
- package/UI/Components/Forms/Fields/IconPicker.tsx +48 -10
- package/UI/Components/Forms/Types/Field.ts +7 -0
- package/UI/Components/Forms/Validation.ts +63 -1
- package/UI/Components/Input/Input.tsx +31 -3
- package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +11 -216
- package/UI/Components/Modal/Modal.tsx +37 -4
- package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +11 -210
- package/UI/Components/Workflow/ArgumentsForm.tsx +194 -7
- package/UI/Components/Workflow/Component.tsx +28 -26
- package/UI/Components/Workflow/ComponentSettingsModal.tsx +69 -8
- package/UI/Components/Workflow/GraphLint.ts +591 -0
- package/UI/Components/Workflow/ModelColumnEditor.tsx +396 -0
- package/UI/Components/Workflow/ModelSchema.ts +196 -0
- package/UI/Components/Workflow/RunForm.tsx +41 -7
- package/UI/Components/Workflow/RunStatusWatcher.ts +122 -0
- package/UI/Components/Workflow/StepTraceViewer.tsx +186 -0
- package/UI/Components/Workflow/Utils.ts +93 -1
- package/UI/Components/Workflow/Workflow.tsx +77 -4
- package/UI/Types/LayeredDismissal.ts +31 -0
- package/UI/Types/UseAnchoredFieldPopup.ts +99 -0
- package/Utils/Monitor/NetworkDeviceRoleUtil.ts +533 -0
- package/Utils/Monitor/NetworkTopologyUtil.ts +45 -7
- package/Utils/Telemetry/EntityKey.ts +66 -0
- package/build/dist/Models/DatabaseModels/TelemetryEntity.js +47 -0
- package/build/dist/Models/DatabaseModels/TelemetryEntity.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js +27 -0
- package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkflowLog.js +39 -0
- package/build/dist/Models/DatabaseModels/WorkflowLog.js.map +1 -1
- package/build/dist/Server/API/UserAPI.js +16 -1
- package/build/dist/Server/API/UserAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js +11 -6
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +3 -0
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryEntityService.js +91 -1
- package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Utils.js +28 -0
- package/build/dist/Server/Types/Workflow/Components/API/Utils.js.map +1 -1
- package/build/dist/Server/Types/Workflow/TriggerCode.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +507 -0
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -0
- package/build/dist/Server/Utils/VM/VMAPI.js +51 -4
- package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
- package/build/dist/Types/SerializableObjectDictionary.js +133 -39
- package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
- package/build/dist/Types/Telemetry/EntitySource.js +39 -0
- package/build/dist/Types/Telemetry/EntitySource.js.map +1 -0
- package/build/dist/Types/Telemetry/EntityType.js +27 -0
- package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
- package/build/dist/Types/Telemetry/EntityTypeGroups.js +57 -0
- package/build/dist/Types/Telemetry/EntityTypeGroups.js.map +1 -0
- package/build/dist/Types/Workflow/Component.js +21 -0
- package/build/dist/Types/Workflow/Component.js.map +1 -1
- package/build/dist/Types/Workflow/Components/API.js +35 -0
- package/build/dist/Types/Workflow/Components/API.js.map +1 -1
- package/build/dist/Types/Workflow/Components/BaseModel.js +7 -3
- package/build/dist/Types/Workflow/Components/BaseModel.js.map +1 -1
- package/build/dist/Types/Workflow/StepTrace.js +104 -0
- package/build/dist/Types/Workflow/StepTrace.js.map +1 -0
- package/build/dist/Types/Workflow/TemplateSyntax.js +326 -0
- package/build/dist/Types/Workflow/TemplateSyntax.js.map +1 -0
- package/build/dist/Types/Workflow/Templates.js +218 -0
- package/build/dist/Types/Workflow/Templates.js.map +1 -0
- package/build/dist/UI/Components/Date/CustomTimeRangeModal.js +126 -0
- package/build/dist/UI/Components/Date/CustomTimeRangeModal.js.map +1 -0
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +123 -0
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -0
- package/build/dist/UI/Components/Dictionary/Dictionary.js +22 -6
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/ColorPicker.js +52 -13
- package/build/dist/UI/Components/Forms/Fields/ColorPicker.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/IconPicker.js +28 -12
- package/build/dist/UI/Components/Forms/Fields/IconPicker.js.map +1 -1
- package/build/dist/UI/Components/Forms/Validation.js +44 -0
- package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +12 -4
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +5 -108
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/Modal/Modal.js +31 -5
- package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +5 -105
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js +140 -11
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/Component.js +20 -19
- package/build/dist/UI/Components/Workflow/Component.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js +31 -3
- package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js.map +1 -1
- package/build/dist/UI/Components/Workflow/GraphLint.js +396 -0
- package/build/dist/UI/Components/Workflow/GraphLint.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +225 -0
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ModelSchema.js +118 -0
- package/build/dist/UI/Components/Workflow/ModelSchema.js.map +1 -0
- package/build/dist/UI/Components/Workflow/RunForm.js +22 -6
- package/build/dist/UI/Components/Workflow/RunForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/RunStatusWatcher.js +76 -0
- package/build/dist/UI/Components/Workflow/RunStatusWatcher.js.map +1 -0
- package/build/dist/UI/Components/Workflow/StepTraceViewer.js +76 -0
- package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -0
- package/build/dist/UI/Components/Workflow/Utils.js +73 -1
- package/build/dist/UI/Components/Workflow/Utils.js.map +1 -1
- package/build/dist/UI/Components/Workflow/Workflow.js +51 -6
- package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
- package/build/dist/UI/Types/LayeredDismissal.js +21 -0
- package/build/dist/UI/Types/LayeredDismissal.js.map +1 -0
- package/build/dist/UI/Types/UseAnchoredFieldPopup.js +74 -1
- package/build/dist/UI/Types/UseAnchoredFieldPopup.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +386 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +35 -6
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/Telemetry/EntityKey.js +52 -0
- package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Pure helpers behind the builder's "what happened to the run I just started"
|
|
3
|
+
* strip.
|
|
4
|
+
*
|
|
5
|
+
* Triggering a run from the builder used to end at a modal saying the workflow
|
|
6
|
+
* had been scheduled and that the Logs tab would know more. That left the one
|
|
7
|
+
* question the builder actually has — did it work — to a different page, a
|
|
8
|
+
* table, and a text blob. The runner does not hand back a log id at enqueue
|
|
9
|
+
* time (RunWorkflow creates the WorkflowLog itself once it picks the job up),
|
|
10
|
+
* so the builder watches for the run instead of being told about it.
|
|
11
|
+
*
|
|
12
|
+
* The logic that decides when to stop watching lives here, separate from the
|
|
13
|
+
* page, because it is the part worth testing.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import WorkflowStatus from "../../../Types/Workflow/WorkflowStatus";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Statuses a run never moves on from. Waiting is not one of them — a Sleep
|
|
20
|
+
* step parks a run for as long as it likes and it carries on afterwards.
|
|
21
|
+
*/
|
|
22
|
+
const TERMINAL_STATUSES: Array<WorkflowStatus> = [
|
|
23
|
+
WorkflowStatus.Success,
|
|
24
|
+
WorkflowStatus.Error,
|
|
25
|
+
WorkflowStatus.Timeout,
|
|
26
|
+
WorkflowStatus.WorkflowCountExceeded,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export type IsTerminalRunStatusFunction = (
|
|
30
|
+
status: WorkflowStatus | null | undefined,
|
|
31
|
+
) => boolean;
|
|
32
|
+
|
|
33
|
+
export const isTerminalRunStatus: IsTerminalRunStatusFunction = (
|
|
34
|
+
status: WorkflowStatus | null | undefined,
|
|
35
|
+
): boolean => {
|
|
36
|
+
if (!status) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return TERMINAL_STATUSES.includes(status);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type IsFailedRunStatusFunction = (
|
|
44
|
+
status: WorkflowStatus | null | undefined,
|
|
45
|
+
) => boolean;
|
|
46
|
+
|
|
47
|
+
export const isFailedRunStatus: IsFailedRunStatusFunction = (
|
|
48
|
+
status: WorkflowStatus | null | undefined,
|
|
49
|
+
): boolean => {
|
|
50
|
+
return (
|
|
51
|
+
status === WorkflowStatus.Error ||
|
|
52
|
+
status === WorkflowStatus.Timeout ||
|
|
53
|
+
status === WorkflowStatus.WorkflowCountExceeded
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
* How long to keep watching. A run that suspends on a Sleep step can outlive
|
|
59
|
+
* any sensible watch, so the strip stops following it and points at the Logs
|
|
60
|
+
* tab rather than polling for hours.
|
|
61
|
+
*/
|
|
62
|
+
export const MAX_RUN_WATCH_POLLS: number = 60;
|
|
63
|
+
export const RUN_WATCH_POLL_INTERVAL_MS: number = 2000;
|
|
64
|
+
|
|
65
|
+
export interface WatchedRun {
|
|
66
|
+
runId: string;
|
|
67
|
+
status: WorkflowStatus;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface RunWatchDecision {
|
|
71
|
+
/** Poll again? */
|
|
72
|
+
shouldContinue: boolean;
|
|
73
|
+
/** Text for the strip. Null when there is nothing to say yet. */
|
|
74
|
+
message: string | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type DecideRunWatchFunction = (params: {
|
|
78
|
+
run: WatchedRun | null;
|
|
79
|
+
pollCount: number;
|
|
80
|
+
}) => RunWatchDecision;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* What the strip should say, and whether to keep looking.
|
|
84
|
+
*/
|
|
85
|
+
export const decideRunWatch: DecideRunWatchFunction = (params: {
|
|
86
|
+
run: WatchedRun | null;
|
|
87
|
+
pollCount: number;
|
|
88
|
+
}): RunWatchDecision => {
|
|
89
|
+
if (params.pollCount >= MAX_RUN_WATCH_POLLS) {
|
|
90
|
+
return {
|
|
91
|
+
shouldContinue: false,
|
|
92
|
+
message:
|
|
93
|
+
"This run is taking a while. Open the Logs tab to follow it from there.",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!params.run) {
|
|
98
|
+
return { shouldContinue: true, message: "Starting run…" };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (isTerminalRunStatus(params.run.status)) {
|
|
102
|
+
return {
|
|
103
|
+
shouldContinue: false,
|
|
104
|
+
message: isFailedRunStatus(params.run.status)
|
|
105
|
+
? `Run ${params.run.status.toLowerCase()}. Open the Logs tab to see why.`
|
|
106
|
+
: "Run finished successfully.",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (params.run.status === WorkflowStatus.Waiting) {
|
|
111
|
+
return {
|
|
112
|
+
shouldContinue: false,
|
|
113
|
+
message:
|
|
114
|
+
"This run is sleeping and will carry on by itself. Follow it in the Logs tab.",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
shouldContinue: true,
|
|
120
|
+
message: `Run ${params.run.status.toLowerCase()}…`,
|
|
121
|
+
};
|
|
122
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A workflow run, read as the list of steps it actually took.
|
|
3
|
+
*
|
|
4
|
+
* The raw log is still there and still useful — it carries the component's own
|
|
5
|
+
* log lines and the warnings the runner emits — but "what did this step
|
|
6
|
+
* receive, what did it return, and where did it go next" is a question about
|
|
7
|
+
* structure, and answering it by scanning a newline-joined blob was the whole
|
|
8
|
+
* problem.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import Icon from "../Icon/Icon";
|
|
12
|
+
import IconProp from "../../../Types/Icon/IconProp";
|
|
13
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
14
|
+
import {
|
|
15
|
+
WorkflowStepStatus,
|
|
16
|
+
WorkflowStepTrace,
|
|
17
|
+
WorkflowStepTraceEntry,
|
|
18
|
+
} from "../../../Types/Workflow/StepTrace";
|
|
19
|
+
import React, { FunctionComponent, ReactElement, useState } from "react";
|
|
20
|
+
|
|
21
|
+
export interface ComponentProps {
|
|
22
|
+
trace: WorkflowStepTrace;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type FormatDurationFunction = (durationInMs: number) => string;
|
|
26
|
+
|
|
27
|
+
export const formatStepDuration: FormatDurationFunction = (
|
|
28
|
+
durationInMs: number,
|
|
29
|
+
): string => {
|
|
30
|
+
if (durationInMs < 1000) {
|
|
31
|
+
return `${durationInMs}ms`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (durationInMs < 60000) {
|
|
35
|
+
return `${(durationInMs / 1000).toFixed(1)}s`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const minutes: number = Math.floor(durationInMs / 60000);
|
|
39
|
+
const seconds: number = Math.round((durationInMs % 60000) / 1000);
|
|
40
|
+
|
|
41
|
+
return `${minutes}m ${seconds}s`;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
interface ValueBlockProps {
|
|
45
|
+
title: string;
|
|
46
|
+
values: JSONObject;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const ValueBlock: FunctionComponent<ValueBlockProps> = (
|
|
50
|
+
props: ValueBlockProps,
|
|
51
|
+
): ReactElement => {
|
|
52
|
+
const keys: Array<string> = Object.keys(props.values || {});
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div>
|
|
56
|
+
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-500 mb-1">
|
|
57
|
+
{props.title}
|
|
58
|
+
</p>
|
|
59
|
+
{keys.length === 0 ? (
|
|
60
|
+
<p className="text-sm text-gray-400">None</p>
|
|
61
|
+
) : (
|
|
62
|
+
<dl className="space-y-1">
|
|
63
|
+
{keys.map((key: string) => {
|
|
64
|
+
const value: unknown = props.values[key];
|
|
65
|
+
const text: string =
|
|
66
|
+
typeof value === "string"
|
|
67
|
+
? value
|
|
68
|
+
: JSON.stringify(value, null, 2);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div key={key} className="text-sm">
|
|
72
|
+
<dt className="text-gray-500 font-medium">{key}</dt>
|
|
73
|
+
<dd className="text-gray-800 font-mono text-xs whitespace-pre-wrap break-all">
|
|
74
|
+
{text}
|
|
75
|
+
</dd>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
})}
|
|
79
|
+
</dl>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
interface StepRowProps {
|
|
86
|
+
step: WorkflowStepTraceEntry;
|
|
87
|
+
index: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const StepRow: FunctionComponent<StepRowProps> = (
|
|
91
|
+
props: StepRowProps,
|
|
92
|
+
): ReactElement => {
|
|
93
|
+
const [isOpen, setIsOpen] = useState<boolean>(
|
|
94
|
+
props.step.status === WorkflowStepStatus.Error,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const isError: boolean = props.step.status === WorkflowStepStatus.Error;
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div
|
|
101
|
+
className={`rounded-md border ${
|
|
102
|
+
isError ? "border-red-200 bg-red-50" : "border-gray-200 bg-white"
|
|
103
|
+
}`}
|
|
104
|
+
>
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
aria-expanded={isOpen}
|
|
108
|
+
className="w-full text-left p-3 flex items-center gap-3 cursor-pointer"
|
|
109
|
+
onClick={() => {
|
|
110
|
+
setIsOpen(!isOpen);
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
<span className="text-xs text-gray-400 w-5 shrink-0">
|
|
114
|
+
{props.index + 1}
|
|
115
|
+
</span>
|
|
116
|
+
|
|
117
|
+
<Icon
|
|
118
|
+
icon={isError ? IconProp.Alert : IconProp.CheckCircle}
|
|
119
|
+
className={`h-4 w-4 shrink-0 ${
|
|
120
|
+
isError ? "text-red-500" : "text-green-500"
|
|
121
|
+
}`}
|
|
122
|
+
/>
|
|
123
|
+
|
|
124
|
+
<span className="flex-1 min-w-0">
|
|
125
|
+
<span className="block text-sm font-medium text-gray-900 truncate">
|
|
126
|
+
{props.step.title}
|
|
127
|
+
</span>
|
|
128
|
+
<span className="block text-xs text-gray-500 truncate">
|
|
129
|
+
{props.step.componentId}
|
|
130
|
+
</span>
|
|
131
|
+
</span>
|
|
132
|
+
|
|
133
|
+
<span className="text-xs text-gray-500 shrink-0">
|
|
134
|
+
{formatStepDuration(props.step.durationInMs)}
|
|
135
|
+
</span>
|
|
136
|
+
|
|
137
|
+
{props.step.executedPort && (
|
|
138
|
+
<span className="text-xs text-gray-500 shrink-0 hidden sm:inline">
|
|
139
|
+
→ {props.step.executedPort}
|
|
140
|
+
</span>
|
|
141
|
+
)}
|
|
142
|
+
</button>
|
|
143
|
+
|
|
144
|
+
{isOpen && (
|
|
145
|
+
<div className="border-t border-gray-200 p-3 space-y-3">
|
|
146
|
+
{props.step.errorMessage && (
|
|
147
|
+
<p className="text-sm text-red-700">{props.step.errorMessage}</p>
|
|
148
|
+
)}
|
|
149
|
+
<ValueBlock title="Received" values={props.step.argumentValues} />
|
|
150
|
+
<ValueBlock title="Returned" values={props.step.returnValues} />
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const StepTraceViewer: FunctionComponent<ComponentProps> = (
|
|
158
|
+
props: ComponentProps,
|
|
159
|
+
): ReactElement => {
|
|
160
|
+
const steps: Array<WorkflowStepTraceEntry> = props.trace?.steps || [];
|
|
161
|
+
|
|
162
|
+
if (steps.length === 0) {
|
|
163
|
+
return (
|
|
164
|
+
<p className="text-sm text-gray-500">
|
|
165
|
+
This run has no recorded steps. Runs from before step recording was
|
|
166
|
+
added show only the log below.
|
|
167
|
+
</p>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div className="space-y-2">
|
|
173
|
+
{props.trace.truncated && (
|
|
174
|
+
<p className="text-sm text-amber-700">
|
|
175
|
+
Only the last {steps.length} steps of this run were kept.
|
|
176
|
+
</p>
|
|
177
|
+
)}
|
|
178
|
+
|
|
179
|
+
{steps.map((step: WorkflowStepTraceEntry, index: number) => {
|
|
180
|
+
return <StepRow key={index} step={step} index={index} />;
|
|
181
|
+
})}
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export default StepTraceViewer;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { DropdownOption } from "../Dropdown/Dropdown";
|
|
2
2
|
import FormFieldSchemaType from "../Forms/Types/FormFieldSchemaType";
|
|
3
3
|
import IconProp from "../../../Types/Icon/IconProp";
|
|
4
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
5
|
+
import JSONFunctions from "../../../Types/JSONFunctions";
|
|
4
6
|
import ComponentMetadata, {
|
|
5
7
|
ComponentCategory,
|
|
6
8
|
ComponentInputType,
|
|
@@ -44,6 +46,81 @@ export const loadComponentsAndCategories: LoadComponentsAndCategoriesFunction =
|
|
|
44
46
|
return { components: initComponents, categories: initCategories };
|
|
45
47
|
};
|
|
46
48
|
|
|
49
|
+
export type ParseStringDictionaryValueFunction = (
|
|
50
|
+
argValue: unknown,
|
|
51
|
+
) => JSONObject | null;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The key/value view of a StringDictionary argument, or null when the value
|
|
55
|
+
* cannot be shown that way and belongs in the JSON editor instead.
|
|
56
|
+
*
|
|
57
|
+
* Editable as rows:
|
|
58
|
+
* - nothing yet (a new component)
|
|
59
|
+
* - an object of primitives, whether stored as an object (written by the row
|
|
60
|
+
* editor) or as a JSON string (written by the old JSON editor, and by
|
|
61
|
+
* every workflow that already exists)
|
|
62
|
+
*
|
|
63
|
+
* Not editable as rows, so left in the JSON editor:
|
|
64
|
+
* - a whole-field template such as {{local.components.x.returnValues.headers}},
|
|
65
|
+
* which substitutes an entire object at run time and is not JSON as written
|
|
66
|
+
* - text that does not parse, which is exactly the case where the builder
|
|
67
|
+
* most needs to see and fix what they typed
|
|
68
|
+
* - nested objects or arrays, which the row editor cannot represent and would
|
|
69
|
+
* therefore quietly discard
|
|
70
|
+
*/
|
|
71
|
+
export const parseStringDictionaryValue: ParseStringDictionaryValueFunction = (
|
|
72
|
+
argValue: unknown,
|
|
73
|
+
): JSONObject | null => {
|
|
74
|
+
if (argValue === null || argValue === undefined || argValue === "") {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let candidate: unknown = argValue;
|
|
79
|
+
|
|
80
|
+
if (typeof candidate === "string") {
|
|
81
|
+
if (candidate.trim() === "") {
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/*
|
|
86
|
+
* JSONFunctions.parse is JSON5, which is what the components themselves use
|
|
87
|
+
* on these values (ApiComponentUtils.sanitizeArgs). Parsing more strictly
|
|
88
|
+
* here than the runtime does would push a value that works today into the
|
|
89
|
+
* JSON editor for no reason.
|
|
90
|
+
*/
|
|
91
|
+
try {
|
|
92
|
+
candidate = JSONFunctions.parse(candidate);
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (
|
|
99
|
+
typeof candidate !== "object" ||
|
|
100
|
+
candidate === null ||
|
|
101
|
+
Array.isArray(candidate)
|
|
102
|
+
) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const entries: JSONObject = candidate as JSONObject;
|
|
107
|
+
|
|
108
|
+
for (const key of Object.keys(entries)) {
|
|
109
|
+
const value: unknown = entries[key];
|
|
110
|
+
|
|
111
|
+
const isPrimitive: boolean =
|
|
112
|
+
typeof value === "string" ||
|
|
113
|
+
typeof value === "number" ||
|
|
114
|
+
typeof value === "boolean";
|
|
115
|
+
|
|
116
|
+
if (!isPrimitive) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return entries;
|
|
122
|
+
};
|
|
123
|
+
|
|
47
124
|
type ComponentInputTypeToFormFieldTypeFunction = (
|
|
48
125
|
componentInputType: ComponentInputType,
|
|
49
126
|
argValue: unknown,
|
|
@@ -110,9 +187,24 @@ export const componentInputTypeToFormFieldType: ComponentInputTypeToFormFieldTyp
|
|
|
110
187
|
};
|
|
111
188
|
}
|
|
112
189
|
|
|
190
|
+
/*
|
|
191
|
+
* A dictionary of strings is a list of key/value pairs — request headers,
|
|
192
|
+
* query params — and asking someone to hand-write the braces and commas
|
|
193
|
+
* for that is where a good share of workflow JSON mistakes come from. Give
|
|
194
|
+
* it the key/value row editor instead.
|
|
195
|
+
*
|
|
196
|
+
* Values written before this change are JSON strings, and a value can also
|
|
197
|
+
* be something the row editor cannot represent (a whole-field
|
|
198
|
+
* {{...}} substitution, or nested objects). parseStringDictionaryValue
|
|
199
|
+
* decides; anything it cannot represent stays in the JSON editor, so no
|
|
200
|
+
* existing value is ever silently dropped on the floor.
|
|
201
|
+
*/
|
|
113
202
|
if (componentInputType === ComponentInputType.StringDictionary) {
|
|
114
203
|
return {
|
|
115
|
-
fieldType:
|
|
204
|
+
fieldType:
|
|
205
|
+
parseStringDictionaryValue(argValue) === null
|
|
206
|
+
? FormFieldSchemaType.JSON
|
|
207
|
+
: FormFieldSchemaType.Dictionary,
|
|
116
208
|
};
|
|
117
209
|
}
|
|
118
210
|
|
|
@@ -2,6 +2,12 @@ import WorkflowComponent from "./Component";
|
|
|
2
2
|
import ComponentSettingsModal from "./ComponentSettingsModal";
|
|
3
3
|
import ComponentsModal from "./ComponentsModal";
|
|
4
4
|
import RunModal from "./RunModal";
|
|
5
|
+
import {
|
|
6
|
+
LintGraphEdge,
|
|
7
|
+
LintGraphNode,
|
|
8
|
+
WorkflowLintResult,
|
|
9
|
+
lintWorkflowGraph,
|
|
10
|
+
} from "./GraphLint";
|
|
5
11
|
import { loadComponentsAndCategories } from "./Utils";
|
|
6
12
|
import { VoidFunction } from "../../../Types/FunctionTypes";
|
|
7
13
|
import IconProp from "../../../Types/Icon/IconProp";
|
|
@@ -17,6 +23,7 @@ import React, {
|
|
|
17
23
|
FunctionComponent,
|
|
18
24
|
useCallback,
|
|
19
25
|
useEffect,
|
|
26
|
+
useMemo,
|
|
20
27
|
useRef,
|
|
21
28
|
useState,
|
|
22
29
|
} from "react";
|
|
@@ -111,7 +118,14 @@ export interface ComponentProps {
|
|
|
111
118
|
workflowId: ObjectID;
|
|
112
119
|
onRunModalUpdate: (isModalShown: boolean) => void;
|
|
113
120
|
onRun: (trigger: NodeDataProp) => void;
|
|
121
|
+
/** Run one component on its own. */
|
|
122
|
+
onRunStep?: ((component: NodeDataProp) => void) | undefined;
|
|
114
123
|
webhookSecretKey?: string | undefined;
|
|
124
|
+
/**
|
|
125
|
+
* Called whenever the static checks over the graph are recomputed, so the
|
|
126
|
+
* page around the canvas can show a count and decide what to do about it.
|
|
127
|
+
*/
|
|
128
|
+
onLintResultChange?: ((result: WorkflowLintResult) => void) | undefined;
|
|
115
129
|
}
|
|
116
130
|
|
|
117
131
|
const Workflow: FunctionComponent<ComponentProps> = (props: ComponentProps) => {
|
|
@@ -244,6 +258,45 @@ const Workflow: FunctionComponent<ComponentProps> = (props: ComponentProps) => {
|
|
|
244
258
|
}
|
|
245
259
|
}, [nodes, edges]);
|
|
246
260
|
|
|
261
|
+
/*
|
|
262
|
+
* Static checks over the graph, recomputed as it is edited.
|
|
263
|
+
*
|
|
264
|
+
* The results are deliberately NOT written back into node state. Node data is
|
|
265
|
+
* what gets persisted (Builder.saveGraph sends node.data verbatim, minus
|
|
266
|
+
* metadata), so storing lint output there would save today's warnings into
|
|
267
|
+
* the workflow itself and show them again on reload even once they were
|
|
268
|
+
* fixed. Deriving a separate array for rendering keeps the saved graph clean.
|
|
269
|
+
*/
|
|
270
|
+
const lintResult: WorkflowLintResult = useMemo(() => {
|
|
271
|
+
return lintWorkflowGraph({
|
|
272
|
+
nodes: nodes as unknown as Array<LintGraphNode>,
|
|
273
|
+
edges: edges as unknown as Array<LintGraphEdge>,
|
|
274
|
+
});
|
|
275
|
+
}, [nodes, edges]);
|
|
276
|
+
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
props.onLintResultChange?.(lintResult);
|
|
279
|
+
}, [lintResult]);
|
|
280
|
+
|
|
281
|
+
const nodesToRender: Array<Node> = useMemo(() => {
|
|
282
|
+
return nodes.map((node: Node) => {
|
|
283
|
+
const error: string = lintResult.errorsByNodeId[node.id] || "";
|
|
284
|
+
|
|
285
|
+
// Same object when there is nothing to say, so react-flow can bail early.
|
|
286
|
+
if (!error && !node.data.error) {
|
|
287
|
+
return node;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
...node,
|
|
292
|
+
data: {
|
|
293
|
+
...node.data,
|
|
294
|
+
error: error,
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
}, [nodes, lintResult]);
|
|
299
|
+
|
|
247
300
|
const proOptions: ProOptions = { hideAttribution: true };
|
|
248
301
|
|
|
249
302
|
const onConnect: OnConnect = useCallback(
|
|
@@ -467,7 +520,7 @@ const Workflow: FunctionComponent<ComponentProps> = (props: ComponentProps) => {
|
|
|
467
520
|
`}
|
|
468
521
|
</style>
|
|
469
522
|
<ReactFlow
|
|
470
|
-
nodes={
|
|
523
|
+
nodes={nodesToRender}
|
|
471
524
|
edges={edges}
|
|
472
525
|
fitView={true}
|
|
473
526
|
onEdgeClick={() => {
|
|
@@ -575,6 +628,16 @@ const Workflow: FunctionComponent<ComponentProps> = (props: ComponentProps) => {
|
|
|
575
628
|
onDelete={(component: NodeDataProp) => {
|
|
576
629
|
deleteNode(component.id);
|
|
577
630
|
}}
|
|
631
|
+
/*
|
|
632
|
+
* Triggers have nothing to run on their own — they are the thing
|
|
633
|
+
* that starts a run, so "run just this step" is meaningless for one.
|
|
634
|
+
*/
|
|
635
|
+
onRunStep={
|
|
636
|
+
props.onRunStep &&
|
|
637
|
+
selectedNodeData.componentType !== ComponentType.Trigger
|
|
638
|
+
? props.onRunStep
|
|
639
|
+
: undefined
|
|
640
|
+
}
|
|
578
641
|
description={
|
|
579
642
|
selectedNodeData && selectedNodeData.metadata.description
|
|
580
643
|
? selectedNodeData.metadata.description
|
|
@@ -584,12 +647,22 @@ const Workflow: FunctionComponent<ComponentProps> = (props: ComponentProps) => {
|
|
|
584
647
|
setShowComponentSettingsModal(false);
|
|
585
648
|
}}
|
|
586
649
|
onSave={(componentData: NodeDataProp) => {
|
|
587
|
-
|
|
650
|
+
/*
|
|
651
|
+
* The settings modal was opened from the rendered node, so what
|
|
652
|
+
* comes back carries whatever lint message was on it. Clear that
|
|
653
|
+
* before it reaches node state — state is what gets saved, and a
|
|
654
|
+
* message about today's mistake has no business being written into
|
|
655
|
+
* the workflow.
|
|
656
|
+
*/
|
|
657
|
+
const dataToStore: NodeDataProp = {
|
|
658
|
+
...componentData,
|
|
659
|
+
error: "",
|
|
660
|
+
};
|
|
588
661
|
|
|
589
662
|
setNodes((nds: Array<Node>) => {
|
|
590
663
|
return nds.map((n: Node) => {
|
|
591
|
-
if (n.data.internalId ===
|
|
592
|
-
n.data =
|
|
664
|
+
if (n.data.internalId === dataToStore.internalId) {
|
|
665
|
+
n.data = dataToStore;
|
|
593
666
|
}
|
|
594
667
|
|
|
595
668
|
return n;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Dismissal belongs to the topmost layer that is open.
|
|
3
|
+
*
|
|
4
|
+
* A colour picker, an icon grid or an autocomplete menu inside a modal is
|
|
5
|
+
* portalled to document.body so the modal body cannot clip it, and it closes
|
|
6
|
+
* itself on a press that lands anywhere else. Very often "anywhere else" is the
|
|
7
|
+
* modal's own backdrop, which reads the same press as a dismissal of the whole
|
|
8
|
+
* dialog - so one click aimed at the popup takes the half-filled form with it.
|
|
9
|
+
*
|
|
10
|
+
* A popup that consumes a press records it here, and the backdrop skips any
|
|
11
|
+
* press it finds. The set is keyed on the native event object and holds it
|
|
12
|
+
* weakly, so nothing outlives the press it describes.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const pressesConsumedByAnAnchoredPopup: WeakSet<Event> = new WeakSet<Event>();
|
|
16
|
+
|
|
17
|
+
export type ConsumePressFunction = (event: Event) => void;
|
|
18
|
+
|
|
19
|
+
export const consumePressForAnchoredPopup: ConsumePressFunction = (
|
|
20
|
+
event: Event,
|
|
21
|
+
): void => {
|
|
22
|
+
pressesConsumedByAnAnchoredPopup.add(event);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type WasPressConsumedFunction = (event: Event) => boolean;
|
|
26
|
+
|
|
27
|
+
export const wasPressConsumedByAnAnchoredPopup: WasPressConsumedFunction = (
|
|
28
|
+
event: Event,
|
|
29
|
+
): boolean => {
|
|
30
|
+
return pressesConsumedByAnAnchoredPopup.has(event);
|
|
31
|
+
};
|