@morscherlab/mint-sdk 1.1.0-beta.4 → 1.1.0-beta.6
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 +18 -4
- package/dist/{EmptyState-D1c4RdC7.js → EmptyState-DgBRoYAy.js} +22 -13
- package/dist/EmptyState-DgBRoYAy.js.map +1 -0
- package/dist/{ExperimentSelectorModal-B7w6k3Y8.js → ExperimentSelectorModal-BBWXC7KW.js} +3 -3
- package/dist/{ExperimentSelectorModal-B7w6k3Y8.js.map → ExperimentSelectorModal-BBWXC7KW.js.map} +1 -1
- package/dist/{SettingsModal-DPDwCbXK.js → SettingsModal-Dpg-X1OM.js} +3 -3
- package/dist/{SettingsModal-DPDwCbXK.js.map → SettingsModal-Dpg-X1OM.js.map} +1 -1
- package/dist/__tests__/components/BaseToggle.test.d.ts +1 -0
- package/dist/__tests__/composables/analysisArtifactTarget.test.d.ts +1 -0
- package/dist/analysisArtifactTypes-ejqR8MX3.js.map +1 -1
- package/dist/components/BaseSelect.vue.d.ts +1 -1
- package/dist/components/BaseToggle.vue.d.ts +2 -0
- package/dist/components/FitPanel.vue.d.ts +1 -1
- package/dist/components/PluginWorkspaceView.props.d.ts +1 -1
- package/dist/components/index.js +5 -5
- package/dist/{components-DFH5whuh.js → components-Ci_BTx00.js} +13 -14
- package/dist/components-Ci_BTx00.js.map +1 -0
- package/dist/composables/analysisArtifactTarget.d.ts +5 -0
- package/dist/composables/index.d.ts +3 -1
- package/dist/composables/index.js +8 -8
- package/dist/composables/platformContextHelpers.d.ts +2 -0
- package/dist/composables/useAnalysisArtifacts.d.ts +3 -1
- package/dist/composables/useGeneratedAnalysis.d.ts +1 -1
- package/dist/composables/usePluginClient.d.ts +11 -5
- package/dist/{composables-B512bWJP.js → composables-C0VEJ73O.js} +40 -5
- package/dist/composables-C0VEJ73O.js.map +1 -0
- package/dist/index.js +11 -11
- package/dist/install.js +5 -5
- package/dist/styles.css +11 -11
- package/dist/templates/index.js +3 -3
- package/dist/{templates-Cttbjbyc.js → templates-Cdj93S9C.js} +2 -2
- package/dist/{templates-Cttbjbyc.js.map → templates-Cdj93S9C.js.map} +1 -1
- package/dist/types/analysisArtifactTypes.d.ts +8 -0
- package/dist/types/generated-ui.d.ts +2 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/{useControlSchema-Dallso-L.js → useControlSchema-B_qgU4rW.js} +48 -19
- package/dist/useControlSchema-B_qgU4rW.js.map +1 -0
- package/dist/{useExperimentSelector-BMjzNcf2.js → useExperimentSelector-BuQZRgMt.js} +2 -2
- package/dist/{useExperimentSelector-BMjzNcf2.js.map → useExperimentSelector-BuQZRgMt.js.map} +1 -1
- package/dist/{useFormBuilder-BXaaDI41.js → useFormBuilder-D1G5J5Z5.js} +2 -2
- package/dist/{useFormBuilder-BXaaDI41.js.map → useFormBuilder-D1G5J5Z5.js.map} +1 -1
- package/dist/{useProtocolTemplates-BP7_4RGQ.js → useProtocolTemplates-DeaOmMaw.js} +4 -4
- package/dist/{useProtocolTemplates-BP7_4RGQ.js.map → useProtocolTemplates-DeaOmMaw.js.map} +1 -1
- package/package.json +1 -1
- package/src/__tests__/components/BaseSelect.test.ts +52 -0
- package/src/__tests__/components/BaseToggle.test.ts +40 -0
- package/src/__tests__/components/GeneratedPluginView.test.ts +10 -0
- package/src/__tests__/components/PluginWorkspaceView.test.ts +68 -14
- package/src/__tests__/composables/analysisArtifactTarget.test.ts +77 -0
- package/src/__tests__/composables/useAnalysisArtifacts.test.ts +74 -1
- package/src/__tests__/composables/useGeneratedAnalysis.test.ts +2 -0
- package/src/__tests__/composables/usePluginClient.test.ts +36 -1
- package/src/components/BaseSelect.vue +16 -2
- package/src/components/BaseToggle.vue +3 -1
- package/src/components/GeneratedPluginView.vue +1 -1
- package/src/components/PluginWorkspaceView.props.ts +1 -1
- package/src/components/PluginWorkspaceView.vue +1 -5
- package/src/composables/analysisArtifactTarget.ts +53 -0
- package/src/composables/index.ts +6 -0
- package/src/composables/platformContextHelpers.ts +10 -3
- package/src/composables/useAnalysisArtifacts.ts +48 -0
- package/src/composables/useGeneratedAnalysis.ts +30 -12
- package/src/composables/usePluginClient.ts +12 -5
- package/src/types/analysisArtifactTypes.ts +9 -0
- package/src/types/generated-ui.ts +2 -0
- package/src/types/index.ts +1 -0
- package/dist/EmptyState-D1c4RdC7.js.map +0 -1
- package/dist/components-DFH5whuh.js.map +0 -1
- package/dist/composables-B512bWJP.js.map +0 -1
- package/dist/useControlSchema-Dallso-L.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morscherlab/mint-sdk",
|
|
3
|
-
"version": "1.1.0-beta.
|
|
3
|
+
"version": "1.1.0-beta.6",
|
|
4
4
|
"description": "MINT Platform SDK — Vue 3 components, composables, and types for plugin development. MINT = Mass-spec INtegrated Toolkit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -3,6 +3,58 @@ import { describe, expect, it } from 'vitest'
|
|
|
3
3
|
import BaseSelect from '../../components/BaseSelect.vue'
|
|
4
4
|
|
|
5
5
|
describe('BaseSelect', () => {
|
|
6
|
+
it('forwards native attributes to the select instead of the wrapper', () => {
|
|
7
|
+
const wrapper = mount(BaseSelect, {
|
|
8
|
+
props: {
|
|
9
|
+
options: ['linear'],
|
|
10
|
+
},
|
|
11
|
+
attrs: {
|
|
12
|
+
id: 'curve-model',
|
|
13
|
+
name: 'curve-model',
|
|
14
|
+
'aria-label': 'Curve model',
|
|
15
|
+
'data-testid': 'curve-model-select',
|
|
16
|
+
class: 'curve-model-layout',
|
|
17
|
+
style: 'width: 12rem;',
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const select = wrapper.get('select')
|
|
22
|
+
|
|
23
|
+
expect({
|
|
24
|
+
select: {
|
|
25
|
+
id: select.attributes('id'),
|
|
26
|
+
name: select.attributes('name'),
|
|
27
|
+
ariaLabel: select.attributes('aria-label'),
|
|
28
|
+
testId: select.attributes('data-testid'),
|
|
29
|
+
layoutClass: select.classes().includes('curve-model-layout'),
|
|
30
|
+
},
|
|
31
|
+
wrapper: {
|
|
32
|
+
id: wrapper.attributes('id'),
|
|
33
|
+
name: wrapper.attributes('name'),
|
|
34
|
+
ariaLabel: wrapper.attributes('aria-label'),
|
|
35
|
+
testId: wrapper.attributes('data-testid'),
|
|
36
|
+
layoutClass: wrapper.classes().includes('curve-model-layout'),
|
|
37
|
+
style: wrapper.attributes('style'),
|
|
38
|
+
},
|
|
39
|
+
}).toEqual({
|
|
40
|
+
select: {
|
|
41
|
+
id: 'curve-model',
|
|
42
|
+
name: 'curve-model',
|
|
43
|
+
ariaLabel: 'Curve model',
|
|
44
|
+
testId: 'curve-model-select',
|
|
45
|
+
layoutClass: false,
|
|
46
|
+
},
|
|
47
|
+
wrapper: {
|
|
48
|
+
id: undefined,
|
|
49
|
+
name: undefined,
|
|
50
|
+
ariaLabel: undefined,
|
|
51
|
+
testId: undefined,
|
|
52
|
+
layoutClass: true,
|
|
53
|
+
style: 'width: 12rem;',
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
6
58
|
it('accepts string and number shorthand options', async () => {
|
|
7
59
|
const wrapper = mount(BaseSelect, {
|
|
8
60
|
props: {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import BaseToggle from '../../components/BaseToggle.vue'
|
|
4
|
+
|
|
5
|
+
describe('BaseToggle', () => {
|
|
6
|
+
it('uses ariaLabel for the switch name without changing the visual label', () => {
|
|
7
|
+
const wrapper = mount(BaseToggle, {
|
|
8
|
+
props: {
|
|
9
|
+
label: 'Enabled',
|
|
10
|
+
ariaLabel: 'Email notifications',
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
expect({
|
|
15
|
+
accessibleName: wrapper.get('[role="switch"]').attributes('aria-label'),
|
|
16
|
+
visualText: wrapper.text(),
|
|
17
|
+
wrapperAriaLabel: wrapper.attributes('aria-label'),
|
|
18
|
+
}).toEqual({
|
|
19
|
+
accessibleName: 'Email notifications',
|
|
20
|
+
visualText: 'Enabled',
|
|
21
|
+
wrapperAriaLabel: undefined,
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('keeps using the visual label as the default switch name', () => {
|
|
26
|
+
const wrapper = mount(BaseToggle, {
|
|
27
|
+
props: {
|
|
28
|
+
label: 'Enabled',
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
expect(wrapper.get('[role="switch"]').attributes('aria-label')).toBe('Enabled')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('keeps the existing fallback name when no labels are provided', () => {
|
|
36
|
+
const wrapper = mount(BaseToggle)
|
|
37
|
+
|
|
38
|
+
expect(wrapper.get('[role="switch"]').attributes('aria-label')).toBe('Toggle')
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -190,6 +190,16 @@ describe('GeneratedPluginView', () => {
|
|
|
190
190
|
expect(wrapper.find('[aria-label="Job resource requirements"]').text()).toBe('Host service')
|
|
191
191
|
})
|
|
192
192
|
|
|
193
|
+
it('describes process CPU declarations as an upper bound', async () => {
|
|
194
|
+
const wrapper = mount(GeneratedPluginView, {
|
|
195
|
+
props: { baseUrl: '/api/demo' },
|
|
196
|
+
global: { plugins: [createPinia()], stubs: { 'router-link': true, Teleport: true } },
|
|
197
|
+
})
|
|
198
|
+
await flushPromises()
|
|
199
|
+
|
|
200
|
+
expect(wrapper.find('[aria-label="Job resource requirements"]').text()).toBe('Up to 2 CPU')
|
|
201
|
+
})
|
|
202
|
+
|
|
193
203
|
it('should pass the host transport to the generated analysis composable', async () => {
|
|
194
204
|
const transport = vi.fn()
|
|
195
205
|
mount(GeneratedPluginView, {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { flushPromises, mount } from '@vue/test-utils'
|
|
2
|
+
import type { VueWrapper } from '@vue/test-utils'
|
|
2
3
|
import { createPinia } from 'pinia'
|
|
3
4
|
import { computed, h, nextTick, ref } from 'vue'
|
|
4
5
|
import { describe, expect, it, vi } from 'vitest'
|
|
@@ -10,7 +11,7 @@ import ExperimentSelectorModal from '../../components/ExperimentSelectorModal.vu
|
|
|
10
11
|
import PluginWorkspaceView from '../../components/PluginWorkspaceView.vue'
|
|
11
12
|
import { defineDoseDesignControlModel } from '../../composables/useControlSchema'
|
|
12
13
|
import { usePlatformContext } from '../../composables/usePlatformContext'
|
|
13
|
-
import type { PillNavItem, SidebarToolSection } from '../../types'
|
|
14
|
+
import type { PageSelectorItemInput, PillNavItem, SidebarToolSection } from '../../types'
|
|
14
15
|
|
|
15
16
|
vi.mock('../../composables/usePlatformContext', () => ({
|
|
16
17
|
usePlatformContext: vi.fn(() => ({
|
|
@@ -79,6 +80,44 @@ const globalOptions = {
|
|
|
79
80
|
},
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
interface PageSelectorWorkspaceProps {
|
|
84
|
+
pageSelector?: PageSelectorItemInput[]
|
|
85
|
+
currentPageSelectorId?: string
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function mountIntegratedPageSelectorWorkspace(
|
|
89
|
+
props: PageSelectorWorkspaceProps = {},
|
|
90
|
+
): VueWrapper {
|
|
91
|
+
vi.mocked(usePlatformContext).mockReturnValueOnce({
|
|
92
|
+
isIntegrated: computed(() => true),
|
|
93
|
+
context: ref({ isIntegrated: true, theme: 'system' }),
|
|
94
|
+
plugin: computed(() => ({
|
|
95
|
+
id: 'workspace-plugin',
|
|
96
|
+
name: 'Workspace Plugin',
|
|
97
|
+
version: '1.0.0',
|
|
98
|
+
route_prefix: '/workspace-plugin',
|
|
99
|
+
api_prefix: '/api/workspace-plugin',
|
|
100
|
+
nav_items: [
|
|
101
|
+
{ path: '/platform-one', label: 'Platform One' },
|
|
102
|
+
{ path: '/platform-two', label: 'Platform Two' },
|
|
103
|
+
],
|
|
104
|
+
})),
|
|
105
|
+
user: computed(() => undefined),
|
|
106
|
+
theme: computed(() => 'system' as const),
|
|
107
|
+
features: computed(() => undefined),
|
|
108
|
+
navigate: vi.fn(),
|
|
109
|
+
notify: vi.fn(),
|
|
110
|
+
sendToPlatform: vi.fn(),
|
|
111
|
+
})
|
|
112
|
+
return mount(PluginWorkspaceView, {
|
|
113
|
+
props: {
|
|
114
|
+
title: 'Workspace',
|
|
115
|
+
...props,
|
|
116
|
+
},
|
|
117
|
+
global: globalOptions,
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
82
121
|
describe('PluginWorkspaceView', () => {
|
|
83
122
|
it('renders the standard AppLayout, AppTopBar, and AppSidebar plugin shell', () => {
|
|
84
123
|
const wrapper = mount(PluginWorkspaceView, {
|
|
@@ -146,22 +185,37 @@ describe('PluginWorkspaceView', () => {
|
|
|
146
185
|
expect(wrapper.emitted('pill-select')).toEqual([[{ id: 'results', label: 'Results' }]])
|
|
147
186
|
})
|
|
148
187
|
|
|
149
|
-
it('
|
|
150
|
-
const wrapper =
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
188
|
+
it('should use platform pages when pageSelector is omitted', () => {
|
|
189
|
+
const wrapper = mountIntegratedPageSelectorWorkspace()
|
|
190
|
+
|
|
191
|
+
expect(wrapper.find('.mint-page-selector').exists()).toBe(true)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('should suppress platform pages when pageSelector is explicitly empty', () => {
|
|
195
|
+
const wrapper = mountIntegratedPageSelectorWorkspace({ pageSelector: [] })
|
|
196
|
+
|
|
197
|
+
expect(wrapper.find('.mint-page-selector').exists()).toBe(false)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('should hide one explicit page without falling back to platform pages', () => {
|
|
201
|
+
const wrapper = mountIntegratedPageSelectorWorkspace({
|
|
202
|
+
pageSelector: [{ id: 'downloads', label: 'Downloads', to: '/' }],
|
|
203
|
+
currentPageSelectorId: 'downloads',
|
|
160
204
|
})
|
|
161
205
|
|
|
162
|
-
expect(wrapper.findComponent(AppTopBar).props('pageSelector')).toBeUndefined()
|
|
163
206
|
expect(wrapper.find('.mint-page-selector').exists()).toBe(false)
|
|
164
|
-
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('should render multiple explicit pages ahead of platform pages', () => {
|
|
210
|
+
const wrapper = mountIntegratedPageSelectorWorkspace({
|
|
211
|
+
pageSelector: [
|
|
212
|
+
{ id: 'overview', label: 'Overview', to: '/' },
|
|
213
|
+
{ id: 'reports', label: 'Reports', to: '/reports' },
|
|
214
|
+
],
|
|
215
|
+
currentPageSelectorId: 'overview',
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
expect(wrapper.get('.mint-page-selector__label').text()).toBe('Overview')
|
|
165
219
|
})
|
|
166
220
|
|
|
167
221
|
it('passes compact control models to the sidebar for generated forms', () => {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildAnalysisArtifactTargetQuery,
|
|
5
|
+
parseAnalysisArtifactTarget,
|
|
6
|
+
} from '../../composables/analysisArtifactTarget'
|
|
7
|
+
import {
|
|
8
|
+
currentExperimentIdFromUrl,
|
|
9
|
+
resolveCurrentPluginId,
|
|
10
|
+
} from '../../composables/platformContextHelpers'
|
|
11
|
+
|
|
12
|
+
describe('analysis artifact target', () => {
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
delete (window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__
|
|
15
|
+
vi.unstubAllGlobals()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('should resolve the canonical injected plugin id', () => {
|
|
19
|
+
;(window as unknown as { __MINT_PLATFORM__: unknown }).__MINT_PLATFORM__ = {
|
|
20
|
+
isIntegrated: true,
|
|
21
|
+
plugin: { id: 'LEAF', name: 'Leaf UI' },
|
|
22
|
+
theme: 'system',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
expect(resolveCurrentPluginId()).toBe('LEAF')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should tolerate a host that provides a partial location object', () => {
|
|
29
|
+
vi.stubGlobal('window', {
|
|
30
|
+
location: { search: '' },
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
expect(currentExperimentIdFromUrl()).toBeUndefined()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should build the canonical readable query when an artifact id hint is present', () => {
|
|
37
|
+
const query = buildAnalysisArtifactTargetQuery({
|
|
38
|
+
experimentId: 66,
|
|
39
|
+
artifactPluginId: 'LEAF',
|
|
40
|
+
artifactKey: 'exp316_260703_ys_rna_ide_scr',
|
|
41
|
+
artifactId: 206,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
expect(query.toString()).toBe(
|
|
45
|
+
'experiment_id=66&artifact_plugin_id=LEAF'
|
|
46
|
+
+ '&artifact_key=exp316_260703_ys_rna_ide_scr&artifact_id=206',
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should parse the canonical identity without requiring an artifact id hint', () => {
|
|
51
|
+
const target = parseAnalysisArtifactTarget(new URLSearchParams(
|
|
52
|
+
'experiment_id=66&artifact_plugin_id=LEAF&artifact_key=result',
|
|
53
|
+
))
|
|
54
|
+
|
|
55
|
+
expect(target).toEqual({
|
|
56
|
+
experimentId: 66,
|
|
57
|
+
artifactPluginId: 'LEAF',
|
|
58
|
+
artifactKey: 'result',
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should reject an incomplete target instead of guessing another artifact', () => {
|
|
63
|
+
const target = parseAnalysisArtifactTarget(new URLSearchParams(
|
|
64
|
+
'experiment_id=66&artifact_key=result',
|
|
65
|
+
))
|
|
66
|
+
|
|
67
|
+
expect(target).toBeNull()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should reject non-positive numeric identifiers', () => {
|
|
71
|
+
const target = parseAnalysisArtifactTarget(new URLSearchParams(
|
|
72
|
+
'experiment_id=0&artifact_plugin_id=LEAF&artifact_key=result&artifact_id=-1',
|
|
73
|
+
))
|
|
74
|
+
|
|
75
|
+
expect(target).toBeNull()
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -19,7 +19,10 @@ vi.mock('../../composables/useApi', () => ({
|
|
|
19
19
|
}))
|
|
20
20
|
|
|
21
21
|
import { useAnalysisArtifacts } from '../../composables/useAnalysisArtifacts'
|
|
22
|
-
import type {
|
|
22
|
+
import type {
|
|
23
|
+
AnalysisArtifactDetail,
|
|
24
|
+
AnalysisArtifactSummary,
|
|
25
|
+
} from '../../types/analysisArtifactTypes'
|
|
23
26
|
|
|
24
27
|
// ---------------------------------------------------------------------------
|
|
25
28
|
// Fixtures
|
|
@@ -177,6 +180,76 @@ describe('useAnalysisArtifacts', () => {
|
|
|
177
180
|
})
|
|
178
181
|
})
|
|
179
182
|
|
|
183
|
+
describe('resolveTarget', () => {
|
|
184
|
+
it('should resolve the exact stable identity when the numeric hint is stale', async () => {
|
|
185
|
+
const detail = {
|
|
186
|
+
...makeArtifact({
|
|
187
|
+
id: 2,
|
|
188
|
+
experiment_id: 42,
|
|
189
|
+
plugin_id: 'plugin-b',
|
|
190
|
+
artifact_key: 'peaks',
|
|
191
|
+
}),
|
|
192
|
+
result: { schema_version: 'mint.analysis_file.v1' },
|
|
193
|
+
tree: [],
|
|
194
|
+
summary: null,
|
|
195
|
+
} satisfies AnalysisArtifactDetail
|
|
196
|
+
mockGet
|
|
197
|
+
.mockResolvedValueOnce({ artifacts: ARTIFACTS })
|
|
198
|
+
.mockResolvedValueOnce(detail)
|
|
199
|
+
const { resolveTarget } = setup()
|
|
200
|
+
|
|
201
|
+
const resolved = await resolveTarget({
|
|
202
|
+
experimentId: 42,
|
|
203
|
+
artifactPluginId: 'plugin-b',
|
|
204
|
+
artifactKey: 'peaks',
|
|
205
|
+
artifactId: 999,
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
expect({
|
|
209
|
+
resolvedId: resolved?.id,
|
|
210
|
+
detailPath: mockGet.mock.calls[1]?.[0],
|
|
211
|
+
}).toEqual({
|
|
212
|
+
resolvedId: 2,
|
|
213
|
+
detailPath: '/experiments/42/analysis-artifacts/2',
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('should fail explicitly when the stable identity is not available', async () => {
|
|
218
|
+
const { resolveTarget, error } = setup()
|
|
219
|
+
|
|
220
|
+
const resolved = await resolveTarget({
|
|
221
|
+
experimentId: 42,
|
|
222
|
+
artifactPluginId: 'LEAF',
|
|
223
|
+
artifactKey: 'missing',
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
expect({ resolved, error: error.value }).toEqual({
|
|
227
|
+
resolved: null,
|
|
228
|
+
error: 'Analysis artifact LEAF/missing is not available.',
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('should reject a target from another experiment before making a request', async () => {
|
|
233
|
+
const { resolveTarget, error } = setup()
|
|
234
|
+
|
|
235
|
+
const resolved = await resolveTarget({
|
|
236
|
+
experimentId: 66,
|
|
237
|
+
artifactPluginId: 'LEAF',
|
|
238
|
+
artifactKey: 'result',
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
expect({
|
|
242
|
+
resolved,
|
|
243
|
+
error: error.value,
|
|
244
|
+
requests: mockGet.mock.calls,
|
|
245
|
+
}).toEqual({
|
|
246
|
+
resolved: null,
|
|
247
|
+
error: 'Analysis artifact target belongs to experiment 66, not 42.',
|
|
248
|
+
requests: [],
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
|
|
180
253
|
// -------------------------------------------------------------------------
|
|
181
254
|
// experiment id resolution
|
|
182
255
|
// -------------------------------------------------------------------------
|
|
@@ -407,6 +407,7 @@ describe('useGeneratedAnalysis', () => {
|
|
|
407
407
|
uploadUrl: upload?.[0],
|
|
408
408
|
uploadMethod: upload?.[1]?.method,
|
|
409
409
|
uploadSessionHeader: new Headers(upload?.[1]?.headers).get('X-Mint-Job-Session'),
|
|
410
|
+
uploadDefinitionHeader: new Headers(upload?.[1]?.headers).get('X-Mint-Job-Definition'),
|
|
410
411
|
sessionId: form.get('session_id'),
|
|
411
412
|
relativePaths: form.getAll('relative_paths'),
|
|
412
413
|
submittedSource: submit.inputs.source,
|
|
@@ -414,6 +415,7 @@ describe('useGeneratedAnalysis', () => {
|
|
|
414
415
|
uploadUrl: '/api/demo/jobs/uploads',
|
|
415
416
|
uploadMethod: 'POST',
|
|
416
417
|
uploadSessionHeader: 'session-1',
|
|
418
|
+
uploadDefinitionHeader: 'run',
|
|
417
419
|
sessionId: 'session-1',
|
|
418
420
|
relativePaths: ['data.csv'],
|
|
419
421
|
submittedSource: { token: 'upload-token' },
|
|
@@ -22,12 +22,37 @@ import { AuthenticationRequiredError } from '../../composables/useApi'
|
|
|
22
22
|
import { useAuthStore } from '../../stores/auth'
|
|
23
23
|
|
|
24
24
|
const contract: PluginContract = {
|
|
25
|
-
schemaVersion:
|
|
25
|
+
schemaVersion: 2,
|
|
26
26
|
plugin: {
|
|
27
27
|
routesPrefix: '/drp',
|
|
28
28
|
apiPrefix: '/api/drp',
|
|
29
29
|
type: 'analysis',
|
|
30
30
|
},
|
|
31
|
+
settings: {
|
|
32
|
+
modelName: 'PluginSettings',
|
|
33
|
+
valueSchema: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
threshold: { type: 'number', default: 0.05 },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
formSchema: {
|
|
40
|
+
groups: [
|
|
41
|
+
{
|
|
42
|
+
id: 'settings',
|
|
43
|
+
label: 'Settings',
|
|
44
|
+
fields: [
|
|
45
|
+
{
|
|
46
|
+
name: 'threshold',
|
|
47
|
+
label: 'Threshold',
|
|
48
|
+
type: 'number',
|
|
49
|
+
defaultValue: 0.05,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
31
56
|
endpoints: [],
|
|
32
57
|
hash: 'sha256:test',
|
|
33
58
|
}
|
|
@@ -818,6 +843,16 @@ describe('usePluginClient', () => {
|
|
|
818
843
|
})
|
|
819
844
|
})
|
|
820
845
|
|
|
846
|
+
it('should preserve the contract v2 form schema in settingsConfig', () => {
|
|
847
|
+
const settings = usePluginSettings({
|
|
848
|
+
apiBaseUrl: '/api/drp',
|
|
849
|
+
loadOnMount: false,
|
|
850
|
+
schema: contract.settings.formSchema,
|
|
851
|
+
})
|
|
852
|
+
|
|
853
|
+
expect(settings.settingsConfig.value.schema).toBe(contract.settings.formSchema)
|
|
854
|
+
})
|
|
855
|
+
|
|
821
856
|
it('should use the platform facade for an explicit different plugin name', async () => {
|
|
822
857
|
;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
|
|
823
858
|
isIntegrated: true,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/** Renders a native `<select>` element with size, error, and disabled states. */
|
|
3
|
-
import { computed } from 'vue'
|
|
3
|
+
import { computed, useAttrs } from 'vue'
|
|
4
|
+
import type { HTMLAttributes } from 'vue'
|
|
4
5
|
import type { SelectOption, SelectOptionInput } from '../types'
|
|
5
6
|
import { findOptionByDomValue, normalizeOptionInput } from '../utils/options'
|
|
6
7
|
|
|
8
|
+
defineOptions({ inheritAttrs: false })
|
|
9
|
+
|
|
7
10
|
interface Props {
|
|
8
11
|
modelValue?: string | number
|
|
9
12
|
options: SelectOptionInput<string | number>[]
|
|
@@ -19,6 +22,16 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
19
22
|
error: false,
|
|
20
23
|
size: 'md',
|
|
21
24
|
})
|
|
25
|
+
const attrs = useAttrs()
|
|
26
|
+
|
|
27
|
+
const controlAttrs = computed(() => {
|
|
28
|
+
const { class: _class, style: _style, ...nativeAttrs } = attrs
|
|
29
|
+
return nativeAttrs
|
|
30
|
+
})
|
|
31
|
+
const wrapperAttrs = computed<Pick<HTMLAttributes, 'class' | 'style'>>(() => ({
|
|
32
|
+
class: attrs.class as HTMLAttributes['class'],
|
|
33
|
+
style: attrs.style as HTMLAttributes['style'],
|
|
34
|
+
}))
|
|
22
35
|
|
|
23
36
|
const emit = defineEmits<{
|
|
24
37
|
'update:modelValue': [value: string | number]
|
|
@@ -36,8 +49,9 @@ function handleChange(event: Event) {
|
|
|
36
49
|
</script>
|
|
37
50
|
|
|
38
51
|
<template>
|
|
39
|
-
<div class="mint-select">
|
|
52
|
+
<div v-bind="wrapperAttrs" class="mint-select">
|
|
40
53
|
<select
|
|
54
|
+
v-bind="controlAttrs"
|
|
41
55
|
:value="modelValue"
|
|
42
56
|
:disabled="disabled"
|
|
43
57
|
:aria-invalid="error || undefined"
|
|
@@ -5,6 +5,8 @@ import Tooltip from './Tooltip.vue'
|
|
|
5
5
|
interface Props {
|
|
6
6
|
modelValue?: boolean
|
|
7
7
|
label?: string
|
|
8
|
+
/** Accessible switch name; defaults to the visual label, then "Toggle". */
|
|
9
|
+
ariaLabel?: string
|
|
8
10
|
description?: string
|
|
9
11
|
icon?: string | string[]
|
|
10
12
|
iconColor?: string
|
|
@@ -111,7 +113,7 @@ function iconStyle(): Record<string, string> {
|
|
|
111
113
|
role="switch"
|
|
112
114
|
:tabindex="disabled ? -1 : 0"
|
|
113
115
|
:aria-checked="modelValue"
|
|
114
|
-
:aria-label="label || 'Toggle'"
|
|
116
|
+
:aria-label="ariaLabel || label || 'Toggle'"
|
|
115
117
|
:class="[
|
|
116
118
|
'mint-toggle__track',
|
|
117
119
|
`mint-toggle__track--${size}`,
|
|
@@ -116,7 +116,7 @@ function resourceSummary(definition: GeneratedJobDefinition): string {
|
|
|
116
116
|
if (definition.lifecycle === 'staged' || definition.profile?.kind === 'staged') {
|
|
117
117
|
return 'Staged upload'
|
|
118
118
|
}
|
|
119
|
-
return
|
|
119
|
+
return `Up to ${definition.cpu} CPU`
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
onMounted(() => {
|
|
@@ -34,7 +34,7 @@ export interface PluginWorkspaceViewProps {
|
|
|
34
34
|
homePath?: string
|
|
35
35
|
/** Show the default MINT logo when no icon/logo slot is provided. */
|
|
36
36
|
showLogo?: boolean
|
|
37
|
-
/** Preferred route-level page selector entries. */
|
|
37
|
+
/** Preferred route-level page selector entries. Pass [] to suppress integrated platform fallback. */
|
|
38
38
|
pageSelector?: PageSelectorItemInput[]
|
|
39
39
|
/** Active page selector id. Defaults to activeView. */
|
|
40
40
|
currentPageSelectorId?: string
|
|
@@ -208,10 +208,6 @@ const {
|
|
|
208
208
|
emitPillSelect: (item) => emit('pill-select', item),
|
|
209
209
|
})
|
|
210
210
|
|
|
211
|
-
const visiblePageSelector = computed(() =>
|
|
212
|
-
(props.pageSelector?.length ?? 0) > 1 ? props.pageSelector : undefined,
|
|
213
|
-
)
|
|
214
|
-
|
|
215
211
|
const hasSidebarSurface = computed(() => {
|
|
216
212
|
return hasPluginWorkspaceSidebarSurface({
|
|
217
213
|
showSidebar: props.showSidebar,
|
|
@@ -274,7 +270,7 @@ function handleFormCancel(sectionId: string) {
|
|
|
274
270
|
:variant="topBarVariant"
|
|
275
271
|
:home-path="homePath"
|
|
276
272
|
:show-logo="showLogo"
|
|
277
|
-
:page-selector="
|
|
273
|
+
:page-selector="pageSelector"
|
|
278
274
|
:current-page-selector-id="resolvedCurrentPageSelectorId"
|
|
279
275
|
:plugin-switcher="pluginSwitcher"
|
|
280
276
|
:pill-nav="resolvedPillNav"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { AnalysisArtifactTarget } from '../types/analysisArtifactTypes'
|
|
2
|
+
|
|
3
|
+
const EXPERIMENT_ID = 'experiment_id'
|
|
4
|
+
const ARTIFACT_PLUGIN_ID = 'artifact_plugin_id'
|
|
5
|
+
const ARTIFACT_KEY = 'artifact_key'
|
|
6
|
+
const ARTIFACT_ID = 'artifact_id'
|
|
7
|
+
|
|
8
|
+
function parsePositiveInteger(value: string | null): number | undefined {
|
|
9
|
+
if (value === null || !/^\d+$/.test(value)) return undefined
|
|
10
|
+
const parsed = Number(value)
|
|
11
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Build the canonical query used to open one analysis artifact in a plugin. */
|
|
15
|
+
export function buildAnalysisArtifactTargetQuery(
|
|
16
|
+
target: AnalysisArtifactTarget,
|
|
17
|
+
): URLSearchParams {
|
|
18
|
+
const query = new URLSearchParams()
|
|
19
|
+
query.set(EXPERIMENT_ID, String(target.experimentId))
|
|
20
|
+
query.set(ARTIFACT_PLUGIN_ID, target.artifactPluginId)
|
|
21
|
+
query.set(ARTIFACT_KEY, target.artifactKey)
|
|
22
|
+
if (target.artifactId !== undefined) {
|
|
23
|
+
query.set(ARTIFACT_ID, String(target.artifactId))
|
|
24
|
+
}
|
|
25
|
+
return query
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Parse a complete canonical target. Incomplete or invalid input is not guessed. */
|
|
29
|
+
export function parseAnalysisArtifactTarget(
|
|
30
|
+
query: URLSearchParams,
|
|
31
|
+
): AnalysisArtifactTarget | null {
|
|
32
|
+
const experimentId = parsePositiveInteger(query.get(EXPERIMENT_ID))
|
|
33
|
+
const artifactPluginId = query.get(ARTIFACT_PLUGIN_ID)?.trim()
|
|
34
|
+
const artifactKey = query.get(ARTIFACT_KEY)?.trim()
|
|
35
|
+
const rawArtifactId = query.get(ARTIFACT_ID)
|
|
36
|
+
const artifactId = parsePositiveInteger(rawArtifactId)
|
|
37
|
+
|
|
38
|
+
if (
|
|
39
|
+
experimentId === undefined
|
|
40
|
+
|| !artifactPluginId
|
|
41
|
+
|| !artifactKey
|
|
42
|
+
|| (rawArtifactId !== null && artifactId === undefined)
|
|
43
|
+
) {
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
experimentId,
|
|
49
|
+
artifactPluginId,
|
|
50
|
+
artifactKey,
|
|
51
|
+
...(artifactId === undefined ? {} : { artifactId }),
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/composables/index.ts
CHANGED
|
@@ -159,6 +159,11 @@ export {
|
|
|
159
159
|
type UseAnalysisArtifactsOptions,
|
|
160
160
|
type UseAnalysisArtifactsReturn,
|
|
161
161
|
} from './useAnalysisArtifacts'
|
|
162
|
+
export {
|
|
163
|
+
buildAnalysisArtifactTargetQuery,
|
|
164
|
+
parseAnalysisArtifactTarget,
|
|
165
|
+
} from './analysisArtifactTarget'
|
|
166
|
+
export { resolveCurrentPluginId } from './platformContextHelpers'
|
|
162
167
|
export {
|
|
163
168
|
formatExperimentDate,
|
|
164
169
|
formatExperimentStatus,
|
|
@@ -476,6 +481,7 @@ export {
|
|
|
476
481
|
type DownloadBlobOptions,
|
|
477
482
|
type DownloadPluginEndpointOptions,
|
|
478
483
|
type PluginContract,
|
|
484
|
+
type PluginContractSettings,
|
|
479
485
|
type PluginClientRequestOptions,
|
|
480
486
|
type PluginEndpointContract,
|
|
481
487
|
type PluginEndpointDefinition,
|