@revenexx/studio-integrations 0.5.1 → 0.7.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.
Files changed (171) hide show
  1. package/CHANGELOG.md +428 -0
  2. package/README.md +48 -4
  3. package/dist/module.d.mts +17 -1
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +10 -2
  6. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +43 -41
  7. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +123 -50
  8. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +108 -71
  9. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +12 -15
  10. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
  11. package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
  12. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +7 -4
  13. package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
  14. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
  15. package/dist/runtime/components/Integrations/ExpressionInput.vue +67 -37
  16. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
  17. package/dist/runtime/components/Integrations/GroupFrameView.vue +30 -20
  18. package/dist/runtime/components/Integrations/Layout.vue +48 -4
  19. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +23 -23
  20. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +11 -13
  21. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
  22. package/dist/runtime/components/Integrations/NodeConfigField.vue +187 -93
  23. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
  24. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
  25. package/dist/runtime/components/Integrations/NodeInspector.vue +1157 -376
  26. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
  27. package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
  28. package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
  29. package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
  30. package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
  31. package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
  32. package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
  33. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
  34. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +43 -0
  35. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
  36. package/dist/runtime/components/Integrations/NodePalette.vue +122 -55
  37. package/dist/runtime/components/Integrations/QueryBuilder.vue +46 -38
  38. package/dist/runtime/components/Integrations/RunActionsMenu.vue +38 -12
  39. package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
  40. package/dist/runtime/components/Integrations/RunPayloadInput.vue +27 -13
  41. package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
  42. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
  43. package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
  44. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
  45. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
  46. package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
  47. package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
  48. package/dist/runtime/components/Integrations/StickyNoteView.vue +38 -23
  49. package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
  50. package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
  51. package/dist/runtime/components/Integrations/TemplateCard.vue +119 -0
  52. package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
  53. package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
  54. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +16 -18
  55. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
  56. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
  57. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +52 -67
  58. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
  59. package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
  60. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +29 -11
  61. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
  62. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +20 -1
  63. package/dist/runtime/components/Integrations/WorkflowEditor.vue +240 -116
  64. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +20 -1
  65. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +28 -43
  66. package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
  67. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +17 -7
  68. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
  69. package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
  70. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +198 -56
  71. package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
  72. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +56 -59
  73. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +28 -21
  74. package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
  75. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +15 -9
  76. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
  77. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -10
  78. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +50 -36
  79. package/dist/runtime/components/Integrations/WorkflowsTable.vue +126 -48
  80. package/dist/runtime/composables/integrationsApi.js +4 -2
  81. package/dist/runtime/composables/useCredentialTypes.js +8 -6
  82. package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
  83. package/dist/runtime/composables/useIntegrationsCredentials.js +10 -8
  84. package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
  85. package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
  86. package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
  87. package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
  88. package/dist/runtime/composables/useIntegrationsNodes.js +7 -5
  89. package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
  90. package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
  91. package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
  92. package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
  93. package/dist/runtime/composables/useIntegrationsRuns.js +14 -7
  94. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
  95. package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
  96. package/dist/runtime/composables/useIntegrationsSecrets.js +14 -9
  97. package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
  98. package/dist/runtime/composables/useIntegrationsT.js +9 -0
  99. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
  100. package/dist/runtime/composables/useIntegrationsTemplates.js +28 -16
  101. package/dist/runtime/composables/useIntegrationsTriggers.js +10 -8
  102. package/dist/runtime/composables/useIntegrationsWorkflows.js +13 -7
  103. package/dist/runtime/composables/useLoadGate.d.ts +17 -0
  104. package/dist/runtime/composables/useLoadGate.js +8 -0
  105. package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
  106. package/dist/runtime/data/branchReorder.d.ts +37 -0
  107. package/dist/runtime/data/branchReorder.js +71 -0
  108. package/dist/runtime/data/handleId.d.ts +23 -0
  109. package/dist/runtime/data/handleId.js +6 -0
  110. package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
  111. package/dist/runtime/data/integrationNodeCatalog.js +14 -0
  112. package/dist/runtime/data/integrationTemplates.d.ts +48 -0
  113. package/dist/runtime/data/integrationTemplates.js +21 -0
  114. package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
  115. package/dist/runtime/data/nodeIcons.d.ts +2 -0
  116. package/dist/runtime/data/nodeIcons.js +73 -0
  117. package/dist/runtime/data/nodeInputData.d.ts +16 -4
  118. package/dist/runtime/data/nodeInputData.js +4 -0
  119. package/dist/runtime/data/payloadForm.d.ts +9 -2
  120. package/dist/runtime/data/payloadForm.js +29 -4
  121. package/dist/runtime/data/portTone.d.ts +17 -0
  122. package/dist/runtime/data/portTone.js +7 -0
  123. package/dist/runtime/data/webhookAuth.d.ts +18 -0
  124. package/dist/runtime/data/webhookAuth.js +19 -0
  125. package/dist/runtime/data/workflowIconCatalog.js +4 -6
  126. package/dist/runtime/i18n/messages/de.json +1207 -0
  127. package/dist/runtime/i18n/messages/en.json +1207 -0
  128. package/dist/runtime/i18n/translate.d.ts +83 -0
  129. package/dist/runtime/i18n/translate.js +41 -0
  130. package/dist/runtime/pages/integrations/audit.vue +140 -37
  131. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +9 -6
  132. package/dist/runtime/pages/integrations/credentials.vue +180 -64
  133. package/dist/runtime/pages/integrations/index.vue +91 -92
  134. package/dist/runtime/pages/integrations/runs.vue +33 -21
  135. package/dist/runtime/pages/integrations/schedules.vue +88 -23
  136. package/dist/runtime/pages/integrations/secrets.vue +57 -35
  137. package/dist/runtime/pages/integrations/templates/[slug].vue +119 -88
  138. package/dist/runtime/pages/integrations/templates/index.vue +82 -103
  139. package/dist/runtime/pages/integrations/workflows/[id].vue +344 -91
  140. package/dist/runtime/pages/integrations/workflows/index.vue +23 -5
  141. package/dist/runtime/plugins/locale.d.ts +10 -0
  142. package/dist/runtime/plugins/locale.js +6 -0
  143. package/dist/runtime/plugins/realtime.client.d.ts +32 -0
  144. package/dist/runtime/plugins/realtime.client.js +4 -0
  145. package/dist/runtime/types/integrations.d.ts +65 -6
  146. package/dist/runtime/utils/apiError.d.ts +47 -0
  147. package/dist/runtime/utils/apiError.js +32 -0
  148. package/dist/runtime/utils/cockpitSources.d.ts +153 -0
  149. package/dist/runtime/utils/cockpitSources.js +14 -0
  150. package/dist/runtime/utils/credentialState.d.ts +24 -1
  151. package/dist/runtime/utils/credentialState.js +26 -11
  152. package/dist/runtime/utils/loadGate.d.ts +61 -0
  153. package/dist/runtime/utils/loadGate.js +22 -0
  154. package/dist/runtime/utils/localize.d.ts +46 -4
  155. package/dist/runtime/utils/localize.js +34 -7
  156. package/dist/runtime/utils/notify.d.ts +59 -0
  157. package/dist/runtime/utils/notify.js +21 -0
  158. package/dist/runtime/utils/pollBudget.d.ts +72 -0
  159. package/dist/runtime/utils/pollBudget.js +17 -0
  160. package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
  161. package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
  162. package/dist/runtime/utils/schedule.d.ts +116 -0
  163. package/dist/runtime/utils/schedule.js +219 -0
  164. package/dist/runtime/utils/statusStyle.d.ts +26 -2
  165. package/dist/runtime/utils/statusStyle.js +9 -1
  166. package/dist/runtime/utils/validationFindings.d.ts +101 -0
  167. package/dist/runtime/utils/validationFindings.js +54 -0
  168. package/dist/runtime/utils/whenIdle.d.ts +42 -0
  169. package/dist/runtime/utils/whenIdle.js +12 -0
  170. package/dist/types.d.mts +2 -6
  171. package/package.json +10 -3
