@morscherlab/mint-sdk 1.1.24 → 1.2.0-beta.1

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.
Files changed (74) hide show
  1. package/README.md +9 -7
  2. package/dist/{ExperimentSelectorModal-B6P2msT4.js → ExperimentSelectorModal-DyPAUCLw.js} +3 -3
  3. package/dist/{ExperimentSelectorModal-B6P2msT4.js.map → ExperimentSelectorModal-DyPAUCLw.js.map} +1 -1
  4. package/dist/{SettingsModal-DoS2fzyz.js → SettingsModal-BgKY93zh.js} +5 -4
  5. package/dist/SettingsModal-BgKY93zh.js.map +1 -0
  6. package/dist/__tests__/composables/useEventStream.test.d.ts +1 -0
  7. package/dist/components/AutoGroupModal.vue.d.ts +1 -1
  8. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +1 -1
  9. package/dist/components/JobsStatusTray.vue.d.ts +29 -10
  10. package/dist/components/SampleLegend.vue.d.ts +1 -1
  11. package/dist/components/SampleSelector.vue.d.ts +1 -1
  12. package/dist/components/SettingsModal.vue.d.ts +1 -1
  13. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +1 -1
  14. package/dist/components/index.js +4 -4
  15. package/dist/{components-CW3hE1T3.js → components-DkHddRMl.js} +39 -40
  16. package/dist/components-DkHddRMl.js.map +1 -0
  17. package/dist/composables/index.d.ts +2 -3
  18. package/dist/composables/index.js +8 -8
  19. package/dist/composables/useEventStream.d.ts +43 -0
  20. package/dist/composables/useGeneratedAnalysis.d.ts +1 -3
  21. package/dist/composables/useJobsStatusTray.d.ts +1 -25
  22. package/dist/composables/usePluginClient.d.ts +3 -18
  23. package/dist/{composables-CkXDe7nm.js → composables-C_i90sGS.js} +32 -308
  24. package/dist/composables-C_i90sGS.js.map +1 -0
  25. package/dist/{experiment-Y9Iz7KYi.js → experiment-Cq145oEW.js} +2 -2
  26. package/dist/{experiment-Y9Iz7KYi.js.map → experiment-Cq145oEW.js.map} +1 -1
  27. package/dist/index.js +12 -12
  28. package/dist/install.js +4 -4
  29. package/dist/stores/auth.d.ts +0 -6
  30. package/dist/stores/index.js +2 -2
  31. package/dist/templates/index.js +2 -2
  32. package/dist/{templates-CovvRZ-9.js → templates-DsP51i-S.js} +2 -2
  33. package/dist/{templates-CovvRZ-9.js.map → templates-DsP51i-S.js.map} +1 -1
  34. package/dist/types/auth.d.ts +0 -1
  35. package/dist/{useApi-CDBq6z23.js → useApi-J6zFkHmn.js} +1 -2
  36. package/dist/useApi-J6zFkHmn.js.map +1 -0
  37. package/dist/{useControlSchema-BhoJzWzx.js → useControlSchema-BDx2Olha.js} +239 -103
  38. package/dist/useControlSchema-BDx2Olha.js.map +1 -0
  39. package/dist/{useExperimentSelector-D0pjj9WH.js → useExperimentSelector-CV4iNfMS.js} +2 -2
  40. package/dist/{useExperimentSelector-D0pjj9WH.js.map → useExperimentSelector-CV4iNfMS.js.map} +1 -1
  41. package/dist/{useFormBuilder-DiEcDis2.js → useFormBuilder-BLxG3BOX.js} +2 -2
  42. package/dist/{useFormBuilder-DiEcDis2.js.map → useFormBuilder-BLxG3BOX.js.map} +1 -1
  43. package/dist/{useProtocolTemplates-DHwSveE6.js → useProtocolTemplates-CS_SQl6U.js} +22 -113
  44. package/dist/useProtocolTemplates-CS_SQl6U.js.map +1 -0
  45. package/package.json +1 -1
  46. package/src/__tests__/components/GeneratedPluginView.test.ts +0 -5
  47. package/src/__tests__/components/JobsStatusTray.test.ts +43 -19
  48. package/src/__tests__/composables/useAuth.test.ts +23 -0
  49. package/src/__tests__/composables/useEventStream.test.ts +140 -0
  50. package/src/__tests__/composables/useGeneratedAnalysis.test.ts +103 -0
  51. package/src/__tests__/composables/useJobsStatusTray.test.ts +25 -161
  52. package/src/__tests__/composables/usePluginClient.test.ts +8 -13
  53. package/src/__tests__/docs/frontendDocsCatalog.test.ts +22 -1
  54. package/src/components/JobsStatusTray.story.vue +16 -10
  55. package/src/components/JobsStatusTray.vue +22 -28
  56. package/src/components/SettingsModal.schema.ts +3 -0
  57. package/src/composables/index.ts +0 -7
  58. package/src/composables/useAuth.ts +0 -3
  59. package/src/composables/useEventStream.ts +314 -0
  60. package/src/composables/useGeneratedAnalysis.ts +40 -109
  61. package/src/composables/useJobsStatusTray.ts +17 -184
  62. package/src/composables/usePluginClient.ts +45 -249
  63. package/src/stores/auth.ts +0 -1
  64. package/src/types/auth.ts +0 -1
  65. package/dist/SettingsModal-DoS2fzyz.js.map +0 -1
  66. package/dist/__tests__/composables/usePluginConfig.test.d.ts +0 -15
  67. package/dist/components-CW3hE1T3.js.map +0 -1
  68. package/dist/composables/usePluginConfig.d.ts +0 -25
  69. package/dist/composables-CkXDe7nm.js.map +0 -1
  70. package/dist/useApi-CDBq6z23.js.map +0 -1
  71. package/dist/useControlSchema-BhoJzWzx.js.map +0 -1
  72. package/dist/useProtocolTemplates-DHwSveE6.js.map +0 -1
  73. package/src/__tests__/composables/usePluginConfig.test.ts +0 -497
  74. package/src/composables/usePluginConfig.ts +0 -250
