@revenexx/studio-integrations 0.7.0 → 1.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/CHANGELOG.md +633 -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/DeadLettersPanel.vue +2 -6
- 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 +135 -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/RunDetailPanel.vue +9 -6
- package/dist/runtime/components/Integrations/RunsTable.vue +2 -2
- 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 +79 -12
- 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 +13 -3
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +13 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +95 -29
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +13 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +11 -10
- 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/components/Integrations/WorkflowsTable.vue +9 -4
- package/dist/runtime/composables/useIntegrationsCredentials.d.ts +11 -0
- package/dist/runtime/composables/useIntegrationsCredentials.js +2 -2
- package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +9 -2
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +27 -18
- package/dist/runtime/composables/useIntegrationsNodes.js +11 -1
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +30 -1
- package/dist/runtime/composables/useIntegrationsRuns.js +70 -29
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +7 -14
- package/dist/runtime/composables/useIntegrationsSchedules.js +80 -35
- package/dist/runtime/composables/useIntegrationsTriggers.d.ts +30 -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/loopBody.d.ts +129 -0
- package/dist/runtime/data/loopBody.js +76 -0
- 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 +62 -33
- package/dist/runtime/i18n/messages/en.json +32 -3
- package/dist/runtime/pages/integrations/credentials.vue +4 -1
- package/dist/runtime/pages/integrations/index.vue +32 -34
- package/dist/runtime/pages/integrations/runs.vue +22 -10
- package/dist/runtime/pages/integrations/schedules.vue +64 -7
- 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 +64 -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/dist/runtime/utils/statusStyle.d.ts +52 -0
- package/dist/runtime/utils/statusStyle.js +26 -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>
|
|
@@ -50,13 +50,18 @@ watch(groupByTags, (on) => {
|
|
|
50
50
|
} catch {
|
|
51
51
|
}
|
|
52
52
|
});
|
|
53
|
-
const {
|
|
53
|
+
const {
|
|
54
|
+
latestPerWorkflow,
|
|
55
|
+
latestLoaded: runsLoaded,
|
|
56
|
+
loadLatestPerWorkflow,
|
|
57
|
+
lastRunFor: latestRunFor
|
|
58
|
+
} = useIntegrationsRuns();
|
|
54
59
|
onMounted(() => {
|
|
55
|
-
void
|
|
60
|
+
void loadLatestPerWorkflow();
|
|
56
61
|
});
|
|
57
62
|
function lastRunFor(id) {
|
|
58
|
-
void
|
|
59
|
-
return
|
|
63
|
+
void latestPerWorkflow.value.length;
|
|
64
|
+
return latestRunFor(id);
|
|
60
65
|
}
|
|
61
66
|
function runBadge(status) {
|
|
62
67
|
switch (status) {
|
|
@@ -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
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { DeadLetterRow } from '../types/integrations.js';
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* A dead letter as the tenant-wide list serves it: the platform now reports the
|
|
4
|
+
* workflow's name on the row, where the fan-out had to inject it from the loop
|
|
5
|
+
* variable.
|
|
6
|
+
*/
|
|
3
7
|
export interface GlobalDeadLetterRow extends DeadLetterRow {
|
|
4
|
-
|
|
8
|
+
workflow_name?: string | null;
|
|
5
9
|
}
|
|
6
10
|
export declare function useIntegrationsDeadLetters(): {
|
|
7
11
|
list: (workflowId: number | string, opts?: {
|
|
@@ -18,5 +22,8 @@ export declare function useIntegrationsDeadLetters(): {
|
|
|
18
22
|
loadGlobal: (force?: boolean) => Promise<GlobalDeadLetterRow[]>;
|
|
19
23
|
globalItems: any;
|
|
20
24
|
globalLoading: any;
|
|
25
|
+
globalLoaded: any;
|
|
26
|
+
globalHasMore: any;
|
|
27
|
+
summary: any;
|
|
21
28
|
pendingCount: import("vue").ComputedRef<any>;
|
|
22
29
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
2
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
3
|
import { useLoadGate } from "./useLoadGate.js";
|
|
4
|
+
const EMPTY_SUMMARY = { total: 0, by_status: {} };
|
|
4
5
|
export function useIntegrationsDeadLetters() {
|
|
5
6
|
const api = useIntegrationsApi();
|
|
6
7
|
async function list(workflowId, opts = {}) {
|
|
@@ -18,34 +19,42 @@ export function useIntegrationsDeadLetters() {
|
|
|
18
19
|
}
|
|
19
20
|
const replay = (id) => api.post(`/dead-letters/${id}/replay`, {});
|
|
20
21
|
const discard = (id) => api.post(`/dead-letters/${id}/discard`, {});
|
|
21
|
-
const { workflows, load: loadWorkflows, loaded: workflowsLoaded } = useIntegrationsWorkflows();
|
|
22
22
|
const globalItems = useState("integrations.deadletters.global", () => []);
|
|
23
23
|
const globalLoading = useState("integrations.deadletters.global.loading", () => false);
|
|
24
24
|
const globalLoaded = useState("integrations.deadletters.global.loaded", () => false);
|
|
25
25
|
const gate = useLoadGate("integrations.deadletters.global", { loading: globalLoading, loaded: globalLoaded });
|
|
26
|
-
const
|
|
26
|
+
const summary = useState("integrations.deadletters.summary", () => ({ ...EMPTY_SUMMARY }));
|
|
27
|
+
const globalHasMore = useState("integrations.deadletters.global.hasMore", () => false);
|
|
28
|
+
const pendingCount = computed(() => summary.value.by_status.pending ?? 0);
|
|
27
29
|
async function loadGlobal(force = false) {
|
|
28
30
|
if (!await gate.claim(force)) return globalItems.value;
|
|
29
31
|
try {
|
|
30
|
-
await
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return out;
|
|
42
|
-
}));
|
|
43
|
-
globalItems.value = per.flatMap((p) => p.status === "fulfilled" ? p.value : []).sort((a, b) => new Date(b.created_at ?? 0).getTime() - new Date(a.created_at ?? 0).getTime());
|
|
44
|
-
globalLoaded.value = workflowsLoaded.value && per.every((p) => p.status === "fulfilled");
|
|
32
|
+
const [page, counts] = await Promise.all([
|
|
33
|
+
api.get("/dead-letters?page=1&per_page=100"),
|
|
34
|
+
api.get("/dead-letters/summary")
|
|
35
|
+
]);
|
|
36
|
+
globalItems.value = page?.data ?? [];
|
|
37
|
+
globalHasMore.value = page?.meta?.has_more ?? false;
|
|
38
|
+
summary.value = counts?.data ?? { ...EMPTY_SUMMARY };
|
|
39
|
+
globalLoaded.value = true;
|
|
40
|
+
} catch (e) {
|
|
41
|
+
globalLoaded.value = false;
|
|
42
|
+
throw e;
|
|
45
43
|
} finally {
|
|
46
44
|
gate.release();
|
|
47
45
|
}
|
|
48
46
|
return globalItems.value;
|
|
49
47
|
}
|
|
50
|
-
return {
|
|
48
|
+
return {
|
|
49
|
+
list,
|
|
50
|
+
replay,
|
|
51
|
+
discard,
|
|
52
|
+
loadGlobal,
|
|
53
|
+
globalItems,
|
|
54
|
+
globalLoading,
|
|
55
|
+
globalLoaded,
|
|
56
|
+
globalHasMore,
|
|
57
|
+
summary,
|
|
58
|
+
pendingCount
|
|
59
|
+
};
|
|
51
60
|
}
|
|
@@ -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,13 +1,36 @@
|
|
|
1
1
|
import type { RunStatus, RunStepRow, WorkflowRun } from '../types/integrations.js';
|
|
2
|
+
interface RunSummary {
|
|
3
|
+
total: number;
|
|
4
|
+
in_flight: number;
|
|
5
|
+
by_status: Record<string, number>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Which page of the tenant's list to read.
|
|
9
|
+
*
|
|
10
|
+
* Only the page: `/runs` also narrows by status, trigger kind, workflow, window
|
|
11
|
+
* and text, and none of that is driven from here yet. The shared DataTable
|
|
12
|
+
* narrows and searches what it holds, and it cannot report a filter selection
|
|
13
|
+
* back to the page under `serverMode` — so a narrowing sent to the platform
|
|
14
|
+
* would have nothing to set it. That is the remaining half of PO-231, and the
|
|
15
|
+
* parameters are waiting on the endpoint for it.
|
|
16
|
+
*/
|
|
17
|
+
export interface RunFeedQuery {
|
|
18
|
+
page: number;
|
|
19
|
+
perPage: number;
|
|
20
|
+
}
|
|
2
21
|
export declare function useIntegrationsRuns(): {
|
|
3
22
|
runs: any;
|
|
4
23
|
loading: any;
|
|
5
24
|
error: any;
|
|
6
25
|
loaded: any;
|
|
7
26
|
total: import("vue").ComputedRef<any>;
|
|
8
|
-
failedCount: import("vue").ComputedRef<
|
|
27
|
+
failedCount: import("vue").ComputedRef<number>;
|
|
9
28
|
runningCount: import("vue").ComputedRef<any>;
|
|
29
|
+
summary: any;
|
|
30
|
+
hasMore: any;
|
|
31
|
+
query: any;
|
|
10
32
|
load: (force?: boolean) => Promise<WorkflowRun[]>;
|
|
33
|
+
loadSummary: () => Promise<RunSummary>;
|
|
11
34
|
loadForWorkflow: (workflowId: number, workflowName: string, opts?: {
|
|
12
35
|
reconcile?: boolean;
|
|
13
36
|
page?: number;
|
|
@@ -24,6 +47,11 @@ export declare function useIntegrationsRuns(): {
|
|
|
24
47
|
}) => WorkflowRun[];
|
|
25
48
|
get: (id: string) => WorkflowRun | null;
|
|
26
49
|
fetchRun: (workflowId: number | string, runId: number | string, workflowName: string) => Promise<WorkflowRun>;
|
|
50
|
+
latestPerWorkflow: any;
|
|
51
|
+
latestLoaded: any;
|
|
52
|
+
latestLoading: any;
|
|
53
|
+
loadLatestPerWorkflow: (force?: boolean) => Promise<WorkflowRun[]>;
|
|
54
|
+
lastRunFor: (workflowId: number) => WorkflowRun | null;
|
|
27
55
|
trigger: (workflowId: number | string, payload?: Record<string, unknown>, mode?: "async" | "sync") => Promise<unknown>;
|
|
28
56
|
cancelRun: (workflowId: number | string, runId: number | string, reason?: string) => Promise<unknown>;
|
|
29
57
|
terminateRun: (workflowId: number | string, runId: number | string, reason?: string) => Promise<unknown>;
|
|
@@ -49,3 +77,4 @@ export declare function useIntegrationsRuns(): {
|
|
|
49
77
|
}>;
|
|
50
78
|
getStackTrace: (workflowId: number | string, runId: number | string) => Promise<string>;
|
|
51
79
|
};
|
|
80
|
+
export {};
|