@stacksjs/defaults 0.75.242 → 0.75.243
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-actions/SKILL.md +1 -1
- package/ai/skills/stacks-native/CATALOG.md +6 -6
- package/app/Actions/Dashboard/hq-read.ts +144 -0
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/resources/components/Dashboard/Commerce/CommerceManufacturersTable.stx +58 -61
- package/resources/components/Dashboard/Commerce/CommerceUnitsTable.stx +59 -61
- package/resources/components/Dashboard/Commerce/ProductWaitlistTable.stx +49 -61
- package/resources/components/Dashboard/Commerce/RestaurantWaitlistTable.stx +66 -71
- package/resources/components/Dashboard/Library/DependenciesDashboard.stx +68 -14
- package/resources/components/Dashboard/Realtime/RealtimeDashboard.stx +25 -26
- package/resources/components/Dashboard/UI/Table.stx +244 -9
- package/stx-components-plugin.ts +8 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import type { ProductWaitlistOptions, ProductWaitlistRecord } from '../../../../app/Actions/Dashboard/Commerce/product-waitlist-records'
|
|
3
5
|
|
|
@@ -9,73 +11,59 @@ const options = useReactiveProp('options', {
|
|
|
9
11
|
const loading = useReactiveProp('loading', false)
|
|
10
12
|
const emit = defineEmits()
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return Number.isNaN(date.getTime()) ? value : new Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(date)
|
|
14
|
+
const STATUS_CLASSES = {
|
|
15
|
+
purchased: 'text-purple-700 dark:text-purple-300 bg-purple-50 dark:bg-purple-950/30',
|
|
16
|
+
notified: 'text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/30',
|
|
17
|
+
waiting: 'text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/30',
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
const COLUMNS = [
|
|
21
|
+
{ key: 'name', label: 'Customer', sortable: true, emptyText: 'Unnamed', subtitleKey: 'contact', class: 'font-medium text-gray-900 dark:text-white' },
|
|
22
|
+
{ key: 'productName', label: 'Product', sortable: true },
|
|
23
|
+
{ key: 'quantity', label: 'Quantity', type: 'number' as const, sortable: true, align: 'right' as const, class: 'font-mono' },
|
|
24
|
+
{ key: 'status', label: 'Status', type: 'badge' as const, sortable: true, badgeClasses: STATUS_CLASSES, emptyText: 'unknown' },
|
|
25
|
+
{ key: 'notificationPreference', label: 'Preference', sortable: true, emptyText: 'Not recorded', class: 'capitalize' },
|
|
26
|
+
{ key: 'createdAt', label: 'Created', type: 'date' as const, sortable: true, emptyText: 'Not recorded', class: 'whitespace-nowrap' },
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
const ROW_ACTIONS = [
|
|
30
|
+
{ action: 'edit', label: 'Edit waitlist entry for', icon: 'i-hugeicons-edit-01' },
|
|
31
|
+
{ action: 'delete', label: 'Delete waitlist entry for', icon: 'i-hugeicons-waste', class: 'text-red-500' },
|
|
32
|
+
]
|
|
33
|
+
|
|
19
34
|
function productLabel(productId: string): string {
|
|
20
35
|
return options().products.find(option => option.id === productId)?.label || (productId ? `Product ${productId}` : 'Not linked')
|
|
21
36
|
}
|
|
22
37
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
/**
|
|
39
|
+
* `contact` and `productName` are computed per row because a column reads a
|
|
40
|
+
* field: the contact line falls back through email then phone, and the product
|
|
41
|
+
* name is looked up in the options the parent passed.
|
|
42
|
+
*/
|
|
43
|
+
const rows = derived(() => records().map(entry => ({
|
|
44
|
+
...entry,
|
|
45
|
+
contact: entry.email || entry.phone || 'No contact recorded',
|
|
46
|
+
productName: productLabel(entry.productId),
|
|
47
|
+
})))
|
|
48
|
+
|
|
49
|
+
function handleRowAction(payload: { action: string, row: ProductWaitlistRecord }): void {
|
|
50
|
+
emit(payload.action, payload.row)
|
|
31
51
|
}
|
|
32
52
|
</script>
|
|
33
53
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
<th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-left text-xs uppercase dark:text-neutral-400">Status</th>
|
|
51
|
-
<th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-left text-xs uppercase dark:text-neutral-400">Preference</th>
|
|
52
|
-
<th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-left text-xs uppercase dark:text-neutral-400">Created</th>
|
|
53
|
-
<th scope="col" class="relative px-4 py-3"><span class="sr-only">Actions</span></th>
|
|
54
|
-
</tr>
|
|
55
|
-
</thead>
|
|
56
|
-
<tbody class="divide-gray-100 divide-y dark:divide-neutral-700">
|
|
57
|
-
<template :for="entry in records()" :key="entry.id">
|
|
58
|
-
<tr class="hover:bg-gray-50/60 dark:hover:bg-neutral-700/50">
|
|
59
|
-
<td class="px-4 py-3">
|
|
60
|
-
<p class="font-medium text-gray-900 text-sm dark:text-white">{{ entry.name || 'Unnamed' }}</p>
|
|
61
|
-
<p class="mt-1 text-gray-500 text-xs dark:text-neutral-400">{{ entry.email || entry.phone || 'No contact recorded' }}</p>
|
|
62
|
-
</td>
|
|
63
|
-
<td class="px-4 py-3 text-gray-700 text-sm dark:text-neutral-200">{{ productLabel(entry.productId) }}</td>
|
|
64
|
-
<td class="px-4 py-3 font-mono text-gray-700 text-right text-sm dark:text-neutral-200">{{ entry.quantity }}</td>
|
|
65
|
-
<td class="px-4 py-3"><span :class="'inline-flex px-2 py-1 font-medium text-xs capitalize rounded-full ' + statusClass(entry.status)">{{ entry.status || 'unknown' }}</span></td>
|
|
66
|
-
<td class="px-4 py-3 capitalize text-gray-600 text-sm dark:text-neutral-300">{{ entry.notificationPreference || 'Not recorded' }}</td>
|
|
67
|
-
<td class="px-4 py-3 text-gray-600 text-sm whitespace-nowrap dark:text-neutral-300">{{ formatDate(entry.createdAt) }}</td>
|
|
68
|
-
<td class="px-4 py-3 text-right whitespace-nowrap">
|
|
69
|
-
<Button variant="ghost" size="sm" iconOnly :ariaLabel="'Edit waitlist entry for ' + (entry.name || 'customer')" @click="emit('edit', entry)">
|
|
70
|
-
<span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-01"></span>
|
|
71
|
-
</Button>
|
|
72
|
-
<Button variant="ghost" size="sm" iconOnly :ariaLabel="'Delete waitlist entry for ' + (entry.name || 'customer')" @click="emit('delete', entry)">
|
|
73
|
-
<span class="block h-4 w-4 text-red-500 i-hugeicons-waste"></span>
|
|
74
|
-
</Button>
|
|
75
|
-
</td>
|
|
76
|
-
</tr>
|
|
77
|
-
</template>
|
|
78
|
-
</tbody>
|
|
79
|
-
</table>
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
54
|
+
{{--
|
|
55
|
+
No `searchable` or `filterable`: the parent dashboard owns the search box and
|
|
56
|
+
the filter selects. Paging is what was missing.
|
|
57
|
+
--}}
|
|
58
|
+
<Table
|
|
59
|
+
:columns="COLUMNS"
|
|
60
|
+
:data="rows()"
|
|
61
|
+
:loading="loading()"
|
|
62
|
+
:row-actions="ROW_ACTIONS"
|
|
63
|
+
action-label-key="name"
|
|
64
|
+
paginated
|
|
65
|
+
:page-size="25"
|
|
66
|
+
empty-title="No product waitlist records found"
|
|
67
|
+
empty-description="Add an entry or adjust the current filters."
|
|
68
|
+
@row-action="handleRowAction"
|
|
69
|
+
/>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import {
|
|
3
5
|
restaurantWaitlistTimestamp,
|
|
@@ -9,9 +11,38 @@ const loading = useReactiveProp('loading', false)
|
|
|
9
11
|
const busyId = useReactiveProp('busyId', '')
|
|
10
12
|
const emit = defineEmits()
|
|
11
13
|
|
|
14
|
+
const STATUS_CLASSES = {
|
|
15
|
+
seated: 'text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/30',
|
|
16
|
+
waiting: 'text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/30',
|
|
17
|
+
no_show: 'text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-950/30',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const COLUMNS = [
|
|
21
|
+
{ key: 'name', label: 'Party', sortable: true, emptyText: 'Unnamed party', subtitleKey: 'contact', class: 'font-medium text-gray-900 dark:text-white' },
|
|
22
|
+
{ key: 'partySize', label: 'Size', type: 'number' as const, sortable: true, align: 'right' as const, class: 'font-mono' },
|
|
23
|
+
// `no_show` renders as "No show" from the badge's own humanising, so no
|
|
24
|
+
// label map is needed for it.
|
|
25
|
+
{ key: 'status', label: 'Status', type: 'badge' as const, sortable: true, badgeClasses: STATUS_CLASSES },
|
|
26
|
+
{ key: 'quotedWait', label: 'Quoted wait', sortable: true, align: 'right' as const, class: 'font-mono' },
|
|
27
|
+
{ key: 'preference', label: 'Preference', sortable: true, class: 'capitalize' },
|
|
28
|
+
{ key: 'checkIn', label: 'Check in', sortable: true, emptyText: 'Not recorded', class: 'whitespace-nowrap' },
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Seating and marking a no-show only apply while the party is still waiting,
|
|
33
|
+
* which is what `when` expresses. All four are disabled on the row whose write
|
|
34
|
+
* is in flight, via `busy-key`/`busy-value`.
|
|
35
|
+
*/
|
|
36
|
+
const ROW_ACTIONS = [
|
|
37
|
+
{ action: 'seat', label: 'Seat party for', icon: 'i-hugeicons-checkmark-circle-02', class: 'text-green-600 dark:text-green-300', when: { key: 'status', equals: 'waiting' } },
|
|
38
|
+
{ action: 'no-show', label: 'Mark no show for', icon: 'i-hugeicons-user-remove-01', class: 'text-red-600 dark:text-red-300', when: { key: 'status', equals: 'waiting' } },
|
|
39
|
+
{ action: 'edit', label: 'Edit restaurant waitlist entry for', icon: 'i-hugeicons-edit-01' },
|
|
40
|
+
{ action: 'delete', label: 'Delete restaurant waitlist entry for', icon: 'i-hugeicons-waste', class: 'text-red-600 dark:text-red-300' },
|
|
41
|
+
]
|
|
42
|
+
|
|
12
43
|
function formatDateTime(value: string): string {
|
|
13
44
|
if (!value)
|
|
14
|
-
return '
|
|
45
|
+
return ''
|
|
15
46
|
const timestamp = restaurantWaitlistTimestamp(value)
|
|
16
47
|
if (!timestamp)
|
|
17
48
|
return value
|
|
@@ -22,79 +53,43 @@ function formatDateTime(value: string): string {
|
|
|
22
53
|
}
|
|
23
54
|
|
|
24
55
|
function preferenceLabel(value: string): string {
|
|
25
|
-
return value === 'no_preference'
|
|
26
|
-
? 'No preference'
|
|
27
|
-
: value.replaceAll('_', ' ')
|
|
56
|
+
return value === 'no_preference' ? 'No preference' : value.replaceAll('_', ' ')
|
|
28
57
|
}
|
|
29
58
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Computed per row because a column reads a field. `checkIn` keeps this
|
|
61
|
+
* component's own parser - a restaurant check-in is stored as a unix
|
|
62
|
+
* timestamp, which `UI/Table`'s `datetime` type does not read - and the
|
|
63
|
+
* quoted wait carries its unit.
|
|
64
|
+
*/
|
|
65
|
+
const rows = derived(() => records().map(entry => ({
|
|
66
|
+
...entry,
|
|
67
|
+
contact: entry.phone || entry.email || 'No contact recorded',
|
|
68
|
+
quotedWait: `${entry.quotedWaitTime} min`,
|
|
69
|
+
preference: preferenceLabel(entry.tablePreference),
|
|
70
|
+
checkIn: formatDateTime(entry.checkInTime),
|
|
71
|
+
})))
|
|
33
72
|
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
return 'text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/30'
|
|
37
|
-
if (value === 'waiting')
|
|
38
|
-
return 'text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/30'
|
|
39
|
-
if (value === 'no_show')
|
|
40
|
-
return 'text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-950/30'
|
|
41
|
-
return 'text-gray-700 dark:text-neutral-300 bg-gray-100 dark:bg-neutral-700'
|
|
73
|
+
function handleRowAction(payload: { action: string, row: RestaurantWaitlistRecord }): void {
|
|
74
|
+
emit(payload.action, payload.row)
|
|
42
75
|
}
|
|
43
76
|
</script>
|
|
44
77
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-left text-xs uppercase dark:text-neutral-400">Check in</th>
|
|
64
|
-
<th scope="col" class="relative px-4 py-3"><span class="sr-only">Actions</span></th>
|
|
65
|
-
</tr>
|
|
66
|
-
</thead>
|
|
67
|
-
<tbody class="divide-gray-100 divide-y dark:divide-neutral-700">
|
|
68
|
-
<template :for="entry in records()" :key="entry.id">
|
|
69
|
-
<tr class="hover:bg-gray-50/60 dark:hover:bg-neutral-700/50">
|
|
70
|
-
<td class="px-4 py-3">
|
|
71
|
-
<p class="font-medium text-gray-900 text-sm dark:text-white">{{ entry.name || 'Unnamed party' }}</p>
|
|
72
|
-
<p class="mt-1 text-gray-500 text-xs dark:text-neutral-400">{{ entry.phone || entry.email || 'No contact recorded' }}</p>
|
|
73
|
-
</td>
|
|
74
|
-
<td class="px-4 py-3 font-mono text-gray-700 text-right text-sm dark:text-neutral-200">{{ entry.partySize }}</td>
|
|
75
|
-
<td class="px-4 py-3">
|
|
76
|
-
<span :class="'inline-flex px-2 py-1 font-medium text-xs capitalize rounded-full ' + statusClass(entry.status)">{{ statusLabel(entry.status) }}</span>
|
|
77
|
-
</td>
|
|
78
|
-
<td class="px-4 py-3 font-mono text-gray-700 text-right text-sm dark:text-neutral-200">{{ entry.quotedWaitTime }} min</td>
|
|
79
|
-
<td class="px-4 py-3 capitalize text-gray-600 text-sm dark:text-neutral-300">{{ preferenceLabel(entry.tablePreference) }}</td>
|
|
80
|
-
<td class="px-4 py-3 text-gray-600 text-sm whitespace-nowrap dark:text-neutral-300">{{ formatDateTime(entry.checkInTime) }}</td>
|
|
81
|
-
<td class="px-4 py-3 text-right whitespace-nowrap">
|
|
82
|
-
<Button :if="entry.status === 'waiting'" variant="ghost" size="md" iconOnly :disabled="busyId() === entry.id" :ariaLabel="'Seat party for ' + (entry.name || 'customer')" @click="emit('seat', entry)">
|
|
83
|
-
<span class="block h-5 w-5 text-green-600 dark:text-green-300 i-hugeicons-checkmark-circle-02"></span>
|
|
84
|
-
</Button>
|
|
85
|
-
<Button :if="entry.status === 'waiting'" variant="ghost" size="md" iconOnly :disabled="busyId() === entry.id" :ariaLabel="'Mark no show for ' + (entry.name || 'customer')" @click="emit('no-show', entry)">
|
|
86
|
-
<span class="block h-5 w-5 text-red-600 dark:text-red-300 i-hugeicons-user-remove-01"></span>
|
|
87
|
-
</Button>
|
|
88
|
-
<Button variant="ghost" size="md" iconOnly :disabled="busyId() === entry.id" :ariaLabel="'Edit restaurant waitlist entry for ' + (entry.name || 'customer')" @click="emit('edit', entry)">
|
|
89
|
-
<span class="block h-5 w-5 text-blue-600 dark:text-blue-300 i-hugeicons-edit-01"></span>
|
|
90
|
-
</Button>
|
|
91
|
-
<Button variant="ghost" size="md" iconOnly :disabled="busyId() === entry.id" :ariaLabel="'Delete restaurant waitlist entry for ' + (entry.name || 'customer')" @click="emit('delete', entry)">
|
|
92
|
-
<span class="block h-5 w-5 text-red-600 dark:text-red-300 i-hugeicons-waste"></span>
|
|
93
|
-
</Button>
|
|
94
|
-
</td>
|
|
95
|
-
</tr>
|
|
96
|
-
</template>
|
|
97
|
-
</tbody>
|
|
98
|
-
</table>
|
|
99
|
-
</div>
|
|
100
|
-
</div>
|
|
78
|
+
{{--
|
|
79
|
+
No `searchable` or `filterable`: the parent dashboard owns the search box and
|
|
80
|
+
the filter selects. Paging is what was missing.
|
|
81
|
+
--}}
|
|
82
|
+
<Table
|
|
83
|
+
:columns="COLUMNS"
|
|
84
|
+
:data="rows()"
|
|
85
|
+
:loading="loading()"
|
|
86
|
+
:row-actions="ROW_ACTIONS"
|
|
87
|
+
action-label-key="name"
|
|
88
|
+
busy-key="id"
|
|
89
|
+
:busy-value="busyId()"
|
|
90
|
+
paginated
|
|
91
|
+
:page-size="25"
|
|
92
|
+
empty-title="No restaurant waitlist records found"
|
|
93
|
+
empty-description="Add a party or adjust the current filters."
|
|
94
|
+
@row-action="handleRowAction"
|
|
95
|
+
/>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
@import('../UI/Button')
|
|
2
|
+
@import('../UI/Table')
|
|
2
3
|
|
|
3
4
|
<script client>
|
|
4
5
|
import { dashboardApi } from '../../../../functions/dashboard-api'
|
|
@@ -11,13 +12,31 @@ interface DependencyRow {
|
|
|
11
12
|
isScoped?: boolean
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
type DependencyKind = 'workspace' | 'transitive' | 'third-party'
|
|
16
|
+
|
|
17
|
+
const COLUMNS = [
|
|
18
|
+
{ key: 'name', label: 'Package', sortable: true, class: 'font-medium text-gray-900 dark:text-white' },
|
|
19
|
+
{ key: 'version', label: 'Version', sortable: true, class: 'font-mono text-gray-500 dark:text-neutral-400' },
|
|
20
|
+
{
|
|
21
|
+
key: 'kind',
|
|
22
|
+
label: 'Source',
|
|
23
|
+
type: 'badge' as const,
|
|
24
|
+
sortable: true,
|
|
25
|
+
badgeClasses: {
|
|
26
|
+
'workspace': 'text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40',
|
|
27
|
+
'transitive': 'text-gray-600 dark:text-neutral-300 bg-gray-100 dark:bg-neutral-700',
|
|
28
|
+
'third-party': 'text-gray-600 dark:text-neutral-300 bg-gray-100 dark:bg-neutral-700',
|
|
29
|
+
},
|
|
30
|
+
badgeLabels: { 'workspace': 'Workspace', 'transitive': 'Transitive', 'third-party': 'Third-party' },
|
|
31
|
+
},
|
|
32
|
+
]
|
|
33
|
+
|
|
14
34
|
const declared = state<DependencyRow[]>([])
|
|
15
35
|
const transitive = state<DependencyRow[]>([])
|
|
16
36
|
const available = state(true)
|
|
17
37
|
const source = state('bun.lock')
|
|
18
38
|
const loading = state(true)
|
|
19
39
|
const loadError = state('')
|
|
20
|
-
const search = state('')
|
|
21
40
|
const dataset = state('declared')
|
|
22
41
|
const scope = state('all')
|
|
23
42
|
|
|
@@ -25,12 +44,22 @@ function activeRows(): DependencyRow[] {
|
|
|
25
44
|
return dataset() === 'transitive' ? transitive() : declared()
|
|
26
45
|
}
|
|
27
46
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
47
|
+
/**
|
|
48
|
+
* `UI/Table` owns search, sort and paging, so this only applies the two
|
|
49
|
+
* filters that are specific to dependencies: which dataset is being read and,
|
|
50
|
+
* for the declared set, whether workspace packages are in scope.
|
|
51
|
+
*
|
|
52
|
+
* `kind` is computed here rather than in the cell because a badge column maps
|
|
53
|
+
* a row value to a label, and the right label for a non-workspace package
|
|
54
|
+
* depends on the dataset rather than the row.
|
|
55
|
+
*/
|
|
56
|
+
function visibleRows(): Array<DependencyRow & { kind: DependencyKind }> {
|
|
57
|
+
return activeRows()
|
|
58
|
+
.filter(dependency => dataset() === 'transitive' || scope() === 'all' || (scope() === 'workspace' ? dependency.isWorkspace : !dependency.isWorkspace))
|
|
59
|
+
.map(dependency => ({
|
|
60
|
+
...dependency,
|
|
61
|
+
kind: dependency.isWorkspace ? 'workspace' : dataset() === 'transitive' ? 'transitive' : 'third-party',
|
|
62
|
+
}))
|
|
34
63
|
}
|
|
35
64
|
|
|
36
65
|
function workspaceCount(): number {
|
|
@@ -81,12 +110,37 @@ onMount(() => { void loadDependencies() })
|
|
|
81
110
|
<article class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700"><p class="text-gray-500 text-xs dark:text-neutral-400">Transitive</p><p class="mt-2 font-semibold text-2xl text-gray-900 dark:text-white">{{ transitive().length }}</p></article>
|
|
82
111
|
</section>
|
|
83
112
|
|
|
84
|
-
<section
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
113
|
+
<section aria-label="Dependencies">
|
|
114
|
+
<Table
|
|
115
|
+
:columns="COLUMNS"
|
|
116
|
+
:data="visibleRows()"
|
|
117
|
+
:loading="loading()"
|
|
118
|
+
row-key="name"
|
|
119
|
+
searchable
|
|
120
|
+
search-placeholder="Search package or version"
|
|
121
|
+
paginated
|
|
122
|
+
:page-size="50"
|
|
123
|
+
striped
|
|
124
|
+
empty-title="No dependencies found"
|
|
125
|
+
empty-description="Adjust the dataset, scope, or search."
|
|
126
|
+
>
|
|
127
|
+
<template #toolbar>
|
|
128
|
+
<label>
|
|
129
|
+
<span class="sr-only">Choose dependency dataset</span>
|
|
130
|
+
<select x-model="dataset" class="px-2.5 py-1.5 text-sm bg-white dark:bg-neutral-800 border border-stone-300 rounded-lg dark:border-neutral-600">
|
|
131
|
+
<option value="declared">Declared</option>
|
|
132
|
+
<option value="transitive">Transitive</option>
|
|
133
|
+
</select>
|
|
134
|
+
</label>
|
|
135
|
+
<label>
|
|
136
|
+
<span class="sr-only">Filter dependency scope</span>
|
|
137
|
+
<select x-model="scope" :disabled="dataset() === 'transitive'" class="px-2.5 py-1.5 text-sm bg-white dark:bg-neutral-800 border border-stone-300 rounded-lg dark:border-neutral-600 disabled:opacity-50">
|
|
138
|
+
<option value="all">All scopes</option>
|
|
139
|
+
<option value="workspace">Workspace</option>
|
|
140
|
+
<option value="third-party">Third-party</option>
|
|
141
|
+
</select>
|
|
142
|
+
</label>
|
|
143
|
+
</template>
|
|
144
|
+
</Table>
|
|
91
145
|
</section>
|
|
92
146
|
</div>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import type { RealtimeDashboardResponse, RealtimeEvent } from '../../../../functions/realtime'
|
|
3
5
|
import { fetchRealtimeDashboard } from '../../../../functions/realtime'
|
|
@@ -31,6 +33,18 @@ function formatNumber(value: number): string {
|
|
|
31
33
|
return new Intl.NumberFormat().format(value)
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
/**
|
|
37
|
+
* The recorded-events table. `occurredAt` is a `datetime` rather than a
|
|
38
|
+
* `date`: an event log is read to the minute, and this column is also the one
|
|
39
|
+
* it sorts by.
|
|
40
|
+
*/
|
|
41
|
+
const EVENT_COLUMNS = [
|
|
42
|
+
{ key: 'occurredAt', label: 'Time', type: 'datetime' as const, sortable: true, class: 'whitespace-nowrap' },
|
|
43
|
+
{ key: 'type', label: 'Type', sortable: true, filterable: true, class: 'font-medium text-gray-900 dark:text-white' },
|
|
44
|
+
{ key: 'socket', label: 'Socket', sortable: true, class: 'font-mono text-xs' },
|
|
45
|
+
{ key: 'details', label: 'Details', emptyText: 'No details' },
|
|
46
|
+
]
|
|
47
|
+
|
|
34
48
|
function formatDate(value: string): string {
|
|
35
49
|
if (!value)
|
|
36
50
|
return 'Not recorded'
|
|
@@ -208,31 +222,16 @@ onDestroy(() => {
|
|
|
208
222
|
<h2 class="font-semibold text-gray-900 dark:text-white">Recorded Websocket events</h2>
|
|
209
223
|
<p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Persisted through the Websocket model. No latency or throughput is estimated.</p>
|
|
210
224
|
</div>
|
|
211
|
-
<
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
<th class="px-4 py-3 font-medium text-gray-500 text-left text-xs uppercase dark:text-neutral-300">Details</th>
|
|
223
|
-
</tr>
|
|
224
|
-
</thead>
|
|
225
|
-
<tbody class="divide-gray-200 divide-y dark:divide-neutral-700">
|
|
226
|
-
<template :for="event in recordedEvents()">
|
|
227
|
-
<tr>
|
|
228
|
-
<td class="px-4 py-3 text-gray-500 text-sm whitespace-nowrap dark:text-neutral-300">{{ formatDate(event.occurredAt) }}</td>
|
|
229
|
-
<td class="px-4 py-3 font-medium text-gray-900 text-sm dark:text-white">{{ event.type }}</td>
|
|
230
|
-
<td class="px-4 py-3 font-mono text-gray-500 text-xs dark:text-neutral-300">{{ event.socket }}</td>
|
|
231
|
-
<td class="px-4 py-3 text-gray-500 text-sm dark:text-neutral-300">{{ event.details }}</td>
|
|
232
|
-
</tr>
|
|
233
|
-
</template>
|
|
234
|
-
</tbody>
|
|
235
|
-
</table>
|
|
236
|
-
</div>
|
|
225
|
+
<Table
|
|
226
|
+
:columns="EVENT_COLUMNS"
|
|
227
|
+
:data="recordedEvents()"
|
|
228
|
+
:loading="isLoading()"
|
|
229
|
+
searchable
|
|
230
|
+
search-placeholder="Search recorded events"
|
|
231
|
+
paginated
|
|
232
|
+
:page-size="25"
|
|
233
|
+
empty-title="No realtime events recorded"
|
|
234
|
+
empty-description="Connection lifecycle events appear after they are persisted through the Websocket model."
|
|
235
|
+
/>
|
|
237
236
|
</section>
|
|
238
237
|
</div>
|