@morscherlab/mint-sdk 1.0.63 → 1.1.0-beta.2
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 +101 -2
- package/dist/EmptyState-D1c4RdC7.js +638 -0
- package/dist/EmptyState-D1c4RdC7.js.map +1 -0
- package/dist/{ExperimentPopover-BbPkIFsI.js → ExperimentPopover-BFw3QX8L.js} +10 -9
- package/dist/{ExperimentPopover-BbPkIFsI.js.map → ExperimentPopover-BFw3QX8L.js.map} +1 -1
- package/dist/{ExperimentSelectorModal-DZeSd-yr.js → ExperimentSelectorModal-B7w6k3Y8.js} +12 -216
- package/dist/ExperimentSelectorModal-B7w6k3Y8.js.map +1 -0
- package/dist/{SettingsModal-CJgLadsN.js → SettingsModal-DPDwCbXK.js} +12 -9
- package/dist/{SettingsModal-CJgLadsN.js.map → SettingsModal-DPDwCbXK.js.map} +1 -1
- package/dist/__tests__/components/GeneratedPlotlyResult.test.d.ts +1 -0
- package/dist/__tests__/components/GeneratedPluginView.test.d.ts +1 -0
- package/dist/__tests__/components/GeneratedResultRenderer.test.d.ts +1 -0
- package/dist/__tests__/components/InstrumentAlertLog.test.d.ts +1 -0
- package/dist/__tests__/composables/useGeneratedAnalysis.test.d.ts +1 -0
- package/dist/__tests__/generatedUi.test.d.ts +1 -0
- package/dist/{analysisArtifactTypes-Cu40dzSG.js → analysisArtifactTypes-ejqR8MX3.js} +1 -1
- package/dist/analysisArtifactTypes-ejqR8MX3.js.map +1 -0
- package/dist/{auth-DZAwEeis.js → auth-B87xM--s.js} +1 -1
- package/dist/{auth-DZAwEeis.js.map → auth-B87xM--s.js.map} +1 -1
- package/dist/chunk-BoAXSpZd.js +33 -0
- package/dist/components/AppSidebar.vue.d.ts +1 -1
- package/dist/components/AutoGroupModal.vue.d.ts +2 -2
- package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
- package/dist/components/CollapsibleCard.vue.d.ts +1 -1
- package/dist/components/ComponentBindingRenderer.vue.d.ts +1 -1
- package/dist/components/ConfirmDialog.vue.d.ts +1 -1
- package/dist/components/ExperimentDataViewer.vue.d.ts +1 -1
- package/dist/components/ExperimentTimeline.vue.d.ts +1 -1
- package/dist/components/FileUploader.vue.d.ts +4 -2
- package/dist/components/FitPanel.vue.d.ts +1 -1
- package/dist/components/FormActions.vue.d.ts +1 -1
- package/dist/components/FormBuilder.vue.d.ts +1 -1
- package/dist/components/GeneratedPathInput.vue.d.ts +12 -0
- package/dist/components/GeneratedPlotlyResult.vue.d.ts +11 -0
- package/dist/components/GeneratedPluginView.vue.d.ts +10 -0
- package/dist/components/GeneratedResultRenderer.vue.d.ts +6 -0
- package/dist/components/JobsStatusTray.vue.d.ts +5 -1
- package/dist/components/LcmsSequenceTable.vue.d.ts +1 -1
- package/dist/components/SampleLegend.vue.d.ts +1 -1
- package/dist/components/SampleSelector.vue.d.ts +2 -2
- package/dist/components/SettingsModal.vue.d.ts +1 -1
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +2 -2
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.js +8 -9
- package/dist/components/internal/PlateMapEditorToolbarInternal.vue.d.ts +2 -2
- package/dist/{components-DdzHh3a_.js → components-UfkPN1bK.js} +2255 -2303
- package/dist/components-UfkPN1bK.js.map +1 -0
- package/dist/composables/index.d.ts +2 -3
- package/dist/composables/index.js +10 -11
- package/dist/composables/platformContextHelpers.d.ts +0 -2
- package/dist/composables/useAnalysisArtifacts.d.ts +1 -3
- package/dist/composables/useGeneratedAnalysis.d.ts +62 -0
- package/dist/composables/useJobsStatusTray.d.ts +48 -4
- package/dist/composables/usePlatformContext.d.ts +3 -0
- package/dist/composables/usePluginClient.d.ts +6 -3
- package/dist/{composables-BUUphBqg.js → composables-B512bWJP.js} +155 -74
- package/dist/composables-B512bWJP.js.map +1 -0
- package/dist/experiment-utils-CxkruZ3G.js +392 -0
- package/dist/experiment-utils-CxkruZ3G.js.map +1 -0
- package/dist/generated/schema.d.ts +7 -0
- package/dist/generated-ui/main.d.ts +0 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15 -18
- package/dist/index.js.map +1 -1
- package/dist/install.js +7 -8
- package/dist/install.js.map +1 -1
- package/dist/plotly.min-BR53hja4.js +180634 -0
- package/dist/plotly.min-BR53hja4.js.map +1 -0
- package/dist/stores/auth.d.ts +2 -2
- package/dist/stores/index.js +1 -1
- package/dist/styles.css +10543 -10404
- package/dist/templates/index.js +4 -3
- package/dist/templates/packs.d.ts +0 -1
- package/dist/{templates-CNbPQNID.js → templates-Cttbjbyc.js} +2 -4
- package/dist/templates-Cttbjbyc.js.map +1 -0
- package/dist/types/analysisArtifactTypes.d.ts +0 -8
- package/dist/types/form-builder.d.ts +1 -1
- package/dist/types/generated-ui.d.ts +170 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/index.js +1 -1
- package/dist/types/instrument.d.ts +1 -0
- package/dist/types/jobs.d.ts +7 -1
- package/dist/types/platform.d.ts +2 -0
- package/dist/{useControlSchema-BZNdalmL.js → useControlSchema-Dallso-L.js} +1852 -171
- package/dist/useControlSchema-Dallso-L.js.map +1 -0
- package/dist/{useDropdownState-Bb-c_PeE.js → useDropdownState-gVaMXtwW.js} +2 -2
- package/dist/{useDropdownState-Bb-c_PeE.js.map → useDropdownState-gVaMXtwW.js.map} +1 -1
- package/dist/{useExperimentSelector-DRbj6vse.js → useExperimentSelector-BMjzNcf2.js} +4 -255
- package/dist/useExperimentSelector-BMjzNcf2.js.map +1 -0
- package/dist/{useFormBuilder-DKekvXRs.js → useFormBuilder-BXaaDI41.js} +2 -2
- package/dist/{useFormBuilder-DKekvXRs.js.map → useFormBuilder-BXaaDI41.js.map} +1 -1
- package/dist/{useAnalysisArtifacts-C3l0a0vu.js → useProtocolTemplates-BP7_4RGQ.js} +307 -475
- package/dist/useProtocolTemplates-BP7_4RGQ.js.map +1 -0
- package/package.json +7 -3
- package/src/__tests__/components/GeneratedPathInput.test.ts +132 -0
- package/src/__tests__/components/GeneratedPlotlyResult.test.ts +117 -0
- package/src/__tests__/components/GeneratedPluginView.test.ts +230 -0
- package/src/__tests__/components/GeneratedResultRenderer.test.ts +93 -0
- package/src/__tests__/components/InstrumentAlertLog.test.ts +59 -0
- package/src/__tests__/composables/formBuilderRegistry.test.ts +5 -1
- package/src/__tests__/composables/useAnalysisArtifacts.test.ts +1 -74
- package/src/__tests__/composables/useApi.test.ts +8 -0
- package/src/__tests__/composables/useGeneratedAnalysis.test.ts +1740 -0
- package/src/__tests__/composables/useJobsStatusTray.test.ts +179 -1
- package/src/__tests__/composables/usePlatformContext.test.ts +28 -0
- package/src/__tests__/composables/usePluginClient.test.ts +366 -20
- package/src/__tests__/composables/usePluginConfig.test.ts +323 -7
- package/src/__tests__/docs/extractDocsTemplates.test.ts +0 -1
- package/src/__tests__/generatedUi.test.ts +119 -0
- package/src/__tests__/templates/templates.test.ts +1 -1
- package/src/components/FileUploader.vue +49 -11
- package/src/components/GeneratedPathInput.vue +218 -0
- package/src/components/GeneratedPlotlyResult.vue +87 -0
- package/src/components/GeneratedPluginView.vue +279 -0
- package/src/components/GeneratedResultRenderer.vue +137 -0
- package/src/components/InstrumentAlertLog.vue +2 -2
- package/src/components/JobsStatusTray.vue +14 -8
- package/src/components/index.ts +4 -0
- package/src/composables/formBuilderRegistry.ts +2 -0
- package/src/composables/index.ts +18 -5
- package/src/composables/platformContextHelpers.ts +3 -10
- package/src/composables/useAnalysisArtifacts.ts +0 -48
- package/src/composables/useApi.ts +1 -0
- package/src/composables/useGeneratedAnalysis.ts +1252 -0
- package/src/composables/useJobsStatusTray.ts +234 -45
- package/src/composables/usePluginClient.ts +152 -25
- package/src/composables/usePluginConfig.ts +156 -20
- package/src/generated/schema.ts +130 -0
- package/src/generated-ui/main.ts +21 -0
- package/src/index.ts +3 -0
- package/src/jobs.ts +5 -0
- package/src/plotly-dist.d.ts +3 -0
- package/src/templates/packs.ts +0 -3
- package/src/types/analysisArtifactTypes.ts +0 -9
- package/src/types/form-builder.ts +1 -0
- package/src/types/generated-ui.ts +176 -0
- package/src/types/index.ts +2 -1
- package/src/types/instrument.ts +1 -0
- package/src/types/jobs.ts +10 -1
- package/src/types/platform.ts +2 -0
- package/dist/BaseButton-Dgqrze41.js +0 -74
- package/dist/BaseButton-Dgqrze41.js.map +0 -1
- package/dist/BaseModal-B9UA8Y_I.js +0 -165
- package/dist/BaseModal-B9UA8Y_I.js.map +0 -1
- package/dist/BaseSelect-ekgr9fDo.js +0 -179
- package/dist/BaseSelect-ekgr9fDo.js.map +0 -1
- package/dist/ExperimentPopover-Ryusjrhv.js +0 -4
- package/dist/ExperimentSelectorModal-Cg4-LXBD.js +0 -5
- package/dist/ExperimentSelectorModal-DZeSd-yr.js.map +0 -1
- package/dist/SettingsModal-Bl_9w2Y3.js +0 -5
- package/dist/analysisArtifactTypes-Cu40dzSG.js.map +0 -1
- package/dist/components-DdzHh3a_.js.map +0 -1
- package/dist/composables/analysisArtifactTarget.d.ts +0 -5
- package/dist/composables-BUUphBqg.js.map +0 -1
- package/dist/experiment-utils-Bfa7CwPU.js +0 -108
- package/dist/experiment-utils-Bfa7CwPU.js.map +0 -1
- package/dist/templates-CNbPQNID.js.map +0 -1
- package/dist/useAnalysisArtifacts-C3l0a0vu.js.map +0 -1
- package/dist/useControlSchema-BZNdalmL.js.map +0 -1
- package/dist/useEventListener-CfVkP9Xz.js +0 -57
- package/dist/useEventListener-CfVkP9Xz.js.map +0 -1
- package/dist/useExperimentSelector-DRbj6vse.js.map +0 -1
- package/src/__tests__/composables/analysisArtifactTarget.test.ts +0 -77
- package/src/composables/analysisArtifactTarget.ts +0 -53
- /package/dist/__tests__/{composables/analysisArtifactTarget.test.d.ts → components/GeneratedPathInput.test.d.ts} +0 -0
|
@@ -2,7 +2,12 @@ import { mount } from '@vue/test-utils'
|
|
|
2
2
|
import { defineComponent, nextTick, ref, shallowRef } from 'vue'
|
|
3
3
|
import { describe, expect, it, vi } from 'vitest'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
combinePluginJobSources,
|
|
7
|
+
useJobsStatusTray,
|
|
8
|
+
usePluginJobCenter,
|
|
9
|
+
type UseJobsStatusTrayOptions,
|
|
10
|
+
} from '../../composables/useJobsStatusTray'
|
|
6
11
|
import type { JobState, JobStateInput, JobStatus } from '../../types/jobs'
|
|
7
12
|
|
|
8
13
|
function job(status: JobStatus, id: string = status): JobStateInput {
|
|
@@ -62,6 +67,179 @@ function deferred<T>(): {
|
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
describe('useJobsStatusTray', () => {
|
|
70
|
+
it('combines live source jobs and fans out or routes source actions', async () => {
|
|
71
|
+
const firstJobs = shallowRef([job('running', 'first')])
|
|
72
|
+
const secondJobs = shallowRef([job('completed', 'second')])
|
|
73
|
+
const firstRefresh = vi.fn(async () => firstJobs.value)
|
|
74
|
+
const secondRefresh = vi.fn(async () => secondJobs.value)
|
|
75
|
+
const secondCancel = vi.fn(async (jobId: string) => job('cancelled', jobId))
|
|
76
|
+
const firstClear = vi.fn(async () => 1)
|
|
77
|
+
const secondClear = vi.fn(async () => 2)
|
|
78
|
+
const firstDispose = vi.fn()
|
|
79
|
+
const secondDispose = vi.fn()
|
|
80
|
+
const combined = combinePluginJobSources([
|
|
81
|
+
{
|
|
82
|
+
jobs: firstJobs,
|
|
83
|
+
refreshJobs: firstRefresh,
|
|
84
|
+
clearFinishedJobs: firstClear,
|
|
85
|
+
dispose: firstDispose,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
jobs: secondJobs,
|
|
89
|
+
refreshJobs: secondRefresh,
|
|
90
|
+
cancelJob: secondCancel,
|
|
91
|
+
clearFinishedJobs: secondClear,
|
|
92
|
+
dispose: secondDispose,
|
|
93
|
+
},
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
expect(combined.jobs.value.map(item => item.job_id)).toEqual(['first', 'second'])
|
|
97
|
+
secondJobs.value = [job('running', 'replacement')]
|
|
98
|
+
await nextTick()
|
|
99
|
+
expect(combined.jobs.value.map(item => item.job_id)).toEqual(['first', 'replacement'])
|
|
100
|
+
|
|
101
|
+
await combined.refreshJobs?.()
|
|
102
|
+
await combined.cancelJob?.('replacement')
|
|
103
|
+
await expect(combined.clearFinishedJobs?.()).resolves.toBe(3)
|
|
104
|
+
combined.dispose?.()
|
|
105
|
+
|
|
106
|
+
expect({
|
|
107
|
+
refreshes: [firstRefresh.mock.calls.length, secondRefresh.mock.calls.length],
|
|
108
|
+
cancelled: secondCancel.mock.calls[0]?.[0],
|
|
109
|
+
disposed: [firstDispose.mock.calls.length, secondDispose.mock.calls.length],
|
|
110
|
+
}).toEqual({
|
|
111
|
+
refreshes: [1, 1],
|
|
112
|
+
cancelled: 'replacement',
|
|
113
|
+
disposed: [1, 1],
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('retains every failure when disposing combined sources', () => {
|
|
118
|
+
const firstFailure = new Error('first dispose failed')
|
|
119
|
+
const secondFailure = { source: 'second', reason: 'dispose failed' }
|
|
120
|
+
const finalDispose = vi.fn()
|
|
121
|
+
const combined = combinePluginJobSources([
|
|
122
|
+
{
|
|
123
|
+
jobs: [],
|
|
124
|
+
dispose: () => {
|
|
125
|
+
throw firstFailure
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
jobs: [],
|
|
130
|
+
dispose: () => {
|
|
131
|
+
throw secondFailure
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{ jobs: [], dispose: finalDispose },
|
|
135
|
+
])
|
|
136
|
+
|
|
137
|
+
let failure: unknown
|
|
138
|
+
try {
|
|
139
|
+
combined.dispose?.()
|
|
140
|
+
} catch (cause) {
|
|
141
|
+
failure = cause
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
expect(failure).toMatchObject({
|
|
145
|
+
name: 'PluginJobSourceDisposeError',
|
|
146
|
+
message: 'Failed to dispose plugin job sources',
|
|
147
|
+
cause: firstFailure,
|
|
148
|
+
errors: [firstFailure, secondFailure],
|
|
149
|
+
})
|
|
150
|
+
expect(finalDispose).toHaveBeenCalledOnce()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('rejects duplicate ids instead of routing to an arbitrary combined source', async () => {
|
|
154
|
+
const firstCancel = vi.fn(async () => undefined)
|
|
155
|
+
const secondCancel = vi.fn(async () => undefined)
|
|
156
|
+
const combined = combinePluginJobSources([
|
|
157
|
+
{ jobs: [job('running', 'duplicate')], cancelJob: firstCancel },
|
|
158
|
+
{ jobs: [job('queued', 'duplicate')], cancelJob: secondCancel },
|
|
159
|
+
])
|
|
160
|
+
|
|
161
|
+
expect(() => combined.jobs.value).toThrow('Duplicate plugin job id')
|
|
162
|
+
await expect(combined.cancelJob?.('duplicate')).rejects.toThrow('Duplicate plugin job id')
|
|
163
|
+
expect([firstCancel.mock.calls.length, secondCancel.mock.calls.length]).toEqual([0, 0])
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('does not expose a partial bulk clear across combined sources', () => {
|
|
167
|
+
const combined = combinePluginJobSources([
|
|
168
|
+
{ jobs: [job('completed', 'first')], clearFinishedJobs: async () => 1 },
|
|
169
|
+
{ jobs: [job('completed', 'second')] },
|
|
170
|
+
])
|
|
171
|
+
|
|
172
|
+
expect(combined.clearFinishedJobs).toBeUndefined()
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('does not partially delete finished jobs when one combined source cannot delete', async () => {
|
|
176
|
+
const firstDelete = vi.fn(async () => true)
|
|
177
|
+
const combined = combinePluginJobSources([
|
|
178
|
+
{ jobs: [job('completed', 'first')], deleteJob: firstDelete },
|
|
179
|
+
{ jobs: [job('completed', 'second')] },
|
|
180
|
+
])
|
|
181
|
+
let center!: ReturnType<typeof usePluginJobCenter>
|
|
182
|
+
const wrapper = mount(defineComponent({
|
|
183
|
+
setup() {
|
|
184
|
+
center = usePluginJobCenter({ source: combined, loadOnMount: false })
|
|
185
|
+
return () => null
|
|
186
|
+
},
|
|
187
|
+
}))
|
|
188
|
+
|
|
189
|
+
expect(combined.deleteJob).toBeUndefined()
|
|
190
|
+
expect(center.canClearFinished.value).toBe(false)
|
|
191
|
+
await expect(center.clearFinished()).resolves.toBe(false)
|
|
192
|
+
expect(firstDelete).not.toHaveBeenCalled()
|
|
193
|
+
|
|
194
|
+
wrapper.unmount()
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('derives jobs directly from a canonical source ref', async () => {
|
|
198
|
+
const sourceJobs = shallowRef([job('queued', 'first')])
|
|
199
|
+
let center!: ReturnType<typeof usePluginJobCenter>
|
|
200
|
+
const wrapper = mount(defineComponent({
|
|
201
|
+
setup() {
|
|
202
|
+
center = usePluginJobCenter({
|
|
203
|
+
source: { jobs: sourceJobs },
|
|
204
|
+
loadOnMount: false,
|
|
205
|
+
})
|
|
206
|
+
return () => null
|
|
207
|
+
},
|
|
208
|
+
}))
|
|
209
|
+
|
|
210
|
+
sourceJobs.value = [job('running', 'replacement')]
|
|
211
|
+
await nextTick()
|
|
212
|
+
|
|
213
|
+
expect(center.jobs.value.map(item => item.job_id)).toEqual(['replacement'])
|
|
214
|
+
wrapper.unmount()
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('delegates actions by id and leaves source mutation with the source', async () => {
|
|
218
|
+
const sourceJobs = shallowRef([job('running', 'fit')])
|
|
219
|
+
const cancelJob = vi.fn(async (jobId: string) => {
|
|
220
|
+
sourceJobs.value = [job('cancelled', jobId)]
|
|
221
|
+
return sourceJobs.value[0]
|
|
222
|
+
})
|
|
223
|
+
let center!: ReturnType<typeof usePluginJobCenter>
|
|
224
|
+
const wrapper = mount(defineComponent({
|
|
225
|
+
setup() {
|
|
226
|
+
center = usePluginJobCenter({
|
|
227
|
+
source: { jobs: sourceJobs, cancelJob },
|
|
228
|
+
loadOnMount: false,
|
|
229
|
+
})
|
|
230
|
+
return () => null
|
|
231
|
+
},
|
|
232
|
+
}))
|
|
233
|
+
|
|
234
|
+
await center.cancelJob(center.jobs.value[0]!)
|
|
235
|
+
|
|
236
|
+
expect([cancelJob.mock.calls[0]?.[0], center.jobs.value[0]?.status]).toEqual([
|
|
237
|
+
'fit',
|
|
238
|
+
'cancelled',
|
|
239
|
+
])
|
|
240
|
+
wrapper.unmount()
|
|
241
|
+
})
|
|
242
|
+
|
|
65
243
|
it('keeps awaiting input jobs in the active group', () => {
|
|
66
244
|
const { tray } = mountComposable({
|
|
67
245
|
jobs: [job('awaiting_input')],
|
|
@@ -17,6 +17,11 @@ vi.mock('vue', async () => {
|
|
|
17
17
|
})
|
|
18
18
|
|
|
19
19
|
import { usePlatformContext } from '../../composables/usePlatformContext'
|
|
20
|
+
import type { PlatformContext } from '../../types'
|
|
21
|
+
|
|
22
|
+
type InjectedPlatformWindow = Window & {
|
|
23
|
+
__MINT_PLATFORM__?: PlatformContext
|
|
24
|
+
}
|
|
20
25
|
|
|
21
26
|
function sendThemeMessage(origin: string, theme: 'light' | 'dark' | 'system'): void {
|
|
22
27
|
if (!messageHandler) {
|
|
@@ -37,6 +42,7 @@ describe('usePlatformContext', () => {
|
|
|
37
42
|
beforeEach(() => {
|
|
38
43
|
unmountedCallbacks.length = 0
|
|
39
44
|
messageHandler = null
|
|
45
|
+
delete (window as InjectedPlatformWindow).__MINT_PLATFORM__
|
|
40
46
|
|
|
41
47
|
vi.spyOn(window, 'addEventListener').mockImplementation((type, listener) => {
|
|
42
48
|
if (type === 'message') {
|
|
@@ -51,9 +57,31 @@ describe('usePlatformContext', () => {
|
|
|
51
57
|
const callback = unmountedCallbacks.pop()
|
|
52
58
|
callback?.()
|
|
53
59
|
}
|
|
60
|
+
delete (window as InjectedPlatformWindow).__MINT_PLATFORM__
|
|
54
61
|
vi.restoreAllMocks()
|
|
55
62
|
})
|
|
56
63
|
|
|
64
|
+
it('preserves the injected plugin settings API capability', () => {
|
|
65
|
+
const injectedContext: PlatformContext = {
|
|
66
|
+
isIntegrated: true,
|
|
67
|
+
theme: 'light',
|
|
68
|
+
platformApiUrl: '/api',
|
|
69
|
+
plugin: {
|
|
70
|
+
id: 'test-plugin',
|
|
71
|
+
name: 'test-plugin',
|
|
72
|
+
version: '1.0.0',
|
|
73
|
+
route_prefix: '/test-plugin',
|
|
74
|
+
api_prefix: '/api/test-plugin',
|
|
75
|
+
settings_api: 'plugin',
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
;(window as InjectedPlatformWindow).__MINT_PLATFORM__ = injectedContext
|
|
79
|
+
|
|
80
|
+
const context = usePlatformContext()
|
|
81
|
+
|
|
82
|
+
expect(context.plugin.value?.settings_api).toBe('plugin')
|
|
83
|
+
})
|
|
84
|
+
|
|
57
85
|
it('clears allowAnyOrigin policy after last consumer unmounts', () => {
|
|
58
86
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
59
87
|
|
|
@@ -156,6 +156,21 @@ describe('usePluginClient', () => {
|
|
|
156
156
|
expect(requestConfigs[0]!.url).toBe('/health')
|
|
157
157
|
})
|
|
158
158
|
|
|
159
|
+
it('omits MINT auth transport for a public generated endpoint', async () => {
|
|
160
|
+
const client = createPluginClient(contract, {
|
|
161
|
+
endpoints: {
|
|
162
|
+
health: { method: 'get', path: '/health', requiresAuth: false },
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
await client.health()
|
|
167
|
+
|
|
168
|
+
expect(
|
|
169
|
+
(requestConfigs[0] as AxiosRequestConfig & { _mintSkipAuth?: boolean })
|
|
170
|
+
._mintSkipAuth,
|
|
171
|
+
).toBe(true)
|
|
172
|
+
})
|
|
173
|
+
|
|
159
174
|
it('VITE_API_PREFIX overrides contract apiPrefix', async () => {
|
|
160
175
|
vi.stubEnv('VITE_API_PREFIX', '/api/override')
|
|
161
176
|
const client = createPluginClient(contract, {
|
|
@@ -672,7 +687,79 @@ describe('usePluginClient', () => {
|
|
|
672
687
|
expect(requestConfigs).toHaveLength(0)
|
|
673
688
|
})
|
|
674
689
|
|
|
675
|
-
it(
|
|
690
|
+
it.each([
|
|
691
|
+
'platform',
|
|
692
|
+
undefined,
|
|
693
|
+
] as const)(
|
|
694
|
+
'should use the integrated platform config facade when settings_api is %s',
|
|
695
|
+
async (settingsApi) => {
|
|
696
|
+
;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
|
|
697
|
+
isIntegrated: true,
|
|
698
|
+
theme: 'light',
|
|
699
|
+
platformApiUrl: '/api',
|
|
700
|
+
plugin: {
|
|
701
|
+
id: 'drp',
|
|
702
|
+
name: 'drp',
|
|
703
|
+
version: '1.0.0',
|
|
704
|
+
route_prefix: '/drp',
|
|
705
|
+
api_prefix: '/api/drp',
|
|
706
|
+
...(settingsApi ? { settings_api: settingsApi } : {}),
|
|
707
|
+
},
|
|
708
|
+
}
|
|
709
|
+
nextGetData = {
|
|
710
|
+
plugin_name: 'drp',
|
|
711
|
+
config: {
|
|
712
|
+
nasBasePath: '/data',
|
|
713
|
+
allowed_experiment_types: ['metabolomics'],
|
|
714
|
+
},
|
|
715
|
+
revision: 'revision-1',
|
|
716
|
+
}
|
|
717
|
+
nextPatchData = {
|
|
718
|
+
plugin_name: 'drp',
|
|
719
|
+
config: {
|
|
720
|
+
nasBasePath: '/mnt',
|
|
721
|
+
allowed_experiment_types: ['metabolomics'],
|
|
722
|
+
},
|
|
723
|
+
revision: 'revision-2',
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const settings = usePluginSettings<{ nasBasePath?: string }>({ pluginName: 'drp' })
|
|
727
|
+
await settings.load()
|
|
728
|
+
|
|
729
|
+
expect(requestConfigs[0]!.baseURL).toBe('/api')
|
|
730
|
+
expect(requestConfigs[0]!.url).toBe('/plugins/drp/config')
|
|
731
|
+
expect(settings.values.value).toEqual({
|
|
732
|
+
nasBasePath: '/data',
|
|
733
|
+
allowed_experiment_types: ['metabolomics'],
|
|
734
|
+
})
|
|
735
|
+
expect(settings.settings.value).toEqual({
|
|
736
|
+
nasBasePath: '/data',
|
|
737
|
+
allowed_experiment_types: ['metabolomics'],
|
|
738
|
+
})
|
|
739
|
+
expect(settings.isDirty.value).toBe(false)
|
|
740
|
+
|
|
741
|
+
settings.setValues({
|
|
742
|
+
nasBasePath: '/mnt',
|
|
743
|
+
allowed_experiment_types: ['metabolomics'],
|
|
744
|
+
})
|
|
745
|
+
expect(settings.isDirty.value).toBe(true)
|
|
746
|
+
const ok = await settings.save()
|
|
747
|
+
|
|
748
|
+
expect(ok).toBe(true)
|
|
749
|
+
expect(requestConfigs[1]!.baseURL).toBe('/api')
|
|
750
|
+
expect(requestConfigs[1]!.url).toBe('/plugins/drp/config')
|
|
751
|
+
expect(requestMethods[1]).toBe('patch')
|
|
752
|
+
expect(requestConfigs[1]!.headers).toBeUndefined()
|
|
753
|
+
expect(requestBodies[0]).toEqual({ config: { nasBasePath: '/mnt' } })
|
|
754
|
+
expect(settings.values.value).toEqual({
|
|
755
|
+
nasBasePath: '/mnt',
|
|
756
|
+
allowed_experiment_types: ['metabolomics'],
|
|
757
|
+
})
|
|
758
|
+
expect(settings.isDirty.value).toBe(false)
|
|
759
|
+
},
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
it('should use the plugin settings router for an integrated plugin capability', async () => {
|
|
676
763
|
;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
|
|
677
764
|
isIntegrated: true,
|
|
678
765
|
theme: 'light',
|
|
@@ -683,35 +770,271 @@ describe('usePluginClient', () => {
|
|
|
683
770
|
version: '1.0.0',
|
|
684
771
|
route_prefix: '/drp',
|
|
685
772
|
api_prefix: '/api/drp',
|
|
773
|
+
settings_api: 'plugin',
|
|
686
774
|
},
|
|
687
775
|
}
|
|
688
|
-
nextGetData = {
|
|
689
|
-
|
|
776
|
+
nextGetData = {
|
|
777
|
+
settings: { threshold: 1 },
|
|
778
|
+
mode: 'integrated',
|
|
779
|
+
revision: 'revision-1',
|
|
780
|
+
}
|
|
781
|
+
nextPutData = {
|
|
782
|
+
settings: { threshold: 2 },
|
|
783
|
+
mode: 'integrated',
|
|
784
|
+
revision: 'revision-2',
|
|
785
|
+
}
|
|
690
786
|
|
|
691
|
-
const settings = usePluginSettings<{
|
|
787
|
+
const settings = usePluginSettings<{ threshold?: number }>({ pluginName: 'drp' })
|
|
692
788
|
await settings.load()
|
|
789
|
+
const saved = await settings.save({ threshold: 2 })
|
|
693
790
|
|
|
694
|
-
expect(
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
791
|
+
expect({
|
|
792
|
+
saved,
|
|
793
|
+
methods: requestMethods,
|
|
794
|
+
requests: requestConfigs.map(({ baseURL, url, headers }) => ({
|
|
795
|
+
baseURL,
|
|
796
|
+
url,
|
|
797
|
+
headers,
|
|
798
|
+
})),
|
|
799
|
+
bodies: requestBodies,
|
|
800
|
+
values: settings.values.value,
|
|
801
|
+
}).toEqual({
|
|
802
|
+
saved: true,
|
|
803
|
+
methods: ['get', 'put'],
|
|
804
|
+
requests: [
|
|
805
|
+
{
|
|
806
|
+
baseURL: '/api/drp',
|
|
807
|
+
url: '/settings',
|
|
808
|
+
headers: undefined,
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
baseURL: '/api/drp',
|
|
812
|
+
url: '/settings',
|
|
813
|
+
headers: { 'If-Match': '"revision-1"' },
|
|
814
|
+
},
|
|
815
|
+
],
|
|
816
|
+
bodies: [{ threshold: 2 }],
|
|
817
|
+
values: { threshold: 2 },
|
|
818
|
+
})
|
|
819
|
+
})
|
|
699
820
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
821
|
+
it('should use the platform facade for an explicit different plugin name', async () => {
|
|
822
|
+
;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
|
|
823
|
+
isIntegrated: true,
|
|
824
|
+
theme: 'light',
|
|
825
|
+
platformApiUrl: '/api',
|
|
826
|
+
plugin: {
|
|
827
|
+
id: 'current-plugin',
|
|
828
|
+
name: 'current-plugin',
|
|
829
|
+
version: '1.0.0',
|
|
830
|
+
route_prefix: '/current-plugin',
|
|
831
|
+
api_prefix: '/api/current-plugin',
|
|
832
|
+
settings_api: 'plugin',
|
|
833
|
+
},
|
|
834
|
+
}
|
|
835
|
+
nextGetData = {
|
|
836
|
+
plugin_name: 'other-plugin',
|
|
837
|
+
config: { threshold: 1 },
|
|
838
|
+
revision: 'revision-1',
|
|
839
|
+
}
|
|
703
840
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
expect(
|
|
841
|
+
const settings = usePluginSettings<{ threshold?: number }>({
|
|
842
|
+
pluginName: 'other-plugin',
|
|
843
|
+
})
|
|
844
|
+
await settings.load()
|
|
845
|
+
|
|
846
|
+
expect({
|
|
847
|
+
method: requestMethods[0],
|
|
848
|
+
baseURL: requestConfigs[0]!.baseURL,
|
|
849
|
+
url: requestConfigs[0]!.url,
|
|
850
|
+
values: settings.values.value,
|
|
851
|
+
}).toEqual({
|
|
852
|
+
method: 'get',
|
|
853
|
+
baseURL: '/api',
|
|
854
|
+
url: '/plugins/other-plugin/config',
|
|
855
|
+
values: { threshold: 1 },
|
|
856
|
+
})
|
|
857
|
+
})
|
|
858
|
+
|
|
859
|
+
it('should split changed platform-owned settings from a versioned typed save', async () => {
|
|
860
|
+
;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
|
|
861
|
+
isIntegrated: true,
|
|
862
|
+
theme: 'light',
|
|
863
|
+
platformApiUrl: '/api',
|
|
864
|
+
plugin: {
|
|
865
|
+
id: 'drp',
|
|
866
|
+
name: 'drp',
|
|
867
|
+
version: '1.0.0',
|
|
868
|
+
route_prefix: '/drp',
|
|
869
|
+
api_prefix: '/api/drp',
|
|
870
|
+
},
|
|
871
|
+
}
|
|
872
|
+
nextGetData = {
|
|
873
|
+
plugin_name: 'drp',
|
|
874
|
+
config: {
|
|
875
|
+
threshold: 1,
|
|
876
|
+
allowed_experiment_types: ['metabolomics'],
|
|
877
|
+
},
|
|
878
|
+
revision: 'revision-1',
|
|
879
|
+
}
|
|
880
|
+
nextPatchData = {
|
|
881
|
+
plugin_name: 'drp',
|
|
882
|
+
config: {
|
|
883
|
+
threshold: 2,
|
|
884
|
+
allowed_experiment_types: ['proteomics'],
|
|
885
|
+
},
|
|
886
|
+
revision: 'untrusted-platform-revision',
|
|
887
|
+
}
|
|
888
|
+
const settings = usePluginSettings<{ threshold?: number }>({ pluginName: 'drp' })
|
|
889
|
+
await settings.load()
|
|
890
|
+
settings.setValues({
|
|
891
|
+
threshold: 2,
|
|
892
|
+
allowed_experiment_types: ['proteomics'],
|
|
893
|
+
})
|
|
894
|
+
|
|
895
|
+
const saved = await settings.save()
|
|
896
|
+
nextPatchData = {
|
|
897
|
+
plugin_name: 'drp',
|
|
898
|
+
config: {
|
|
899
|
+
threshold: 3,
|
|
900
|
+
allowed_experiment_types: ['proteomics'],
|
|
901
|
+
},
|
|
902
|
+
revision: 'revision-3',
|
|
903
|
+
}
|
|
904
|
+
settings.setValues({
|
|
905
|
+
threshold: 3,
|
|
906
|
+
allowed_experiment_types: ['proteomics'],
|
|
907
|
+
})
|
|
908
|
+
const savedAgain = await settings.save()
|
|
909
|
+
|
|
910
|
+
expect({
|
|
911
|
+
saved,
|
|
912
|
+
savedAgain,
|
|
913
|
+
methods: requestMethods,
|
|
914
|
+
bodies: requestBodies,
|
|
915
|
+
writeHeaders: requestConfigs.slice(1).map(config => config.headers),
|
|
916
|
+
values: settings.values.value,
|
|
917
|
+
isDirty: settings.isDirty.value,
|
|
918
|
+
}).toEqual({
|
|
919
|
+
saved: true,
|
|
920
|
+
savedAgain: true,
|
|
921
|
+
methods: ['get', 'patch', 'patch', 'patch'],
|
|
922
|
+
bodies: [
|
|
923
|
+
{ config: { threshold: 2 } },
|
|
924
|
+
{ config: { allowed_experiment_types: ['proteomics'] } },
|
|
925
|
+
{ config: { threshold: 3 } },
|
|
926
|
+
],
|
|
927
|
+
writeHeaders: [undefined, undefined, undefined],
|
|
928
|
+
values: {
|
|
929
|
+
threshold: 3,
|
|
930
|
+
allowed_experiment_types: ['proteomics'],
|
|
931
|
+
},
|
|
932
|
+
isDirty: false,
|
|
933
|
+
})
|
|
934
|
+
})
|
|
935
|
+
|
|
936
|
+
it('should avoid a typed PUT when only the platform-owned setting changes', async () => {
|
|
937
|
+
;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
|
|
938
|
+
isIntegrated: true,
|
|
939
|
+
theme: 'light',
|
|
940
|
+
platformApiUrl: '/api',
|
|
941
|
+
plugin: {
|
|
942
|
+
id: 'drp',
|
|
943
|
+
name: 'drp',
|
|
944
|
+
version: '1.0.0',
|
|
945
|
+
route_prefix: '/drp',
|
|
946
|
+
api_prefix: '/api/drp',
|
|
947
|
+
},
|
|
948
|
+
}
|
|
949
|
+
nextGetData = {
|
|
950
|
+
plugin_name: 'drp',
|
|
951
|
+
config: {
|
|
952
|
+
threshold: 1,
|
|
953
|
+
allowed_experiment_types: ['metabolomics'],
|
|
954
|
+
},
|
|
955
|
+
revision: 'revision-1',
|
|
956
|
+
}
|
|
957
|
+
nextPatchData = {
|
|
958
|
+
plugin_name: 'drp',
|
|
959
|
+
config: {
|
|
960
|
+
threshold: 1,
|
|
961
|
+
allowed_experiment_types: ['proteomics'],
|
|
962
|
+
},
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const settings = usePluginSettings<{ threshold?: number }>({ pluginName: 'drp' })
|
|
966
|
+
await settings.load()
|
|
967
|
+
settings.setValues({
|
|
968
|
+
threshold: 1,
|
|
969
|
+
allowed_experiment_types: ['proteomics'],
|
|
970
|
+
})
|
|
971
|
+
|
|
972
|
+
const saved = await settings.save()
|
|
973
|
+
|
|
974
|
+
expect({
|
|
975
|
+
saved,
|
|
976
|
+
methods: requestMethods,
|
|
977
|
+
bodies: requestBodies,
|
|
978
|
+
values: settings.values.value,
|
|
979
|
+
}).toEqual({
|
|
980
|
+
saved: true,
|
|
981
|
+
methods: ['get', 'patch'],
|
|
982
|
+
bodies: [{ config: { allowed_experiment_types: ['proteomics'] } }],
|
|
983
|
+
values: {
|
|
984
|
+
threshold: 1,
|
|
985
|
+
allowed_experiment_types: ['proteomics'],
|
|
986
|
+
},
|
|
987
|
+
})
|
|
988
|
+
})
|
|
989
|
+
|
|
990
|
+
it('should detect nested settings edits and restore the saved JSON snapshot', async () => {
|
|
991
|
+
nextGetData = {
|
|
992
|
+
settings: {
|
|
993
|
+
rules: {
|
|
994
|
+
thresholds: [1, 2],
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
mode: 'standalone',
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const settings = usePluginSettings<{
|
|
1001
|
+
rules?: { thresholds: number[] }
|
|
1002
|
+
}>({
|
|
1003
|
+
apiBaseUrl: '/api/drp',
|
|
1004
|
+
loadOnMount: false,
|
|
1005
|
+
})
|
|
1006
|
+
await settings.load()
|
|
1007
|
+
settings.values.value.rules!.thresholds[0] = 9
|
|
1008
|
+
|
|
1009
|
+
const dirtyAfterMutation = settings.isDirty.value
|
|
1010
|
+
settings.reset()
|
|
1011
|
+
|
|
1012
|
+
expect({
|
|
1013
|
+
dirtyAfterMutation,
|
|
1014
|
+
values: settings.values.value,
|
|
1015
|
+
isDirty: settings.isDirty.value,
|
|
1016
|
+
}).toEqual({
|
|
1017
|
+
dirtyAfterMutation: true,
|
|
1018
|
+
values: {
|
|
1019
|
+
rules: {
|
|
1020
|
+
thresholds: [1, 2],
|
|
1021
|
+
},
|
|
1022
|
+
},
|
|
1023
|
+
isDirty: false,
|
|
1024
|
+
})
|
|
710
1025
|
})
|
|
711
1026
|
|
|
712
1027
|
it('loads and saves plugin settings through the standalone settings router', async () => {
|
|
713
|
-
nextGetData = {
|
|
714
|
-
|
|
1028
|
+
nextGetData = {
|
|
1029
|
+
settings: { nas_base_path: '/data' },
|
|
1030
|
+
mode: 'standalone',
|
|
1031
|
+
revision: 'revision-1',
|
|
1032
|
+
}
|
|
1033
|
+
nextPutData = {
|
|
1034
|
+
settings: { nas_base_path: '/mnt' },
|
|
1035
|
+
mode: 'standalone',
|
|
1036
|
+
revision: 'revision-2',
|
|
1037
|
+
}
|
|
715
1038
|
|
|
716
1039
|
const settings = usePluginSettings<{ nas_base_path?: string }>({
|
|
717
1040
|
apiBaseUrl: '/api/drp',
|
|
@@ -725,6 +1048,7 @@ describe('usePluginClient', () => {
|
|
|
725
1048
|
expect(requestConfigs[0]!.url).toBe('/settings')
|
|
726
1049
|
expect(requestConfigs[1]!.baseURL).toBe('/api/drp')
|
|
727
1050
|
expect(requestConfigs[1]!.url).toBe('/settings')
|
|
1051
|
+
expect(requestConfigs[1]!.headers).toEqual({ 'If-Match': '"revision-1"' })
|
|
728
1052
|
expect(requestBodies[0]).toEqual({ nas_base_path: '/mnt' })
|
|
729
1053
|
expect(settings.settingsConfig.value.values).toEqual({ nas_base_path: '/mnt' })
|
|
730
1054
|
})
|
|
@@ -774,6 +1098,28 @@ describe('usePluginClient', () => {
|
|
|
774
1098
|
expect(stream.lastEventId.value).toBe('7')
|
|
775
1099
|
})
|
|
776
1100
|
|
|
1101
|
+
it('omits MINT auth from a public plugin event stream', async () => {
|
|
1102
|
+
const authStore = useAuthStore()
|
|
1103
|
+
authStore.setToken('stream-token', 3600)
|
|
1104
|
+
const fetchMock = vi.fn<typeof fetch>(async () =>
|
|
1105
|
+
new Response(new ReadableStream({ start: controller => controller.close() }), {
|
|
1106
|
+
status: 200,
|
|
1107
|
+
}),
|
|
1108
|
+
)
|
|
1109
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
1110
|
+
|
|
1111
|
+
usePluginEventStream(
|
|
1112
|
+
contract,
|
|
1113
|
+
{ method: 'get', path: '/public/events', requiresAuth: false },
|
|
1114
|
+
{ reconnect: false },
|
|
1115
|
+
)
|
|
1116
|
+
|
|
1117
|
+
await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledOnce())
|
|
1118
|
+
const [, init] = fetchMock.mock.calls[0]!
|
|
1119
|
+
|
|
1120
|
+
expect((init?.headers as Headers).get('Authorization')).toBeNull()
|
|
1121
|
+
})
|
|
1122
|
+
|
|
777
1123
|
it('normalizes stream auth failures and does not reconnect rejected requests', async () => {
|
|
778
1124
|
const authStore = useAuthStore()
|
|
779
1125
|
authStore.setToken('expired-stream-token', 3600)
|