@@ -3,29 +3,47 @@ import { AddCircle as Plus } from "#solar-icons";
3
3
  import { Button } from "@revenexx/studio";
4
4
  import WorkflowsTable from "../../../components/Integrations/WorkflowsTable.vue";
5
5
  import WorkflowCreateDialog from "../../../components/Integrations/WorkflowCreateDialog.vue";
6
- useHead({ title: "Workflows \xB7 Revenexx Cockpit" });
7
- const { workflows, loading, load } = useIntegrationsWorkflows();
6
+ const t = useIntegrationsT();
7
+ useHead({ title: () => t("integration.workflows.list.head.title") });
8
+ const { workflows, loading, error, load } = useIntegrationsWorkflows();
8
9
  onMounted(() => {
9
10
  void load();
10
11
  });
12
+ const { buildTick } = useIntegrationsRealtime();
13
+ watch(buildTick, () => {
14
+ void load(true);
15
+ });
16
+ const loadFailed = computed(() => !!error && !!error.value && !workflows.value.length);
11
17
  const createOpen = ref(false);
12
18
  </script>
13
19
 
14
20
  <template>
15
21
  <IntegrationsLayout>
16
22
  <PageHeader
17
- title="Workflows"
23
+ :title="t('integration.workflows.list.header.title')"
18
24
  >
19
25
  <template #actions>
20
26
  <Button class="h-7 text-sm" @click="createOpen = true">
21
27
  <Plus :size="14" />
22
- New workflow
28
+ {{ t("integration.workflows.list.action.new") }}
23
29
  </Button>
24
30
  </template>
25
31
  </PageHeader>
26
32
 
27
33
  <div class="p-4">
28
- <WorkflowsTable :workflows="workflows" :loading="loading" />
34
+ <StateCard
35
+ v-if="loadFailed"
36
+ variant="error"
37
+ :title="t('integration.workflows.list.error.title')"
38
+ :description="error ?? void 0"
39
+ >
40
+ <template #actions>
41
+ <Button variant="outline" class="h-8 text-sm" @click="load(true)">
42
+ {{ t("integration.common.action.retry") }}
43
+ </Button>
44
+ </template>
45
+ </StateCard>
46
+ <WorkflowsTable v-else :workflows="workflows" :loading="loading" />
29
47
  </div>
30
48
 
