@revenexx/studio-integrations 0.1.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/README.md +51 -0
- package/dist/module.d.mts +6 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +45 -0
- package/dist/runtime/components/Integrations/Layout.d.vue.ts +12 -0
- package/dist/runtime/components/Integrations/Layout.vue +20 -0
- package/dist/runtime/components/Integrations/Layout.vue.d.ts +12 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +16 -0
- package/dist/runtime/components/Integrations/NodeInspector.vue +450 -0
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +16 -0
- package/dist/runtime/components/Integrations/NodePalette.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/NodePalette.vue +140 -0
- package/dist/runtime/components/Integrations/NodePalette.vue.d.ts +2 -0
- package/dist/runtime/components/Integrations/RunDetailDialog.d.vue.ts +11 -0
- package/dist/runtime/components/Integrations/RunDetailDialog.vue +169 -0
- package/dist/runtime/components/Integrations/RunDetailDialog.vue.d.ts +11 -0
- package/dist/runtime/components/Integrations/Sidebar.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/Sidebar.vue +32 -0
- package/dist/runtime/components/Integrations/Sidebar.vue.d.ts +2 -0
- package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +23 -0
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +86 -0
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +23 -0
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +14 -0
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +419 -0
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +14 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +11 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +200 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +11 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.d.vue.ts +8 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +82 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue.d.ts +8 -0
- package/dist/runtime/components/Integrations/WorkflowsTable.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +268 -0
- package/dist/runtime/components/Integrations/WorkflowsTable.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/workflowLayout.d.ts +17 -0
- package/dist/runtime/components/Integrations/workflowLayout.js +29 -0
- package/dist/runtime/composables/integrationsApi.d.ts +13 -0
- package/dist/runtime/composables/integrationsApi.js +55 -0
- package/dist/runtime/composables/useCredentialTypes.d.ts +19 -0
- package/dist/runtime/composables/useCredentialTypes.js +47 -0
- package/dist/runtime/composables/useIntegrationsAudit.d.ts +26 -0
- package/dist/runtime/composables/useIntegrationsAudit.js +39 -0
- package/dist/runtime/composables/useIntegrationsCredentials.d.ts +43 -0
- package/dist/runtime/composables/useIntegrationsCredentials.js +98 -0
- package/dist/runtime/composables/useIntegrationsFocus.d.ts +16 -0
- package/dist/runtime/composables/useIntegrationsFocus.js +17 -0
- package/dist/runtime/composables/useIntegrationsNodes.d.ts +6 -0
- package/dist/runtime/composables/useIntegrationsNodes.js +32 -0
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +31 -0
- package/dist/runtime/composables/useIntegrationsRuns.js +109 -0
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +14 -0
- package/dist/runtime/composables/useIntegrationsSchedules.js +32 -0
- package/dist/runtime/composables/useIntegrationsSecrets.d.ts +27 -0
- package/dist/runtime/composables/useIntegrationsSecrets.js +76 -0
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +19 -0
- package/dist/runtime/composables/useIntegrationsTemplates.js +141 -0
- package/dist/runtime/composables/useIntegrationsTriggers.d.ts +39 -0
- package/dist/runtime/composables/useIntegrationsTriggers.js +39 -0
- package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +28 -0
- package/dist/runtime/composables/useIntegrationsWorkflows.js +152 -0
- package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
- package/dist/runtime/data/integrationNodeCatalog.js +67 -0
- package/dist/runtime/data/integrationRuns.d.ts +7 -0
- package/dist/runtime/data/integrationRuns.js +109 -0
- package/dist/runtime/data/integrationTemplates.d.ts +11 -0
- package/dist/runtime/data/integrationTemplates.js +9 -0
- package/dist/runtime/data/integrationWorkflowSamples.d.ts +18 -0
- package/dist/runtime/data/integrationWorkflowSamples.js +318 -0
- package/dist/runtime/pages/integrations/audit.d.vue.ts +2 -0
- package/dist/runtime/pages/integrations/audit.vue +156 -0
- package/dist/runtime/pages/integrations/audit.vue.d.ts +2 -0
- package/dist/runtime/pages/integrations/credentials/oauth-complete.d.vue.ts +2 -0
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +28 -0
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue.d.ts +2 -0
- package/dist/runtime/pages/integrations/credentials.d.vue.ts +2 -0
- package/dist/runtime/pages/integrations/credentials.vue +403 -0
- package/dist/runtime/pages/integrations/credentials.vue.d.ts +2 -0
- package/dist/runtime/pages/integrations/index.d.vue.ts +2 -0
- package/dist/runtime/pages/integrations/index.vue +226 -0
- package/dist/runtime/pages/integrations/index.vue.d.ts +2 -0
- package/dist/runtime/pages/integrations/runs.d.vue.ts +2 -0
- package/dist/runtime/pages/integrations/runs.vue +100 -0
- package/dist/runtime/pages/integrations/runs.vue.d.ts +2 -0
- package/dist/runtime/pages/integrations/schedules.d.vue.ts +2 -0
- package/dist/runtime/pages/integrations/schedules.vue +111 -0
- package/dist/runtime/pages/integrations/schedules.vue.d.ts +2 -0
- package/dist/runtime/pages/integrations/secrets.d.vue.ts +2 -0
- package/dist/runtime/pages/integrations/secrets.vue +248 -0
- package/dist/runtime/pages/integrations/secrets.vue.d.ts +2 -0
- package/dist/runtime/pages/integrations/workflows/[id].d.vue.ts +2 -0
- package/dist/runtime/pages/integrations/workflows/[id].vue +326 -0
- package/dist/runtime/pages/integrations/workflows/[id].vue.d.ts +2 -0
- package/dist/runtime/pages/integrations/workflows/index.d.vue.ts +2 -0
- package/dist/runtime/pages/integrations/workflows/index.vue +31 -0
- package/dist/runtime/pages/integrations/workflows/index.vue.d.ts +2 -0
- package/dist/runtime/pages/integrations/workflows/new.d.vue.ts +2 -0
- package/dist/runtime/pages/integrations/workflows/new.vue +153 -0
- package/dist/runtime/pages/integrations/workflows/new.vue.d.ts +2 -0
- package/dist/runtime/types/integrations.d.ts +427 -0
- package/dist/runtime/types/integrations.js +0 -0
- package/dist/runtime/utils/localize.d.ts +9 -0
- package/dist/runtime/utils/localize.js +21 -0
- package/dist/runtime/utils/statusStyle.d.ts +15 -0
- package/dist/runtime/utils/statusStyle.js +78 -0
- package/dist/types.d.mts +9 -0
- package/package.json +46 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, onMounted, reactive, ref } from "vue";
|
|
3
|
+
import {
|
|
4
|
+
CheckCircle as CheckCircle2,
|
|
5
|
+
RefreshCircle as Loader2,
|
|
6
|
+
Magnifier as Search,
|
|
7
|
+
AddCircle as Plus,
|
|
8
|
+
TrashBinTrash as Trash2,
|
|
9
|
+
TestTube as TestIcon,
|
|
10
|
+
ShieldKeyhole as KeyIcon
|
|
11
|
+
} from "#solar-icons";
|
|
12
|
+
import {
|
|
13
|
+
Badge,
|
|
14
|
+
Button,
|
|
15
|
+
Dialog,
|
|
16
|
+
DialogContent,
|
|
17
|
+
DialogDescription,
|
|
18
|
+
DialogFooter,
|
|
19
|
+
DialogHeader,
|
|
20
|
+
DialogTitle,
|
|
21
|
+
Input,
|
|
22
|
+
Label,
|
|
23
|
+
Switch,
|
|
24
|
+
Select,
|
|
25
|
+
SelectContent,
|
|
26
|
+
SelectItem,
|
|
27
|
+
SelectTrigger,
|
|
28
|
+
SelectValue
|
|
29
|
+
} from "@revenexx/studio";
|
|
30
|
+
import { toast } from "vue-sonner";
|
|
31
|
+
useHead({ title: "Credentials \xB7 Revenexx Cockpit" });
|
|
32
|
+
const { credentials, loading, error, load, create, update, remove, testInline, test, oauthAuthorizeUrl } = useIntegrationsCredentials();
|
|
33
|
+
const { types, load: loadTypes, bySlug } = useCredentialTypes();
|
|
34
|
+
onMounted(async () => {
|
|
35
|
+
await Promise.all([load(), loadTypes()]);
|
|
36
|
+
});
|
|
37
|
+
const search = ref("");
|
|
38
|
+
const filtered = computed(() => {
|
|
39
|
+
const q = search.value.trim().toLowerCase();
|
|
40
|
+
const list = [...credentials.value].sort((a, b) => a.name.localeCompare(b.name));
|
|
41
|
+
if (!q) return list;
|
|
42
|
+
return list.filter((c) => c.name.toLowerCase().includes(q) || c.credential_type_slug.toLowerCase().includes(q));
|
|
43
|
+
});
|
|
44
|
+
function typeFor(slug) {
|
|
45
|
+
return bySlug(slug);
|
|
46
|
+
}
|
|
47
|
+
function loc(v) {
|
|
48
|
+
if (!v) return "";
|
|
49
|
+
if (typeof v === "string") return v;
|
|
50
|
+
const o = v;
|
|
51
|
+
return o.en ?? o.de ?? Object.values(o)[0] ?? "";
|
|
52
|
+
}
|
|
53
|
+
function typeName(slug) {
|
|
54
|
+
return loc(typeFor(slug)?.name) || slug;
|
|
55
|
+
}
|
|
56
|
+
function isOAuthAuthcode(t) {
|
|
57
|
+
return t?.auth_kind === "oauth2-authcode";
|
|
58
|
+
}
|
|
59
|
+
function formatDateTime(iso) {
|
|
60
|
+
if (!iso) return "\u2014";
|
|
61
|
+
try {
|
|
62
|
+
return new Date(iso).toLocaleString(void 0, { month: "short", day: "2-digit", hour: "2-digit", minute: "2-digit" });
|
|
63
|
+
} catch {
|
|
64
|
+
return iso;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const dialogOpen = ref(false);
|
|
68
|
+
const editing = ref(null);
|
|
69
|
+
const pickedType = ref(null);
|
|
70
|
+
const form = reactive({ name: "", config: {} });
|
|
71
|
+
const saving = ref(false);
|
|
72
|
+
const testing = ref(false);
|
|
73
|
+
const testResult = ref(null);
|
|
74
|
+
const isEdit = computed(() => editing.value !== null);
|
|
75
|
+
function fieldsOf(t) {
|
|
76
|
+
return t?.fields ?? [];
|
|
77
|
+
}
|
|
78
|
+
function openCreate(t) {
|
|
79
|
+
editing.value = null;
|
|
80
|
+
pickedType.value = t ?? null;
|
|
81
|
+
form.name = "";
|
|
82
|
+
form.config = {};
|
|
83
|
+
testResult.value = null;
|
|
84
|
+
dialogOpen.value = true;
|
|
85
|
+
}
|
|
86
|
+
function openEdit(c) {
|
|
87
|
+
editing.value = c;
|
|
88
|
+
pickedType.value = typeFor(c.credential_type_slug);
|
|
89
|
+
form.name = c.name;
|
|
90
|
+
form.config = { ...c.public_config ?? {} };
|
|
91
|
+
testResult.value = null;
|
|
92
|
+
dialogOpen.value = true;
|
|
93
|
+
}
|
|
94
|
+
function buildConfigPayload() {
|
|
95
|
+
const out = {};
|
|
96
|
+
for (const f of fieldsOf(pickedType.value)) {
|
|
97
|
+
const v = form.config[f.key];
|
|
98
|
+
if (f.type === "secret" && isEdit.value && (v === void 0 || v === "")) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (v !== void 0) out[f.key] = v;
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
async function runTest() {
|
|
106
|
+
if (!pickedType.value) return;
|
|
107
|
+
testing.value = true;
|
|
108
|
+
testResult.value = null;
|
|
109
|
+
try {
|
|
110
|
+
testResult.value = isEdit.value && editing.value ? await test(editing.value.id) : await testInline(pickedType.value.slug, buildConfigPayload());
|
|
111
|
+
} finally {
|
|
112
|
+
testing.value = false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function submit() {
|
|
116
|
+
if (!pickedType.value || !form.name.trim()) return;
|
|
117
|
+
saving.value = true;
|
|
118
|
+
try {
|
|
119
|
+
if (isEdit.value && editing.value) {
|
|
120
|
+
await update(editing.value.id, { name: form.name.trim(), config: buildConfigPayload() });
|
|
121
|
+
toast.success("Credential updated");
|
|
122
|
+
} else {
|
|
123
|
+
const created = await create({
|
|
124
|
+
credential_type_slug: pickedType.value.slug,
|
|
125
|
+
name: form.name.trim(),
|
|
126
|
+
config: buildConfigPayload()
|
|
127
|
+
});
|
|
128
|
+
toast.success("Credential created");
|
|
129
|
+
if (isOAuthAuthcode(pickedType.value)) {
|
|
130
|
+
await connect(created);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
dialogOpen.value = false;
|
|
134
|
+
} catch (e) {
|
|
135
|
+
toast.error(e?.message ?? "Could not save credential.");
|
|
136
|
+
} finally {
|
|
137
|
+
saving.value = false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const connecting = ref(null);
|
|
141
|
+
async function connect(c) {
|
|
142
|
+
connecting.value = c.id;
|
|
143
|
+
try {
|
|
144
|
+
const authorizeUrl = await oauthAuthorizeUrl(c.id);
|
|
145
|
+
const popup = window.open(authorizeUrl, "oauth-connect", "width=600,height=720");
|
|
146
|
+
const onMessage = async (ev) => {
|
|
147
|
+
if (ev.data?.type === "integrations-oauth-complete") {
|
|
148
|
+
window.removeEventListener("message", onMessage);
|
|
149
|
+
await load(true);
|
|
150
|
+
toast.success("Connected");
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
window.addEventListener("message", onMessage);
|
|
154
|
+
const poll = window.setInterval(async () => {
|
|
155
|
+
if (popup?.closed) {
|
|
156
|
+
window.clearInterval(poll);
|
|
157
|
+
window.removeEventListener("message", onMessage);
|
|
158
|
+
await load(true);
|
|
159
|
+
}
|
|
160
|
+
}, 1e3);
|
|
161
|
+
} catch (e) {
|
|
162
|
+
toast.error(e?.message ?? "Could not start OAuth connect.");
|
|
163
|
+
} finally {
|
|
164
|
+
connecting.value = null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const cardTesting = ref(null);
|
|
168
|
+
async function testCard(c) {
|
|
169
|
+
cardTesting.value = c.id;
|
|
170
|
+
try {
|
|
171
|
+
const res = await test(c.id);
|
|
172
|
+
if (res.ok) toast.success(`${c.name}: connection ok`);
|
|
173
|
+
else toast.error(`${c.name}: ${res.error ?? "connection failed"}`);
|
|
174
|
+
} finally {
|
|
175
|
+
cardTesting.value = null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function destroy(c) {
|
|
179
|
+
if (!confirm(`Delete credential "${c.name}"? Workflows referencing it will fail until re-bound.`)) return;
|
|
180
|
+
try {
|
|
181
|
+
await remove(c.id);
|
|
182
|
+
toast.success("Credential deleted");
|
|
183
|
+
} catch (e) {
|
|
184
|
+
toast.error(e?.message ?? "Could not delete credential.");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
</script>
|
|
188
|
+
|
|
189
|
+
<template>
|
|
190
|
+
<IntegrationsLayout>
|
|
191
|
+
<PageHeader
|
|
192
|
+
title="Credentials"
|
|
193
|
+
description="Typed, testable access data for the systems your workflows talk to — one per connection."
|
|
194
|
+
>
|
|
195
|
+
<template #actions>
|
|
196
|
+
<Button class="h-7 text-sm" :disabled="!types.length" @click="openCreate()">
|
|
197
|
+
<Plus :size="14" class="mr-1" />
|
|
198
|
+
New credential
|
|
199
|
+
</Button>
|
|
200
|
+
</template>
|
|
201
|
+
</PageHeader>
|
|
202
|
+
|
|
203
|
+
<div class="flex items-center justify-between gap-3 border-b border-black/[0.06] px-4 py-2">
|
|
204
|
+
<div class="text-sm text-muted-foreground">
|
|
205
|
+
{{ filtered.length }} credential{{ filtered.length === 1 ? "" : "s" }}
|
|
206
|
+
</div>
|
|
207
|
+
<div class="relative w-64">
|
|
208
|
+
<Search :size="12" class="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
209
|
+
<Input v-model="search" placeholder="Search…" class="h-7 pl-7 text-sm bg-white" />
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<div v-if="error" class="m-4 rounded-md border border-rose-500/20 bg-rose-500/5 px-3 py-2 text-sm text-rose-700">
|
|
214
|
+
{{ error }}
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div class="p-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
218
|
+
<div
|
|
219
|
+
v-for="c in filtered"
|
|
220
|
+
:key="c.id"
|
|
221
|
+
class="rounded-lg border border-black/[0.06] bg-white p-4 space-y-3 transition hover:shadow-sm"
|
|
222
|
+
>
|
|
223
|
+
<div class="flex items-start gap-3">
|
|
224
|
+
<span class="flex size-10 items-center justify-center rounded-md bg-slate-100 text-slate-500 shrink-0">
|
|
225
|
+
<KeyIcon :size="18" />
|
|
226
|
+
</span>
|
|
227
|
+
<div class="flex-1 min-w-0">
|
|
228
|
+
<div class="text-sm font-semibold truncate">
|
|
229
|
+
{{ c.name }}
|
|
230
|
+
</div>
|
|
231
|
+
<div class="text-sm text-muted-foreground truncate">
|
|
232
|
+
{{ typeName(c.credential_type_slug) }}
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
<Badge
|
|
236
|
+
v-if="c.status === 'active'"
|
|
237
|
+
variant="outline"
|
|
238
|
+
class="text-sm border bg-emerald-500/10 border-emerald-500/20 text-emerald-700 gap-1"
|
|
239
|
+
>
|
|
240
|
+
<CheckCircle2 :size="12" />
|
|
241
|
+
Active
|
|
242
|
+
</Badge>
|
|
243
|
+
<Badge
|
|
244
|
+
v-else
|
|
245
|
+
variant="outline"
|
|
246
|
+
class="text-sm border bg-amber-500/10 border-amber-500/20 text-amber-700"
|
|
247
|
+
>
|
|
248
|
+
Needs reconnect
|
|
249
|
+
</Badge>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<div class="flex items-center justify-between border-t border-black/[0.04] pt-2 text-sm text-muted-foreground">
|
|
253
|
+
<span>Updated {{ formatDateTime(c.updated_at) }}</span>
|
|
254
|
+
<div class="flex items-center gap-2">
|
|
255
|
+
<button
|
|
256
|
+
v-if="c.status === 'needs_reauth' && isOAuthAuthcode(typeFor(c.credential_type_slug))"
|
|
257
|
+
type="button"
|
|
258
|
+
class="text-amber-600 hover:underline"
|
|
259
|
+
:disabled="connecting === c.id"
|
|
260
|
+
@click="connect(c)"
|
|
261
|
+
>
|
|
262
|
+
Reconnect
|
|
263
|
+
</button>
|
|
264
|
+
<button type="button" class="text-violet-600 hover:underline disabled:opacity-50" :disabled="cardTesting === c.id" @click="testCard(c)">
|
|
265
|
+
<Loader2 v-if="cardTesting === c.id" :size="12" class="inline animate-spin" />
|
|
266
|
+
<span v-else>Test</span>
|
|
267
|
+
</button>
|
|
268
|
+
<button type="button" class="text-foreground/50 hover:text-foreground" @click="openEdit(c)">
|
|
269
|
+
Edit
|
|
270
|
+
</button>
|
|
271
|
+
<button type="button" class="text-foreground/40 hover:text-rose-600" @click="destroy(c)">
|
|
272
|
+
<Trash2 :size="13" />
|
|
273
|
+
</button>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<div v-if="!loading && !filtered.length" class="col-span-full px-3 py-12 text-center text-sm text-muted-foreground">
|
|
279
|
+
No credentials yet. Hit "New credential" to connect your first system.
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<!-- Create / edit dialog -->
|
|
284
|
+
<Dialog v-model:open="dialogOpen">
|
|
285
|
+
<DialogContent class="sm:max-w-lg">
|
|
286
|
+
<DialogHeader>
|
|
287
|
+
<DialogTitle>{{ isEdit ? "Edit credential" : "New credential" }}</DialogTitle>
|
|
288
|
+
<DialogDescription class="text-sm">
|
|
289
|
+
{{ isEdit ? "Update the credential. Leave secret fields blank to keep the current value." : "Pick a type and fill in its access data." }}
|
|
290
|
+
</DialogDescription>
|
|
291
|
+
</DialogHeader>
|
|
292
|
+
|
|
293
|
+
<!-- Step 1: type picker (create only) -->
|
|
294
|
+
<div v-if="!pickedType" class="grid grid-cols-2 gap-2 py-2 max-h-80 overflow-y-auto">
|
|
295
|
+
<button
|
|
296
|
+
v-for="t in types"
|
|
297
|
+
:key="t.slug"
|
|
298
|
+
type="button"
|
|
299
|
+
class="flex items-start gap-2 rounded-lg border border-black/[0.08] p-3 text-left transition hover:border-black/20 hover:bg-muted/30"
|
|
300
|
+
@click="pickedType = t;
|
|
301
|
+
form.name = loc(t.name)"
|
|
302
|
+
>
|
|
303
|
+
<span class="flex size-8 items-center justify-center rounded-md bg-slate-100 text-slate-500 shrink-0">
|
|
304
|
+
<KeyIcon :size="16" />
|
|
305
|
+
</span>
|
|
306
|
+
<div class="min-w-0">
|
|
307
|
+
<div class="text-sm font-semibold truncate">
|
|
308
|
+
{{ loc(t.name) }}
|
|
309
|
+
</div>
|
|
310
|
+
<div class="text-sm text-muted-foreground truncate">
|
|
311
|
+
{{ t.auth_kind }}
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
</button>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
<!-- Step 2: dynamic form -->
|
|
318
|
+
<div v-else class="space-y-3 py-1 max-h-[28rem] overflow-y-auto pr-1">
|
|
319
|
+
<div class="space-y-1.5">
|
|
320
|
+
<Label for="cred-name">Name</Label>
|
|
321
|
+
<Input id="cred-name" v-model="form.name" class="bg-white" placeholder="e.g. Production SMTP" />
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<div v-for="f in fieldsOf(pickedType)" :key="f.key" class="space-y-1.5">
|
|
325
|
+
<Label>{{ loc(f.label) }}{{ f.required ? " *" : "" }}</Label>
|
|
326
|
+
|
|
327
|
+
<!-- secret -->
|
|
328
|
+
<Input
|
|
329
|
+
v-if="f.type === 'secret'"
|
|
330
|
+
:model-value="form.config[f.key] ?? ''"
|
|
331
|
+
type="password"
|
|
332
|
+
autocomplete="new-password"
|
|
333
|
+
:placeholder="isEdit ? 'Leave blank to keep current' : ''"
|
|
334
|
+
class="bg-white"
|
|
335
|
+
@update:model-value="(v) => form.config[f.key] = v"
|
|
336
|
+
/>
|
|
337
|
+
<!-- number -->
|
|
338
|
+
<Input
|
|
339
|
+
v-else-if="f.type === 'number'"
|
|
340
|
+
type="number"
|
|
341
|
+
:model-value="form.config[f.key] ?? ''"
|
|
342
|
+
class="bg-white"
|
|
343
|
+
@update:model-value="(v) => form.config[f.key] = v === '' ? void 0 : Number(v)"
|
|
344
|
+
/>
|
|
345
|
+
<!-- boolean -->
|
|
346
|
+
<div v-else-if="f.type === 'boolean'" class="flex items-center gap-2 pt-1">
|
|
347
|
+
<Switch :model-value="Boolean(form.config[f.key])" @update:model-value="(v) => form.config[f.key] = v" />
|
|
348
|
+
</div>
|
|
349
|
+
<!-- select -->
|
|
350
|
+
<Select
|
|
351
|
+
v-else-if="f.type === 'select'"
|
|
352
|
+
:model-value="form.config[f.key] ?? ''"
|
|
353
|
+
@update:model-value="(v) => form.config[f.key] = v"
|
|
354
|
+
>
|
|
355
|
+
<SelectTrigger class="bg-white">
|
|
356
|
+
<SelectValue placeholder="Pick one" />
|
|
357
|
+
</SelectTrigger>
|
|
358
|
+
<SelectContent>
|
|
359
|
+
<SelectItem v-for="opt in f.options ?? []" :key="String(opt.value)" :value="String(opt.value)">
|
|
360
|
+
{{ loc(opt.label) }}
|
|
361
|
+
</SelectItem>
|
|
362
|
+
</SelectContent>
|
|
363
|
+
</Select>
|
|
364
|
+
<!-- string (default) -->
|
|
365
|
+
<Input
|
|
366
|
+
v-else
|
|
367
|
+
:model-value="form.config[f.key] ?? ''"
|
|
368
|
+
class="bg-white"
|
|
369
|
+
@update:model-value="(v) => form.config[f.key] = v"
|
|
370
|
+
/>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
<p v-if="isOAuthAuthcode(pickedType) && !isEdit" class="text-sm text-muted-foreground">
|
|
374
|
+
After saving, a window opens to connect with the provider.
|
|
375
|
+
</p>
|
|
376
|
+
|
|
377
|
+
<div
|
|
378
|
+
v-if="testResult"
|
|
379
|
+
class="rounded-md px-3 py-2 text-sm"
|
|
380
|
+
:class="testResult.ok ? 'bg-emerald-500/10 text-emerald-700' : 'bg-rose-500/10 text-rose-700'"
|
|
381
|
+
>
|
|
382
|
+
{{ testResult.ok ? "Connection ok." : testResult.error ?? "Connection failed." }}
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
|
|
386
|
+
<DialogFooter v-if="pickedType">
|
|
387
|
+
<Button v-if="!isEdit" variant="outline" :disabled="saving || testing" @click="pickedType = null">
|
|
388
|
+
Back
|
|
389
|
+
</Button>
|
|
390
|
+
<Button variant="outline" :disabled="saving || testing" @click="runTest">
|
|
391
|
+
<Loader2 v-if="testing" :size="14" class="mr-1 animate-spin" />
|
|
392
|
+
<TestIcon v-else :size="14" class="mr-1" />
|
|
393
|
+
Test
|
|
394
|
+
</Button>
|
|
395
|
+
<Button :disabled="saving || !form.name.trim()" @click="submit">
|
|
396
|
+
<Loader2 v-if="saving" :size="14" class="mr-1 animate-spin" />
|
|
397
|
+
{{ isEdit ? "Save" : isOAuthAuthcode(pickedType) ? "Save & connect" : "Create" }}
|
|
398
|
+
</Button>
|
|
399
|
+
</DialogFooter>
|
|
400
|
+
</DialogContent>
|
|
401
|
+
</Dialog>
|
|
402
|
+
</IntegrationsLayout>
|
|
403
|
+
</template>
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
export default _default;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
export default _default;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { Pulse as Activity, DangerTriangle as AlertTriangle, ArrowRight, ClockCircle as Clock, Layers, AddCircle as Plus, MagicStick as Sparkles, GraphUp as TrendingUp, Programming as Workflow } from "#solar-icons";
|
|
4
|
+
import { Badge, Button } from "@revenexx/studio";
|
|
5
|
+
useHead({ title: "Dashboard \xB7 Integration Studio" });
|
|
6
|
+
const { workflows, activeCount: activeWorkflows, load: loadWorkflows } = useIntegrationsWorkflows();
|
|
7
|
+
const { runs, runningCount, failedCount, load: loadRuns } = useIntegrationsRuns();
|
|
8
|
+
const { popular, load: loadTemplates } = useIntegrationsTemplates();
|
|
9
|
+
const { activeCount: schedulesActive, load: loadSchedules } = useIntegrationsSchedules();
|
|
10
|
+
onMounted(() => {
|
|
11
|
+
void loadWorkflows();
|
|
12
|
+
void loadRuns();
|
|
13
|
+
void loadTemplates();
|
|
14
|
+
void loadSchedules();
|
|
15
|
+
});
|
|
16
|
+
const recentRuns = computed(() => runs.value.slice(0, 6));
|
|
17
|
+
const failedRecent = computed(() => runs.value.filter((r) => r.status === "failed").slice(0, 3));
|
|
18
|
+
function formatRelative(iso) {
|
|
19
|
+
if (!iso) return "\u2014";
|
|
20
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
21
|
+
if (ms < 6e4) return "just now";
|
|
22
|
+
if (ms < 36e5) return `${Math.floor(ms / 6e4)}m ago`;
|
|
23
|
+
if (ms < 864e5) return `${Math.floor(ms / 36e5)}h ago`;
|
|
24
|
+
return `${Math.floor(ms / 864e5)}d ago`;
|
|
25
|
+
}
|
|
26
|
+
function statusClass(status) {
|
|
27
|
+
switch (status) {
|
|
28
|
+
case "succeeded":
|
|
29
|
+
return "bg-emerald-500/15 text-emerald-700 border-emerald-500/20";
|
|
30
|
+
case "running":
|
|
31
|
+
return "bg-sky-500/15 text-sky-700 border-sky-500/20";
|
|
32
|
+
case "failed":
|
|
33
|
+
return "bg-rose-500/15 text-rose-700 border-rose-500/20";
|
|
34
|
+
case "queued":
|
|
35
|
+
return "bg-amber-500/15 text-amber-700 border-amber-500/20";
|
|
36
|
+
case "cancelled":
|
|
37
|
+
return "bg-slate-500/15 text-slate-700 border-slate-500/20";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const kpis = computed(() => [
|
|
41
|
+
{
|
|
42
|
+
label: "Active workflows",
|
|
43
|
+
value: activeWorkflows.value,
|
|
44
|
+
sub: `${workflows.value.length - activeWorkflows.value} paused or drafts`,
|
|
45
|
+
icon: Workflow,
|
|
46
|
+
tile: "bg-emerald-500 text-white"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
label: "Running now",
|
|
50
|
+
value: runningCount.value,
|
|
51
|
+
sub: "live executions in flight",
|
|
52
|
+
icon: Activity,
|
|
53
|
+
tile: "bg-sky-500 text-white"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
label: "Scheduled",
|
|
57
|
+
value: schedulesActive.value,
|
|
58
|
+
sub: "cron triggers active",
|
|
59
|
+
icon: Clock,
|
|
60
|
+
tile: "bg-violet-500 text-white"
|
|
61
|
+
}
|
|
62
|
+
]);
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<template>
|
|
66
|
+
<IntegrationsLayout>
|
|
67
|
+
<PageHeader
|
|
68
|
+
title="Dashboard"
|
|
69
|
+
description="Everything that connects Revenue Cloud to the outside world."
|
|
70
|
+
>
|
|
71
|
+
<template #actions>
|
|
72
|
+
<NuxtLink to="/integrations/templates">
|
|
73
|
+
<Button variant="outline" class="h-7 text-sm">
|
|
74
|
+
<Layers :size="12" />
|
|
75
|
+
Browse templates
|
|
76
|
+
</Button>
|
|
77
|
+
</NuxtLink>
|
|
78
|
+
<NuxtLink to="/integrations/workflows/new">
|
|
79
|
+
<Button class="h-7 text-sm">
|
|
80
|
+
<Plus :size="14" />
|
|
81
|
+
New workflow
|
|
82
|
+
</Button>
|
|
83
|
+
</NuxtLink>
|
|
84
|
+
</template>
|
|
85
|
+
</PageHeader>
|
|
86
|
+
|
|
87
|
+
<div class="p-4 space-y-4">
|
|
88
|
+
<!-- KPI strip -->
|
|
89
|
+
<div class="grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
|
|
90
|
+
<div v-for="k in kpis" :key="k.label" class="rounded-lg border border-black/[0.06] bg-white p-3">
|
|
91
|
+
<div class="flex items-start justify-between gap-2">
|
|
92
|
+
<div class="min-w-0">
|
|
93
|
+
<div class="text-mono text-muted-foreground truncate">
|
|
94
|
+
{{ k.label }}
|
|
95
|
+
</div>
|
|
96
|
+
<div class="text-2xl font-semibold tabular-nums text-[#0B0522]">
|
|
97
|
+
{{ k.value }}
|
|
98
|
+
</div>
|
|
99
|
+
<div class="text-sm text-muted-foreground">
|
|
100
|
+
{{ k.sub }}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
<span class="flex size-8 shrink-0 items-center justify-center rounded-md" :class="k.tile">
|
|
104
|
+
<component :is="k.icon" :size="16" />
|
|
105
|
+
</span>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<!-- Two-column: recent runs + needs attention + popular templates -->
|
|
111
|
+
<div class="grid gap-4 lg:grid-cols-3">
|
|
112
|
+
<!-- Recent runs ─ takes 2 cols -->
|
|
113
|
+
<div class="lg:col-span-2 rounded-lg border border-black/[0.06] bg-white">
|
|
114
|
+
<div class="flex items-center justify-between border-b border-black/[0.06] px-3 py-2">
|
|
115
|
+
<div class="flex items-center gap-2">
|
|
116
|
+
<span class="flex size-6 items-center justify-center rounded-md bg-slate-100 text-slate-500">
|
|
117
|
+
<Activity :size="12" />
|
|
118
|
+
</span>
|
|
119
|
+
<h3 class="text-sm font-semibold">Recent runs</h3>
|
|
120
|
+
</div>
|
|
121
|
+
<NuxtLink to="/integrations/runs" class="text-sm text-muted-foreground hover:text-foreground inline-flex items-center gap-1">
|
|
122
|
+
View all <ArrowRight :size="11" />
|
|
123
|
+
</NuxtLink>
|
|
124
|
+
</div>
|
|
125
|
+
<div>
|
|
126
|
+
<div
|
|
127
|
+
v-for="r in recentRuns"
|
|
128
|
+
:key="r.id"
|
|
129
|
+
class="grid grid-cols-[80px_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 cursor-pointer hover:bg-muted/30"
|
|
130
|
+
@click="$router.push(`/integrations/workflows/${r.workflowId}`)"
|
|
131
|
+
>
|
|
132
|
+
<Badge variant="outline" :class="['text-sm border', statusClass(r.status)]">
|
|
133
|
+
{{ r.status }}
|
|
134
|
+
</Badge>
|
|
135
|
+
<div class="truncate">
|
|
136
|
+
<div class="font-medium text-foreground truncate">{{ r.workflowName }}</div>
|
|
137
|
+
<div class="font-mono text-sm text-muted-foreground truncate">{{ r.id }}</div>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="text-sm text-muted-foreground tabular-nums truncate">
|
|
140
|
+
{{ formatRelative(r.startedAt) }}
|
|
141
|
+
</div>
|
|
142
|
+
<div class="text-sm text-muted-foreground tabular-nums text-right">
|
|
143
|
+
{{ r.durationMs == null ? "\u2026" : `${(r.durationMs / 1e3).toFixed(2)}s` }}
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
<div v-if="!recentRuns.length" class="px-3 py-8 text-center text-sm text-muted-foreground">
|
|
147
|
+
No runs yet.
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<!-- Needs attention -->
|
|
153
|
+
<div class="rounded-lg border border-black/[0.06] bg-white">
|
|
154
|
+
<div class="flex items-center justify-between border-b border-black/[0.06] px-3 py-2">
|
|
155
|
+
<div class="flex items-center gap-2">
|
|
156
|
+
<span class="flex size-6 items-center justify-center rounded-md bg-slate-100 text-slate-500">
|
|
157
|
+
<AlertTriangle :size="12" />
|
|
158
|
+
</span>
|
|
159
|
+
<h3 class="text-sm font-semibold">Needs attention</h3>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
<div v-if="failedRecent.length">
|
|
163
|
+
<div
|
|
164
|
+
v-for="r in failedRecent"
|
|
165
|
+
:key="r.id"
|
|
166
|
+
class="space-y-1 border-b border-black/[0.04] px-3 py-2 text-sm last:border-b-0 cursor-pointer hover:bg-muted/30"
|
|
167
|
+
@click="$router.push(`/integrations/workflows/${r.workflowId}`)"
|
|
168
|
+
>
|
|
169
|
+
<div class="font-medium truncate">{{ r.workflowName }}</div>
|
|
170
|
+
<div class="text-sm text-rose-700 truncate" :title="r.errorMessage">
|
|
171
|
+
{{ r.errorMessage ?? "Failed." }}
|
|
172
|
+
</div>
|
|
173
|
+
<div class="text-sm text-muted-foreground tabular-nums">
|
|
174
|
+
{{ formatRelative(r.startedAt) }}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
<div v-else class="px-3 py-6 text-center text-sm text-muted-foreground">
|
|
179
|
+
All workflows healthy.
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<!-- Popular templates -->
|
|
185
|
+
<div class="rounded-lg border border-black/[0.06] bg-white">
|
|
186
|
+
<div class="flex items-center justify-between border-b border-black/[0.06] px-3 py-2">
|
|
187
|
+
<div class="flex items-center gap-2">
|
|
188
|
+
<span class="flex size-6 items-center justify-center rounded-md bg-slate-100 text-slate-500">
|
|
189
|
+
<Sparkles :size="12" />
|
|
190
|
+
</span>
|
|
191
|
+
<h3 class="text-sm font-semibold">Popular templates</h3>
|
|
192
|
+
</div>
|
|
193
|
+
<NuxtLink to="/integrations/templates" class="text-sm text-muted-foreground hover:text-foreground inline-flex items-center gap-1">
|
|
194
|
+
See all <ArrowRight :size="11" />
|
|
195
|
+
</NuxtLink>
|
|
196
|
+
</div>
|
|
197
|
+
<div v-if="!popular.length" class="px-3 py-6 text-center text-sm text-muted-foreground">
|
|
198
|
+
No templates registered yet.
|
|
199
|
+
</div>
|
|
200
|
+
<div v-else class="grid gap-2 p-3 sm:grid-cols-2 lg:grid-cols-4">
|
|
201
|
+
<NuxtLink
|
|
202
|
+
v-for="tpl in popular"
|
|
203
|
+
:key="tpl.slug"
|
|
204
|
+
:to="`/integrations/templates/${tpl.slug}`"
|
|
205
|
+
class="block rounded-md border border-black/[0.06] bg-white p-3 transition hover:border-black/20 hover:shadow-sm"
|
|
206
|
+
>
|
|
207
|
+
<div class="flex items-center gap-2 mb-2">
|
|
208
|
+
<Badge variant="outline" class="text-sm">
|
|
209
|
+
{{ tpl.category }}
|
|
210
|
+
</Badge>
|
|
211
|
+
<span class="flex-1" />
|
|
212
|
+
<span class="text-sm text-muted-foreground tabular-nums inline-flex items-center gap-0.5">
|
|
213
|
+
<TrendingUp :size="10" />
|
|
214
|
+
{{ tpl.installs }}
|
|
215
|
+
</span>
|
|
216
|
+
</div>
|
|
217
|
+
<div class="text-sm font-medium truncate">{{ tpl.name }}</div>
|
|
218
|
+
<p class="mt-1 text-sm text-muted-foreground line-clamp-2">
|
|
219
|
+
{{ tpl.description }}
|
|
220
|
+
</p>
|
|
221
|
+
</NuxtLink>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</IntegrationsLayout>
|
|
226
|
+
</template>
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
export default _default;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
export default _default;
|