@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.
- package/CHANGELOG.md +428 -0
- package/README.md +48 -4
- package/dist/module.d.mts +17 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +10 -2
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +43 -41
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +123 -50
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +108 -71
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +12 -15
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
- package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +7 -4
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +67 -37
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +30 -20
- package/dist/runtime/components/Integrations/Layout.vue +48 -4
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +23 -23
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +11 -13
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +187 -93
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/NodeInspector.vue +1157 -376
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
- package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
- package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
- package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
- package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
- package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +43 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/NodePalette.vue +122 -55
- package/dist/runtime/components/Integrations/QueryBuilder.vue +46 -38
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +38 -12
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +27 -13
- package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
- package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
- package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
- package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
- package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
- package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
- package/dist/runtime/components/Integrations/StickyNoteView.vue +38 -23
- package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
- package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue +119 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +16 -18
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +52 -67
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
- package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +29 -11
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +20 -1
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +240 -116
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +20 -1
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +28 -43
- package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +17 -7
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +198 -56
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +56 -59
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +28 -21
- package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +15 -9
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -10
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +50 -36
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +126 -48
- package/dist/runtime/composables/integrationsApi.js +4 -2
- package/dist/runtime/composables/useCredentialTypes.js +8 -6
- package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
- package/dist/runtime/composables/useIntegrationsCredentials.js +10 -8
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
- package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
- package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
- package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
- package/dist/runtime/composables/useIntegrationsNodes.js +7 -5
- package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
- package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
- package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
- package/dist/runtime/composables/useIntegrationsRuns.js +14 -7
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
- package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
- package/dist/runtime/composables/useIntegrationsSecrets.js +14 -9
- package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
- package/dist/runtime/composables/useIntegrationsT.js +9 -0
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
- package/dist/runtime/composables/useIntegrationsTemplates.js +28 -16
- package/dist/runtime/composables/useIntegrationsTriggers.js +10 -8
- package/dist/runtime/composables/useIntegrationsWorkflows.js +13 -7
- package/dist/runtime/composables/useLoadGate.d.ts +17 -0
- package/dist/runtime/composables/useLoadGate.js +8 -0
- package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
- package/dist/runtime/data/branchReorder.d.ts +37 -0
- package/dist/runtime/data/branchReorder.js +71 -0
- package/dist/runtime/data/handleId.d.ts +23 -0
- package/dist/runtime/data/handleId.js +6 -0
- package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
- package/dist/runtime/data/integrationNodeCatalog.js +14 -0
- package/dist/runtime/data/integrationTemplates.d.ts +48 -0
- package/dist/runtime/data/integrationTemplates.js +21 -0
- package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
- package/dist/runtime/data/nodeIcons.d.ts +2 -0
- package/dist/runtime/data/nodeIcons.js +73 -0
- package/dist/runtime/data/nodeInputData.d.ts +16 -4
- package/dist/runtime/data/nodeInputData.js +4 -0
- package/dist/runtime/data/payloadForm.d.ts +9 -2
- package/dist/runtime/data/payloadForm.js +29 -4
- package/dist/runtime/data/portTone.d.ts +17 -0
- package/dist/runtime/data/portTone.js +7 -0
- package/dist/runtime/data/webhookAuth.d.ts +18 -0
- package/dist/runtime/data/webhookAuth.js +19 -0
- package/dist/runtime/data/workflowIconCatalog.js +4 -6
- package/dist/runtime/i18n/messages/de.json +1207 -0
- package/dist/runtime/i18n/messages/en.json +1207 -0
- package/dist/runtime/i18n/translate.d.ts +83 -0
- package/dist/runtime/i18n/translate.js +41 -0
- package/dist/runtime/pages/integrations/audit.vue +140 -37
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +9 -6
- package/dist/runtime/pages/integrations/credentials.vue +180 -64
- package/dist/runtime/pages/integrations/index.vue +91 -92
- package/dist/runtime/pages/integrations/runs.vue +33 -21
- package/dist/runtime/pages/integrations/schedules.vue +88 -23
- package/dist/runtime/pages/integrations/secrets.vue +57 -35
- package/dist/runtime/pages/integrations/templates/[slug].vue +119 -88
- package/dist/runtime/pages/integrations/templates/index.vue +82 -103
- package/dist/runtime/pages/integrations/workflows/[id].vue +344 -91
- package/dist/runtime/pages/integrations/workflows/index.vue +23 -5
- package/dist/runtime/plugins/locale.d.ts +10 -0
- package/dist/runtime/plugins/locale.js +6 -0
- package/dist/runtime/plugins/realtime.client.d.ts +32 -0
- package/dist/runtime/plugins/realtime.client.js +4 -0
- package/dist/runtime/types/integrations.d.ts +65 -6
- package/dist/runtime/utils/apiError.d.ts +47 -0
- package/dist/runtime/utils/apiError.js +32 -0
- package/dist/runtime/utils/cockpitSources.d.ts +153 -0
- package/dist/runtime/utils/cockpitSources.js +14 -0
- package/dist/runtime/utils/credentialState.d.ts +24 -1
- package/dist/runtime/utils/credentialState.js +26 -11
- package/dist/runtime/utils/loadGate.d.ts +61 -0
- package/dist/runtime/utils/loadGate.js +22 -0
- package/dist/runtime/utils/localize.d.ts +46 -4
- package/dist/runtime/utils/localize.js +34 -7
- package/dist/runtime/utils/notify.d.ts +59 -0
- package/dist/runtime/utils/notify.js +21 -0
- package/dist/runtime/utils/pollBudget.d.ts +72 -0
- package/dist/runtime/utils/pollBudget.js +17 -0
- package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
- package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
- package/dist/runtime/utils/schedule.d.ts +116 -0
- package/dist/runtime/utils/schedule.js +219 -0
- package/dist/runtime/utils/statusStyle.d.ts +26 -2
- package/dist/runtime/utils/statusStyle.js +9 -1
- package/dist/runtime/utils/validationFindings.d.ts +101 -0
- package/dist/runtime/utils/validationFindings.js +54 -0
- package/dist/runtime/utils/whenIdle.d.ts +42 -0
- package/dist/runtime/utils/whenIdle.js +12 -0
- package/dist/types.d.mts +2 -6
- 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
|
-
|
|
7
|
-
|
|
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="
|
|
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
|
-
|
|
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
|
-
<
|
|
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;
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
22
|
-
if (Number.isNaN(
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
if (ms <
|
|
26
|
-
if (ms <
|
|
27
|
-
if (ms <
|
|
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;
|