31
49
  <WorkflowCreateDialog v-model:open="createOpen" />
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Apply the language a host configured statically (`studioIntegrations.locale`,
3
+ * or `NUXT_PUBLIC_STUDIO_INTEGRATIONS_LOCALE`) before anything renders.
4
+ *
5
+ * It runs ahead of the host's own plugins — module plugins are registered
6
+ * first — so a host that switches the language at runtime is applying its
7
+ * choice after this one, never being overwritten by it.
8
+ */
9
+ declare const _default: any;
10
+ export default _default;
@@ -0,0 +1,6 @@
1
+ import { setStudioLocale } from "../utils/localize.js";
2
+ import { defineNuxtPlugin, useRuntimeConfig } from "#app";
3
+ export default defineNuxtPlugin(() => {
4
+ const configured = useRuntimeConfig().public.studioIntegrations?.locale;
5
+ if (configured) setStudioLocale(configured);
6
+ });
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Starts realtime once per tab, client-only.
3
+ *
4
+ * TWO THINGS HAPPEN HERE AND NEITHER HAPPENS ANYWHERE ELSE.
5
+ *
6
+ * `usePlatformTalkback()` is what BUILDS the Talkback client: it imports the
7
+ * package, mints a connection token and opens the socket on its first call, then
8
+ * registers the client in studio-shared's registry. The channel composables only
9
+ * READ that registry — so without this call `useTalkbackTopic()` would find no
10
+ * client, subscribe to nothing and report a null channel, with no error anywhere.
11
+ * A plugin rather than a call inside the composable because building the client has
12
+ * side effects (a dynamic import, a WebSocket, a token mint) and a composable a
13
+ * component may reach during render is the wrong place for those.
14
+ *
15
+ * `useIntegrationsRealtime()` then takes the subscriptions. Warming it here rather
16
+ * than leaving it to the first surface that asks means an event arriving before the
17
+ * runs page is opened still moves the signals, and it puts the detached scope's
18
+ * creation at a defined point in the app's life instead of inside whichever
19
+ * component rendered first.
20
+ *
21
+ * BOTH ARE IDEMPOTENT AND BOTH ARE FREE WHEN REALTIME IS OFF. An empty
22
+ * `talkbackHost` makes `usePlatformTalkback()` return without building anything —
23
+ * neither `@revenexx/talkback-js` nor `centrifuge` is imported — and this plugin
24
+ * costs two function calls.
25
+ *
26
+ * The bare names resolve per application, so a host that overrides
27
+ * `usePlatformTalkback` (the standalone `studio-ui` does, because its API dev token
28
+ * is not one the Talkback facade will accept) gets its own client here, and a host
29
+ * that does not gets studio-shared's default.
30
+ */
31
+ declare const _default: any;
32
+ export default _default;
@@ -0,0 +1,4 @@
1
+ export default defineNuxtPlugin(() => {
2
+ usePlatformTalkback();
3
+ useIntegrationsRealtime();
4
+ });
@@ -11,8 +11,19 @@ export type LocalizedString = string | Record<string, string>;
11
11
  export type WorkflowVariableType = 'string' | 'number' | 'boolean' | 'object' | 'array';
12
12
  export interface WorkflowVariable {
13
13
  type: WorkflowVariableType;
14
+ /** The value the workflow runs with — the studio labels it "Value". */
14
15
  default?: unknown;
15
16
  description?: LocalizedString;
17
+ /**
18
+ * @deprecated No effect anywhere: the engine resolves `default` regardless,
19
+ * and nothing validates it. Not editable in the studio any more, but kept in
20
+ * the type because existing blobs still carry the field and must round-trip.
21
+ *
22
+ * That round-trip does not rest on the dialog being careful: the save path
23
+ * spreads the stored `variables` object verbatim (`useIntegrationsWorkflows.ts`,
24
+ * no per-field mapping), so nothing in the studio is in a position to drop the
25
+ * field. Change that spread and this guarantee is what you are changing.
26
+ */
16
27
  required?: boolean;
17
28
  }
18
29
  export type TriggerType = 'manual' | 'schedule' | 'webhook' | 'event';
@@ -27,10 +38,35 @@ export interface WorkflowTrigger {
27
38
  timezone?: string;
28
39
  path?: string;
29
40
  method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
41
+ /** @deprecated superseded by authProfile/authConfig — was never enforced */
30
42
  secretRef?: string;
43
+ /** Ingress verification profile for the public endpoint. */
44
+ authProfile?: 'none' | 'token' | 'basic' | 'hmac-generic' | 'stripe' | 'standard-webhooks';
45
+ authConfig?: {
46
+ header?: string;
47
+ prefix?: string;
48
+ username?: string;
49
+ algorithm?: string;
50
+ encoding?: string;
51
+ mode?: string;
52
+ };
53
+ public?: boolean;
54
+ /** Server-owned: the provisioned ingress endpoint. */
55
+ inbound?: {
56
+ endpoint_id?: string;
57
+ url?: string;
58
+ };
31
59
  event_type?: string;
60
+ payloadFields?: DeclaredPayloadField[];
32
61
  };
33
62
  }
