@revenexx/studio-integrations 0.7.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 +416 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +4 -1
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +8 -2
- package/dist/runtime/components/Integrations/ExpressionInput.vue +5 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.d.vue.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +16 -3
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue.d.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +1 -0
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +2 -0
- 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 +95 -10
- 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 +34 -8
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +2 -1
- package/dist/runtime/components/Integrations/NodePalette.vue +12 -8
- package/dist/runtime/components/Integrations/StickyNoteView.vue +45 -11
- 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 +3 -3
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +12 -3
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +5 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +16 -0
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +52 -11
- 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 +4 -2
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +59 -28
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +3 -4
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.d.vue.ts +16 -17
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +37 -26
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue.d.ts +16 -17
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +4 -5
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +53 -14
- package/dist/runtime/composables/useIntegrationsCredentials.d.ts +11 -0
- package/dist/runtime/composables/useIntegrationsCredentials.js +2 -2
- package/dist/runtime/composables/useIntegrationsNodes.js +11 -1
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +3 -2
- package/dist/runtime/composables/useIntegrationsTriggers.d.ts +23 -3
- package/dist/runtime/composables/useIntegrationsTriggers.js +18 -7
- package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +10 -1
- package/dist/runtime/composables/useIntegrationsWorkflows.js +25 -4
- package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
- package/dist/runtime/data/integrationNodeCatalog.d.ts +22 -1
- package/dist/runtime/data/integrationNodeCatalog.js +8 -4
- package/dist/runtime/data/nodeInputData.d.ts +58 -1
- package/dist/runtime/data/nodeInputData.js +30 -5
- package/dist/runtime/data/triggerGlyphs.d.ts +47 -0
- package/dist/runtime/data/triggerGlyphs.js +11 -0
- package/dist/runtime/i18n/messages/de.json +20 -3
- package/dist/runtime/i18n/messages/en.json +20 -3
- package/dist/runtime/pages/integrations/credentials.vue +4 -1
- package/dist/runtime/pages/integrations/index.vue +16 -17
- package/dist/runtime/pages/integrations/templates/[slug].vue +13 -5
- package/dist/runtime/pages/integrations/workflows/[id].vue +193 -104
- 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 +6 -1
- package/dist/runtime/utils/credentialState.js +1 -1
- package/dist/runtime/utils/localize.d.ts +34 -0
- package/dist/runtime/utils/localize.js +22 -7
- package/dist/runtime/utils/pollBudget.d.ts +1 -1
- package/dist/runtime/utils/servedPaths.d.ts +22 -0
- package/dist/runtime/utils/servedPaths.js +3 -0
- package/package.json +1 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { ref, watch } from "vue";
|
|
3
|
-
import { RefreshCircle as Loader2 } from "#solar-icons";
|
|
2
|
+
import { computed, ref, watch } from "vue";
|
|
4
3
|
import {
|
|
5
4
|
Button,
|
|
6
5
|
Dialog,
|
|
7
6
|
DialogContent,
|
|
8
7
|
DialogDescription,
|
|
8
|
+
DialogFooter,
|
|
9
9
|
DialogHeader,
|
|
10
10
|
DialogTitle,
|
|
11
11
|
Input,
|
|
@@ -23,24 +23,35 @@ const props = defineProps({
|
|
|
23
23
|
open: { type: Boolean, required: true },
|
|
24
24
|
workflow: { type: Object, required: true },
|
|
25
25
|
modes: { type: Array, required: true },
|
|
26
|
-
|
|
27
|
-
savingMode: { type: Boolean, required: false },
|
|
28
|
-
savingMeta: { type: Boolean, required: false },
|
|
26
|
+
current: { type: Object, required: true },
|
|
29
27
|
tagSuggestions: { type: Array, required: false }
|
|
30
28
|
});
|
|
31
|
-
const emit = defineEmits(["update:open", "
|
|
29
|
+
const emit = defineEmits(["update:open", "apply"]);
|
|
32
30
|
const t = useIntegrationsT();
|
|
33
31
|
const nameDraft = ref("");
|
|
34
32
|
const descriptionDraft = ref("");
|
|
35
33
|
const tagsDraft = ref([]);
|
|
36
34
|
const iconDraft = ref(null);
|
|
35
|
+
const modeDraft = ref("");
|
|
37
36
|
watch(() => props.open, (v) => {
|
|
38
37
|
if (!v) return;
|
|
39
|
-
nameDraft.value = props.
|
|
40
|
-
descriptionDraft.value = props.
|
|
41
|
-
tagsDraft.value = [...props.
|
|
42
|
-
iconDraft.value = props.
|
|
38
|
+
nameDraft.value = props.current.name;
|
|
39
|
+
descriptionDraft.value = props.current.description;
|
|
40
|
+
tagsDraft.value = [...props.current.tags];
|
|
41
|
+
iconDraft.value = props.current.icon;
|
|
42
|
+
modeDraft.value = props.current.executionMode;
|
|
43
43
|
}, { immediate: true });
|
|
44
|
+
const edited = computed(() => ({
|
|
45
|
+
name: nameDraft.value.trim(),
|
|
46
|
+
description: descriptionDraft.value.trim(),
|
|
47
|
+
tags: tagsDraft.value,
|
|
48
|
+
icon: iconDraft.value,
|
|
49
|
+
executionMode: modeDraft.value
|
|
50
|
+
}));
|
|
51
|
+
function applyAndClose() {
|
|
52
|
+
if (JSON.stringify(edited.value) !== JSON.stringify(props.current)) emit("apply", edited.value);
|
|
53
|
+
emit("update:open", false);
|
|
54
|
+
}
|
|
44
55
|
function formatDateTime(iso) {
|
|
45
56
|
if (!iso) return "\u2014";
|
|
46
57
|
try {
|
|
@@ -57,7 +68,10 @@ function formatDateTime(iso) {
|
|
|
57
68
|
<DialogHeader>
|
|
58
69
|
<DialogTitle>{{ t("integration.workflows.settings.dialog.title") }}</DialogTitle>
|
|
59
70
|
<DialogDescription class="text-sm">
|
|
60
|
-
|
|
71
|
+
<!-- The name as it stands, drafted included: this dialog is where a rename is
|
|
72
|
+
typed, so naming the workflow by its stored name here contradicts the
|
|
73
|
+
field directly below. -->
|
|
74
|
+
{{ t("integration.workflows.settings.dialog.description", { name: current.name }) }}
|
|
61
75
|
</DialogDescription>
|
|
62
76
|
</DialogHeader>
|
|
63
77
|
|
|
@@ -66,9 +80,7 @@ function formatDateTime(iso) {
|
|
|
66
80
|
<div class="text-mono text-muted-foreground">{{ t("integration.workflows.settings.section.execution") }}</div>
|
|
67
81
|
<div class="flex items-center gap-2">
|
|
68
82
|
<Select
|
|
69
|
-
|
|
70
|
-
:disabled="savingMode"
|
|
71
|
-
@update:model-value="(v) => emit('change-mode', v)"
|
|
83
|
+
v-model="modeDraft"
|
|
72
84
|
>
|
|
73
85
|
<!-- Named explicitly: the section caption above says "Execution",
|
|
74
86
|
which is not a name the control itself carries. -->
|
|
@@ -81,7 +93,6 @@ function formatDateTime(iso) {
|
|
|
81
93
|
</SelectItem>
|
|
82
94
|
</SelectContent>
|
|
83
95
|
</Select>
|
|
84
|
-
<Loader2 v-if="savingMode" :size="14" class="shrink-0 animate-spin text-muted-foreground" />
|
|
85
96
|
</div>
|
|
86
97
|
<p class="text-xs text-muted-foreground">
|
|
87
98
|
{{ modes.find((m2) => m2.value === modeDraft)?.hint }}
|
|
@@ -109,17 +120,6 @@ function formatDateTime(iso) {
|
|
|
109
120
|
<Label id="wf-set-icon-label" class="text-sm font-normal text-muted-foreground">{{ t("integration.workflows.settings.field.icon.label") }}</Label>
|
|
110
121
|
<WorkflowIconPicker v-model="iconDraft" trigger-labelledby="wf-set-icon-label" />
|
|
111
122
|
</div>
|
|
112
|
-
<div class="flex justify-end">
|
|
113
|
-
<Button
|
|
114
|
-
size="sm"
|
|
115
|
-
class="h-7 text-sm"
|
|
116
|
-
:disabled="savingMeta || !nameDraft.trim()"
|
|
117
|
-
@click="emit('save-meta', { name: nameDraft.trim(), description: descriptionDraft.trim(), tags: tagsDraft, icon: iconDraft })"
|
|
118
|
-
>
|
|
119
|
-
<Loader2 v-if="savingMeta" :size="12" class="animate-spin" />
|
|
120
|
-
{{ t("integration.workflows.settings.action.save") }}
|
|
121
|
-
</Button>
|
|
122
|
-
</div>
|
|
123
123
|
</div>
|
|
124
124
|
|
|
125
125
|
<div class="rounded-lg border border-black/[0.06] bg-white p-4 space-y-2">
|
|
@@ -138,6 +138,17 @@ function formatDateTime(iso) {
|
|
|
138
138
|
</div>
|
|
139
139
|
</div>
|
|
140
140
|
</div>
|
|
141
|
+
|
|
142
|
+
<!-- Cancel / Done, the pair every dialog in the editor closes on. Done reaches
|
|
143
|
+
the workflow's unsaved state; the header's Save reaches the platform. -->
|
|
144
|
+
<DialogFooter>
|
|
145
|
+
<Button variant="outline" size="sm" class="h-8" @click="emit('update:open', false)">
|
|
146
|
+
{{ t("integration.common.action.cancel") }}
|
|
147
|
+
</Button>
|
|
148
|
+
<Button size="sm" class="h-8" :disabled="!nameDraft.trim()" @click="applyAndClose">
|
|
149
|
+
{{ t("integration.workflows.settings.action.done") }}
|
|
150
|
+
</Button>
|
|
151
|
+
</DialogFooter>
|
|
141
152
|
</DialogContent>
|
|
142
153
|
</Dialog>
|
|
143
154
|
</template>
|
|
@@ -4,33 +4,32 @@ export interface ExecutionModeOption {
|
|
|
4
4
|
label: string;
|
|
5
5
|
hint: string;
|
|
6
6
|
}
|
|
7
|
+
/** What this dialog edits — the values as they stand, saved or not. */
|
|
8
|
+
export interface WorkflowSettingsValues {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
tags: string[];
|
|
12
|
+
icon: WorkflowIcon | null;
|
|
13
|
+
executionMode: string;
|
|
14
|
+
}
|
|
7
15
|
type __VLS_Props = {
|
|
8
16
|
open: boolean;
|
|
9
17
|
workflow: Workflow;
|
|
10
18
|
modes: ExecutionModeOption[];
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
/**
|
|
20
|
+
* The values to edit: the editor's unsaved ones where it has any, the saved ones
|
|
21
|
+
* otherwise. Reopening the dialog therefore shows what the operator last entered
|
|
22
|
+
* rather than what the platform still holds.
|
|
23
|
+
*/
|
|
24
|
+
current: WorkflowSettingsValues;
|
|
14
25
|
/** Tag suggestions (tags already used across the tenant's workflows). */
|
|
15
26
|
tagSuggestions?: string[];
|
|
16
27
|
};
|
|
17
28
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
18
29
|
"update:open": (args_0: boolean) => any;
|
|
19
|
-
|
|
20
|
-
"save-meta": (meta: {
|
|
21
|
-
name: string;
|
|
22
|
-
description: string;
|
|
23
|
-
tags: string[];
|
|
24
|
-
icon: WorkflowIcon | null;
|
|
25
|
-
}) => any;
|
|
30
|
+
apply: (values: WorkflowSettingsValues) => any;
|
|
26
31
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
27
32
|
"onUpdate:open"?: ((args_0: boolean) => any) | undefined;
|
|
28
|
-
|
|
29
|
-
"onSave-meta"?: ((meta: {
|
|
30
|
-
name: string;
|
|
31
|
-
description: string;
|
|
32
|
-
tags: string[];
|
|
33
|
-
icon: WorkflowIcon | null;
|
|
34
|
-
}) => any) | undefined;
|
|
33
|
+
onApply?: ((values: WorkflowSettingsValues) => any) | undefined;
|
|
35
34
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
36
35
|
export default _default;
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { computed, ref } from "vue";
|
|
3
3
|
import { Handle, Position, useVueFlow } from "@vue-flow/core";
|
|
4
4
|
import { NodeToolbar } from "@vue-flow/node-toolbar";
|
|
5
|
-
import {
|
|
5
|
+
import { TrashBinMinimalistic as TrashBin } from "#solar-icons";
|
|
6
|
+
import { triggerGlyph as glyphForTrigger } from "../../data/triggerGlyphs";
|
|
6
7
|
import { resolveDisplayName } from "../../utils/localize";
|
|
7
8
|
import { triggerTypeLabel } from "../../data/integrationNodeCatalog";
|
|
8
9
|
import { describeCron } from "../../utils/schedule";
|
|
@@ -14,9 +15,7 @@ const props = defineProps({
|
|
|
14
15
|
defineOptions({ inheritAttrs: false });
|
|
15
16
|
defineEmits(["updateNodeInternals"]);
|
|
16
17
|
const t = useIntegrationsT();
|
|
17
|
-
const icon = computed(() =>
|
|
18
|
-
return props.data.type === "schedule" ? Clock : props.data.type === "webhook" ? Webhook : props.data.type === "event" ? Bolt : Play;
|
|
19
|
-
});
|
|
18
|
+
const icon = computed(() => glyphForTrigger(props.data.type));
|
|
20
19
|
const subtitle = computed(() => {
|
|
21
20
|
const c = props.data.config;
|
|
22
21
|
if (props.data.type === "schedule") return describeCron(c.cron) ?? c.cron ?? t("integration.canvas.trigger.subtitle.noSchedule");
|
|
@@ -77,7 +76,7 @@ function sideToPosition(s) {
|
|
|
77
76
|
identically (icon chip size-8/15, subtitle plain text-sm, mt-0.5). -->
|
|
78
77
|
<div class="flex items-center gap-2.5 px-3 py-2.5">
|
|
79
78
|
<span class="flex size-8 shrink-0 items-center justify-center rounded-md bg-emerald-500 text-white shadow-sm">
|
|
80
|
-
<component :is="icon" :size="15"
|
|
79
|
+
<component :is="icon" :size="15" />
|
|
81
80
|
</span>
|
|
82
81
|
<div class="min-w-0 flex-1">
|
|
83
82
|
<div class="text-sm font-semibold text-foreground leading-tight truncate">
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Dialog,
|
|
7
7
|
DialogContent,
|
|
8
8
|
DialogDescription,
|
|
9
|
+
DialogFooter,
|
|
9
10
|
DialogHeader,
|
|
10
11
|
DialogTitle,
|
|
11
12
|
Input,
|
|
@@ -18,7 +19,7 @@ import {
|
|
|
18
19
|
SelectValue
|
|
19
20
|
} from "@revenexx/studio";
|
|
20
21
|
import { toast } from "vue-sonner";
|
|
21
|
-
import { resolveDisplayName } from "../../utils/localize";
|
|
22
|
+
import { languageKeyRead, languageKeyToWrite, resolveDisplayName } from "../../utils/localize";
|
|
22
23
|
const props = defineProps({
|
|
23
24
|
open: { type: Boolean, required: true },
|
|
24
25
|
variables: { type: Object, required: true }
|
|
@@ -27,32 +28,50 @@ const emit = defineEmits(["update:open", "update:variables"]);
|
|
|
27
28
|
const t = useIntegrationsT();
|
|
28
29
|
const KEY_PATTERN = /^[a-z_][a-z0-9_]*$/i;
|
|
29
30
|
const TYPES = ["string", "number", "boolean", "object", "array"];
|
|
31
|
+
const working = ref({});
|
|
30
32
|
const jsonDrafts = reactive({});
|
|
31
33
|
const jsonErrors = reactive({});
|
|
34
|
+
const descDrafts = reactive({});
|
|
32
35
|
watch(() => props.open, (o) => {
|
|
33
36
|
if (!o) return;
|
|
37
|
+
working.value = JSON.parse(JSON.stringify(props.variables));
|
|
34
38
|
for (const k of Object.keys(jsonDrafts)) delete jsonDrafts[k];
|
|
35
39
|
for (const k of Object.keys(jsonErrors)) delete jsonErrors[k];
|
|
36
|
-
for (const
|
|
40
|
+
for (const k of Object.keys(descDrafts)) delete descDrafts[k];
|
|
41
|
+
for (const [key, v] of Object.entries(working.value)) {
|
|
42
|
+
descDrafts[key] = descriptionText(v);
|
|
37
43
|
if (v.type === "object" || v.type === "array") {
|
|
38
44
|
jsonDrafts[key] = v.default === void 0 ? "" : JSON.stringify(v.default, null, 2);
|
|
39
45
|
jsonErrors[key] = null;
|
|
40
46
|
}
|
|
41
47
|
}
|
|
42
48
|
});
|
|
43
|
-
const entries = computed(() => Object.entries(
|
|
49
|
+
const entries = computed(() => Object.entries(working.value));
|
|
50
|
+
const changed = computed(() => JSON.stringify(working.value) !== JSON.stringify(props.variables));
|
|
51
|
+
const hasJsonError = computed(() => Object.values(jsonErrors).some(Boolean));
|
|
44
52
|
function commit(next) {
|
|
45
|
-
|
|
53
|
+
working.value = next;
|
|
54
|
+
}
|
|
55
|
+
function applyAndClose() {
|
|
56
|
+
for (const [key, v] of Object.entries(working.value)) {
|
|
57
|
+
if ((v.type === "object" || v.type === "array") && jsonDrafts[key] !== void 0) {
|
|
58
|
+
applyJsonDraft(key, v.type);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (hasJsonError.value) return;
|
|
62
|
+
if (changed.value) emit("update:variables", working.value);
|
|
63
|
+
emit("update:open", false);
|
|
46
64
|
}
|
|
47
65
|
function patchVar(key, patch) {
|
|
48
|
-
const cur =
|
|
66
|
+
const cur = working.value[key];
|
|
49
67
|
if (!cur) return;
|
|
50
|
-
commit({ ...
|
|
68
|
+
commit({ ...working.value, [key]: { ...cur, ...patch } });
|
|
51
69
|
}
|
|
52
70
|
function removeVar(key) {
|
|
53
|
-
const next = { ...
|
|
71
|
+
const next = { ...working.value };
|
|
54
72
|
delete next[key];
|
|
55
73
|
delete jsonDrafts[key];
|
|
74
|
+
delete descDrafts[key];
|
|
56
75
|
delete jsonErrors[key];
|
|
57
76
|
commit(next);
|
|
58
77
|
}
|
|
@@ -65,6 +84,8 @@ function changeType(key, type) {
|
|
|
65
84
|
array: void 0
|
|
66
85
|
};
|
|
67
86
|
patchVar(key, { type, default: blank[type] });
|
|
87
|
+
delete jsonDrafts[key];
|
|
88
|
+
delete jsonErrors[key];
|
|
68
89
|
if (type === "object" || type === "array") {
|
|
69
90
|
jsonDrafts[key] = "";
|
|
70
91
|
jsonErrors[key] = null;
|
|
@@ -94,28 +115,34 @@ function descriptionText(v) {
|
|
|
94
115
|
return resolveDisplayName(v.description);
|
|
95
116
|
}
|
|
96
117
|
function writeDescription(key, text) {
|
|
97
|
-
const cur =
|
|
118
|
+
const cur = working.value[key];
|
|
98
119
|
if (!cur) return;
|
|
120
|
+
descDrafts[key] = text;
|
|
121
|
+
const existing = typeof cur.description === "object" && cur.description !== null ? { ...cur.description } : {};
|
|
99
122
|
if (!text.trim()) {
|
|
123
|
+
delete existing[languageKeyRead(cur.description)];
|
|
124
|
+
if (Object.keys(existing).length) {
|
|
125
|
+
patchVar(key, { description: existing });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
100
128
|
const { description: _drop, ...rest } = cur;
|
|
101
|
-
commit({ ...
|
|
129
|
+
commit({ ...working.value, [key]: rest });
|
|
102
130
|
return;
|
|
103
131
|
}
|
|
104
|
-
|
|
105
|
-
patchVar(key, { description: { ...existing, en: text } });
|
|
132
|
+
patchVar(key, { description: { ...existing, [languageKeyToWrite(cur.description)]: text } });
|
|
106
133
|
}
|
|
107
134
|
const newKey = ref("");
|
|
108
135
|
const newKeyError = computed(() => {
|
|
109
136
|
const k = newKey.value.trim();
|
|
110
137
|
if (!k) return null;
|
|
111
138
|
if (!KEY_PATTERN.test(k)) return t("integration.workflows.variables.error.invalidKey");
|
|
112
|
-
if (
|
|
139
|
+
if (working.value[k]) return t("integration.workflows.variables.error.keyExists");
|
|
113
140
|
return null;
|
|
114
141
|
});
|
|
115
142
|
function addVariable() {
|
|
116
143
|
const k = newKey.value.trim();
|
|
117
144
|
if (!k || newKeyError.value) return;
|
|
118
|
-
commit({ ...
|
|
145
|
+
commit({ ...working.value, [k]: { type: "string", default: "" } });
|
|
119
146
|
newKey.value = "";
|
|
120
147
|
}
|
|
121
148
|
async function copyUsage(key) {
|
|
@@ -247,7 +274,7 @@ async function copyUsage(key) {
|
|
|
247
274
|
<Label :for="`var-${key}-desc`" class="text-mono text-muted-foreground">{{ t("integration.workflows.variables.field.description.label") }}</Label>
|
|
248
275
|
<Input
|
|
249
276
|
:id="`var-${key}-desc`"
|
|
250
|
-
:model-value="descriptionText(v)"
|
|
277
|
+
:model-value="descDrafts[key] ?? descriptionText(v)"
|
|
251
278
|
class="h-8 bg-white text-sm"
|
|
252
279
|
:placeholder="t('integration.workflows.variables.field.description.placeholder')"
|
|
253
280
|
@update:model-value="(val) => writeDescription(key, String(val))"
|
|
@@ -274,6 +301,18 @@ async function copyUsage(key) {
|
|
|
274
301
|
<p v-if="newKeyError" class="mt-1 text-xs text-rose-600">{{ newKeyError }}</p>
|
|
275
302
|
</div>
|
|
276
303
|
</div>
|
|
304
|
+
|
|
305
|
+
<!-- Cancel / Done, the same pair every dialog in the editor closes on. Done
|
|
306
|
+
reaches the workflow's unsaved state; only the header's Save reaches the
|
|
307
|
+
platform. -->
|
|
308
|
+
<DialogFooter>
|
|
309
|
+
<Button variant="outline" size="sm" class="h-8" @click="emit('update:open', false)">
|
|
310
|
+
{{ t("integration.common.action.cancel") }}
|
|
311
|
+
</Button>
|
|
312
|
+
<Button size="sm" class="h-8" :disabled="hasJsonError" @click="applyAndClose">
|
|
313
|
+
{{ t("integration.workflows.variables.action.done") }}
|
|
314
|
+
</Button>
|
|
315
|
+
</DialogFooter>
|
|
277
316
|
</DialogContent>
|
|
278
317
|
</Dialog>
|
|
279
318
|
</template>
|
|
@@ -13,8 +13,19 @@ import type { Credential } from '../types/integrations.js';
|
|
|
13
13
|
* POST /credentials/{id}/test → test saved instance
|
|
14
14
|
* POST /credentials/{id}/oauth/authorize-url → { authorize_url, state }
|
|
15
15
|
*/
|
|
16
|
+
/**
|
|
17
|
+
* `message` is what the credential type said about the test, whichever way it went —
|
|
18
|
+
* which host answered, which environment, which plan, or why it was refused. The
|
|
19
|
+
* service sends it on every path, and it may be absent.
|
|
20
|
+
*
|
|
21
|
+
* `error` is not the service's: it is what THIS composable puts there when the call
|
|
22
|
+
* never arrived — a network failure, a refusal that is not an answer. Nothing on the
|
|
23
|
+
* wire ever carries it, so a surface that reads `error` alone and falls back to a
|
|
24
|
+
* fixed sentence throws away every reason the platform did send.
|
|
25
|
+
*/
|
|
16
26
|
export interface CredentialTestResult {
|
|
17
27
|
ok: boolean;
|
|
28
|
+
message?: string;
|
|
18
29
|
error?: string;
|
|
19
30
|
}
|
|
20
31
|
export declare function useIntegrationsCredentials(): {
|
|
@@ -60,7 +60,7 @@ export function useIntegrationsCredentials() {
|
|
|
60
60
|
async function testInline(credentialTypeSlug, config) {
|
|
61
61
|
try {
|
|
62
62
|
const res = await api.post("/credentials/test", { credential_type_slug: credentialTypeSlug, config });
|
|
63
|
-
return res?.ok === false ? res : { ok: true };
|
|
63
|
+
return res?.ok === false ? res : { ok: true, message: res?.message };
|
|
64
64
|
} catch (e) {
|
|
65
65
|
return { ok: false, error: captureError(e, t("integration.credentials.error.testFailed")) };
|
|
66
66
|
}
|
|
@@ -68,7 +68,7 @@ export function useIntegrationsCredentials() {
|
|
|
68
68
|
async function test(id) {
|
|
69
69
|
try {
|
|
70
70
|
const res = await api.post(`/credentials/${encodeURIComponent(id)}/test`);
|
|
71
|
-
return res?.ok === false ? res : { ok: true };
|
|
71
|
+
return res?.ok === false ? res : { ok: true, message: res?.message };
|
|
72
72
|
} catch (e) {
|
|
73
73
|
return { ok: false, error: captureError(e, t("integration.credentials.error.testFailed")) };
|
|
74
74
|
}
|
|
@@ -4,14 +4,17 @@ import { useIntegrationsApi } from "./integrationsApi.js";
|
|
|
4
4
|
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
5
5
|
import { IntegrationsApiError } from "../utils/apiError.js";
|
|
6
6
|
import { useLoadGate } from "./useLoadGate.js";
|
|
7
|
+
const RETRY_COOLDOWN_MS = 3e4;
|
|
7
8
|
export function useIntegrationsNodes() {
|
|
8
9
|
const loading = useState("integrations.nodes.loading", () => false);
|
|
9
10
|
const error = useState("integrations.nodes.error", () => null);
|
|
10
11
|
const loaded = useState("integrations.nodes.loaded", () => false);
|
|
12
|
+
const failedAt = useState("integrations.nodes.failedAt", () => 0);
|
|
11
13
|
const gate = useLoadGate("integrations.nodes", { loading, loaded });
|
|
12
14
|
const api = useIntegrationsApi();
|
|
13
15
|
const t = useIntegrationsT();
|
|
14
16
|
async function load(force = false) {
|
|
17
|
+
if (!force && failedAt.value && Date.now() - failedAt.value < RETRY_COOLDOWN_MS) return;
|
|
15
18
|
if (!await gate.claim(force)) return;
|
|
16
19
|
error.value = null;
|
|
17
20
|
try {
|
|
@@ -23,12 +26,19 @@ export function useIntegrationsNodes() {
|
|
|
23
26
|
version: n.manifest.version ?? n.version,
|
|
24
27
|
name: n.manifest.name ?? n.name,
|
|
25
28
|
// Keep the source package — the palette groups app nodes by it.
|
|
26
|
-
__package: n.package ? { name: n.package.name, label: n.package.label ?? null } : void 0
|
|
29
|
+
__package: n.package ? { name: n.package.name, label: n.package.label ?? null } : void 0,
|
|
30
|
+
// Registered and built are two different things: a version whose code
|
|
31
|
+
// was never built can be drawn but not executed, so the palette must
|
|
32
|
+
// be able to leave it out (PO-338). Missing field = assume built.
|
|
33
|
+
__hasBundle: n.has_bundle !== false
|
|
27
34
|
}));
|
|
28
35
|
applyNodeCatalog(manifests);
|
|
29
36
|
loaded.value = true;
|
|
37
|
+
failedAt.value = 0;
|
|
30
38
|
} catch (e) {
|
|
31
39
|
applyNodeCatalog([]);
|
|
40
|
+
loaded.value = false;
|
|
41
|
+
failedAt.value = Date.now();
|
|
32
42
|
error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? t("integration.nodes.error.loadFailed");
|
|
33
43
|
} finally {
|
|
34
44
|
gate.release();
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ScheduleEntry } from '../types/integrations.js';
|
|
2
2
|
/**
|
|
3
|
-
* Derived view — every trigger of type `schedule` across all workflows,
|
|
4
|
-
*
|
|
3
|
+
* Derived view — every trigger of type `schedule` across all workflows, as
|
|
4
|
+
* `useIntegrationsTriggers` holds them: read out of the workflow list, which carries
|
|
5
|
+
* each workflow's triggers, rather than fetched per workflow (PO-236).
|
|
5
6
|
* Next/last-run timestamps are mocked deterministically (no telemetry
|
|
6
7
|
* endpoint yet).
|
|
7
8
|
*
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import type { TriggerType } from '../types/integrations.js';
|
|
2
|
+
import type { ApiTriggerLite } from './useIntegrationsWorkflows.js';
|
|
2
3
|
/**
|
|
3
4
|
* Workflow triggers — a per-workflow sub-resource in the Integrations service
|
|
4
|
-
* (`/workflows/{id}/triggers`), NOT part of the workflow blob.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* (`/workflows/{id}/triggers`), NOT part of the workflow blob. `list()`, `create()`,
|
|
6
|
+
* `update()` and `remove()` address that sub-resource, one workflow at a time, which
|
|
7
|
+
* is how the editor's trigger settings work.
|
|
8
|
+
*
|
|
9
|
+
* `loadAll()` does NOT: the workflow list carries each workflow's triggers in full,
|
|
10
|
+
* so the tenant-wide view is derived from that response rather than fetched per
|
|
11
|
+
* workflow (PO-236). What it still costs is the workflow list, once.
|
|
7
12
|
*/
|
|
8
13
|
export interface ApiTrigger {
|
|
9
14
|
id: number;
|
|
@@ -26,6 +31,21 @@ export interface TriggerInput {
|
|
|
26
31
|
config?: Record<string, unknown> | null;
|
|
27
32
|
active?: boolean;
|
|
28
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* The tenant's triggers, read off the workflow list rather than fetched per workflow
|
|
36
|
+
* (PO-236). Pure, and exported for that reason: it is the whole of what the tenant-wide
|
|
37
|
+
* view now does, and the composable around it needs a Nuxt app that the unit setup has
|
|
38
|
+
* no environment for.
|
|
39
|
+
*
|
|
40
|
+
* `byWorkflow` holds the triggers as the API sent them (`useIntegrationsWorkflows`
|
|
41
|
+
* keeps them beside the cockpit model, whose `id` is the handle). Two fields are not
|
|
42
|
+
* in there and are filled from the workflow the triggers arrived with: `workflow_id`,
|
|
43
|
+
* which every row id and route is built from, and `workflowName`, which the views show.
|
|
44
|
+
*/
|
|
45
|
+
export declare function tenantTriggers(workflows: {
|
|
46
|
+
id: number;
|
|
47
|
+
name: string;
|
|
48
|
+
}[], byWorkflow: Record<number, ApiTriggerLite[]>): TriggerWithWorkflow[];
|
|
29
49
|
export declare function useIntegrationsTriggers(): {
|
|
30
50
|
all: any;
|
|
31
51
|
loading: any;
|
|
@@ -2,6 +2,19 @@ import { useIntegrationsApi } from "./integrationsApi.js";
|
|
|
2
2
|
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
3
3
|
import { IntegrationsApiError } from "../utils/apiError.js";
|
|
4
4
|
import { useLoadGate } from "./useLoadGate.js";
|
|
5
|
+
export function tenantTriggers(workflows, byWorkflow) {
|
|
6
|
+
return workflows.flatMap((wf) => (byWorkflow[wf.id] ?? []).flatMap((t) => {
|
|
7
|
+
if (typeof t.id !== "number") return [];
|
|
8
|
+
return [{
|
|
9
|
+
...t,
|
|
10
|
+
id: t.id,
|
|
11
|
+
workflow_id: wf.id,
|
|
12
|
+
workflowName: wf.name,
|
|
13
|
+
handle: t.handle ?? "",
|
|
14
|
+
active: t.active ?? true
|
|
15
|
+
}];
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
5
18
|
export function useIntegrationsTriggers() {
|
|
6
19
|
const all = useState("integrations.triggers.all", () => []);
|
|
7
20
|
const loading = useState("integrations.triggers.loading", () => false);
|
|
@@ -9,7 +22,7 @@ export function useIntegrationsTriggers() {
|
|
|
9
22
|
const loaded = useState("integrations.triggers.loaded", () => false);
|
|
10
23
|
const api = useIntegrationsApi();
|
|
11
24
|
const t = useIntegrationsT();
|
|
12
|
-
const { workflows, load: loadWorkflows, loaded: workflowsLoaded } = useIntegrationsWorkflows();
|
|
25
|
+
const { workflows, apiTriggers, load: loadWorkflows, loaded: workflowsLoaded, error: workflowsError } = useIntegrationsWorkflows();
|
|
13
26
|
const gate = useLoadGate("integrations.triggers", { loading, loaded });
|
|
14
27
|
function unwrapList(res) {
|
|
15
28
|
if (Array.isArray(res)) return res;
|
|
@@ -22,12 +35,10 @@ export function useIntegrationsTriggers() {
|
|
|
22
35
|
if (!await gate.claim(force)) return all.value;
|
|
23
36
|
error.value = null;
|
|
24
37
|
try {
|
|
25
|
-
await loadWorkflows();
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
all.value = settled.flatMap((p) => p.status === "fulfilled" ? p.value : []);
|
|
30
|
-
loaded.value = workflowsLoaded.value && settled.every((p) => p.status === "fulfilled");
|
|
38
|
+
await loadWorkflows(force);
|
|
39
|
+
all.value = tenantTriggers(workflows.value, apiTriggers.value);
|
|
40
|
+
error.value = workflowsError.value;
|
|
41
|
+
loaded.value = workflowsLoaded.value && !workflowsError.value;
|
|
31
42
|
} catch (e) {
|
|
32
43
|
error.value = e instanceof IntegrationsApiError ? e.message : e?.message ?? t("integration.triggers.error.loadFailed");
|
|
33
44
|
} finally {
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import type { Workflow, WorkflowBlob, WorkflowExecutionMode } from '../types/integrations.js';
|
|
1
|
+
import type { TriggerType, Workflow, WorkflowBlob, WorkflowExecutionMode } from '../types/integrations.js';
|
|
2
|
+
export interface ApiTriggerLite {
|
|
3
|
+
id?: number;
|
|
4
|
+
handle?: string;
|
|
5
|
+
type: TriggerType;
|
|
6
|
+
name?: string | Record<string, string> | null;
|
|
7
|
+
config?: Record<string, unknown> | null;
|
|
8
|
+
active?: boolean;
|
|
9
|
+
}
|
|
2
10
|
export declare function useIntegrationsWorkflows(): {
|
|
3
11
|
workflows: any;
|
|
4
12
|
loading: any;
|
|
5
13
|
error: any;
|
|
6
14
|
loaded: any;
|
|
15
|
+
apiTriggers: any;
|
|
7
16
|
total: import("vue").ComputedRef<any>;
|
|
8
17
|
activeCount: import("vue").ComputedRef<any>;
|
|
9
18
|
load: (force?: boolean) => Promise<Workflow[]>;
|
|
@@ -82,6 +82,7 @@ export function useIntegrationsWorkflows() {
|
|
|
82
82
|
const loading = useState("integrations.workflows.loading", () => false);
|
|
83
83
|
const error = useState("integrations.workflows.error", () => null);
|
|
84
84
|
const loaded = useState("integrations.workflows.loaded", () => false);
|
|
85
|
+
const apiTriggers = useState("integrations.workflows.apiTriggers", () => ({}));
|
|
85
86
|
const gate = useLoadGate("integrations.workflows", { loading, loaded });
|
|
86
87
|
const api = useIntegrationsApi();
|
|
87
88
|
const t = useIntegrationsT();
|
|
@@ -92,6 +93,24 @@ export function useIntegrationsWorkflows() {
|
|
|
92
93
|
if (e instanceof IntegrationsApiError) return e.message;
|
|
93
94
|
return e?.message ?? fallback;
|
|
94
95
|
}
|
|
96
|
+
function remember(api2, into) {
|
|
97
|
+
if (api2.triggers !== void 0) into[Number(api2.id)] = api2.triggers;
|
|
98
|
+
}
|
|
99
|
+
function ingest(api2) {
|
|
100
|
+
const next = { ...apiTriggers.value };
|
|
101
|
+
remember(api2, next);
|
|
102
|
+
apiTriggers.value = next;
|
|
103
|
+
return toCockpit(api2);
|
|
104
|
+
}
|
|
105
|
+
function ingestList(list2) {
|
|
106
|
+
const next = { ...apiTriggers.value };
|
|
107
|
+
const out = list2.map((api2) => {
|
|
108
|
+
remember(api2, next);
|
|
109
|
+
return toCockpit(api2);
|
|
110
|
+
});
|
|
111
|
+
apiTriggers.value = next;
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
95
114
|
function upsertCache(wf) {
|
|
96
115
|
const idx = workflows.value.findIndex((w) => w.id === wf.id);
|
|
97
116
|
workflows.value = idx < 0 ? [...workflows.value, wf] : workflows.value.map((w) => w.id === wf.id ? wf : w);
|
|
@@ -101,10 +120,11 @@ export function useIntegrationsWorkflows() {
|
|
|
101
120
|
error.value = null;
|
|
102
121
|
try {
|
|
103
122
|
const list2 = unwrap(await api.get("/workflows")) ?? [];
|
|
104
|
-
workflows.value = list2
|
|
123
|
+
workflows.value = ingestList(list2);
|
|
105
124
|
loaded.value = true;
|
|
106
125
|
} catch (e) {
|
|
107
126
|
error.value = captureError(e, t("integration.workflows.error.loadFailed"));
|
|
127
|
+
loaded.value = false;
|
|
108
128
|
} finally {
|
|
109
129
|
gate.release();
|
|
110
130
|
}
|
|
@@ -116,7 +136,7 @@ export function useIntegrationsWorkflows() {
|
|
|
116
136
|
}
|
|
117
137
|
async function get(id) {
|
|
118
138
|
try {
|
|
119
|
-
const wf =
|
|
139
|
+
const wf = ingest(unwrap(await api.get(`/workflows/${id}`)));
|
|
120
140
|
upsertCache(wf);
|
|
121
141
|
return wf;
|
|
122
142
|
} catch {
|
|
@@ -124,7 +144,7 @@ export function useIntegrationsWorkflows() {
|
|
|
124
144
|
}
|
|
125
145
|
}
|
|
126
146
|
async function create(input) {
|
|
127
|
-
const wf =
|
|
147
|
+
const wf = ingest(unwrap(await api.post("/workflows", toPayload({
|
|
128
148
|
name: input.name,
|
|
129
149
|
description: input.description,
|
|
130
150
|
blob: input.blob,
|
|
@@ -175,7 +195,7 @@ export function useIntegrationsWorkflows() {
|
|
|
175
195
|
throw lastError instanceof Error ? lastError : new Error(t("integration.workflows.error.duplicateFailed"));
|
|
176
196
|
}
|
|
177
197
|
async function update(id, patch) {
|
|
178
|
-
const wf =
|
|
198
|
+
const wf = ingest(unwrap(await api.put(`/workflows/${id}`, toPayload({
|
|
179
199
|
name: patch.name,
|
|
180
200
|
description: patch.description,
|
|
181
201
|
blob: patch.blob,
|
|
@@ -201,6 +221,7 @@ export function useIntegrationsWorkflows() {
|
|
|
201
221
|
loading,
|
|
202
222
|
error,
|
|
203
223
|
loaded,
|
|
224
|
+
apiTriggers,
|
|
204
225
|
total,
|
|
205
226
|
activeCount,
|
|
206
227
|
load,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { computed, onScopeDispose, ref, toValue, watch } from "vue";
|
|
2
|
+
import { isVersionExecutable } from "../data/integrationNodeCatalog.js";
|
|
2
3
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
4
|
import { IntegrationsApiError } from "../utils/apiError.js";
|
|
4
5
|
export function isResolvableValue(v) {
|
|
@@ -54,7 +55,7 @@ export function useNodeConfigResolve(opts) {
|
|
|
54
55
|
});
|
|
55
56
|
const gated = computed(() => unsatisfied.value.length > 0);
|
|
56
57
|
const isEnabled = computed(
|
|
57
|
-
() => (toValue(opts.enabled) ?? true) && !gated.value && !!toValue(opts.slug) && !!toValue(opts.version)
|
|
58
|
+
() => (toValue(opts.enabled) ?? true) && isVersionExecutable(toValue(opts.slug), toValue(opts.version)) && !gated.value && !!toValue(opts.slug) && !!toValue(opts.version)
|
|
58
59
|
);
|
|
59
60
|
const subsetKey = computed(() => {
|
|
60
61
|
const cfg = toValue(opts.config);
|