@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,7 +3,7 @@ import { computed, onMounted, reactive, ref } from "vue";
|
|
|
3
3
|
import {
|
|
4
4
|
RefreshCircle as Loader2,
|
|
5
5
|
AddCircle as Plus,
|
|
6
|
-
|
|
6
|
+
TrashBinMinimalistic as TrashBin,
|
|
7
7
|
TestTube as TestIcon
|
|
8
8
|
} from "#solar-icons";
|
|
9
9
|
import {
|
|
@@ -32,10 +32,11 @@ import {
|
|
|
32
32
|
} from "@revenexx/studio";
|
|
33
33
|
import { toast } from "../../utils/notify";
|
|
34
34
|
import { apiFieldErrors, refusalReason } from "../../utils/apiError";
|
|
35
|
-
import { credentialState } from "../../utils/credentialState";
|
|
35
|
+
import { CREDENTIAL_STATES, credentialState, credentialStateLabel } from "../../utils/credentialState";
|
|
36
36
|
import { hasRevenexxSource } from "../../utils/revenexxCredentialSource";
|
|
37
37
|
import { resolveLocalized } from "../../utils/localize";
|
|
38
|
-
|
|
38
|
+
const t = useIntegrationsT();
|
|
39
|
+
useHead({ title: () => t("integration.credentials.list.head.title") });
|
|
39
40
|
const { credentials, loading, error, load, create, update, remove, testInline, test, oauthAuthorizeUrl } = useIntegrationsCredentials();
|
|
40
41
|
const { types, load: loadTypes, bySlug } = useCredentialTypes();
|
|
41
42
|
onMounted(async () => {
|
|
@@ -47,8 +48,8 @@ function typeFor(slug) {
|
|
|
47
48
|
function typeName(slug) {
|
|
48
49
|
return resolveLocalized(typeFor(slug)?.name) || slug;
|
|
49
50
|
}
|
|
50
|
-
function isOAuthAuthcode(
|
|
51
|
-
return
|
|
51
|
+
function isOAuthAuthcode(t2) {
|
|
52
|
+
return t2?.auth_kind === "oauth2-authcode";
|
|
52
53
|
}
|
|
53
54
|
function formatDateTime(iso) {
|
|
54
55
|
if (!iso) return "\u2014";
|
|
@@ -58,15 +59,32 @@ function formatDateTime(iso) {
|
|
|
58
59
|
return iso;
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
const filterTabs = computed(() => {
|
|
63
|
+
const systems = [...new Set(credentials.value.map((c) => c.credential_type_slug))].map((slug) => ({ value: slug, label: typeName(slug) })).sort((a, b) => a.label.localeCompare(b.label));
|
|
64
|
+
return [
|
|
65
|
+
{
|
|
66
|
+
key: "state",
|
|
67
|
+
label: t("integration.credentials.list.table.column.status"),
|
|
68
|
+
getter: (c) => credentialState(c).key,
|
|
69
|
+
options: CREDENTIAL_STATES.map((key) => ({ value: key, label: credentialStateLabel(key) }))
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: "system",
|
|
73
|
+
label: t("integration.credentials.list.table.column.system"),
|
|
74
|
+
getter: (c) => c.credential_type_slug,
|
|
75
|
+
options: systems
|
|
76
|
+
}
|
|
77
|
+
];
|
|
78
|
+
});
|
|
79
|
+
const columns = computed(() => [
|
|
80
|
+
{ key: "system", label: t("integration.credentials.list.table.column.system"), width: "minmax(160px,220px)", sortable: true, sortGetter: (c) => typeName(c.credential_type_slug).toLowerCase(), searchGetter: (c) => `${typeName(c.credential_type_slug)} ${c.credential_type_slug}` },
|
|
81
|
+
{ key: "name", label: t("integration.credentials.list.table.column.name"), sortable: true, sortGetter: (c) => c.name.toLowerCase(), searchGetter: (c) => c.name },
|
|
82
|
+
{ key: "status", label: t("integration.credentials.list.table.column.status"), width: "150px", sortable: true, sortGetter: (c) => credentialState(c).key },
|
|
83
|
+
{ key: "connected", label: t("integration.credentials.list.table.column.connected"), width: "150px", sortable: true, sortGetter: (c) => c.last_test_ok ? c.last_test_at ?? "" : "" },
|
|
84
|
+
{ key: "created", label: t("integration.credentials.list.table.column.created"), width: "150px", sortable: true, sortGetter: (c) => c.created_at ?? "" },
|
|
85
|
+
{ key: "updated", label: t("integration.credentials.list.table.column.updated"), width: "150px", sortable: true, sortGetter: (c) => c.updated_at ?? "" },
|
|
68
86
|
{ key: "actions", label: "", width: "80px" }
|
|
69
|
-
];
|
|
87
|
+
]);
|
|
70
88
|
const dialogOpen = ref(false);
|
|
71
89
|
const editing = ref(null);
|
|
72
90
|
const pickedType = ref(null);
|
|
@@ -78,8 +96,8 @@ const saveError = ref(null);
|
|
|
78
96
|
const fieldErrors = ref({});
|
|
79
97
|
const sourceActive = ref(false);
|
|
80
98
|
const isEdit = computed(() => editing.value !== null);
|
|
81
|
-
function fieldsOf(
|
|
82
|
-
return
|
|
99
|
+
function fieldsOf(t2) {
|
|
100
|
+
return t2?.fields ?? [];
|
|
83
101
|
}
|
|
84
102
|
function configFieldAria(key) {
|
|
85
103
|
return fieldErrors.value[`config.${key}`] ? { "aria-invalid": "true", "aria-describedby": `cred-field-${key}-error` } : {};
|
|
@@ -88,9 +106,9 @@ function clearRefusal() {
|
|
|
88
106
|
saveError.value = null;
|
|
89
107
|
fieldErrors.value = {};
|
|
90
108
|
}
|
|
91
|
-
function openCreate(
|
|
109
|
+
function openCreate(t2) {
|
|
92
110
|
editing.value = null;
|
|
93
|
-
pickedType.value =
|
|
111
|
+
pickedType.value = t2 ?? null;
|
|
94
112
|
form.name = "";
|
|
95
113
|
form.config = {};
|
|
96
114
|
testResult.value = null;
|
|
@@ -144,7 +162,7 @@ async function submit() {
|
|
|
144
162
|
if (isEdit.value && editing.value) {
|
|
145
163
|
await update(editing.value.id, { name: form.name.trim(), config: buildConfigPayload() });
|
|
146
164
|
dialogOpen.value = false;
|
|
147
|
-
toast.success("
|
|
165
|
+
toast.success(t("integration.credentials.toast.updated"));
|
|
148
166
|
} else {
|
|
149
167
|
const created = await create({
|
|
150
168
|
credential_type_slug: pickedType.value.slug,
|
|
@@ -152,14 +170,14 @@ async function submit() {
|
|
|
152
170
|
config: buildConfigPayload()
|
|
153
171
|
});
|
|
154
172
|
dialogOpen.value = false;
|
|
155
|
-
toast.success("
|
|
173
|
+
toast.success(t("integration.credentials.toast.created"));
|
|
156
174
|
if (isOAuthAuthcode(pickedType.value)) {
|
|
157
175
|
await connect(created);
|
|
158
176
|
}
|
|
159
177
|
}
|
|
160
178
|
} catch (e) {
|
|
161
179
|
fieldErrors.value = apiFieldErrors(e);
|
|
162
|
-
saveError.value = refusalReason(e, "
|
|
180
|
+
saveError.value = refusalReason(e, t("integration.credentials.error.save"));
|
|
163
181
|
toast.error(saveError.value);
|
|
164
182
|
} finally {
|
|
165
183
|
saving.value = false;
|
|
@@ -175,7 +193,7 @@ async function connect(c) {
|
|
|
175
193
|
if (ev.data?.type === "integrations-oauth-complete") {
|
|
176
194
|
window.removeEventListener("message", onMessage);
|
|
177
195
|
await load(true);
|
|
178
|
-
toast.success("
|
|
196
|
+
toast.success(t("integration.credentials.toast.connected"));
|
|
179
197
|
}
|
|
180
198
|
};
|
|
181
199
|
window.addEventListener("message", onMessage);
|
|
@@ -187,7 +205,7 @@ async function connect(c) {
|
|
|
187
205
|
}
|
|
188
206
|
}, 1e3);
|
|
189
207
|
} catch (e) {
|
|
190
|
-
toast.error(e?.message ?? "
|
|
208
|
+
toast.error(e?.message ?? t("integration.credentials.error.oauthConnect"));
|
|
191
209
|
} finally {
|
|
192
210
|
connecting.value = null;
|
|
193
211
|
}
|
|
@@ -200,10 +218,10 @@ async function confirmDelete() {
|
|
|
200
218
|
deleting.value = true;
|
|
201
219
|
try {
|
|
202
220
|
await remove(c.id);
|
|
203
|
-
toast.success("
|
|
221
|
+
toast.success(t("integration.credentials.toast.deleted"));
|
|
204
222
|
pendingDelete.value = null;
|
|
205
223
|
} catch (e) {
|
|
206
|
-
toast.error(e?.message ?? "
|
|
224
|
+
toast.error(e?.message ?? t("integration.credentials.error.delete"));
|
|
207
225
|
} finally {
|
|
208
226
|
deleting.value = false;
|
|
209
227
|
}
|
|
@@ -213,13 +231,13 @@ async function confirmDelete() {
|
|
|
213
231
|
<template>
|
|
214
232
|
<IntegrationsLayout>
|
|
215
233
|
<PageHeader
|
|
216
|
-
title="
|
|
217
|
-
description="
|
|
234
|
+
:title="t('integration.credentials.list.header.title')"
|
|
235
|
+
:description="t('integration.credentials.list.header.description')"
|
|
218
236
|
>
|
|
219
237
|
<template #actions>
|
|
220
238
|
<Button class="h-7 text-sm" :disabled="!types.length" @click="openCreate()">
|
|
221
239
|
<Plus :size="14" class="mr-1" />
|
|
222
|
-
|
|
240
|
+
{{ t("integration.credentials.list.action.new") }}
|
|
223
241
|
</Button>
|
|
224
242
|
</template>
|
|
225
243
|
</PageHeader>
|
|
@@ -234,10 +252,10 @@ async function confirmDelete() {
|
|
|
234
252
|
:columns="columns"
|
|
235
253
|
:row-key="(c2) => c2.id"
|
|
236
254
|
:default-sort="{ column: 'name', direction: 'asc' }"
|
|
237
|
-
:search="{ placeholder: '
|
|
238
|
-
:filter="
|
|
255
|
+
:search="{ placeholder: t('integration.credentials.list.search.placeholder') }"
|
|
256
|
+
:filter-tabs="filterTabs"
|
|
239
257
|
:loading="loading"
|
|
240
|
-
empty-text="
|
|
258
|
+
:empty-text="t('integration.credentials.list.empty', { action: t('integration.credentials.list.action.new') })"
|
|
241
259
|
@row-click="(c2) => openEdit(c2)"
|
|
242
260
|
>
|
|
243
261
|
<template #cell-system="{ row: c }">
|
|
@@ -288,13 +306,13 @@ async function confirmDelete() {
|
|
|
288
306
|
size="sm"
|
|
289
307
|
class="h-7 text-amber-600"
|
|
290
308
|
:disabled="connecting === c.id"
|
|
291
|
-
:aria-label="
|
|
309
|
+
:aria-label="t('integration.credentials.list.action.reconnect.aria', { name: c.name })"
|
|
292
310
|
@click="connect(c)"
|
|
293
311
|
>
|
|
294
|
-
|
|
312
|
+
{{ t("integration.credentials.list.action.reconnect") }}
|
|
295
313
|
</Button>
|
|
296
|
-
<button type="button" class="flex size-7 items-center justify-center rounded text-foreground/40 hover:text-rose-600 hover:bg-rose-500/10" :title="
|
|
297
|
-
<
|
|
314
|
+
<button type="button" class="flex size-7 items-center justify-center rounded text-foreground/40 hover:text-rose-600 hover:bg-rose-500/10" :title="t('integration.credentials.list.action.delete.aria', { name: c.name })" :aria-label="t('integration.credentials.list.action.delete.aria', { name: c.name })" @click="pendingDelete = c">
|
|
315
|
+
<TrashBin :size="13" />
|
|
298
316
|
</button>
|
|
299
317
|
</div>
|
|
300
318
|
</template>
|
|
@@ -305,9 +323,9 @@ async function confirmDelete() {
|
|
|
305
323
|
<Dialog v-model:open="dialogOpen">
|
|
306
324
|
<DialogContent class="sm:max-w-lg">
|
|
307
325
|
<DialogHeader>
|
|
308
|
-
<DialogTitle>{{ isEdit ? "
|
|
326
|
+
<DialogTitle>{{ isEdit ? t("integration.credentials.shared.dialog.title.edit") : t("integration.credentials.shared.dialog.title.create") }}</DialogTitle>
|
|
309
327
|
<DialogDescription class="text-sm">
|
|
310
|
-
{{ isEdit ? "
|
|
328
|
+
{{ isEdit ? t("integration.credentials.list.dialog.description.edit") : t("integration.credentials.list.dialog.description.create") }}
|
|
311
329
|
</DialogDescription>
|
|
312
330
|
</DialogHeader>
|
|
313
331
|
|
|
@@ -315,9 +333,9 @@ async function confirmDelete() {
|
|
|
315
333
|
<IntegrationsCredentialTypePicker
|
|
316
334
|
v-if="!pickedType"
|
|
317
335
|
:types="types"
|
|
318
|
-
@pick="(
|
|
319
|
-
pickedType =
|
|
320
|
-
form.name = resolveLocalized(
|
|
336
|
+
@pick="(picked) => {
|
|
337
|
+
pickedType = picked;
|
|
338
|
+
form.name = resolveLocalized(picked.name);
|
|
321
339
|
clearRefusal();
|
|
322
340
|
}"
|
|
323
341
|
/>
|
|
@@ -325,12 +343,12 @@ async function confirmDelete() {
|
|
|
325
343
|
<!-- Step 2: dynamic form -->
|
|
326
344
|
<div v-else class="space-y-3 py-1 max-h-[28rem] overflow-y-auto pr-1">
|
|
327
345
|
<div class="space-y-1.5">
|
|
328
|
-
<Label for="cred-name">
|
|
346
|
+
<Label for="cred-name">{{ t("integration.credentials.shared.field.name.label") }}</Label>
|
|
329
347
|
<Input
|
|
330
348
|
id="cred-name"
|
|
331
349
|
v-model="form.name"
|
|
332
350
|
class="bg-white"
|
|
333
|
-
placeholder="
|
|
351
|
+
:placeholder="t('integration.credentials.list.dialog.field.name.placeholder')"
|
|
334
352
|
:aria-invalid="fieldErrors.name ? 'true' : void 0"
|
|
335
353
|
:aria-describedby="fieldErrors.name ? 'cred-name-error' : void 0"
|
|
336
354
|
/>
|
|
@@ -362,7 +380,7 @@ async function confirmDelete() {
|
|
|
362
380
|
:model-value="form.config[f.key] ?? ''"
|
|
363
381
|
type="password"
|
|
364
382
|
autocomplete="new-password"
|
|
365
|
-
:placeholder="isEdit ? '
|
|
383
|
+
:placeholder="isEdit ? t('integration.credentials.shared.field.secret.placeholder') : ''"
|
|
366
384
|
class="bg-white"
|
|
367
385
|
v-bind="configFieldAria(f.key)"
|
|
368
386
|
@update:model-value="(v) => form.config[f.key] = v"
|
|
@@ -388,7 +406,7 @@ async function confirmDelete() {
|
|
|
388
406
|
@update:model-value="(v) => form.config[f.key] = v"
|
|
389
407
|
>
|
|
390
408
|
<SelectTrigger :id="`cred-field-${f.key}`" class="bg-white" v-bind="configFieldAria(f.key)">
|
|
391
|
-
<SelectValue placeholder="
|
|
409
|
+
<SelectValue :placeholder="t('integration.credentials.shared.field.select.placeholder')" />
|
|
392
410
|
</SelectTrigger>
|
|
393
411
|
<SelectContent>
|
|
394
412
|
<SelectItem v-for="opt in f.options ?? []" :key="String(opt.value)" :value="String(opt.value)">
|
|
@@ -416,7 +434,7 @@ async function confirmDelete() {
|
|
|
416
434
|
</div>
|
|
417
435
|
|
|
418
436
|
<p v-if="isOAuthAuthcode(pickedType) && !isEdit" class="text-sm text-muted-foreground">
|
|
419
|
-
|
|
437
|
+
{{ t("integration.credentials.shared.oauthHint") }}
|
|
420
438
|
</p>
|
|
421
439
|
|
|
422
440
|
<div
|
|
@@ -424,7 +442,7 @@ async function confirmDelete() {
|
|
|
424
442
|
class="rounded-md px-3 py-2 text-sm"
|
|
425
443
|
:class="testResult.ok ? 'bg-emerald-500/10 text-emerald-700' : 'bg-rose-500/10 text-rose-700'"
|
|
426
444
|
>
|
|
427
|
-
{{ testResult.ok ? "
|
|
445
|
+
{{ testResult.ok ? t("integration.credentials.shared.test.ok") : testResult.error ?? t("integration.credentials.shared.test.failed") }}
|
|
428
446
|
</div>
|
|
429
447
|
|
|
430
448
|
</div>
|
|
@@ -442,18 +460,18 @@ async function confirmDelete() {
|
|
|
442
460
|
<DialogFooter v-if="pickedType" class="sm:justify-between">
|
|
443
461
|
<Button v-if="!isEdit" variant="ghost" size="sm" :disabled="saving || testing" @click="pickedType = null;
|
|
444
462
|
clearRefusal()">
|
|
445
|
-
|
|
463
|
+
{{ t("integration.credentials.list.dialog.action.back") }}
|
|
446
464
|
</Button>
|
|
447
465
|
<span v-else />
|
|
448
466
|
<div class="flex items-center gap-2">
|
|
449
467
|
<Button variant="outline" :disabled="saving || testing" @click="runTest">
|
|
450
468
|
<Loader2 v-if="testing" :size="14" class="mr-1 animate-spin" />
|
|
451
469
|
<TestIcon v-else :size="14" class="mr-1" />
|
|
452
|
-
|
|
470
|
+
{{ t("integration.credentials.shared.action.testConnection") }}
|
|
453
471
|
</Button>
|
|
454
472
|
<Button :disabled="saving || !form.name.trim()" @click="submit">
|
|
455
473
|
<Loader2 v-if="saving" :size="14" class="mr-1 animate-spin" />
|
|
456
|
-
|
|
474
|
+
{{ t("integration.common.action.save") }}
|
|
457
475
|
</Button>
|
|
458
476
|
</div>
|
|
459
477
|
</DialogFooter>
|
|
@@ -465,21 +483,21 @@ async function confirmDelete() {
|
|
|
465
483
|
}">
|
|
466
484
|
<AlertDialogContent>
|
|
467
485
|
<AlertDialogHeader>
|
|
468
|
-
<AlertDialogTitle>
|
|
486
|
+
<AlertDialogTitle>{{ t("integration.credentials.shared.confirm.delete.title") }}</AlertDialogTitle>
|
|
469
487
|
<AlertDialogDescription>
|
|
470
488
|
<span v-if="pendingDelete">
|
|
471
489
|
<span class="font-medium text-foreground">{{ pendingDelete.name }}</span>
|
|
472
|
-
|
|
490
|
+
{{ t("integration.credentials.shared.confirm.delete.body") }}
|
|
473
491
|
</span>
|
|
474
492
|
</AlertDialogDescription>
|
|
475
493
|
</AlertDialogHeader>
|
|
476
494
|
<AlertDialogFooter>
|
|
477
495
|
<Button variant="outline" :disabled="deleting" @click="pendingDelete = null">
|
|
478
|
-
|
|
496
|
+
{{ t("integration.common.action.cancel") }}
|
|
479
497
|
</Button>
|
|
480
498
|
<Button class="bg-destructive text-white hover:bg-destructive/90" :disabled="deleting" @click="confirmDelete">
|
|
481
499
|
<Loader2 v-if="deleting" :size="14" class="animate-spin" />
|
|
482
|
-
{{ deleting ? "
|
|
500
|
+
{{ deleting ? t("integration.common.state.deleting") : t("integration.common.action.delete") }}
|
|
483
501
|
</Button>
|
|
484
502
|
</AlertDialogFooter>
|
|
485
503
|
</AlertDialogContent>
|
|
@@ -4,12 +4,14 @@ import TemplateCard from "../../components/Integrations/TemplateCard.vue";
|
|
|
4
4
|
import WorkflowCreateDialog from "../../components/Integrations/WorkflowCreateDialog.vue";
|
|
5
5
|
import { Pulse as Activity, DangerTriangle as AlertTriangle, ArrowRight, Box as Boxes, ClockCircle as Clock, Database, Layers, AddCircle as Plus, Radio, MagicStick as Sparkles, LinkRound as Webhook, Programming as Workflow } from "#solar-icons";
|
|
6
6
|
import { Badge, Button } from "@revenexx/studio";
|
|
7
|
-
|
|
7
|
+
import { runStatusLabel, statusClass } from "../../utils/statusStyle";
|
|
8
|
+
const t = useIntegrationsT();
|
|
9
|
+
useHead({ title: () => t("integration.dashboard.head.title") });
|
|
8
10
|
const { workflows, activeCount: activeWorkflows, loading: workflowsLoading, load: loadWorkflows } = useIntegrationsWorkflows();
|
|
9
11
|
const { runs, runningCount, failedCount, loading: runsLoading, load: loadRuns } = useIntegrationsRuns();
|
|
10
12
|
const { startingPoints, loading: templatesLoading, load: loadTemplates, ensureDetails } = useIntegrationsTemplates();
|
|
11
|
-
watch(() => startingPoints.value.map((
|
|
12
|
-
if (startingPoints.value.length) void ensureDetails(startingPoints.value.map((
|
|
13
|
+
watch(() => startingPoints.value.map((t2) => t2.slug).join(","), () => {
|
|
14
|
+
if (startingPoints.value.length) void ensureDetails(startingPoints.value.map((t2) => t2.slug));
|
|
13
15
|
}, { immediate: true });
|
|
14
16
|
const { activeCount: schedulesActive, load: loadSchedules } = useIntegrationsSchedules();
|
|
15
17
|
const { pendingCount: missedRuns, globalLoading: missedLoading, loadGlobal: loadMissedRuns } = useIntegrationsDeadLetters();
|
|
@@ -39,80 +41,72 @@ const failedRecent = computed(() => latestByWorkflow.value.filter((r) => r.statu
|
|
|
39
41
|
function formatRelative(iso) {
|
|
40
42
|
if (!iso) return "\u2014";
|
|
41
43
|
const ms = Date.now() - new Date(iso).getTime();
|
|
42
|
-
if (ms < 6e4) return "
|
|
43
|
-
if (ms < 36e5) return
|
|
44
|
-
if (ms < 864e5) return
|
|
45
|
-
return
|
|
46
|
-
}
|
|
47
|
-
function statusClass(status) {
|
|
48
|
-
switch (status) {
|
|
49
|
-
case "succeeded":
|
|
50
|
-
return "bg-emerald-500/15 text-emerald-700 border-emerald-500/20";
|
|
51
|
-
case "running":
|
|
52
|
-
return "bg-sky-500/15 text-sky-700 border-sky-500/20";
|
|
53
|
-
case "failed":
|
|
54
|
-
return "bg-rose-500/15 text-rose-700 border-rose-500/20";
|
|
55
|
-
case "queued":
|
|
56
|
-
return "bg-amber-500/15 text-amber-700 border-amber-500/20";
|
|
57
|
-
case "cancelled":
|
|
58
|
-
return "bg-slate-500/15 text-slate-700 border-slate-500/20";
|
|
59
|
-
}
|
|
44
|
+
if (ms < 6e4) return t("integration.common.time.justNow");
|
|
45
|
+
if (ms < 36e5) return t("integration.common.time.minutesAgo", Math.floor(ms / 6e4));
|
|
46
|
+
if (ms < 864e5) return t("integration.common.time.hoursAgo", Math.floor(ms / 36e5));
|
|
47
|
+
return t("integration.common.time.daysAgo", Math.floor(ms / 864e5));
|
|
60
48
|
}
|
|
61
49
|
const kpis = computed(() => [
|
|
62
50
|
{
|
|
63
|
-
|
|
51
|
+
key: "activeWorkflows",
|
|
52
|
+
label: t("integration.dashboard.kpi.activeWorkflows.label"),
|
|
64
53
|
value: activeWorkflows.value,
|
|
65
|
-
|
|
54
|
+
// A plural key, so the count is the second argument. Called without one it
|
|
55
|
+
// would put the raw "a | b" on screen.
|
|
56
|
+
sub: t("integration.dashboard.kpi.activeWorkflows.sub", workflows.value.length - activeWorkflows.value),
|
|
66
57
|
icon: Workflow,
|
|
67
58
|
tile: "bg-emerald-500 text-white"
|
|
68
59
|
},
|
|
69
60
|
{
|
|
70
|
-
|
|
61
|
+
key: "runningNow",
|
|
62
|
+
label: t("integration.dashboard.kpi.runningNow.label"),
|
|
71
63
|
value: runningCount.value,
|
|
72
|
-
sub: "
|
|
64
|
+
sub: t("integration.dashboard.kpi.runningNow.sub"),
|
|
73
65
|
icon: Activity,
|
|
74
66
|
tile: "bg-sky-500 text-white"
|
|
75
67
|
},
|
|
76
68
|
{
|
|
77
|
-
|
|
69
|
+
key: "scheduled",
|
|
70
|
+
label: t("integration.dashboard.kpi.scheduled.label"),
|
|
78
71
|
value: schedulesActive.value,
|
|
79
|
-
sub: "scheduled
|
|
72
|
+
sub: t("integration.dashboard.kpi.scheduled.sub"),
|
|
80
73
|
icon: Clock,
|
|
81
74
|
tile: "bg-violet-500 text-white"
|
|
82
75
|
},
|
|
83
76
|
{
|
|
84
|
-
|
|
77
|
+
key: "missedRuns",
|
|
78
|
+
label: t("integration.dashboard.kpi.missedRuns.label"),
|
|
85
79
|
value: missedRuns.value,
|
|
86
|
-
sub: "
|
|
80
|
+
sub: t("integration.dashboard.kpi.missedRuns.sub"),
|
|
87
81
|
icon: AlertTriangle,
|
|
88
82
|
tile: missedRuns.value ? "bg-amber-500 text-white" : "bg-slate-300 text-white",
|
|
89
83
|
to: "/integrations/runs?tab=missed"
|
|
90
84
|
}
|
|
91
85
|
]);
|
|
92
|
-
const surfaces = [
|
|
93
|
-
{ label: "
|
|
94
|
-
{ label: "
|
|
95
|
-
{ label: "
|
|
96
|
-
{ label: "
|
|
97
|
-
];
|
|
86
|
+
const surfaces = computed(() => [
|
|
87
|
+
{ label: t("integration.dashboard.surface.io.label"), to: "/integrations/io", icon: Database, hint: t("integration.dashboard.surface.io.hint") },
|
|
88
|
+
{ label: t("integration.dashboard.surface.sftp.label"), to: "/integrations/sftp", icon: Boxes, hint: t("integration.dashboard.surface.sftp.hint") },
|
|
89
|
+
{ label: t("integration.dashboard.surface.events.label"), to: "/integrations/events", icon: Radio, hint: t("integration.dashboard.surface.events.hint") },
|
|
90
|
+
{ label: t("integration.dashboard.surface.webhooks.label"), to: "/integrations/webhooks", icon: Webhook, hint: t("integration.dashboard.surface.webhooks.hint") }
|
|
91
|
+
]);
|
|
98
92
|
</script>
|
|
99
93
|
|
|
100
94
|
<template>
|
|
101
95
|
<IntegrationsLayout>
|
|
102
96
|
<PageHeader
|
|
103
|
-
title="
|
|
104
|
-
description="
|
|
97
|
+
:title="t('integration.dashboard.header.title')"
|
|
98
|
+
:description="t('integration.dashboard.header.description')"
|
|
105
99
|
>
|
|
106
100
|
<template #actions>
|
|
107
101
|
<NuxtLink to="/integrations/templates">
|
|
108
102
|
<Button variant="outline" class="h-7 text-sm">
|
|
109
103
|
<Layers :size="12" />
|
|
110
|
-
|
|
104
|
+
{{ t("integration.dashboard.action.browseTemplates") }}
|
|
111
105
|
</Button>
|
|
112
106
|
</NuxtLink>
|
|
113
107
|
<Button class="h-7 text-sm" @click="createOpen = true">
|
|
114
108
|
<Plus :size="14" />
|
|
115
|
-
|
|
109
|
+
{{ t("integration.dashboard.action.newWorkflow") }}
|
|
116
110
|
</Button>
|
|
117
111
|
</template>
|
|
118
112
|
</PageHeader>
|
|
@@ -122,11 +116,11 @@ const surfaces = [
|
|
|
122
116
|
tile is a link, and a link takes its name from its label instead of its
|
|
123
117
|
contents — named "Missed runs" alone it would announce less than the
|
|
124
118
|
three tiles beside it. -->
|
|
125
|
-
<div role="group" aria-label="
|
|
119
|
+
<div role="group" :aria-label="t('integration.dashboard.kpi.aria.group')" class="grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
|
|
126
120
|
<component
|
|
127
121
|
:is="k.to ? NuxtLinkComp : 'div'"
|
|
128
122
|
v-for="k in kpis"
|
|
129
|
-
:key="k.
|
|
123
|
+
:key="k.key"
|
|
130
124
|
:to="k.to"
|
|
131
125
|
:role="k.to ? void 0 : 'group'"
|
|
132
126
|
:aria-label="`${k.label}: ${k.value}`"
|
|
@@ -156,16 +150,16 @@ const surfaces = [
|
|
|
156
150
|
<!-- Two-column: recent runs + needs attention + templates to start from -->
|
|
157
151
|
<div class="grid gap-4 lg:grid-cols-3">
|
|
158
152
|
<!-- Recent runs ─ takes 2 cols -->
|
|
159
|
-
<div role="group" aria-label="
|
|
153
|
+
<div role="group" :aria-label="t('integration.dashboard.runs.title')" class="lg:col-span-2 rounded-lg border border-black/[0.06] bg-white">
|
|
160
154
|
<div class="flex items-center justify-between border-b border-black/[0.06] px-3 py-2">
|
|
161
155
|
<div class="flex items-center gap-2">
|
|
162
156
|
<span class="flex size-6 items-center justify-center rounded-md bg-slate-100 text-slate-500">
|
|
163
157
|
<Activity :size="12" />
|
|
164
158
|
</span>
|
|
165
|
-
<h3 class="text-sm font-semibold">
|
|
159
|
+
<h3 class="text-sm font-semibold">{{ t("integration.dashboard.runs.title") }}</h3>
|
|
166
160
|
</div>
|
|
167
161
|
<NuxtLink to="/integrations/runs" class="text-sm text-muted-foreground hover:text-foreground inline-flex items-center gap-1">
|
|
168
|
-
|
|
162
|
+
{{ t("integration.common.action.viewAll") }} <ArrowRight :size="11" />
|
|
169
163
|
</NuxtLink>
|
|
170
164
|
</div>
|
|
171
165
|
<div>
|
|
@@ -180,7 +174,7 @@ const surfaces = [
|
|
|
180
174
|
class="grid grid-cols-[90px_minmax(0,1fr)_100px_70px] items-center gap-2 border-b border-black/[0.04] px-3 py-2 text-sm last:border-b-0 hover:bg-muted/30"
|
|
181
175
|
>
|
|
182
176
|
<Badge variant="outline" :class="['border text-xs font-medium uppercase', statusClass(r.status)]">
|
|
183
|
-
{{ r.status }}
|
|
177
|
+
{{ runStatusLabel(r.status) }}
|
|
184
178
|
</Badge>
|
|
185
179
|
<div class="truncate">
|
|
186
180
|
<div class="font-medium text-foreground truncate">{{ r.workflowName }}</div>
|
|
@@ -209,19 +203,19 @@ const surfaces = [
|
|
|
209
203
|
</div>
|
|
210
204
|
</template>
|
|
211
205
|
<div v-else-if="!latestPerWorkflow.length" class="px-3 py-8 text-center text-sm text-muted-foreground">
|
|
212
|
-
|
|
206
|
+
{{ t("integration.dashboard.runs.empty") }}
|
|
213
207
|
</div>
|
|
214
208
|
</div>
|
|
215
209
|
</div>
|
|
216
210
|
|
|
217
211
|
<!-- Needs attention -->
|
|
218
|
-
<div role="group" aria-label="
|
|
212
|
+
<div role="group" :aria-label="t('integration.dashboard.attention.title')" class="rounded-lg border border-black/[0.06] bg-white">
|
|
219
213
|
<div class="flex items-center justify-between border-b border-black/[0.06] px-3 py-2">
|
|
220
214
|
<div class="flex items-center gap-2">
|
|
221
215
|
<span class="flex size-6 items-center justify-center rounded-md bg-slate-100 text-slate-500">
|
|
222
216
|
<AlertTriangle :size="12" />
|
|
223
217
|
</span>
|
|
224
|
-
<h3 class="text-sm font-semibold">
|
|
218
|
+
<h3 class="text-sm font-semibold">{{ t("integration.dashboard.attention.title") }}</h3>
|
|
225
219
|
</div>
|
|
226
220
|
</div>
|
|
227
221
|
<div v-if="(missedLoading || latestLoading) && !missedRuns && !failedRecent.length" class="space-y-2 p-3">
|
|
@@ -234,9 +228,9 @@ const surfaces = [
|
|
|
234
228
|
to="/integrations/runs?tab=missed"
|
|
235
229
|
class="block space-y-1 border-b border-black/[0.04] px-3 py-2 text-sm hover:bg-muted/30"
|
|
236
230
|
>
|
|
237
|
-
<div class="font-medium">{{ missedRuns
|
|
231
|
+
<div class="font-medium">{{ t("integration.dashboard.attention.missedRuns", missedRuns) }}</div>
|
|
238
232
|
<div class="text-sm text-amber-700">
|
|
239
|
-
|
|
233
|
+
{{ t("integration.dashboard.attention.missedRuns.hint") }}
|
|
240
234
|
</div>
|
|
241
235
|
</NuxtLink>
|
|
242
236
|
<!-- A link, for the same reason as the entries next door — and like
|
|
@@ -249,7 +243,7 @@ const surfaces = [
|
|
|
249
243
|
>
|
|
250
244
|
<div class="font-medium truncate">{{ r.workflowName }}</div>
|
|
251
245
|
<div class="text-sm text-rose-700 truncate" :title="r.errorMessage">
|
|
252
|
-
{{ r.errorMessage ?? "
|
|
246
|
+
{{ r.errorMessage ?? t("integration.dashboard.attention.failed") }}
|
|
253
247
|
</div>
|
|
254
248
|
<!-- When it failed is when it ended, not when it set off: a run
|
|
255
249
|
that broke after an hour would otherwise be dated an hour early. -->
|
|
@@ -259,7 +253,7 @@ const surfaces = [
|
|
|
259
253
|
</NuxtLink>
|
|
260
254
|
</div>
|
|
261
255
|
<div v-else class="px-3 py-6 text-center text-sm text-muted-foreground">
|
|
262
|
-
|
|
256
|
+
{{ t("integration.dashboard.attention.healthy") }}
|
|
263
257
|
</div>
|
|
264
258
|
</div>
|
|
265
259
|
</div>
|
|
@@ -290,16 +284,16 @@ const surfaces = [
|
|
|
290
284
|
produced this list compared nothing at all. The wording deliberately
|
|
291
285
|
echoes the create-workflow dialog's "Start from template" tab — the same
|
|
292
286
|
offer, so the same words, rather than a second name for it. -->
|
|
293
|
-
<div role="group" aria-label="
|
|
287
|
+
<div role="group" :aria-label="t('integration.dashboard.templates.title')" class="rounded-lg border border-black/[0.06] bg-white">
|
|
294
288
|
<div class="flex items-center justify-between border-b border-black/[0.06] px-3 py-2">
|
|
295
289
|
<div class="flex items-center gap-2">
|
|
296
290
|
<span class="flex size-6 items-center justify-center rounded-md bg-slate-100 text-slate-500">
|
|
297
291
|
<Sparkles :size="12" />
|
|
298
292
|
</span>
|
|
299
|
-
<h3 class="text-sm font-semibold">
|
|
293
|
+
<h3 class="text-sm font-semibold">{{ t("integration.dashboard.templates.title") }}</h3>
|
|
300
294
|
</div>
|
|
301
295
|
<NuxtLink to="/integrations/templates" class="text-sm text-muted-foreground hover:text-foreground inline-flex items-center gap-1">
|
|
302
|
-
|
|
296
|
+
{{ t("integration.common.action.seeAll") }} <ArrowRight :size="11" />
|
|
303
297
|
</NuxtLink>
|
|
304
298
|
</div>
|
|
305
299
|
<div v-if="templatesLoading && !startingPoints.length" class="grid gap-2 p-3 sm:grid-cols-2 lg:grid-cols-4">
|
|
@@ -310,7 +304,7 @@ const surfaces = [
|
|
|
310
304
|
</div>
|
|
311
305
|
</div>
|
|
312
306
|
<div v-else-if="!startingPoints.length" class="px-3 py-6 text-center text-sm text-muted-foreground">
|
|
313
|
-
|
|
307
|
+
{{ t("integration.dashboard.templates.empty") }}
|
|
314
308
|
</div>
|
|
315
309
|
<div v-else class="grid gap-2 p-3 sm:grid-cols-2 lg:grid-cols-4">
|
|
316
310
|
<TemplateCard
|