63
+ /** A payload field the workflow author declares on a manual trigger. */
64
+ export interface DeclaredPayloadField {
65
+ /** Dot-path below `trigger.payload` (e.g. `orderId`, `import.file`). */
66
+ path: string;
67
+ label?: string;
68
+ kind?: 'text' | 'file';
69
+ }
34
70
  export type RetryStrategy = 'linear' | 'exponential';
35
71
  export interface WorkflowNodeRetry {
36
72
  maxAttempts: number;
@@ -151,6 +187,17 @@ export type WorkflowExecutionMode = 'async_only' | 'sync_only' | 'caller_decides
151
187
  * bundle is `ready`; the API rejects runs with 409 in any other state.
152
188
  */
153
189
  export type WorkflowBuildStatus = 'pending' | 'building' | 'ready' | 'failed';
190
+ /**
191
+ * A non-blocking manifest cross-validation finding returned on the create /
192
+ * update / show responses (never on the *list* response — that path skips the
193
+ * per-node DB resolution). `path` is keyed into the blob (`blob.nodes.N.config.KEY`),
194
+ * matching the `errors` keys of the 422 an active save is rejected with. See
195
+ * `docs/workflow-validation.md` (PO-186 port; behavior: specs/workflow-validation.md).
196
+ */
197
+ export interface WorkflowWarning {
198
+ path: string;
199
+ message: string;
200
+ }
154
201
  export interface Workflow {
155
202
  id: number;
156
203
  name: string;
@@ -164,6 +211,13 @@ export interface Workflow {
164
211
  buildStatus: WorkflowBuildStatus | null;
165
212
  /** Human-readable build failure reason, present when `buildStatus === 'failed'`. */
166
213
  buildError: string | null;
214
+ /**
215
+ * Non-blocking manifest validation findings for the persisted blob. Present on
216
+ * show/create/update, **absent (undefined) on the list response** — so `undefined`
217
+ * means "unknown", not "valid". Callers must fail open: only gate activation when
218
+ * this is a non-empty array. See [[WorkflowWarning]].
219
+ */
220
+ warnings?: WorkflowWarning[];
167
221
  created_at: string;
168
222
  updated_at: string;
169
223
  }
@@ -219,8 +273,7 @@ export interface ConfigField {
219
273
  * (e.g. 'revenexx:business-central'). An array lets a node accept more than
220
274
  * one credential type; the picker merges the tenant's credentials across all. */
221
275
  credentialType?: string | string[];
222
- /** keys of other config fields this field's options/schema depend on. The
223
- * driving `select` fields also form the node's canvas subtitle. */
276
+ /** keys of other config fields this field's options/schema depend on. */
224
277
  dependsOn?: string[];
225
278
  validation?: {
226
279
  pattern?: string;
@@ -266,6 +319,8 @@ export interface NodeManifest {
266
319
  category?: string;
267
320
  }>;
268
321
  input: NodeManifestInput;
322
+ /** Empty marks a terminal node (PO-201): the run ends there, so the canvas
323
+ * draws no output handle and no edge may leave it. */
269
324
  outputs: NodeManifestOutput[];
270
325
  config?: ConfigField[];
271
326
  /** Source package (editor-side enrichment from GET /nodes; not manifest data). */
@@ -379,7 +434,8 @@ export interface ScheduleEntry {
379
434
  workflowId: number;
380
435
  workflowName: string;
381
436
  triggerId: string;
382
- cron: string;
437
+ /** `null` when the trigger carries no expression at all — never invented. */
438
+ cron: string | null;
383
439
  timezone: string;
384
440
  active: boolean;
385
441
  nextRunAt: string | null;
@@ -409,7 +465,12 @@ export interface WorkflowTemplate {
409
465
  name: string;
410
466
  label: string | null;
411
467
  } | null;
412
- category: TemplateCategory;
468
+ /**
469
+ * `''` when the registry published none. The studio does not pick a category
470
+ * on a publisher's behalf: a chip reading "Sync" over a template nobody filed
471
+ * under `sync` is a name invented by the reader's side of the wire.
472
+ */
473
+ category: TemplateCategory | '';
413
474
  industries: Array<'mechanical-engineering' | 'chemicals' | 'wholesale' | 'manufacturing' | 'retail' | 'any'>;
414
475
  /** Vendor slugs this template wires together (used to compute prerequisites). */
415
476
  vendors: string[];
@@ -421,8 +482,6 @@ export interface WorkflowTemplate {
421
482
  outline: string[];
422
483
  /** Number of node instances in the canonical blob. */
423
484
  nodeCount: number;
424
- /** Number of installs across the platform (used for "popular" ranking). */
425
- installs: number;
426
485
  /** Author identity. */
427
486
  publisher: {
428
487
  name: string;
@@ -15,3 +15,50 @@ export declare class IntegrationsApiError extends Error {
15
15
  body: unknown;
16
16
  constructor(status: number, body: unknown, message: string);
17
17
  }
18
+ /**
19
+ * Manifest validation issues parsed out of a 422 rejection body.
20
+ *
21
+ * Saving a workflow with `active: true` re-runs the DB-aware manifest
22
+ * cross-validation as a hard block: on any issue the backend responds `422`
23
+ * with Laravel-style `errors` keyed by the blob path
24
+ * (`{ "blob.nodes.0.config.credentials": ["Required config field ..."] }`).
25
+ * This flattens that map into `{ path, message }[]` — the same shape as the
26
+ * non-blocking `warnings` on a show/update response.
27
+ *
28
+ * Keys are filtered to the blob keyspace only: `blob` itself and any
29
+ * `blob.*` child, but **not** the sibling `blob_definition_version` (a
30
+ * `startsWith('blob')` would wrongly swallow it). A non-blob-keyed 422 (e.g.
31
+ * a duplicate-name validation error) yields `[]`, so callers can tell an
32
+ * activation-blocked 422 apart from an unrelated one.
33
+ */
34
+ export declare function apiValidationIssues(err: unknown): {
35
+ path: string;
36
+ message: string;
37
+ }[];
38
+ /**
39
+ * Field-keyed validation messages from a rejection, as `{ field: message }`.
40
+ *
41
+ * The counterpart to `apiValidationIssues` above, for the ordinary case that has
42
+ * nothing to do with the workflow blob: a form field the service refused, named the
43
+ * way the form names it. Laravel answers a `422` with `errors` keyed by field and a
44
+ * list of messages per key; the first message is the one a person reads, so that is
45
+ * the one kept.
46
+ *
47
+ * A form uses this to put the refusal where the operator is looking — "The name has
48
+ * already been taken." under the Name field, rather than as a sentence somewhere
49
+ * else about a field they then have to find (PO-275). Keys that address the blob are
50
+ * left out: those belong to `apiValidationIssues`, which resolves them to nodes.
51
+ */
52
+ export declare function apiFieldErrors(err: unknown): Record<string, string>;
53
+ /**
54
+ * The reason a rejection gives, without the count of everything else it found.
55
+ *
56
+ * The service leads with one message and appends how many more there were — "The name
57
+ * has already been taken. (and 2 more errors)". That count is honest arithmetic and
58
+ * misleading copy: it counts by validation rule, so a missing field the service checks
59
+ * twice (`config`, then `config.authKey`) is two of them, while the operator can see
60
+ * one — and it counts things no form draws. A surface that shows every field-keyed
61
+ * message at its field does not need the number at all; what it needs is the reason
62
+ * (PO-275, reported from use).
63
+ */
64
+ export declare function refusalReason(err: unknown, fallback: string): string;
@@ -6,3 +6,35 @@ export class IntegrationsApiError extends Error {
6
6
  this.name = "IntegrationsApiError";
7
7
  }
8
8
  }
9
+ export function apiValidationIssues(err) {
10
+ if (!(err instanceof IntegrationsApiError) || err.status !== 422) return [];
11
+ const errors = err.body?.errors;
12
+ if (!errors || typeof errors !== "object") return [];
13
+ const issues = [];
14
+ for (const [key, val] of Object.entries(errors)) {
15
+ if (key !== "blob" && !key.startsWith("blob.")) continue;
16
+ const messages = Array.isArray(val) ? val : [val];
17
+ for (const m of messages) issues.push({ path: key, message: String(m) });
18
+ }
19
+ return issues;
20
+ }
21
+ export function apiFieldErrors(err) {
22
+ if (!(err instanceof IntegrationsApiError) || err.status !== 422) return {};
23
+ const errors = err.body?.errors;
24
+ if (!errors || typeof errors !== "object") return {};
25
+ const out = {};
26
+ for (const [key, val] of Object.entries(errors)) {
27
+ if (key === "blob" || key.startsWith("blob.")) continue;
28
+ const first = Array.isArray(val) ? val[0] : val;
29
+ if (first !== void 0 && first !== null) out[key] = String(first);
30
+ }
31
+ for (const key of Object.keys(out)) {
32
+ if (Object.keys(out).some((other) => other.startsWith(`${key}.`))) delete out[key];
33
+ }
34
+ return out;
35
+ }
36
+ export function refusalReason(err, fallback) {
37
+ const raw = err?.message?.trim();
38
+ if (!raw) return fallback;
39
+ return raw.replace(/\s*\(and \d+ more errors?\)\s*$/, "").trim() || fallback;
40
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * What the module expects a **cockpit** host to provide, and how it asks.
3
+ *
4
+ * The three managed credential types (`revenexx:sftp`, `revenexx:smtp`,
5
+ * `revenexx:api`) offer a "Use revenexx …" alternative to typing access data by
6
+ * hand: the operator picks an SFTP account, a mail credential or a fresh API key
7
+ * that already exists elsewhere in the cockpit, and the studio copies its access
8
+ * data into the credential. That is cockpit-coupled by design.
9
+ *
10
+ * The coupling is five composables the cockpit auto-imports. They are not part of
11
+ * this module and there is no import to write for them — in the cockpit they are
12
+ * simply in scope, and in any other host they are not there at all. Referencing
13
+ * one where it does not exist throws a `ReferenceError`, and thrown from a setup
14
+ * it takes the whole surrounding form down: before PO-222 that left both credential
15
+ * dialogs with no fields at all for those three types, so a credential of one could
16
+ * neither be created nor edited outside the cockpit — including in this
17
+ * repository's own documented dev loop, which serves exactly those three.
18
+ *
19
+ * **`typeof` on a bare identifier is the only safe way to ask.** It is the one
20
+ * expression that does not throw on a name that was never declared. It is
21
+ * deliberately not `globalThis.useConsoleTenants`: an auto-import is injected at
22
+ * build time as a module binding, not hung off the global object, so a
23
+ * `globalThis` lookup answers "absent" **in the cockpit too** — which would
24
+ * silently switch the feature off in the one host it exists for. That is the
25
+ * failure this file is most at risk of, so it is written down rather than left to
26
+ * be rediscovered.
27
+ *
28
+ * **The ambient declarations below must stay `declare const`, never `declare
29
+ * function`.** They are the same failure arriving by a second door: unimport
30
+ * decides what to inject by scanning the source for identifiers that are not
31
+ * already declared in it, and one of its exclusion patterns is
32
+ * `/\bfunction\s*([\w$]+)\s*\(/` — so a `declare function useConsoleSftp()` reads
33
+ * as "this file already provides it" and the injection is skipped. In the cockpit
34
+ * the name is then genuinely absent, `typeof` answers `'undefined'`, and the panel
35
+ * disables itself in the one host it works in. `declare const useConsoleSftp:
36
+ * (() => …) | undefined` is not excluded, because the pattern for a variable
37
+ * captures up to `=`, `;` or the line end and the captured text (`useConsoleSftp:
38
+ * (() => …) | undefined`) is not the bare identifier.
39
+ *
40
+ * That is measured, not read off the regex: unimport's own `injectImports`, run
41
+ * over this file, injects all five — and over the same file with the declarations
42
+ * rewritten as `declare function`, injects nothing. Repeat it that way if the
43
+ * spelling ever has to change. Nothing stands guard over it automatically, because
44
+ * a test would mean this repository taking a dependency on a build tool it does
45
+ * not otherwise use, to assert that tool's behaviour rather than its own.
46
+ */
47
+ /**
48
+ * The shapes this module uses.
49
+ *
50
+ * Narrowed to the members it reads — but narrowed against the cockpit's own
51
+ * source (`app/composables/*.ts`, `app/types/consoleSftp.ts`), not guessed. A
52
+ * declaration that omits a member the code below reads is not a typing nit: it is
53
+ * this file claiming a contract the caller then violates, and nothing in CI
54
+ * typechecks these files today, so the claim has to be right by construction.
55
+ *
56
+ * Two members exist here only because the code reads them off values the cockpit
57
+ * may hand back as `null`: `getCredential` and `createCredential` both resolve to
58
+ * `MailCredential | null`, and the callers have to say what they do about it.
59
+ */
60
+ export interface CockpitSources {
61
+ tenants: {
62
+ /** `ConsoleTenant[]` — the picker renders `name` and keys by `slug`. */
63
+ tenants: {
64
+ value: {
65
+ slug: string;
66
+ name: string;
67
+ }[];
68
+ };
69
+ onlyTenantSlug: {
70
+ value: string | null;
71
+ };
72
+ load: () => Promise<unknown>;
73
+ };
74
+ market: {
75
+ markets: {
76
+ value: {
77
+ code: string;
78
+ name: string;
79
+ }[];
80
+ };
81
+ activeCode: {
82
+ value: string;
83
+ };
84
+ GLOBAL_MARKET_CODE: string;
85
+ fetchMarkets: () => Promise<unknown>;
86
+ };
87
+ sftp: {
88
+ /**
89
+ * `ConsoleSftpAccount[]`. Provisioning is asynchronous, so a row carries both
90
+ * the flag and the state machine — `create()` hands back a `pending` row and
91
+ * the caller polls `refresh()` until one of them says active.
92
+ */
93
+ accounts: {
94
+ value: CockpitSftpAccount[];
95
+ };
96
+ load: (tenant: string | null) => Promise<unknown>;
97
+ /** `null` while the account is still provisioning (the Console answers 404). */
98
+ revealPassword: (tenant: string, id: number) => Promise<{
99
+ username: string;
100
+ password: string;
101
+ server?: string;
102
+ } | null>;
103
+ create: (tenant: string, name?: string) => Promise<CockpitSftpAccount>;
104
+ refresh: (tenant: string, id: number) => Promise<CockpitSftpAccount>;
105
+ };
106
+ mail: {
107
+ listCredentials: () => Promise<CockpitMailCredential[]>;
108
+ getCredential: (uuid: string) => Promise<CockpitMailCredential | null>;
109
+ createCredential: (input: {
110
+ name: string;
111
+ type: 'SMTP' | 'API';
112
+ }) => Promise<CockpitMailCredential | null>;
113
+ };
114
+ keys: {
115
+ create: (name: string, scopes: string[]) => Promise<{
116
+ secret: string;
117
+ }>;
118
+ };
119
+ }
120
+ /** The members of the cockpit's `ConsoleSftpAccount` this module reads. */
121
+ export interface CockpitSftpAccount {
122
+ id: number;
123
+ /** The gateway login. `null` until the account finished provisioning. */
124
+ login_username: string | null;
125
+ status: string;
126
+ is_active: boolean;
127
+ error_message: string | null;
128
+ }
129
+ /**
130
+ * The members of the cockpit's `MailCredential` this module reads.
131
+ *
132
+ * The SMTP pair is optional on the wire: the mail engine returns it on `show` and
133
+ * on create, but the type does not promise it, which is why every read of it here
134
+ * falls back to an empty string rather than assuming.
135
+ */
136
+ export interface CockpitMailCredential {
137
+ uuid: string;
138
+ name: string;
139
+ type?: string;
140
+ smtp_username?: string;
141
+ smtp_password?: string;
142
+ }
143
+ /**
144
+ * The cockpit's sources, or `null` in a host that does not have them.
145
+ *
146
+ * All five or none: a host with three of them is not a cockpit, and half a picker
147
+ * is worse than none — the operator would meet a control that lists nothing and
148
+ * says nothing about why. `null` is what the caller turns into "the managed
149
+ * sources are unavailable here", with the manual fields left in place.
150
+ *
151
+ * Must be called from a component's setup, like the composables it calls.
152
+ */
153
+ export declare function cockpitSources(): CockpitSources | null;
@@ -0,0 +1,14 @@
1
+ export function cockpitSources() {
2
+ if (typeof useConsoleTenants !== "function") return null;
3
+ if (typeof useActiveMarket !== "function") return null;
4
+ if (typeof useConsoleSftp !== "function") return null;
5
+ if (typeof useMailAdmin !== "function") return null;
6
+ if (typeof useConsoleApiKeys !== "function") return null;
7
+ return {
8
+ tenants: useConsoleTenants(),
9
+ market: useActiveMarket(),
10
+ sftp: useConsoleSftp(),
11
+ mail: useMailAdmin(),
12
+ keys: useConsoleApiKeys()
13
+ };
14
+ }
@@ -22,7 +22,30 @@ export interface CredentialStateInfo {
22
22
  at?: string | null;
23
23
  }
24
24
  type TestableCredential = Pick<Credential, 'status' | 'last_test_ok' | 'last_test_at' | 'last_test_message'>;
25
+ /** Every state a credential can be in, in the order a reader is offered them. */
26
+ export declare const CREDENTIAL_STATES: readonly ["connected", "failed", "needs_reauth", "untested"];
27
+ /**
28
+ * What a state is called for the reader, keyed rather than derived from a
29
+ * credential.
30
+ *
31
+ * `credentialState()` below answers "what state is this credential in, and what
32
+ * is it called"; this answers the second half on its own, for the list's filter,
33
+ * which offers all four states whether or not a credential is in them. They read
34
+ * the same map, so the badge on a row and the option that finds it cannot come to
35
+ * disagree — they did, briefly, as two copies of the same four `t()` calls.
36
+ */
37
+ export declare function credentialStateLabel(key: CredentialConnState): string;
38
+ /**
39
+ * The `label` is read at call time rather than held in a map beside `TONE`: a
40
+ * map built once would keep the language it was built in, and this function is
41
+ * called from inside the table's `computed()` on every render anyway.
42
+ */
25
43
  export declare function credentialState(c: TestableCredential): CredentialStateInfo;
26
- /** "just now" / "3m ago" / "2h ago" / "5d ago" / a date. Empty for no timestamp. */
44
+ /**
45
+ * "just now" / "3m ago" / "2h ago" / "5d ago" / a date. Empty for no timestamp.
46
+ *
47
+ * Reuses the studio's common time words (`integration.common.time.*`) rather
48
+ * than minting its own — the same rule as the dashboard's `formatRelative`.
49
+ */
27
50
  export declare function relativeTime(iso?: string | null): string;
28
51
  export {};
@@ -1,3 +1,16 @@
1
+ import { useIntegrationsT } from "../composables/useIntegrationsT.js";
2
+ export const CREDENTIAL_STATES = ["connected", "failed", "needs_reauth", "untested"];
3
+ const MESSAGE_KEY = {
4
+ connected: "integration.credentials.state.connected",
5
+ failed: "integration.credentials.state.failed",
6
+ // Not derivable from the key: the state is called `needs_reauth` on the wire and
7
+ // "Needs reconnect" to a reader, which is the reason this map exists at all.
8
+ needs_reauth: "integration.credentials.state.needsReconnect",
9
+ untested: "integration.credentials.state.untested"
10
+ };
11
+ export function credentialStateLabel(key) {
12
+ return useIntegrationsT()(MESSAGE_KEY[key]);
13
+ }
1
14
  const TONE = {
2
15
  connected: "bg-emerald-500/10 border-emerald-500/20 text-emerald-700",
3
16
  failed: "bg-rose-500/10 border-rose-500/20 text-rose-700",
@@ -5,25 +18,27 @@ const TONE = {
5
18
  untested: "bg-slate-500/10 border-slate-500/20 text-slate-600"
6
19
  };
7
20
  export function credentialState(c) {
21
+ const t = useIntegrationsT();
8
22
  if (c.status === "needs_reauth") {
9
- return { key: "needs_reauth", label: "Needs reconnect", toneClass: TONE.needs_reauth, at: c.last_test_at };
23
+ return { key: "needs_reauth", label: t(MESSAGE_KEY.needs_reauth), toneClass: TONE.needs_reauth, at: c.last_test_at };
10
24
  }
11
25
  if (c.last_test_ok === true) {
12
- return { key: "connected", label: "Connected", toneClass: TONE.connected, at: c.last_test_at };
26
+ return { key: "connected", label: t(MESSAGE_KEY.connected), toneClass: TONE.connected, at: c.last_test_at };
13
27
  }
14
28
  if (c.last_test_ok === false) {
15
- return { key: "failed", label: "Failed", toneClass: TONE.failed, message: c.last_test_message, at: c.last_test_at };
29
+ return { key: "failed", label: t(MESSAGE_KEY.failed), toneClass: TONE.failed, message: c.last_test_message, at: c.last_test_at };
16
30
  }
17
- return { key: "untested", label: "Not Connected", toneClass: TONE.untested };
31
+ return { key: "untested", label: t(MESSAGE_KEY.untested), toneClass: TONE.untested };
18
32
  }
19
33
  export function relativeTime(iso) {
20
34
  if (!iso) return "";
21
- const t = new Date(iso).getTime();
22
- if (Number.isNaN(t)) return "";
23
- const ms = Date.now() - t;
24
- if (ms < 6e4) return "just now";
25
- if (ms < 36e5) return `${Math.floor(ms / 6e4)}m ago`;
26
- if (ms < 864e5) return `${Math.floor(ms / 36e5)}h ago`;
27
- if (ms < 30 * 864e5) return `${Math.floor(ms / 864e5)}d ago`;
35
+ const at = new Date(iso).getTime();
36
+ if (Number.isNaN(at)) return "";
37
+ const t = useIntegrationsT();
38
+ const ms = Date.now() - at;
39
+ if (ms < 6e4) return t("integration.common.time.justNow");
40
+ if (ms < 36e5) return t("integration.common.time.minutesAgo", Math.floor(ms / 6e4));
41
+ if (ms < 864e5) return t("integration.common.time.hoursAgo", Math.floor(ms / 36e5));
42
+ if (ms < 30 * 864e5) return t("integration.common.time.daysAgo", Math.floor(ms / 864e5));
28
43
  return new Date(iso).toLocaleDateString();
29
44
  }
@@ -0,0 +1,61 @@
1
+ import type { Ref } from 'vue';
2
+ /**
3
+ * The load guard of every composable in the studio, in one place: who waits, who
4
+ * sends a request of its own, and the rule that keeps **one** load in flight.
5
+ *
6
+ * `whenIdle` next door only answers "has the load finished". What a caller does
7
+ * *after* waiting is the part every hand-written guard got wrong the same way, in
8
+ * both directions:
9
+ *
10
+ * - Returning the state **as it stands** handed a cold caller an empty list, which
11
+ * it then fanned out over and cached as an answer (PO-219, PO-230).
12
+ * - Waiting and then always sending its own request lost the other half: two forced
13
+ * callers woken by the same load each sent one, so `loading` went down with a
14
+ * request still out. The flag stopped meaning "a load is out" — and `runs.vue`'s
15
+ * poll reads exactly that flag to decide whether to shed a tick (found in review
16
+ * on PO-230, reproduced with three discards in quick succession).
17
+ *
18
+ * So the gate counts loads rather than trusting a boolean. A caller that waited is
19
+ * satisfied by the load it waited for **unless** it asked for a fresh read, and a
20
+ * forced caller is satisfied only by a load that *began after it asked* — which is
21
+ * what "Try again", a replay and a discard all mean. Everything else waits again,
22
+ * so at most one request is ever out and at most one extra one is ever sent, no
23
+ * matter how many callers arrive during the same load.
24
+ *
25
+ * Two properties worth relying on:
26
+ *
27
+ * - **`loading` is true exactly while a load is out.** The gate is its only writer;
28
+ * a caller that sets it itself breaks the same invariant again.
29
+ * - **A forced caller waits for at most two loads** — the one in flight, then the
30
+ * one that answers it — rather than chaining behind every later arrival.
31
+ *
32
+ * The one thing it cannot do is decide what a waiting caller *returns*: that is the
33
+ * composable's own state, and its shape differs (a list, nothing, a page appended).
34
+ * `claim()` says whether to load; the caller says what to answer with.
35
+ */
36
+ export interface LoadGateState {
37
+ /** True exactly while a load is out. Written by the gate alone. */
38
+ loading: Ref<boolean>;
39
+ /**
40
+ * Set by the composable once a load has actually read something. Left out where
41
+ * nothing is cached — the audit feed is paged by cursor, so every caller either
42
+ * opens it or asks for the next page.
43
+ */
44
+ loaded?: Ref<boolean>;
45
+ /** Loads begun. */
46
+ started: Ref<number>;
47
+ /** Loads finished. Together with `started` this is what tells a forced caller
48
+ * whether the answer it waited for was read after it asked. */
49
+ finished: Ref<number>;
50
+ }
51
+ export interface LoadGate {
52
+ /**
53
+ * Resolves to `true` when the caller must run the load itself — in which case
54
+ * `loading` is already up and `release()` is owed in a `finally`. `false` means
55
+ * the state now holds an answer the caller can return.
56
+ */
57
+ claim: (force?: boolean) => Promise<boolean>;
58
+ /** The load is done: the flag goes down and the load is counted. */
59
+ release: () => void;
60
+ }
61
+ export declare function createLoadGate(state: LoadGateState): LoadGate;