@revenexx/studio-integrations 0.6.0 → 1.0.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 +643 -0
- package/README.md +31 -0
- package/dist/module.d.mts +17 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +12 -3
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +32 -28
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +45 -38
- 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 +21 -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.d.vue.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +30 -16
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue.d.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -8
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +45 -42
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +3 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/NodeInspector.vue +315 -221
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +2 -1
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +37 -10
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +2 -1
- package/dist/runtime/components/Integrations/NodePalette.vue +40 -35
- 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 +82 -34
- package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
- package/dist/runtime/components/Integrations/SurfaceCard.d.vue.ts +50 -0
- package/dist/runtime/components/Integrations/SurfaceCard.vue +55 -0
- package/dist/runtime/components/Integrations/SurfaceCard.vue.d.ts +50 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue +18 -8
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +22 -12
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +41 -39
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +6 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +16 -0
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +131 -87
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +16 -0
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +27 -24
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +7 -6
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +76 -46
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +55 -43
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.d.vue.ts +16 -17
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +55 -43
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue.d.ts +16 -17
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +9 -8
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -13
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +79 -39
- 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.d.ts +11 -0
- package/dist/runtime/composables/useIntegrationsCredentials.js +8 -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 +14 -2
- 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/useIntegrationsSchedules.d.ts +3 -2
- 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.d.ts +23 -3
- package/dist/runtime/composables/useIntegrationsTriggers.js +21 -8
- package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +10 -1
- package/dist/runtime/composables/useIntegrationsWorkflows.js +30 -7
- package/dist/runtime/composables/useNodeConfigResolve.js +4 -2
- package/dist/runtime/data/integrationNodeCatalog.d.ts +63 -1
- package/dist/runtime/data/integrationNodeCatalog.js +22 -4
- package/dist/runtime/data/integrationTemplates.d.ts +9 -0
- package/dist/runtime/data/integrationTemplates.js +7 -1
- package/dist/runtime/data/nodeInputData.d.ts +58 -1
- package/dist/runtime/data/nodeInputData.js +30 -5
- package/dist/runtime/data/payloadForm.js +2 -1
- package/dist/runtime/data/triggerGlyphs.d.ts +47 -0
- package/dist/runtime/data/triggerGlyphs.js +11 -0
- package/dist/runtime/data/workflowIconCatalog.js +2 -1
- package/dist/runtime/i18n/messages/de.json +1224 -0
- package/dist/runtime/i18n/messages/en.json +1224 -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 +73 -52
- package/dist/runtime/pages/integrations/index.vue +66 -73
- 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 +89 -72
- package/dist/runtime/pages/integrations/templates/index.vue +35 -27
- package/dist/runtime/pages/integrations/workflows/[id].vue +351 -197
- 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/types/integrations.d.ts +25 -1
- package/dist/runtime/utils/cockpitSources.d.ts +22 -3
- package/dist/runtime/utils/credentialState.d.ts +30 -2
- package/dist/runtime/utils/credentialState.js +26 -11
- package/dist/runtime/utils/localize.d.ts +52 -5
- package/dist/runtime/utils/localize.js +47 -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/servedPaths.d.ts +22 -0
- package/dist/runtime/utils/servedPaths.js +3 -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 +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;
|
|
@@ -318,7 +318,24 @@ export interface NodeManifest {
|
|
|
318
318
|
title?: LocalizedString;
|
|
319
319
|
category?: string;
|
|
320
320
|
}>;
|
|
321
|
-
|
|
321
|
+
/**
|
|
322
|
+
* The named ports a node accepts data on, keyed by port name — the mirror of
|
|
323
|
+
* `outputs`.
|
|
324
|
+
*
|
|
325
|
+
* **A node that accepts nothing arrives as `[]`, not `{}`**, which is why the
|
|
326
|
+
* empty array is in the type: the service encodes an empty PHP array as a JSON
|
|
327
|
+
* array, so the empty case is a different JSON *type* from the populated one. It
|
|
328
|
+
* is `never[]` rather than `NodeManifestInput[]` because a *populated* array is
|
|
329
|
+
* not a shape the service produces and not one anything here would read — only
|
|
330
|
+
* `[]` is real. Read the key through `inputPortsForManifest`, which handles both
|
|
331
|
+
* forms once.
|
|
332
|
+
*
|
|
333
|
+
* **Optional here, required there.** Every published manifest carries it — the
|
|
334
|
+
* service's schema lists it among the required keys — but the studio also builds
|
|
335
|
+
* manifest objects in tests and has to tolerate one that predates a field or
|
|
336
|
+
* omits it, so the type does not claim more than the studio can rely on.
|
|
337
|
+
*/
|
|
338
|
+
inputs?: Record<string, NodeManifestInput> | never[];
|
|
322
339
|
/** Empty marks a terminal node (PO-201): the run ends there, so the canvas
|
|
323
340
|
* draws no output handle and no edge may leave it. */
|
|
324
341
|
outputs: NodeManifestOutput[];
|
|
@@ -328,6 +345,13 @@ export interface NodeManifest {
|
|
|
328
345
|
name: string;
|
|
329
346
|
label: string | null;
|
|
330
347
|
};
|
|
348
|
+
/**
|
|
349
|
+
* Whether this version's code was built and can therefore be executed
|
|
350
|
+
* (editor-side enrichment from `GET /nodes`; not manifest data). Absent means
|
|
351
|
+
* an API that does not report it — treated as executable, so an older host
|
|
352
|
+
* does not make every node disappear.
|
|
353
|
+
*/
|
|
354
|
+
__hasBundle?: boolean;
|
|
331
355
|
}
|
|
332
356
|
export interface SecretRecord {
|
|
333
357
|
key: string;
|
|
@@ -40,9 +40,18 @@
|
|
|
40
40
|
* That is measured, not read off the regex: unimport's own `injectImports`, run
|
|
41
41
|
* over this file, injects all five — and over the same file with the declarations
|
|
42
42
|
* rewritten as `declare function`, injects nothing. Repeat it that way if the
|
|
43
|
-
* spelling ever has to change.
|
|
44
|
-
*
|
|
45
|
-
*
|
|
43
|
+
* spelling ever has to change.
|
|
44
|
+
*
|
|
45
|
+
* **Since PO-271 something does stand guard over it, incidentally rather than by
|
|
46
|
+
* design.** The end-to-end suite runs against a host that auto-imports the same five
|
|
47
|
+
* names (`playground/app/composables/cockpitFakes.ts`), so the injection has to work
|
|
48
|
+
* *there* for `specs/credential-managed-source.md` AC-2 to pass at all: rewrite these
|
|
49
|
+
* as `declare function`, and unimport skips the injection in the playground exactly
|
|
50
|
+
* as it would in the cockpit, all five names are undefined, the panel disables
|
|
51
|
+
* itself, and AC-2 goes red. AC-1 would stay green, which is why this is worth
|
|
52
|
+
* writing down — the half of the spec that is about a host *without* the sources
|
|
53
|
+
* cannot see the difference. There is still no test asserting the build tool's
|
|
54
|
+
* behaviour directly, and there deliberately is not one.
|
|
46
55
|
*/
|
|
47
56
|
/**
|
|
48
57
|
* The shapes this module uses.
|
|
@@ -148,6 +157,16 @@ export interface CockpitMailCredential {
|
|
|
148
157
|
* says nothing about why. `null` is what the caller turns into "the managed
|
|
149
158
|
* sources are unavailable here", with the manual fields left in place.
|
|
150
159
|
*
|
|
160
|
+
* **This is not a test for being inside a cockpit, and PO-223 learned that the hard
|
|
161
|
+
* way.** The dashboard read `!== null` as "am I in a cockpit" for its four shortcuts
|
|
162
|
+
* into host-owned surfaces, and the standalone dev studio answered yes: it provides
|
|
163
|
+
* all five names as inert stand-ins on purpose
|
|
164
|
+
* (`integrations/studio-ui/app/composables/consoleStubs.ts`) so that the managed
|
|
165
|
+
* credential panels render their empty states rather than nothing. What this answers
|
|
166
|
+
* is whether the five names are in scope — exactly what the managed sources need to
|
|
167
|
+
* know, and nothing beyond it. A caller asking anything else has to ask its own
|
|
168
|
+
* question; that dashboard now asks the router.
|
|
169
|
+
*
|
|
151
170
|
* Must be called from a component's setup, like the composables it calls.
|
|
152
171
|
*/
|
|
153
172
|
export declare function cockpitSources(): CockpitSources | null;
|
|
@@ -16,13 +16,41 @@ export interface CredentialStateInfo {
|
|
|
16
16
|
label: string;
|
|
17
17
|
/** Badge/pill classes for this state. */
|
|
18
18
|
toneClass: string;
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* What the last test said, whether it passed or failed. A credential type that
|
|
21
|
+
* reached something can name it — which host, which environment, which plan —
|
|
22
|
+
* and that is exactly what an operator cannot see anywhere else once a node
|
|
23
|
+
* stops asking them to configure it.
|
|
24
|
+
*/
|
|
20
25
|
message?: string | null;
|
|
21
26
|
/** When the underlying result was recorded (ISO). */
|
|
22
27
|
at?: string | null;
|
|
23
28
|
}
|
|
24
29
|
type TestableCredential = Pick<Credential, 'status' | 'last_test_ok' | 'last_test_at' | 'last_test_message'>;
|
|
30
|
+
/** Every state a credential can be in, in the order a reader is offered them. */
|
|
31
|
+
export declare const CREDENTIAL_STATES: readonly ["connected", "failed", "needs_reauth", "untested"];
|
|
32
|
+
/**
|
|
33
|
+
* What a state is called for the reader, keyed rather than derived from a
|
|
34
|
+
* credential.
|
|
35
|
+
*
|
|
36
|
+
* `credentialState()` below answers "what state is this credential in, and what
|
|
37
|
+
* is it called"; this answers the second half on its own, for the list's filter,
|
|
38
|
+
* which offers all four states whether or not a credential is in them. They read
|
|
39
|
+
* the same map, so the badge on a row and the option that finds it cannot come to
|
|
40
|
+
* disagree — they did, briefly, as two copies of the same four `t()` calls.
|
|
41
|
+
*/
|
|
42
|
+
export declare function credentialStateLabel(key: CredentialConnState): string;
|
|
43
|
+
/**
|
|
44
|
+
* The `label` is read at call time rather than held in a map beside `TONE`: a
|
|
45
|
+
* map built once would keep the language it was built in, and this function is
|
|
46
|
+
* called from inside the table's `computed()` on every render anyway.
|
|
47
|
+
*/
|
|
25
48
|
export declare function credentialState(c: TestableCredential): CredentialStateInfo;
|
|
26
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* "just now" / "3m ago" / "2h ago" / "5d ago" / a date. Empty for no timestamp.
|
|
51
|
+
*
|
|
52
|
+
* Reuses the studio's common time words (`integration.common.time.*`) rather
|
|
53
|
+
* than minting its own — the same rule as the dashboard's `formatRelative`.
|
|
54
|
+
*/
|
|
27
55
|
export declare function relativeTime(iso?: string | null): string;
|
|
28
56
|
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, message: c.last_test_message, 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,59 @@
|
|
|
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;
|
|
14
|
+
/**
|
|
15
|
+
* The key a read answers from — which is the key a *deletion* has to remove, since
|
|
16
|
+
* what the user is clearing is the text in front of them.
|
|
17
|
+
*
|
|
18
|
+
* All four steps, in the order `resolveLocalized` reads them: the exact tag, the same
|
|
19
|
+
* language in another region, English, and the map's own first entry. `''` when the
|
|
20
|
+
* map holds no string at all.
|
|
21
|
+
*/
|
|
22
|
+
export declare function languageKeyRead(map: unknown, locale?: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* The key an edit is written to.
|
|
25
|
+
*
|
|
26
|
+
* The first two steps of {@link languageKeyRead} and deliberately not its last two:
|
|
27
|
+
* falling back to English or to the first entry answers "what can I show you", and
|
|
28
|
+
* writing there would put the author's text into a language they are not reading —
|
|
29
|
+
* the silent loss this exists to prevent (PO-259). A language the map does not hold
|
|
30
|
+
* yet is written under its base subtag, never the tag the host set, so that reading
|
|
31
|
+
* it back finds it: a `de-CH` studio reading a `de` text through the region step
|
|
32
|
+
* would otherwise write a new `de-CH` key and leave every plain-`de` reader on the
|
|
33
|
+
* old wording.
|
|
34
|
+
*
|
|
35
|
+
* **That guarantee is about the keys this function invents.** The first two steps
|
|
36
|
+
* return a key the map already holds, verbatim — so a map carrying `DE-ch` is written
|
|
37
|
+
* back into `DE-ch`, which the API's `^[a-z]{2,3}(-[A-Z]{2})?$` rejects. Not a case
|
|
38
|
+
* that can arise: such a map could not have been saved in the first place, and the
|
|
39
|
+
* only writer of these keys is this function. Returning an existing key as it stands
|
|
40
|
+
* is what makes the write land where the read came from, which is worth more here than
|
|
41
|
+
* normalising a shape nothing can produce — but it is why the sentence above says
|
|
42
|
+
* *invents* rather than *writes*.
|
|
43
|
+
*
|
|
44
|
+
* Deleting is the other question and has the other answer — see
|
|
45
|
+
* {@link languageKeyRead}.
|
|
46
|
+
*/
|
|
47
|
+
export declare function languageKeyToWrite(map: unknown, locale?: string): string;
|
|
1
48
|
/**
|
|
2
49
|
* Resolve a multilingual value from the Integrations API. The API returns
|
|
3
50
|
* names and descriptions either as a plain string or as an already-parsed
|
|
4
51
|
* object `{de:"…", en:"…"}` (the HTTP client does JSON decoding automatically).
|
|
5
52
|
* JSON-encoded strings like `'{"de":"…","en":"…"}'` are also handled.
|
|
6
53
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
54
|
+
* The language is the studio's ({@link setStudioLocale}) unless the caller
|
|
55
|
+
* names one. English is the fallback, and the map's own first entry the
|
|
56
|
+
* fallback after that.
|
|
10
57
|
*
|
|
11
58
|
* **This is the studio's only resolver, and that is the point.** The same three
|
|
12
59
|
* lines used to sit in seventeen components under six names (`labelText`, `loc`,
|
|
@@ -22,7 +69,7 @@
|
|
|
22
69
|
* `descriptionText`), so no grep for the rule's shape found them. Both resolve
|
|
23
70
|
* text a person typed and went to `resolveDisplayName` below, not here.
|
|
24
71
|
*/
|
|
25
|
-
export declare function resolveLocalized(value: unknown): string;
|
|
72
|
+
export declare function resolveLocalized(value: unknown, locale?: string): string;
|
|
26
73
|
/**
|
|
27
74
|
* The same resolution, for a value that may be **text a person wrote** — a
|
|
28
75
|
* node's own name, a group frame's label.
|
|
@@ -37,4 +84,4 @@ export declare function resolveLocalized(value: unknown): string;
|
|
|
37
84
|
* resolved. Use this wherever the value can be something a person typed; use
|
|
38
85
|
* `resolveLocalized` for anything that can only have come from a manifest.
|
|
39
86
|
*/
|
|
40
|
-
export declare function resolveDisplayName(value: string | Record<string, string> | undefined | null): string;
|
|
87
|
+
export declare function resolveDisplayName(value: string | Record<string, string> | undefined | null, locale?: string): string;
|
|
@@ -1,11 +1,49 @@
|
|
|
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 baseLanguage(locale) {
|
|
11
|
+
const base = locale.toLowerCase().split(/[-_]/)[0] ?? "";
|
|
12
|
+
return /^[a-z]{2,3}$/.test(base) ? base : "en";
|
|
13
|
+
}
|
|
14
|
+
export function languageKeyRead(map, locale = studioLocale()) {
|
|
15
|
+
const obj = map && typeof map === "object" ? map : {};
|
|
16
|
+
const keys = Object.keys(obj);
|
|
17
|
+
if (!keys.length) return "";
|
|
18
|
+
const wanted = locale.toLowerCase();
|
|
19
|
+
const base = baseLanguage(locale);
|
|
20
|
+
const candidates = [
|
|
21
|
+
keys.find((k) => k.toLowerCase() === wanted),
|
|
22
|
+
keys.find((k) => baseLanguage(k) === base),
|
|
23
|
+
keys.find((k) => baseLanguage(k) === "en"),
|
|
24
|
+
keys[0]
|
|
25
|
+
];
|
|
26
|
+
for (const key of candidates) {
|
|
27
|
+
if (key === void 0) continue;
|
|
28
|
+
if (typeof obj[key] === "string") return key;
|
|
29
|
+
}
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
export function languageKeyToWrite(map, locale = studioLocale()) {
|
|
33
|
+
const keys = map && typeof map === "object" ? Object.keys(map) : [];
|
|
34
|
+
const wanted = locale.toLowerCase();
|
|
35
|
+
const base = baseLanguage(locale);
|
|
36
|
+
return keys.find((k) => k.toLowerCase() === wanted) ?? keys.find((k) => baseLanguage(k) === base) ?? base;
|
|
37
|
+
}
|
|
38
|
+
function pickLanguage(map, locale) {
|
|
39
|
+
const key = languageKeyRead(map, locale);
|
|
40
|
+
const picked = key ? map[key] : void 0;
|
|
41
|
+
return typeof picked === "string" ? picked : "";
|
|
42
|
+
}
|
|
43
|
+
export function resolveLocalized(value, locale = uiLocale.value) {
|
|
2
44
|
if (value === null || value === void 0) return "";
|
|
3
45
|
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 : "";
|
|
46
|
+
return pickLanguage(value, locale);
|
|
9
47
|
}
|
|
10
48
|
if (typeof value !== "string") return String(value);
|
|
11
49
|
const trimmed = value.trim();
|
|
@@ -13,12 +51,12 @@ export function resolveLocalized(value) {
|
|
|
13
51
|
try {
|
|
14
52
|
const parsed = JSON.parse(trimmed);
|
|
15
53
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return trimmed;
|
|
16
|
-
return resolveLocalized(parsed);
|
|
54
|
+
return resolveLocalized(parsed, locale);
|
|
17
55
|
} catch {
|
|
18
56
|
return trimmed;
|
|
19
57
|
}
|
|
20
58
|
}
|
|
21
|
-
export function resolveDisplayName(value) {
|
|
59
|
+
export function resolveDisplayName(value, locale = uiLocale.value) {
|
|
22
60
|
if (typeof value === "string") return value;
|
|
23
|
-
return resolveLocalized(value);
|
|
61
|
+
return resolveLocalized(value, locale);
|
|
24
62
|
}
|
|
@@ -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-8'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. */
|