@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,13 +1,13 @@
|
|
|
1
|
-
import { mount } from '@vue/test-utils'
|
|
1
|
+
import { flushPromises, mount } from '@vue/test-utils'
|
|
2
2
|
import { defineComponent, nextTick, shallowRef } from 'vue'
|
|
3
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
combinePluginJobSources,
|
|
7
7
|
useJobCenter,
|
|
8
8
|
type UseJobCenterOptions,
|
|
9
9
|
} from '../../composables/useJobCenter'
|
|
10
|
-
import type { JobStateInput, JobStatus } from '../../types/jobs'
|
|
10
|
+
import type { JobStateInput, JobStatus, TopBarJob } from '../../types/jobs'
|
|
11
11
|
|
|
12
12
|
function job(status: JobStatus, id: string = status): JobStateInput {
|
|
13
13
|
return {
|
|
@@ -281,3 +281,223 @@ describe('useJobCenter', () => {
|
|
|
281
281
|
expect(tray.deletableJobs.value).toEqual([])
|
|
282
282
|
})
|
|
283
283
|
})
|
|
284
|
+
|
|
285
|
+
describe('useJobCenter top-bar chip', () => {
|
|
286
|
+
const platformWindow = window as unknown as { __MINT_PLATFORM__?: unknown }
|
|
287
|
+
function integrateAs(userId: number, pluginId = 'peak-qc') {
|
|
288
|
+
platformWindow.__MINT_PLATFORM__ = { user: { id: userId }, plugin: { id: pluginId } }
|
|
289
|
+
}
|
|
290
|
+
const owned = (status: JobStatus, id: string, owner: string | null, user = owner): JobStateInput => ({
|
|
291
|
+
...job(status, id), owner_user_id: owner, user_id: user,
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
beforeEach(() => localStorage.clear())
|
|
295
|
+
afterEach(() => {
|
|
296
|
+
delete platformWindow.__MINT_PLATFORM__
|
|
297
|
+
vi.useRealTimers()
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it.each<[string, JobStateInput[], TopBarJob | null]>([
|
|
301
|
+
['an awaiting-input job first', [job('running', 'r'), job('awaiting_input', 'a')],
|
|
302
|
+
{ status: 'awaiting_input', label: 'a waiting for input', percent: null }],
|
|
303
|
+
['the first active job', [job('queued', 'q'), job('running', 'r')],
|
|
304
|
+
{ status: 'queued', label: 'q queued', percent: null }],
|
|
305
|
+
['the progress message and percent of a running job',
|
|
306
|
+
[{ ...job('running', 'r'), progress: { ...job('running', 'r').progress, message: 'Integrating' } }],
|
|
307
|
+
{ status: 'running', label: 'Integrating', percent: 42 }],
|
|
308
|
+
['nothing without active, held or failed jobs', [job('completed', 'c'), job('cancelled', 'x')], null],
|
|
309
|
+
])('should show %s when the viewer runs jobs', (_case, jobs, expected) => {
|
|
310
|
+
const { tray } = mountComposable({ source: { jobs }, loadOnMount: false })
|
|
311
|
+
expect(tray.topBarJob.value).toEqual(expected)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it('should show only the viewer own jobs when the page is integrated', () => {
|
|
315
|
+
integrateAs(1)
|
|
316
|
+
const { tray } = mountComposable({
|
|
317
|
+
source: { jobs: [owned('running', 'theirs', '2'), owned('queued', 'mine', null, '1')] },
|
|
318
|
+
loadOnMount: false,
|
|
319
|
+
})
|
|
320
|
+
expect(tray.topBarJob.value).toMatchObject({ status: 'queued', label: 'mine queued' })
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it.each([
|
|
324
|
+
['auth is off', false, 'running'],
|
|
325
|
+
['auth is required', true, undefined],
|
|
326
|
+
])('should treat jobs of an integrated page without a signed-in user as own only when %s', (_case, authRequired, status) => {
|
|
327
|
+
platformWindow.__MINT_PLATFORM__ = { user: null, plugin: { id: 'peak-qc' }, authRequired }
|
|
328
|
+
const { tray } = mountComposable({ source: { jobs: [owned('running', 'theirs', '2')] }, loadOnMount: false })
|
|
329
|
+
expect(tray.topBarJob.value?.status).toBe(status)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('should hold a completed job when several jobs leave the active list in one update', async () => {
|
|
333
|
+
const jobs = shallowRef([job('running', 'a'), job('running', 'b')])
|
|
334
|
+
const { tray } = mountComposable({ source: { jobs }, loadOnMount: false })
|
|
335
|
+
|
|
336
|
+
jobs.value = [job('failed', 'a'), job('completed', 'b')]
|
|
337
|
+
await nextTick()
|
|
338
|
+
|
|
339
|
+
expect(tray.topBarJob.value).toEqual({ status: 'completed', label: 'b completed' })
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
it('should close the open tray instead of selecting the held job when the chip is clicked', async () => {
|
|
343
|
+
const jobs = shallowRef([job('running', 'r')])
|
|
344
|
+
const { tray } = mountComposable({ source: { jobs }, loadOnMount: false })
|
|
345
|
+
const onSelect = vi.fn()
|
|
346
|
+
jobs.value = [job('completed', 'r')]
|
|
347
|
+
await nextTick()
|
|
348
|
+
tray.open()
|
|
349
|
+
|
|
350
|
+
tray.handleTopBarClick(onSelect)
|
|
351
|
+
|
|
352
|
+
expect([tray.isOpen.value, onSelect.mock.calls.length]).toEqual([false, 0])
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('should drop the held job when it leaves the job list', async () => {
|
|
356
|
+
const jobs = shallowRef([job('running', 'r')])
|
|
357
|
+
const { tray } = mountComposable({ source: { jobs }, loadOnMount: false })
|
|
358
|
+
const onSelect = vi.fn()
|
|
359
|
+
jobs.value = [job('completed', 'r')]
|
|
360
|
+
await nextTick()
|
|
361
|
+
jobs.value = []
|
|
362
|
+
await nextTick()
|
|
363
|
+
|
|
364
|
+
tray.handleTopBarClick(onSelect)
|
|
365
|
+
|
|
366
|
+
expect([tray.topBarJob.value, onSelect.mock.calls.length, tray.isOpen.value]).toEqual([null, 0, true])
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it('should mark a failure seen when the job fails while the tray is open', async () => {
|
|
370
|
+
localStorage.setItem('mint.jobs.standalone.seen-failed', '[]')
|
|
371
|
+
const jobs = shallowRef([job('running', 'r')])
|
|
372
|
+
const { tray } = mountComposable({ source: { jobs }, loadOnMount: false })
|
|
373
|
+
tray.open()
|
|
374
|
+
await nextTick()
|
|
375
|
+
|
|
376
|
+
jobs.value = [job('failed', 'r')]
|
|
377
|
+
await nextTick()
|
|
378
|
+
tray.close()
|
|
379
|
+
await nextTick()
|
|
380
|
+
|
|
381
|
+
expect(tray.topBarJob.value).toBeNull()
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it.each([
|
|
385
|
+
['an empty list', [] as JobStateInput[], { status: 'failed', label: '1 failed' }],
|
|
386
|
+
['an old failure', [job('failed', 'old')], { status: 'failed', label: '1 failed' }],
|
|
387
|
+
])('should take the baseline when the initial load returns %s', async (_case, loaded, later) => {
|
|
388
|
+
const jobs = shallowRef<JobStateInput[]>([])
|
|
389
|
+
const { tray } = mountComposable({ source: { jobs, refresh: async () => { jobs.value = loaded } } })
|
|
390
|
+
await flushPromises()
|
|
391
|
+
const baseline = tray.topBarJob.value
|
|
392
|
+
|
|
393
|
+
jobs.value = [...loaded, job('failed', 'new')]
|
|
394
|
+
await nextTick()
|
|
395
|
+
|
|
396
|
+
expect([baseline, tray.topBarJob.value]).toEqual([null, later])
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
it('should not hold a job when the source is replaced by one where it already completed', async () => {
|
|
400
|
+
const source = shallowRef({ jobs: [job('running', 'x')] })
|
|
401
|
+
const { tray } = mountComposable({ source, loadOnMount: false })
|
|
402
|
+
|
|
403
|
+
source.value = { jobs: [job('completed', 'x')] }
|
|
404
|
+
await nextTick()
|
|
405
|
+
|
|
406
|
+
expect(tray.topBarJob.value).toBeNull()
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('should alert only failures after the first job list when nothing was stored', async () => {
|
|
410
|
+
const jobs = shallowRef<JobStateInput[]>([])
|
|
411
|
+
const { tray } = mountComposable({ source: { jobs }, loadOnMount: false })
|
|
412
|
+
|
|
413
|
+
jobs.value = [job('failed', 'old')]
|
|
414
|
+
await nextTick()
|
|
415
|
+
const baseline = tray.topBarJob.value
|
|
416
|
+
jobs.value = [job('failed', 'old'), job('failed', 'new')]
|
|
417
|
+
await nextTick()
|
|
418
|
+
|
|
419
|
+
expect({
|
|
420
|
+
baseline,
|
|
421
|
+
later: tray.topBarJob.value,
|
|
422
|
+
stored: JSON.parse(localStorage.getItem('mint.jobs.standalone.seen-failed') ?? 'null'),
|
|
423
|
+
}).toEqual({ baseline: null, later: { status: 'failed', label: '1 failed' }, stored: ['old'] })
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
it('should hold a just-completed job for 6 s and select it on click when nothing else runs', async () => {
|
|
427
|
+
vi.useFakeTimers()
|
|
428
|
+
const jobs = shallowRef([job('running', 'r')])
|
|
429
|
+
const { tray } = mountComposable({ source: { jobs }, loadOnMount: false })
|
|
430
|
+
const onSelect = vi.fn()
|
|
431
|
+
|
|
432
|
+
jobs.value = [{ ...job('completed', 'r') }]
|
|
433
|
+
await nextTick()
|
|
434
|
+
const held = tray.topBarJob.value
|
|
435
|
+
tray.handleTopBarClick(onSelect)
|
|
436
|
+
vi.advanceTimersByTime(5999)
|
|
437
|
+
const stillHeld = tray.topBarJob.value
|
|
438
|
+
vi.advanceTimersByTime(1)
|
|
439
|
+
tray.handleTopBarClick(onSelect)
|
|
440
|
+
|
|
441
|
+
expect({
|
|
442
|
+
held,
|
|
443
|
+
stillHeld: stillHeld?.status,
|
|
444
|
+
after: tray.topBarJob.value,
|
|
445
|
+
selected: onSelect.mock.calls.map(([selected]) => selected.job_id),
|
|
446
|
+
open: tray.isOpen.value,
|
|
447
|
+
}).toEqual({
|
|
448
|
+
held: { status: 'completed', label: 'r completed' },
|
|
449
|
+
stillHeld: 'completed',
|
|
450
|
+
after: null,
|
|
451
|
+
selected: ['r'],
|
|
452
|
+
open: true,
|
|
453
|
+
})
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
it.each([
|
|
457
|
+
['standalone', () => undefined, 'mint.jobs.standalone.seen-failed'],
|
|
458
|
+
['integrated', () => integrateAs(1), 'mint.jobs.peak-qc.seen-failed'],
|
|
459
|
+
])('should count unseen failures until the tray opens when %s', async (_mode, setup, key) => {
|
|
460
|
+
setup()
|
|
461
|
+
localStorage.setItem(key, '[]')
|
|
462
|
+
const jobs = [owned('failed', 'f1', '1'), owned('failed', 'f2', '1'), owned('failed', 'other', '2')]
|
|
463
|
+
const { tray } = mountComposable({ source: { jobs }, loadOnMount: false })
|
|
464
|
+
const before = tray.topBarJob.value
|
|
465
|
+
|
|
466
|
+
tray.open()
|
|
467
|
+
await nextTick()
|
|
468
|
+
const reopened = mountComposable({ source: { jobs }, loadOnMount: false }).tray
|
|
469
|
+
|
|
470
|
+
expect({
|
|
471
|
+
before,
|
|
472
|
+
after: tray.topBarJob.value,
|
|
473
|
+
reopened: reopened.topBarJob.value,
|
|
474
|
+
stored: JSON.parse(localStorage.getItem(key) ?? 'null'),
|
|
475
|
+
}).toEqual({
|
|
476
|
+
before: { status: 'failed', label: _mode === 'standalone' ? '3 failed' : '2 failed' },
|
|
477
|
+
after: null,
|
|
478
|
+
reopened: null,
|
|
479
|
+
stored: _mode === 'standalone' ? ['f1', 'f2', 'other'] : ['f1', 'f2'],
|
|
480
|
+
})
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
it.each<[string, TopBarJob | null, TopBarJob]>([
|
|
484
|
+
['replace the default mapping', { status: 'running', label: 'Decoding in the browser', percent: 10 },
|
|
485
|
+
{ status: 'running', label: 'Decoding in the browser', percent: 10 }],
|
|
486
|
+
['keep the default mapping', null, { status: 'awaiting_input', label: 'a waiting for input', percent: null }],
|
|
487
|
+
])('should %s when the topBarJob option returns that value', (_case, override, expected) => {
|
|
488
|
+
const { tray } = mountComposable({
|
|
489
|
+
source: { jobs: [job('awaiting_input', 'a')] },
|
|
490
|
+
loadOnMount: false,
|
|
491
|
+
topBarJob: () => override,
|
|
492
|
+
})
|
|
493
|
+
expect(tray.topBarJob.value).toEqual(expected)
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
it('should count only completed jobs when the list holds finished and active jobs', () => {
|
|
497
|
+
const { tray } = mountComposable({
|
|
498
|
+
source: { jobs: [job('completed', 'c1'), job('completed', 'c2'), job('failed', 'f'), job('running', 'r')] },
|
|
499
|
+
loadOnMount: false,
|
|
500
|
+
})
|
|
501
|
+
expect(tray.completedCount.value).toBe(2)
|
|
502
|
+
})
|
|
503
|
+
})
|
|
@@ -266,13 +266,12 @@ describe('frontend docs catalog coverage', () => {
|
|
|
266
266
|
})
|
|
267
267
|
})
|
|
268
268
|
|
|
269
|
-
it('documents only the
|
|
269
|
+
it('documents only the center-driven JobsStatusTray props', () => {
|
|
270
270
|
const manifest = extractFrontendManifest()
|
|
271
271
|
const jobsStatusTray = manifest.categories.components.find(item => item.name === 'JobsStatusTray')
|
|
272
272
|
|
|
273
273
|
expect(jobsStatusTray?.props?.map(prop => prop.name).sort()).toEqual([
|
|
274
|
-
'
|
|
275
|
-
'source',
|
|
274
|
+
'center',
|
|
276
275
|
'teleportTo',
|
|
277
276
|
'title',
|
|
278
277
|
])
|
|
@@ -375,6 +374,7 @@ describe('frontend docs catalog coverage', () => {
|
|
|
375
374
|
'usePluginJobs',
|
|
376
375
|
'useGeneratedAnalysis',
|
|
377
376
|
'usePluginJobCenter',
|
|
377
|
+
'JobsStatusTrayTab',
|
|
378
378
|
'useRequestSyncState',
|
|
379
379
|
'useAnalysisArtifacts',
|
|
380
380
|
'useTimeUtils',
|
|
@@ -20,6 +20,14 @@ function rule(text: string, selector: string): string {
|
|
|
20
20
|
const rem = (value: string): number => Number(value.replace('rem', '')) * 16
|
|
21
21
|
|
|
22
22
|
describe('v2 audit style rules', () => {
|
|
23
|
+
it('should stop the page bounce in a tab vertically and in a PWA on both axes', () => {
|
|
24
|
+
const variables = css('variables.css')
|
|
25
|
+
expect(rule(variables, 'html,\nbody')).toMatch(/overscroll-behavior-y:\s*none/)
|
|
26
|
+
expect(variables).toMatch(
|
|
27
|
+
/@media \(display-mode: standalone\)\s*\{\s*html,\s*body\s*\{\s*overscroll-behavior:\s*none;\s*\}/,
|
|
28
|
+
)
|
|
29
|
+
})
|
|
30
|
+
|
|
23
31
|
it('should keep the outermost cards at least 16px from the window edge', () => {
|
|
24
32
|
const inset = /--mint-page-inset:\s*([\d.]+rem)/.exec(css('variables.css'))?.[1] ?? '0rem'
|
|
25
33
|
expect(rem(inset)).toBeGreaterThanOrEqual(16)
|
|
@@ -49,10 +49,18 @@ interface Props {
|
|
|
49
49
|
* `null` shows the idle Jobs button. Both emit `job-click`.
|
|
50
50
|
*/
|
|
51
51
|
job?: TopBarJob | null
|
|
52
|
+
/** Completed jobs, shown as a small count on the idle Jobs button. Hidden when 0. */
|
|
53
|
+
jobsDone?: number
|
|
54
|
+
/** Whether the jobs panel is open; `aria-expanded` of the job control. */
|
|
55
|
+
jobsOpen?: boolean
|
|
56
|
+
/** DOM id of the jobs panel; `aria-controls` of the job control. */
|
|
57
|
+
jobsPanelId?: string
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
const props = withDefaults(defineProps<Props>(), {
|
|
55
61
|
spec: () => ({}),
|
|
62
|
+
// No boolean cast: without a jobs panel the control has no aria-expanded.
|
|
63
|
+
jobsOpen: undefined,
|
|
56
64
|
})
|
|
57
65
|
|
|
58
66
|
const config = computed(() => applySpecDefaults({
|
|
@@ -377,6 +385,8 @@ const normalizedSettingsTabs = computed<SettingsTab[]>(() =>
|
|
|
377
385
|
:class="['mint-topbar__job', `mint-topbar__job--${JOB_TONES[job.status]}`]"
|
|
378
386
|
:title="jobName"
|
|
379
387
|
:aria-label="jobName"
|
|
388
|
+
:aria-expanded="jobsOpen"
|
|
389
|
+
:aria-controls="jobsPanelId"
|
|
380
390
|
@click="emit('job-click')"
|
|
381
391
|
>
|
|
382
392
|
<component
|
|
@@ -394,10 +404,14 @@ const normalizedSettingsTabs = computed<SettingsTab[]>(() =>
|
|
|
394
404
|
key="idle"
|
|
395
405
|
type="button"
|
|
396
406
|
class="mint-topbar__job mint-topbar__job--idle"
|
|
407
|
+
:aria-label="jobsDone ? `Jobs, ${jobsDone} completed` : undefined"
|
|
408
|
+
:aria-expanded="jobsOpen"
|
|
409
|
+
:aria-controls="jobsPanelId"
|
|
397
410
|
@click="emit('job-click')"
|
|
398
411
|
>
|
|
399
412
|
<ListChecks class="mint-topbar__job-icon" :size="14" aria-hidden="true" />
|
|
400
413
|
<span class="mint-topbar__job-label">Jobs</span>
|
|
414
|
+
<span v-if="jobsDone" class="mint-topbar__job-count" aria-hidden="true">{{ jobsDone }}</span>
|
|
401
415
|
</button>
|
|
402
416
|
</Transition>
|
|
403
417
|
</div>
|
|
@@ -12,11 +12,11 @@ import { computed, onMounted, provide, reactive, ref, watch } from 'vue'
|
|
|
12
12
|
import { generatedJobDefaults, generatedJobFormSchema, normalizeGeneratedJobInput } from '../generated/schema'
|
|
13
13
|
import type { FormSchema } from '../types/form-builder'
|
|
14
14
|
import type {
|
|
15
|
-
GeneratedAnalysisJob,
|
|
16
15
|
GeneratedAnalysisResult,
|
|
17
16
|
GeneratedJobDefinition,
|
|
18
17
|
} from '../types/generated-ui'
|
|
19
18
|
import type { PillNavItem, SidebarToolSection } from '../types'
|
|
19
|
+
import type { JobState } from '../types/jobs'
|
|
20
20
|
import {
|
|
21
21
|
useJobsEndpoint,
|
|
22
22
|
type GeneratedAnalysisTransport,
|
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
import AlertBox from './AlertBox.vue'
|
|
25
25
|
import EmptyState from './EmptyState.vue'
|
|
26
26
|
import GeneratedResultRenderer from './GeneratedResultRenderer.vue'
|
|
27
|
-
import JobsStatusTray from './JobsStatusTray.vue'
|
|
28
27
|
import LoadingSpinner from './LoadingSpinner.vue'
|
|
29
28
|
import PluginWorkspaceView from './PluginWorkspaceView.vue'
|
|
30
29
|
import { GENERATED_JOBS_API_KEY } from './internal/generatedJobsApi'
|
|
@@ -84,6 +83,7 @@ const workspace = computed(() => definePluginWorkspaceView({
|
|
|
84
83
|
standaloneLabel: props.showStandaloneLabel ? 'Standalone' : false,
|
|
85
84
|
},
|
|
86
85
|
sidebar: { title: 'Parameters', panels: panels.value, forms: forms.value },
|
|
86
|
+
jobs: generated,
|
|
87
87
|
}))
|
|
88
88
|
|
|
89
89
|
// Seeds the standalone top bar icon; the title and subtitle above win over the seeded text.
|
|
@@ -137,7 +137,7 @@ async function submitJob(sectionId: string, values: Record<string, unknown>): Pr
|
|
|
137
137
|
await generated.submit(definition.id, normalizeGeneratedJobInput(definition, values))
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
function selectJob(job:
|
|
140
|
+
function selectJob(job: JobState): void {
|
|
141
141
|
selectedJobId.value = job.job_id
|
|
142
142
|
activeDefinitionId.value = String(job.metadata.definition_id ?? activeDefinitionId.value)
|
|
143
143
|
}
|
|
@@ -166,6 +166,7 @@ onMounted(() => {
|
|
|
166
166
|
:form-disabled="generated.isSubmitting.value"
|
|
167
167
|
@update:values="updateValues"
|
|
168
168
|
@form-submit="submitJob"
|
|
169
|
+
@job-select="selectJob"
|
|
169
170
|
>
|
|
170
171
|
<main class="mint-generated-workspace">
|
|
171
172
|
<div v-if="generated.isLoading.value" class="mint-generated-workspace__loading">
|
|
@@ -200,12 +201,6 @@ onMounted(() => {
|
|
|
200
201
|
/>
|
|
201
202
|
</section>
|
|
202
203
|
</template>
|
|
203
|
-
|
|
204
|
-
<JobsStatusTray
|
|
205
|
-
:source="generated"
|
|
206
|
-
:teleport-to="false"
|
|
207
|
-
@select="selectJob"
|
|
208
|
-
/>
|
|
209
204
|
</main>
|
|
210
205
|
</PluginWorkspaceView>
|
|
211
206
|
</template>
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { defineComponent, h, onMounted, type PropType } from 'vue'
|
|
3
|
+
import AppTopBar from './AppTopBar.vue'
|
|
2
4
|
import JobsStatusTray from './JobsStatusTray.vue'
|
|
3
|
-
import
|
|
5
|
+
import { useJobCenter } from '../composables/useJobCenter'
|
|
6
|
+
import type { JobState, JobStateInput, JobStatus } from '../types/jobs'
|
|
4
7
|
|
|
5
8
|
function job(
|
|
6
9
|
status: JobStatus,
|
|
@@ -43,13 +46,6 @@ function job(
|
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
48
|
|
|
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
49
|
const explicitActionJobs = [
|
|
54
50
|
job('awaiting_input', 'J-201', { can_cancel: true }),
|
|
55
51
|
job('completed', 'J-200', { can_delete: false }),
|
|
@@ -57,69 +53,108 @@ const explicitActionJobs = [
|
|
|
57
53
|
const cancelJob = async () => undefined
|
|
58
54
|
const deleteJob = async () => true
|
|
59
55
|
|
|
60
|
-
function openJob(
|
|
61
|
-
console.info('open',
|
|
56
|
+
function openJob(selected: JobState) {
|
|
57
|
+
console.info('open', selected.job_id)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The parent owns the job center: it feeds the top-bar chip and the tray. */
|
|
61
|
+
const TrayDemo = defineComponent({
|
|
62
|
+
props: {
|
|
63
|
+
jobs: { type: Array as PropType<JobStateInput[]>, required: true },
|
|
64
|
+
title: { type: String, default: undefined },
|
|
65
|
+
open: Boolean,
|
|
66
|
+
},
|
|
67
|
+
setup(props) {
|
|
68
|
+
// One source object: a new source identity resets the just-completed hold.
|
|
69
|
+
const center = useJobCenter({
|
|
70
|
+
source: { jobs: () => props.jobs, cancel: cancelJob, delete: deleteJob },
|
|
71
|
+
})
|
|
72
|
+
onMounted(() => { if (props.open) center.open() })
|
|
73
|
+
return () => [
|
|
74
|
+
h(AppTopBar, {
|
|
75
|
+
title: 'Peak QC',
|
|
76
|
+
job: center.topBarJob.value,
|
|
77
|
+
jobsDone: center.completedCount.value,
|
|
78
|
+
jobsOpen: center.isOpen.value,
|
|
79
|
+
jobsPanelId: center.panelId,
|
|
80
|
+
onJobClick: () => center.handleTopBarClick(openJob),
|
|
81
|
+
}),
|
|
82
|
+
h(JobsStatusTray, { center, title: props.title, onSelect: openJob }, {
|
|
83
|
+
'job-actions': ({ job: row }: { job: JobState }) => row.status === 'completed'
|
|
84
|
+
? h('button', { type: 'button', class: 'mint-jobs-tray__section-action', onClick: () => openJob(row) }, 'Open')
|
|
85
|
+
: null,
|
|
86
|
+
}),
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
function playgroundJobs(percent: number, finished: boolean): JobStateInput[] {
|
|
92
|
+
const running = finished
|
|
93
|
+
? job('completed', 'J-103', { completed_at: '2026-07-14T10:12:00+00:00' })
|
|
94
|
+
: job('running', 'J-103', {
|
|
95
|
+
progress: { ...job('running', 'J-103').progress, percent, message: 'Integrating peaks' },
|
|
96
|
+
})
|
|
97
|
+
return [
|
|
98
|
+
job('awaiting_input', 'J-104'),
|
|
99
|
+
running,
|
|
100
|
+
job('queued', 'J-102'),
|
|
101
|
+
job('completed', 'J-101'),
|
|
102
|
+
job('failed', 'J-100'),
|
|
103
|
+
]
|
|
62
104
|
}
|
|
63
105
|
|
|
64
106
|
function initState() {
|
|
65
107
|
return {
|
|
66
|
-
title: '
|
|
67
|
-
|
|
108
|
+
title: 'Jobs',
|
|
109
|
+
percent: 37,
|
|
110
|
+
finished: false,
|
|
111
|
+
open: true,
|
|
68
112
|
}
|
|
69
113
|
}
|
|
70
114
|
</script>
|
|
71
115
|
|
|
72
116
|
<template>
|
|
73
|
-
<Story title="Workflow/JobsStatusTray">
|
|
117
|
+
<Story title="Workflow/JobsStatusTray" :layout="{ type: 'single', iframe: true }">
|
|
74
118
|
<Variant title="Playground" :init-state="initState">
|
|
75
119
|
<template #default="{ state }">
|
|
76
|
-
<div style="
|
|
77
|
-
<
|
|
120
|
+
<div style="min-height: 560px;">
|
|
121
|
+
<TrayDemo
|
|
122
|
+
:key="String(state.open)"
|
|
123
|
+
:jobs="playgroundJobs(state.percent, state.finished)"
|
|
78
124
|
:title="state.title"
|
|
79
|
-
:
|
|
80
|
-
|
|
81
|
-
:teleport-to="false"
|
|
82
|
-
>
|
|
83
|
-
<template #job-actions="{ job }">
|
|
84
|
-
<button type="button" class="mint-jobs-tray__action" @click="() => openJob(job.job_id)">
|
|
85
|
-
Open
|
|
86
|
-
</button>
|
|
87
|
-
</template>
|
|
88
|
-
</JobsStatusTray>
|
|
125
|
+
:open="state.open"
|
|
126
|
+
/>
|
|
89
127
|
</div>
|
|
90
128
|
</template>
|
|
91
129
|
<template #controls="{ state }">
|
|
92
130
|
<HstText v-model="state.title" title="Title" />
|
|
131
|
+
<HstSlider v-model="state.percent" title="Running percent" :min="0" :max="100" />
|
|
132
|
+
<HstCheckbox v-model="state.finished" title="Running job completed (6 s chip)" />
|
|
133
|
+
<HstCheckbox v-model="state.open" title="Open on mount" />
|
|
93
134
|
</template>
|
|
94
135
|
</Variant>
|
|
95
136
|
|
|
96
137
|
<Variant title="Awaiting input">
|
|
97
|
-
<div style="
|
|
98
|
-
<
|
|
99
|
-
:source="{ jobs: [job('awaiting_input', 'J-301')] }"
|
|
100
|
-
:load-on-mount="false"
|
|
101
|
-
:teleport-to="false"
|
|
102
|
-
/>
|
|
138
|
+
<div style="min-height: 560px;">
|
|
139
|
+
<TrayDemo :jobs="[job('awaiting_input', 'J-301')]" open />
|
|
103
140
|
</div>
|
|
104
141
|
</Variant>
|
|
105
142
|
|
|
106
143
|
<Variant title="Explicit actions">
|
|
107
|
-
<div style="
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
144
|
+
<div style="min-height: 560px;">
|
|
145
|
+
<TrayDemo :jobs="explicitActionJobs" open />
|
|
146
|
+
</div>
|
|
147
|
+
</Variant>
|
|
148
|
+
|
|
149
|
+
<Variant title="Idle with completed jobs">
|
|
150
|
+
<div style="min-height: 560px;">
|
|
151
|
+
<TrayDemo :jobs="[job('completed', 'J-401'), job('completed', 'J-402'), job('cancelled', 'J-403')]" />
|
|
113
152
|
</div>
|
|
114
153
|
</Variant>
|
|
115
154
|
|
|
116
155
|
<Variant title="Empty">
|
|
117
|
-
<div style="
|
|
118
|
-
<
|
|
119
|
-
:source="{ jobs: [] }"
|
|
120
|
-
:load-on-mount="false"
|
|
121
|
-
:teleport-to="false"
|
|
122
|
-
/>
|
|
156
|
+
<div style="min-height: 560px;">
|
|
157
|
+
<TrayDemo :jobs="[]" open />
|
|
123
158
|
</div>
|
|
124
159
|
</Variant>
|
|
125
160
|
</Story>
|