@morscherlab/mint-sdk 1.0.51 → 1.0.52
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/__tests__/components/JobsStatusTray.test.d.ts +1 -0
- package/dist/__tests__/composables/useJobsStatusTray.test.d.ts +1 -0
- package/dist/__tests__/utils/jobs.test.d.ts +1 -0
- package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +1 -1
- package/dist/components/JobsStatusTray.vue.d.ts +26 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +2 -2
- package/dist/{components-C7UFkNIp.js → components-Lk_h_rON.js} +1663 -1339
- package/dist/components-Lk_h_rON.js.map +1 -0
- package/dist/composables/index.d.ts +1 -0
- package/dist/composables/index.js +3 -3
- package/dist/composables/useJobsStatusTray.d.ts +43 -0
- package/dist/composables/usePluginClient.d.ts +4 -1
- package/dist/{composables-CpBhNKHf.js → composables-DyfGpYZw.js} +50 -17
- package/dist/{composables-CpBhNKHf.js.map → composables-DyfGpYZw.js.map} +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -4
- package/dist/install.js +1 -1
- package/dist/jobs.d.ts +19 -0
- package/dist/styles.css +731 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/jobs.d.ts +49 -0
- package/dist/{useProtocolTemplates-C8-YlHj1.js → useJobsStatusTray-DAH999_N.js} +250 -2
- package/dist/useJobsStatusTray-DAH999_N.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/components/JobsStatusTray.test.ts +141 -0
- package/src/__tests__/composables/useJobsStatusTray.test.ts +241 -0
- package/src/__tests__/composables/usePluginClient.test.ts +243 -0
- package/src/__tests__/utils/jobs.test.ts +95 -0
- package/src/components/JobsStatusTray.story.vue +120 -0
- package/src/components/JobsStatusTray.vue +385 -0
- package/src/components/index.ts +1 -0
- package/src/composables/index.ts +8 -0
- package/src/composables/useJobsStatusTray.ts +304 -0
- package/src/composables/usePluginClient.ts +65 -15
- package/src/index.ts +3 -0
- package/src/jobs.ts +75 -0
- package/src/styles/components/jobs-status-tray.css +420 -0
- package/src/styles/index.css +1 -0
- package/src/types/index.ts +10 -0
- package/src/types/jobs.ts +61 -0
- package/dist/components-C7UFkNIp.js.map +0 -1
- package/dist/useProtocolTemplates-C8-YlHj1.js.map +0 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
normalizeJobPercent,
|
|
5
|
+
normalizeJobState,
|
|
6
|
+
resolveJobCapabilities,
|
|
7
|
+
} from '../../jobs'
|
|
8
|
+
import type { JobStateInput, JobStatus } from '../../types/jobs'
|
|
9
|
+
|
|
10
|
+
const legacyCapabilities: Record<JobStatus, [boolean, boolean]> = {
|
|
11
|
+
awaiting_input: [false, false],
|
|
12
|
+
queued: [true, false],
|
|
13
|
+
running: [true, false],
|
|
14
|
+
completed: [false, true],
|
|
15
|
+
failed: [false, true],
|
|
16
|
+
cancelled: [false, true],
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function job(status: JobStatus): JobStateInput {
|
|
20
|
+
return {
|
|
21
|
+
id: status,
|
|
22
|
+
job_id: status,
|
|
23
|
+
kind: 'analysis',
|
|
24
|
+
name: status,
|
|
25
|
+
status,
|
|
26
|
+
progress: {
|
|
27
|
+
current_file: 0,
|
|
28
|
+
total_files: 0,
|
|
29
|
+
current_filename: null,
|
|
30
|
+
percent: 0,
|
|
31
|
+
message: '',
|
|
32
|
+
stage: status,
|
|
33
|
+
},
|
|
34
|
+
queue_position: 0,
|
|
35
|
+
warnings: [],
|
|
36
|
+
error: null,
|
|
37
|
+
output_path: null,
|
|
38
|
+
save_path: null,
|
|
39
|
+
session_id: null,
|
|
40
|
+
folders: [],
|
|
41
|
+
folder_path: null,
|
|
42
|
+
user_id: '1',
|
|
43
|
+
owner_user_id: '1',
|
|
44
|
+
experiment_id: null,
|
|
45
|
+
extraction_result_id: null,
|
|
46
|
+
metadata: {},
|
|
47
|
+
created_at: '2026-07-14T10:00:00+00:00',
|
|
48
|
+
started_at: null,
|
|
49
|
+
completed_at: null,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('job capability normalization', () => {
|
|
54
|
+
it.each(Object.entries(legacyCapabilities))(
|
|
55
|
+
'infers legacy capabilities for %s',
|
|
56
|
+
(status, expected) => {
|
|
57
|
+
const capabilities = resolveJobCapabilities({ status: status as JobStatus })
|
|
58
|
+
|
|
59
|
+
expect([capabilities.can_cancel, capabilities.can_delete]).toEqual(expected)
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
it('keeps explicit capabilities authoritative', () => {
|
|
64
|
+
const capabilities = resolveJobCapabilities({
|
|
65
|
+
status: 'running',
|
|
66
|
+
can_cancel: false,
|
|
67
|
+
can_delete: true,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
expect(capabilities).toEqual({ can_cancel: false, can_delete: true })
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('fails closed for an unknown legacy status', () => {
|
|
74
|
+
const capabilities = resolveJobCapabilities({ status: 'custom-status' })
|
|
75
|
+
|
|
76
|
+
expect(capabilities).toEqual({ can_cancel: false, can_delete: false })
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('normalizes without mutating the transport input', () => {
|
|
80
|
+
const input = job('awaiting_input')
|
|
81
|
+
|
|
82
|
+
normalizeJobState(input)
|
|
83
|
+
|
|
84
|
+
expect('can_cancel' in input).toBe(false)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it.each([
|
|
88
|
+
[Number.NaN, 0],
|
|
89
|
+
[Number.POSITIVE_INFINITY, 0],
|
|
90
|
+
[-1, 0],
|
|
91
|
+
[101, 100],
|
|
92
|
+
])('clamps unsafe progress value %s', (value, expected) => {
|
|
93
|
+
expect(normalizeJobPercent(value)).toBe(expected)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import JobsStatusTray from './JobsStatusTray.vue'
|
|
3
|
+
import type { JobStateInput, JobStatus } from '../types/jobs'
|
|
4
|
+
|
|
5
|
+
function job(
|
|
6
|
+
status: JobStatus,
|
|
7
|
+
id: string,
|
|
8
|
+
overrides: Partial<JobStateInput> = {},
|
|
9
|
+
): JobStateInput {
|
|
10
|
+
return {
|
|
11
|
+
id,
|
|
12
|
+
job_id: id,
|
|
13
|
+
kind: 'analysis',
|
|
14
|
+
name: `LC-MS analysis ${id}`,
|
|
15
|
+
status,
|
|
16
|
+
progress: {
|
|
17
|
+
current_file: status === 'running' ? 3 : 0,
|
|
18
|
+
total_files: status === 'running' ? 8 : 0,
|
|
19
|
+
current_filename: status === 'running' ? 'sample_03.raw' : null,
|
|
20
|
+
percent: status === 'running' ? 37.5 : 0,
|
|
21
|
+
message: status === 'awaiting_input' ? 'Select a reference library to continue' : '',
|
|
22
|
+
stage: status,
|
|
23
|
+
},
|
|
24
|
+
queue_position: status === 'queued' ? 2 : 0,
|
|
25
|
+
warnings: [],
|
|
26
|
+
error: status === 'failed' ? 'Reference peaks were not detected' : null,
|
|
27
|
+
output_path: null,
|
|
28
|
+
save_path: null,
|
|
29
|
+
session_id: null,
|
|
30
|
+
folders: [],
|
|
31
|
+
folder_path: null,
|
|
32
|
+
user_id: 'researcher-1',
|
|
33
|
+
owner_user_id: 'researcher-1',
|
|
34
|
+
experiment_id: 'EXP-042',
|
|
35
|
+
extraction_result_id: null,
|
|
36
|
+
metadata: {},
|
|
37
|
+
created_at: '2026-07-14T10:00:00+00:00',
|
|
38
|
+
started_at: status === 'running' ? '2026-07-14T10:01:00+00:00' : null,
|
|
39
|
+
completed_at: ['completed', 'failed', 'cancelled'].includes(status)
|
|
40
|
+
? '2026-07-14T10:08:00+00:00'
|
|
41
|
+
: null,
|
|
42
|
+
...overrides,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const mixedJobs = [
|
|
47
|
+
job('awaiting_input', 'J-104'),
|
|
48
|
+
job('running', 'J-103'),
|
|
49
|
+
job('queued', 'J-102'),
|
|
50
|
+
job('completed', 'J-101'),
|
|
51
|
+
job('failed', 'J-100'),
|
|
52
|
+
]
|
|
53
|
+
const explicitActionJobs = [
|
|
54
|
+
job('awaiting_input', 'J-201', { can_cancel: true }),
|
|
55
|
+
job('completed', 'J-200', { can_delete: false }),
|
|
56
|
+
]
|
|
57
|
+
const adapter = {
|
|
58
|
+
cancelJob: async () => undefined,
|
|
59
|
+
deleteJob: async () => true,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function initState() {
|
|
63
|
+
return {
|
|
64
|
+
title: 'Analysis jobs',
|
|
65
|
+
jobs: mixedJobs,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<template>
|
|
71
|
+
<Story title="Workflow/JobsStatusTray">
|
|
72
|
+
<Variant title="Playground" :init-state="initState">
|
|
73
|
+
<template #default="{ state }">
|
|
74
|
+
<div style="padding: 2rem; min-height: 520px;">
|
|
75
|
+
<JobsStatusTray
|
|
76
|
+
:title="state.title"
|
|
77
|
+
:jobs="state.jobs"
|
|
78
|
+
:adapter="adapter"
|
|
79
|
+
:load-on-mount="false"
|
|
80
|
+
:teleport-to="false"
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
</template>
|
|
84
|
+
<template #controls="{ state }">
|
|
85
|
+
<HstText v-model="state.title" title="Title" />
|
|
86
|
+
</template>
|
|
87
|
+
</Variant>
|
|
88
|
+
|
|
89
|
+
<Variant title="Awaiting input">
|
|
90
|
+
<div style="padding: 2rem; min-height: 520px;">
|
|
91
|
+
<JobsStatusTray
|
|
92
|
+
:jobs="[job('awaiting_input', 'J-301')]"
|
|
93
|
+
:load-on-mount="false"
|
|
94
|
+
:teleport-to="false"
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
</Variant>
|
|
98
|
+
|
|
99
|
+
<Variant title="Explicit actions">
|
|
100
|
+
<div style="padding: 2rem; min-height: 520px;">
|
|
101
|
+
<JobsStatusTray
|
|
102
|
+
:jobs="explicitActionJobs"
|
|
103
|
+
:adapter="adapter"
|
|
104
|
+
:load-on-mount="false"
|
|
105
|
+
:teleport-to="false"
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
</Variant>
|
|
109
|
+
|
|
110
|
+
<Variant title="Empty">
|
|
111
|
+
<div style="padding: 2rem; min-height: 520px;">
|
|
112
|
+
<JobsStatusTray
|
|
113
|
+
:jobs="[]"
|
|
114
|
+
:load-on-mount="false"
|
|
115
|
+
:teleport-to="false"
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
</Variant>
|
|
119
|
+
</Story>
|
|
120
|
+
</template>
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/** Floating, adapter-driven job status tray for plugin and platform workflows. */
|
|
3
|
+
import { computed, nextTick, ref, toRef, useId } from 'vue'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
jobStatusLabel,
|
|
7
|
+
normalizeJobPercent,
|
|
8
|
+
} from '../jobs'
|
|
9
|
+
import {
|
|
10
|
+
useJobsStatusTray,
|
|
11
|
+
type JobsStatusTrayAdapter,
|
|
12
|
+
} from '../composables/useJobsStatusTray'
|
|
13
|
+
import type { UsePluginEventStreamReturn } from '../composables/usePluginClient'
|
|
14
|
+
import type {
|
|
15
|
+
JobState,
|
|
16
|
+
JobStateInput,
|
|
17
|
+
JobStreamData,
|
|
18
|
+
} from '../types/jobs'
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
jobs?: readonly JobStateInput[]
|
|
22
|
+
adapter?: JobsStatusTrayAdapter
|
|
23
|
+
eventStream?: UsePluginEventStreamReturn<JobStreamData>
|
|
24
|
+
loadOnMount?: boolean
|
|
25
|
+
title?: string
|
|
26
|
+
teleportTo?: string | HTMLElement | false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
30
|
+
loadOnMount: true,
|
|
31
|
+
title: 'Analysis jobs',
|
|
32
|
+
teleportTo: 'body',
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const emit = defineEmits<{
|
|
36
|
+
select: [job: JobState]
|
|
37
|
+
error: [message: string]
|
|
38
|
+
}>()
|
|
39
|
+
|
|
40
|
+
const panelId = `mint-jobs-tray-${useId().replace(/:/g, '')}`
|
|
41
|
+
const titleId = `${panelId}-title`
|
|
42
|
+
const triggerRef = ref<HTMLButtonElement | null>(null)
|
|
43
|
+
const panelRef = ref<HTMLElement | null>(null)
|
|
44
|
+
const teleportTarget = computed(() => props.teleportTo || 'body')
|
|
45
|
+
const tray = useJobsStatusTray({
|
|
46
|
+
jobs: toRef(props, 'jobs'),
|
|
47
|
+
adapter: toRef(props, 'adapter'),
|
|
48
|
+
eventStream: toRef(props, 'eventStream'),
|
|
49
|
+
loadOnMount: props.loadOnMount,
|
|
50
|
+
})
|
|
51
|
+
const canClear = computed(() => (
|
|
52
|
+
tray.deletableJobs.value.length > 0
|
|
53
|
+
&& Boolean(props.adapter?.clearFinishedJobs || props.adapter?.deleteJob)
|
|
54
|
+
))
|
|
55
|
+
|
|
56
|
+
async function openTray(): Promise<void> {
|
|
57
|
+
tray.open()
|
|
58
|
+
await nextTick()
|
|
59
|
+
panelRef.value?.focus()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function closeTray(): Promise<void> {
|
|
63
|
+
tray.close()
|
|
64
|
+
await nextTick()
|
|
65
|
+
triggerRef.value?.focus()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function toggleTray(): Promise<void> {
|
|
69
|
+
if (tray.isOpen.value) await closeTray()
|
|
70
|
+
else await openTray()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function onPanelKeydown(event: KeyboardEvent): void {
|
|
74
|
+
if (event.key === 'Escape') {
|
|
75
|
+
void closeTray()
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
if (event.key !== 'Tab' || !panelRef.value) return
|
|
79
|
+
const focusable = Array.from(panelRef.value.querySelectorAll<HTMLElement>(
|
|
80
|
+
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
|
|
81
|
+
))
|
|
82
|
+
if (focusable.length === 0) {
|
|
83
|
+
event.preventDefault()
|
|
84
|
+
panelRef.value.focus()
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
const first = focusable[0]
|
|
88
|
+
const last = focusable[focusable.length - 1]
|
|
89
|
+
const activeElement = document.activeElement as HTMLElement | null
|
|
90
|
+
if (!activeElement || !focusable.includes(activeElement)) {
|
|
91
|
+
event.preventDefault()
|
|
92
|
+
if (event.shiftKey) last?.focus()
|
|
93
|
+
else first?.focus()
|
|
94
|
+
} else if (event.shiftKey && activeElement === first) {
|
|
95
|
+
event.preventDefault()
|
|
96
|
+
last?.focus()
|
|
97
|
+
} else if (!event.shiftKey && activeElement === last) {
|
|
98
|
+
event.preventDefault()
|
|
99
|
+
first?.focus()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function progressText(job: JobState): string {
|
|
104
|
+
if (job.status === 'awaiting_input') {
|
|
105
|
+
return job.progress.message || 'Waiting for external input'
|
|
106
|
+
}
|
|
107
|
+
if (job.status === 'queued' && job.queue_position > 0) {
|
|
108
|
+
return `Queue position ${job.queue_position}`
|
|
109
|
+
}
|
|
110
|
+
return job.progress.message || job.progress.stage || jobStatusLabel(job.status)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function formatTimestamp(value: string | null): string {
|
|
114
|
+
if (!value) return ''
|
|
115
|
+
const date = new Date(value)
|
|
116
|
+
return Number.isNaN(date.getTime())
|
|
117
|
+
? ''
|
|
118
|
+
: new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(date)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function statusClass(job: JobState): string {
|
|
122
|
+
return `mint-jobs-tray__status--${job.status.replace('_', '-')}`
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function cancelJob(job: JobState): Promise<void> {
|
|
126
|
+
const succeeded = await tray.cancelJob(job)
|
|
127
|
+
if (!succeeded && tray.error.value) emit('error', tray.error.value)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function deleteJob(job: JobState): Promise<void> {
|
|
131
|
+
const succeeded = await tray.deleteJob(job)
|
|
132
|
+
if (!succeeded && tray.error.value) emit('error', tray.error.value)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function clearFinished(): Promise<void> {
|
|
136
|
+
const succeeded = await tray.clearFinished()
|
|
137
|
+
if (!succeeded && tray.error.value) emit('error', tray.error.value)
|
|
138
|
+
}
|
|
139
|
+
</script>
|
|
140
|
+
|
|
141
|
+
<template>
|
|
142
|
+
<Teleport :to="teleportTarget" :disabled="teleportTo === false">
|
|
143
|
+
<div class="mint-jobs-tray">
|
|
144
|
+
<button
|
|
145
|
+
ref="triggerRef"
|
|
146
|
+
type="button"
|
|
147
|
+
class="mint-jobs-tray__trigger"
|
|
148
|
+
aria-haspopup="dialog"
|
|
149
|
+
:aria-expanded="tray.isOpen.value"
|
|
150
|
+
:aria-controls="panelId"
|
|
151
|
+
@click="toggleTray"
|
|
152
|
+
>
|
|
153
|
+
<svg
|
|
154
|
+
class="mint-jobs-tray__trigger-icon"
|
|
155
|
+
:class="{ 'mint-jobs-tray__trigger-icon--active': tray.activeJobs.value.length > 0 }"
|
|
156
|
+
viewBox="0 0 24 24"
|
|
157
|
+
fill="none"
|
|
158
|
+
stroke="currentColor"
|
|
159
|
+
aria-hidden="true"
|
|
160
|
+
>
|
|
161
|
+
<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" />
|
|
162
|
+
<path d="M9 5a3 3 0 0 1 6 0v2H9V5Z" />
|
|
163
|
+
</svg>
|
|
164
|
+
<span class="mint-jobs-tray__trigger-label">
|
|
165
|
+
{{ tray.featuredJob.value ? jobStatusLabel(tray.featuredJob.value.status) : 'Jobs' }}
|
|
166
|
+
</span>
|
|
167
|
+
<span
|
|
168
|
+
v-if="tray.activeJobs.value.length > 0"
|
|
169
|
+
class="mint-jobs-tray__count"
|
|
170
|
+
aria-live="polite"
|
|
171
|
+
>
|
|
172
|
+
{{ tray.activeJobs.value.length }}
|
|
173
|
+
</span>
|
|
174
|
+
</button>
|
|
175
|
+
|
|
176
|
+
<Transition name="mint-jobs-tray-fade">
|
|
177
|
+
<div
|
|
178
|
+
v-if="tray.isOpen.value"
|
|
179
|
+
class="mint-jobs-tray__backdrop"
|
|
180
|
+
aria-hidden="true"
|
|
181
|
+
@click="closeTray"
|
|
182
|
+
/>
|
|
183
|
+
</Transition>
|
|
184
|
+
|
|
185
|
+
<Transition name="mint-jobs-tray-slide">
|
|
186
|
+
<section
|
|
187
|
+
v-if="tray.isOpen.value"
|
|
188
|
+
:id="panelId"
|
|
189
|
+
ref="panelRef"
|
|
190
|
+
class="mint-jobs-tray__panel"
|
|
191
|
+
role="dialog"
|
|
192
|
+
aria-modal="true"
|
|
193
|
+
:aria-labelledby="titleId"
|
|
194
|
+
tabindex="-1"
|
|
195
|
+
@keydown="onPanelKeydown"
|
|
196
|
+
>
|
|
197
|
+
<header class="mint-jobs-tray__header">
|
|
198
|
+
<div>
|
|
199
|
+
<h2 :id="titleId" class="mint-jobs-tray__title">{{ title }}</h2>
|
|
200
|
+
<p class="mint-jobs-tray__subtitle">{{ tray.jobs.value.length }} total</p>
|
|
201
|
+
</div>
|
|
202
|
+
<div class="mint-jobs-tray__header-actions">
|
|
203
|
+
<button
|
|
204
|
+
v-if="adapter?.listJobs"
|
|
205
|
+
type="button"
|
|
206
|
+
class="mint-jobs-tray__icon-button"
|
|
207
|
+
aria-label="Refresh jobs"
|
|
208
|
+
:disabled="tray.isLoading.value"
|
|
209
|
+
@click="tray.refresh"
|
|
210
|
+
>
|
|
211
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
|
|
212
|
+
<path d="M20 6v5h-5M4 18v-5h5" />
|
|
213
|
+
<path d="M18.4 9A7 7 0 0 0 6.1 6.1L4 8m2 7a7 7 0 0 0 11.9 2.9L20 16" />
|
|
214
|
+
</svg>
|
|
215
|
+
</button>
|
|
216
|
+
<button
|
|
217
|
+
type="button"
|
|
218
|
+
class="mint-jobs-tray__icon-button"
|
|
219
|
+
aria-label="Close jobs"
|
|
220
|
+
@click="closeTray"
|
|
221
|
+
>
|
|
222
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
|
|
223
|
+
<path d="m6 6 12 12M18 6 6 18" />
|
|
224
|
+
</svg>
|
|
225
|
+
</button>
|
|
226
|
+
</div>
|
|
227
|
+
</header>
|
|
228
|
+
|
|
229
|
+
<div class="mint-jobs-tray__tabs" role="tablist" aria-label="Job groups">
|
|
230
|
+
<button
|
|
231
|
+
type="button"
|
|
232
|
+
role="tab"
|
|
233
|
+
data-tab="active"
|
|
234
|
+
class="mint-jobs-tray__tab"
|
|
235
|
+
:class="{ 'mint-jobs-tray__tab--active': tray.activeTab.value === 'active' }"
|
|
236
|
+
:aria-selected="tray.activeTab.value === 'active'"
|
|
237
|
+
@click="tray.activeTab.value = 'active'"
|
|
238
|
+
>
|
|
239
|
+
Active
|
|
240
|
+
<span class="mint-jobs-tray__tab-count">{{ tray.activeJobs.value.length }}</span>
|
|
241
|
+
</button>
|
|
242
|
+
<button
|
|
243
|
+
type="button"
|
|
244
|
+
role="tab"
|
|
245
|
+
data-tab="history"
|
|
246
|
+
class="mint-jobs-tray__tab"
|
|
247
|
+
:class="{ 'mint-jobs-tray__tab--active': tray.activeTab.value === 'history' }"
|
|
248
|
+
:aria-selected="tray.activeTab.value === 'history'"
|
|
249
|
+
@click="tray.activeTab.value = 'history'"
|
|
250
|
+
>
|
|
251
|
+
History
|
|
252
|
+
<span class="mint-jobs-tray__tab-count">{{ tray.historyJobs.value.length }}</span>
|
|
253
|
+
</button>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<p v-if="tray.error.value" class="mint-jobs-tray__error" role="alert">
|
|
257
|
+
{{ tray.error.value }}
|
|
258
|
+
</p>
|
|
259
|
+
|
|
260
|
+
<div class="mint-jobs-tray__body">
|
|
261
|
+
<template v-if="tray.activeTab.value === 'active'">
|
|
262
|
+
<p v-if="tray.activeJobs.value.length === 0" class="mint-jobs-tray__empty">
|
|
263
|
+
No active jobs
|
|
264
|
+
</p>
|
|
265
|
+
<article
|
|
266
|
+
v-for="job in tray.activeJobs.value"
|
|
267
|
+
:key="job.job_id"
|
|
268
|
+
class="mint-jobs-tray__job mint-jobs-tray__job--active"
|
|
269
|
+
>
|
|
270
|
+
<div class="mint-jobs-tray__job-heading">
|
|
271
|
+
<div class="mint-jobs-tray__job-identity">
|
|
272
|
+
<button
|
|
273
|
+
type="button"
|
|
274
|
+
class="mint-jobs-tray__job-select"
|
|
275
|
+
@click="emit('select', job)"
|
|
276
|
+
>
|
|
277
|
+
{{ job.name }}
|
|
278
|
+
</button>
|
|
279
|
+
<span class="mint-jobs-tray__kind">{{ job.kind }}</span>
|
|
280
|
+
</div>
|
|
281
|
+
<span class="mint-jobs-tray__status" :class="statusClass(job)">
|
|
282
|
+
{{ jobStatusLabel(job.status) }}
|
|
283
|
+
</span>
|
|
284
|
+
</div>
|
|
285
|
+
<div
|
|
286
|
+
v-if="job.status === 'running'"
|
|
287
|
+
class="mint-jobs-tray__progress"
|
|
288
|
+
role="progressbar"
|
|
289
|
+
aria-label="Job progress"
|
|
290
|
+
aria-valuemin="0"
|
|
291
|
+
aria-valuemax="100"
|
|
292
|
+
:aria-valuenow="normalizeJobPercent(job.progress.percent)"
|
|
293
|
+
>
|
|
294
|
+
<span :style="{ width: `${normalizeJobPercent(job.progress.percent)}%` }" />
|
|
295
|
+
</div>
|
|
296
|
+
<div class="mint-jobs-tray__job-footer">
|
|
297
|
+
<span>{{ progressText(job) }}</span>
|
|
298
|
+
<div class="mint-jobs-tray__job-actions">
|
|
299
|
+
<button
|
|
300
|
+
v-if="job.can_cancel && adapter?.cancelJob"
|
|
301
|
+
type="button"
|
|
302
|
+
class="mint-jobs-tray__action mint-jobs-tray__action--danger"
|
|
303
|
+
aria-label="Cancel job"
|
|
304
|
+
:disabled="tray.isPending(job)"
|
|
305
|
+
@click="cancelJob(job)"
|
|
306
|
+
>
|
|
307
|
+
Cancel
|
|
308
|
+
</button>
|
|
309
|
+
<button
|
|
310
|
+
v-if="job.can_delete && adapter?.deleteJob"
|
|
311
|
+
type="button"
|
|
312
|
+
class="mint-jobs-tray__icon-button mint-jobs-tray__icon-button--danger"
|
|
313
|
+
aria-label="Delete job"
|
|
314
|
+
:disabled="tray.isPending(job)"
|
|
315
|
+
@click="deleteJob(job)"
|
|
316
|
+
>
|
|
317
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
|
|
318
|
+
<path d="M4 7h16M9 7V4h6v3m-8 0 1 13h8l1-13M10 11v5M14 11v5" />
|
|
319
|
+
</svg>
|
|
320
|
+
</button>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
</article>
|
|
324
|
+
</template>
|
|
325
|
+
|
|
326
|
+
<template v-else>
|
|
327
|
+
<div v-if="canClear" class="mint-jobs-tray__clear-row">
|
|
328
|
+
<button
|
|
329
|
+
type="button"
|
|
330
|
+
class="mint-jobs-tray__action mint-jobs-tray__action--danger"
|
|
331
|
+
@click="clearFinished"
|
|
332
|
+
>
|
|
333
|
+
Clear finished
|
|
334
|
+
</button>
|
|
335
|
+
</div>
|
|
336
|
+
<p v-if="tray.historyJobs.value.length === 0" class="mint-jobs-tray__empty">
|
|
337
|
+
No job history
|
|
338
|
+
</p>
|
|
339
|
+
<article
|
|
340
|
+
v-for="job in tray.historyJobs.value"
|
|
341
|
+
:key="job.job_id"
|
|
342
|
+
class="mint-jobs-tray__job"
|
|
343
|
+
>
|
|
344
|
+
<div class="mint-jobs-tray__job-heading">
|
|
345
|
+
<div class="mint-jobs-tray__job-identity">
|
|
346
|
+
<button
|
|
347
|
+
type="button"
|
|
348
|
+
class="mint-jobs-tray__job-select"
|
|
349
|
+
@click="emit('select', job)"
|
|
350
|
+
>
|
|
351
|
+
{{ job.name }}
|
|
352
|
+
</button>
|
|
353
|
+
<span class="mint-jobs-tray__kind">{{ job.kind }}</span>
|
|
354
|
+
</div>
|
|
355
|
+
<span class="mint-jobs-tray__status" :class="statusClass(job)">
|
|
356
|
+
{{ jobStatusLabel(job.status) }}
|
|
357
|
+
</span>
|
|
358
|
+
</div>
|
|
359
|
+
<div class="mint-jobs-tray__job-footer">
|
|
360
|
+
<span>{{ formatTimestamp(job.completed_at || job.created_at) }}</span>
|
|
361
|
+
<button
|
|
362
|
+
v-if="job.can_delete && adapter?.deleteJob"
|
|
363
|
+
type="button"
|
|
364
|
+
class="mint-jobs-tray__icon-button mint-jobs-tray__icon-button--danger"
|
|
365
|
+
aria-label="Delete job"
|
|
366
|
+
:disabled="tray.isPending(job)"
|
|
367
|
+
@click="deleteJob(job)"
|
|
368
|
+
>
|
|
369
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
|
|
370
|
+
<path d="M4 7h16M9 7V4h6v3m-8 0 1 13h8l1-13M10 11v5M14 11v5" />
|
|
371
|
+
</svg>
|
|
372
|
+
</button>
|
|
373
|
+
</div>
|
|
374
|
+
</article>
|
|
375
|
+
</template>
|
|
376
|
+
</div>
|
|
377
|
+
</section>
|
|
378
|
+
</Transition>
|
|
379
|
+
</div>
|
|
380
|
+
</Teleport>
|
|
381
|
+
</template>
|
|
382
|
+
|
|
383
|
+
<style>
|
|
384
|
+
@import '../styles/components/jobs-status-tray.css';
|
|
385
|
+
</style>
|
package/src/components/index.ts
CHANGED
|
@@ -108,6 +108,7 @@ export { default as UnitInput } from './UnitInput.vue'
|
|
|
108
108
|
export { default as StepWizard } from './StepWizard.vue'
|
|
109
109
|
export { default as AuditTrail } from './AuditTrail.vue'
|
|
110
110
|
export { default as BatchProgressList } from './BatchProgressList.vue'
|
|
111
|
+
export { default as JobsStatusTray } from './JobsStatusTray.vue'
|
|
111
112
|
|
|
112
113
|
// Form builder components
|
|
113
114
|
export { default as FormBuilder } from './FormBuilder.vue'
|
package/src/composables/index.ts
CHANGED
|
@@ -468,3 +468,11 @@ export {
|
|
|
468
468
|
type UsePluginSettingsOptions,
|
|
469
469
|
type UsePluginSettingsReturn,
|
|
470
470
|
} from './usePluginClient'
|
|
471
|
+
export {
|
|
472
|
+
useJobsStatusTray,
|
|
473
|
+
type JobsStatusTrayAdapter,
|
|
474
|
+
type JobsStatusTrayTab,
|
|
475
|
+
type JobStreamMessage,
|
|
476
|
+
type UseJobsStatusTrayOptions,
|
|
477
|
+
type UseJobsStatusTrayReturn,
|
|
478
|
+
} from './useJobsStatusTray'
|