@revenexx/studio-integrations 0.1.4 → 0.2.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/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.d.vue.ts +13 -0
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +279 -0
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue.d.ts +13 -0
- package/dist/runtime/components/Integrations/CredentialDetailDialog.d.vue.ts +20 -0
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +342 -0
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue.d.ts +20 -0
- package/dist/runtime/components/Integrations/CredentialLogo.d.vue.ts +14 -0
- package/dist/runtime/components/Integrations/CredentialLogo.vue +37 -0
- package/dist/runtime/components/Integrations/CredentialLogo.vue.d.ts +14 -0
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.d.vue.ts +12 -0
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +364 -0
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue.d.ts +12 -0
- package/dist/runtime/components/Integrations/CredentialTypePicker.d.vue.ts +15 -0
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +71 -0
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue.d.ts +15 -0
- package/dist/runtime/components/Integrations/DeadLettersPanel.d.vue.ts +5 -0
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +151 -0
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue.d.ts +5 -0
- package/dist/runtime/components/Integrations/EditorTip.d.vue.ts +17 -0
- package/dist/runtime/components/Integrations/EditorTip.vue +46 -0
- package/dist/runtime/components/Integrations/EditorTip.vue.d.ts +17 -0
- package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +9 -0
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +64 -0
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +9 -0
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +19 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +236 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +19 -0
- package/dist/runtime/components/Integrations/GroupFrameView.d.vue.ts +28 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +222 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue.d.ts +28 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.d.vue.ts +14 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +184 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue.d.ts +14 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.d.vue.ts +16 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +102 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue.d.ts +16 -0
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +23 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +587 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +23 -0
- package/dist/runtime/components/Integrations/NodeDynamicOutputsResolver.d.vue.ts +14 -0
- package/dist/runtime/components/Integrations/NodeDynamicOutputsResolver.vue +80 -0
- package/dist/runtime/components/Integrations/NodeDynamicOutputsResolver.vue.d.ts +14 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +22 -3
- package/dist/runtime/components/Integrations/NodeInspector.vue +1074 -294
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +22 -3
- package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +9 -0
- package/dist/runtime/components/Integrations/NodeLogo.vue +38 -0
- package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +9 -0
- package/dist/runtime/components/Integrations/NodePalette.d.vue.ts +22 -1
- package/dist/runtime/components/Integrations/NodePalette.vue +411 -90
- package/dist/runtime/components/Integrations/NodePalette.vue.d.ts +22 -1
- package/dist/runtime/components/Integrations/QueryBuilder.d.vue.ts +9 -0
- package/dist/runtime/components/Integrations/QueryBuilder.vue +239 -0
- package/dist/runtime/components/Integrations/QueryBuilder.vue.d.ts +9 -0
- package/dist/runtime/components/Integrations/{RunDetailDialog.d.vue.ts → RunActionsMenu.d.vue.ts} +3 -4
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +86 -0
- package/dist/runtime/components/Integrations/{RunDetailDialog.vue.d.ts → RunActionsMenu.vue.d.ts} +3 -4
- package/dist/runtime/components/Integrations/RunDetailPanel.d.vue.ts +14 -0
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +777 -0
- package/dist/runtime/components/Integrations/RunDetailPanel.vue.d.ts +14 -0
- package/dist/runtime/components/Integrations/RunPayloadInput.d.vue.ts +13 -0
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +175 -0
- package/dist/runtime/components/Integrations/RunPayloadInput.vue.d.ts +13 -0
- package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +27 -0
- package/dist/runtime/components/Integrations/RunResponseModal.vue +84 -0
- package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +27 -0
- package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +21 -0
- package/dist/runtime/components/Integrations/RunsTable.vue +114 -0
- package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.d.vue.ts +11 -0
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +91 -0
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue.d.ts +11 -0
- package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +28 -0
- package/dist/runtime/components/Integrations/StickyNoteView.vue +208 -0
- package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +28 -0
- package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +29 -0
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +201 -0
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +29 -0
- package/dist/runtime/components/Integrations/WorkflowContextMenu.d.vue.ts +32 -0
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +138 -0
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue.d.ts +32 -0
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.d.vue.ts +9 -0
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +322 -0
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue.d.ts +9 -0
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.d.vue.ts +18 -0
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +63 -0
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue.d.ts +18 -0
- package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +12 -1
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +68 -6
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +12 -1
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +8 -1
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +1424 -144
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +8 -1
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.d.vue.ts +12 -0
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +417 -0
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue.d.ts +12 -0
- package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +10 -0
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +89 -0
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +10 -0
- package/dist/runtime/components/Integrations/WorkflowIconView.d.vue.ts +10 -0
- package/dist/runtime/components/Integrations/WorkflowIconView.vue +35 -0
- package/dist/runtime/components/Integrations/WorkflowIconView.vue.d.ts +10 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +5 -1
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +114 -84
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +5 -1
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.d.vue.ts +12 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +654 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue.d.ts +12 -0
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.d.vue.ts +36 -0
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +136 -0
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue.d.ts +36 -0
- package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +14 -0
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +63 -0
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +14 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +35 -15
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.d.vue.ts +13 -0
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +265 -0
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue.d.ts +13 -0
- package/dist/runtime/components/Integrations/WorkflowsTable.d.vue.ts +4 -1
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +122 -39
- package/dist/runtime/components/Integrations/WorkflowsTable.vue.d.ts +4 -1
- package/dist/runtime/components/Integrations/workflowLayout.d.ts +5 -0
- package/dist/runtime/components/Integrations/workflowLayout.js +5 -3
- package/dist/runtime/composables/integrationsApi.d.ts +6 -2
- package/dist/runtime/composables/integrationsApi.js +5 -4
- package/dist/runtime/composables/useCanvasShortcuts.d.ts +37 -0
- package/dist/runtime/composables/useCanvasShortcuts.js +62 -0
- package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +22 -0
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +50 -0
- package/dist/runtime/composables/useIntegrationsNodes.d.ts +4 -0
- package/dist/runtime/composables/useIntegrationsNodes.js +6 -3
- package/dist/runtime/composables/useIntegrationsRevisions.d.ts +40 -0
- package/dist/runtime/composables/useIntegrationsRevisions.js +51 -0
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +21 -1
- package/dist/runtime/composables/useIntegrationsRuns.js +92 -4
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +1 -0
- package/dist/runtime/composables/useIntegrationsTemplates.js +32 -4
- package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +1 -0
- package/dist/runtime/composables/useIntegrationsWorkflows.js +57 -1
- package/dist/runtime/composables/useMiniMapLayout.d.ts +34 -0
- package/dist/runtime/composables/useMiniMapLayout.js +119 -0
- package/dist/runtime/composables/useNodeConfigResolve.d.ts +43 -0
- package/dist/runtime/composables/useNodeConfigResolve.js +123 -0
- package/dist/runtime/composables/useNodeDrag.d.ts +22 -0
- package/dist/runtime/composables/useNodeDrag.js +10 -0
- package/dist/runtime/composables/usePaletteLayout.d.ts +41 -0
- package/dist/runtime/composables/usePaletteLayout.js +113 -0
- package/dist/runtime/data/expressionEditor.d.ts +22 -0
- package/dist/runtime/data/expressionEditor.js +40 -0
- package/dist/runtime/data/integrationNodeCatalog.js +8 -8
- package/dist/runtime/data/nodeInputData.d.ts +37 -0
- package/dist/runtime/data/nodeInputData.js +62 -0
- package/dist/runtime/data/nodeRunData.d.ts +56 -0
- package/dist/runtime/data/nodeRunData.js +91 -0
- package/dist/runtime/data/payloadForm.d.ts +44 -0
- package/dist/runtime/data/payloadForm.js +126 -0
- package/dist/runtime/data/workflowIconCatalog.d.ts +34 -0
- package/dist/runtime/data/workflowIconCatalog.js +116 -0
- package/dist/runtime/pages/integrations/audit.vue +25 -35
- package/dist/runtime/pages/integrations/credentials.vue +117 -122
- package/dist/runtime/pages/integrations/index.vue +133 -36
- package/dist/runtime/pages/integrations/runs.vue +125 -75
- package/dist/runtime/pages/integrations/schedules.vue +5 -16
- package/dist/runtime/pages/integrations/secrets.vue +1 -3
- package/dist/runtime/pages/integrations/workflows/[id].vue +599 -194
- package/dist/runtime/pages/integrations/workflows/index.vue +10 -8
- package/dist/runtime/types/integrations.d.ts +190 -4
- package/dist/runtime/utils/credentialState.d.ts +28 -0
- package/dist/runtime/utils/credentialState.js +29 -0
- package/dist/runtime/utils/localize.d.ts +3 -1
- package/dist/runtime/utils/localize.js +1 -2
- package/dist/runtime/utils/statusStyle.d.ts +6 -0
- package/dist/runtime/utils/statusStyle.js +15 -0
- package/package.json +11 -1
- package/dist/runtime/components/Integrations/RunDetailDialog.vue +0 -169
- package/dist/runtime/pages/integrations/workflows/new.d.vue.ts +0 -2
- package/dist/runtime/pages/integrations/workflows/new.vue +0 -153
- package/dist/runtime/pages/integrations/workflows/new.vue.d.ts +0 -2
package/dist/module.json
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
open: boolean;
|
|
3
|
+
/** Credential type slug(s) the field accepts — the dialog is scoped to these. */
|
|
4
|
+
credentialType?: string | string[];
|
|
5
|
+
};
|
|
6
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
7
|
+
"update:open": (args_0: boolean) => any;
|
|
8
|
+
created: (credentialId: string) => any;
|
|
9
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
10
|
+
"onUpdate:open"?: ((args_0: boolean) => any) | undefined;
|
|
11
|
+
onCreated?: ((credentialId: string) => any) | undefined;
|
|
12
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
13
|
+
export default _default;
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, reactive, ref, watch } from "vue";
|
|
3
|
+
import { RefreshCircle as Loader2, TestTube as TestIcon } from "#solar-icons";
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogDescription,
|
|
9
|
+
DialogFooter,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
Input,
|
|
13
|
+
Label,
|
|
14
|
+
Switch,
|
|
15
|
+
Select,
|
|
16
|
+
SelectContent,
|
|
17
|
+
SelectItem,
|
|
18
|
+
SelectTrigger,
|
|
19
|
+
SelectValue
|
|
20
|
+
} from "@revenexx/studio";
|
|
21
|
+
import { toast } from "vue-sonner";
|
|
22
|
+
const props = defineProps({
|
|
23
|
+
open: { type: Boolean, required: true },
|
|
24
|
+
credentialType: { type: [String, Array], required: false }
|
|
25
|
+
});
|
|
26
|
+
const emit = defineEmits(["update:open", "created"]);
|
|
27
|
+
const { create, testInline, load, oauthAuthorizeUrl } = useIntegrationsCredentials();
|
|
28
|
+
const { load: loadTypes, bySlug } = useCredentialTypes();
|
|
29
|
+
function loc(v) {
|
|
30
|
+
if (!v) return "";
|
|
31
|
+
if (typeof v === "string") return v;
|
|
32
|
+
const o = v;
|
|
33
|
+
return o.en ?? o.de ?? Object.values(o)[0] ?? "";
|
|
34
|
+
}
|
|
35
|
+
function authKindLabel(k) {
|
|
36
|
+
switch (k) {
|
|
37
|
+
case "api-key":
|
|
38
|
+
return "API Key";
|
|
39
|
+
case "oauth2-authcode":
|
|
40
|
+
case "oauth2-client-credentials":
|
|
41
|
+
return "OAuth2";
|
|
42
|
+
case "basic-auth":
|
|
43
|
+
return "Basic Auth";
|
|
44
|
+
case "static":
|
|
45
|
+
return "Static";
|
|
46
|
+
case "secret":
|
|
47
|
+
return "Secret";
|
|
48
|
+
default:
|
|
49
|
+
return k ?? "";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const allowedSlugs = computed(() => {
|
|
53
|
+
const t = props.credentialType;
|
|
54
|
+
if (!t) return [];
|
|
55
|
+
return (Array.isArray(t) ? t : [t]).map((s) => s.trim()).filter(Boolean);
|
|
56
|
+
});
|
|
57
|
+
const allowedTypes = computed(() => allowedSlugs.value.map((s) => bySlug(s)).filter((x) => !!x));
|
|
58
|
+
const pickedType = ref(null);
|
|
59
|
+
const form = reactive({ name: "", config: {} });
|
|
60
|
+
const sourceActive = ref(false);
|
|
61
|
+
const saving = ref(false);
|
|
62
|
+
const testing = ref(false);
|
|
63
|
+
const testResult = ref(null);
|
|
64
|
+
function fieldsOf(t) {
|
|
65
|
+
return t?.fields ?? [];
|
|
66
|
+
}
|
|
67
|
+
function isOAuthAuthcode(t) {
|
|
68
|
+
return t?.auth_kind === "oauth2-authcode";
|
|
69
|
+
}
|
|
70
|
+
watch(() => props.open, async (o) => {
|
|
71
|
+
if (!o) return;
|
|
72
|
+
testResult.value = null;
|
|
73
|
+
sourceActive.value = false;
|
|
74
|
+
form.config = {};
|
|
75
|
+
await loadTypes();
|
|
76
|
+
if (allowedTypes.value.length === 1) {
|
|
77
|
+
pickedType.value = allowedTypes.value[0];
|
|
78
|
+
form.name = loc(pickedType.value.name);
|
|
79
|
+
} else {
|
|
80
|
+
pickedType.value = null;
|
|
81
|
+
form.name = "";
|
|
82
|
+
}
|
|
83
|
+
}, { immediate: true });
|
|
84
|
+
function pickType(t) {
|
|
85
|
+
pickedType.value = t;
|
|
86
|
+
form.config = {};
|
|
87
|
+
testResult.value = null;
|
|
88
|
+
sourceActive.value = false;
|
|
89
|
+
if (!form.name) form.name = loc(t.name);
|
|
90
|
+
}
|
|
91
|
+
function buildConfigPayload() {
|
|
92
|
+
const out = {};
|
|
93
|
+
for (const f of fieldsOf(pickedType.value)) {
|
|
94
|
+
const v = form.config[f.key];
|
|
95
|
+
if (v !== void 0 && v !== "") out[f.key] = v;
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
async function runTest() {
|
|
100
|
+
if (!pickedType.value) return;
|
|
101
|
+
testing.value = true;
|
|
102
|
+
testResult.value = null;
|
|
103
|
+
try {
|
|
104
|
+
testResult.value = await testInline(pickedType.value.slug, buildConfigPayload());
|
|
105
|
+
} finally {
|
|
106
|
+
testing.value = false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function connect(id) {
|
|
110
|
+
return oauthAuthorizeUrl(id).then((authorizeUrl) => {
|
|
111
|
+
const popup = window.open(authorizeUrl, "oauth-connect", "width=600,height=720");
|
|
112
|
+
const onMessage = (ev) => {
|
|
113
|
+
if (ev.data?.type === "integrations-oauth-complete") {
|
|
114
|
+
window.removeEventListener("message", onMessage);
|
|
115
|
+
void load(true);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
window.addEventListener("message", onMessage);
|
|
119
|
+
const poll = window.setInterval(() => {
|
|
120
|
+
if (popup?.closed) {
|
|
121
|
+
window.clearInterval(poll);
|
|
122
|
+
window.removeEventListener("message", onMessage);
|
|
123
|
+
void load(true);
|
|
124
|
+
}
|
|
125
|
+
}, 1e3);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
async function submit() {
|
|
129
|
+
if (!pickedType.value || !form.name.trim()) return;
|
|
130
|
+
saving.value = true;
|
|
131
|
+
try {
|
|
132
|
+
const created = await create({
|
|
133
|
+
credential_type_slug: pickedType.value.slug,
|
|
134
|
+
name: form.name.trim(),
|
|
135
|
+
config: buildConfigPayload()
|
|
136
|
+
});
|
|
137
|
+
await load(true);
|
|
138
|
+
toast.success("Credential created");
|
|
139
|
+
if (isOAuthAuthcode(pickedType.value)) {
|
|
140
|
+
void connect(created.id);
|
|
141
|
+
}
|
|
142
|
+
emit("created", created.id);
|
|
143
|
+
emit("update:open", false);
|
|
144
|
+
} catch (e) {
|
|
145
|
+
toast.error(e?.message ?? "Could not create credential.");
|
|
146
|
+
} finally {
|
|
147
|
+
saving.value = false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
</script>
|
|
151
|
+
|
|
152
|
+
<template>
|
|
153
|
+
<Dialog :open="open" @update:open="(v) => emit('update:open', v)">
|
|
154
|
+
<DialogContent class="sm:max-w-lg">
|
|
155
|
+
<DialogHeader>
|
|
156
|
+
<DialogTitle>New credential</DialogTitle>
|
|
157
|
+
<DialogDescription class="text-sm">
|
|
158
|
+
Create a credential for this node without leaving the editor.
|
|
159
|
+
</DialogDescription>
|
|
160
|
+
</DialogHeader>
|
|
161
|
+
|
|
162
|
+
<!-- Choose the type when the field accepts more than one -->
|
|
163
|
+
<div v-if="!pickedType" class="space-y-2 py-1">
|
|
164
|
+
<p v-if="!allowedTypes.length" class="text-sm text-muted-foreground">
|
|
165
|
+
No credential type available for this field.
|
|
166
|
+
</p>
|
|
167
|
+
<button
|
|
168
|
+
v-for="t in allowedTypes"
|
|
169
|
+
:key="t.slug"
|
|
170
|
+
type="button"
|
|
171
|
+
class="flex w-full items-center gap-2 rounded-lg border border-black/[0.08] p-3 text-left transition hover:border-black/20 hover:bg-muted/30"
|
|
172
|
+
@click="pickType(t)"
|
|
173
|
+
>
|
|
174
|
+
<IntegrationsCredentialLogo :type="t" :size="18" />
|
|
175
|
+
<div class="min-w-0 truncate text-sm">
|
|
176
|
+
<span class="font-semibold">{{ loc(t.name) }}</span>
|
|
177
|
+
<span class="ml-1.5 text-muted-foreground">({{ authKindLabel(t.auth_kind) }})</span>
|
|
178
|
+
</div>
|
|
179
|
+
</button>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<!-- Dynamic form -->
|
|
183
|
+
<div v-else class="space-y-3 py-1 max-h-[26rem] overflow-y-auto pr-1">
|
|
184
|
+
<div class="flex items-center gap-2">
|
|
185
|
+
<IntegrationsCredentialLogo :type="pickedType" :size="18" />
|
|
186
|
+
<div class="text-sm font-medium">{{ loc(pickedType.name) }}</div>
|
|
187
|
+
<button
|
|
188
|
+
v-if="allowedTypes.length > 1"
|
|
189
|
+
type="button"
|
|
190
|
+
class="ml-auto text-sm text-muted-foreground hover:text-foreground"
|
|
191
|
+
@click="pickedType = null"
|
|
192
|
+
>
|
|
193
|
+
Change type
|
|
194
|
+
</button>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div class="space-y-1.5">
|
|
198
|
+
<Label for="cred-name">Name</Label>
|
|
199
|
+
<Input id="cred-name" v-model="form.name" class="bg-white" placeholder="e.g. Production key" />
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<!-- "Use revenexx …" source picker (SFTP / Mail / API key) -->
|
|
203
|
+
<IntegrationsCredentialRevenexxSource
|
|
204
|
+
:type="pickedType"
|
|
205
|
+
@update:active="(v) => sourceActive = v"
|
|
206
|
+
@apply="(c) => Object.assign(form.config, c)"
|
|
207
|
+
/>
|
|
208
|
+
|
|
209
|
+
<div v-for="f in fieldsOf(pickedType)" v-show="!sourceActive" :key="f.key" class="space-y-1.5">
|
|
210
|
+
<Label>{{ loc(f.label) }}{{ f.required ? " *" : "" }}</Label>
|
|
211
|
+
<Input
|
|
212
|
+
v-if="f.type === 'secret'"
|
|
213
|
+
:model-value="form.config[f.key] ?? ''"
|
|
214
|
+
type="password"
|
|
215
|
+
autocomplete="new-password"
|
|
216
|
+
class="bg-white"
|
|
217
|
+
@update:model-value="(v) => form.config[f.key] = v"
|
|
218
|
+
/>
|
|
219
|
+
<Input
|
|
220
|
+
v-else-if="f.type === 'number'"
|
|
221
|
+
type="number"
|
|
222
|
+
:model-value="form.config[f.key] ?? ''"
|
|
223
|
+
class="bg-white"
|
|
224
|
+
@update:model-value="(v) => form.config[f.key] = v === '' ? void 0 : Number(v)"
|
|
225
|
+
/>
|
|
226
|
+
<div v-else-if="f.type === 'boolean'" class="flex items-center gap-2 pt-1">
|
|
227
|
+
<Switch :model-value="Boolean(form.config[f.key])" @update:model-value="(v) => form.config[f.key] = v" />
|
|
228
|
+
</div>
|
|
229
|
+
<Select
|
|
230
|
+
v-else-if="f.type === 'select'"
|
|
231
|
+
:model-value="form.config[f.key] ?? ''"
|
|
232
|
+
@update:model-value="(v) => form.config[f.key] = v"
|
|
233
|
+
>
|
|
234
|
+
<SelectTrigger class="bg-white">
|
|
235
|
+
<SelectValue placeholder="Pick one" />
|
|
236
|
+
</SelectTrigger>
|
|
237
|
+
<SelectContent>
|
|
238
|
+
<SelectItem v-for="opt in f.options ?? []" :key="String(opt.value)" :value="String(opt.value)">
|
|
239
|
+
{{ loc(opt.label) }}
|
|
240
|
+
</SelectItem>
|
|
241
|
+
</SelectContent>
|
|
242
|
+
</Select>
|
|
243
|
+
<Input
|
|
244
|
+
v-else
|
|
245
|
+
:model-value="form.config[f.key] ?? ''"
|
|
246
|
+
class="bg-white"
|
|
247
|
+
@update:model-value="(v) => form.config[f.key] = v"
|
|
248
|
+
/>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
<p v-if="isOAuthAuthcode(pickedType)" class="text-sm text-muted-foreground">
|
|
252
|
+
After saving, a window opens to connect with the provider.
|
|
253
|
+
</p>
|
|
254
|
+
|
|
255
|
+
<div
|
|
256
|
+
v-if="testResult"
|
|
257
|
+
class="rounded-md px-3 py-2 text-sm"
|
|
258
|
+
:class="testResult.ok ? 'bg-emerald-500/10 text-emerald-700' : 'bg-rose-500/10 text-rose-700'"
|
|
259
|
+
>
|
|
260
|
+
{{ testResult.ok ? "Connection ok." : testResult.error ?? "Connection failed." }}
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<DialogFooter v-if="pickedType">
|
|
265
|
+
<div class="flex items-center gap-2">
|
|
266
|
+
<Button variant="outline" :disabled="saving || testing" @click="runTest">
|
|
267
|
+
<Loader2 v-if="testing" :size="14" class="mr-1 animate-spin" />
|
|
268
|
+
<TestIcon v-else :size="14" class="mr-1" />
|
|
269
|
+
Connect
|
|
270
|
+
</Button>
|
|
271
|
+
<Button :disabled="saving || !form.name.trim()" @click="submit">
|
|
272
|
+
<Loader2 v-if="saving" :size="14" class="mr-1 animate-spin" />
|
|
273
|
+
Save
|
|
274
|
+
</Button>
|
|
275
|
+
</div>
|
|
276
|
+
</DialogFooter>
|
|
277
|
+
</DialogContent>
|
|
278
|
+
</Dialog>
|
|
279
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
open: boolean;
|
|
3
|
+
/** Credential type slug(s) the field accepts — the dialog is scoped to these. */
|
|
4
|
+
credentialType?: string | string[];
|
|
5
|
+
};
|
|
6
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
7
|
+
"update:open": (args_0: boolean) => any;
|
|
8
|
+
created: (credentialId: string) => any;
|
|
9
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
10
|
+
"onUpdate:open"?: ((args_0: boolean) => any) | undefined;
|
|
11
|
+
onCreated?: ((credentialId: string) => any) | undefined;
|
|
12
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
13
|
+
export default _default;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Credential, CredentialType } from '../../types/integrations.js';
|
|
2
|
+
/**
|
|
3
|
+
* Credential detail / edit view — a single scrolling page (no tabs): the
|
|
4
|
+
* connection state up top, the editable fields, then read-only metadata.
|
|
5
|
+
* Self-contained — owns update / delete / test / OAuth reconnect against the
|
|
6
|
+
* Integrations service via the shared composable.
|
|
7
|
+
*/
|
|
8
|
+
type __VLS_Props = {
|
|
9
|
+
open: boolean;
|
|
10
|
+
credential: Credential | null;
|
|
11
|
+
type: CredentialType | null;
|
|
12
|
+
};
|
|
13
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
14
|
+
"update:open": (args_0: boolean) => any;
|
|
15
|
+
changed: () => any;
|
|
16
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
17
|
+
"onUpdate:open"?: ((args_0: boolean) => any) | undefined;
|
|
18
|
+
onChanged?: (() => any) | undefined;
|
|
19
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
20
|
+
export default _default;
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, reactive, ref, watch } from "vue";
|
|
3
|
+
import {
|
|
4
|
+
CheckCircle as CheckCircle2,
|
|
5
|
+
CloseCircle as XCircle,
|
|
6
|
+
RefreshCircle as Loader2,
|
|
7
|
+
TestTube as TestIcon,
|
|
8
|
+
TrashBinTrash as Trash2
|
|
9
|
+
} from "#solar-icons";
|
|
10
|
+
import {
|
|
11
|
+
Button,
|
|
12
|
+
Dialog,
|
|
13
|
+
DialogContent,
|
|
14
|
+
DialogDescription,
|
|
15
|
+
DialogHeader,
|
|
16
|
+
DialogTitle,
|
|
17
|
+
Input,
|
|
18
|
+
Label,
|
|
19
|
+
Switch,
|
|
20
|
+
Select,
|
|
21
|
+
SelectContent,
|
|
22
|
+
SelectItem,
|
|
23
|
+
SelectTrigger,
|
|
24
|
+
SelectValue
|
|
25
|
+
} from "@revenexx/studio";
|
|
26
|
+
import { toast } from "vue-sonner";
|
|
27
|
+
import { credentialState, relativeTime } from "../../utils/credentialState";
|
|
28
|
+
const props = defineProps({
|
|
29
|
+
open: { type: Boolean, required: true },
|
|
30
|
+
credential: { type: [Object, null], required: true },
|
|
31
|
+
type: { type: [Object, null], required: true }
|
|
32
|
+
});
|
|
33
|
+
const emit = defineEmits(["update:open", "changed"]);
|
|
34
|
+
const { update, remove, test, oauthAuthorizeUrl, load } = useIntegrationsCredentials();
|
|
35
|
+
const tab = ref("credentials");
|
|
36
|
+
const tabs = [
|
|
37
|
+
{ key: "credentials", label: "Credentials" },
|
|
38
|
+
{ key: "details", label: "Details" }
|
|
39
|
+
];
|
|
40
|
+
const form = reactive({ name: "", config: {} });
|
|
41
|
+
const sourceActive = ref(false);
|
|
42
|
+
const saving = ref(false);
|
|
43
|
+
const deleting = ref(false);
|
|
44
|
+
const testing = ref(false);
|
|
45
|
+
const liveTest = ref(null);
|
|
46
|
+
const connecting = ref(false);
|
|
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
|
+
const typeName = computed(() => loc(props.type?.name) || props.credential?.credential_type_slug || "");
|
|
54
|
+
const isOAuthAuthcode = computed(() => props.type?.auth_kind === "oauth2-authcode");
|
|
55
|
+
const state = computed(() => props.credential ? credentialState(props.credential) : null);
|
|
56
|
+
function fieldsOf(t) {
|
|
57
|
+
return t?.fields ?? [];
|
|
58
|
+
}
|
|
59
|
+
function formatDateTime(iso) {
|
|
60
|
+
if (!iso) return "\u2014";
|
|
61
|
+
try {
|
|
62
|
+
return new Date(iso).toLocaleString();
|
|
63
|
+
} catch {
|
|
64
|
+
return String(iso);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
watch(
|
|
68
|
+
() => [props.open, props.credential?.id],
|
|
69
|
+
() => {
|
|
70
|
+
if (!props.open || !props.credential) return;
|
|
71
|
+
tab.value = "credentials";
|
|
72
|
+
form.name = props.credential.name;
|
|
73
|
+
form.config = { ...props.credential.public_config ?? {} };
|
|
74
|
+
sourceActive.value = false;
|
|
75
|
+
liveTest.value = null;
|
|
76
|
+
},
|
|
77
|
+
{ immediate: true }
|
|
78
|
+
);
|
|
79
|
+
function buildConfigPayload() {
|
|
80
|
+
const out = {};
|
|
81
|
+
for (const f of fieldsOf(props.type)) {
|
|
82
|
+
const v = form.config[f.key];
|
|
83
|
+
if (f.type === "secret" && (v === void 0 || v === "")) continue;
|
|
84
|
+
if (v !== void 0) out[f.key] = v;
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
async function runTest() {
|
|
89
|
+
if (!props.credential) return;
|
|
90
|
+
testing.value = true;
|
|
91
|
+
liveTest.value = null;
|
|
92
|
+
try {
|
|
93
|
+
liveTest.value = await test(props.credential.id);
|
|
94
|
+
await load(true);
|
|
95
|
+
emit("changed");
|
|
96
|
+
} finally {
|
|
97
|
+
testing.value = false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function save() {
|
|
101
|
+
if (!props.credential || !form.name.trim()) return;
|
|
102
|
+
saving.value = true;
|
|
103
|
+
try {
|
|
104
|
+
await update(props.credential.id, { name: form.name.trim(), config: buildConfigPayload() });
|
|
105
|
+
toast.success("Credential updated");
|
|
106
|
+
emit("changed");
|
|
107
|
+
emit("update:open", false);
|
|
108
|
+
} catch (e) {
|
|
109
|
+
toast.error(e?.message ?? "Could not save credential.");
|
|
110
|
+
} finally {
|
|
111
|
+
saving.value = false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function destroy() {
|
|
115
|
+
if (!props.credential) return;
|
|
116
|
+
if (!confirm(`Delete credential "${props.credential.name}"? Workflows referencing it will fail until re-bound.`)) return;
|
|
117
|
+
deleting.value = true;
|
|
118
|
+
try {
|
|
119
|
+
await remove(props.credential.id);
|
|
120
|
+
toast.success("Credential deleted");
|
|
121
|
+
emit("changed");
|
|
122
|
+
emit("update:open", false);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
toast.error(e?.message ?? "Could not delete credential.");
|
|
125
|
+
} finally {
|
|
126
|
+
deleting.value = false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function connect() {
|
|
130
|
+
if (!props.credential) return;
|
|
131
|
+
connecting.value = true;
|
|
132
|
+
try {
|
|
133
|
+
const authorizeUrl = await oauthAuthorizeUrl(props.credential.id);
|
|
134
|
+
const popup = window.open(authorizeUrl, "oauth-connect", "width=600,height=720");
|
|
135
|
+
const onMessage = async (ev) => {
|
|
136
|
+
if (ev.data?.type === "integrations-oauth-complete") {
|
|
137
|
+
window.removeEventListener("message", onMessage);
|
|
138
|
+
await load(true);
|
|
139
|
+
emit("changed");
|
|
140
|
+
toast.success("Connected");
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
window.addEventListener("message", onMessage);
|
|
144
|
+
const poll = window.setInterval(async () => {
|
|
145
|
+
if (popup?.closed) {
|
|
146
|
+
window.clearInterval(poll);
|
|
147
|
+
window.removeEventListener("message", onMessage);
|
|
148
|
+
await load(true);
|
|
149
|
+
emit("changed");
|
|
150
|
+
}
|
|
151
|
+
}, 1e3);
|
|
152
|
+
} catch (e) {
|
|
153
|
+
toast.error(e?.message ?? "Could not start OAuth connect.");
|
|
154
|
+
} finally {
|
|
155
|
+
connecting.value = false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
</script>
|
|
159
|
+
|
|
160
|
+
<template>
|
|
161
|
+
<Dialog :open="open" @update:open="(v) => emit('update:open', v)">
|
|
162
|
+
<DialogContent class="sm:max-w-3xl">
|
|
163
|
+
<DialogHeader>
|
|
164
|
+
<div class="flex items-start gap-3">
|
|
165
|
+
<IntegrationsCredentialLogo :type="type" :size="22" />
|
|
166
|
+
<div class="min-w-0 flex-1">
|
|
167
|
+
<DialogTitle class="truncate">{{ credential?.name }}</DialogTitle>
|
|
168
|
+
<DialogDescription class="text-sm truncate">{{ typeName }}</DialogDescription>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</DialogHeader>
|
|
172
|
+
|
|
173
|
+
<div v-if="credential && state">
|
|
174
|
+
<!-- Standard cockpit tab bar (violet underline) -->
|
|
175
|
+
<TabBar :model-value="tab" :tabs="tabs" @update:model-value="(k) => tab = k" />
|
|
176
|
+
|
|
177
|
+
<!-- Tab 1: Credentials -->
|
|
178
|
+
<div v-if="tab === 'credentials'" class="space-y-5 max-h-[62vh] overflow-y-auto pr-1 pt-4">
|
|
179
|
+
<!-- Connection state — the headline answer: are these credentials correct? -->
|
|
180
|
+
<div class="rounded-lg border p-3" :class="state.toneClass">
|
|
181
|
+
<div class="flex items-center gap-2">
|
|
182
|
+
<CheckCircle2 v-if="state.key === 'connected'" :size="16" class="shrink-0" />
|
|
183
|
+
<XCircle v-else-if="state.key === 'failed'" :size="16" class="shrink-0" />
|
|
184
|
+
<span class="font-medium">{{ state.label }}</span>
|
|
185
|
+
<span v-if="state.at" class="text-sm opacity-70">· verified {{ relativeTime(state.at) }}</span>
|
|
186
|
+
<div class="flex-1" />
|
|
187
|
+
<Button
|
|
188
|
+
v-if="isOAuthAuthcode"
|
|
189
|
+
size="sm"
|
|
190
|
+
variant="outline"
|
|
191
|
+
class="bg-white/60"
|
|
192
|
+
:disabled="connecting"
|
|
193
|
+
@click="connect"
|
|
194
|
+
>
|
|
195
|
+
<Loader2 v-if="connecting" :size="13" class="mr-1 animate-spin" />
|
|
196
|
+
Reconnect
|
|
197
|
+
</Button>
|
|
198
|
+
</div>
|
|
199
|
+
<p v-if="state.key === 'failed' && state.message" class="mt-1 text-sm break-words opacity-90">
|
|
200
|
+
{{ state.message }}
|
|
201
|
+
</p>
|
|
202
|
+
<p v-else-if="state.key === 'untested'" class="mt-1 text-sm opacity-80">
|
|
203
|
+
Run a test to verify these credentials actually connect.
|
|
204
|
+
</p>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<!-- Editable fields -->
|
|
208
|
+
<div class="space-y-3">
|
|
209
|
+
<div class="space-y-1.5">
|
|
210
|
+
<Label for="detail-cred-name">Name</Label>
|
|
211
|
+
<Input id="detail-cred-name" v-model="form.name" class="bg-white" />
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<!-- "Use revenexx …" source picker (SFTP / Mail / API key) -->
|
|
215
|
+
<IntegrationsCredentialRevenexxSource
|
|
216
|
+
:type="type"
|
|
217
|
+
@update:active="(v) => sourceActive = v"
|
|
218
|
+
@apply="(c) => Object.assign(form.config, c)"
|
|
219
|
+
/>
|
|
220
|
+
|
|
221
|
+
<div v-for="f in fieldsOf(type)" v-show="!sourceActive" :key="f.key" class="space-y-1.5">
|
|
222
|
+
<Label>{{ loc(f.label) }}{{ f.required ? " *" : "" }}</Label>
|
|
223
|
+
|
|
224
|
+
<Input
|
|
225
|
+
v-if="f.type === 'secret'"
|
|
226
|
+
:model-value="form.config[f.key] ?? ''"
|
|
227
|
+
type="password"
|
|
228
|
+
autocomplete="new-password"
|
|
229
|
+
placeholder="Leave blank to keep current"
|
|
230
|
+
class="bg-white"
|
|
231
|
+
@update:model-value="(v) => form.config[f.key] = v"
|
|
232
|
+
/>
|
|
233
|
+
<Input
|
|
234
|
+
v-else-if="f.type === 'number'"
|
|
235
|
+
type="number"
|
|
236
|
+
:model-value="form.config[f.key] ?? ''"
|
|
237
|
+
class="bg-white"
|
|
238
|
+
@update:model-value="(v) => form.config[f.key] = v === '' ? void 0 : Number(v)"
|
|
239
|
+
/>
|
|
240
|
+
<div v-else-if="f.type === 'boolean'" class="flex items-center gap-2 pt-1">
|
|
241
|
+
<Switch :model-value="Boolean(form.config[f.key])" @update:model-value="(v) => form.config[f.key] = v" />
|
|
242
|
+
</div>
|
|
243
|
+
<Select
|
|
244
|
+
v-else-if="f.type === 'select'"
|
|
245
|
+
:model-value="form.config[f.key] ?? ''"
|
|
246
|
+
@update:model-value="(v) => form.config[f.key] = v"
|
|
247
|
+
>
|
|
248
|
+
<SelectTrigger class="bg-white">
|
|
249
|
+
<SelectValue placeholder="Pick one" />
|
|
250
|
+
</SelectTrigger>
|
|
251
|
+
<SelectContent>
|
|
252
|
+
<SelectItem v-for="opt in f.options ?? []" :key="String(opt.value)" :value="String(opt.value)">
|
|
253
|
+
{{ loc(opt.label) }}
|
|
254
|
+
</SelectItem>
|
|
255
|
+
</SelectContent>
|
|
256
|
+
</Select>
|
|
257
|
+
<Input
|
|
258
|
+
v-else
|
|
259
|
+
:model-value="form.config[f.key] ?? ''"
|
|
260
|
+
class="bg-white"
|
|
261
|
+
@update:model-value="(v) => form.config[f.key] = v"
|
|
262
|
+
/>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<!-- Live test feedback (from the test the operator just ran) -->
|
|
266
|
+
<div
|
|
267
|
+
v-if="liveTest"
|
|
268
|
+
class="rounded-md px-3 py-2 text-sm"
|
|
269
|
+
:class="liveTest.ok ? 'bg-emerald-500/10 text-emerald-700' : 'bg-rose-500/10 text-rose-700'"
|
|
270
|
+
>
|
|
271
|
+
{{ liveTest.ok ? "Connection ok." : liveTest.error ?? "Connection failed." }}
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<div class="flex items-center gap-2 pt-1">
|
|
275
|
+
<div class="flex-1" />
|
|
276
|
+
<Button variant="outline" :disabled="testing || saving" @click="runTest">
|
|
277
|
+
<Loader2 v-if="testing" :size="14" class="mr-1 animate-spin" />
|
|
278
|
+
<TestIcon v-else :size="14" class="mr-1" />
|
|
279
|
+
Connect
|
|
280
|
+
</Button>
|
|
281
|
+
<Button :disabled="saving || !form.name.trim()" @click="save">
|
|
282
|
+
<Loader2 v-if="saving" :size="14" class="mr-1 animate-spin" />
|
|
283
|
+
Save
|
|
284
|
+
</Button>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<!-- Tab 2: Details -->
|
|
291
|
+
<div v-else class="space-y-5 max-h-[62vh] overflow-y-auto pr-1 pt-4">
|
|
292
|
+
<div class="grid grid-cols-[9rem_1fr] gap-x-4 gap-y-2 text-sm">
|
|
293
|
+
<span class="text-muted-foreground">Type</span>
|
|
294
|
+
<span>{{ typeName }}</span>
|
|
295
|
+
|
|
296
|
+
<span class="text-muted-foreground">Type slug</span>
|
|
297
|
+
<span class="font-mono text-xs">{{ credential.credential_type_slug }}</span>
|
|
298
|
+
|
|
299
|
+
<span class="text-muted-foreground">Version</span>
|
|
300
|
+
<span class="font-mono text-xs">{{ type?.version ?? "\u2014" }}</span>
|
|
301
|
+
|
|
302
|
+
<span class="text-muted-foreground">Auth kind</span>
|
|
303
|
+
<span class="font-mono text-xs">{{ type?.auth_kind ?? "\u2014" }}</span>
|
|
304
|
+
|
|
305
|
+
<span class="text-muted-foreground">Last tested</span>
|
|
306
|
+
<span class="tabular-nums">{{ formatDateTime(credential.last_test_at) }}</span>
|
|
307
|
+
|
|
308
|
+
<span class="text-muted-foreground">Credential ID</span>
|
|
309
|
+
<span class="font-mono text-xs break-all">{{ credential.id }}</span>
|
|
310
|
+
|
|
311
|
+
<span class="text-muted-foreground">Created</span>
|
|
312
|
+
<span class="tabular-nums">{{ formatDateTime(credential.created_at) }}</span>
|
|
313
|
+
|
|
314
|
+
<span class="text-muted-foreground">Updated</span>
|
|
315
|
+
<span class="tabular-nums">{{ formatDateTime(credential.updated_at) }}</span>
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
<!-- Danger zone -->
|
|
319
|
+
<div class="rounded-lg border border-rose-500/30 bg-rose-500/[0.03] p-3">
|
|
320
|
+
<div class="text-sm font-semibold text-rose-700">Danger zone</div>
|
|
321
|
+
<div class="mt-1.5 flex items-center justify-between gap-3">
|
|
322
|
+
<p class="text-sm text-muted-foreground">
|
|
323
|
+
Deleting this credential breaks any workflow that references it until it's re-bound.
|
|
324
|
+
</p>
|
|
325
|
+
<Button
|
|
326
|
+
variant="outline"
|
|
327
|
+
size="sm"
|
|
328
|
+
class="shrink-0 border-rose-500/40 text-rose-700 hover:bg-rose-500/10"
|
|
329
|
+
:disabled="deleting"
|
|
330
|
+
@click="destroy"
|
|
331
|
+
>
|
|
332
|
+
<Loader2 v-if="deleting" :size="14" class="mr-1 animate-spin" />
|
|
333
|
+
<Trash2 v-else :size="14" class="mr-1" />
|
|
334
|
+
Delete credential
|
|
335
|
+
</Button>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
</DialogContent>
|
|
341
|
+
</Dialog>
|
|
342
|
+
</template>
|