@stacksjs/defaults 0.75.263 → 0.75.266
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/ai/skills/stacks-browse/SKILL.md +3 -0
- package/ai/skills/stacks-browse/scripts/browse.ts +73 -14
- package/ai/skills/stacks-native/CATALOG.md +4 -4
- package/ide/vscode/package.json +1 -1
- package/package.json +4 -4
- package/resources/components/Dashboard/App/RequestsOverview.stx +75 -58
- package/resources/components/Dashboard/App/SourceInventory.stx +68 -59
- package/resources/components/Dashboard/Auth/AccessTokens.stx +57 -37
- package/resources/components/Dashboard/Billing/BillingTransactionHistory.stx +27 -33
- package/resources/components/Dashboard/Commerce/CommercePaymentsTable.stx +85 -49
- package/resources/components/Dashboard/Commerce/CommerceTaxesTable.stx +63 -66
- package/resources/components/Dashboard/Commerce/CommerceVariantsTable.stx +67 -23
- package/resources/components/Dashboard/Content/BlogDashboard.stx +73 -67
- package/resources/components/Dashboard/Content/FileManagerDashboard.stx +39 -12
- package/resources/components/Dashboard/Data/DataRecords.stx +85 -88
- package/resources/components/Dashboard/Deployments/DeploymentTable.stx +49 -64
- package/resources/components/Dashboard/Infrastructure/DnsDashboard.stx +46 -28
- package/resources/components/Dashboard/Operations/RecoveryPointTable.stx +72 -61
- package/resources/components/Dashboard/Operations/SchedulerTaskTable.stx +74 -70
- package/resources/components/Dashboard/Queue/QueueDashboard.stx +27 -61
- package/resources/components/Dashboard/Releases/ReleaseDashboard.stx +35 -61
- package/resources/components/Dashboard/Teams/TeamPeopleDashboard.stx +127 -143
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import type {
|
|
3
5
|
DashboardDnsDomain,
|
|
@@ -61,10 +63,40 @@ function typeColor(type: string): string {
|
|
|
61
63
|
return 'bg-rose-500'
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
const TYPE_BADGE = 'font-mono font-semibold text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40'
|
|
67
|
+
|
|
68
|
+
const COLUMNS = [
|
|
69
|
+
{
|
|
70
|
+
key: 'type',
|
|
71
|
+
label: 'Type',
|
|
72
|
+
type: 'badge' as const,
|
|
73
|
+
sortable: true,
|
|
74
|
+
badgeClasses: { A: TYPE_BADGE, AAAA: TYPE_BADGE, CNAME: TYPE_BADGE, MX: TYPE_BADGE, TXT: TYPE_BADGE },
|
|
75
|
+
},
|
|
76
|
+
{ key: 'name', label: 'Name', sortable: true, class: 'font-mono text-neutral-900 whitespace-nowrap dark:text-white' },
|
|
77
|
+
{ key: 'value', label: 'Value', sortable: true, emptyText: 'Not set', class: 'max-w-md font-mono truncate' },
|
|
78
|
+
{ key: 'ttlLabel', label: 'TTL', class: 'whitespace-nowrap' },
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
const ROW_ACTIONS = [
|
|
82
|
+
{ action: 'inspect', label: 'Inspect', icon: 'i-hugeicons-view' },
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
const recordRows = derived(() => filteredRecords().map((record, index) => ({
|
|
86
|
+
...record,
|
|
87
|
+
rowId: `${record.type}:${record.name}:${index}`,
|
|
88
|
+
ttlLabel: ttlLabel(record),
|
|
89
|
+
})))
|
|
90
|
+
|
|
64
91
|
function inspectRecord(record: DashboardDnsRecord): void {
|
|
65
92
|
selectedRecord.set(record)
|
|
66
93
|
}
|
|
67
94
|
|
|
95
|
+
function handleRowAction(payload: { action: string, row: DashboardDnsRecord }): void {
|
|
96
|
+
if (payload.action === 'inspect')
|
|
97
|
+
inspectRecord(payload.row)
|
|
98
|
+
}
|
|
99
|
+
|
|
68
100
|
function closeRecord(): void {
|
|
69
101
|
selectedRecord.set(null)
|
|
70
102
|
}
|
|
@@ -202,34 +234,20 @@ onMount(() => {
|
|
|
202
234
|
</div>
|
|
203
235
|
</div>
|
|
204
236
|
|
|
205
|
-
<div
|
|
206
|
-
<
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
<th scope="col" class="px-5 py-3 font-semibold text-left text-neutral-500 text-xs uppercase dark:text-neutral-400">TTL</th>
|
|
220
|
-
<th scope="col" class="px-5 py-3"><span class="sr-only">Actions</span></th>
|
|
221
|
-
</tr>
|
|
222
|
-
</thead>
|
|
223
|
-
<tbody class="divide-neutral-100 divide-y dark:divide-neutral-800">
|
|
224
|
-
<tr :for="record in filteredRecords()" class="hover:bg-neutral-50 dark:hover:bg-neutral-800/50">
|
|
225
|
-
<td class="px-5 py-4"><span class="inline-flex px-2 py-1 font-mono font-semibold text-blue-700 text-xs dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40 rounded-md">{{ record.type }}</span></td>
|
|
226
|
-
<td class="px-5 py-4 font-mono text-neutral-900 text-sm whitespace-nowrap dark:text-white">{{ record.name }}</td>
|
|
227
|
-
<td class="px-5 py-4 max-w-md font-mono text-neutral-500 text-sm truncate dark:text-neutral-400">{{ record.value || 'Not set' }}</td>
|
|
228
|
-
<td class="px-5 py-4 text-neutral-500 text-sm whitespace-nowrap dark:text-neutral-400">{{ ttlLabel(record) }}</td>
|
|
229
|
-
<td class="px-5 py-4 text-right"><Button variant="ghost" size="sm" @click="inspectRecord(record)">Inspect</Button></td>
|
|
230
|
-
</tr>
|
|
231
|
-
</tbody>
|
|
232
|
-
</table>
|
|
237
|
+
<div class="mt-4 bg-white dark:bg-neutral-900 border border-neutral-200 rounded-xl dark:border-neutral-700">
|
|
238
|
+
<Table
|
|
239
|
+
:columns="COLUMNS"
|
|
240
|
+
:data="recordRows()"
|
|
241
|
+
:loading="loading()"
|
|
242
|
+
:row-actions="ROW_ACTIONS"
|
|
243
|
+
action-label-key="name"
|
|
244
|
+
row-key="rowId"
|
|
245
|
+
paginated
|
|
246
|
+
:page-size="25"
|
|
247
|
+
:empty-title="records().length ? 'No matching DNS records' : 'No DNS records configured'"
|
|
248
|
+
:empty-description="records().length ? 'Adjust the search or record type filter.' : 'Add records to config/dns.ts, then refresh this page.'"
|
|
249
|
+
@row-action="handleRowAction"
|
|
250
|
+
/>
|
|
233
251
|
</div>
|
|
234
252
|
</section>
|
|
235
253
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import type { RecoveryPoint, RecoveryPolicy } from '../../../../functions/operations'
|
|
3
5
|
|
|
@@ -6,17 +8,8 @@ const policyItems = useReactiveProp('policies', [])
|
|
|
6
8
|
const busyId = useReactiveProp('busyId', '')
|
|
7
9
|
const emit = defineEmits()
|
|
8
10
|
|
|
9
|
-
function points(): RecoveryPoint[] { return pointItems() as RecoveryPoint[] }
|
|
10
|
-
function policies(): RecoveryPolicy[] { return policyItems() as RecoveryPolicy[] }
|
|
11
|
-
|
|
12
11
|
function policyName(id?: string): string {
|
|
13
|
-
return
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function formatDate(value?: string): string {
|
|
17
|
-
if (!value) return 'Not recorded'
|
|
18
|
-
const date = new Date(value)
|
|
19
|
-
return Number.isNaN(date.getTime()) ? value : new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(date)
|
|
12
|
+
return (policyItems() as RecoveryPolicy[]).find(policy => policy.id === id)?.name || 'Manual recovery point'
|
|
20
13
|
}
|
|
21
14
|
|
|
22
15
|
function formatBytes(value: number): string {
|
|
@@ -26,57 +19,75 @@ function formatBytes(value: number): string {
|
|
|
26
19
|
return `${(value / 1024 ** index).toFixed(index === 0 ? 0 : 1)} ${units[index]}`
|
|
27
20
|
}
|
|
28
21
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (state === '
|
|
32
|
-
if (state === '
|
|
33
|
-
return '
|
|
22
|
+
/** The pill's colour: verified, failed in either way, in progress, or not yet. */
|
|
23
|
+
function verificationTone(state: RecoveryPoint['verificationState']): string {
|
|
24
|
+
if (state === 'verified') return 'verified'
|
|
25
|
+
if (state === 'corrupt' || state === 'failed') return 'failed'
|
|
26
|
+
if (state === 'verifying') return 'verifying'
|
|
27
|
+
return 'unverified'
|
|
34
28
|
}
|
|
29
|
+
|
|
30
|
+
/** Protection is slotted, for its two independent badges. */
|
|
31
|
+
const COLUMNS = [
|
|
32
|
+
{ key: 'policyName', label: 'Recovery point', sortable: true, subtitleKey: 'detail', class: 'font-medium text-gray-900 dark:text-white' },
|
|
33
|
+
{ key: 'pointInTime', label: 'Created', type: 'datetime' as const, sortable: true, emptyText: 'Not recorded', class: 'whitespace-nowrap' },
|
|
34
|
+
{
|
|
35
|
+
key: 'verificationTone',
|
|
36
|
+
label: 'Verification',
|
|
37
|
+
type: 'badge' as const,
|
|
38
|
+
sortable: true,
|
|
39
|
+
badgeLabelKey: 'verificationState',
|
|
40
|
+
badgeClasses: {
|
|
41
|
+
verified: 'text-emerald-700 dark:text-emerald-300 bg-emerald-50 dark:bg-emerald-950/40',
|
|
42
|
+
failed: 'text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-950/40',
|
|
43
|
+
verifying: 'text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40',
|
|
44
|
+
unverified: 'text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-950/40',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{ key: 'protection', label: 'Protection', slotted: true, searchable: false },
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
const rows = derived(() => (pointItems() as RecoveryPoint[]).map(point => ({
|
|
51
|
+
...point,
|
|
52
|
+
policyName: policyName(point.policyId),
|
|
53
|
+
detail: `${point.id} · ${point.kind} · ${formatBytes(point.sizeBytes)}`,
|
|
54
|
+
verificationTone: verificationTone(point.verificationState),
|
|
55
|
+
protection: [point.pinned ? 'Pinned' : '', point.held ? 'Held' : ''].filter(Boolean).join(', ') || 'Retention policy',
|
|
56
|
+
})))
|
|
35
57
|
</script>
|
|
36
58
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
</
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
<Button size="sm" :disabled="busyId() !== '' || point.status !== 'available'" :loading="busyId() === point.id + ':verify'" @click="emit('verify', point.id)">Verify</Button>
|
|
73
|
-
<Button size="sm" variant="outline" :disabled="busyId() !== '' || point.status !== 'available' || point.verificationState !== 'verified'" @click="emit('restore', point)">Restore</Button>
|
|
74
|
-
<Button size="sm" variant="ghost" :disabled="busyId() !== ''" :loading="busyId() === point.id + ':protect'" @click="emit('protect', { id: point.id, pinned: !point.pinned })">{{ point.pinned ? 'Unpin' : 'Pin' }}</Button>
|
|
75
|
-
</div>
|
|
76
|
-
</td>
|
|
77
|
-
</tr>
|
|
78
|
-
</template>
|
|
79
|
-
</tbody>
|
|
80
|
-
</table>
|
|
81
|
-
</div>
|
|
82
|
-
</div>
|
|
59
|
+
{{--
|
|
60
|
+
The actions are a slot: text buttons whose disabled and loading state
|
|
61
|
+
depends on the row and on which request is in flight.
|
|
62
|
+
--}}
|
|
63
|
+
<Table
|
|
64
|
+
:columns="COLUMNS"
|
|
65
|
+
:data="rows()"
|
|
66
|
+
paginated
|
|
67
|
+
:page-size="25"
|
|
68
|
+
empty-title="No recovery points yet"
|
|
69
|
+
empty-description="Create and run a recovery policy to produce the first independently verifiable point."
|
|
70
|
+
>
|
|
71
|
+
<template #cell="{ row }">
|
|
72
|
+
<div class="flex flex-wrap gap-1.5">
|
|
73
|
+
{{-- A directive on a component inside a slot is not evaluated, so each
|
|
74
|
+
conditional component sits in a template that carries it. --}}
|
|
75
|
+
<template :if="row.pinned"><Badge variant="primary">Pinned</Badge></template>
|
|
76
|
+
<template :if="row.held"><Badge variant="warning">Held</Badge></template>
|
|
77
|
+
<span :if="!row.pinned && !row.held" class="text-gray-500 text-xs dark:text-neutral-400">Retention policy</span>
|
|
78
|
+
</div>
|
|
79
|
+
</template>
|
|
80
|
+
|
|
81
|
+
<template #actions="{ row }">
|
|
82
|
+
<div class="inline-flex gap-2 items-center justify-end w-full whitespace-nowrap">
|
|
83
|
+
<Button size="sm" :disabled="busyId() !== '' || row.status !== 'available'" :loading="busyId() === row.id + ':verify'" @click="emit('verify', row.id)">Verify</Button>
|
|
84
|
+
<Button size="sm" variant="outline" :disabled="busyId() !== '' || row.status !== 'available' || row.verificationState !== 'verified'" @click="emit('restore', row)">Restore</Button>
|
|
85
|
+
<template :if="row.pinned">
|
|
86
|
+
<Button size="sm" variant="ghost" :disabled="busyId() !== ''" :loading="busyId() === row.id + ':protect'" @click="emit('protect', { id: row.id, pinned: false })">Unpin</Button>
|
|
87
|
+
</template>
|
|
88
|
+
<template :else>
|
|
89
|
+
<Button size="sm" variant="ghost" :disabled="busyId() !== ''" :loading="busyId() === row.id + ':protect'" @click="emit('protect', { id: row.id, pinned: true })">Pin</Button>
|
|
90
|
+
</template>
|
|
91
|
+
</div>
|
|
92
|
+
</template>
|
|
93
|
+
</Table>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import type { SchedulerTask } from '../../../../functions/operations'
|
|
3
5
|
|
|
@@ -6,76 +8,78 @@ const loading = useReactiveProp('loading', false)
|
|
|
6
8
|
const busyTask = useReactiveProp('busyTask', '')
|
|
7
9
|
const emit = defineEmits()
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
11
|
+
const COLUMNS = [
|
|
12
|
+
{ key: 'name', label: 'Task', sortable: true, subtitleKey: 'timezone', class: 'font-medium text-gray-900 dark:text-white' },
|
|
13
|
+
{ key: 'pattern', label: 'Schedule', emptyText: 'Not configured', class: 'font-mono whitespace-nowrap' },
|
|
14
|
+
{ key: 'nextRun', label: 'Next run', type: 'datetime' as const, sortable: true, emptyText: 'Not scheduled', class: 'whitespace-nowrap' },
|
|
15
|
+
{
|
|
16
|
+
key: 'state',
|
|
17
|
+
label: 'Status',
|
|
18
|
+
type: 'badge' as const,
|
|
19
|
+
sortable: true,
|
|
20
|
+
badgeClasses: {
|
|
21
|
+
active: 'text-emerald-700 dark:text-emerald-300 bg-emerald-50 dark:bg-emerald-950/40',
|
|
22
|
+
paused: 'text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-950/40',
|
|
23
|
+
},
|
|
24
|
+
badgeLabels: { active: 'Active', paused: 'Paused' },
|
|
25
|
+
},
|
|
26
|
+
]
|
|
12
27
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return Number.isNaN(date.getTime())
|
|
18
|
-
? value
|
|
19
|
-
: new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(date)
|
|
20
|
-
}
|
|
28
|
+
const rows = derived(() => (tasks() as SchedulerTask[]).map(task => ({
|
|
29
|
+
...task,
|
|
30
|
+
state: task.enabled ? 'active' : 'paused',
|
|
31
|
+
})))
|
|
21
32
|
</script>
|
|
22
33
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
</td>
|
|
76
|
-
</tr>
|
|
77
|
-
</template>
|
|
78
|
-
</tbody>
|
|
79
|
-
</table>
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
34
|
+
{{--
|
|
35
|
+
The actions are a slot rather than declared row actions: they are text
|
|
36
|
+
buttons whose disabled and loading state depends on which request is in
|
|
37
|
+
flight, not icons.
|
|
38
|
+
--}}
|
|
39
|
+
<Table
|
|
40
|
+
:columns="COLUMNS"
|
|
41
|
+
:data="rows()"
|
|
42
|
+
:loading="loading()"
|
|
43
|
+
row-key="name"
|
|
44
|
+
paginated
|
|
45
|
+
:page-size="25"
|
|
46
|
+
empty-title="No scheduled tasks registered"
|
|
47
|
+
empty-description="Add a rate to an app job or register a task in app/Scheduler.ts."
|
|
48
|
+
>
|
|
49
|
+
<template #actions="{ row }">
|
|
50
|
+
<div class="inline-flex gap-2 items-center justify-end w-full whitespace-nowrap">
|
|
51
|
+
<Button
|
|
52
|
+
size="sm"
|
|
53
|
+
:disabled="busyTask() !== '' || !row.enabled"
|
|
54
|
+
:loading="busyTask() === row.name + ':run'"
|
|
55
|
+
@click="emit('run', row.name)"
|
|
56
|
+
>
|
|
57
|
+
Run now
|
|
58
|
+
</Button>
|
|
59
|
+
{{-- Two buttons rather than one with a computed label: the label is
|
|
60
|
+
the Button's slot content, which a binding on Button cannot set. --}}
|
|
61
|
+
<template :if="row.enabled">
|
|
62
|
+
<Button
|
|
63
|
+
size="sm"
|
|
64
|
+
variant="outline"
|
|
65
|
+
:disabled="busyTask() !== ''"
|
|
66
|
+
:loading="busyTask() === row.name + ':toggle'"
|
|
67
|
+
@click="emit('toggle', { name: row.name, enabled: false })"
|
|
68
|
+
>
|
|
69
|
+
Pause
|
|
70
|
+
</Button>
|
|
71
|
+
</template>
|
|
72
|
+
<template :else>
|
|
73
|
+
<Button
|
|
74
|
+
size="sm"
|
|
75
|
+
variant="outline"
|
|
76
|
+
:disabled="busyTask() !== ''"
|
|
77
|
+
:loading="busyTask() === row.name + ':toggle'"
|
|
78
|
+
@click="emit('toggle', { name: row.name, enabled: true })"
|
|
79
|
+
>
|
|
80
|
+
Resume
|
|
81
|
+
</Button>
|
|
82
|
+
</template>
|
|
83
|
+
</div>
|
|
84
|
+
</template>
|
|
85
|
+
</Table>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import type { QueueAggregateStats, QueueBucket, QueueWorker } from '../../../../functions/queue'
|
|
3
5
|
import { fetchQueueStats, fetchQueueWorkers, retryQueueFailures } from '../../../../functions/queue'
|
|
@@ -29,14 +31,27 @@ function workerRows(): QueueWorker[] {
|
|
|
29
31
|
return workers()
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
const count = (key: string, label: string) => ({ key, label, type: 'number' as const, align: 'right' as const, sortable: true, searchable: false, class: 'text-gray-500 dark:text-neutral-300' })
|
|
35
|
+
|
|
36
|
+
const QUEUE_COLUMNS = [
|
|
37
|
+
{ key: 'name', label: 'Queue', sortable: true, subtitleKey: 'status', class: 'font-medium text-gray-900 dark:text-white' },
|
|
38
|
+
count('pending', 'Pending'),
|
|
39
|
+
count('active', 'Active'),
|
|
40
|
+
count('completed', 'Completed'),
|
|
41
|
+
count('failed', 'Failed'),
|
|
42
|
+
{ ...count('total', 'Total'), class: 'font-medium text-gray-900 dark:text-white' },
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
const WORKER_COLUMNS = [
|
|
46
|
+
{ key: 'name', label: 'Worker', sortable: true, class: 'font-medium text-gray-900 dark:text-white' },
|
|
47
|
+
{ key: 'queuesLabel', label: 'Queues', sortable: true, class: 'text-gray-500 dark:text-neutral-300' },
|
|
48
|
+
{ key: 'status', label: 'Status', sortable: true, class: 'text-gray-500 dark:text-neutral-300' },
|
|
49
|
+
count('jobs_processed', 'Processed'),
|
|
50
|
+
count('failed_jobs', 'Failed'),
|
|
51
|
+
{ key: 'last_heartbeat', label: 'Last heartbeat', type: 'datetime' as const, align: 'right' as const, sortable: true, emptyText: 'Not recorded', class: 'text-gray-500 dark:text-neutral-300' },
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
const workerTableRows = derived(() => workerRows().map(worker => ({ ...worker, queuesLabel: worker.queues.join(', ') })))
|
|
40
55
|
|
|
41
56
|
async function loadQueue(): Promise<void> {
|
|
42
57
|
isLoading.set(true)
|
|
@@ -129,34 +144,8 @@ onMount(() => {
|
|
|
129
144
|
<p class="font-semibold text-gray-900 dark:text-white">No queues have recorded jobs</p>
|
|
130
145
|
<p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Dispatch a job to populate queue metrics.</p>
|
|
131
146
|
</div>
|
|
132
|
-
<div :if="queueRows().length > 0"
|
|
133
|
-
<
|
|
134
|
-
<thead class="bg-gray-50 dark:bg-neutral-700/50">
|
|
135
|
-
<tr>
|
|
136
|
-
<th class="px-4 py-3 font-medium text-gray-500 text-left text-xs uppercase dark:text-neutral-300">Queue</th>
|
|
137
|
-
<th class="px-4 py-3 font-medium text-gray-500 text-right text-xs uppercase dark:text-neutral-300">Pending</th>
|
|
138
|
-
<th class="px-4 py-3 font-medium text-gray-500 text-right text-xs uppercase dark:text-neutral-300">Active</th>
|
|
139
|
-
<th class="px-4 py-3 font-medium text-gray-500 text-right text-xs uppercase dark:text-neutral-300">Completed</th>
|
|
140
|
-
<th class="px-4 py-3 font-medium text-gray-500 text-right text-xs uppercase dark:text-neutral-300">Failed</th>
|
|
141
|
-
<th class="px-4 py-3 font-medium text-gray-500 text-right text-xs uppercase dark:text-neutral-300">Total</th>
|
|
142
|
-
</tr>
|
|
143
|
-
</thead>
|
|
144
|
-
<tbody class="divide-gray-200 divide-y dark:divide-neutral-700">
|
|
145
|
-
<template :for="queue in queueRows()">
|
|
146
|
-
<tr>
|
|
147
|
-
<td class="px-4 py-3">
|
|
148
|
-
<p class="font-medium text-gray-900 text-sm dark:text-white">{{ queue.name }}</p>
|
|
149
|
-
<p class="text-gray-500 text-xs dark:text-neutral-400">{{ queue.status }}</p>
|
|
150
|
-
</td>
|
|
151
|
-
<td class="px-4 py-3 text-gray-500 text-right text-sm dark:text-neutral-300">{{ queue.pending }}</td>
|
|
152
|
-
<td class="px-4 py-3 text-gray-500 text-right text-sm dark:text-neutral-300">{{ queue.active }}</td>
|
|
153
|
-
<td class="px-4 py-3 text-gray-500 text-right text-sm dark:text-neutral-300">{{ queue.completed }}</td>
|
|
154
|
-
<td class="px-4 py-3 text-gray-500 text-right text-sm dark:text-neutral-300">{{ queue.failed }}</td>
|
|
155
|
-
<td class="px-4 py-3 font-medium text-gray-900 text-right text-sm dark:text-white">{{ queue.total }}</td>
|
|
156
|
-
</tr>
|
|
157
|
-
</template>
|
|
158
|
-
</tbody>
|
|
159
|
-
</table>
|
|
147
|
+
<div :if="queueRows().length > 0">
|
|
148
|
+
<Table :columns="QUEUE_COLUMNS" :data="queueRows()" row-key="name" paginated :page-size="25" />
|
|
160
149
|
</div>
|
|
161
150
|
</section>
|
|
162
151
|
|
|
@@ -175,31 +164,8 @@ onMount(() => {
|
|
|
175
164
|
<p class="font-semibold text-gray-900 dark:text-white">No workers tracked in this process</p>
|
|
176
165
|
<p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Start a queue worker to populate heartbeat and processing metrics.</p>
|
|
177
166
|
</div>
|
|
178
|
-
<div :if="workerRows().length > 0"
|
|
179
|
-
<
|
|
180
|
-
<thead class="bg-gray-50 dark:bg-neutral-700/50">
|
|
181
|
-
<tr>
|
|
182
|
-
<th class="px-4 py-3 font-medium text-gray-500 text-left text-xs uppercase dark:text-neutral-300">Worker</th>
|
|
183
|
-
<th class="px-4 py-3 font-medium text-gray-500 text-left text-xs uppercase dark:text-neutral-300">Queues</th>
|
|
184
|
-
<th class="px-4 py-3 font-medium text-gray-500 text-left text-xs uppercase dark:text-neutral-300">Status</th>
|
|
185
|
-
<th class="px-4 py-3 font-medium text-gray-500 text-right text-xs uppercase dark:text-neutral-300">Processed</th>
|
|
186
|
-
<th class="px-4 py-3 font-medium text-gray-500 text-right text-xs uppercase dark:text-neutral-300">Failed</th>
|
|
187
|
-
<th class="px-4 py-3 font-medium text-gray-500 text-right text-xs uppercase dark:text-neutral-300">Last heartbeat</th>
|
|
188
|
-
</tr>
|
|
189
|
-
</thead>
|
|
190
|
-
<tbody class="divide-gray-200 divide-y dark:divide-neutral-700">
|
|
191
|
-
<template :for="worker in workerRows()">
|
|
192
|
-
<tr>
|
|
193
|
-
<td class="px-4 py-3 font-medium text-gray-900 text-sm dark:text-white">{{ worker.name }}</td>
|
|
194
|
-
<td class="px-4 py-3 text-gray-500 text-sm dark:text-neutral-300">{{ worker.queues.join(', ') }}</td>
|
|
195
|
-
<td class="px-4 py-3 text-gray-500 text-sm dark:text-neutral-300">{{ worker.status }}</td>
|
|
196
|
-
<td class="px-4 py-3 text-gray-500 text-right text-sm dark:text-neutral-300">{{ formatNumber(worker.jobs_processed) }}</td>
|
|
197
|
-
<td class="px-4 py-3 text-gray-500 text-right text-sm dark:text-neutral-300">{{ formatNumber(worker.failed_jobs) }}</td>
|
|
198
|
-
<td class="px-4 py-3 text-gray-500 text-right text-sm dark:text-neutral-300">{{ formatDate(worker.last_heartbeat) }}</td>
|
|
199
|
-
</tr>
|
|
200
|
-
</template>
|
|
201
|
-
</tbody>
|
|
202
|
-
</table>
|
|
167
|
+
<div :if="workerRows().length > 0">
|
|
168
|
+
<Table :columns="WORKER_COLUMNS" :data="workerTableRows()" row-key="name" paginated :page-size="25" />
|
|
203
169
|
</div>
|
|
204
170
|
</section>
|
|
205
171
|
</div>
|