@morscherlab/mint-sdk 1.4.0-beta.16 → 1.4.0-beta.18
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/README.md +14 -3
- package/dist/{AlertBox-SnsbVow-.js → AlertBox-CzLg0UaL.js} +2 -2
- package/dist/{AlertBox-SnsbVow-.js.map → AlertBox-CzLg0UaL.js.map} +1 -1
- package/dist/{AppSidebar-CB0-RQ4m.js → AppSidebar-6oTT-gPO.js} +43 -13
- package/dist/AppSidebar-6oTT-gPO.js.map +1 -0
- package/dist/{ExperimentPopover-ChKCrklH.js → ExperimentPopover-9l4QZlvQ.js} +2 -2
- package/dist/{ExperimentPopover-ChKCrklH.js.map → ExperimentPopover-9l4QZlvQ.js.map} +1 -1
- package/dist/{ExperimentSelectorModal-BqifjNaN.js → ExperimentSelectorModal-pR5fK3pA.js} +3 -3
- package/dist/{ExperimentSelectorModal-BqifjNaN.js.map → ExperimentSelectorModal-pR5fK3pA.js.map} +1 -1
- package/dist/{SegmentedControl-hOgBVdju.js → SegmentedControl-CJ7EAmOq.js} +2 -2
- package/dist/{SegmentedControl-hOgBVdju.js.map → SegmentedControl-CJ7EAmOq.js.map} +1 -1
- package/dist/{SettingsModal-CoZK3EDN.js → SettingsModal-Dyitgzfp.js} +6 -6
- package/dist/{SettingsModal-CoZK3EDN.js.map → SettingsModal-Dyitgzfp.js.map} +1 -1
- package/dist/components/AppTopBar.vue.d.ts +7 -0
- package/dist/components/JobsStatusTray.vue.d.ts +11 -4
- package/dist/components/PluginWorkspaceView.props.d.ts +3 -0
- package/dist/components/PluginWorkspaceView.vue.d.ts +3 -0
- package/dist/components/internal/WorkspaceShellInternal.vue.d.ts +8 -0
- package/dist/composables/useJobCenter.d.ts +11 -3
- package/dist/{controlSchemaModel-CIH5Hqpw.js → controlSchemaModel-DCUOx8ll.js} +2 -2
- package/dist/{controlSchemaModel-CIH5Hqpw.js.map → controlSchemaModel-DCUOx8ll.js.map} +1 -1
- package/dist/{data-OZ94DhVC.js → data-BIiV4kKw.js} +9 -9
- package/dist/{data-OZ94DhVC.js.map → data-BIiV4kKw.js.map} +1 -1
- package/dist/data.js +7 -7
- package/dist/index.js +16 -16
- package/dist/{install-BKgb5qZw.js → install-Dg-Z6ze5.js} +13 -13
- package/dist/{install-BKgb5qZw.js.map → install-Dg-Z6ze5.js.map} +1 -1
- package/dist/install.js +1 -1
- package/dist/{lab-ZDKlx1Oo.js → lab-BHVT1qN2.js} +9 -9
- package/dist/{lab-ZDKlx1Oo.js.map → lab-BHVT1qN2.js.map} +1 -1
- package/dist/lab.js +4 -4
- package/dist/{records-B4yl2oIu.js → records-CR_pYzIP.js} +62 -62
- package/dist/{records-B4yl2oIu.js.map → records-CR_pYzIP.js.map} +1 -1
- package/dist/{runtime-Cz1GJa_O.js → runtime-cHv1phcc.js} +379 -31
- package/dist/runtime-cHv1phcc.js.map +1 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +6 -6
- package/dist/{shell-kq9p4VsX.js → shell-BDuZf2zb.js} +4 -4
- package/dist/{shell-kq9p4VsX.js.map → shell-BDuZf2zb.js.map} +1 -1
- package/dist/shell.js +4 -4
- package/dist/styles.css +163 -307
- package/dist/types/components.d.ts +2 -0
- package/dist/{useExperimentSamples-BLx29CkM.js → useExperimentSamples-Dno1fFMq.js} +3 -3
- package/dist/{useExperimentSamples-BLx29CkM.js.map → useExperimentSamples-Dno1fFMq.js.map} +1 -1
- package/dist/{useListReorder-C_8RYeVP.js → useListReorder-CQ9OCScc.js} +76 -76
- package/dist/useListReorder-CQ9OCScc.js.map +1 -0
- package/dist/{useRackEditor-BhYlXKFM.js → useRackEditor-wL3kPrRT.js} +6 -6
- package/dist/{useRackEditor-BhYlXKFM.js.map → useRackEditor-wL3kPrRT.js.map} +1 -1
- package/dist/useRealtime-BW02xy4Z.js +398 -0
- package/dist/useRealtime-BW02xy4Z.js.map +1 -0
- package/dist/{useRequest-C9uwM79q.js → useRequest-CDkrXNU7.js} +2 -2
- package/dist/{useRequest-C9uwM79q.js.map → useRequest-CDkrXNU7.js.map} +1 -1
- package/package.json +1 -1
- package/src/__tests__/components/AppTopBar.test.ts +33 -2
- package/src/__tests__/components/GeneratedPluginView.test.ts +1 -1
- package/src/__tests__/components/JobsStatusTray.test.ts +137 -71
- package/src/__tests__/components/PluginWorkspaceView.test.ts +56 -1
- package/src/__tests__/composables/useJobCenter.test.ts +223 -3
- package/src/__tests__/docs/frontendDocsCatalog.test.ts +3 -3
- package/src/__tests__/styles/v2AuditRules.test.ts +8 -0
- package/src/components/AppTopBar.vue +14 -0
- package/src/components/GeneratedPluginView.vue +4 -9
- package/src/components/JobsStatusTray.story.vue +78 -43
- package/src/components/JobsStatusTray.vue +171 -279
- package/src/components/PluginWorkspaceView.props.ts +3 -0
- package/src/components/PluginWorkspaceView.vue +17 -0
- package/src/components/internal/WorkspaceShellInternal.vue +12 -0
- package/src/composables/useJobCenter.ts +127 -6
- package/src/runtime.ts +0 -1
- package/src/styles/components/app-top-bar.css +7 -0
- package/src/styles/components/jobs-status-tray.css +129 -296
- package/src/styles/variables.css +16 -0
- package/src/types/components.ts +2 -0
- package/dist/AppSidebar-CB0-RQ4m.js.map +0 -1
- package/dist/runtime-Cz1GJa_O.js.map +0 -1
- package/dist/useListReorder-C_8RYeVP.js.map +0 -1
- package/dist/useRealtime-CUMbW_ln.js +0 -699
- package/dist/useRealtime-CUMbW_ln.js.map +0 -1
|
@@ -1,34 +1,45 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Compact, non-modal job panel under the top bar, driven by a job center the parent owns.
|
|
4
|
+
*
|
|
5
|
+
* The parent passes the center's `topBarJob`, `completedCount` and `handleTopBarClick`
|
|
6
|
+
* to `AppTopBar`; its run slot opens this panel. `PluginWorkspaceView` does this for you
|
|
7
|
+
* when its spec sets `jobs`.
|
|
4
8
|
*
|
|
5
9
|
* @example Recommended
|
|
6
10
|
* ```vue
|
|
7
|
-
* <
|
|
11
|
+
* <AppTopBar :job="center.topBarJob.value" :jobs-done="center.completedCount.value"
|
|
12
|
+
* @job-click="center.handleTopBarClick(openResult)" />
|
|
13
|
+
* <JobsStatusTray :center="center" @select="openResult" />
|
|
8
14
|
* ```
|
|
9
15
|
*/
|
|
10
|
-
import { computed, nextTick, ref,
|
|
11
|
-
|
|
16
|
+
import { computed, nextTick, onBeforeUnmount, ref, watch, type Component } from 'vue'
|
|
12
17
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
CircleAlert,
|
|
19
|
+
CircleCheck,
|
|
20
|
+
CircleSlash,
|
|
21
|
+
CircleX,
|
|
22
|
+
Clock,
|
|
23
|
+
LoaderCircle,
|
|
24
|
+
RefreshCw,
|
|
25
|
+
Square,
|
|
26
|
+
Trash2,
|
|
27
|
+
X,
|
|
28
|
+
} from 'lucide-vue-next'
|
|
29
|
+
|
|
30
|
+
import { isActiveJobStatus, jobStatusLabel, normalizeJobPercent } from '../jobs'
|
|
31
|
+
import type { UseJobCenterReturn } from '../composables/useJobCenter'
|
|
32
|
+
import type { JobState, JobStatus } from '../types/jobs'
|
|
21
33
|
|
|
22
34
|
interface Props {
|
|
23
|
-
|
|
24
|
-
|
|
35
|
+
/** Job center from `useJobCenter()`; the parent owns it and opens the panel through it. */
|
|
36
|
+
center: UseJobCenterReturn
|
|
25
37
|
title?: string
|
|
26
38
|
teleportTo?: string | HTMLElement | false
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
const props = withDefaults(defineProps<Props>(), {
|
|
30
|
-
|
|
31
|
-
title: 'Analysis jobs',
|
|
42
|
+
title: 'Jobs',
|
|
32
43
|
teleportTo: 'body',
|
|
33
44
|
})
|
|
34
45
|
|
|
@@ -40,84 +51,78 @@ const emit = defineEmits<{
|
|
|
40
51
|
defineSlots<{
|
|
41
52
|
/** Extra per-row actions, rendered before the built-in Cancel / Delete. */
|
|
42
53
|
'job-actions'?: (props: { job: JobState }) => unknown
|
|
54
|
+
/** Row title. Default: the job name. */
|
|
55
|
+
'job-title'?: (props: { job: JobState }) => unknown
|
|
56
|
+
/** Detail line under the title. Default: the error, progress message or status. */
|
|
57
|
+
'job-detail'?: (props: { job: JobState }) => unknown
|
|
43
58
|
}>()
|
|
44
59
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
60
|
+
const STATUS_ICONS: Record<JobStatus, Component> = {
|
|
61
|
+
running: LoaderCircle,
|
|
62
|
+
awaiting_input: CircleAlert,
|
|
63
|
+
queued: Clock,
|
|
64
|
+
completed: CircleCheck,
|
|
65
|
+
failed: CircleX,
|
|
66
|
+
cancelled: CircleSlash,
|
|
67
|
+
}
|
|
68
|
+
|
|
48
69
|
const panelRef = ref<HTMLElement | null>(null)
|
|
49
70
|
const teleportTarget = computed(() => props.teleportTo || 'body')
|
|
50
|
-
const tray =
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
})
|
|
54
|
-
// 2f: a job waiting for input is the only one that needs a person — pin it first.
|
|
71
|
+
const tray = props.center
|
|
72
|
+
const titleId = `${tray.panelId}-title`
|
|
73
|
+
// A job waiting for input is the only one that needs a person: pin it first.
|
|
55
74
|
const activeRows = computed(() => [
|
|
56
75
|
...tray.activeJobs.value.filter(job => job.status === 'awaiting_input'),
|
|
57
76
|
...tray.activeJobs.value.filter(job => job.status !== 'awaiting_input'),
|
|
58
77
|
])
|
|
59
|
-
const
|
|
60
|
-
|
|
78
|
+
const sections = computed(() => [
|
|
79
|
+
{ key: 'active', title: 'Active', jobs: activeRows.value },
|
|
80
|
+
{ key: 'finished', title: 'Finished', jobs: tray.historyJobs.value },
|
|
81
|
+
].filter(section => section.jobs.length > 0))
|
|
82
|
+
const headCount = computed(() => {
|
|
61
83
|
const parts = [
|
|
62
|
-
[
|
|
63
|
-
[
|
|
64
|
-
[
|
|
84
|
+
[tray.activeJobs.value.length, 'active'],
|
|
85
|
+
[tray.completedCount.value, 'done'],
|
|
86
|
+
[tray.historyJobs.value.filter(job => job.status === 'failed').length, 'failed'],
|
|
65
87
|
].filter(([n]) => n).map(([n, word]) => `${n} ${word}`)
|
|
66
|
-
return parts.length ? parts.join(' · ') :
|
|
88
|
+
return parts.length ? parts.join(' · ') : 'No jobs'
|
|
67
89
|
})
|
|
68
90
|
const canClear = computed(() => (
|
|
69
91
|
tray.deletableJobs.value.length > 0
|
|
70
92
|
&& tray.canClearFinished.value
|
|
71
93
|
))
|
|
72
94
|
|
|
73
|
-
|
|
74
|
-
|
|
95
|
+
// Escape returns focus to the element that opened the panel (the top-bar chip).
|
|
96
|
+
// The panel is non-modal, so Escape is heard on the document while it is open.
|
|
97
|
+
let opener: HTMLElement | null = null
|
|
98
|
+
function onDocumentKeydown(event: KeyboardEvent): void {
|
|
99
|
+
if (event.key === 'Escape') closeTray()
|
|
100
|
+
}
|
|
101
|
+
watch(tray.isOpen, async (open) => {
|
|
102
|
+
if (!open) {
|
|
103
|
+
document.removeEventListener('keydown', onDocumentKeydown)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
opener = document.activeElement instanceof HTMLElement ? document.activeElement : null
|
|
107
|
+
document.addEventListener('keydown', onDocumentKeydown)
|
|
75
108
|
await nextTick()
|
|
76
109
|
panelRef.value?.focus()
|
|
77
|
-
}
|
|
110
|
+
}, { flush: 'sync' })
|
|
111
|
+
onBeforeUnmount(() => document.removeEventListener('keydown', onDocumentKeydown))
|
|
78
112
|
|
|
79
|
-
|
|
113
|
+
function closeTray(): void {
|
|
80
114
|
tray.close()
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function onPanelKeydown(event: KeyboardEvent): void {
|
|
91
|
-
if (event.key === 'Escape') {
|
|
92
|
-
void closeTray()
|
|
93
|
-
return
|
|
94
|
-
}
|
|
95
|
-
if (event.key !== 'Tab' || !panelRef.value) return
|
|
96
|
-
const focusable = Array.from(panelRef.value.querySelectorAll<HTMLElement>(
|
|
97
|
-
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
|
|
98
|
-
))
|
|
99
|
-
if (focusable.length === 0) {
|
|
100
|
-
event.preventDefault()
|
|
101
|
-
panelRef.value.focus()
|
|
102
|
-
return
|
|
103
|
-
}
|
|
104
|
-
const first = focusable[0]
|
|
105
|
-
const last = focusable[focusable.length - 1]
|
|
106
|
-
const activeElement = document.activeElement as HTMLElement | null
|
|
107
|
-
if (!activeElement || !focusable.includes(activeElement)) {
|
|
108
|
-
event.preventDefault()
|
|
109
|
-
if (event.shiftKey) last?.focus()
|
|
110
|
-
else first?.focus()
|
|
111
|
-
} else if (event.shiftKey && activeElement === first) {
|
|
112
|
-
event.preventDefault()
|
|
113
|
-
last?.focus()
|
|
114
|
-
} else if (!event.shiftKey && activeElement === last) {
|
|
115
|
-
event.preventDefault()
|
|
116
|
-
first?.focus()
|
|
117
|
-
}
|
|
115
|
+
// Safari leaves a clicked button unfocused (opener = body), and the chip node is
|
|
116
|
+
// replaced when it switches between job chip and idle button: fall back to the
|
|
117
|
+
// top-bar jobs control.
|
|
118
|
+
const target = opener?.isConnected && opener !== document.body
|
|
119
|
+
? opener
|
|
120
|
+
: document.querySelector<HTMLElement>('.mint-topbar__job')
|
|
121
|
+
target?.focus()
|
|
118
122
|
}
|
|
119
123
|
|
|
120
124
|
function progressText(job: JobState): string {
|
|
125
|
+
if (job.error) return job.error
|
|
121
126
|
if (job.status === 'awaiting_input') {
|
|
122
127
|
return job.progress.message || 'Waiting for external input'
|
|
123
128
|
}
|
|
@@ -127,22 +132,24 @@ function progressText(job: JobState): string {
|
|
|
127
132
|
return job.progress.message || job.progress.stage || jobStatusLabel(job.status)
|
|
128
133
|
}
|
|
129
134
|
|
|
130
|
-
function
|
|
135
|
+
function formatTime(value: string | null): string {
|
|
131
136
|
if (!value) return ''
|
|
132
137
|
const date = new Date(value)
|
|
133
138
|
return Number.isNaN(date.getTime())
|
|
134
139
|
? ''
|
|
135
|
-
:
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function statusClass(job: JobState): string {
|
|
139
|
-
return `mint-jobs-tray__status--${job.status.replace('_', '-')}`
|
|
140
|
+
: date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
function jobPercent(job: JobState): number {
|
|
143
144
|
return Math.round(normalizeJobPercent(job.progress.percent))
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
/** Right-hand value: the percent while a job runs, else when it ended or started. */
|
|
148
|
+
function jobValue(job: JobState): string {
|
|
149
|
+
if (job.status === 'running') return `${jobPercent(job)}%`
|
|
150
|
+
return formatTime(job.completed_at || job.started_at || job.created_at)
|
|
151
|
+
}
|
|
152
|
+
|
|
146
153
|
async function cancelJob(job: JobState): Promise<void> {
|
|
147
154
|
const succeeded = await tray.cancelJob(job)
|
|
148
155
|
if (!succeeded && tray.error.value) emit('error', tray.error.value)
|
|
@@ -162,245 +169,130 @@ async function clearFinished(): Promise<void> {
|
|
|
162
169
|
<template>
|
|
163
170
|
<Teleport :to="teleportTarget" :disabled="teleportTo === false">
|
|
164
171
|
<div class="mint-jobs-tray">
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
class="mint-jobs-
|
|
169
|
-
aria-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
@click="toggleTray"
|
|
173
|
-
>
|
|
174
|
-
<svg
|
|
175
|
-
class="mint-jobs-tray__trigger-icon"
|
|
176
|
-
:class="{ 'mint-jobs-tray__trigger-icon--active': tray.activeJobs.value.length > 0 }"
|
|
177
|
-
viewBox="0 0 24 24"
|
|
178
|
-
fill="none"
|
|
179
|
-
stroke="currentColor"
|
|
180
|
-
aria-hidden="true"
|
|
181
|
-
>
|
|
182
|
-
<path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2" />
|
|
183
|
-
<path d="M9 5a3 3 0 0 1 6 0v2H9V5Z" />
|
|
184
|
-
</svg>
|
|
185
|
-
<span class="mint-jobs-tray__trigger-label">
|
|
186
|
-
{{ tray.featuredJob.value ? jobStatusLabel(tray.featuredJob.value.status) : 'Jobs' }}
|
|
187
|
-
</span>
|
|
188
|
-
<span
|
|
189
|
-
v-if="tray.activeJobs.value.length > 0"
|
|
190
|
-
class="mint-jobs-tray__count"
|
|
191
|
-
aria-live="polite"
|
|
192
|
-
>
|
|
193
|
-
{{ tray.activeJobs.value.length }}
|
|
194
|
-
</span>
|
|
195
|
-
</button>
|
|
172
|
+
<!-- Clicks outside close the panel; the page is not dimmed. -->
|
|
173
|
+
<div
|
|
174
|
+
v-if="tray.isOpen.value"
|
|
175
|
+
class="mint-jobs-tray__scrim"
|
|
176
|
+
aria-hidden="true"
|
|
177
|
+
@click="tray.close()"
|
|
178
|
+
/>
|
|
196
179
|
|
|
197
180
|
<Transition name="mint-jobs-tray-fade">
|
|
198
|
-
<div
|
|
199
|
-
v-if="tray.isOpen.value"
|
|
200
|
-
class="mint-jobs-tray__backdrop"
|
|
201
|
-
aria-hidden="true"
|
|
202
|
-
@click="closeTray"
|
|
203
|
-
/>
|
|
204
|
-
</Transition>
|
|
205
|
-
|
|
206
|
-
<Transition name="mint-jobs-tray-slide">
|
|
207
181
|
<section
|
|
208
182
|
v-if="tray.isOpen.value"
|
|
209
|
-
:id="panelId"
|
|
183
|
+
:id="tray.panelId"
|
|
210
184
|
ref="panelRef"
|
|
211
185
|
class="mint-jobs-tray__panel"
|
|
212
|
-
role="dialog"
|
|
213
|
-
aria-modal="true"
|
|
214
186
|
:aria-labelledby="titleId"
|
|
215
187
|
tabindex="-1"
|
|
216
|
-
@keydown="onPanelKeydown"
|
|
217
188
|
>
|
|
218
|
-
<header class="mint-jobs-
|
|
219
|
-
<
|
|
220
|
-
|
|
221
|
-
<p class="mint-jobs-tray__subtitle" :title="subtitle">{{ subtitle }}</p>
|
|
222
|
-
</div>
|
|
223
|
-
<div class="mint-jobs-tray__header-actions">
|
|
224
|
-
<button
|
|
225
|
-
v-if="tray.canRefresh.value"
|
|
226
|
-
type="button"
|
|
227
|
-
class="mint-jobs-tray__icon-button"
|
|
228
|
-
aria-label="Refresh jobs"
|
|
229
|
-
:disabled="tray.isLoading.value"
|
|
230
|
-
@click="tray.refresh"
|
|
231
|
-
>
|
|
232
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
|
|
233
|
-
<path d="M20 6v5h-5M4 18v-5h5" />
|
|
234
|
-
<path d="M18.4 9A7 7 0 0 0 6.1 6.1L4 8m2 7a7 7 0 0 0 11.9 2.9L20 16" />
|
|
235
|
-
</svg>
|
|
236
|
-
</button>
|
|
237
|
-
<button
|
|
238
|
-
type="button"
|
|
239
|
-
class="mint-jobs-tray__icon-button"
|
|
240
|
-
aria-label="Close jobs"
|
|
241
|
-
@click="closeTray"
|
|
242
|
-
>
|
|
243
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
|
|
244
|
-
<path d="m6 6 12 12M18 6 6 18" />
|
|
245
|
-
</svg>
|
|
246
|
-
</button>
|
|
247
|
-
</div>
|
|
248
|
-
</header>
|
|
249
|
-
|
|
250
|
-
<div class="mint-jobs-tray__tabs" role="tablist" aria-label="Job groups">
|
|
189
|
+
<header class="mint-jobs-tray__head">
|
|
190
|
+
<h2 :id="titleId" class="mint-jobs-tray__title">{{ title }}</h2>
|
|
191
|
+
<span class="mint-jobs-tray__head-count">{{ headCount }}</span>
|
|
251
192
|
<button
|
|
193
|
+
v-if="tray.canRefresh.value"
|
|
252
194
|
type="button"
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
:
|
|
257
|
-
|
|
258
|
-
@click="tray.activeTab.value = 'active'"
|
|
195
|
+
class="mint-jobs-tray__icon-button"
|
|
196
|
+
aria-label="Refresh jobs"
|
|
197
|
+
title="Refresh jobs"
|
|
198
|
+
:disabled="tray.isLoading.value"
|
|
199
|
+
@click="tray.refresh"
|
|
259
200
|
>
|
|
260
|
-
|
|
261
|
-
<span class="mint-jobs-tray__tab-count">{{ tray.activeJobs.value.length }}</span>
|
|
201
|
+
<RefreshCw :size="14" aria-hidden="true" />
|
|
262
202
|
</button>
|
|
263
203
|
<button
|
|
264
204
|
type="button"
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
:aria-selected="tray.activeTab.value === 'history'"
|
|
270
|
-
@click="tray.activeTab.value = 'history'"
|
|
205
|
+
class="mint-jobs-tray__icon-button"
|
|
206
|
+
aria-label="Close jobs"
|
|
207
|
+
title="Close"
|
|
208
|
+
@click="closeTray"
|
|
271
209
|
>
|
|
272
|
-
|
|
273
|
-
<span class="mint-jobs-tray__tab-count">{{ tray.historyJobs.value.length }}</span>
|
|
210
|
+
<X :size="14" aria-hidden="true" />
|
|
274
211
|
</button>
|
|
275
|
-
</
|
|
212
|
+
</header>
|
|
276
213
|
|
|
277
214
|
<p v-if="tray.error.value" class="mint-jobs-tray__error" role="alert">
|
|
278
215
|
{{ tray.error.value }}
|
|
279
216
|
</p>
|
|
280
217
|
|
|
281
218
|
<div class="mint-jobs-tray__body">
|
|
282
|
-
<
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
219
|
+
<p v-if="sections.length === 0" class="mint-jobs-tray__empty">No jobs yet</p>
|
|
220
|
+
|
|
221
|
+
<template v-for="section in sections" :key="section.key">
|
|
222
|
+
<div class="mint-jobs-tray__section" :data-section="section.key">
|
|
223
|
+
{{ section.title }}
|
|
224
|
+
<span class="mint-jobs-tray__section-count">· {{ section.jobs.length }}</span>
|
|
225
|
+
<button
|
|
226
|
+
v-if="section.key === 'finished' && canClear"
|
|
227
|
+
type="button"
|
|
228
|
+
class="mint-jobs-tray__section-action"
|
|
229
|
+
@click="clearFinished"
|
|
230
|
+
>
|
|
231
|
+
Clear
|
|
232
|
+
</button>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<div
|
|
236
|
+
v-for="job in section.jobs"
|
|
288
237
|
:key="job.job_id"
|
|
289
|
-
class="mint-jobs-
|
|
290
|
-
:class="{ 'mint-jobs-
|
|
238
|
+
class="mint-jobs-tray__row"
|
|
239
|
+
:class="{ 'mint-jobs-tray__row--awaiting-input': job.status === 'awaiting_input' }"
|
|
240
|
+
:data-status="job.status"
|
|
291
241
|
>
|
|
292
|
-
<
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
<span class="mint-jobs-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
242
|
+
<component
|
|
243
|
+
:is="STATUS_ICONS[job.status]"
|
|
244
|
+
:size="15"
|
|
245
|
+
:class="['mint-jobs-tray__status', `mint-jobs-tray__status--${job.status.replace('_', '-')}`]"
|
|
246
|
+
role="img"
|
|
247
|
+
:aria-label="jobStatusLabel(job.status)"
|
|
248
|
+
/>
|
|
249
|
+
<button type="button" class="mint-jobs-tray__row-title" @click="emit('select', job)">
|
|
250
|
+
<slot name="job-title" :job="job">{{ job.name }}</slot>
|
|
251
|
+
</button>
|
|
252
|
+
<span class="mint-jobs-tray__tools">
|
|
253
|
+
<span class="mint-jobs-tray__value">{{ jobValue(job) }}</span>
|
|
254
|
+
<slot name="job-actions" :job="job" />
|
|
255
|
+
<button
|
|
256
|
+
v-if="isActiveJobStatus(job.status) && job.can_cancel && tray.canCancel.value"
|
|
257
|
+
type="button"
|
|
258
|
+
class="mint-jobs-tray__icon-button mint-jobs-tray__icon-button--danger"
|
|
259
|
+
:aria-label="`Cancel ${job.name}`"
|
|
260
|
+
title="Cancel"
|
|
261
|
+
:disabled="tray.isPending(job)"
|
|
262
|
+
@click="cancelJob(job)"
|
|
263
|
+
>
|
|
264
|
+
<Square :size="12" aria-hidden="true" />
|
|
265
|
+
</button>
|
|
266
|
+
<button
|
|
267
|
+
v-if="job.can_delete && tray.canDelete.value"
|
|
268
|
+
type="button"
|
|
269
|
+
class="mint-jobs-tray__icon-button"
|
|
270
|
+
:aria-label="`Delete ${job.name}`"
|
|
271
|
+
title="Delete"
|
|
272
|
+
:disabled="tray.isPending(job)"
|
|
273
|
+
@click="deleteJob(job)"
|
|
274
|
+
>
|
|
275
|
+
<Trash2 :size="14" aria-hidden="true" />
|
|
276
|
+
</button>
|
|
277
|
+
</span>
|
|
309
278
|
<div
|
|
310
279
|
v-if="job.status === 'running'"
|
|
311
280
|
class="mint-jobs-tray__progress"
|
|
312
281
|
role="progressbar"
|
|
313
|
-
aria-label="
|
|
282
|
+
:aria-label="`${job.name} progress`"
|
|
314
283
|
aria-valuemin="0"
|
|
315
284
|
aria-valuemax="100"
|
|
316
285
|
:aria-valuenow="jobPercent(job)"
|
|
317
286
|
>
|
|
318
287
|
<span :style="{ width: `${jobPercent(job)}%` }" />
|
|
319
288
|
</div>
|
|
320
|
-
<
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
<slot name="job-actions" :job="job" />
|
|
324
|
-
<button
|
|
325
|
-
v-if="job.can_cancel && tray.canCancel.value"
|
|
326
|
-
type="button"
|
|
327
|
-
class="mint-jobs-tray__action mint-jobs-tray__action--danger"
|
|
328
|
-
aria-label="Cancel job"
|
|
329
|
-
:disabled="tray.isPending(job)"
|
|
330
|
-
@click="cancelJob(job)"
|
|
331
|
-
>
|
|
332
|
-
Cancel
|
|
333
|
-
</button>
|
|
334
|
-
<button
|
|
335
|
-
v-if="job.can_delete && tray.canDelete.value"
|
|
336
|
-
type="button"
|
|
337
|
-
class="mint-jobs-tray__icon-button mint-jobs-tray__icon-button--danger"
|
|
338
|
-
aria-label="Delete job"
|
|
339
|
-
:disabled="tray.isPending(job)"
|
|
340
|
-
@click="deleteJob(job)"
|
|
341
|
-
>
|
|
342
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
|
|
343
|
-
<path d="M4 7h16M9 7V4h6v3m-8 0 1 13h8l1-13M10 11v5M14 11v5" />
|
|
344
|
-
</svg>
|
|
345
|
-
</button>
|
|
346
|
-
</div>
|
|
347
|
-
</div>
|
|
348
|
-
</article>
|
|
349
|
-
</template>
|
|
350
|
-
|
|
351
|
-
<template v-else>
|
|
352
|
-
<div v-if="canClear" class="mint-jobs-tray__clear-row">
|
|
353
|
-
<button
|
|
354
|
-
type="button"
|
|
355
|
-
class="mint-jobs-tray__action mint-jobs-tray__action--danger"
|
|
356
|
-
@click="clearFinished"
|
|
289
|
+
<span
|
|
290
|
+
class="mint-jobs-tray__detail"
|
|
291
|
+
:class="{ 'mint-jobs-tray__detail--error': job.status === 'failed' }"
|
|
357
292
|
>
|
|
358
|
-
|
|
359
|
-
</
|
|
293
|
+
<slot name="job-detail" :job="job">{{ progressText(job) }}</slot>
|
|
294
|
+
</span>
|
|
360
295
|
</div>
|
|
361
|
-
<p v-if="tray.historyJobs.value.length === 0" class="mint-jobs-tray__empty">
|
|
362
|
-
No job history
|
|
363
|
-
</p>
|
|
364
|
-
<article
|
|
365
|
-
v-for="job in tray.historyJobs.value"
|
|
366
|
-
:key="job.job_id"
|
|
367
|
-
class="mint-jobs-tray__job"
|
|
368
|
-
>
|
|
369
|
-
<div class="mint-jobs-tray__job-heading">
|
|
370
|
-
<div class="mint-jobs-tray__job-identity">
|
|
371
|
-
<button
|
|
372
|
-
type="button"
|
|
373
|
-
class="mint-jobs-tray__job-select"
|
|
374
|
-
@click="emit('select', job)"
|
|
375
|
-
>
|
|
376
|
-
{{ job.name }}
|
|
377
|
-
</button>
|
|
378
|
-
<span class="mint-jobs-tray__kind">{{ job.kind }}</span>
|
|
379
|
-
</div>
|
|
380
|
-
<span class="mint-jobs-tray__status" :class="statusClass(job)">
|
|
381
|
-
<span class="mint-jobs-tray__status-dot" aria-hidden="true" />
|
|
382
|
-
{{ jobStatusLabel(job.status) }}
|
|
383
|
-
</span>
|
|
384
|
-
</div>
|
|
385
|
-
<div class="mint-jobs-tray__job-footer">
|
|
386
|
-
<span>{{ formatTimestamp(job.completed_at || job.created_at) }}</span>
|
|
387
|
-
<div class="mint-jobs-tray__job-actions">
|
|
388
|
-
<slot name="job-actions" :job="job" />
|
|
389
|
-
<button
|
|
390
|
-
v-if="job.can_delete && tray.canDelete.value"
|
|
391
|
-
type="button"
|
|
392
|
-
class="mint-jobs-tray__icon-button mint-jobs-tray__icon-button--danger"
|
|
393
|
-
aria-label="Delete job"
|
|
394
|
-
:disabled="tray.isPending(job)"
|
|
395
|
-
@click="deleteJob(job)"
|
|
396
|
-
>
|
|
397
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
|
|
398
|
-
<path d="M4 7h16M9 7V4h6v3m-8 0 1 13h8l1-13M10 11v5M14 11v5" />
|
|
399
|
-
</svg>
|
|
400
|
-
</button>
|
|
401
|
-
</div>
|
|
402
|
-
</div>
|
|
403
|
-
</article>
|
|
404
296
|
</template>
|
|
405
297
|
</div>
|
|
406
298
|
</section>
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
PillNavOption,
|
|
5
5
|
PluginWorkspaceViewSpec,
|
|
6
6
|
} from '../types'
|
|
7
|
+
import type { JobState } from '../types/jobs'
|
|
7
8
|
import type {
|
|
8
9
|
ControlComponentBinding,
|
|
9
10
|
ControlComponentBindingsById,
|
|
@@ -46,6 +47,8 @@ export interface PluginWorkspaceViewEmits {
|
|
|
46
47
|
'section-action': [sectionId: string, actionId: string]
|
|
47
48
|
'form-submit': [sectionId: string, values: Record<string, unknown>]
|
|
48
49
|
'form-cancel': [sectionId: string]
|
|
50
|
+
/** A job row in the jobs tray, or the just-completed job on the top-bar chip, was chosen. */
|
|
51
|
+
'job-select': [job: JobState]
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
/** Typed `emit` function shape produced by `defineEmits<PluginWorkspaceViewEmits>()`. */
|
|
@@ -20,9 +20,12 @@ import type {
|
|
|
20
20
|
PillNavItem,
|
|
21
21
|
PillNavOption,
|
|
22
22
|
} from '../types'
|
|
23
|
+
import type { JobState } from '../types/jobs'
|
|
23
24
|
import { useAppExperiment } from '../composables/useAppExperiment'
|
|
25
|
+
import { useJobCenter } from '../composables/useJobCenter'
|
|
24
26
|
import { usePluginWorkspace } from '../composables/usePluginWorkspace'
|
|
25
27
|
import AppToastContainer from './AppToastContainer.vue'
|
|
28
|
+
import JobsStatusTray from './JobsStatusTray.vue'
|
|
26
29
|
import WorkspaceControlsInternal from './internal/WorkspaceControlsInternal.vue'
|
|
27
30
|
import WorkspaceShellInternal from './internal/WorkspaceShellInternal.vue'
|
|
28
31
|
import WorkspaceSidebarInternal from './internal/WorkspaceSidebarInternal.vue'
|
|
@@ -76,6 +79,14 @@ if (props.spec.experiment) {
|
|
|
76
79
|
})
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
// The job center is setup-time structure too: no `jobs`, no jobs control.
|
|
83
|
+
const jobs = props.spec.jobs
|
|
84
|
+
const jobCenter = jobs ? useJobCenter({ source: jobs }) : null
|
|
85
|
+
|
|
86
|
+
function handleJobSelect(job: JobState) {
|
|
87
|
+
emit('job-select', job)
|
|
88
|
+
}
|
|
89
|
+
|
|
79
90
|
const {
|
|
80
91
|
shell,
|
|
81
92
|
sidebar,
|
|
@@ -105,6 +116,11 @@ function handleFormCancel(sectionId: string) {
|
|
|
105
116
|
<template>
|
|
106
117
|
<WorkspaceShellInternal
|
|
107
118
|
:shell="shell"
|
|
119
|
+
:job="jobCenter ? jobCenter.topBarJob.value : undefined"
|
|
120
|
+
:jobs-done="jobCenter?.completedCount.value"
|
|
121
|
+
:jobs-open="jobCenter?.isOpen.value"
|
|
122
|
+
:jobs-panel-id="jobCenter?.panelId"
|
|
123
|
+
@job-click="jobCenter?.handleTopBarClick(handleJobSelect)"
|
|
108
124
|
@pill-select="handlePillSelect"
|
|
109
125
|
@pill-option-select="(option: PillNavOption, item: PillNavItem) => emit('pill-option-select', option, item)"
|
|
110
126
|
@settings-values-change="emit('settings-values-change', $event)"
|
|
@@ -158,6 +174,7 @@ function handleFormCancel(sectionId: string) {
|
|
|
158
174
|
</template>
|
|
159
175
|
</WorkspaceControlsInternal>
|
|
160
176
|
<AppToastContainer v-if="spec.toasts !== false" />
|
|
177
|
+
<JobsStatusTray v-if="jobCenter" :center="jobCenter" @select="handleJobSelect" />
|
|
161
178
|
</template>
|
|
162
179
|
</WorkspaceShellInternal>
|
|
163
180
|
</template>
|
|
@@ -12,15 +12,22 @@ import type {
|
|
|
12
12
|
import AppLayout from '../AppLayout.vue'
|
|
13
13
|
import AppTopBar from '../AppTopBar.vue'
|
|
14
14
|
import type { PluginWorkspaceShellModel } from '../../composables/pluginWorkspaceModel'
|
|
15
|
+
import type { TopBarJob } from '../../types/jobs'
|
|
15
16
|
|
|
16
17
|
defineProps<{
|
|
17
18
|
shell: PluginWorkspaceShellModel
|
|
19
|
+
/** Passed to AppTopBar; undefined means no jobs control. */
|
|
20
|
+
job?: TopBarJob | null
|
|
21
|
+
jobsDone?: number
|
|
22
|
+
jobsOpen?: boolean
|
|
23
|
+
jobsPanelId?: string
|
|
18
24
|
}>()
|
|
19
25
|
|
|
20
26
|
const emit = defineEmits<{
|
|
21
27
|
'pill-select': [item: PillNavItem]
|
|
22
28
|
'pill-option-select': [option: PillNavOption, item: PillNavItem]
|
|
23
29
|
'settings-values-change': [data: Record<string, unknown>]
|
|
30
|
+
'job-click': []
|
|
24
31
|
}>()
|
|
25
32
|
|
|
26
33
|
function handlePillOptionSelect(option: PillNavOption, item: PillNavItem) {
|
|
@@ -58,6 +65,11 @@ defineSlots<{
|
|
|
58
65
|
:subtitle="shell.subtitle"
|
|
59
66
|
:spec="shell.topBar"
|
|
60
67
|
:pill="shell.pill"
|
|
68
|
+
:job="job"
|
|
69
|
+
:jobs-done="jobsDone"
|
|
70
|
+
:jobs-open="jobsOpen"
|
|
71
|
+
:jobs-panel-id="jobsPanelId"
|
|
72
|
+
@job-click="emit('job-click')"
|
|
61
73
|
@pill-select="emit('pill-select', $event)"
|
|
62
74
|
@pill-option-select="handlePillOptionSelect"
|
|
63
75
|
@settings-values-change="emit('settings-values-change', $event)"
|