@@ -1,14 +1,13 @@
1
1
  import { mount } from '@vue/test-utils'
2
- import { defineComponent, nextTick, ref, shallowRef } from 'vue'
2
+ import { defineComponent, nextTick, shallowRef } from 'vue'
3
3
  import { describe, expect, it, vi } from 'vitest'
4
4
 
5
5
  import {
6
6
  combinePluginJobSources,
7
- useJobsStatusTray,
8
7
  usePluginJobCenter,
9
- type UseJobsStatusTrayOptions,
8
+ type UsePluginJobCenterOptions,
10
9
  } from '../../composables/useJobsStatusTray'
11
- import type { JobState, JobStateInput, JobStatus } from '../../types/jobs'
10
+ import type { JobStateInput, JobStatus } from '../../types/jobs'
12
11
 
13
12
  function job(status: JobStatus, id: string = status): JobStateInput {
14
13
  return {
@@ -44,29 +43,18 @@ function job(status: JobStatus, id: string = status): JobStateInput {
44
43
  }
45
44
  }
46
45
 
47
- function mountComposable(options: UseJobsStatusTrayOptions) {
48
- let tray!: ReturnType<typeof useJobsStatusTray>
46
+ function mountComposable(options: UsePluginJobCenterOptions) {
47
+ let tray!: ReturnType<typeof usePluginJobCenter>
49
48
  const wrapper = mount(defineComponent({
50
49
  setup() {
51
- tray = useJobsStatusTray(options)
50
+ tray = usePluginJobCenter(options)
52
51
  return () => null
53
52
  },
54
53
  }))
55
54
  return { tray, wrapper }
56
55
  }
57
56
 
58
- function deferred<T>(): {
59
- promise: Promise<T>
60
- resolve: (value: T) => void
61
- } {
62
- let resolve!: (value: T) => void
63
- const promise = new Promise<T>((done) => {
64
- resolve = done
65
- })
66
- return { promise, resolve }
67
- }
68
-
69
- describe('useJobsStatusTray', () => {
57
+ describe('usePluginJobCenter', () => {
70
58
  it('combines live source jobs and fans out or routes source actions', async () => {
71
59
  const firstJobs = shallowRef([job('running', 'first')])
72
60
  const secondJobs = shallowRef([job('completed', 'second')])
@@ -242,37 +230,20 @@ describe('useJobsStatusTray', () => {
242
230
 
243
231
  it('keeps awaiting input jobs in the active group', () => {
244
232
  const { tray } = mountComposable({
245
- jobs: [job('awaiting_input')],
233
+ source: { jobs: [job('awaiting_input')] },
246
234
  loadOnMount: false,
247
235
  })
248
236
 
249
237
  expect(tray.activeJobs.value.map(item => item.job_id)).toEqual(['awaiting_input'])
250
238
  })
251
239
 
252
- it('replaces stale jobs when an SSE snapshot arrives', () => {
253
- const { tray } = mountComposable({ jobs: [job('running', 'old')], loadOnMount: false })
254
-
255
- tray.applyStreamMessage({
256
- event: 'snapshot',
257
- data: { jobs: [job('queued', 'new')] },
258
- })
259
-
260
- expect(tray.jobs.value.map(item => item.job_id)).toEqual(['new'])
261
- })
262
-
263
- it('moves an SSE terminal update into history', () => {
264
- const { tray } = mountComposable({ jobs: [job('running', 'fit')], loadOnMount: false })
265
-
266
- tray.applyStreamMessage({ event: 'terminal', data: job('completed', 'fit') })
267
-
268
- expect(tray.historyJobs.value.map(item => item.job_id)).toEqual(['fit'])
269
- })
270
-
271
- it('does not call a cancel adapter when the capability is disabled', async () => {
240
+ it('does not call a source cancel action when the capability is disabled', async () => {
272
241
  const cancelJob = vi.fn()
273
242
  const { tray } = mountComposable({
274
- jobs: [{ ...job('running'), can_cancel: false }],
275
- adapter: { cancelJob },
243
+ source: {
244
+ jobs: [{ ...job('running'), can_cancel: false }],
245
+ cancelJob,
246
+ },
276
247
  loadOnMount: false,
277
248
  })
278
249
 
@@ -281,139 +252,32 @@ describe('useJobsStatusTray', () => {
281
252
  expect(cancelJob).not.toHaveBeenCalled()
282
253
  })
283
254
 
284
- it('does not let a stale list response overwrite an SSE update', async () => {
285
- let resolveList!: (jobs: JobStateInput[]) => void
286
- const listJobs = vi.fn(() => new Promise<JobStateInput[]>((resolve) => {
287
- resolveList = resolve
288
- }))
289
- const { tray } = mountComposable({
290
- adapter: { listJobs },
291
- loadOnMount: false,
255
+ it('refreshes through the source without mirroring its jobs', async () => {
256
+ const sourceJobs = shallowRef([job('running', 'old')])
257
+ const refreshJobs = vi.fn(async () => {
258
+ sourceJobs.value = [job('queued', 'new')]
259
+ return sourceJobs.value
292
260
  })
293
-
294
- const refresh = tray.refresh()
295
- tray.applyStreamMessage({ event: 'job', data: job('running', 'live') })
296
- resolveList([job('queued', 'stale')])
297
- await refresh
298
- await nextTick()
299
-
300
- expect(tray.jobs.value.map(item => item.job_id)).toEqual(['live'])
301
- })
302
-
303
- it('passes only explicitly deletable jobs to the bulk clear adapter', async () => {
304
- const clearFinishedJobs = vi.fn(async (_jobs: readonly JobState[]) => undefined)
305
261
  const { tray } = mountComposable({
306
- jobs: [
307
- job('completed', 'legacy-deletable'),
308
- { ...job('failed', 'retained'), can_delete: false },
309
- ],
310
- adapter: { clearFinishedJobs },
262
+ source: { jobs: sourceJobs, refreshJobs },
311
263
  loadOnMount: false,
312
264
  })
313
265
 
314
- await tray.clearFinished()
266
+ await tray.refresh()
267
+ await nextTick()
315
268
 
316
- expect(clearFinishedJobs.mock.calls[0]![0].map(item => item.job_id)).toEqual([
317
- 'legacy-deletable',
269
+ expect([refreshJobs.mock.calls.length, tray.jobs.value.map(item => item.job_id)]).toEqual([
270
+ 1,
271
+ ['new'],
318
272
  ])
319
273
  })
320
274
 
321
275
  it('does not include active deletable jobs in bulk clear targets', () => {
322
276
  const { tray } = mountComposable({
323
- jobs: [{ ...job('awaiting_input'), can_delete: true }],
277
+ source: { jobs: [{ ...job('awaiting_input'), can_delete: true }] },
324
278
  loadOnMount: false,
325
279
  })
326
280
 
327
281
  expect(tray.deletableJobs.value).toEqual([])
328
282
  })
329
-
330
- it('does not let a late cancel response overwrite a newer SSE state', async () => {
331
- const pending = deferred<JobStateInput>()
332
- const { tray } = mountComposable({
333
- jobs: [job('running', 'fit')],
334
- adapter: { cancelJob: async () => pending.promise },
335
- loadOnMount: false,
336
- })
337
-
338
- const cancellation = tray.cancelJob(tray.jobs.value[0]!)
339
- tray.applyStreamMessage({ event: 'terminal', data: job('completed', 'fit') })
340
- pending.resolve(job('cancelled', 'fit'))
341
- await cancellation
342
-
343
- expect(tray.jobs.value[0]!.status).toBe('completed')
344
- })
345
-
346
- it('applies a cancel response after an unrelated job update', async () => {
347
- const pending = deferred<JobStateInput>()
348
- const { tray } = mountComposable({
349
- jobs: [job('running', 'fit'), job('running', 'export')],
350
- adapter: { cancelJob: async () => pending.promise },
351
- loadOnMount: false,
352
- })
353
-
354
- const cancellation = tray.cancelJob(tray.jobs.value[0]!)
355
- tray.applyStreamMessage({ event: 'job', data: job('running', 'export') })
356
- pending.resolve(job('cancelled', 'fit'))
357
- await cancellation
358
-
359
- expect(tray.jobs.value.find(item => item.job_id === 'fit')?.status).toBe('cancelled')
360
- })
361
-
362
- it('refreshes instead of removing every target after a partial bulk clear', async () => {
363
- const listJobs = vi.fn(async () => [job('completed', 'retained')])
364
- const clearFinishedJobs = vi.fn(async () => 1)
365
- const { tray } = mountComposable({
366
- jobs: [job('completed', 'deleted'), job('completed', 'retained')],
367
- adapter: { listJobs, clearFinishedJobs },
368
- loadOnMount: false,
369
- })
370
-
371
- const cleared = await tray.clearFinished()
372
-
373
- expect([
374
- cleared,
375
- listJobs.mock.calls.length,
376
- tray.jobs.value.map(item => item.job_id),
377
- ]).toEqual([false, 1, ['retained']])
378
- })
379
-
380
- it('uses a replacement reactive adapter for later actions', async () => {
381
- const first = vi.fn(async () => undefined)
382
- const second = vi.fn(async () => undefined)
383
- const adapter = shallowRef({ cancelJob: first })
384
- const { tray } = mountComposable({
385
- jobs: [job('running')],
386
- adapter,
387
- loadOnMount: false,
388
- })
389
- adapter.value = { cancelJob: second }
390
-
391
- await tray.cancelJob(tray.jobs.value[0]!)
392
-
393
- expect([first.mock.calls.length, second.mock.calls.length]).toEqual([0, 1])
394
- })
395
-
396
- it('applies the latest message from a replacement stream immediately', async () => {
397
- const eventStream = shallowRef({
398
- isConnected: ref(true),
399
- isConnecting: ref(false),
400
- error: ref<string | null>(null),
401
- lastMessage: shallowRef({
402
- event: 'snapshot',
403
- data: { jobs: [job('queued', 'replacement')] },
404
- raw: '',
405
- }),
406
- lastEventId: ref<string | null>(null),
407
- start: vi.fn(),
408
- stop: vi.fn(),
409
- })
410
-
411
- const { tray } = mountComposable({
412
- eventStream,
413
- loadOnMount: false,
414
- })
415
- await nextTick()
416
-
417
- expect(tray.jobs.value.map(item => item.job_id)).toEqual(['replacement'])
418
- })
419
283
  })
@@ -14,7 +14,6 @@ import {
14
14
  uploadPluginEndpoint,
15
15
  useCurrentExperiment,
16
16
  usePluginEventStream,
17
- usePluginClient,
18
17
  usePluginSettings,
19
18
  type PluginContract,
20
19
  } from '../../composables/usePluginClient'
@@ -80,7 +79,13 @@ function pendingEventStreamResponse(): {
80
79
  })
81
80
  return {
82
81
  response: new Response(body, { status: 200 }),
83
- close: () => streamController.close(),
82
+ close: () => {
83
+ try {
84
+ streamController.close()
85
+ } catch {
86
+ // stop() may already have cancelled the reader.
87
+ }
88
+ },
84
89
  }
85
90
  }
86
91
 
@@ -1396,11 +1401,7 @@ describe('usePluginClient', () => {
1396
1401
 
1397
1402
  try {
1398
1403
  stream.start()
1399
- await Promise.resolve()
1400
- await Promise.resolve()
1401
- await Promise.resolve()
1402
-
1403
- expect(onOpen).toHaveBeenCalledOnce()
1404
+ await vi.waitFor(() => expect(onOpen).toHaveBeenCalledOnce())
1404
1405
  } finally {
1405
1406
  stopWatching()
1406
1407
  stream.stop()
@@ -1684,12 +1685,6 @@ describe('usePluginClient', () => {
1684
1685
  expect(requestConfigs).toHaveLength(0)
1685
1686
  })
1686
1687
 
1687
- it('returns the provided client identity from usePluginClient', () => {
1688
- const client = createPluginClient(contract, { endpoints: {} })
1689
-
1690
- expect(usePluginClient(client)).toBe(client)
1691
- })
1692
-
1693
1688
  it('loads the current experiment from URL conventions', async () => {
1694
1689
  window.history.replaceState({}, '', '/analysis?experimentId=42')
1695
1690
  const current = useCurrentExperiment<{ called: string }>({ immediate: false })
@@ -271,6 +271,18 @@ describe('frontend docs catalog coverage', () => {
271
271
  })
272
272
  })
273
273
 
274
+ it('documents only the source-driven JobsStatusTray props', () => {
275
+ const manifest = extractFrontendManifest()
276
+ const jobsStatusTray = manifest.categories.components.find(item => item.name === 'JobsStatusTray')
277
+
278
+ expect(jobsStatusTray?.props?.map(prop => prop.name).sort()).toEqual([
279
+ 'loadOnMount',
280
+ 'source',
281
+ 'teleportTo',
282
+ 'title',
283
+ ])
284
+ })
285
+
274
286
  it('re-exports every public component from the root SDK entrypoint', () => {
275
287
  const publicComponents = parseComponentExports()
276
288
  const rootComponents = parseRootComponentExports()
@@ -295,6 +307,7 @@ describe('frontend docs catalog coverage', () => {
295
307
  const rootIndex = readFileSync(join(SDK_ROOT, 'src', 'index.ts'), 'utf8')
296
308
  const componentsIndex = readFileSync(join(SDK_ROOT, 'src', 'components', 'index.ts'), 'utf8')
297
309
  const composablesIndex = readFileSync(join(SDK_ROOT, 'src', 'composables', 'index.ts'), 'utf8')
310
+ const composableExports = composablesIndex.replace(/from\s+['"][^'"]+['"]/g, '')
298
311
 
299
312
  const retiredNames = [
300
313
  'ToastNotification',
@@ -306,12 +319,20 @@ describe('frontend docs catalog coverage', () => {
306
319
  'AppPillNav',
307
320
  'usePluginApi',
308
321
  'UsePluginApiOptions',
322
+ 'useJobsStatusTray',
323
+ 'UseJobsStatusTrayOptions',
324
+ 'UseJobsStatusTrayReturn',
325
+ 'JobsStatusTrayAdapter',
326
+ 'JobStreamMessage',
327
+ 'usePluginConfig',
328
+ 'UsePluginConfigReturn',
329
+ 'usePluginClient',
309
330
  ]
310
331
 
311
332
  for (const name of retiredNames) {
312
333
  expect(rootIndex).not.toMatch(new RegExp(`\\b${name}\\b`))
313
334
  expect(componentsIndex).not.toMatch(new RegExp(`\\b${name}\\b`))
314
- expect(composablesIndex).not.toMatch(new RegExp(`\\b${name}\\b`))
335
+ expect(composableExports).not.toMatch(new RegExp(`\\b${name}\\b`))
315
336
  }
316
337
  })
317
338
 
@@ -54,9 +54,11 @@ const explicitActionJobs = [
54
54
  job('awaiting_input', 'J-201', { can_cancel: true }),
55
55
  job('completed', 'J-200', { can_delete: false }),
56
56
  ]
57
- const adapter = {
58
- cancelJob: async () => undefined,
59
- deleteJob: async () => true,
57
+ const cancelJob = async () => undefined
58
+ const deleteJob = async () => true
59
+
60
+ function openJob(jobId: string) {
61
+ console.info('open', jobId)
60
62
  }
61
63
 
62
64
  function initState() {
@@ -74,11 +76,16 @@ function initState() {
74
76
  <div style="padding: 2rem; min-height: 520px;">
75
77
  <JobsStatusTray
76
78
  :title="state.title"
77
- :jobs="state.jobs"
78
- :adapter="adapter"
79
+ :source="{ jobs: state.jobs, cancelJob, deleteJob }"
79
80
  :load-on-mount="false"
80
81
  :teleport-to="false"
81
- />
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>
82
89
  </div>
83
90
  </template>
84
91
  <template #controls="{ state }">
@@ -89,7 +96,7 @@ function initState() {
89
96
  <Variant title="Awaiting input">
90
97
  <div style="padding: 2rem; min-height: 520px;">
91
98
  <JobsStatusTray
92
- :jobs="[job('awaiting_input', 'J-301')]"
99
+ :source="{ jobs: [job('awaiting_input', 'J-301')] }"
93
100
  :load-on-mount="false"
94
101
  :teleport-to="false"
95
102
  />
@@ -99,8 +106,7 @@ function initState() {
99
106
  <Variant title="Explicit actions">
100
107
  <div style="padding: 2rem; min-height: 520px;">
101
108
  <JobsStatusTray
102
- :jobs="explicitActionJobs"
103
- :adapter="adapter"
109
+ :source="{ jobs: explicitActionJobs, cancelJob, deleteJob }"
104
110
  :load-on-mount="false"
105
111
  :teleport-to="false"
106
112
  />
@@ -110,7 +116,7 @@ function initState() {
110
116
  <Variant title="Empty">
111
117
  <div style="padding: 2rem; min-height: 520px;">
112
118
  <JobsStatusTray
113
- :jobs="[]"
119
+ :source="{ jobs: [] }"
114
120
  :load-on-mount="false"
115
121
  :teleport-to="false"
116
122
  />
@@ -15,24 +15,12 @@ import {
15
15
  } from '../jobs'
16
16
  import {
17
17
  usePluginJobCenter,
18
- type JobsStatusTrayAdapter,
19
18
  type PluginJobCenterSource,
20
19
  } from '../composables/useJobsStatusTray'
21
- import type { UsePluginEventStreamReturn } from '../composables/usePluginClient'
22
- import type {
23
- JobState,
24
- JobStateInput,
25
- JobStreamData,
26
- } from '../types/jobs'
20
+ import type { JobState } from '../types/jobs'
27
21
 
28
22
  interface Props {
29
23
  source?: PluginJobCenterSource
30
- /** @deprecated Pass a source instead. */
31
- jobs?: readonly JobStateInput[]
32
- /** @deprecated Pass source actions instead. */
33
- adapter?: JobsStatusTrayAdapter
34
- /** @deprecated Event streams belong to the source runtime. */
35
- eventStream?: UsePluginEventStreamReturn<JobStreamData>
36
24
  loadOnMount?: boolean
37
25
  title?: string
38
26
  teleportTo?: string | HTMLElement | false
@@ -49,6 +37,11 @@ const emit = defineEmits<{
49
37
  error: [message: string]
50
38
  }>()
51
39
 
40
+ defineSlots<{
41
+ /** Extra per-row actions, rendered before the built-in Cancel / Delete. */
42
+ 'job-actions'?: (props: { job: JobState }) => unknown
43
+ }>()
44
+
52
45
  const panelId = `mint-jobs-tray-${useId().replace(/:/g, '')}`
53
46
  const titleId = `${panelId}-title`
54
47
  const triggerRef = ref<HTMLButtonElement | null>(null)
@@ -56,9 +49,6 @@ const panelRef = ref<HTMLElement | null>(null)
56
49
  const teleportTarget = computed(() => props.teleportTo || 'body')
57
50
  const tray = usePluginJobCenter({
58
51
  source: toRef(props, 'source'),
59
- jobs: toRef(props, 'jobs'),
60
- adapter: toRef(props, 'adapter'),
61
- eventStream: toRef(props, 'eventStream'),
62
52
  loadOnMount: props.loadOnMount,
63
53
  })
64
54
  const canClear = computed(() => (
@@ -309,6 +299,7 @@ async function clearFinished(): Promise<void> {
309
299
  <div class="mint-jobs-tray__job-footer">
310
300
  <span>{{ progressText(job) }}</span>
311
301
  <div class="mint-jobs-tray__job-actions">
302
+ <slot name="job-actions" :job="job" />
312
303
  <button
313
304
  v-if="job.can_cancel && tray.canCancel.value"
314
305
  type="button"
@@ -371,18 +362,21 @@ async function clearFinished(): Promise<void> {
371
362
  </div>
372
363
  <div class="mint-jobs-tray__job-footer">
373
364
  <span>{{ formatTimestamp(job.completed_at || job.created_at) }}</span>
374
- <button
375
- v-if="job.can_delete && tray.canDelete.value"
376
- type="button"
377
- class="mint-jobs-tray__icon-button mint-jobs-tray__icon-button--danger"
378
- aria-label="Delete job"
379
- :disabled="tray.isPending(job)"
380
- @click="deleteJob(job)"
381
- >
382
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
383
- <path d="M4 7h16M9 7V4h6v3m-8 0 1 13h8l1-13M10 11v5M14 11v5" />
384
- </svg>
385
- </button>
365
+ <div class="mint-jobs-tray__job-actions">
366
+ <slot name="job-actions" :job="job" />
367
+ <button
368
+ v-if="job.can_delete && tray.canDelete.value"
369
+ type="button"
370
+ class="mint-jobs-tray__icon-button mint-jobs-tray__icon-button--danger"
371
+ aria-label="Delete job"
372
+ :disabled="tray.isPending(job)"
373
+ @click="deleteJob(job)"
374
+ >
375
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
376
+ <path d="M4 7h16M9 7V4h6v3m-8 0 1 13h8l1-13M10 11v5M14 11v5" />
377
+ </svg>
378
+ </button>
379
+ </div>
386
380
  </div>
387
381
  </article>
388
382
  </template>
@@ -14,6 +14,9 @@ export const APPEARANCE_TAB_ID = 'appearance'
14
14
  export const APPEARANCE_TAB: SettingsTab = {
15
15
  id: APPEARANCE_TAB_ID,
16
16
  label: 'Appearance',
17
+ // Heroicons "swatch" — the built-in tab sits beside plugin tabs that all
18
+ // carry icons, so it needs one of its own to keep the rail consistent.
19
+ icon: '<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.098 19.902a3.75 3.75 0 0 0 5.304 0l6.401-6.402M6.75 21A3.75 3.75 0 0 1 3 17.25V4.125C3 3.504 3.504 3 4.125 3h5.25c.621 0 1.125.504 1.125 1.125v4.072M6.75 21a3.75 3.75 0 0 0 3.75-3.75V8.197M6.75 21h13.125c.621 0 1.125-.504 1.125-1.125v-5.25c0-.621-.504-1.125-1.125-1.125h-4.072M10.5 8.197l2.88-2.88c.438-.439 1.15-.439 1.59 0l3.712 3.713c.44.44.44 1.152 0 1.59l-2.879 2.88M6.75 17.25h.008v.008H6.75v-.008Z"></path></svg>',
17
20
  description: 'Theme, color palette, and table density',
18
21
  }
19
22
 
@@ -162,7 +162,6 @@ export {
162
162
  type DilutionPreset,
163
163
  type UseReagentSeriesReturn,
164
164
  } from './useReagentSeries'
165
- export { usePluginConfig, type UsePluginConfigReturn } from './usePluginConfig'
166
165
  export {
167
166
  useExperimentSelector,
168
167
  type UseExperimentSelectorOptions,
@@ -488,7 +487,6 @@ export {
488
487
  pluginFormDataFromPayload,
489
488
  resolvePluginBaseUrl,
490
489
  uploadPluginEndpoint,
491
- usePluginClient,
492
490
  usePluginEventStream,
493
491
  usePluginSettings,
494
492
  useCurrentExperiment,
@@ -517,14 +515,9 @@ export {
517
515
  export {
518
516
  combinePluginJobSources,
519
517
  usePluginJobCenter,
520
- useJobsStatusTray,
521
518
  type CombinedPluginJobCenterSource,
522
519
  type PluginJobCenterSource,
523
- type JobsStatusTrayAdapter,
524
520
  type JobsStatusTrayTab,
525
- type JobStreamMessage,
526
521
  type UsePluginJobCenterOptions,
527
522
  type UsePluginJobCenterReturn,
528
- type UseJobsStatusTrayOptions,
529
- type UseJobsStatusTrayReturn,
530
523
  } from './useJobsStatusTray'
@@ -93,7 +93,6 @@ export function useAuth(): UseAuthReturn {
93
93
  passkey_enabled: boolean
94
94
  passkey_registered?: boolean
95
95
  registration_enabled?: boolean
96
- database_mode?: string
97
96
  access?: {
98
97
  experiment_visibility_mode?: 'open' | 'restricted'
99
98
  experimentVisibilityMode?: 'open' | 'restricted'
@@ -105,7 +104,6 @@ export function useAuth(): UseAuthReturn {
105
104
  passkeyEnabled: response.data.passkey_enabled,
106
105
  passkeyRegistered: response.data.passkey_registered ?? false,
107
106
  registrationEnabled: response.data.registration_enabled ?? false,
108
- databaseMode: response.data.database_mode ?? 'none',
109
107
  experimentVisibilityMode:
110
108
  response.data.access?.experiment_visibility_mode
111
109
  ?? response.data.access?.experimentVisibilityMode
@@ -121,7 +119,6 @@ export function useAuth(): UseAuthReturn {
121
119
  passkeyEnabled: false,
122
120
  passkeyRegistered: false,
123
121
  registrationEnabled: false,
124
- databaseMode: 'none',
125
122
  experimentVisibilityMode: 'open',
126
123
  }
127
124
  }