@revenexx/studio-integrations 0.6.0 → 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 +227 -0
- package/README.md +31 -0
- 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 +29 -28
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +38 -37
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -58
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +10 -9
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -3
- package/dist/runtime/components/Integrations/ExpressionInput.vue +16 -15
- package/dist/runtime/components/Integrations/GroupFrameView.vue +29 -19
- package/dist/runtime/components/Integrations/Layout.vue +28 -3
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +15 -14
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +9 -8
- package/dist/runtime/components/Integrations/NodeConfigField.vue +43 -42
- package/dist/runtime/components/Integrations/NodeInspector.vue +225 -216
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +1 -1
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +3 -2
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +1 -1
- package/dist/runtime/components/Integrations/NodePalette.vue +30 -29
- package/dist/runtime/components/Integrations/QueryBuilder.vue +44 -43
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +36 -12
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +14 -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 +37 -23
- package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
- package/dist/runtime/components/Integrations/TemplateCard.vue +16 -6
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +10 -9
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +36 -35
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +6 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +82 -79
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +24 -23
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +7 -6
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +19 -20
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +52 -39
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +20 -19
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +9 -8
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +12 -9
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +27 -26
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +83 -45
- package/dist/runtime/composables/integrationsApi.js +4 -2
- package/dist/runtime/composables/useCredentialTypes.js +4 -2
- package/dist/runtime/composables/useIntegrationsCredentials.js +6 -4
- 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 +3 -1
- 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 +3 -1
- package/dist/runtime/composables/useIntegrationsSecrets.js +4 -2
- 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 +16 -8
- package/dist/runtime/composables/useIntegrationsTriggers.js +4 -2
- package/dist/runtime/composables/useIntegrationsWorkflows.js +5 -3
- package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
- package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
- package/dist/runtime/data/integrationNodeCatalog.js +14 -0
- package/dist/runtime/data/integrationTemplates.d.ts +9 -0
- package/dist/runtime/data/integrationTemplates.js +7 -1
- package/dist/runtime/data/payloadForm.js +2 -1
- package/dist/runtime/data/workflowIconCatalog.js +2 -1
- 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 +52 -34
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +3 -2
- package/dist/runtime/pages/integrations/credentials.vue +70 -52
- package/dist/runtime/pages/integrations/index.vue +50 -56
- package/dist/runtime/pages/integrations/runs.vue +33 -21
- package/dist/runtime/pages/integrations/schedules.vue +49 -28
- package/dist/runtime/pages/integrations/secrets.vue +34 -34
- package/dist/runtime/pages/integrations/templates/[slug].vue +77 -68
- package/dist/runtime/pages/integrations/templates/index.vue +35 -27
- package/dist/runtime/pages/integrations/workflows/[id].vue +164 -99
- package/dist/runtime/pages/integrations/workflows/index.vue +10 -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/utils/credentialState.d.ts +24 -1
- package/dist/runtime/utils/credentialState.js +26 -11
- package/dist/runtime/utils/localize.d.ts +18 -5
- package/dist/runtime/utils/localize.js +32 -9
- package/dist/runtime/utils/pollBudget.d.ts +72 -0
- package/dist/runtime/utils/pollBudget.js +17 -0
- package/dist/runtime/utils/schedule.d.ts +0 -25
- package/dist/runtime/utils/schedule.js +37 -25
- package/dist/runtime/utils/statusStyle.d.ts +26 -2
- package/dist/runtime/utils/statusStyle.js +9 -1
- package/dist/runtime/utils/validationFindings.d.ts +2 -2
- package/dist/runtime/utils/validationFindings.js +4 -3
- package/dist/types.d.mts +2 -6
- package/package.json +3 -2
|
@@ -3,11 +3,16 @@ 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
|
-
|
|
6
|
+
const t = useIntegrationsT();
|
|
7
|
+
useHead({ title: () => t("integration.workflows.list.head.title") });
|
|
7
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
|
+
});
|
|
11
16
|
const loadFailed = computed(() => !!error && !!error.value && !workflows.value.length);
|
|
12
17
|
const createOpen = ref(false);
|
|
13
18
|
</script>
|
|
@@ -15,12 +20,12 @@ const createOpen = ref(false);
|
|
|
15
20
|
<template>
|
|
16
21
|
<IntegrationsLayout>
|
|
17
22
|
<PageHeader
|
|
18
|
-
title="
|
|
23
|
+
:title="t('integration.workflows.list.header.title')"
|
|
19
24
|
>
|
|
20
25
|
<template #actions>
|
|
21
26
|
<Button class="h-7 text-sm" @click="createOpen = true">
|
|
22
27
|
<Plus :size="14" />
|
|
23
|
-
|
|
28
|
+
{{ t("integration.workflows.list.action.new") }}
|
|
24
29
|
</Button>
|
|
25
30
|
</template>
|
|
26
31
|
</PageHeader>
|
|
@@ -29,12 +34,12 @@ const createOpen = ref(false);
|
|
|
29
34
|
<StateCard
|
|
30
35
|
v-if="loadFailed"
|
|
31
36
|
variant="error"
|
|
32
|
-
title="
|
|
37
|
+
:title="t('integration.workflows.list.error.title')"
|
|
33
38
|
:description="error ?? void 0"
|
|
34
39
|
>
|
|
35
40
|
<template #actions>
|
|
36
41
|
<Button variant="outline" class="h-8 text-sm" @click="load(true)">
|
|
37
|
-
|
|
42
|
+
{{ t("integration.common.action.retry") }}
|
|
38
43
|
</Button>
|
|
39
44
|
</template>
|
|
40
45
|
</StateCard>
|
|
@@ -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;
|
|
@@ -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
|
}
|
|
@@ -1,12 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tell the studio which language to render in. **This is the host's job**, and
|
|
3
|
+
* it is the whole contract: the module is published, so it must not go looking
|
|
4
|
+
* for the cockpit's storage keys or import the cockpit's i18n. A host with a
|
|
5
|
+
* language switcher calls this whenever the choice changes; a host with a fixed
|
|
6
|
+
* language sets `studioIntegrations.locale` in its Nuxt config instead and
|
|
7
|
+
* never calls this at all.
|
|
8
|
+
*
|
|
9
|
+
* A BCP-47 tag (`de`, `en`, `de-CH`). Empty or absent means English.
|
|
10
|
+
*/
|
|
11
|
+
export declare function setStudioLocale(locale: string | null | undefined): void;
|
|
12
|
+
/** The language the studio is currently rendering in. Reactive when read in an effect. */
|
|
13
|
+
export declare function studioLocale(): string;
|
|
1
14
|
/**
|
|
2
15
|
* Resolve a multilingual value from the Integrations API. The API returns
|
|
3
16
|
* names and descriptions either as a plain string or as an already-parsed
|
|
4
17
|
* object `{de:"…", en:"…"}` (the HTTP client does JSON decoding automatically).
|
|
5
18
|
* JSON-encoded strings like `'{"de":"…","en":"…"}'` are also handled.
|
|
6
19
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
20
|
+
* The language is the studio's ({@link setStudioLocale}) unless the caller
|
|
21
|
+
* names one. English is the fallback, and the map's own first entry the
|
|
22
|
+
* fallback after that.
|
|
10
23
|
*
|
|
11
24
|
* **This is the studio's only resolver, and that is the point.** The same three
|
|
12
25
|
* lines used to sit in seventeen components under six names (`labelText`, `loc`,
|
|
@@ -22,7 +35,7 @@
|
|
|
22
35
|
* `descriptionText`), so no grep for the rule's shape found them. Both resolve
|
|
23
36
|
* text a person typed and went to `resolveDisplayName` below, not here.
|
|
24
37
|
*/
|
|
25
|
-
export declare function resolveLocalized(value: unknown): string;
|
|
38
|
+
export declare function resolveLocalized(value: unknown, locale?: string): string;
|
|
26
39
|
/**
|
|
27
40
|
* The same resolution, for a value that may be **text a person wrote** — a
|
|
28
41
|
* node's own name, a group frame's label.
|
|
@@ -37,4 +50,4 @@ export declare function resolveLocalized(value: unknown): string;
|
|
|
37
50
|
* resolved. Use this wherever the value can be something a person typed; use
|
|
38
51
|
* `resolveLocalized` for anything that can only have come from a manifest.
|
|
39
52
|
*/
|
|
40
|
-
export declare function resolveDisplayName(value: string | Record<string, string> | undefined | null): string;
|
|
53
|
+
export declare function resolveDisplayName(value: string | Record<string, string> | undefined | null, locale?: string): string;
|
|
@@ -1,11 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
import { shallowRef } from "vue";
|
|
2
|
+
const uiLocale = shallowRef("en");
|
|
3
|
+
export function setStudioLocale(locale) {
|
|
4
|
+
const next = typeof locale === "string" ? locale.trim() : "";
|
|
5
|
+
uiLocale.value = next || "en";
|
|
6
|
+
}
|
|
7
|
+
export function studioLocale() {
|
|
8
|
+
return uiLocale.value;
|
|
9
|
+
}
|
|
10
|
+
function pickLanguage(map, locale) {
|
|
11
|
+
const keys = Object.keys(map);
|
|
12
|
+
if (!keys.length) return "";
|
|
13
|
+
const wanted = locale.toLowerCase();
|
|
14
|
+
const base = wanted.split("-")[0];
|
|
15
|
+
const candidates = [
|
|
16
|
+
keys.find((k) => k.toLowerCase() === wanted),
|
|
17
|
+
keys.find((k) => k.toLowerCase().split("-")[0] === base),
|
|
18
|
+
keys.find((k) => k.toLowerCase().split("-")[0] === "en"),
|
|
19
|
+
keys[0]
|
|
20
|
+
];
|
|
21
|
+
for (const key of candidates) {
|
|
22
|
+
if (key === void 0) continue;
|
|
23
|
+
const picked = map[key];
|
|
24
|
+
if (typeof picked === "string") return picked;
|
|
25
|
+
}
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
export function resolveLocalized(value, locale = uiLocale.value) {
|
|
2
29
|
if (value === null || value === void 0) return "";
|
|
3
30
|
if (typeof value === "object" && !Array.isArray(value)) {
|
|
4
|
-
|
|
5
|
-
const keys = Object.keys(map);
|
|
6
|
-
if (!keys.length) return "";
|
|
7
|
-
const picked = map["en"] ?? map[keys[0]];
|
|
8
|
-
return typeof picked === "string" ? picked : "";
|
|
31
|
+
return pickLanguage(value, locale);
|
|
9
32
|
}
|
|
10
33
|
if (typeof value !== "string") return String(value);
|
|
11
34
|
const trimmed = value.trim();
|
|
@@ -13,12 +36,12 @@ export function resolveLocalized(value) {
|
|
|
13
36
|
try {
|
|
14
37
|
const parsed = JSON.parse(trimmed);
|
|
15
38
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return trimmed;
|
|
16
|
-
return resolveLocalized(parsed);
|
|
39
|
+
return resolveLocalized(parsed, locale);
|
|
17
40
|
} catch {
|
|
18
41
|
return trimmed;
|
|
19
42
|
}
|
|
20
43
|
}
|
|
21
|
-
export function resolveDisplayName(value) {
|
|
44
|
+
export function resolveDisplayName(value, locale = uiLocale.value) {
|
|
22
45
|
if (typeof value === "string") return value;
|
|
23
|
-
return resolveLocalized(value);
|
|
46
|
+
return resolveLocalized(value, locale);
|
|
24
47
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How often a poll is still allowed to fetch once realtime is carrying the same
|
|
3
|
+
* updates.
|
|
4
|
+
*
|
|
5
|
+
* Every run surface polls: the runs list every three seconds, the detail panel
|
|
6
|
+
* every three and its timeline every ten, the replay canvas every four. That is the
|
|
7
|
+
* only thing that made a `running → completed` transition appear at all, and it is
|
|
8
|
+
* expensive — `runs.vue`'s pass fans out one request per workflow and then
|
|
9
|
+
* reconciles up to five active runs, so the list can cost dozens of requests every
|
|
10
|
+
* three seconds while anything is in flight.
|
|
11
|
+
*
|
|
12
|
+
* Talkback replaces the *latency* argument for polling, not the safety argument. So
|
|
13
|
+
* the intervals stay exactly where they are and this decides, per tick, whether the
|
|
14
|
+
* tick may spend a request: at the ordinary cadence while nothing is being reported,
|
|
15
|
+
* and at a slow safety cadence once something is.
|
|
16
|
+
*
|
|
17
|
+
* COUNTING TICKS, NOT READING A CLOCK, and that is deliberate twice over. The tick
|
|
18
|
+
* is the only thing that has actually happened — a wall-clock reading would make
|
|
19
|
+
* this disagree with a `setInterval` the browser throttled in a background tab. And
|
|
20
|
+
* `specs/runs.md` AC-12's test installs Playwright's fake clock and fast-forwards
|
|
21
|
+
* it; a `Date.now()` in here would read the fake time, which is the kind of
|
|
22
|
+
* coupling that makes a test pass for the wrong reason.
|
|
23
|
+
*
|
|
24
|
+
* WHAT `delivering` HAS TO MEAN, and this is the argument the parameter's name is
|
|
25
|
+
* carrying. Being subscribed is no evidence: a channel resolves from the local
|
|
26
|
+
* subscription, so a tenant whose event catalogue never publishes these topics holds
|
|
27
|
+
* open channels that stay silent for the life of the tab — and thinning on that state
|
|
28
|
+
* would make such a tenant ten times slower than before any of this existed, with
|
|
29
|
+
* nothing anywhere saying so. `useIntegrationsRealtime` therefore counts deliveries,
|
|
30
|
+
* and this is only ever thinned once something has really arrived.
|
|
31
|
+
*
|
|
32
|
+
* WHY A SLOW POLL RATHER THAN NONE. What is still unavailable is whether delivery is
|
|
33
|
+
* CONTINUING: a socket that dies quietly after one event keeps reading as delivering,
|
|
34
|
+
* because the per-channel confirmation that would settle it is not passed through the
|
|
35
|
+
* packages between Centrifugo and here. A budget that stopped fetching entirely would
|
|
36
|
+
* then show a stale list for as long as the tab stayed open, with nothing anywhere
|
|
37
|
+
* saying so. Backing off from three seconds to thirty gives up latency we now get
|
|
38
|
+
* from events and keeps the property that mattered: the view converges without the
|
|
39
|
+
* operator doing anything.
|
|
40
|
+
*
|
|
41
|
+
* The caller keeps its own tick-shedding (`!loading.value`) in front of this, so a
|
|
42
|
+
* tick that finds a load still out neither fetches nor spends budget — see
|
|
43
|
+
* `loadGate.ts` for why that flag is trustworthy.
|
|
44
|
+
*/
|
|
45
|
+
export interface PollBudgetOptions {
|
|
46
|
+
/** The caller's own interval, i.e. how much time one `due()` call stands for. */
|
|
47
|
+
intervalMs: number;
|
|
48
|
+
/** The gap between fetches while reports are arriving. */
|
|
49
|
+
liveIntervalMs: number;
|
|
50
|
+
}
|
|
51
|
+
export interface PollBudget {
|
|
52
|
+
/**
|
|
53
|
+
* Whether this tick may fetch. Advances the budget, so call it once per tick and
|
|
54
|
+
* last in the condition — `hasActive && !loading && budget.due(delivering)`.
|
|
55
|
+
*/
|
|
56
|
+
due: (delivering: boolean) => boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Counts a fetch this budget did not authorise: a report's refetch, a manual
|
|
59
|
+
* refresh, a row action, an opening surface's first read. Without it the next due
|
|
60
|
+
* tick would send a second request for a state that was just read.
|
|
61
|
+
*
|
|
62
|
+
* CALL IT IN THE LOADER, not at each call site, wherever the surface has a loader
|
|
63
|
+
* of its own to put it in — `[id].vue`'s `loadRuns`, `RunDetailPanel`'s
|
|
64
|
+
* `loadSummary` and `loadTimelineEvents`, `runs.vue`'s `reload`. A contract that
|
|
65
|
+
* asks every future caller to remember is one that ends up wider than the wiring,
|
|
66
|
+
* which is how this note came to be written.
|
|
67
|
+
*/
|
|
68
|
+
spend: () => void;
|
|
69
|
+
}
|
|
70
|
+
/** The safety cadence every run surface backs off to while reports are arriving. */
|
|
71
|
+
export declare const SAFETY_POLL_MS = 30000;
|
|
72
|
+
export declare function createPollBudget(options: PollBudgetOptions): PollBudget;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const SAFETY_POLL_MS = 3e4;
|
|
2
|
+
export function createPollBudget(options) {
|
|
3
|
+
let elapsed = 0;
|
|
4
|
+
return {
|
|
5
|
+
due(delivering) {
|
|
6
|
+
elapsed += options.intervalMs;
|
|
7
|
+
if (!delivering || elapsed >= options.liveIntervalMs) {
|
|
8
|
+
elapsed = 0;
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
return false;
|
|
12
|
+
},
|
|
13
|
+
spend() {
|
|
14
|
+
elapsed = 0;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -1,28 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A schedule, in both directions: the structured rhythm the studio lets a user
|
|
3
|
-
* pick, the five-field cron expression the platform runs on, and the sentence
|
|
4
|
-
* that says it in words.
|
|
5
|
-
*
|
|
6
|
-
* The studio no longer asks anyone to write cron. It asks for a rhythm — every
|
|
7
|
-
* few minutes, daily at a time, on these weekdays — and `toCron` turns that into
|
|
8
|
-
* the expression the API stores. `parseCron` is the way back, so a schedule that
|
|
9
|
-
* arrives from anywhere (a template, the MCP tools, a hand-written expression from
|
|
10
|
-
* before this existed) opens in the same controls if it can, and stays an
|
|
11
|
-
* expression if it cannot.
|
|
12
|
-
*
|
|
13
|
-
* **The sentence is total over what the picker can build.** `describeRecurrence`
|
|
14
|
-
* always answers; the refusal to guess lives in `parseCron`, which returns `null`
|
|
15
|
-
* for everything outside the shapes below — several stepped fields, minute and
|
|
16
|
-
* hour lists, `L`/`#` syntax, month restrictions, seconds precision. That is what
|
|
17
|
-
* lets the studio show the sentence instead of the expression: for anything it
|
|
18
|
-
* authored there is always a sentence, and where there is none the expression is
|
|
19
|
-
* shown, because nothing else would be true.
|
|
20
|
-
*
|
|
21
|
-
* **Nothing here converts a time.** The platform evaluates the expression in the
|
|
22
|
-
* trigger's own timezone (`CronExpression->isDue($now->setTimezone($tz))`), so the
|
|
23
|
-
* numbers in it are wall-clock time in that zone. The zone is therefore half of
|
|
24
|
-
* the schedule and the caller's to show — it is not a formatting detail.
|
|
25
|
-
*/
|
|
26
1
|
/** A rhythm the picker can build, and `describeRecurrence` can always say. */
|
|
27
2
|
export type Recurrence =
|
|
28
3
|
/** `interval` minutes apart, all day, every day. `1` means every minute. */
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import { useIntegrationsT } from "../composables/useIntegrationsT.js";
|
|
2
|
+
import { studioLocale } from "./localize.js";
|
|
3
|
+
const WEEKDAY_KEYS = [
|
|
4
|
+
"integration.schedules.cron.weekday.sunday",
|
|
5
|
+
"integration.schedules.cron.weekday.monday",
|
|
6
|
+
"integration.schedules.cron.weekday.tuesday",
|
|
7
|
+
"integration.schedules.cron.weekday.wednesday",
|
|
8
|
+
"integration.schedules.cron.weekday.thursday",
|
|
9
|
+
"integration.schedules.cron.weekday.friday",
|
|
10
|
+
"integration.schedules.cron.weekday.saturday"
|
|
11
|
+
];
|
|
2
12
|
export const PLATFORM_TIMEZONE = "UTC";
|
|
3
13
|
export function scheduleTimezone(timezone) {
|
|
4
14
|
return typeof timezone === "string" && timezone.trim() !== "" ? timezone.trim() : PLATFORM_TIMEZONE;
|
|
@@ -126,54 +136,56 @@ export function toCron(r) {
|
|
|
126
136
|
function at(hour, minute) {
|
|
127
137
|
return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
|
|
128
138
|
}
|
|
129
|
-
function plural(n, unit) {
|
|
130
|
-
return `${n} ${unit}${n === 1 ? "" : "s"}`;
|
|
131
|
-
}
|
|
132
139
|
function ordinal(n) {
|
|
140
|
+
if (studioLocale().split("-")[0] === "de") return `${n}.`;
|
|
133
141
|
const suffix = n % 100 >= 11 && n % 100 <= 13 ? "th" : ["th", "st", "nd", "rd"][n % 10] ?? "th";
|
|
134
142
|
return `${n}${suffix}`;
|
|
135
143
|
}
|
|
136
144
|
function sameDays(days, other) {
|
|
137
145
|
return days.length === other.length && days.every((d) => other.includes(d));
|
|
138
146
|
}
|
|
139
|
-
function listWords(words) {
|
|
147
|
+
function listWords(words, t) {
|
|
140
148
|
if (words.length <= 1) return words[0] ?? "";
|
|
141
|
-
return `${words.slice(0, -1).join(", ")} and ${words[words.length - 1]}`;
|
|
149
|
+
return `${words.slice(0, -1).join(", ")} ${t("integration.common.list.and")} ${words[words.length - 1]}`;
|
|
150
|
+
}
|
|
151
|
+
function isEveryDay(days) {
|
|
152
|
+
if (!days) return true;
|
|
153
|
+
const set = new Set(days.map((d) => d === 7 ? 0 : d));
|
|
154
|
+
return set.size === 0 || set.size === 7;
|
|
142
155
|
}
|
|
143
|
-
function dayPhrase(days) {
|
|
144
|
-
if (
|
|
145
|
-
const set = [...new Set(days.map((d) => d === 7 ? 0 : d))].sort((a, b) => a - b);
|
|
146
|
-
if (
|
|
147
|
-
if (sameDays(set,
|
|
148
|
-
|
|
149
|
-
return listWords(set.map((d) => DAY_NAMES[d] ?? "").filter(Boolean));
|
|
156
|
+
function dayPhrase(days, t) {
|
|
157
|
+
if (isEveryDay(days)) return t("integration.schedules.cron.day.daily");
|
|
158
|
+
const set = [...new Set((days ?? []).map((d) => d === 7 ? 0 : d))].sort((a, b) => a - b);
|
|
159
|
+
if (sameDays(set, WEEKDAYS)) return t("integration.schedules.cron.day.weekdays");
|
|
160
|
+
if (sameDays(set, WEEKEND)) return t("integration.schedules.cron.day.weekends");
|
|
161
|
+
return listWords(set.map((d) => t(WEEKDAY_KEYS[d])), t);
|
|
150
162
|
}
|
|
151
163
|
export function describeRecurrence(r) {
|
|
164
|
+
const t = useIntegrationsT();
|
|
152
165
|
switch (r.kind) {
|
|
153
166
|
case "minutes":
|
|
154
|
-
return r.interval <= 1 ? "
|
|
167
|
+
return r.interval <= 1 ? t("integration.schedules.cron.everyMinute") : t("integration.schedules.cron.everyNMinutes", { n: r.interval });
|
|
155
168
|
case "hourly":
|
|
156
|
-
return r.minute === 0 ? "
|
|
169
|
+
return r.minute === 0 ? t("integration.schedules.cron.hourlyOnTheHour") : t("integration.schedules.cron.hourlyAt", { minute: String(r.minute).padStart(2, "0") });
|
|
157
170
|
case "hours":
|
|
158
171
|
if (r.interval <= 1) return describeRecurrence({ kind: "hourly", minute: r.minute });
|
|
159
|
-
return r.minute === 0 ?
|
|
172
|
+
return r.minute === 0 ? t("integration.schedules.cron.everyNHoursOnTheHour", { n: r.interval }) : t("integration.schedules.cron.everyNHoursAt", { n: r.interval, minute: String(r.minute).padStart(2, "0") });
|
|
160
173
|
case "daily":
|
|
161
|
-
return
|
|
174
|
+
return t("integration.schedules.cron.dailyAt", { time: at(r.hour, r.minute) });
|
|
162
175
|
case "times": {
|
|
163
176
|
const hours = [...new Set(r.hours)].sort((a, b) => a - b);
|
|
164
177
|
if (hours.length < 2) return describeRecurrence({ kind: "weekly", days: r.days ?? [0, 1, 2, 3, 4, 5, 6], hour: hours[0] ?? 0, minute: r.minute });
|
|
165
|
-
const phrase = dayPhrase(r.days);
|
|
166
178
|
if (saysAsSpan(hours)) {
|
|
167
|
-
const
|
|
168
|
-
|
|
179
|
+
const from = at(hours[0], r.minute);
|
|
180
|
+
const to = at(hours[hours.length - 1], r.minute);
|
|
181
|
+
return isEveryDay(r.days) ? t("integration.schedules.cron.spanAllDay", { from, to }) : t("integration.schedules.cron.spanWithDays", { days: dayPhrase(r.days, t), from, to });
|
|
169
182
|
}
|
|
170
|
-
return
|
|
171
|
-
}
|
|
172
|
-
case "weekly": {
|
|
173
|
-
return `${dayPhrase(r.days)} at ${at(r.hour, r.minute)}`;
|
|
183
|
+
return t("integration.schedules.cron.namedTimes", { days: dayPhrase(r.days, t), times: listWords(hours.map((h) => at(h, r.minute)), t) });
|
|
174
184
|
}
|
|
185
|
+
case "weekly":
|
|
186
|
+
return t("integration.schedules.cron.weeklyAt", { days: dayPhrase(r.days, t), time: at(r.hour, r.minute) });
|
|
175
187
|
case "monthly":
|
|
176
|
-
return
|
|
188
|
+
return t("integration.schedules.cron.monthlyAt", { ordinal: ordinal(r.day), time: at(r.hour, r.minute) });
|
|
177
189
|
}
|
|
178
190
|
}
|
|
179
191
|
export function describeCron(cron) {
|
|
@@ -13,9 +13,33 @@ export declare function statusTone(status: string | null | undefined): StatusTon
|
|
|
13
13
|
export declare function statusClass(status: string | null | undefined): string;
|
|
14
14
|
/** Tone classes directly (when you already have a tone). */
|
|
15
15
|
export declare function toneClass(tone: StatusTone): string;
|
|
16
|
+
/** The run-status words the studio itself displays (as opposed to whatever
|
|
17
|
+
* vocabulary Temporal or the raw API used to get there) — the only ones
|
|
18
|
+
* `runStatusLabel` translates. Anything else passes through unlabelled: a
|
|
19
|
+
* status this catalogue doesn't know is a raw field, not the studio's chrome.
|
|
20
|
+
*
|
|
21
|
+
* An array rather than a set, and exported, because the runs list offers these
|
|
22
|
+
* five as the choices it can be narrowed to and has to offer them in a fixed
|
|
23
|
+
* order. It used to keep its own copy of the list, so a sixth word added here
|
|
24
|
+
* would have been labelled everywhere and silently missing from that menu. */
|
|
25
|
+
export declare const RUN_STATUS_WORDS: readonly ["running", "succeeded", "failed", "cancelled", "queued"];
|
|
16
26
|
/**
|
|
17
27
|
* Raw API/Temporal run vocabulary → the cockpit's run-status labels, so a run
|
|
18
|
-
* never reads "completed" in one surface and "succeeded" in another.
|
|
19
|
-
*
|
|
28
|
+
* never reads "completed" in one surface and "succeeded" in another. This IS
|
|
29
|
+
* the studio's own chrome (Temporal's "completed" becomes our "succeeded"), so
|
|
30
|
+
* unlike a raw `run.status` read elsewhere, it is translated. Unknown statuses
|
|
31
|
+
* still pass through lowercased, untranslated — this function only relabels
|
|
32
|
+
* the fixed run-status vocabulary, not arbitrary strings.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* The platform's run vocabulary reduced to the studio's own five words — Temporal's
|
|
36
|
+
* `completed` is our `succeeded`, `pending` is `queued`, and two ways of dying are
|
|
37
|
+
* both `failed`. A status this catalogue does not know comes back as itself.
|
|
38
|
+
*
|
|
39
|
+
* Exported because a run's status is used as an *identity* as well as a word: the runs
|
|
40
|
+
* list narrows by it, and what a filter stores has to be something that does not move
|
|
41
|
+
* when the reader's language does. Both sides read this, so a filter and a row can
|
|
42
|
+
* never disagree about which status they mean.
|
|
20
43
|
*/
|
|
44
|
+
export declare function normalisedRunStatus(status: string | null | undefined): string;
|
|
21
45
|
export declare function runStatusLabel(status: string | null | undefined): string;
|
|
@@ -76,7 +76,9 @@ export function statusClass(status) {
|
|
|
76
76
|
export function toneClass(tone) {
|
|
77
77
|
return TONE_CLASS[tone];
|
|
78
78
|
}
|
|
79
|
-
export
|
|
79
|
+
export const RUN_STATUS_WORDS = ["running", "succeeded", "failed", "cancelled", "queued"];
|
|
80
|
+
const RUN_STATUS_WORD_SET = new Set(RUN_STATUS_WORDS);
|
|
81
|
+
export function normalisedRunStatus(status) {
|
|
80
82
|
if (!status) return "";
|
|
81
83
|
const s = String(status).toLowerCase().replace(/[\s-]+/g, "_");
|
|
82
84
|
switch (s) {
|
|
@@ -91,3 +93,9 @@ export function runStatusLabel(status) {
|
|
|
91
93
|
return s;
|
|
92
94
|
}
|
|
93
95
|
}
|
|
96
|
+
export function runStatusLabel(status) {
|
|
97
|
+
const normalized = normalisedRunStatus(status);
|
|
98
|
+
if (!normalized) return "";
|
|
99
|
+
if (!RUN_STATUS_WORD_SET.has(normalized)) return normalized;
|
|
100
|
+
return useIntegrationsT()(`integration.runs.status.${normalized}`);
|
|
101
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { WorkflowNode } from '../types/integrations.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Reading the backend's validation findings — for the editor's validation
|
|
4
4
|
* banner (behavior specs/workflow-validation.md, how docs/workflow-validation.md)
|
|
5
|
-
* and for the node configuration dialog (specs/node-
|
|
5
|
+
* and for the node configuration dialog (specs/node-configuration.md AC-8).
|
|
6
6
|
*
|
|
7
7
|
* The backend reports one finding per incomplete setting, each keyed by its
|
|
8
8
|
* blob path (`blob.nodes.4.config.companyId`). The two readers want different
|
|
@@ -74,7 +74,7 @@ export declare function findingFieldKey(path: string): string | null;
|
|
|
74
74
|
export declare function groupFindingsByNode(findings: readonly ValidationFinding[], nodes: readonly WorkflowNode[], labelOf?: (node: WorkflowNode) => string): FindingNodeGroup[];
|
|
75
75
|
/**
|
|
76
76
|
* Node id → config key → the message to show AT that setting
|
|
77
|
-
* (specs/node-
|
|
77
|
+
* (specs/node-configuration.md AC-8).
|
|
78
78
|
*
|
|
79
79
|
* First finding wins: the backend reports the most specific reason first, and
|
|
80
80
|
* two sentences stacked under one input would be noise.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useIntegrationsT } from "../composables/useIntegrationsT.js";
|
|
1
2
|
import { findManifest } from "../data/integrationNodeCatalog.js";
|
|
2
3
|
import { resolveLocalized } from "./localize.js";
|
|
3
4
|
export function nodeDisplayLabel(node) {
|
|
@@ -47,7 +48,7 @@ export function findingsByNodeField(findings, nodes) {
|
|
|
47
48
|
return out;
|
|
48
49
|
}
|
|
49
50
|
export function findingsSummary(findingCount, nodeCount) {
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
return
|
|
51
|
+
const t = useIntegrationsT();
|
|
52
|
+
const nodesClause = nodeCount ? ` ${t("integration.workflows.validation.summary.nodesClause", nodeCount)}` : "";
|
|
53
|
+
return t("integration.workflows.validation.summary", findingCount, { nodesClause });
|
|
53
54
|
}
|
package/dist/types.d.mts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import type { NuxtModule } from '@nuxt/schema'
|
|
2
|
-
|
|
3
|
-
import type { default as Module } from './module.mjs'
|
|
4
|
-
|
|
5
|
-
export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
|
|
6
|
-
|
|
7
1
|
export { default } from './module.mjs'
|
|
8
2
|
|
|
3
|
+
export { type ModuleOptions } from './module.mjs'
|
|
4
|
+
|
|
9
5
|
export * from '../dist/runtime/types/integrations.js'
|