@nvent-addon/app 0.5.7 → 0.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/dist/runtime/app/components/SelectableListItem.d.vue.ts +36 -0
- package/dist/runtime/app/components/SelectableListItem.vue +73 -0
- package/dist/runtime/app/components/SelectableListItem.vue.d.ts +36 -0
- package/dist/runtime/app/composables/useFlowRuns.d.ts +29 -7
- package/dist/runtime/app/composables/useFlowRuns.js +21 -8
- package/dist/runtime/app/pages/flows/[name].vue +156 -84
- package/dist/runtime/app/pages/queues/jobs.vue +18 -40
- package/dist/runtime/app/pages/triggers/[name].vue +11 -38
- package/package.json +1 -1
- package/dist/runtime/app/components/JobScheduling.d.vue.ts +0 -6
- package/dist/runtime/app/components/JobScheduling.vue +0 -203
- package/dist/runtime/app/components/JobScheduling.vue.d.ts +0 -6
- package/dist/runtime/app/components/ListItem.d.vue.ts +0 -20
- package/dist/runtime/app/components/ListItem.vue +0 -69
- package/dist/runtime/app/components/ListItem.vue.d.ts +0 -20
- package/dist/runtime/app/composables/useFlowRunsInfinite.d.ts +0 -24
- package/dist/runtime/app/composables/useFlowRunsInfinite.js +0 -123
- package/dist/runtime/app/composables/useFlowRunsPolling.d.ts +0 -9
- package/dist/runtime/app/composables/useFlowRunsPolling.js +0 -33
package/dist/module.json
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
selected?: boolean;
|
|
3
|
+
icon?: string;
|
|
4
|
+
iconClass?: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
subtitle?: string;
|
|
7
|
+
badge?: string;
|
|
8
|
+
badgeColor?: 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | 'neutral';
|
|
9
|
+
meta?: string;
|
|
10
|
+
metaSecondary?: string;
|
|
11
|
+
};
|
|
12
|
+
declare var __VLS_1: {}, __VLS_9: {}, __VLS_11: {}, __VLS_19: {}, __VLS_21: {};
|
|
13
|
+
type __VLS_Slots = {} & {
|
|
14
|
+
icon?: (props: typeof __VLS_1) => any;
|
|
15
|
+
} & {
|
|
16
|
+
title?: (props: typeof __VLS_9) => any;
|
|
17
|
+
} & {
|
|
18
|
+
badge?: (props: typeof __VLS_11) => any;
|
|
19
|
+
} & {
|
|
20
|
+
subtitle?: (props: typeof __VLS_19) => any;
|
|
21
|
+
} & {
|
|
22
|
+
meta?: (props: typeof __VLS_21) => any;
|
|
23
|
+
};
|
|
24
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
25
|
+
click: () => any;
|
|
26
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
27
|
+
onClick?: (() => any) | undefined;
|
|
28
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
29
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
30
|
+
declare const _default: typeof __VLS_export;
|
|
31
|
+
export default _default;
|
|
32
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
33
|
+
new (): {
|
|
34
|
+
$slots: S;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-900/50 cursor-pointer transition-colors"
|
|
4
|
+
:class="{
|
|
5
|
+
'bg-blue-50 dark:bg-blue-950/30 border-l-2 border-l-blue-500': selected
|
|
6
|
+
}"
|
|
7
|
+
@click="$emit('click')"
|
|
8
|
+
>
|
|
9
|
+
<div class="flex items-start gap-3">
|
|
10
|
+
<div
|
|
11
|
+
v-if="icon || $slots.icon"
|
|
12
|
+
class="flex-shrink-0 mt-0.5"
|
|
13
|
+
>
|
|
14
|
+
<slot name="icon">
|
|
15
|
+
<UIcon
|
|
16
|
+
v-if="icon"
|
|
17
|
+
:name="icon"
|
|
18
|
+
class="w-5 h-5"
|
|
19
|
+
:class="iconClass"
|
|
20
|
+
/>
|
|
21
|
+
</slot>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="flex-1 min-w-0">
|
|
24
|
+
<div class="flex items-center justify-between gap-2 mb-1">
|
|
25
|
+
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
|
|
26
|
+
<slot name="title">
|
|
27
|
+
{{ title }}
|
|
28
|
+
</slot>
|
|
29
|
+
</h3>
|
|
30
|
+
<slot name="badge">
|
|
31
|
+
<UBadge
|
|
32
|
+
v-if="badge"
|
|
33
|
+
:label="badge"
|
|
34
|
+
:color="badgeColor"
|
|
35
|
+
variant="subtle"
|
|
36
|
+
size="xs"
|
|
37
|
+
class="capitalize flex-shrink-0"
|
|
38
|
+
/>
|
|
39
|
+
</slot>
|
|
40
|
+
</div>
|
|
41
|
+
<p
|
|
42
|
+
v-if="subtitle || $slots.subtitle"
|
|
43
|
+
class="text-xs text-gray-500 dark:text-gray-400 font-mono truncate mb-1"
|
|
44
|
+
>
|
|
45
|
+
<slot name="subtitle">
|
|
46
|
+
{{ subtitle }}
|
|
47
|
+
</slot>
|
|
48
|
+
</p>
|
|
49
|
+
<div class="flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400">
|
|
50
|
+
<slot name="meta">
|
|
51
|
+
<span v-if="meta">{{ meta }}</span>
|
|
52
|
+
<span v-if="metaSecondary">• {{ metaSecondary }}</span>
|
|
53
|
+
</slot>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</template>
|
|
59
|
+
|
|
60
|
+
<script setup>
|
|
61
|
+
defineProps({
|
|
62
|
+
selected: { type: Boolean, required: false },
|
|
63
|
+
icon: { type: String, required: false },
|
|
64
|
+
iconClass: { type: String, required: false },
|
|
65
|
+
title: { type: String, required: false },
|
|
66
|
+
subtitle: { type: String, required: false },
|
|
67
|
+
badge: { type: String, required: false },
|
|
68
|
+
badgeColor: { type: String, required: false },
|
|
69
|
+
meta: { type: String, required: false },
|
|
70
|
+
metaSecondary: { type: String, required: false }
|
|
71
|
+
});
|
|
72
|
+
defineEmits(["click"]);
|
|
73
|
+
</script>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
selected?: boolean;
|
|
3
|
+
icon?: string;
|
|
4
|
+
iconClass?: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
subtitle?: string;
|
|
7
|
+
badge?: string;
|
|
8
|
+
badgeColor?: 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | 'neutral';
|
|
9
|
+
meta?: string;
|
|
10
|
+
metaSecondary?: string;
|
|
11
|
+
};
|
|
12
|
+
declare var __VLS_1: {}, __VLS_9: {}, __VLS_11: {}, __VLS_19: {}, __VLS_21: {};
|
|
13
|
+
type __VLS_Slots = {} & {
|
|
14
|
+
icon?: (props: typeof __VLS_1) => any;
|
|
15
|
+
} & {
|
|
16
|
+
title?: (props: typeof __VLS_9) => any;
|
|
17
|
+
} & {
|
|
18
|
+
badge?: (props: typeof __VLS_11) => any;
|
|
19
|
+
} & {
|
|
20
|
+
subtitle?: (props: typeof __VLS_19) => any;
|
|
21
|
+
} & {
|
|
22
|
+
meta?: (props: typeof __VLS_21) => any;
|
|
23
|
+
};
|
|
24
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
25
|
+
click: () => any;
|
|
26
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
27
|
+
onClick?: (() => any) | undefined;
|
|
28
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
29
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
30
|
+
declare const _default: typeof __VLS_export;
|
|
31
|
+
export default _default;
|
|
32
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
33
|
+
new (): {
|
|
34
|
+
$slots: S;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
@@ -1,18 +1,40 @@
|
|
|
1
1
|
import { type Ref } from '#imports';
|
|
2
2
|
import type { FetchError } from 'ofetch';
|
|
3
|
-
interface FlowRun {
|
|
3
|
+
export interface FlowRun {
|
|
4
4
|
id: string;
|
|
5
|
-
|
|
5
|
+
flowName: string;
|
|
6
|
+
status: 'running' | 'completed' | 'failed' | 'canceled' | 'stalled' | 'awaiting' | 'unknown';
|
|
7
|
+
createdAt: string;
|
|
8
|
+
startedAt?: string;
|
|
9
|
+
completedAt?: string;
|
|
10
|
+
stepCount: number;
|
|
11
|
+
completedSteps: number;
|
|
12
|
+
}
|
|
13
|
+
export interface FlowRunsResponse {
|
|
14
|
+
flowName: string;
|
|
15
|
+
count: number;
|
|
16
|
+
total: number;
|
|
17
|
+
offset: number;
|
|
18
|
+
limit: number;
|
|
19
|
+
hasMore: boolean;
|
|
20
|
+
items: FlowRun[];
|
|
6
21
|
}
|
|
7
22
|
/**
|
|
8
|
-
* Composable for fetching
|
|
9
|
-
*
|
|
23
|
+
* Composable for fetching flow runs with pagination
|
|
24
|
+
* Similar pattern to useTriggerEvents - supports server-side pagination
|
|
10
25
|
* Client-only to avoid hydration mismatches
|
|
11
26
|
*/
|
|
12
|
-
export declare function useFlowRuns(flowId: Ref<string
|
|
13
|
-
|
|
27
|
+
export declare function useFlowRuns(flowId: Ref<string>, options?: Ref<{
|
|
28
|
+
limit?: number;
|
|
29
|
+
offset?: number;
|
|
30
|
+
status?: string | null;
|
|
31
|
+
}> | {
|
|
32
|
+
limit?: number;
|
|
33
|
+
offset?: number;
|
|
34
|
+
status?: string | null;
|
|
35
|
+
}): {
|
|
36
|
+
runs: Ref<FlowRunsResponse | null | undefined>;
|
|
14
37
|
refresh: () => Promise<void>;
|
|
15
38
|
status: Ref<'idle' | 'pending' | 'success' | 'error'>;
|
|
16
39
|
error: Ref<FetchError | null | undefined>;
|
|
17
40
|
};
|
|
18
|
-
export {};
|
|
@@ -1,28 +1,41 @@
|
|
|
1
|
-
import { ref, watch, useFetch } from "#imports";
|
|
2
|
-
export function useFlowRuns(flowId) {
|
|
1
|
+
import { ref, watch, useFetch, isRef } from "#imports";
|
|
2
|
+
export function useFlowRuns(flowId, options) {
|
|
3
3
|
const refreshCounter = ref(0);
|
|
4
|
+
const opts = isRef(options) ? options : ref(options || {});
|
|
5
|
+
const buildUrl = () => {
|
|
6
|
+
if (!flowId.value) return "/api/_flows/__invalid__/runs";
|
|
7
|
+
const params = new URLSearchParams();
|
|
8
|
+
params.append("_t", refreshCounter.value.toString());
|
|
9
|
+
if (opts.value.limit) params.append("limit", opts.value.limit.toString());
|
|
10
|
+
if (opts.value.offset !== void 0) params.append("offset", opts.value.offset.toString());
|
|
11
|
+
if (opts.value.status) params.append("status", opts.value.status);
|
|
12
|
+
return `/api/_flows/${encodeURIComponent(flowId.value)}/runs?${params.toString()}`;
|
|
13
|
+
};
|
|
4
14
|
const { data: runs, refresh: _refresh, status, error } = useFetch(
|
|
5
|
-
|
|
15
|
+
buildUrl,
|
|
6
16
|
{
|
|
7
17
|
immediate: false,
|
|
8
18
|
watch: false,
|
|
9
19
|
// Disable automatic watch to prevent SSR execution
|
|
10
20
|
server: false
|
|
11
21
|
// Client-only to avoid hydration issues
|
|
12
|
-
// Don't use a key - this prevents Nuxt from caching across calls
|
|
13
22
|
}
|
|
14
23
|
);
|
|
15
24
|
const refresh = async () => {
|
|
25
|
+
if (!flowId.value) return;
|
|
16
26
|
refreshCounter.value++;
|
|
17
27
|
await _refresh();
|
|
18
28
|
};
|
|
19
29
|
watch(flowId, (newFlow, oldFlow) => {
|
|
20
|
-
if (import.meta.client && newFlow) {
|
|
21
|
-
|
|
22
|
-
refresh();
|
|
23
|
-
}
|
|
30
|
+
if (import.meta.client && newFlow && newFlow !== oldFlow) {
|
|
31
|
+
refresh();
|
|
24
32
|
}
|
|
25
33
|
}, { immediate: true });
|
|
34
|
+
watch(() => opts.value, () => {
|
|
35
|
+
if (import.meta.client && flowId.value) {
|
|
36
|
+
refresh();
|
|
37
|
+
}
|
|
38
|
+
}, { deep: true });
|
|
26
39
|
return {
|
|
27
40
|
runs,
|
|
28
41
|
refresh,
|
|
@@ -131,54 +131,23 @@
|
|
|
131
131
|
</div>
|
|
132
132
|
<div
|
|
133
133
|
v-else
|
|
134
|
-
|
|
135
|
-
class="flex-1 overflow-y-auto min-h-0 divide-y divide-gray-100 dark:divide-gray-800"
|
|
136
|
-
@scroll="handleRunsScroll"
|
|
134
|
+
class="flex-1 min-h-0 flex flex-col overflow-hidden"
|
|
137
135
|
>
|
|
138
|
-
<div
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
class="
|
|
145
|
-
:
|
|
136
|
+
<div class="flex-1 min-h-0 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800">
|
|
137
|
+
<SelectableListItem
|
|
138
|
+
v-for="r in runs"
|
|
139
|
+
:key="r.id"
|
|
140
|
+
:selected="selectedRunId === r.id"
|
|
141
|
+
:icon="getRunStatusIcon(r.status)"
|
|
142
|
+
:icon-class="getRunStatusIconClass(r.status)"
|
|
143
|
+
:subtitle="truncateId(r.id)"
|
|
144
|
+
:meta="formatTime(r.createdAt)"
|
|
146
145
|
@click="selectRun(r.id)"
|
|
147
146
|
>
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
</div>
|
|
153
|
-
<div class="flex items-center gap-3 mt-1.5">
|
|
154
|
-
<div class="text-xs text-gray-500">
|
|
155
|
-
{{ formatTime(r.createdAt) }}
|
|
156
|
-
</div>
|
|
157
|
-
<!-- Step progress -->
|
|
158
|
-
<div
|
|
159
|
-
v-if="r.stepCount > 0"
|
|
160
|
-
class="text-xs text-gray-500 flex items-center gap-1"
|
|
161
|
-
>
|
|
162
|
-
<UIcon
|
|
163
|
-
name="i-lucide-list-checks"
|
|
164
|
-
class="w-3 h-3"
|
|
165
|
-
/>
|
|
166
|
-
<span>{{ r.completedSteps }}/{{ r.stepCount }}</span>
|
|
167
|
-
</div>
|
|
168
|
-
<!-- Duration (if completed) -->
|
|
169
|
-
<div
|
|
170
|
-
v-if="r.completedAt && r.startedAt"
|
|
171
|
-
class="text-xs text-gray-500 flex items-center gap-1"
|
|
172
|
-
>
|
|
173
|
-
<UIcon
|
|
174
|
-
name="i-lucide-timer"
|
|
175
|
-
class="w-3 h-3"
|
|
176
|
-
/>
|
|
177
|
-
<span>{{ formatDuration(r.startedAt, r.completedAt) }}</span>
|
|
178
|
-
</div>
|
|
179
|
-
</div>
|
|
180
|
-
</div>
|
|
181
|
-
<!-- Status badge -->
|
|
147
|
+
<template #title>
|
|
148
|
+
Run {{ r.id?.substring(0, 8) }}
|
|
149
|
+
</template>
|
|
150
|
+
<template #badge>
|
|
182
151
|
<FlowRunStatusBadge
|
|
183
152
|
:is-running="r.status === 'running'"
|
|
184
153
|
:is-completed="r.status === 'completed'"
|
|
@@ -187,28 +156,56 @@
|
|
|
187
156
|
:is-stalled="r.status === 'stalled'"
|
|
188
157
|
:is-awaiting="r.status === 'awaiting'"
|
|
189
158
|
/>
|
|
190
|
-
</
|
|
159
|
+
</template>
|
|
160
|
+
<template #meta>
|
|
161
|
+
<span>{{ formatTime(r.createdAt) }}</span>
|
|
162
|
+
<span
|
|
163
|
+
v-if="r.stepCount > 0"
|
|
164
|
+
class="flex items-center gap-1"
|
|
165
|
+
>
|
|
166
|
+
<UIcon
|
|
167
|
+
name="i-lucide-list-checks"
|
|
168
|
+
class="w-3 h-3"
|
|
169
|
+
/>
|
|
170
|
+
{{ r.completedSteps }}/{{ r.stepCount }}
|
|
171
|
+
</span>
|
|
172
|
+
<span
|
|
173
|
+
v-if="r.completedAt && r.startedAt"
|
|
174
|
+
class="flex items-center gap-1"
|
|
175
|
+
>
|
|
176
|
+
<UIcon
|
|
177
|
+
name="i-lucide-timer"
|
|
178
|
+
class="w-3 h-3"
|
|
179
|
+
/>
|
|
180
|
+
{{ formatDuration(r.startedAt, r.completedAt) }}
|
|
181
|
+
</span>
|
|
182
|
+
</template>
|
|
183
|
+
</SelectableListItem>
|
|
184
|
+
|
|
185
|
+
<!-- Loading indicator -->
|
|
186
|
+
<div
|
|
187
|
+
v-if="loadingRuns"
|
|
188
|
+
class="px-4 py-3 text-center text-xs text-gray-400"
|
|
189
|
+
>
|
|
190
|
+
<UIcon
|
|
191
|
+
name="i-lucide-loader-2"
|
|
192
|
+
class="w-4 h-4 animate-spin inline-block"
|
|
193
|
+
/>
|
|
194
|
+
<span class="ml-2">Loading runs...</span>
|
|
191
195
|
</div>
|
|
192
196
|
</div>
|
|
193
197
|
|
|
194
|
-
<!--
|
|
198
|
+
<!-- Pagination Footer -->
|
|
195
199
|
<div
|
|
196
|
-
v-if="
|
|
197
|
-
class="px-4 py-3
|
|
200
|
+
v-if="totalRuns > runsPerPage"
|
|
201
|
+
class="border-t border-gray-200 dark:border-gray-800 px-4 py-3 flex items-center justify-center shrink-0"
|
|
198
202
|
>
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
|
|
203
|
+
<UPagination
|
|
204
|
+
v-model:page="currentPage"
|
|
205
|
+
:items-per-page="runsPerPage"
|
|
206
|
+
:total="totalRuns"
|
|
207
|
+
size="xs"
|
|
202
208
|
/>
|
|
203
|
-
<span class="ml-2">Loading more runs...</span>
|
|
204
|
-
</div>
|
|
205
|
-
|
|
206
|
-
<!-- End of list indicator -->
|
|
207
|
-
<div
|
|
208
|
-
v-else-if="!hasMoreRuns && runs.length > 0"
|
|
209
|
-
class="px-4 py-3 text-center text-xs text-gray-400"
|
|
210
|
-
>
|
|
211
|
-
All runs loaded
|
|
212
209
|
</div>
|
|
213
210
|
</div>
|
|
214
211
|
</div>
|
|
@@ -391,17 +388,17 @@
|
|
|
391
388
|
</template>
|
|
392
389
|
|
|
393
390
|
<script setup>
|
|
394
|
-
import { ref, computed, watch } from "#imports";
|
|
391
|
+
import { ref, computed, watch, onMounted, onUnmounted } from "#imports";
|
|
395
392
|
import FlowDiagram from "../../components/flow/Diagram.vue";
|
|
396
393
|
import FlowRunOverview from "../../components/flow/RunOverview.vue";
|
|
397
394
|
import FlowRunTimeline from "../../components/flow/RunTimeline.vue";
|
|
398
395
|
import FlowRunStatusBadge from "../../components/flow/RunStatusBadge.vue";
|
|
399
396
|
import ConfirmDialog from "../../components/ConfirmDialog.vue";
|
|
397
|
+
import SelectableListItem from "../../components/SelectableListItem.vue";
|
|
400
398
|
import { useRoute, useRouter } from "#app";
|
|
401
399
|
import { useAnalyzedFlows } from "../../composables/useAnalyzedFlows";
|
|
402
|
-
import {
|
|
400
|
+
import { useFlowRuns } from "../../composables/useFlowRuns";
|
|
403
401
|
import { useFlowRunTimeline } from "../../composables/useFlowRunTimeline";
|
|
404
|
-
import { useFlowRunsPolling } from "../../composables/useFlowRunsPolling";
|
|
405
402
|
import { useComponentRouter } from "../../composables/useComponentRouter";
|
|
406
403
|
const componentRouter = useComponentRouter();
|
|
407
404
|
const router = useRouter();
|
|
@@ -449,18 +446,64 @@ const selectedFlowDef = computed(() => {
|
|
|
449
446
|
});
|
|
450
447
|
const selectedFlowRef = computed(() => selectedFlow.value || "");
|
|
451
448
|
const selectedRunIdRef = computed(() => selectedRunId.value || "");
|
|
449
|
+
const runsPerPage = 20;
|
|
450
|
+
const currentPage = computed({
|
|
451
|
+
get: () => {
|
|
452
|
+
const page = route.query.page;
|
|
453
|
+
return page ? Number.parseInt(page, 10) : 1;
|
|
454
|
+
},
|
|
455
|
+
set: (value) => {
|
|
456
|
+
router.push({
|
|
457
|
+
query: {
|
|
458
|
+
...route.query,
|
|
459
|
+
page: value > 1 ? value.toString() : void 0
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
const runsQueryOptions = computed(() => ({
|
|
465
|
+
limit: runsPerPage,
|
|
466
|
+
offset: (currentPage.value - 1) * runsPerPage
|
|
467
|
+
}));
|
|
452
468
|
const {
|
|
453
|
-
|
|
454
|
-
total: totalRuns,
|
|
455
|
-
loading: loadingRuns,
|
|
456
|
-
hasMore: hasMoreRuns,
|
|
457
|
-
loadMore: loadMoreRuns,
|
|
469
|
+
runs: runsResponse,
|
|
458
470
|
refresh: refreshRuns,
|
|
459
|
-
|
|
460
|
-
} =
|
|
471
|
+
status: runsStatus
|
|
472
|
+
} = useFlowRuns(selectedFlowRef, runsQueryOptions);
|
|
473
|
+
const runs = computed(() => runsResponse.value?.items || []);
|
|
474
|
+
const totalRuns = computed(() => runsResponse.value?.total || 0);
|
|
475
|
+
const loadingRuns = computed(() => runsStatus.value === "pending");
|
|
461
476
|
const { flowState, isConnected, isReconnecting } = useFlowRunTimeline(selectedFlowRef, selectedRunIdRef);
|
|
462
|
-
|
|
463
|
-
|
|
477
|
+
let pollInterval = null;
|
|
478
|
+
const startPolling = () => {
|
|
479
|
+
if (pollInterval) return;
|
|
480
|
+
pollInterval = setInterval(() => {
|
|
481
|
+
if (selectedFlow.value) {
|
|
482
|
+
refreshRuns();
|
|
483
|
+
}
|
|
484
|
+
}, 3e3);
|
|
485
|
+
};
|
|
486
|
+
const stopPolling = () => {
|
|
487
|
+
if (pollInterval) {
|
|
488
|
+
clearInterval(pollInterval);
|
|
489
|
+
pollInterval = null;
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
watch(selectedFlow, (flow) => {
|
|
493
|
+
if (flow) {
|
|
494
|
+
startPolling();
|
|
495
|
+
} else {
|
|
496
|
+
stopPolling();
|
|
497
|
+
}
|
|
498
|
+
}, { immediate: true });
|
|
499
|
+
onMounted(() => {
|
|
500
|
+
if (import.meta.client && selectedFlow.value) {
|
|
501
|
+
startPolling();
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
onUnmounted(() => {
|
|
505
|
+
stopPolling();
|
|
506
|
+
});
|
|
464
507
|
const startFlowModalOpen = ref(false);
|
|
465
508
|
const flowInputJson = ref("{}");
|
|
466
509
|
const jsonError = ref("");
|
|
@@ -491,17 +534,6 @@ watch(flowInputJson, (value) => {
|
|
|
491
534
|
jsonError.value = err instanceof Error ? err.message : "Invalid JSON";
|
|
492
535
|
}
|
|
493
536
|
});
|
|
494
|
-
const runsScrollContainer = ref(null);
|
|
495
|
-
const handleRunsScroll = (event) => {
|
|
496
|
-
if (!hasMoreRuns.value || loadingRuns.value) return;
|
|
497
|
-
const container = event.target;
|
|
498
|
-
const scrollTop = container.scrollTop;
|
|
499
|
-
const scrollHeight = container.scrollHeight;
|
|
500
|
-
const clientHeight = container.clientHeight;
|
|
501
|
-
if (scrollTop + clientHeight >= scrollHeight - 200) {
|
|
502
|
-
loadMoreRuns();
|
|
503
|
-
}
|
|
504
|
-
};
|
|
505
537
|
const formatTime = (timestamp) => {
|
|
506
538
|
const date = new Date(timestamp);
|
|
507
539
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -533,6 +565,46 @@ const formatDuration = (start, end) => {
|
|
|
533
565
|
}
|
|
534
566
|
return `${seconds}s`;
|
|
535
567
|
};
|
|
568
|
+
const truncateId = (id) => {
|
|
569
|
+
if (!id || id.length <= 16) return id;
|
|
570
|
+
return `${id.substring(0, 8)}...${id.substring(id.length - 8)}`;
|
|
571
|
+
};
|
|
572
|
+
const getRunStatusIcon = (status) => {
|
|
573
|
+
switch (status) {
|
|
574
|
+
case "running":
|
|
575
|
+
return "i-lucide-loader-2";
|
|
576
|
+
case "completed":
|
|
577
|
+
return "i-lucide-check-circle";
|
|
578
|
+
case "failed":
|
|
579
|
+
return "i-lucide-x-circle";
|
|
580
|
+
case "canceled":
|
|
581
|
+
return "i-lucide-ban";
|
|
582
|
+
case "stalled":
|
|
583
|
+
return "i-lucide-alert-triangle";
|
|
584
|
+
case "awaiting":
|
|
585
|
+
return "i-lucide-pause-circle";
|
|
586
|
+
default:
|
|
587
|
+
return "i-lucide-circle";
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
const getRunStatusIconClass = (status) => {
|
|
591
|
+
switch (status) {
|
|
592
|
+
case "running":
|
|
593
|
+
return "text-blue-500 animate-spin";
|
|
594
|
+
case "completed":
|
|
595
|
+
return "text-emerald-500";
|
|
596
|
+
case "failed":
|
|
597
|
+
return "text-red-500";
|
|
598
|
+
case "canceled":
|
|
599
|
+
return "text-gray-500";
|
|
600
|
+
case "stalled":
|
|
601
|
+
return "text-amber-500";
|
|
602
|
+
case "awaiting":
|
|
603
|
+
return "text-purple-500";
|
|
604
|
+
default:
|
|
605
|
+
return "text-gray-400";
|
|
606
|
+
}
|
|
607
|
+
};
|
|
536
608
|
const runSnapshot = computed(() => {
|
|
537
609
|
const state = flowState.state.value;
|
|
538
610
|
const flowMeta = selectedFlowMeta.value;
|
|
@@ -88,50 +88,27 @@
|
|
|
88
88
|
class="flex-1 min-h-0 overflow-y-auto"
|
|
89
89
|
>
|
|
90
90
|
<div class="divide-y divide-gray-100 dark:divide-gray-800">
|
|
91
|
-
<
|
|
91
|
+
<SelectableListItem
|
|
92
92
|
v-for="job in paginatedJobs"
|
|
93
93
|
:key="job.id"
|
|
94
|
-
|
|
95
|
-
:
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
:selected="selectedJobId === job.id"
|
|
95
|
+
:icon="getJobIcon(job.state)"
|
|
96
|
+
:icon-class="getJobIconColor(job.state)"
|
|
97
|
+
:title="job.name"
|
|
98
|
+
:subtitle="truncateId(job.id)"
|
|
99
|
+
:badge="job.state || 'unknown'"
|
|
100
|
+
:badge-color="getStateBadgeColor(job.state)"
|
|
98
101
|
@click="selectJob(job.id)"
|
|
99
102
|
>
|
|
100
|
-
<
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
<div class="flex items-center justify-between gap-2 mb-1">
|
|
110
|
-
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
|
|
111
|
-
{{ job.name }}
|
|
112
|
-
</h3>
|
|
113
|
-
<UBadge
|
|
114
|
-
:label="job.state || 'unknown'"
|
|
115
|
-
:color="getStateBadgeColor(job.state)"
|
|
116
|
-
variant="subtle"
|
|
117
|
-
size="xs"
|
|
118
|
-
class="capitalize flex-shrink-0"
|
|
119
|
-
/>
|
|
120
|
-
</div>
|
|
121
|
-
<p class="text-xs text-gray-500 dark:text-gray-400 font-mono truncate mb-1">
|
|
122
|
-
{{ truncateId(job.id) }}
|
|
123
|
-
</p>
|
|
124
|
-
<div class="flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400">
|
|
125
|
-
<span v-if="job.timestamp">
|
|
126
|
-
{{ formatTime(job.timestamp) }}
|
|
127
|
-
</span>
|
|
128
|
-
<span v-if="job.finishedOn && job.processedOn">
|
|
129
|
-
• {{ formatDuration(job.processedOn, job.finishedOn) }}
|
|
130
|
-
</span>
|
|
131
|
-
</div>
|
|
132
|
-
</div>
|
|
133
|
-
</div>
|
|
134
|
-
</div>
|
|
103
|
+
<template #meta>
|
|
104
|
+
<span v-if="job.timestamp">
|
|
105
|
+
{{ formatTime(job.timestamp) }}
|
|
106
|
+
</span>
|
|
107
|
+
<span v-if="job.finishedOn && job.processedOn">
|
|
108
|
+
• {{ formatDuration(job.processedOn, job.finishedOn) }}
|
|
109
|
+
</span>
|
|
110
|
+
</template>
|
|
111
|
+
</SelectableListItem>
|
|
135
112
|
</div>
|
|
136
113
|
</div>
|
|
137
114
|
|
|
@@ -407,6 +384,7 @@ import { useComponentRouter } from "../../composables/useComponentRouter";
|
|
|
407
384
|
import { useRoute, useRouter } from "#app";
|
|
408
385
|
import StatCard from "../../components/StatCard.vue";
|
|
409
386
|
import QueueConfiguration from "../../components/QueueConfiguration.vue";
|
|
387
|
+
import SelectableListItem from "../../components/SelectableListItem.vue";
|
|
410
388
|
const componentRouter = useComponentRouter();
|
|
411
389
|
const router = useRouter();
|
|
412
390
|
const route = useRoute();
|
|
@@ -184,47 +184,19 @@
|
|
|
184
184
|
class="flex-1 min-h-0 overflow-y-auto"
|
|
185
185
|
>
|
|
186
186
|
<div class="divide-y divide-gray-100 dark:divide-gray-800">
|
|
187
|
-
<
|
|
187
|
+
<SelectableListItem
|
|
188
188
|
v-for="(event, idx) in paginatedEvents"
|
|
189
189
|
:key="idx"
|
|
190
|
-
|
|
191
|
-
:
|
|
192
|
-
|
|
193
|
-
|
|
190
|
+
:selected="!!(selectedEvent && selectedEvent.type === event.type && (selectedEvent.ts || selectedEvent.timestamp) === (event.ts || event.timestamp))"
|
|
191
|
+
:icon="getEventIcon(event.type)"
|
|
192
|
+
:icon-class="getEventIconColor(event.type)"
|
|
193
|
+
:title="event.type"
|
|
194
|
+
:subtitle="formatDate(event.ts || event.timestamp)"
|
|
195
|
+
:badge="event.type.split('.')[1] || 'event'"
|
|
196
|
+
:badge-color="getEventBadgeColor(event.type)"
|
|
197
|
+
:meta="formatTime(event.ts || event.timestamp)"
|
|
194
198
|
@click="selectEvent(event)"
|
|
195
|
-
|
|
196
|
-
<div class="flex items-start gap-3">
|
|
197
|
-
<div class="flex-shrink-0 mt-0.5">
|
|
198
|
-
<UIcon
|
|
199
|
-
:name="getEventIcon(event.type)"
|
|
200
|
-
class="w-5 h-5"
|
|
201
|
-
:class="getEventIconColor(event.type)"
|
|
202
|
-
/>
|
|
203
|
-
</div>
|
|
204
|
-
<div class="flex-1 min-w-0">
|
|
205
|
-
<div class="flex items-center justify-between gap-2 mb-1">
|
|
206
|
-
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
|
|
207
|
-
{{ event.type }}
|
|
208
|
-
</h3>
|
|
209
|
-
<UBadge
|
|
210
|
-
:label="event.type.split('.')[1] || 'event'"
|
|
211
|
-
:color="getEventBadgeColor(event.type)"
|
|
212
|
-
variant="subtle"
|
|
213
|
-
size="xs"
|
|
214
|
-
class="capitalize flex-shrink-0"
|
|
215
|
-
/>
|
|
216
|
-
</div>
|
|
217
|
-
<p class="text-xs text-gray-500 dark:text-gray-400 font-mono truncate mb-1">
|
|
218
|
-
{{ formatDate(event.ts || event.timestamp) }}
|
|
219
|
-
</p>
|
|
220
|
-
<div class="flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400">
|
|
221
|
-
<span>
|
|
222
|
-
{{ formatTime(event.ts || event.timestamp) }}
|
|
223
|
-
</span>
|
|
224
|
-
</div>
|
|
225
|
-
</div>
|
|
226
|
-
</div>
|
|
227
|
-
</div>
|
|
199
|
+
/>
|
|
228
200
|
</div>
|
|
229
201
|
</div>
|
|
230
202
|
|
|
@@ -549,6 +521,7 @@ import { useComponentRouter } from "../../composables/useComponentRouter";
|
|
|
549
521
|
import { useTriggerWebSocket } from "../../composables/useTriggerWebSocket";
|
|
550
522
|
import { useRoute, useRouter } from "#app";
|
|
551
523
|
import StatCard from "../../components/StatCard.vue";
|
|
524
|
+
import SelectableListItem from "../../components/SelectableListItem.vue";
|
|
552
525
|
const componentRouter = useComponentRouter();
|
|
553
526
|
const router = useRouter();
|
|
554
527
|
const route = useRoute();
|
package/package.json
CHANGED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
type __VLS_Props = {
|
|
2
|
-
queue: string;
|
|
3
|
-
};
|
|
4
|
-
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
5
|
-
declare const _default: typeof __VLS_export;
|
|
6
|
-
export default _default;
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="space-y-4">
|
|
3
|
-
<div class="flex justify-end">
|
|
4
|
-
<Button
|
|
5
|
-
icon="i-heroicons-clock"
|
|
6
|
-
color="neutral"
|
|
7
|
-
variant="outline"
|
|
8
|
-
@click="jobSchedulerEditor = !jobSchedulerEditor"
|
|
9
|
-
>
|
|
10
|
-
Job Scheduler
|
|
11
|
-
</Button>
|
|
12
|
-
</div>
|
|
13
|
-
<UForm
|
|
14
|
-
:schema="schema"
|
|
15
|
-
:state="state"
|
|
16
|
-
@submit="onSubmit"
|
|
17
|
-
>
|
|
18
|
-
<UCard
|
|
19
|
-
v-if="jobSchedulerEditor"
|
|
20
|
-
:ui="{
|
|
21
|
-
body: 'space-y-6'
|
|
22
|
-
}"
|
|
23
|
-
>
|
|
24
|
-
<UFormField
|
|
25
|
-
label="Name"
|
|
26
|
-
name="name"
|
|
27
|
-
>
|
|
28
|
-
<UInput
|
|
29
|
-
v-model="state.name"
|
|
30
|
-
placeholder="Name"
|
|
31
|
-
class="w-full"
|
|
32
|
-
/>
|
|
33
|
-
</UFormField>
|
|
34
|
-
<UTabs
|
|
35
|
-
v-model="state.scheduleType"
|
|
36
|
-
:items="scheduleInputTypes"
|
|
37
|
-
size="xs"
|
|
38
|
-
default-value="every"
|
|
39
|
-
variant="pill"
|
|
40
|
-
color="neutral"
|
|
41
|
-
:ui="{
|
|
42
|
-
list: 'w-28 self-start',
|
|
43
|
-
content: 'w-full'
|
|
44
|
-
}"
|
|
45
|
-
>
|
|
46
|
-
<template #content="{ item }">
|
|
47
|
-
<UFormField name="scheduleValue">
|
|
48
|
-
<UInput
|
|
49
|
-
v-if="item.value === 'every'"
|
|
50
|
-
v-model="state.scheduleValue"
|
|
51
|
-
class="w-full"
|
|
52
|
-
type="number"
|
|
53
|
-
/>
|
|
54
|
-
<UInput
|
|
55
|
-
v-if="item.value === 'cron'"
|
|
56
|
-
v-model="state.scheduleValue"
|
|
57
|
-
class="w-full"
|
|
58
|
-
type="string"
|
|
59
|
-
/>
|
|
60
|
-
</UFormField>
|
|
61
|
-
</template>
|
|
62
|
-
</UTabs>
|
|
63
|
-
<div>
|
|
64
|
-
<div class="text-sm font-bold mb-2">
|
|
65
|
-
Job
|
|
66
|
-
</div>
|
|
67
|
-
<div class="flex flex-col space-y-2 p-2 rounded-sm ring-1 ring-gray-200 dark:ring-gray-800 shadow">
|
|
68
|
-
<UFormField
|
|
69
|
-
label="Name"
|
|
70
|
-
name="jobName"
|
|
71
|
-
>
|
|
72
|
-
<UInput
|
|
73
|
-
v-model="state.jobName"
|
|
74
|
-
placeholder="Job Name"
|
|
75
|
-
class="w-full"
|
|
76
|
-
/>
|
|
77
|
-
</UFormField>
|
|
78
|
-
<UFormField
|
|
79
|
-
label="Data"
|
|
80
|
-
name="jobData"
|
|
81
|
-
>
|
|
82
|
-
<JsonEditorVue
|
|
83
|
-
v-model="state.jobData"
|
|
84
|
-
:main-menu-bar="false"
|
|
85
|
-
mode="text"
|
|
86
|
-
/>
|
|
87
|
-
</UFormField>
|
|
88
|
-
</div>
|
|
89
|
-
</div>
|
|
90
|
-
<template #footer>
|
|
91
|
-
<div class="flex justify-end">
|
|
92
|
-
<Button
|
|
93
|
-
type="submit"
|
|
94
|
-
color="neutral"
|
|
95
|
-
variant="outline"
|
|
96
|
-
class="cursor-pointer"
|
|
97
|
-
>
|
|
98
|
-
Create
|
|
99
|
-
</Button>
|
|
100
|
-
</div>
|
|
101
|
-
</template>
|
|
102
|
-
</UCard>
|
|
103
|
-
</UForm>
|
|
104
|
-
<div>
|
|
105
|
-
<div
|
|
106
|
-
v-if="scheduler && scheduler.length > 0"
|
|
107
|
-
class="space-y-4"
|
|
108
|
-
>
|
|
109
|
-
<div
|
|
110
|
-
v-for="item of scheduler"
|
|
111
|
-
:key="item.key"
|
|
112
|
-
class="flex flex-col rounded-sm ring-1 ring-gray-200 dark:ring-gray-800 shadow p-4"
|
|
113
|
-
>
|
|
114
|
-
<div class="flex justify-end">
|
|
115
|
-
<Button
|
|
116
|
-
icon="i-heroicons-x-circle"
|
|
117
|
-
color="error"
|
|
118
|
-
variant="outline"
|
|
119
|
-
class="cursor-pointer"
|
|
120
|
-
@click="deleteScheduledJob(item.key)"
|
|
121
|
-
/>
|
|
122
|
-
</div>
|
|
123
|
-
<div>
|
|
124
|
-
<span class="text-sm font-bold">Name:</span> {{ item.key }}
|
|
125
|
-
</div>
|
|
126
|
-
<div>
|
|
127
|
-
<span class="text-sm font-bold">Next schedule:</span> {{
|
|
128
|
-
new Date(item.next).toLocaleString("de", {
|
|
129
|
-
day: "numeric",
|
|
130
|
-
month: "short",
|
|
131
|
-
hour: "2-digit",
|
|
132
|
-
minute: "2-digit",
|
|
133
|
-
second: "2-digit",
|
|
134
|
-
hour12: false
|
|
135
|
-
})
|
|
136
|
-
}}
|
|
137
|
-
</div>
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
<div v-else>
|
|
141
|
-
<UAlert
|
|
142
|
-
color="info"
|
|
143
|
-
title="No scheduled jobs"
|
|
144
|
-
variant="subtle"
|
|
145
|
-
icon="i-heroicons-information-circle"
|
|
146
|
-
class="flex items-center space-x-2"
|
|
147
|
-
/>
|
|
148
|
-
</div>
|
|
149
|
-
</div>
|
|
150
|
-
</div>
|
|
151
|
-
</template>
|
|
152
|
-
|
|
153
|
-
<script setup>
|
|
154
|
-
import { z } from "zod";
|
|
155
|
-
import { ref, useFetch } from "#imports";
|
|
156
|
-
const props = defineProps({
|
|
157
|
-
queue: { type: String, required: true }
|
|
158
|
-
});
|
|
159
|
-
const jobSchedulerEditor = ref(false);
|
|
160
|
-
const scheduleInputTypes = [
|
|
161
|
-
{ label: "Every", value: "every" },
|
|
162
|
-
{ label: "Cron", value: "cron" }
|
|
163
|
-
];
|
|
164
|
-
const {
|
|
165
|
-
data: scheduler,
|
|
166
|
-
refresh
|
|
167
|
-
} = await useFetch(`/api/_queue/${props.queue}/job/scheduler`, {
|
|
168
|
-
method: "GET"
|
|
169
|
-
});
|
|
170
|
-
const state = ref({
|
|
171
|
-
name: void 0,
|
|
172
|
-
scheduleType: "every",
|
|
173
|
-
scheduleValue: void 0,
|
|
174
|
-
jobName: void 0,
|
|
175
|
-
jobData: void 0
|
|
176
|
-
});
|
|
177
|
-
const schema = z.object({
|
|
178
|
-
name: z.string().regex(/^\S*$/gm, "No spaces allowed"),
|
|
179
|
-
scheduleType: z.enum(["every", "cron"]),
|
|
180
|
-
scheduleValue: z.any(),
|
|
181
|
-
jobName: z.string().regex(/^\S*$/gm, "No spaces allowed"),
|
|
182
|
-
jobData: z.string().default("{}")
|
|
183
|
-
});
|
|
184
|
-
async function onSubmit(event) {
|
|
185
|
-
await $fetch(`/api/_queue/${props.queue}/job/scheduler`, {
|
|
186
|
-
method: "POST",
|
|
187
|
-
body: {
|
|
188
|
-
name: event.data.name,
|
|
189
|
-
scheduleType: event.data.scheduleType,
|
|
190
|
-
scheduleValue: event.data.scheduleValue,
|
|
191
|
-
jobName: event.data.jobName,
|
|
192
|
-
jobData: JSON.stringify(event.data.jobData)
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
refresh();
|
|
196
|
-
}
|
|
197
|
-
const deleteScheduledJob = async (id) => {
|
|
198
|
-
await $fetch(`/api/_queue/${props.queue}/job/scheduler/${id}`, {
|
|
199
|
-
method: "DELETE"
|
|
200
|
-
});
|
|
201
|
-
refresh();
|
|
202
|
-
};
|
|
203
|
-
</script>
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
type __VLS_Props = {
|
|
2
|
-
queue: string;
|
|
3
|
-
};
|
|
4
|
-
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
5
|
-
declare const _default: typeof __VLS_export;
|
|
6
|
-
export default _default;
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { DropdownMenuItem } from '@nuxt/ui';
|
|
2
|
-
type __VLS_Props = {
|
|
3
|
-
title?: string;
|
|
4
|
-
link: string;
|
|
5
|
-
origin?: string | null;
|
|
6
|
-
dropdown?: DropdownMenuItem[];
|
|
7
|
-
};
|
|
8
|
-
declare var __VLS_27: {};
|
|
9
|
-
type __VLS_Slots = {} & {
|
|
10
|
-
default?: (props: typeof __VLS_27) => any;
|
|
11
|
-
};
|
|
12
|
-
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
13
|
-
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
14
|
-
declare const _default: typeof __VLS_export;
|
|
15
|
-
export default _default;
|
|
16
|
-
type __VLS_WithSlots<T, S> = T & {
|
|
17
|
-
new (): {
|
|
18
|
-
$slots: S;
|
|
19
|
-
};
|
|
20
|
-
};
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div
|
|
3
|
-
:to="link"
|
|
4
|
-
class="rounded-lg divide-y divide-gray-200 dark:divide-gray-800 ring-1 ring-gray-200 dark:ring-gray-800 shadow bg-white dark:bg-gray-900"
|
|
5
|
-
>
|
|
6
|
-
<div class="px-4 py-5 sm:p-6">
|
|
7
|
-
<div class="flex flex-col md:flex-row">
|
|
8
|
-
<div class="flex-none flex flex-col justify-between space-y-2">
|
|
9
|
-
<ULink
|
|
10
|
-
class="inline-flex items-center gap-1"
|
|
11
|
-
@click="push(link)"
|
|
12
|
-
>
|
|
13
|
-
<span class="text-lg font-semibold">{{ title }}</span>
|
|
14
|
-
<UIcon
|
|
15
|
-
name="i-heroicons-arrow-up-right"
|
|
16
|
-
class="w-5 h-5 text-primary-500"
|
|
17
|
-
/>
|
|
18
|
-
</ULink>
|
|
19
|
-
<div class="flex flex-wrap items-center gap-2">
|
|
20
|
-
<div class="inline-flex gap-1 items-center">
|
|
21
|
-
<UIcon
|
|
22
|
-
name="i-heroicons-check-circle"
|
|
23
|
-
class="w-4 h-4 text-green-500"
|
|
24
|
-
/>
|
|
25
|
-
<span class="text-sm">Active</span>
|
|
26
|
-
</div>
|
|
27
|
-
<UBadge
|
|
28
|
-
v-if="origin"
|
|
29
|
-
size="sm"
|
|
30
|
-
color="neutral"
|
|
31
|
-
>
|
|
32
|
-
<span v-if="origin === 'local'">Local</span>
|
|
33
|
-
<span v-if="origin === 'remote'">Remote</span>
|
|
34
|
-
</UBadge>
|
|
35
|
-
</div>
|
|
36
|
-
</div>
|
|
37
|
-
<div class="grow pr-12">
|
|
38
|
-
<div class="flex flex-row gap-4 justify-end">
|
|
39
|
-
<slot />
|
|
40
|
-
</div>
|
|
41
|
-
</div>
|
|
42
|
-
<div class="flex-none">
|
|
43
|
-
<div class="flex gap-2 items-center">
|
|
44
|
-
<UDropdownMenu
|
|
45
|
-
:items="dropdown"
|
|
46
|
-
>
|
|
47
|
-
<UButton
|
|
48
|
-
icon="i-heroicons-ellipsis-vertical"
|
|
49
|
-
color="neutral"
|
|
50
|
-
variant="outline"
|
|
51
|
-
/>
|
|
52
|
-
</UDropdownMenu>
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
57
|
-
</div>
|
|
58
|
-
</template>
|
|
59
|
-
|
|
60
|
-
<script setup>
|
|
61
|
-
import { useComponentRouter } from "#imports";
|
|
62
|
-
defineProps({
|
|
63
|
-
title: { type: String, required: false, default: "" },
|
|
64
|
-
link: { type: String, required: true },
|
|
65
|
-
origin: { type: [String, null], required: false, default: null },
|
|
66
|
-
dropdown: { type: Array, required: false }
|
|
67
|
-
});
|
|
68
|
-
const { push } = useComponentRouter();
|
|
69
|
-
</script>
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { DropdownMenuItem } from '@nuxt/ui';
|
|
2
|
-
type __VLS_Props = {
|
|
3
|
-
title?: string;
|
|
4
|
-
link: string;
|
|
5
|
-
origin?: string | null;
|
|
6
|
-
dropdown?: DropdownMenuItem[];
|
|
7
|
-
};
|
|
8
|
-
declare var __VLS_27: {};
|
|
9
|
-
type __VLS_Slots = {} & {
|
|
10
|
-
default?: (props: typeof __VLS_27) => any;
|
|
11
|
-
};
|
|
12
|
-
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
13
|
-
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
14
|
-
declare const _default: typeof __VLS_export;
|
|
15
|
-
export default _default;
|
|
16
|
-
type __VLS_WithSlots<T, S> = T & {
|
|
17
|
-
new (): {
|
|
18
|
-
$slots: S;
|
|
19
|
-
};
|
|
20
|
-
};
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { type Ref } from '#imports';
|
|
2
|
-
/**
|
|
3
|
-
* Composable for infinite scroll flow runs with pagination
|
|
4
|
-
*/
|
|
5
|
-
export declare function useFlowRunsInfinite(flowId: Ref<string>): {
|
|
6
|
-
items: import("vue").ComputedRef<{
|
|
7
|
-
id: string;
|
|
8
|
-
flowName: string;
|
|
9
|
-
status: "running" | "completed" | "failed" | "canceled" | "stalled" | "awaiting" | "unknown";
|
|
10
|
-
createdAt: string;
|
|
11
|
-
startedAt?: string | undefined;
|
|
12
|
-
completedAt?: string | undefined;
|
|
13
|
-
stepCount: number;
|
|
14
|
-
completedSteps: number;
|
|
15
|
-
}[]>;
|
|
16
|
-
total: import("vue").ComputedRef<number>;
|
|
17
|
-
loading: import("vue").ComputedRef<boolean>;
|
|
18
|
-
hasMore: import("vue").ComputedRef<boolean>;
|
|
19
|
-
error: import("vue").ComputedRef<Error | null>;
|
|
20
|
-
loadMore: () => void;
|
|
21
|
-
refresh: () => Promise<void>;
|
|
22
|
-
checkForNewRuns: () => Promise<void>;
|
|
23
|
-
reset: () => void;
|
|
24
|
-
};
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { ref, computed, watch } from "#imports";
|
|
2
|
-
export function useFlowRunsInfinite(flowId) {
|
|
3
|
-
const items = ref([]);
|
|
4
|
-
const total = ref(0);
|
|
5
|
-
const offset = ref(0);
|
|
6
|
-
const limit = 50;
|
|
7
|
-
const loading = ref(false);
|
|
8
|
-
const hasMore = ref(true);
|
|
9
|
-
const error = ref(null);
|
|
10
|
-
const newestRunId = ref(null);
|
|
11
|
-
const reset = () => {
|
|
12
|
-
items.value = [];
|
|
13
|
-
total.value = 0;
|
|
14
|
-
offset.value = 0;
|
|
15
|
-
hasMore.value = true;
|
|
16
|
-
error.value = null;
|
|
17
|
-
newestRunId.value = null;
|
|
18
|
-
};
|
|
19
|
-
const fetchPage = async (resetData = false) => {
|
|
20
|
-
if (!flowId.value || loading.value) return;
|
|
21
|
-
if (resetData) {
|
|
22
|
-
reset();
|
|
23
|
-
}
|
|
24
|
-
if (!hasMore.value && !resetData) return;
|
|
25
|
-
try {
|
|
26
|
-
loading.value = true;
|
|
27
|
-
error.value = null;
|
|
28
|
-
const response = await $fetch(
|
|
29
|
-
`/api/_flows/${encodeURIComponent(flowId.value)}/runs`,
|
|
30
|
-
{
|
|
31
|
-
query: {
|
|
32
|
-
limit,
|
|
33
|
-
offset: resetData ? 0 : offset.value,
|
|
34
|
-
_t: Date.now()
|
|
35
|
-
// Cache busting
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
);
|
|
39
|
-
if (resetData) {
|
|
40
|
-
items.value = response.items;
|
|
41
|
-
offset.value = response.items.length;
|
|
42
|
-
if (response.items.length > 0) {
|
|
43
|
-
newestRunId.value = response.items[0].id;
|
|
44
|
-
}
|
|
45
|
-
} else {
|
|
46
|
-
items.value.push(...response.items);
|
|
47
|
-
offset.value += response.items.length;
|
|
48
|
-
}
|
|
49
|
-
total.value = response.total;
|
|
50
|
-
hasMore.value = response.hasMore;
|
|
51
|
-
} catch (err) {
|
|
52
|
-
console.error("[useFlowRunsInfinite] fetch error:", err);
|
|
53
|
-
error.value = err instanceof Error ? err : new Error(String(err));
|
|
54
|
-
} finally {
|
|
55
|
-
loading.value = false;
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
const loadMore = () => {
|
|
59
|
-
if (!loading.value && hasMore.value) {
|
|
60
|
-
fetchPage(false);
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
const refresh = async () => {
|
|
64
|
-
await fetchPage(true);
|
|
65
|
-
};
|
|
66
|
-
const checkForNewRuns = async () => {
|
|
67
|
-
if (!flowId.value || loading.value) return;
|
|
68
|
-
try {
|
|
69
|
-
const response = await $fetch(
|
|
70
|
-
`/api/_flows/${encodeURIComponent(flowId.value)}/runs`,
|
|
71
|
-
{
|
|
72
|
-
query: {
|
|
73
|
-
limit: Math.max(items.value.length, 10),
|
|
74
|
-
// Fetch at least as many as we have loaded
|
|
75
|
-
offset: 0,
|
|
76
|
-
_t: Date.now()
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
);
|
|
80
|
-
if (response.items.length === 0) return;
|
|
81
|
-
const latestRunId = response.items[0].id;
|
|
82
|
-
const updatedItems = [...items.value];
|
|
83
|
-
const newRuns = [];
|
|
84
|
-
for (const freshRun of response.items) {
|
|
85
|
-
const existingIndex = updatedItems.findIndex((r) => r.id === freshRun.id);
|
|
86
|
-
if (existingIndex >= 0) {
|
|
87
|
-
updatedItems[existingIndex] = freshRun;
|
|
88
|
-
} else {
|
|
89
|
-
newRuns.push(freshRun);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
if (newRuns.length > 0) {
|
|
93
|
-
items.value = [...newRuns, ...updatedItems];
|
|
94
|
-
newestRunId.value = latestRunId;
|
|
95
|
-
} else {
|
|
96
|
-
items.value = updatedItems;
|
|
97
|
-
}
|
|
98
|
-
total.value = response.total;
|
|
99
|
-
if (!newestRunId.value) {
|
|
100
|
-
newestRunId.value = latestRunId;
|
|
101
|
-
}
|
|
102
|
-
} catch (err) {
|
|
103
|
-
console.error("[useFlowRunsInfinite] checkForNewRuns error:", err);
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
watch(flowId, (newFlow, oldFlow) => {
|
|
107
|
-
if (import.meta.client && newFlow && newFlow !== oldFlow) {
|
|
108
|
-
refresh();
|
|
109
|
-
}
|
|
110
|
-
}, { immediate: true });
|
|
111
|
-
return {
|
|
112
|
-
items: computed(() => items.value),
|
|
113
|
-
total: computed(() => total.value),
|
|
114
|
-
loading: computed(() => loading.value),
|
|
115
|
-
hasMore: computed(() => hasMore.value),
|
|
116
|
-
error: computed(() => error.value),
|
|
117
|
-
loadMore,
|
|
118
|
-
refresh,
|
|
119
|
-
checkForNewRuns,
|
|
120
|
-
// For polling - prepends new runs without resetting
|
|
121
|
-
reset
|
|
122
|
-
};
|
|
123
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { type Ref } from '#imports';
|
|
2
|
-
/**
|
|
3
|
-
* Composable for auto-polling flow runs list
|
|
4
|
-
* Polls continuously to keep the list fresh
|
|
5
|
-
*/
|
|
6
|
-
export declare function useFlowRunsPolling(refresh: () => Promise<void>, shouldPoll: Ref<boolean>, intervalMs?: number): {
|
|
7
|
-
pause: () => void;
|
|
8
|
-
resume: () => void;
|
|
9
|
-
};
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { watch, onBeforeUnmount } from "#imports";
|
|
2
|
-
export function useFlowRunsPolling(refresh, shouldPoll, intervalMs = 3e3) {
|
|
3
|
-
let intervalId = null;
|
|
4
|
-
const pause = () => {
|
|
5
|
-
if (intervalId) {
|
|
6
|
-
clearInterval(intervalId);
|
|
7
|
-
intervalId = null;
|
|
8
|
-
}
|
|
9
|
-
};
|
|
10
|
-
const resume = () => {
|
|
11
|
-
if (!intervalId) {
|
|
12
|
-
intervalId = setInterval(async () => {
|
|
13
|
-
if (shouldPoll.value) {
|
|
14
|
-
await refresh();
|
|
15
|
-
}
|
|
16
|
-
}, intervalMs);
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
watch(shouldPoll, (should) => {
|
|
20
|
-
if (should) {
|
|
21
|
-
resume();
|
|
22
|
-
} else {
|
|
23
|
-
pause();
|
|
24
|
-
}
|
|
25
|
-
}, { immediate: true });
|
|
26
|
-
onBeforeUnmount(() => {
|
|
27
|
-
pause();
|
|
28
|
-
});
|
|
29
|
-
return {
|
|
30
|
-
pause,
|
|
31
|
-
resume
|
|
32
|
-
};
|
|
33
|
-
}
|