@revenexx/studio-integrations 0.7.0 → 1.1.0
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/CHANGELOG.md +633 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +4 -1
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +8 -2
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +2 -6
- package/dist/runtime/components/Integrations/ExpressionInput.vue +5 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.d.vue.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +16 -3
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue.d.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +1 -0
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +2 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +3 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/NodeInspector.vue +135 -10
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +2 -1
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +34 -8
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +2 -1
- package/dist/runtime/components/Integrations/NodePalette.vue +12 -8
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +9 -6
- package/dist/runtime/components/Integrations/RunsTable.vue +2 -2
- package/dist/runtime/components/Integrations/StickyNoteView.vue +45 -11
- package/dist/runtime/components/Integrations/SurfaceCard.d.vue.ts +50 -0
- package/dist/runtime/components/Integrations/SurfaceCard.vue +55 -0
- package/dist/runtime/components/Integrations/SurfaceCard.vue.d.ts +50 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue +3 -3
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +12 -3
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +5 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +16 -0
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +79 -12
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +16 -0
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +13 -3
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +13 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +95 -29
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +13 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +11 -10
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.d.vue.ts +16 -17
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +37 -26
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue.d.ts +16 -17
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +4 -5
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +53 -14
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +9 -4
- package/dist/runtime/composables/useIntegrationsCredentials.d.ts +11 -0
- package/dist/runtime/composables/useIntegrationsCredentials.js +2 -2
- package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +9 -2
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +27 -18
- package/dist/runtime/composables/useIntegrationsNodes.js +11 -1
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +30 -1
- package/dist/runtime/composables/useIntegrationsRuns.js +70 -29
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +7 -14
- package/dist/runtime/composables/useIntegrationsSchedules.js +80 -35
- package/dist/runtime/composables/useIntegrationsTriggers.d.ts +30 -3
- package/dist/runtime/composables/useIntegrationsTriggers.js +18 -7
- package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +10 -1
- package/dist/runtime/composables/useIntegrationsWorkflows.js +25 -4
- package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
- package/dist/runtime/data/integrationNodeCatalog.d.ts +22 -1
- package/dist/runtime/data/integrationNodeCatalog.js +8 -4
- package/dist/runtime/data/loopBody.d.ts +129 -0
- package/dist/runtime/data/loopBody.js +76 -0
- package/dist/runtime/data/nodeInputData.d.ts +58 -1
- package/dist/runtime/data/nodeInputData.js +30 -5
- package/dist/runtime/data/triggerGlyphs.d.ts +47 -0
- package/dist/runtime/data/triggerGlyphs.js +11 -0
- package/dist/runtime/i18n/messages/de.json +62 -33
- package/dist/runtime/i18n/messages/en.json +32 -3
- package/dist/runtime/pages/integrations/credentials.vue +4 -1
- package/dist/runtime/pages/integrations/index.vue +32 -34
- package/dist/runtime/pages/integrations/runs.vue +22 -10
- package/dist/runtime/pages/integrations/schedules.vue +64 -7
- package/dist/runtime/pages/integrations/templates/[slug].vue +13 -5
- package/dist/runtime/pages/integrations/workflows/[id].vue +193 -104
- package/dist/runtime/types/integrations.d.ts +64 -1
- package/dist/runtime/utils/cockpitSources.d.ts +22 -3
- package/dist/runtime/utils/credentialState.d.ts +6 -1
- package/dist/runtime/utils/credentialState.js +1 -1
- package/dist/runtime/utils/localize.d.ts +34 -0
- package/dist/runtime/utils/localize.js +22 -7
- package/dist/runtime/utils/pollBudget.d.ts +1 -1
- package/dist/runtime/utils/servedPaths.d.ts +22 -0
- package/dist/runtime/utils/servedPaths.js +3 -0
- package/dist/runtime/utils/statusStyle.d.ts +52 -0
- package/dist/runtime/utils/statusStyle.js +26 -0
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import { useIntegrationsApi } from "./integrationsApi.js";
|
|
|
3
3
|
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
4
4
|
import { IntegrationsApiError } from "../utils/apiError.js";
|
|
5
5
|
import { useLoadGate } from "./useLoadGate.js";
|
|
6
|
+
const EMPTY_SUMMARY = { total: 0, in_flight: 0, by_status: {} };
|
|
6
7
|
function mapStatus(status) {
|
|
7
8
|
switch (status) {
|
|
8
9
|
case "completed":
|
|
@@ -31,22 +32,30 @@ export function useIntegrationsRuns() {
|
|
|
31
32
|
const error = useState("integrations.runs.error", () => null);
|
|
32
33
|
const loaded = useState("integrations.runs.loaded", () => false);
|
|
33
34
|
const reconciling = useState("integrations.runs.reconciling", () => false);
|
|
35
|
+
const summary = useState("integrations.runs.summary", () => ({ ...EMPTY_SUMMARY }));
|
|
36
|
+
const hasMore = useState("integrations.runs.hasMore", () => false);
|
|
37
|
+
const query = useState("integrations.runs.query", () => ({
|
|
38
|
+
page: 1,
|
|
39
|
+
perPage: 100
|
|
40
|
+
}));
|
|
34
41
|
const gate = useLoadGate("integrations.runs", { loading, loaded });
|
|
35
42
|
const SYNC_LIMIT = 5;
|
|
36
43
|
const api = useIntegrationsApi();
|
|
37
44
|
const t = useIntegrationsT();
|
|
38
|
-
|
|
39
|
-
function mapRun(r, workflowName) {
|
|
45
|
+
function mapRun(r, workflowName = "") {
|
|
40
46
|
const startedMs = r.started_at ? new Date(r.started_at).getTime() : null;
|
|
41
47
|
const finishedMs = r.completed_at ? new Date(r.completed_at).getTime() : null;
|
|
42
48
|
return {
|
|
43
49
|
id: String(r.id),
|
|
44
50
|
workflowId: r.workflow_id,
|
|
45
|
-
workflowName,
|
|
51
|
+
workflowName: r.workflow_name ?? workflowName,
|
|
46
52
|
status: mapStatus(r.status),
|
|
47
53
|
temporalRunId: r.temporal_run_id ?? null,
|
|
48
|
-
triggerId: "",
|
|
49
|
-
|
|
54
|
+
triggerId: r.trigger_handle ?? "",
|
|
55
|
+
// The platform reports what started the run. This was hardcoded to
|
|
56
|
+
// `manual` because the payload carried nothing, which left the trigger
|
|
57
|
+
// column and its filter with one populated value in production.
|
|
58
|
+
triggerType: r.trigger_type ?? "manual",
|
|
50
59
|
startedAt: r.started_at ?? "",
|
|
51
60
|
finishedAt: r.completed_at ?? null,
|
|
52
61
|
// The list carries the reason a run failed; dropping it here was why every
|
|
@@ -63,25 +72,14 @@ export function useIntegrationsRuns() {
|
|
|
63
72
|
if (!await gate.claim(force)) return runs.value;
|
|
64
73
|
error.value = null;
|
|
65
74
|
try {
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
`/workflows/${wf.id}/runs?page=${page}&per_page=100`
|
|
75
|
-
);
|
|
76
|
-
out.push(...(res?.data ?? []).map((r) => mapRun(r, wf.name)));
|
|
77
|
-
const last = res?.meta?.last_page ?? 1;
|
|
78
|
-
if (page >= last) break;
|
|
79
|
-
page++;
|
|
80
|
-
}
|
|
81
|
-
return out;
|
|
82
|
-
})
|
|
83
|
-
);
|
|
84
|
-
runs.value = perWorkflow.flatMap((p) => p.status === "fulfilled" ? p.value : []).sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime());
|
|
75
|
+
const { page: pageNumber, perPage } = query.value;
|
|
76
|
+
const [page, counts] = await Promise.all([
|
|
77
|
+
api.get(`/runs?page=${pageNumber}&per_page=${perPage}`),
|
|
78
|
+
api.get("/runs/summary")
|
|
79
|
+
]);
|
|
80
|
+
runs.value = (page?.data ?? []).map((r) => mapRun(r));
|
|
81
|
+
hasMore.value = page?.meta?.has_more ?? false;
|
|
82
|
+
summary.value = counts?.data ?? { ...EMPTY_SUMMARY };
|
|
85
83
|
if (!reconciling.value) {
|
|
86
84
|
reconciling.value = true;
|
|
87
85
|
try {
|
|
@@ -99,9 +97,10 @@ export function useIntegrationsRuns() {
|
|
|
99
97
|
reconciling.value = false;
|
|
100
98
|
}
|
|
101
99
|
}
|
|
102
|
-
loaded.value =
|
|
100
|
+
loaded.value = true;
|
|
103
101
|
} catch (e) {
|
|
104
102
|
error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? t("integration.runs.error.loadFailed");
|
|
103
|
+
loaded.value = false;
|
|
105
104
|
} finally {
|
|
106
105
|
gate.release();
|
|
107
106
|
}
|
|
@@ -137,7 +136,40 @@ export function useIntegrationsRuns() {
|
|
|
137
136
|
let result = [...runs.value];
|
|
138
137
|
if (filter?.workflowId !== void 0) result = result.filter((r) => r.workflowId === filter.workflowId);
|
|
139
138
|
if (filter?.status) result = result.filter((r) => r.status === filter.status);
|
|
140
|
-
return result
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
async function loadSummary() {
|
|
142
|
+
try {
|
|
143
|
+
const res = await api.get("/runs/summary");
|
|
144
|
+
summary.value = res?.data ?? { ...EMPTY_SUMMARY };
|
|
145
|
+
} catch (e) {
|
|
146
|
+
error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? t("integration.runs.error.loadFailed");
|
|
147
|
+
}
|
|
148
|
+
return summary.value;
|
|
149
|
+
}
|
|
150
|
+
const latestPerWorkflow = useState("integrations.runs.latest", () => []);
|
|
151
|
+
const latestLoaded = useState("integrations.runs.latest.loaded", () => false);
|
|
152
|
+
const latestLoading = useState("integrations.runs.latest.loading", () => false);
|
|
153
|
+
const latestGate = useLoadGate("integrations.runs.latest", {
|
|
154
|
+
loading: latestLoading,
|
|
155
|
+
loaded: latestLoaded
|
|
156
|
+
});
|
|
157
|
+
async function loadLatestPerWorkflow(force = false) {
|
|
158
|
+
if (!await latestGate.claim(force)) return latestPerWorkflow.value;
|
|
159
|
+
try {
|
|
160
|
+
const res = await api.get("/runs/latest-per-workflow");
|
|
161
|
+
latestPerWorkflow.value = (res?.data ?? []).map((r) => mapRun(r));
|
|
162
|
+
latestLoaded.value = true;
|
|
163
|
+
} catch (e) {
|
|
164
|
+
error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? t("integration.runs.error.loadFailed");
|
|
165
|
+
latestLoaded.value = false;
|
|
166
|
+
} finally {
|
|
167
|
+
latestGate.release();
|
|
168
|
+
}
|
|
169
|
+
return latestPerWorkflow.value;
|
|
170
|
+
}
|
|
171
|
+
function lastRunFor(workflowId) {
|
|
172
|
+
return latestPerWorkflow.value.find((r) => r.workflowId === workflowId) ?? null;
|
|
141
173
|
}
|
|
142
174
|
function get(id) {
|
|
143
175
|
return runs.value.find((r) => r.id === id) ?? null;
|
|
@@ -175,9 +207,9 @@ export function useIntegrationsRuns() {
|
|
|
175
207
|
return api.get(`/workflows/${workflowId}/runs/${runId}/history${suffix}`);
|
|
176
208
|
};
|
|
177
209
|
const getStackTrace = (workflowId, runId) => api.get(`/workflows/${workflowId}/runs/${runId}/stack-trace`).then((r) => r.stack_trace);
|
|
178
|
-
const total = computed(() =>
|
|
179
|
-
const failedCount = computed(() =>
|
|
180
|
-
const runningCount = computed(() =>
|
|
210
|
+
const total = computed(() => summary.value.total);
|
|
211
|
+
const failedCount = computed(() => Object.entries(summary.value.by_status).filter(([status]) => mapStatus(status) === "failed").reduce((sum, [, count]) => sum + count, 0));
|
|
212
|
+
const runningCount = computed(() => summary.value.in_flight);
|
|
181
213
|
return {
|
|
182
214
|
runs,
|
|
183
215
|
loading,
|
|
@@ -186,11 +218,20 @@ export function useIntegrationsRuns() {
|
|
|
186
218
|
total,
|
|
187
219
|
failedCount,
|
|
188
220
|
runningCount,
|
|
221
|
+
summary,
|
|
222
|
+
hasMore,
|
|
223
|
+
query,
|
|
189
224
|
load,
|
|
225
|
+
loadSummary,
|
|
190
226
|
loadForWorkflow,
|
|
191
227
|
list,
|
|
192
228
|
get,
|
|
193
229
|
fetchRun,
|
|
230
|
+
latestPerWorkflow,
|
|
231
|
+
latestLoaded,
|
|
232
|
+
latestLoading,
|
|
233
|
+
loadLatestPerWorkflow,
|
|
234
|
+
lastRunFor,
|
|
194
235
|
trigger,
|
|
195
236
|
cancelRun,
|
|
196
237
|
terminateRun,
|
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import type { ScheduleEntry } from '../types/integrations.js';
|
|
2
|
-
/**
|
|
3
|
-
* Derived view — every trigger of type `schedule` across all workflows,
|
|
4
|
-
* sourced from the real triggers sub-resource (useIntegrationsTriggers).
|
|
5
|
-
* Next/last-run timestamps are mocked deterministically (no telemetry
|
|
6
|
-
* endpoint yet).
|
|
7
|
-
*
|
|
8
|
-
* Unordered on purpose: the page's table sorts, by its own default and by
|
|
9
|
-
* whichever column the operator clicks, so an order established here would be
|
|
10
|
-
* replaced before anyone saw it.
|
|
11
|
-
*/
|
|
12
2
|
export declare function useIntegrationsSchedules(): {
|
|
13
|
-
entries:
|
|
14
|
-
total: import("vue").ComputedRef<
|
|
15
|
-
activeCount: import("vue").ComputedRef<
|
|
3
|
+
entries: any;
|
|
4
|
+
total: import("vue").ComputedRef<any>;
|
|
5
|
+
activeCount: import("vue").ComputedRef<any>;
|
|
6
|
+
timezonesInUse: import("vue").ComputedRef<any>;
|
|
16
7
|
loading: any;
|
|
17
|
-
|
|
8
|
+
loaded: any;
|
|
9
|
+
error: any;
|
|
10
|
+
load: (force?: boolean) => Promise<ScheduleEntry[]>;
|
|
18
11
|
};
|
|
@@ -1,39 +1,84 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
|
+
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
|
+
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
4
|
+
import { useLoadGate } from "./useLoadGate.js";
|
|
5
|
+
import { IntegrationsApiError } from "../utils/apiError.js";
|
|
2
6
|
import { scheduleTimezone } from "../utils/schedule.js";
|
|
3
7
|
export function useIntegrationsSchedules() {
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
8
|
+
const api = useIntegrationsApi();
|
|
9
|
+
const t = useIntegrationsT();
|
|
10
|
+
const entries = useState("integrations.schedules", () => []);
|
|
11
|
+
const loading = useState("integrations.schedules.loading", () => false);
|
|
12
|
+
const loaded = useState("integrations.schedules.loaded", () => false);
|
|
13
|
+
const error = useState("integrations.schedules.error", () => null);
|
|
14
|
+
const meta = useState("integrations.schedules.meta", () => ({
|
|
15
|
+
total: 0,
|
|
16
|
+
live_count: 0,
|
|
17
|
+
timezones: []
|
|
18
|
+
}));
|
|
19
|
+
const gate = useLoadGate("integrations.schedules", { loading, loaded });
|
|
20
|
+
function mapEntry(row) {
|
|
21
|
+
const block = row.schedule;
|
|
22
|
+
const firstFiring = block?.next_fire_times?.[0] ?? null;
|
|
23
|
+
return {
|
|
24
|
+
id: `${row.workflow_id}:${row.id}`,
|
|
25
|
+
workflowId: row.workflow_id,
|
|
26
|
+
workflowName: row.workflow_name ?? "",
|
|
27
|
+
triggerId: String(row.id),
|
|
28
|
+
// Never invented: a trigger without an expression says so on the page.
|
|
29
|
+
// Standing in `* * * * *` for it would read as "every minute", and with the
|
|
30
|
+
// expression no longer on screen that guess would be invisible.
|
|
31
|
+
cron: block?.cron ?? null,
|
|
32
|
+
// The zone the platform resolved, rather than one guessed here — but the
|
|
33
|
+
// platform's default put back where it reports none. `timezone` is null on
|
|
34
|
+
// the wire whenever the expression could not be read AND the schedule
|
|
35
|
+
// carries no zone of its own (`ScheduleTelemetry::present` falls through to
|
|
36
|
+
// `configuredTimezone()`), which is exactly the row AC-9 promises exists.
|
|
37
|
+
//
|
|
38
|
+
// A blank there is not merely ugly, it is unreachable: `meta.timezones`
|
|
39
|
+
// contributes the platform default for a schedule carrying none, and the
|
|
40
|
+
// service's `scopeToTimezone()` matches those rows when the reader narrows
|
|
41
|
+
// to that default — so a row with no zone at all would show a bare globe
|
|
42
|
+
// and then vanish from every timezone the filter offers. The same helper
|
|
43
|
+
// the trigger settings use, so one schedule cannot read two zones
|
|
44
|
+
// (`utils/schedule.ts`, and the note in `NodeInspector.vue`).
|
|
45
|
+
timezone: scheduleTimezone(block?.timezone),
|
|
46
|
+
active: row.active,
|
|
47
|
+
workflowActive: row.workflow_active,
|
|
48
|
+
nextRunAt: block?.next_fire_at ?? null,
|
|
49
|
+
nextRunLocal: firstFiring?.local ?? null,
|
|
50
|
+
// When the run STARTED, which is nullable on the platform
|
|
51
|
+
// (`workflow_runs.started_at`): a run the scheduler has created and the
|
|
52
|
+
// worker has not picked up yet carries a status and no start. So this
|
|
53
|
+
// being null does not mean "never ran" — `lastRunStatus` is what separates
|
|
54
|
+
// the two, and the page states the queued run rather than showing the
|
|
55
|
+
// dash a schedule that has never fired gets (`specs/schedules.md` AC-12).
|
|
56
|
+
lastRunAt: block?.last_run?.started_at ?? null,
|
|
57
|
+
/** Non-null exactly when the platform holds a last run at all. */
|
|
58
|
+
lastRunStatus: block?.last_run?.status ?? null,
|
|
59
|
+
lastDispatchAt: block?.last_dispatch_at ?? null,
|
|
60
|
+
holdsFirings: block?.holds_firings ?? false,
|
|
61
|
+
notFiringReason: block?.not_firing_reason ?? null
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function load(force = false) {
|
|
65
|
+
if (!await gate.claim(force)) return entries.value;
|
|
66
|
+
error.value = null;
|
|
67
|
+
try {
|
|
68
|
+
const res = await api.get("/schedules");
|
|
69
|
+
entries.value = (res?.data ?? []).map(mapEntry);
|
|
70
|
+
meta.value = res?.meta ?? { total: 0, live_count: 0, timezones: [] };
|
|
71
|
+
loaded.value = true;
|
|
72
|
+
} catch (e) {
|
|
73
|
+
error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? t("integration.schedules.error.loadFailed");
|
|
74
|
+
loaded.value = false;
|
|
75
|
+
} finally {
|
|
76
|
+
gate.release();
|
|
77
|
+
}
|
|
78
|
+
return entries.value;
|
|
79
|
+
}
|
|
80
|
+
const total = computed(() => meta.value.total);
|
|
81
|
+
const activeCount = computed(() => meta.value.live_count);
|
|
82
|
+
const timezonesInUse = computed(() => meta.value.timezones);
|
|
83
|
+
return { entries, total, activeCount, timezonesInUse, loading, loaded, error, load };
|
|
39
84
|
}
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import type { TriggerType } from '../types/integrations.js';
|
|
2
|
+
import type { ApiTriggerLite } from './useIntegrationsWorkflows.js';
|
|
2
3
|
/**
|
|
3
4
|
* Workflow triggers — a per-workflow sub-resource in the Integrations service
|
|
4
|
-
* (`/workflows/{id}/triggers`), NOT part of the workflow blob.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* (`/workflows/{id}/triggers`), NOT part of the workflow blob. `list()`, `create()`,
|
|
6
|
+
* `update()` and `remove()` address that sub-resource, one workflow at a time, which
|
|
7
|
+
* is how the editor's trigger settings work.
|
|
8
|
+
*
|
|
9
|
+
* `loadAll()` does NOT: the workflow list carries each workflow's triggers in full,
|
|
10
|
+
* so a tenant-wide view is derived from that response rather than fetched per
|
|
11
|
+
* workflow (PO-236). What it still costs is the workflow list, once.
|
|
12
|
+
*
|
|
13
|
+
* Nothing in this module reads `loadAll()` any more. The Schedules page was its
|
|
14
|
+
* one caller and now reads `GET /schedules`, which is the only source that can
|
|
15
|
+
* report when a schedule fires next (PO-231). It is kept because these
|
|
16
|
+
* composables are the module's public, auto-imported surface — a host app may
|
|
17
|
+
* hold the tenant's triggers this way, and the Webhooks view this was also
|
|
18
|
+
* written for has never been built.
|
|
7
19
|
*/
|
|
8
20
|
export interface ApiTrigger {
|
|
9
21
|
id: number;
|
|
@@ -26,6 +38,21 @@ export interface TriggerInput {
|
|
|
26
38
|
config?: Record<string, unknown> | null;
|
|
27
39
|
active?: boolean;
|
|
28
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The tenant's triggers, read off the workflow list rather than fetched per workflow
|
|
43
|
+
* (PO-236). Pure, and exported for that reason: it is the whole of what the tenant-wide
|
|
44
|
+
* view now does, and the composable around it needs a Nuxt app that the unit setup has
|
|
45
|
+
* no environment for.
|
|
46
|
+
*
|
|
47
|
+
* `byWorkflow` holds the triggers as the API sent them (`useIntegrationsWorkflows`
|
|
48
|
+
* keeps them beside the cockpit model, whose `id` is the handle). Two fields are not
|
|
49
|
+
* in there and are filled from the workflow the triggers arrived with: `workflow_id`,
|
|
50
|
+
* which every row id and route is built from, and `workflowName`, which the views show.
|
|
51
|
+
*/
|
|
52
|
+
export declare function tenantTriggers(workflows: {
|
|
53
|
+
id: number;
|
|
54
|
+
name: string;
|
|
55
|
+
}[], byWorkflow: Record<number, ApiTriggerLite[]>): TriggerWithWorkflow[];
|
|
29
56
|
export declare function useIntegrationsTriggers(): {
|
|
30
57
|
all: any;
|
|
31
58
|
loading: any;
|
|
@@ -2,6 +2,19 @@ import { useIntegrationsApi } from "./integrationsApi.js";
|
|
|
2
2
|
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
3
3
|
import { IntegrationsApiError } from "../utils/apiError.js";
|
|
4
4
|
import { useLoadGate } from "./useLoadGate.js";
|
|
5
|
+
export function tenantTriggers(workflows, byWorkflow) {
|
|
6
|
+
return workflows.flatMap((wf) => (byWorkflow[wf.id] ?? []).flatMap((t) => {
|
|
7
|
+
if (typeof t.id !== "number") return [];
|
|
8
|
+
return [{
|
|
9
|
+
...t,
|
|
10
|
+
id: t.id,
|
|
11
|
+
workflow_id: wf.id,
|
|
12
|
+
workflowName: wf.name,
|
|
13
|
+
handle: t.handle ?? "",
|
|
14
|
+
active: t.active ?? true
|
|
15
|
+
}];
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
5
18
|
export function useIntegrationsTriggers() {
|
|
6
19
|
const all = useState("integrations.triggers.all", () => []);
|
|
7
20
|
const loading = useState("integrations.triggers.loading", () => false);
|
|
@@ -9,7 +22,7 @@ export function useIntegrationsTriggers() {
|
|
|
9
22
|
const loaded = useState("integrations.triggers.loaded", () => false);
|
|
10
23
|
const api = useIntegrationsApi();
|
|
11
24
|
const t = useIntegrationsT();
|
|
12
|
-
const { workflows, load: loadWorkflows, loaded: workflowsLoaded } = useIntegrationsWorkflows();
|
|
25
|
+
const { workflows, apiTriggers, load: loadWorkflows, loaded: workflowsLoaded, error: workflowsError } = useIntegrationsWorkflows();
|
|
13
26
|
const gate = useLoadGate("integrations.triggers", { loading, loaded });
|
|
14
27
|
function unwrapList(res) {
|
|
15
28
|
if (Array.isArray(res)) return res;
|
|
@@ -22,12 +35,10 @@ export function useIntegrationsTriggers() {
|
|
|
22
35
|
if (!await gate.claim(force)) return all.value;
|
|
23
36
|
error.value = null;
|
|
24
37
|
try {
|
|
25
|
-
await loadWorkflows();
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
all.value = settled.flatMap((p) => p.status === "fulfilled" ? p.value : []);
|
|
30
|
-
loaded.value = workflowsLoaded.value && settled.every((p) => p.status === "fulfilled");
|
|
38
|
+
await loadWorkflows(force);
|
|
39
|
+
all.value = tenantTriggers(workflows.value, apiTriggers.value);
|
|
40
|
+
error.value = workflowsError.value;
|
|
41
|
+
loaded.value = workflowsLoaded.value && !workflowsError.value;
|
|
31
42
|
} catch (e) {
|
|
32
43
|
error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? t("integration.triggers.error.loadFailed");
|
|
33
44
|
} finally {
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import type { Workflow, WorkflowBlob, WorkflowExecutionMode } from '../types/integrations.js';
|
|
1
|
+
import type { TriggerType, Workflow, WorkflowBlob, WorkflowExecutionMode } from '../types/integrations.js';
|
|
2
|
+
export interface ApiTriggerLite {
|
|
3
|
+
id?: number;
|
|
4
|
+
handle?: string;
|
|
5
|
+
type: TriggerType;
|
|
6
|
+
name?: string | Record<string, string> | null;
|
|
7
|
+
config?: Record<string, unknown> | null;
|
|
8
|
+
active?: boolean;
|
|
9
|
+
}
|
|
2
10
|
export declare function useIntegrationsWorkflows(): {
|
|
3
11
|
workflows: any;
|
|
4
12
|
loading: any;
|
|
5
13
|
error: any;
|
|
6
14
|
loaded: any;
|
|
15
|
+
apiTriggers: any;
|
|
7
16
|
total: import("vue").ComputedRef<any>;
|
|
8
17
|
activeCount: import("vue").ComputedRef<any>;
|
|
9
18
|
load: (force?: boolean) => Promise<Workflow[]>;
|
|
@@ -82,6 +82,7 @@ export function useIntegrationsWorkflows() {
|
|
|
82
82
|
const loading = useState("integrations.workflows.loading", () => false);
|
|
83
83
|
const error = useState("integrations.workflows.error", () => null);
|
|
84
84
|
const loaded = useState("integrations.workflows.loaded", () => false);
|
|
85
|
+
const apiTriggers = useState("integrations.workflows.apiTriggers", () => ({}));
|
|
85
86
|
const gate = useLoadGate("integrations.workflows", { loading, loaded });
|
|
86
87
|
const api = useIntegrationsApi();
|
|
87
88
|
const t = useIntegrationsT();
|
|
@@ -92,6 +93,24 @@ export function useIntegrationsWorkflows() {
|
|
|
92
93
|
if (e instanceof IntegrationsApiError) return e.message;
|
|
93
94
|
return e?.message ?? fallback;
|
|
94
95
|
}
|
|
96
|
+
function remember(api2, into) {
|
|
97
|
+
if (api2.triggers !== void 0) into[Number(api2.id)] = api2.triggers;
|
|
98
|
+
}
|
|
99
|
+
function ingest(api2) {
|
|
100
|
+
const next = { ...apiTriggers.value };
|
|
101
|
+
remember(api2, next);
|
|
102
|
+
apiTriggers.value = next;
|
|
103
|
+
return toCockpit(api2);
|
|
104
|
+
}
|
|
105
|
+
function ingestList(list2) {
|
|
106
|
+
const next = { ...apiTriggers.value };
|
|
107
|
+
const out = list2.map((api2) => {
|
|
108
|
+
remember(api2, next);
|
|
109
|
+
return toCockpit(api2);
|
|
110
|
+
});
|
|
111
|
+
apiTriggers.value = next;
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
95
114
|
function upsertCache(wf) {
|
|
96
115
|
const idx = workflows.value.findIndex((w) => w.id === wf.id);
|
|
97
116
|
workflows.value = idx < 0 ? [...workflows.value, wf] : workflows.value.map((w) => w.id === wf.id ? wf : w);
|
|
@@ -101,10 +120,11 @@ export function useIntegrationsWorkflows() {
|
|
|
101
120
|
error.value = null;
|
|
102
121
|
try {
|
|
103
122
|
const list2 = unwrap(await api.get("/workflows")) ?? [];
|
|
104
|
-
workflows.value = list2
|
|
123
|
+
workflows.value = ingestList(list2);
|
|
105
124
|
loaded.value = true;
|
|
106
125
|
} catch (e) {
|
|
107
126
|
error.value = captureError(e, t("integration.workflows.error.loadFailed"));
|
|
127
|
+
loaded.value = false;
|
|
108
128
|
} finally {
|
|
109
129
|
gate.release();
|
|
110
130
|
}
|
|
@@ -116,7 +136,7 @@ export function useIntegrationsWorkflows() {
|
|
|
116
136
|
}
|
|
117
137
|
async function get(id) {
|
|
118
138
|
try {
|
|
119
|
-
const wf =
|
|
139
|
+
const wf = ingest(unwrap(await api.get(`/workflows/${id}`)));
|
|
120
140
|
upsertCache(wf);
|
|
121
141
|
return wf;
|
|
122
142
|
} catch {
|
|
@@ -124,7 +144,7 @@ export function useIntegrationsWorkflows() {
|
|
|
124
144
|
}
|
|
125
145
|
}
|
|
126
146
|
async function create(input) {
|
|
127
|
-
const wf =
|
|
147
|
+
const wf = ingest(unwrap(await api.post("/workflows", toPayload({
|
|
128
148
|
name: input.name,
|
|
129
149
|
description: input.description,
|
|
130
150
|
blob: input.blob,
|
|
@@ -175,7 +195,7 @@ export function useIntegrationsWorkflows() {
|
|
|
175
195
|
throw lastError instanceof Error ? lastError : new Error(t("integration.workflows.error.duplicateFailed"));
|
|
176
196
|
}
|
|
177
197
|
async function update(id, patch) {
|
|
178
|
-
const wf =
|
|
198
|
+
const wf = ingest(unwrap(await api.put(`/workflows/${id}`, toPayload({
|
|
179
199
|
name: patch.name,
|
|
180
200
|
description: patch.description,
|
|
181
201
|
blob: patch.blob,
|
|
@@ -201,6 +221,7 @@ export function useIntegrationsWorkflows() {
|
|
|
201
221
|
loading,
|
|
202
222
|
error,
|
|
203
223
|
loaded,
|
|
224
|
+
apiTriggers,
|
|
204
225
|
total,
|
|
205
226
|
activeCount,
|
|
206
227
|
load,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { computed, onScopeDispose, ref, toValue, watch } from "vue";
|
|
2
|
+
import { isVersionExecutable } from "../data/integrationNodeCatalog.js";
|
|
2
3
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
4
|
import { IntegrationsApiError } from "../utils/apiError.js";
|
|
4
5
|
export function isResolvableValue(v) {
|
|
@@ -54,7 +55,7 @@ export function useNodeConfigResolve(opts) {
|
|
|
54
55
|
});
|
|
55
56
|
const gated = computed(() => unsatisfied.value.length > 0);
|
|
56
57
|
const isEnabled = computed(
|
|
57
|
-
() => (toValue(opts.enabled) ?? true) && !gated.value && !!toValue(opts.slug) && !!toValue(opts.version)
|
|
58
|
+
() => (toValue(opts.enabled) ?? true) && isVersionExecutable(toValue(opts.slug), toValue(opts.version)) && !gated.value && !!toValue(opts.slug) && !!toValue(opts.version)
|
|
58
59
|
);
|
|
59
60
|
const subsetKey = computed(() => {
|
|
60
61
|
const cfg = toValue(opts.config);
|
|
@@ -59,6 +59,28 @@ export declare const TRIGGER_TYPE_WORDS: readonly ["manual", "schedule", "webhoo
|
|
|
59
59
|
* key itself on screen, because a missing message resolves to its own name.
|
|
60
60
|
*/
|
|
61
61
|
export declare function triggerTypeLabel(type: TriggerType | string | null | undefined): string;
|
|
62
|
+
/**
|
|
63
|
+
* Whether this exact node version can be executed — its code was built, so
|
|
64
|
+
* `execute:test` and `config:resolve` will not be refused (PO-338).
|
|
65
|
+
*
|
|
66
|
+
* Deliberately NOT `findManifest(...)?.__hasBundle !== false`: that helper falls
|
|
67
|
+
* back to the newest registered version when the pinned one is absent, so a node
|
|
68
|
+
* pinned to a withdrawn version would inherit a stranger's build state and every
|
|
69
|
+
* surface would then say something about a version the catalogue does not have.
|
|
70
|
+
* Only an exact match may answer, and anything else — unknown version, catalogue
|
|
71
|
+
* not loaded yet, a host whose API does not report the field — reads as
|
|
72
|
+
* executable, because the alternative is a studio that hides working nodes.
|
|
73
|
+
*/
|
|
74
|
+
export declare function isVersionExecutable(slug: string, version: string): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* The newest version of this node that can be executed, or `null` when none can.
|
|
77
|
+
*
|
|
78
|
+
* What a surface tells the operator to move to. The catalogue is semver-descending
|
|
79
|
+
* within a slug, so the first executable entry is the newest one. `null` where the
|
|
80
|
+
* whole node is unusable — then there is nothing to point at and the surface says
|
|
81
|
+
* only what is wrong.
|
|
82
|
+
*/
|
|
83
|
+
export declare function newestExecutableVersion(slug: string): string | null;
|
|
62
84
|
export declare function findManifest(slug: string, version?: string): NodeManifest | null;
|
|
63
85
|
export declare const TRIGGER_OUTPUT_PORT = "default";
|
|
64
86
|
export interface TriggerTemplate {
|
|
@@ -75,7 +97,6 @@ export interface TriggerTemplate {
|
|
|
75
97
|
en: string;
|
|
76
98
|
de: string;
|
|
77
99
|
};
|
|
78
|
-
icon: string;
|
|
79
100
|
/** Default config the canvas seeds when the user drops the tile. */
|
|
80
101
|
defaultConfig: Record<string, unknown>;
|
|
81
102
|
}
|
|
@@ -26,6 +26,14 @@ export function triggerTypeLabel(type) {
|
|
|
26
26
|
if (!TRIGGER_TYPE_WORD_SET.has(kind)) return kind;
|
|
27
27
|
return useIntegrationsT()(`integration.common.triggerType.${kind}`);
|
|
28
28
|
}
|
|
29
|
+
export function isVersionExecutable(slug, version) {
|
|
30
|
+
const exact = INTEGRATION_NODE_CATALOG.find((m) => m.slug === slug && m.version === version);
|
|
31
|
+
return exact ? exact.__hasBundle !== false : true;
|
|
32
|
+
}
|
|
33
|
+
export function newestExecutableVersion(slug) {
|
|
34
|
+
const hit = INTEGRATION_NODE_CATALOG.find((m) => m.slug === slug && m.__hasBundle !== false);
|
|
35
|
+
return hit?.version ?? null;
|
|
36
|
+
}
|
|
29
37
|
export function findManifest(slug, version) {
|
|
30
38
|
const matches = INTEGRATION_NODE_CATALOG.filter((m) => m.slug === slug);
|
|
31
39
|
if (!matches.length) return null;
|
|
@@ -42,7 +50,6 @@ export const TRIGGER_TEMPLATES = [
|
|
|
42
50
|
en: "Start the workflow yourself with the Run button or an API call.",
|
|
43
51
|
de: "Starte den Workflow selbst \xFCber den Run-Button oder einen API-Aufruf."
|
|
44
52
|
},
|
|
45
|
-
icon: "Play",
|
|
46
53
|
defaultConfig: {}
|
|
47
54
|
},
|
|
48
55
|
{
|
|
@@ -53,7 +60,6 @@ export const TRIGGER_TEMPLATES = [
|
|
|
53
60
|
en: "Runs automatically on a schedule you define, for example every night.",
|
|
54
61
|
de: "L\xE4uft automatisch nach deinem Zeitplan, zum Beispiel jede Nacht."
|
|
55
62
|
},
|
|
56
|
-
icon: "Clock",
|
|
57
63
|
defaultConfig: { cron: "0 * * * *", timezone: "Europe/Berlin" }
|
|
58
64
|
},
|
|
59
65
|
{
|
|
@@ -64,7 +70,6 @@ export const TRIGGER_TEMPLATES = [
|
|
|
64
70
|
en: "Starts when an external system calls this workflow's URL.",
|
|
65
71
|
de: "Startet, wenn ein externes System die URL dieses Workflows aufruft."
|
|
66
72
|
},
|
|
67
|
-
icon: "Webhook",
|
|
68
73
|
defaultConfig: { path: "/incoming", method: "POST" }
|
|
69
74
|
},
|
|
70
75
|
{
|
|
@@ -75,7 +80,6 @@ export const TRIGGER_TEMPLATES = [
|
|
|
75
80
|
en: "Starts when something happens on the platform, for example a new customer.",
|
|
76
81
|
de: "Startet, wenn auf der Plattform etwas passiert, zum Beispiel ein neuer Kunde."
|
|
77
82
|
},
|
|
78
|
-
icon: "Zap",
|
|
79
83
|
defaultConfig: { event_type: "" }
|
|
80
84
|
}
|
|
81
85
|
];
|