@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,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,10 @@ 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
|
-
|
|
445
|
+
<!-- What the credential type said, whichever way the test went. The fixed
|
|
446
|
+
sentences are the fallback for a type that reports nothing, not the
|
|
447
|
+
first choice (specs/credential-test.md). -->
|
|
448
|
+
{{ testResult.ok ? testResult.message ?? t("integration.credentials.shared.test.ok") : testResult.error ?? testResult.message ?? t("integration.credentials.shared.test.failed") }}
|
|
428
449
|
</div>
|
|
429
450
|
|
|
430
451
|
</div>
|
|
@@ -442,18 +463,18 @@ async function confirmDelete() {
|
|
|
442
463
|
<DialogFooter v-if="pickedType" class="sm:justify-between">
|
|
443
464
|
<Button v-if="!isEdit" variant="ghost" size="sm" :disabled="saving || testing" @click="pickedType = null;
|
|
444
465
|
clearRefusal()">
|
|
445
|
-
|
|
466
|
+
{{ t("integration.credentials.list.dialog.action.back") }}
|
|
446
467
|
</Button>
|
|
447
468
|
<span v-else />
|
|
448
469
|
<div class="flex items-center gap-2">
|
|
449
470
|
<Button variant="outline" :disabled="saving || testing" @click="runTest">
|
|
450
471
|
<Loader2 v-if="testing" :size="14" class="mr-1 animate-spin" />
|
|
451
472
|
<TestIcon v-else :size="14" class="mr-1" />
|
|
452
|
-
|
|
473
|
+
{{ t("integration.credentials.shared.action.testConnection") }}
|
|
453
474
|
</Button>
|
|
454
475
|
<Button :disabled="saving || !form.name.trim()" @click="submit">
|
|
455
476
|
<Loader2 v-if="saving" :size="14" class="mr-1 animate-spin" />
|
|
456
|
-
|
|
477
|
+
{{ t("integration.common.action.save") }}
|
|
457
478
|
</Button>
|
|
458
479
|
</div>
|
|
459
480
|
</DialogFooter>
|
|
@@ -465,21 +486,21 @@ async function confirmDelete() {
|
|
|
465
486
|
}">
|
|
466
487
|
<AlertDialogContent>
|
|
467
488
|
<AlertDialogHeader>
|
|
468
|
-
<AlertDialogTitle>
|
|
489
|
+
<AlertDialogTitle>{{ t("integration.credentials.shared.confirm.delete.title") }}</AlertDialogTitle>
|
|
469
490
|
<AlertDialogDescription>
|
|
470
491
|
<span v-if="pendingDelete">
|
|
471
492
|
<span class="font-medium text-foreground">{{ pendingDelete.name }}</span>
|
|
472
|
-
|
|
493
|
+
{{ t("integration.credentials.shared.confirm.delete.body") }}
|
|
473
494
|
</span>
|
|
474
495
|
</AlertDialogDescription>
|
|
475
496
|
</AlertDialogHeader>
|
|
476
497
|
<AlertDialogFooter>
|
|
477
498
|
<Button variant="outline" :disabled="deleting" @click="pendingDelete = null">
|
|
478
|
-
|
|
499
|
+
{{ t("integration.common.action.cancel") }}
|
|
479
500
|
</Button>
|
|
480
501
|
<Button class="bg-destructive text-white hover:bg-destructive/90" :disabled="deleting" @click="confirmDelete">
|
|
481
502
|
<Loader2 v-if="deleting" :size="14" class="animate-spin" />
|
|
482
|
-
{{ deleting ? "
|
|
503
|
+
{{ deleting ? t("integration.common.state.deleting") : t("integration.common.action.delete") }}
|
|
483
504
|
</Button>
|
|
484
505
|
</AlertDialogFooter>
|
|
485
506
|
</AlertDialogContent>
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, ref, watch } from "vue";
|
|
3
|
+
import SurfaceCard from "../../components/Integrations/SurfaceCard.vue";
|
|
3
4
|
import TemplateCard from "../../components/Integrations/TemplateCard.vue";
|
|
4
5
|
import WorkflowCreateDialog from "../../components/Integrations/WorkflowCreateDialog.vue";
|
|
5
6
|
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
7
|
import { Badge, Button } from "@revenexx/studio";
|
|
7
|
-
|
|
8
|
+
import { servedPaths } from "../../utils/servedPaths";
|
|
9
|
+
import { runStatusLabel, statusClass } from "../../utils/statusStyle";
|
|
10
|
+
const t = useIntegrationsT();
|
|
11
|
+
useHead({ title: () => t("integration.dashboard.head.title") });
|
|
8
12
|
const { workflows, activeCount: activeWorkflows, loading: workflowsLoading, load: loadWorkflows } = useIntegrationsWorkflows();
|
|
9
13
|
const { runs, runningCount, failedCount, loading: runsLoading, load: loadRuns } = useIntegrationsRuns();
|
|
10
14
|
const { startingPoints, loading: templatesLoading, load: loadTemplates, ensureDetails } = useIntegrationsTemplates();
|
|
11
|
-
watch(() => startingPoints.value.map((
|
|
12
|
-
if (startingPoints.value.length) void ensureDetails(startingPoints.value.map((
|
|
15
|
+
watch(() => startingPoints.value.map((t2) => t2.slug).join(","), () => {
|
|
16
|
+
if (startingPoints.value.length) void ensureDetails(startingPoints.value.map((t2) => t2.slug));
|
|
13
17
|
}, { immediate: true });
|
|
14
18
|
const { activeCount: schedulesActive, load: loadSchedules } = useIntegrationsSchedules();
|
|
15
19
|
const { pendingCount: missedRuns, globalLoading: missedLoading, loadGlobal: loadMissedRuns } = useIntegrationsDeadLetters();
|
|
@@ -39,80 +43,77 @@ const failedRecent = computed(() => latestByWorkflow.value.filter((r) => r.statu
|
|
|
39
43
|
function formatRelative(iso) {
|
|
40
44
|
if (!iso) return "\u2014";
|
|
41
45
|
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
|
-
}
|
|
46
|
+
if (ms < 6e4) return t("integration.common.time.justNow");
|
|
47
|
+
if (ms < 36e5) return t("integration.common.time.minutesAgo", Math.floor(ms / 6e4));
|
|
48
|
+
if (ms < 864e5) return t("integration.common.time.hoursAgo", Math.floor(ms / 36e5));
|
|
49
|
+
return t("integration.common.time.daysAgo", Math.floor(ms / 864e5));
|
|
60
50
|
}
|
|
61
51
|
const kpis = computed(() => [
|
|
62
52
|
{
|
|
63
|
-
|
|
53
|
+
key: "activeWorkflows",
|
|
54
|
+
label: t("integration.dashboard.kpi.activeWorkflows.label"),
|
|
64
55
|
value: activeWorkflows.value,
|
|
65
|
-
|
|
56
|
+
// A plural key, so the count is the second argument. Called without one it
|
|
57
|
+
// would put the raw "a | b" on screen.
|
|
58
|
+
sub: t("integration.dashboard.kpi.activeWorkflows.sub", workflows.value.length - activeWorkflows.value),
|
|
66
59
|
icon: Workflow,
|
|
67
60
|
tile: "bg-emerald-500 text-white"
|
|
68
61
|
},
|
|
69
62
|
{
|
|
70
|
-
|
|
63
|
+
key: "runningNow",
|
|
64
|
+
label: t("integration.dashboard.kpi.runningNow.label"),
|
|
71
65
|
value: runningCount.value,
|
|
72
|
-
sub: "
|
|
66
|
+
sub: t("integration.dashboard.kpi.runningNow.sub"),
|
|
73
67
|
icon: Activity,
|
|
74
68
|
tile: "bg-sky-500 text-white"
|
|
75
69
|
},
|
|
76
70
|
{
|
|
77
|
-
|
|
71
|
+
key: "scheduled",
|
|
72
|
+
label: t("integration.dashboard.kpi.scheduled.label"),
|
|
78
73
|
value: schedulesActive.value,
|
|
79
|
-
sub: "scheduled
|
|
74
|
+
sub: t("integration.dashboard.kpi.scheduled.sub"),
|
|
80
75
|
icon: Clock,
|
|
81
76
|
tile: "bg-violet-500 text-white"
|
|
82
77
|
},
|
|
83
78
|
{
|
|
84
|
-
|
|
79
|
+
key: "missedRuns",
|
|
80
|
+
label: t("integration.dashboard.kpi.missedRuns.label"),
|
|
85
81
|
value: missedRuns.value,
|
|
86
|
-
sub: "
|
|
82
|
+
sub: t("integration.dashboard.kpi.missedRuns.sub"),
|
|
87
83
|
icon: AlertTriangle,
|
|
88
84
|
tile: missedRuns.value ? "bg-amber-500 text-white" : "bg-slate-300 text-white",
|
|
89
85
|
to: "/integrations/runs?tab=missed"
|
|
90
86
|
}
|
|
91
87
|
]);
|
|
92
|
-
const surfaces = [
|
|
93
|
-
{ label: "
|
|
94
|
-
{ label: "
|
|
95
|
-
{ label: "
|
|
96
|
-
{ label: "
|
|
97
|
-
];
|
|
88
|
+
const surfaces = computed(() => [
|
|
89
|
+
{ label: t("integration.dashboard.surface.io.label"), to: "/integrations/io", icon: Database, hint: t("integration.dashboard.surface.io.hint") },
|
|
90
|
+
{ label: t("integration.dashboard.surface.sftp.label"), to: "/integrations/sftp", icon: Boxes, hint: t("integration.dashboard.surface.sftp.hint") },
|
|
91
|
+
{ label: t("integration.dashboard.surface.events.label"), to: "/integrations/events", icon: Radio, hint: t("integration.dashboard.surface.events.hint") },
|
|
92
|
+
{ label: t("integration.dashboard.surface.webhooks.label"), to: "/integrations/webhooks", icon: Webhook, hint: t("integration.dashboard.surface.webhooks.hint") }
|
|
93
|
+
]);
|
|
94
|
+
const router = useRouter();
|
|
95
|
+
function surfaceCards() {
|
|
96
|
+
const served = servedPaths(router.getRoutes().map((route) => route.path));
|
|
97
|
+
return surfaces.value.map((sfc) => ({ ...sfc, available: served.has(sfc.to) }));
|
|
98
|
+
}
|
|
98
99
|
</script>
|
|
99
100
|
|
|
100
101
|
<template>
|
|
101
102
|
<IntegrationsLayout>
|
|
102
103
|
<PageHeader
|
|
103
|
-
title="
|
|
104
|
-
description="
|
|
104
|
+
:title="t('integration.dashboard.header.title')"
|
|
105
|
+
:description="t('integration.dashboard.header.description')"
|
|
105
106
|
>
|
|
106
107
|
<template #actions>
|
|
107
108
|
<NuxtLink to="/integrations/templates">
|
|
108
109
|
<Button variant="outline" class="h-7 text-sm">
|
|
109
110
|
<Layers :size="12" />
|
|
110
|
-
|
|
111
|
+
{{ t("integration.dashboard.action.browseTemplates") }}
|
|
111
112
|
</Button>
|
|
112
113
|
</NuxtLink>
|
|
113
114
|
<Button class="h-7 text-sm" @click="createOpen = true">
|
|
114
115
|
<Plus :size="14" />
|
|
115
|
-
|
|
116
|
+
{{ t("integration.dashboard.action.newWorkflow") }}
|
|
116
117
|
</Button>
|
|
117
118
|
</template>
|
|
118
119
|
</PageHeader>
|
|
@@ -122,11 +123,11 @@ const surfaces = [
|
|
|
122
123
|
tile is a link, and a link takes its name from its label instead of its
|
|
123
124
|
contents — named "Missed runs" alone it would announce less than the
|
|
124
125
|
three tiles beside it. -->
|
|
125
|
-
<div role="group" aria-label="
|
|
126
|
+
<div role="group" :aria-label="t('integration.dashboard.kpi.aria.group')" class="grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
|
|
126
127
|
<component
|
|
127
128
|
:is="k.to ? NuxtLinkComp : 'div'"
|
|
128
129
|
v-for="k in kpis"
|
|
129
|
-
:key="k.
|
|
130
|
+
:key="k.key"
|
|
130
131
|
:to="k.to"
|
|
131
132
|
:role="k.to ? void 0 : 'group'"
|
|
132
133
|
:aria-label="`${k.label}: ${k.value}`"
|
|
@@ -156,16 +157,16 @@ const surfaces = [
|
|
|
156
157
|
<!-- Two-column: recent runs + needs attention + templates to start from -->
|
|
157
158
|
<div class="grid gap-4 lg:grid-cols-3">
|
|
158
159
|
<!-- Recent runs ─ takes 2 cols -->
|
|
159
|
-
<div role="group" aria-label="
|
|
160
|
+
<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
161
|
<div class="flex items-center justify-between border-b border-black/[0.06] px-3 py-2">
|
|
161
162
|
<div class="flex items-center gap-2">
|
|
162
163
|
<span class="flex size-6 items-center justify-center rounded-md bg-slate-100 text-slate-500">
|
|
163
164
|
<Activity :size="12" />
|
|
164
165
|
</span>
|
|
165
|
-
<h3 class="text-sm font-semibold">
|
|
166
|
+
<h3 class="text-sm font-semibold">{{ t("integration.dashboard.runs.title") }}</h3>
|
|
166
167
|
</div>
|
|
167
168
|
<NuxtLink to="/integrations/runs" class="text-sm text-muted-foreground hover:text-foreground inline-flex items-center gap-1">
|
|
168
|
-
|
|
169
|
+
{{ t("integration.common.action.viewAll") }} <ArrowRight :size="11" />
|
|
169
170
|
</NuxtLink>
|
|
170
171
|
</div>
|
|
171
172
|
<div>
|
|
@@ -180,7 +181,7 @@ const surfaces = [
|
|
|
180
181
|
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
182
|
>
|
|
182
183
|
<Badge variant="outline" :class="['border text-xs font-medium uppercase', statusClass(r.status)]">
|
|
183
|
-
{{ r.status }}
|
|
184
|
+
{{ runStatusLabel(r.status) }}
|
|
184
185
|
</Badge>
|
|
185
186
|
<div class="truncate">
|
|
186
187
|
<div class="font-medium text-foreground truncate">{{ r.workflowName }}</div>
|
|
@@ -209,19 +210,19 @@ const surfaces = [
|
|
|
209
210
|
</div>
|
|
210
211
|
</template>
|
|
211
212
|
<div v-else-if="!latestPerWorkflow.length" class="px-3 py-8 text-center text-sm text-muted-foreground">
|
|
212
|
-
|
|
213
|
+
{{ t("integration.dashboard.runs.empty") }}
|
|
213
214
|
</div>
|
|
214
215
|
</div>
|
|
215
216
|
</div>
|
|
216
217
|
|
|
217
218
|
<!-- Needs attention -->
|
|
218
|
-
<div role="group" aria-label="
|
|
219
|
+
<div role="group" :aria-label="t('integration.dashboard.attention.title')" class="rounded-lg border border-black/[0.06] bg-white">
|
|
219
220
|
<div class="flex items-center justify-between border-b border-black/[0.06] px-3 py-2">
|
|
220
221
|
<div class="flex items-center gap-2">
|
|
221
222
|
<span class="flex size-6 items-center justify-center rounded-md bg-slate-100 text-slate-500">
|
|
222
223
|
<AlertTriangle :size="12" />
|
|
223
224
|
</span>
|
|
224
|
-
<h3 class="text-sm font-semibold">
|
|
225
|
+
<h3 class="text-sm font-semibold">{{ t("integration.dashboard.attention.title") }}</h3>
|
|
225
226
|
</div>
|
|
226
227
|
</div>
|
|
227
228
|
<div v-if="(missedLoading || latestLoading) && !missedRuns && !failedRecent.length" class="space-y-2 p-3">
|
|
@@ -234,9 +235,9 @@ const surfaces = [
|
|
|
234
235
|
to="/integrations/runs?tab=missed"
|
|
235
236
|
class="block space-y-1 border-b border-black/[0.04] px-3 py-2 text-sm hover:bg-muted/30"
|
|
236
237
|
>
|
|
237
|
-
<div class="font-medium">{{ missedRuns
|
|
238
|
+
<div class="font-medium">{{ t("integration.dashboard.attention.missedRuns", missedRuns) }}</div>
|
|
238
239
|
<div class="text-sm text-amber-700">
|
|
239
|
-
|
|
240
|
+
{{ t("integration.dashboard.attention.missedRuns.hint") }}
|
|
240
241
|
</div>
|
|
241
242
|
</NuxtLink>
|
|
242
243
|
<!-- A link, for the same reason as the entries next door — and like
|
|
@@ -249,7 +250,7 @@ const surfaces = [
|
|
|
249
250
|
>
|
|
250
251
|
<div class="font-medium truncate">{{ r.workflowName }}</div>
|
|
251
252
|
<div class="text-sm text-rose-700 truncate" :title="r.errorMessage">
|
|
252
|
-
{{ r.errorMessage ?? "
|
|
253
|
+
{{ r.errorMessage ?? t("integration.dashboard.attention.failed") }}
|
|
253
254
|
</div>
|
|
254
255
|
<!-- When it failed is when it ended, not when it set off: a run
|
|
255
256
|
that broke after an hour would otherwise be dated an hour early. -->
|
|
@@ -259,30 +260,22 @@ const surfaces = [
|
|
|
259
260
|
</NuxtLink>
|
|
260
261
|
</div>
|
|
261
262
|
<div v-else class="px-3 py-6 text-center text-sm text-muted-foreground">
|
|
262
|
-
|
|
263
|
+
{{ t("integration.dashboard.attention.healthy") }}
|
|
263
264
|
</div>
|
|
264
265
|
</div>
|
|
265
266
|
</div>
|
|
266
267
|
|
|
267
|
-
<!-- Data exchange & events —
|
|
268
|
+
<!-- Data exchange & events — surfaces beyond workflows, and the only cards
|
|
269
|
+
here that lead OUT of this module: all four are the cockpit's pages.
|
|
270
|
+
Where the host does not have them the card stays and says so, rather
|
|
271
|
+
than leading into a 404 (PO-223, `SurfaceCard.vue`). -->
|
|
268
272
|
<div class="grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
|
|
269
|
-
<
|
|
270
|
-
v-for="sfc in
|
|
273
|
+
<SurfaceCard
|
|
274
|
+
v-for="sfc in surfaceCards()"
|
|
271
275
|
:key="sfc.to"
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
<div class="flex items-center gap-2">
|
|
276
|
-
<span class="flex size-6 items-center justify-center rounded-md bg-slate-100 text-slate-500">
|
|
277
|
-
<component :is="sfc.icon" :size="12" />
|
|
278
|
-
</span>
|
|
279
|
-
<span class="min-w-0 flex-1 truncate text-sm font-semibold">{{ sfc.label }}</span>
|
|
280
|
-
<ArrowRight :size="11" class="text-muted-foreground transition-transform group-hover:translate-x-0.5" />
|
|
281
|
-
</div>
|
|
282
|
-
<p class="mt-2 text-sm text-muted-foreground">
|
|
283
|
-
{{ sfc.hint }}
|
|
284
|
-
</p>
|
|
285
|
-
</NuxtLink>
|
|
276
|
+
v-bind="sfc"
|
|
277
|
+
:note="t('integration.dashboard.surface.unavailable', { surface: sfc.label })"
|
|
278
|
+
/>
|
|
286
279
|
</div>
|
|
287
280
|
|
|
288
281
|
<!-- Named for the offer it makes, not for a ranking nothing measures
|
|
@@ -290,16 +283,16 @@ const surfaces = [
|
|
|
290
283
|
produced this list compared nothing at all. The wording deliberately
|
|
291
284
|
echoes the create-workflow dialog's "Start from template" tab — the same
|
|
292
285
|
offer, so the same words, rather than a second name for it. -->
|
|
293
|
-
<div role="group" aria-label="
|
|
286
|
+
<div role="group" :aria-label="t('integration.dashboard.templates.title')" class="rounded-lg border border-black/[0.06] bg-white">
|
|
294
287
|
<div class="flex items-center justify-between border-b border-black/[0.06] px-3 py-2">
|
|
295
288
|
<div class="flex items-center gap-2">
|
|
296
289
|
<span class="flex size-6 items-center justify-center rounded-md bg-slate-100 text-slate-500">
|
|
297
290
|
<Sparkles :size="12" />
|
|
298
291
|
</span>
|
|
299
|
-
<h3 class="text-sm font-semibold">
|
|
292
|
+
<h3 class="text-sm font-semibold">{{ t("integration.dashboard.templates.title") }}</h3>
|
|
300
293
|
</div>
|
|
301
294
|
<NuxtLink to="/integrations/templates" class="text-sm text-muted-foreground hover:text-foreground inline-flex items-center gap-1">
|
|
302
|
-
|
|
295
|
+
{{ t("integration.common.action.seeAll") }} <ArrowRight :size="11" />
|
|
303
296
|
</NuxtLink>
|
|
304
297
|
</div>
|
|
305
298
|
<div v-if="templatesLoading && !startingPoints.length" class="grid gap-2 p-3 sm:grid-cols-2 lg:grid-cols-4">
|
|
@@ -310,7 +303,7 @@ const surfaces = [
|
|
|
310
303
|
</div>
|
|
311
304
|
</div>
|
|
312
305
|
<div v-else-if="!startingPoints.length" class="px-3 py-6 text-center text-sm text-muted-foreground">
|
|
313
|
-
|
|
306
|
+
{{ t("integration.dashboard.templates.empty") }}
|
|
314
307
|
</div>
|
|
315
308
|
<div v-else class="grid gap-2 p-3 sm:grid-cols-2 lg:grid-cols-4">
|
|
316
309
|
<TemplateCard
|