@revenexx/studio-integrations 3.2.0 → 3.4.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 +20 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +34 -6
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +42 -10
- package/dist/runtime/components/Integrations/CredentialNetworkSection.d.vue.ts +21 -0
- package/dist/runtime/components/Integrations/CredentialNetworkSection.vue +28 -0
- package/dist/runtime/components/Integrations/CredentialNetworkSection.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +46 -3
- package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +9 -0
- package/dist/runtime/components/Integrations/RunsTable.vue +14 -8
- package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +9 -0
- package/dist/runtime/components/Integrations/VpnRoutePicker.d.vue.ts +20 -0
- package/dist/runtime/components/Integrations/VpnRoutePicker.vue +194 -0
- package/dist/runtime/components/Integrations/VpnRoutePicker.vue.d.ts +20 -0
- package/dist/runtime/composables/useIntegrationsCredentials.d.ts +15 -9
- package/dist/runtime/composables/useIntegrationsCredentials.js +9 -4
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +4 -2
- package/dist/runtime/composables/useIntegrationsRuns.js +12 -39
- package/dist/runtime/composables/useVpnConnections.d.ts +19 -0
- package/dist/runtime/composables/useVpnConnections.js +28 -0
- package/dist/runtime/i18n/messages/de.json +35 -0
- package/dist/runtime/i18n/messages/en.json +35 -0
- package/dist/runtime/pages/integrations/credentials.vue +67 -15
- package/dist/runtime/pages/integrations/workflows/[id].vue +15 -1
- package/dist/runtime/types/integrations.d.ts +64 -2
- package/dist/runtime/utils/configDefaults.d.ts +5 -2
- package/dist/runtime/utils/runFeedQuery.d.ts +8 -5
- package/dist/runtime/utils/runFeedQuery.js +9 -4
- package/dist/runtime/utils/runRecord.d.ts +39 -0
- package/dist/runtime/utils/runRecord.js +39 -0
- package/dist/runtime/utils/vpnRoute.d.ts +69 -0
- package/dist/runtime/utils/vpnRoute.js +79 -0
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, nextTick, onMounted, reactive, ref } from "vue";
|
|
2
|
+
import { computed, nextTick, onMounted, reactive, ref, watch } from "vue";
|
|
3
3
|
import {
|
|
4
4
|
RefreshCircle as Loader2,
|
|
5
5
|
AddCircle as Plus,
|
|
@@ -35,9 +35,11 @@ import { apiRefusal, errorReason } from "../../utils/apiError";
|
|
|
35
35
|
import { CREDENTIAL_STATES, credentialState, credentialStateLabel, providerStepWord } from "../../utils/credentialState";
|
|
36
36
|
import { hasRevenexxSource } from "../../utils/revenexxCredentialSource";
|
|
37
37
|
import { resolveLocalized } from "../../utils/localize";
|
|
38
|
+
import { defaultsFor } from "../../utils/configDefaults";
|
|
38
39
|
import { fromModelValue, toItemValue, toModelValue } from "../../utils/selectOptions";
|
|
39
40
|
import { keepOpenBeside } from "../../utils/dialog";
|
|
40
41
|
import { testOutcomeView } from "../../utils/credentialTest";
|
|
42
|
+
import { credentialNetworkPayload, credentialVpnBadge, replacedFieldTexts } from "../../utils/vpnRoute";
|
|
41
43
|
const t = useIntegrationsT();
|
|
42
44
|
useHead({ title: () => t("integration.credentials.list.head.title") });
|
|
43
45
|
const { credentials, loading, error, load, create, update, remove, testInline, test, oauthAuthorizeUrl } = useIntegrationsCredentials();
|
|
@@ -45,6 +47,13 @@ const { types, load: loadTypes, bySlug } = useCredentialTypes();
|
|
|
45
47
|
onMounted(async () => {
|
|
46
48
|
await Promise.all([load(), loadTypes()]);
|
|
47
49
|
});
|
|
50
|
+
const { connections: vpnConnections, loaded: vpnLoaded, load: loadVpn } = useVpnConnections();
|
|
51
|
+
watch(() => credentials.value.some((c) => c.network), (has) => {
|
|
52
|
+
if (has) void loadVpn();
|
|
53
|
+
}, { immediate: true });
|
|
54
|
+
function vpnBadge(c) {
|
|
55
|
+
return credentialVpnBadge(c.network, vpnConnections.value, vpnLoaded.value);
|
|
56
|
+
}
|
|
48
57
|
function typeFor(slug) {
|
|
49
58
|
return bySlug(slug);
|
|
50
59
|
}
|
|
@@ -108,12 +117,19 @@ const testing = ref(false);
|
|
|
108
117
|
const testResult = ref(null);
|
|
109
118
|
const saveRefusal = ref(null);
|
|
110
119
|
const sourceActive = ref(false);
|
|
120
|
+
const network = ref(null);
|
|
121
|
+
const networkComplete = ref(true);
|
|
122
|
+
const replaced = computed(() => replacedFieldTexts(pickedType.value, network.value, vpnConnections.value));
|
|
123
|
+
function networkPayload() {
|
|
124
|
+
return pickedType.value?.network ? credentialNetworkPayload(network.value) : void 0;
|
|
125
|
+
}
|
|
111
126
|
const isEdit = computed(() => editing.value !== null);
|
|
112
127
|
function fieldsOf(t2) {
|
|
113
128
|
return t2?.fields ?? [];
|
|
114
129
|
}
|
|
115
130
|
const drawnKeys = computed(() => [
|
|
116
131
|
"name",
|
|
132
|
+
...pickedType.value?.network ? ["network"] : [],
|
|
117
133
|
...sourceActive.value ? [] : fieldsOf(pickedType.value).map((f) => `config.${f.key}`)
|
|
118
134
|
]);
|
|
119
135
|
const refusal = computed(() => apiRefusal(saveRefusal.value, drawnKeys.value));
|
|
@@ -131,20 +147,30 @@ async function focusFirstRefusedField() {
|
|
|
131
147
|
await nextTick();
|
|
132
148
|
const key = drawnKeys.value.find((k) => k in fieldErrors.value);
|
|
133
149
|
if (!key) return;
|
|
134
|
-
const id = key === "name" ? "cred-name" : `cred-field-${key.slice("config.".length)}`;
|
|
150
|
+
const id = key === "name" ? "cred-name" : key === "network" ? "cred-network-public" : `cred-field-${key.slice("config.".length)}`;
|
|
135
151
|
document.getElementById(id)?.focus();
|
|
136
152
|
}
|
|
137
153
|
function clearRefusal() {
|
|
138
154
|
saveRefusal.value = null;
|
|
139
155
|
}
|
|
156
|
+
function pickCreateType(picked) {
|
|
157
|
+
pickedType.value = picked;
|
|
158
|
+
form.name = resolveLocalized(picked.name);
|
|
159
|
+
form.config = defaultsFor(fieldsOf(picked));
|
|
160
|
+
network.value = null;
|
|
161
|
+
testResult.value = null;
|
|
162
|
+
sourceActive.value = false;
|
|
163
|
+
clearRefusal();
|
|
164
|
+
}
|
|
140
165
|
function openCreate(t2) {
|
|
141
166
|
editing.value = null;
|
|
142
167
|
pickedType.value = t2 ?? null;
|
|
143
168
|
form.name = "";
|
|
144
|
-
form.config =
|
|
169
|
+
form.config = defaultsFor(fieldsOf(pickedType.value));
|
|
145
170
|
testResult.value = null;
|
|
146
171
|
clearRefusal();
|
|
147
172
|
sourceActive.value = false;
|
|
173
|
+
network.value = null;
|
|
148
174
|
dialogOpen.value = true;
|
|
149
175
|
}
|
|
150
176
|
const detailOpen = ref(false);
|
|
@@ -180,7 +206,7 @@ async function runTest() {
|
|
|
180
206
|
testResult.value = null;
|
|
181
207
|
clearRefusal();
|
|
182
208
|
try {
|
|
183
|
-
testResult.value = isEdit.value && editing.value ? await test(editing.value.id) : await testInline(pickedType.value.slug, buildConfigPayload());
|
|
209
|
+
testResult.value = isEdit.value && editing.value ? await test(editing.value.id) : await testInline(pickedType.value.slug, buildConfigPayload(), networkPayload());
|
|
184
210
|
} finally {
|
|
185
211
|
testing.value = false;
|
|
186
212
|
}
|
|
@@ -191,14 +217,15 @@ async function submit() {
|
|
|
191
217
|
clearRefusal();
|
|
192
218
|
try {
|
|
193
219
|
if (isEdit.value && editing.value) {
|
|
194
|
-
await update(editing.value.id, { name: form.name.trim(), config: buildConfigPayload() });
|
|
220
|
+
await update(editing.value.id, { name: form.name.trim(), config: buildConfigPayload(), network: networkPayload() });
|
|
195
221
|
dialogOpen.value = false;
|
|
196
222
|
toast.success(t("integration.credentials.toast.updated"));
|
|
197
223
|
} else {
|
|
198
224
|
const created = await create({
|
|
199
225
|
credential_type_slug: pickedType.value.slug,
|
|
200
226
|
name: form.name.trim(),
|
|
201
|
-
config: buildConfigPayload()
|
|
227
|
+
config: buildConfigPayload(),
|
|
228
|
+
network: networkPayload()
|
|
202
229
|
});
|
|
203
230
|
dialogOpen.value = false;
|
|
204
231
|
toast.success(t("integration.credentials.toast.created"));
|
|
@@ -285,7 +312,18 @@ async function confirmDelete() {
|
|
|
285
312
|
</template>
|
|
286
313
|
|
|
287
314
|
<template #cell-name="{ row: c }">
|
|
288
|
-
<
|
|
315
|
+
<div class="flex min-w-0 items-center gap-1.5">
|
|
316
|
+
<span class="text-sm truncate">{{ c.name }}</span>
|
|
317
|
+
<!-- Reached through a VPN route: which connection, and — where the
|
|
318
|
+
reference no longer resolves — that it is broken, before a run says
|
|
319
|
+
so (specs/credential-network.md AC-7). -->
|
|
320
|
+
<span
|
|
321
|
+
v-if="vpnBadge(c)"
|
|
322
|
+
class="shrink-0 rounded-full border px-1.5 py-px text-[10px] font-medium"
|
|
323
|
+
:class="vpnBadge(c).tone"
|
|
324
|
+
:aria-label="t('integration.vpn.badge.aria', { name: c.name, badge: vpnBadge(c).text })"
|
|
325
|
+
>{{ vpnBadge(c).text }}</span>
|
|
326
|
+
</div>
|
|
289
327
|
</template>
|
|
290
328
|
|
|
291
329
|
<template #cell-status="{ row: c }">
|
|
@@ -353,11 +391,7 @@ async function confirmDelete() {
|
|
|
353
391
|
<IntegrationsCredentialTypePicker
|
|
354
392
|
v-if="!pickedType"
|
|
355
393
|
:types="types"
|
|
356
|
-
@pick="
|
|
357
|
-
pickedType = picked;
|
|
358
|
-
form.name = resolveLocalized(picked.name);
|
|
359
|
-
clearRefusal();
|
|
360
|
-
}"
|
|
394
|
+
@pick="pickCreateType"
|
|
361
395
|
/>
|
|
362
396
|
|
|
363
397
|
<!-- Step 2: dynamic form -->
|
|
@@ -390,15 +424,33 @@ async function confirmDelete() {
|
|
|
390
424
|
@apply="(c2) => Object.assign(form.config, c2)"
|
|
391
425
|
/>
|
|
392
426
|
|
|
427
|
+
<!-- Public internet or a VPN route — for a type that says which of its
|
|
428
|
+
fields a route stands in for (specs/credential-network.md). -->
|
|
429
|
+
<IntegrationsCredentialNetworkSection
|
|
430
|
+
v-model="network"
|
|
431
|
+
:type="pickedType"
|
|
432
|
+
id-prefix="cred-network"
|
|
433
|
+
:error="fieldErrors.network"
|
|
434
|
+
@update:complete="(c2) => networkComplete = c2"
|
|
435
|
+
/>
|
|
436
|
+
|
|
393
437
|
<!-- Each label names its own control (`for`/`id`): without it a screen
|
|
394
438
|
reader reads the field as unlabelled, and the label text cannot be
|
|
395
439
|
used to reach it at all. -->
|
|
396
440
|
<div v-for="f in fieldsOf(pickedType)" v-show="!sourceActive" :key="f.key" class="space-y-1.5">
|
|
397
441
|
<Label :for="`cred-field-${f.key}`">{{ resolveLocalized(f.label) }}{{ f.required ? " *" : "" }}</Label>
|
|
398
442
|
|
|
443
|
+
<!-- stood in for by the chosen VPN route: what it reaches, not editable -->
|
|
444
|
+
<Input
|
|
445
|
+
v-if="replaced[f.key]"
|
|
446
|
+
:id="`cred-field-${f.key}`"
|
|
447
|
+
:model-value="replaced[f.key]"
|
|
448
|
+
readonly
|
|
449
|
+
class="bg-muted/40"
|
|
450
|
+
/>
|
|
399
451
|
<!-- secret -->
|
|
400
452
|
<Input
|
|
401
|
-
v-if="f.type === 'secret'"
|
|
453
|
+
v-else-if="f.type === 'secret'"
|
|
402
454
|
:id="`cred-field-${f.key}`"
|
|
403
455
|
:model-value="form.config[f.key] ?? ''"
|
|
404
456
|
type="password"
|
|
@@ -492,12 +544,12 @@ async function confirmDelete() {
|
|
|
492
544
|
</Button>
|
|
493
545
|
<span v-else />
|
|
494
546
|
<div class="flex items-center gap-2">
|
|
495
|
-
<Button variant="outline" :disabled="saving || testing" @click="runTest">
|
|
547
|
+
<Button variant="outline" :disabled="saving || testing || !networkComplete" @click="runTest">
|
|
496
548
|
<Loader2 v-if="testing" :size="14" class="mr-1 animate-spin" />
|
|
497
549
|
<TestIcon v-else :size="14" class="mr-1" />
|
|
498
550
|
{{ t("integration.credentials.shared.action.testConnection") }}
|
|
499
551
|
</Button>
|
|
500
|
-
<Button :disabled="saving || !form.name.trim()" @click="submit">
|
|
552
|
+
<Button :disabled="saving || !form.name.trim() || !networkComplete" @click="submit">
|
|
501
553
|
<Loader2 v-if="saving" :size="14" class="mr-1 animate-spin" />
|
|
502
554
|
{{ t("integration.common.action.save") }}
|
|
503
555
|
</Button>
|
|
@@ -209,6 +209,8 @@ watch(id, async () => {
|
|
|
209
209
|
if (!w) await leaveTo("/integrations/workflows");
|
|
210
210
|
draft.value = null;
|
|
211
211
|
warningsExpanded.value = false;
|
|
212
|
+
runsStatus.value = [];
|
|
213
|
+
runsTrigger.value = "";
|
|
212
214
|
if (tab.value === "runs") void loadRuns(1);
|
|
213
215
|
settleBuildStatus();
|
|
214
216
|
}, { immediate: true });
|
|
@@ -530,6 +532,8 @@ const runsLastPage = ref(1);
|
|
|
530
532
|
const runsWanted = ref(1);
|
|
531
533
|
const runsSort = ref(DEFAULT_RUN_SORT);
|
|
532
534
|
const runsDirection = ref(DEFAULT_RUN_DIRECTION);
|
|
535
|
+
const runsStatus = ref([]);
|
|
536
|
+
const runsTrigger = ref("");
|
|
533
537
|
let runsSeq = 0;
|
|
534
538
|
let runsLoadingSeq = 0;
|
|
535
539
|
const RUNS_POLL_MS = 3e3;
|
|
@@ -634,7 +638,8 @@ async function loadRuns(page = runsWanted.value, opts = {}) {
|
|
|
634
638
|
page,
|
|
635
639
|
perPage: RUNS_PER_PAGE,
|
|
636
640
|
sort: runsSort.value,
|
|
637
|
-
direction: runsDirection.value
|
|
641
|
+
direction: runsDirection.value,
|
|
642
|
+
narrowing: { status: runsStatus.value, triggerType: runsTrigger.value }
|
|
638
643
|
});
|
|
639
644
|
if (seq !== runsSeq) return;
|
|
640
645
|
wfRuns.value = res.runs;
|
|
@@ -655,6 +660,11 @@ function reorderRuns(order) {
|
|
|
655
660
|
runsDirection.value = order.direction;
|
|
656
661
|
void loadRuns(1);
|
|
657
662
|
}
|
|
663
|
+
function narrowRuns(patch) {
|
|
664
|
+
if (patch.status !== void 0) runsStatus.value = patch.status;
|
|
665
|
+
if (patch.triggerType !== void 0) runsTrigger.value = patch.triggerType;
|
|
666
|
+
void loadRuns(1);
|
|
667
|
+
}
|
|
658
668
|
function goRunsPage(p) {
|
|
659
669
|
const next = Math.min(Math.max(1, p), runsLastPage.value);
|
|
660
670
|
if (next !== runsWanted.value) void loadRuns(next);
|
|
@@ -1179,9 +1189,13 @@ async function confirmDeactivate() {
|
|
|
1179
1189
|
orderable
|
|
1180
1190
|
:sort="runsSort"
|
|
1181
1191
|
:direction="runsDirection"
|
|
1192
|
+
narrowable
|
|
1193
|
+
:status="runsStatus"
|
|
1194
|
+
:trigger-type="runsTrigger"
|
|
1182
1195
|
:empty-text="t('integration.editor.runs.empty')"
|
|
1183
1196
|
@changed="() => loadRuns()"
|
|
1184
1197
|
@reorder="reorderRuns"
|
|
1198
|
+
@narrow="narrowRuns"
|
|
1185
1199
|
/>
|
|
1186
1200
|
|
|
1187
1201
|
<!-- Server pagination — the endpoint pages, so the whole history is reachable. -->
|
|
@@ -282,7 +282,10 @@ export interface NodeManifestOutput {
|
|
|
282
282
|
}
|
|
283
283
|
export type ConfigFieldType = 'string' | 'number' | 'boolean' | 'select' | 'multiselect' | 'object' | 'array' | 'expression' | 'secret-ref' | 'credentials-ref'
|
|
284
284
|
/** Names one of the workflow's declared state namespaces (PO-374). */
|
|
285
|
-
| 'state-ref'
|
|
285
|
+
| 'state-ref'
|
|
286
|
+
/** A route of one of the tenant's VPN connections, or the public internet
|
|
287
|
+
* (PO-831). Stored as `VpnRouteValue | null`. */
|
|
288
|
+
| 'vpn-route' | 'dynamic-schema';
|
|
286
289
|
export interface ConfigFieldOption {
|
|
287
290
|
value: string | number | boolean;
|
|
288
291
|
label: LocalizedString;
|
|
@@ -335,6 +338,11 @@ export interface ConfigField {
|
|
|
335
338
|
* the stored value is the namespace NAME.
|
|
336
339
|
*/
|
|
337
340
|
stateRole?: WorkflowStateRole;
|
|
341
|
+
/**
|
|
342
|
+
* Only for `type: 'vpn-route'` (PO-831): which kind of route the picker offers.
|
|
343
|
+
* `http` where the node declares nothing, as the SDK defaults it.
|
|
344
|
+
*/
|
|
345
|
+
routeKind?: VpnRouteKind;
|
|
338
346
|
/** keys of other config fields this field's options/schema depend on. */
|
|
339
347
|
dependsOn?: string[];
|
|
340
348
|
/** When this setting applies at all (PO-410): another setting's key, an
|
|
@@ -471,6 +479,8 @@ export interface CredentialTypeField {
|
|
|
471
479
|
type: 'string' | 'number' | 'boolean' | 'select' | 'secret';
|
|
472
480
|
label: LocalizedString;
|
|
473
481
|
required?: boolean;
|
|
482
|
+
/** What the system uses when the operator changes nothing; a new credential's form starts from it (IN-38). */
|
|
483
|
+
default?: string | number | boolean;
|
|
474
484
|
options?: Array<{
|
|
475
485
|
label: LocalizedString;
|
|
476
486
|
value: string | number | boolean;
|
|
@@ -503,6 +513,27 @@ export interface CredentialType {
|
|
|
503
513
|
images?: CredentialTypeImage[];
|
|
504
514
|
auth_kind: AuthKind;
|
|
505
515
|
fields: CredentialTypeField[];
|
|
516
|
+
/**
|
|
517
|
+
* Which of the type's fields a VPN route stands in for (PO-829, the SDK's
|
|
518
|
+
* `ICredentialDescription.network`). Absent: the type cannot be routed through a
|
|
519
|
+
* VPN, and the credential forms draw no Network section for it.
|
|
520
|
+
*/
|
|
521
|
+
network?: CredentialNetworkDeclaration | null;
|
|
522
|
+
}
|
|
523
|
+
/** What a credential type declares a VPN route replaces. */
|
|
524
|
+
export type CredentialNetworkDeclaration = {
|
|
525
|
+
kind: 'tcp';
|
|
526
|
+
hostField: string;
|
|
527
|
+
portField: string;
|
|
528
|
+
} | {
|
|
529
|
+
kind: 'http';
|
|
530
|
+
urlField: string;
|
|
531
|
+
};
|
|
532
|
+
/** How a stored credential reaches its system, when not over the public internet. */
|
|
533
|
+
export interface CredentialNetwork {
|
|
534
|
+
kind: 'vpn';
|
|
535
|
+
connection: string;
|
|
536
|
+
route: string;
|
|
506
537
|
}
|
|
507
538
|
export type CredentialStatus = 'active' | 'needs_reauth';
|
|
508
539
|
/** A tenant's credential instance (secret fields never returned). */
|
|
@@ -517,9 +548,35 @@ export interface Credential {
|
|
|
517
548
|
last_test_at?: string | null;
|
|
518
549
|
last_test_ok?: boolean | null;
|
|
519
550
|
last_test_message?: string | null;
|
|
551
|
+
/** The VPN route this credential is reached through; `null` or absent = public internet. */
|
|
552
|
+
network?: CredentialNetwork | null;
|
|
520
553
|
created_at: string;
|
|
521
554
|
updated_at: string;
|
|
522
555
|
}
|
|
556
|
+
export type VpnRouteKind = 'http' | 'tcp';
|
|
557
|
+
/** Console's derived status of a connection; only `active` carries traffic. */
|
|
558
|
+
export type VpnConnectionStatus = 'provisioning' | 'failed' | 'active' | 'down' | 'standby' | 'disabled' | 'unknown';
|
|
559
|
+
export interface VpnRoute {
|
|
560
|
+
name: string;
|
|
561
|
+
kind: VpnRouteKind;
|
|
562
|
+
/** The target inside the tenant's network. */
|
|
563
|
+
host: string;
|
|
564
|
+
port: number;
|
|
565
|
+
proto?: string | null;
|
|
566
|
+
/** Where the gateway serves the route (http: a URL, tcp: `ip:port`). */
|
|
567
|
+
endpoint?: string | null;
|
|
568
|
+
}
|
|
569
|
+
export interface VpnConnection {
|
|
570
|
+
name: string;
|
|
571
|
+
display_name?: string | null;
|
|
572
|
+
status: VpnConnectionStatus | string;
|
|
573
|
+
routes: VpnRoute[];
|
|
574
|
+
}
|
|
575
|
+
/** What a `vpn-route` setting stores, and what a credential's `network` names. */
|
|
576
|
+
export interface VpnRouteValue {
|
|
577
|
+
connection: string;
|
|
578
|
+
route: string;
|
|
579
|
+
}
|
|
523
580
|
export type ConnectionStatus = 'connected' | 'misconfigured' | 'unverified' | 'failing';
|
|
524
581
|
export interface ConnectionTemplate {
|
|
525
582
|
/** kebab-case slug (e.g. 'shopify'). */
|
|
@@ -765,7 +822,12 @@ export interface WorkflowRun {
|
|
|
765
822
|
/** Temporal run id for this execution; null when the row predates Temporal. */
|
|
766
823
|
temporalRunId?: string | null;
|
|
767
824
|
triggerId: string;
|
|
768
|
-
|
|
825
|
+
/**
|
|
826
|
+
* What started the run, as the platform reported it. Null where it reported
|
|
827
|
+
* nothing — the trigger was deleted, or the run predates it being recorded — and
|
|
828
|
+
* drawn as nothing rather than as a guess (PO-335).
|
|
829
|
+
*/
|
|
830
|
+
triggerType: TriggerType | null;
|
|
769
831
|
startedAt: string;
|
|
770
832
|
finishedAt: string | null;
|
|
771
833
|
durationMs: number | null;
|
|
@@ -9,8 +9,11 @@ import type { ConfigField } from '../types/integrations.js';
|
|
|
9
9
|
* applied never gets to apply it. An absent key is the absence the node's
|
|
10
10
|
* default answers to.
|
|
11
11
|
*
|
|
12
|
-
* Used
|
|
13
|
-
*
|
|
12
|
+
* Used wherever a fresh set of values is made: a node dropped on the canvas
|
|
13
|
+
* (WorkflowEditor), a row added to a list setting (NodeConfigField), and a
|
|
14
|
+
* credential's form — a new one in both create dialogs, and a stored one's
|
|
15
|
+
* missing fields in the detail view (IN-38), which also compares an edit against
|
|
16
|
+
* it. A change here changes all of them.
|
|
14
17
|
* Flat on purpose — it reads `default` at the level it is given and does not
|
|
15
18
|
* descend into `properties` or `items`, so what a newly inserted node carries
|
|
16
19
|
* is exactly what it carried before this became shared.
|
|
@@ -122,13 +122,16 @@ export interface RunFeedQuery {
|
|
|
122
122
|
* empty `status` as a value and answers 422.
|
|
123
123
|
*/
|
|
124
124
|
export declare function runFeedQueryString(query: RunFeedQuery): string;
|
|
125
|
+
/** The narrowing a workflow's own list takes: the feed's, minus the text search. */
|
|
126
|
+
export type WorkflowRunsNarrowing = Pick<RunFeedQuery, 'status' | 'triggerType'>;
|
|
125
127
|
/**
|
|
126
|
-
* The query string for a workflow's own `GET /workflows/{id}/runs`: a page,
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
128
|
+
* The query string for a workflow's own `GET /workflows/{id}/runs`: a page, the
|
|
129
|
+
* order it is read in, and the narrowing by outcome and trigger kind (PO-319) —
|
|
130
|
+
* each sent by the same rule as the tenant feed's, so the two lists can never ask
|
|
131
|
+
* for one thing two ways. No text search: that endpoint takes none, and the tab's
|
|
132
|
+
* search box searches the page in hand.
|
|
130
133
|
*/
|
|
131
|
-
export declare function workflowRunsQueryString(page: number, perPage: number, sort?: RunSort, direction?: RunSortDirection): string;
|
|
134
|
+
export declare function workflowRunsQueryString(page: number, perPage: number, sort?: RunSort, direction?: RunSortDirection, narrowing?: WorkflowRunsNarrowing): string;
|
|
132
135
|
/**
|
|
133
136
|
* The narrowing alone, without the paging — what `GET /runs/summary` is read
|
|
134
137
|
* with so the pager's total describes the same rows as the page beside it.
|
|
@@ -29,10 +29,11 @@ export function runFeedQueryString(query) {
|
|
|
29
29
|
setOrder(params, query.sort, query.direction);
|
|
30
30
|
return params.toString();
|
|
31
31
|
}
|
|
32
|
-
export function workflowRunsQueryString(page, perPage, sort = DEFAULT_RUN_SORT, direction = DEFAULT_RUN_DIRECTION) {
|
|
32
|
+
export function workflowRunsQueryString(page, perPage, sort = DEFAULT_RUN_SORT, direction = DEFAULT_RUN_DIRECTION, narrowing = { status: [], triggerType: "" }) {
|
|
33
33
|
const params = new URLSearchParams();
|
|
34
34
|
params.set("page", String(page));
|
|
35
35
|
params.set("per_page", String(perPage));
|
|
36
|
+
for (const [key, value] of outcomeParams(narrowing)) params.set(key, value);
|
|
36
37
|
setOrder(params, sort, direction);
|
|
37
38
|
return params.toString();
|
|
38
39
|
}
|
|
@@ -45,11 +46,15 @@ function setOrder(params, sort, direction) {
|
|
|
45
46
|
export function runFeedNarrowingString(query) {
|
|
46
47
|
return narrowingParams(query).toString();
|
|
47
48
|
}
|
|
48
|
-
function
|
|
49
|
+
function outcomeParams(narrowing) {
|
|
49
50
|
const params = new URLSearchParams();
|
|
50
|
-
const statuses = apiStatusesFor(
|
|
51
|
+
const statuses = apiStatusesFor(narrowing.status);
|
|
51
52
|
if (statuses.length) params.set("status", statuses.join(","));
|
|
52
|
-
if (
|
|
53
|
+
if (narrowing.triggerType) params.set("trigger_type", narrowing.triggerType);
|
|
54
|
+
return params;
|
|
55
|
+
}
|
|
56
|
+
function narrowingParams(query) {
|
|
57
|
+
const params = outcomeParams(query);
|
|
53
58
|
const search = query.search.trim();
|
|
54
59
|
if (search) params.set("search", search);
|
|
55
60
|
return params;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { TriggerType, WorkflowRun } from '../types/integrations.js';
|
|
2
|
+
/**
|
|
3
|
+
* A run as either list sends it, and as the by-id read does. The three do not
|
|
4
|
+
* carry the same fields: see `mapRun` and `keepListTrigger` for what fills in.
|
|
5
|
+
*/
|
|
6
|
+
export interface ApiRun {
|
|
7
|
+
id: number;
|
|
8
|
+
workflow_id: number;
|
|
9
|
+
status: string;
|
|
10
|
+
temporal_workflow_id?: string;
|
|
11
|
+
temporal_run_id?: string;
|
|
12
|
+
started_at?: string;
|
|
13
|
+
completed_at?: string;
|
|
14
|
+
/** Why the run failed, as the platform recorded it. Present in the list too. */
|
|
15
|
+
error?: string | null;
|
|
16
|
+
/**
|
|
17
|
+
* Present on the tenant feed, absent on the per-workflow subresource — which
|
|
18
|
+
* has no need for them, since the caller supplied the workflow and the tab
|
|
19
|
+
* shows one workflow's runs.
|
|
20
|
+
*/
|
|
21
|
+
workflow_name?: string;
|
|
22
|
+
trigger_type?: TriggerType | null;
|
|
23
|
+
trigger_handle?: string | null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* `workflowName` is a FALLBACK, not the source. The tenant feed carries the
|
|
27
|
+
* name on the row; the per-workflow subresource does not, and its caller knows
|
|
28
|
+
* it. Passing it used to be the only way a row had a name at all.
|
|
29
|
+
*/
|
|
30
|
+
export declare function mapRun(r: ApiRun, workflowName?: string): WorkflowRun;
|
|
31
|
+
/**
|
|
32
|
+
* A run re-read by id, keeping what started it from the row it replaces.
|
|
33
|
+
*
|
|
34
|
+
* The by-id read sends no trigger — only the two lists do — so swapping a
|
|
35
|
+
* reconciled run in whole blanked a schedule's run to no trigger at all, a row
|
|
36
|
+
* after the list had named it (api-contract runs.execution F2). What started a
|
|
37
|
+
* run does not change while it runs, so the list's word stays the right one.
|
|
38
|
+
*/
|
|
39
|
+
export declare function keepListTrigger(fresh: WorkflowRun, listed: WorkflowRun): WorkflowRun;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { mapRunStatus } from "./runFeedQuery.js";
|
|
2
|
+
export function mapRun(r, workflowName = "") {
|
|
3
|
+
const startedMs = r.started_at ? new Date(r.started_at).getTime() : null;
|
|
4
|
+
const finishedMs = r.completed_at ? new Date(r.completed_at).getTime() : null;
|
|
5
|
+
return {
|
|
6
|
+
id: String(r.id),
|
|
7
|
+
workflowId: r.workflow_id,
|
|
8
|
+
workflowName: r.workflow_name ?? workflowName,
|
|
9
|
+
status: mapRunStatus(r.status),
|
|
10
|
+
// Kept beside the mapped word, because the mapping folds `terminated` into
|
|
11
|
+
// `failed` and the platform resumes only the literal `failed` (PO-355).
|
|
12
|
+
apiStatus: r.status,
|
|
13
|
+
temporalRunId: r.temporal_run_id ?? null,
|
|
14
|
+
triggerId: r.trigger_handle ?? "",
|
|
15
|
+
// The platform reports what started the run, on both lists. Null — the
|
|
16
|
+
// trigger was deleted, or the run predates trigger_handle — stays null: it
|
|
17
|
+
// was read as `manual` once, and every row of a workflow's own Runs tab then
|
|
18
|
+
// claimed a start by hand, because that list sent no trigger at all (PO-335).
|
|
19
|
+
// A by-id read still sends none; see `keepListTrigger`.
|
|
20
|
+
triggerType: r.trigger_type ?? null,
|
|
21
|
+
startedAt: r.started_at ?? "",
|
|
22
|
+
finishedAt: r.completed_at ?? null,
|
|
23
|
+
// The list carries the reason a run failed; dropping it here was why every
|
|
24
|
+
// failure read the same wherever the list is shown. An empty string is not
|
|
25
|
+
// a reason — it has to fall through to whatever the surface says instead,
|
|
26
|
+
// or it renders as a blank line where the sentence belongs.
|
|
27
|
+
errorMessage: r.error || void 0,
|
|
28
|
+
// Derived from the timestamps — the list payload carries no duration.
|
|
29
|
+
durationMs: startedMs != null && finishedMs != null && finishedMs >= startedMs ? finishedMs - startedMs : null,
|
|
30
|
+
steps: []
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function keepListTrigger(fresh, listed) {
|
|
34
|
+
return {
|
|
35
|
+
...fresh,
|
|
36
|
+
triggerId: fresh.triggerId || listed.triggerId,
|
|
37
|
+
triggerType: fresh.triggerType ?? listed.triggerType
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { CredentialNetwork, CredentialNetworkDeclaration, CredentialType, VpnConnection, VpnRoute, VpnRouteKind, VpnRouteValue } from '../types/integrations.js';
|
|
2
|
+
/**
|
|
3
|
+
* What the studio makes of a VPN route reference — a credential's `network`, or the
|
|
4
|
+
* value of a `vpn-route` setting — against the tenant's connections
|
|
5
|
+
* (specs/credential-network.md). Pure, so the surfaces that draw it agree.
|
|
6
|
+
*/
|
|
7
|
+
/** The name a connection is shown under: its display name, else its own. */
|
|
8
|
+
export declare function connectionLabel(c: Pick<VpnConnection, 'name' | 'display_name'>): string;
|
|
9
|
+
/** The routes of a connection a picker filtered to `kind` offers. */
|
|
10
|
+
export declare function routesOfKind(c: VpnConnection | undefined, kind: VpnRouteKind): VpnRoute[];
|
|
11
|
+
/** Whether a stored value names a route at all, as opposed to the public internet. */
|
|
12
|
+
export declare function isVpnRouteValue(v: unknown): v is VpnRouteValue;
|
|
13
|
+
export type VpnReference =
|
|
14
|
+
/** The route exists; `active` says whether its connection carries traffic now. */
|
|
15
|
+
{
|
|
16
|
+
state: 'ok';
|
|
17
|
+
connection: VpnConnection;
|
|
18
|
+
route: VpnRoute;
|
|
19
|
+
active: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** The connection or the route is gone — a broken reference. */
|
|
22
|
+
| {
|
|
23
|
+
state: 'missing';
|
|
24
|
+
connection?: VpnConnection;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a reference against the tenant's connections. A connection that is still
|
|
28
|
+
* there but no longer has the route counts as missing, as the backend refuses it
|
|
29
|
+
* with `VPN_ROUTE_MISSING` either way.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveVpnReference(connections: readonly VpnConnection[], ref: VpnRouteValue): VpnReference;
|
|
32
|
+
/** The keys of a credential type's fields a VPN route stands in for. */
|
|
33
|
+
export declare function replacedFieldKeys(decl: CredentialNetworkDeclaration | null | undefined): string[];
|
|
34
|
+
/**
|
|
35
|
+
* What a replaced field reads while a route stands in for it: `host:port via
|
|
36
|
+
* <connection>` for a tcp route, the gateway's endpoint for an http one — the
|
|
37
|
+
* address the request is actually sent to.
|
|
38
|
+
*/
|
|
39
|
+
export declare function routeTargetText(route: VpnRoute, connection: VpnConnection): string;
|
|
40
|
+
/**
|
|
41
|
+
* The read-only text of every field a chosen route replaces, keyed by field. Empty
|
|
42
|
+
* while the credential is reached over the public internet, or while the route
|
|
43
|
+
* cannot be resolved — then the fields keep their stored values on screen.
|
|
44
|
+
*/
|
|
45
|
+
export declare function replacedFieldTexts(type: Pick<CredentialType, 'network'> | null | undefined, value: VpnRouteValue | null, connections: readonly VpnConnection[]): Record<string, string>;
|
|
46
|
+
/** Whether the gateway reaches the target over TLS it does not verify. */
|
|
47
|
+
export declare function isHttpsTarget(route: VpnRoute | undefined): boolean;
|
|
48
|
+
/** The credential's `network` as the service takes it, from what the form holds. */
|
|
49
|
+
export declare function credentialNetworkPayload(value: VpnRouteValue | null): CredentialNetwork | null;
|
|
50
|
+
/** The picker's value from what a credential stores. */
|
|
51
|
+
export declare function fromCredentialNetwork(n: CredentialNetwork | null | undefined): VpnRouteValue | null;
|
|
52
|
+
/** Whether two values name the same route (both public counts as the same). */
|
|
53
|
+
export declare function sameVpnRoute(a: VpnRouteValue | null, b: VpnRouteValue | null): boolean;
|
|
54
|
+
/** A connection's status in the reader's words; an unforeseen one reads as unknown. */
|
|
55
|
+
export declare function connectionStatusLabel(status: string): string;
|
|
56
|
+
/** Badge classes for a connection status: green only for one that carries traffic. */
|
|
57
|
+
export declare function connectionStatusTone(status: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* The VPN badge a credential carries where it is picked or listed, or `null` for one
|
|
60
|
+
* reached over the public internet. `connections` loaded or not decides whether a
|
|
61
|
+
* reference nothing resolves is called broken: before the list arrives it is only
|
|
62
|
+
* named.
|
|
63
|
+
*/
|
|
64
|
+
export declare function credentialVpnBadge(network: CredentialNetwork | null | undefined, connections: readonly VpnConnection[], loaded: boolean): {
|
|
65
|
+
text: string;
|
|
66
|
+
broken: boolean;
|
|
67
|
+
active: boolean;
|
|
68
|
+
tone: string;
|
|
69
|
+
} | null;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useIntegrationsT } from "../composables/useIntegrationsT.js";
|
|
2
|
+
export function connectionLabel(c) {
|
|
3
|
+
return c.display_name?.trim() || c.name;
|
|
4
|
+
}
|
|
5
|
+
export function routesOfKind(c, kind) {
|
|
6
|
+
return (c?.routes ?? []).filter((r) => r.kind === kind);
|
|
7
|
+
}
|
|
8
|
+
export function isVpnRouteValue(v) {
|
|
9
|
+
return !!v && typeof v === "object" && typeof v.connection === "string" && v.connection !== "" && typeof v.route === "string" && v.route !== "";
|
|
10
|
+
}
|
|
11
|
+
export function resolveVpnReference(connections, ref) {
|
|
12
|
+
const connection = connections.find((c) => c.name === ref.connection);
|
|
13
|
+
const route = connection?.routes.find((r) => r.name === ref.route);
|
|
14
|
+
if (!connection || !route) return { state: "missing", connection };
|
|
15
|
+
return { state: "ok", connection, route, active: connection.status === "active" };
|
|
16
|
+
}
|
|
17
|
+
export function replacedFieldKeys(decl) {
|
|
18
|
+
if (!decl) return [];
|
|
19
|
+
return decl.kind === "tcp" ? [decl.hostField, decl.portField] : [decl.urlField];
|
|
20
|
+
}
|
|
21
|
+
export function routeTargetText(route, connection) {
|
|
22
|
+
const t = useIntegrationsT();
|
|
23
|
+
const target = route.kind === "http" && route.endpoint ? route.endpoint : `${route.host}:${route.port}`;
|
|
24
|
+
return t("integration.vpn.target", { target, connection: connectionLabel(connection) });
|
|
25
|
+
}
|
|
26
|
+
export function replacedFieldTexts(type, value, connections) {
|
|
27
|
+
const keys = replacedFieldKeys(type?.network);
|
|
28
|
+
if (!keys.length || !value) return {};
|
|
29
|
+
const ref = resolveVpnReference(connections, value);
|
|
30
|
+
if (ref.state !== "ok") return {};
|
|
31
|
+
const text = routeTargetText(ref.route, ref.connection);
|
|
32
|
+
return Object.fromEntries(keys.map((k) => [k, text]));
|
|
33
|
+
}
|
|
34
|
+
export function isHttpsTarget(route) {
|
|
35
|
+
return (route?.proto ?? "").toLowerCase() === "https";
|
|
36
|
+
}
|
|
37
|
+
export function credentialNetworkPayload(value) {
|
|
38
|
+
return value ? { kind: "vpn", connection: value.connection, route: value.route } : null;
|
|
39
|
+
}
|
|
40
|
+
export function fromCredentialNetwork(n) {
|
|
41
|
+
return n && n.kind === "vpn" && n.connection && n.route ? { connection: n.connection, route: n.route } : null;
|
|
42
|
+
}
|
|
43
|
+
export function sameVpnRoute(a, b) {
|
|
44
|
+
if (!a || !b) return !a && !b;
|
|
45
|
+
return a.connection === b.connection && a.route === b.route;
|
|
46
|
+
}
|
|
47
|
+
const STATUS_KEYS = ["provisioning", "failed", "active", "down", "standby", "disabled", "unknown"];
|
|
48
|
+
export function connectionStatusLabel(status) {
|
|
49
|
+
const key = STATUS_KEYS.includes(status) ? status : "unknown";
|
|
50
|
+
return useIntegrationsT()(`integration.vpn.status.${key}`);
|
|
51
|
+
}
|
|
52
|
+
export function connectionStatusTone(status) {
|
|
53
|
+
if (status === "active") return "bg-emerald-500/10 border-emerald-500/20 text-emerald-700";
|
|
54
|
+
if (status === "failed" || status === "down") return "bg-rose-500/10 border-rose-500/20 text-rose-700";
|
|
55
|
+
if (status === "provisioning" || status === "standby") return "bg-amber-500/10 border-amber-500/20 text-amber-700";
|
|
56
|
+
return "bg-slate-500/10 border-slate-500/20 text-slate-600";
|
|
57
|
+
}
|
|
58
|
+
export function credentialVpnBadge(network, connections, loaded) {
|
|
59
|
+
const value = fromCredentialNetwork(network);
|
|
60
|
+
if (!value) return null;
|
|
61
|
+
const t = useIntegrationsT();
|
|
62
|
+
if (!loaded) {
|
|
63
|
+
return { text: t("integration.vpn.badge.named", { connection: value.connection }), broken: false, active: true, tone: connectionStatusTone("unknown") };
|
|
64
|
+
}
|
|
65
|
+
const ref = resolveVpnReference(connections, value);
|
|
66
|
+
if (ref.state === "missing") {
|
|
67
|
+
return { text: t("integration.vpn.badge.missing"), broken: true, active: false, tone: connectionStatusTone("failed") };
|
|
68
|
+
}
|
|
69
|
+
const name = connectionLabel(ref.connection);
|
|
70
|
+
if (ref.active) {
|
|
71
|
+
return { text: t("integration.vpn.badge.named", { connection: name }), broken: false, active: true, tone: connectionStatusTone("active") };
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
text: t("integration.vpn.badge.withState", { connection: name, status: connectionStatusLabel(String(ref.connection.status)) }),
|
|
75
|
+
broken: false,
|
|
76
|
+
active: false,
|
|
77
|
+
tone: connectionStatusTone(String(ref.connection.status))
|
|
78
|
+
};
|
|
79
|
+
}
|