@morscherlab/mint-sdk 1.1.0-beta.1 → 1.1.0-beta.10
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__/components/SectionCard.test.d.ts +1 -0
- package/dist/__tests__/composables/analysisArtifactTarget.test.d.ts +1 -0
- package/dist/__tests__/types/GeneratedJsonSchema.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/SectionCard.vue.d.ts +47 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +6 -6
- package/dist/{components-UfkPN1bK.js → components-Ci_BTx00.js} +711 -633
- 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/usePlatformContext.d.ts +3 -0
- package/dist/composables/usePluginClient.d.ts +11 -5
- package/dist/{composables-F4JFK9K1.js → composables-C0VEJ73O.js} +172 -30
- package/dist/composables-C0VEJ73O.js.map +1 -0
- package/dist/index.js +11 -11
- package/dist/install.js +5 -5
- package/dist/styles.css +312 -11
- package/dist/tailwind.preset.d.ts +5 -0
- package/dist/tailwind.preset.js +7 -2
- package/dist/tailwind.preset.js.map +1 -1
- 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/components.d.ts +2 -0
- package/dist/types/generated-ui.d.ts +13 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/platform.d.ts +2 -0
- 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__/components/SectionCard.test.ts +181 -0
- 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/usePlatformContext.test.ts +28 -0
- package/src/__tests__/composables/usePluginClient.test.ts +365 -21
- package/src/__tests__/composables/usePluginConfig.test.ts +323 -7
- package/src/__tests__/types/GeneratedJsonSchema.test.ts +62 -0
- 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/components/SectionCard.story.vue +163 -0
- package/src/components/SectionCard.vue +98 -0
- package/src/components/index.ts +1 -0
- 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 +130 -20
- package/src/composables/usePluginConfig.ts +156 -20
- package/src/styles/components/section-card.css +161 -0
- package/src/styles/index.css +1 -0
- package/src/styles/variables.css +25 -0
- package/src/tailwind.preset.ts +5 -0
- package/src/types/analysisArtifactTypes.ts +9 -0
- package/src/types/components.ts +12 -0
- package/src/types/generated-ui.ts +13 -0
- package/src/types/index.ts +3 -0
- package/src/types/platform.ts +2 -0
- package/dist/EmptyState-D1c4RdC7.js.map +0 -1
- package/dist/components-UfkPN1bK.js.map +0 -1
- package/dist/composables-F4JFK9K1.js.map +0 -1
- package/dist/useControlSchema-Dallso-L.js.map +0 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import SectionCard from './SectionCard.vue'
|
|
3
|
+
import type { SectionCardAccent, SectionCardDivider } from '../types'
|
|
4
|
+
|
|
5
|
+
const flaskIcon =
|
|
6
|
+
'M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4'
|
|
7
|
+
const folderIcon = 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z'
|
|
8
|
+
|
|
9
|
+
const experiments = [
|
|
10
|
+
{ code: 'MM-2026-014', name: '13C glucose tracing — HeLa, 24h', updated: '2h ago' },
|
|
11
|
+
{ code: 'MM-2026-013', name: 'Hypoxia timecourse — passage 6', updated: '5h ago' },
|
|
12
|
+
{ code: 'MM-2026-011', name: 'Glutamine dropout, triplicate', updated: 'yesterday' },
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
function initPlayground() {
|
|
16
|
+
return {
|
|
17
|
+
title: 'Experiments',
|
|
18
|
+
count: '14 ongoing · 42 total',
|
|
19
|
+
accent: 'experiment' as SectionCardAccent,
|
|
20
|
+
uppercase: true,
|
|
21
|
+
divider: 'strong' as SectionCardDivider,
|
|
22
|
+
showIcon: true,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<Story title="Layout/SectionCard">
|
|
29
|
+
<Variant title="Playground" :init-state="initPlayground">
|
|
30
|
+
<template #default="{ state }">
|
|
31
|
+
<div style="padding: 2rem; max-width: 600px;">
|
|
32
|
+
<SectionCard
|
|
33
|
+
:title="state.title"
|
|
34
|
+
:count="state.count || undefined"
|
|
35
|
+
:icon="state.showIcon ? flaskIcon : undefined"
|
|
36
|
+
:accent="state.accent"
|
|
37
|
+
:uppercase="state.uppercase"
|
|
38
|
+
:divider="state.divider"
|
|
39
|
+
>
|
|
40
|
+
<div class="mint-section-card__rows">
|
|
41
|
+
<a
|
|
42
|
+
v-for="e in experiments"
|
|
43
|
+
:key="e.code"
|
|
44
|
+
href="#"
|
|
45
|
+
class="mint-section-card__row"
|
|
46
|
+
>
|
|
47
|
+
<span style="flex: 1; min-width: 0">
|
|
48
|
+
<span style="display: block; font-size: 0.8125rem; font-weight: 500; color: var(--text-primary)">{{ e.name }}</span>
|
|
49
|
+
<span style="display: block; font-family: 'Fira Code', monospace; font-size: 0.65625rem; color: var(--text-muted)">{{ e.code }}</span>
|
|
50
|
+
</span>
|
|
51
|
+
<span style="flex-shrink: 0; font-size: 0.71875rem; color: var(--text-muted)">{{ e.updated }}</span>
|
|
52
|
+
</a>
|
|
53
|
+
</div>
|
|
54
|
+
</SectionCard>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<template #controls="{ state }">
|
|
59
|
+
<HstText v-model="state.title" title="Title" />
|
|
60
|
+
<HstText v-model="state.count" title="Count" />
|
|
61
|
+
<HstCheckbox v-model="state.showIcon" title="Show icon chip" />
|
|
62
|
+
<HstCheckbox v-model="state.uppercase" title="Uppercase title" />
|
|
63
|
+
<HstSelect
|
|
64
|
+
v-model="state.accent"
|
|
65
|
+
title="Accent"
|
|
66
|
+
:options="{
|
|
67
|
+
primary: 'primary',
|
|
68
|
+
experiment: 'experiment',
|
|
69
|
+
project: 'project',
|
|
70
|
+
success: 'success',
|
|
71
|
+
error: 'error',
|
|
72
|
+
warning: 'warning',
|
|
73
|
+
info: 'info',
|
|
74
|
+
}"
|
|
75
|
+
/>
|
|
76
|
+
<HstSelect
|
|
77
|
+
v-model="state.divider"
|
|
78
|
+
title="Divider"
|
|
79
|
+
:options="{ strong: 'strong', light: 'light' }"
|
|
80
|
+
/>
|
|
81
|
+
</template>
|
|
82
|
+
</Variant>
|
|
83
|
+
|
|
84
|
+
<Variant title="Home category card">
|
|
85
|
+
<div style="padding: 2rem; max-width: 600px;">
|
|
86
|
+
<SectionCard
|
|
87
|
+
title="Projects"
|
|
88
|
+
count="6 active · 9 total"
|
|
89
|
+
:icon="folderIcon"
|
|
90
|
+
accent="project"
|
|
91
|
+
uppercase
|
|
92
|
+
>
|
|
93
|
+
<template #actions>
|
|
94
|
+
<a href="#" style="font-size: 0.75rem; font-weight: 500; color: var(--color-primary-hover)">All →</a>
|
|
95
|
+
</template>
|
|
96
|
+
<div class="mint-section-card__rows">
|
|
97
|
+
<a href="#" class="mint-section-card__row">
|
|
98
|
+
<span style="flex: 1; font-size: 0.8125rem; font-weight: 500; color: var(--text-primary)">Hypoxia metabolic rewiring</span>
|
|
99
|
+
<span style="font-size: 0.71875rem; color: var(--text-muted)">12 experiments</span>
|
|
100
|
+
</a>
|
|
101
|
+
<a href="#" class="mint-section-card__row">
|
|
102
|
+
<span style="flex: 1; font-size: 0.8125rem; font-weight: 500; color: var(--text-primary)">Serine one-carbon flux</span>
|
|
103
|
+
<span style="font-size: 0.71875rem; color: var(--text-muted)">7 experiments</span>
|
|
104
|
+
</a>
|
|
105
|
+
</div>
|
|
106
|
+
</SectionCard>
|
|
107
|
+
</div>
|
|
108
|
+
</Variant>
|
|
109
|
+
|
|
110
|
+
<Variant title="Detail card with footer">
|
|
111
|
+
<div style="padding: 2rem; max-width: 600px;">
|
|
112
|
+
<SectionCard title="Analysis artifacts" :count="4" divider="light">
|
|
113
|
+
<div class="mint-section-card__rows">
|
|
114
|
+
<div class="mint-section-card__row">
|
|
115
|
+
<span style="flex: 1; font-size: 0.8125rem; color: var(--text-primary)">isotopologue_matrix.csv</span>
|
|
116
|
+
<span style="font-size: 0.71875rem; color: var(--text-secondary)">DRP</span>
|
|
117
|
+
</div>
|
|
118
|
+
<div class="mint-section-card__row">
|
|
119
|
+
<span style="flex: 1; font-size: 0.8125rem; color: var(--text-primary)">natural_abundance_corrected.parquet</span>
|
|
120
|
+
<span style="font-size: 0.71875rem; color: var(--text-secondary)">DRP</span>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
<template #footer>
|
|
124
|
+
<span style="font-size: 0.71875rem; color: var(--text-muted)">Showing 1–2 of 4</span>
|
|
125
|
+
<span style="font-size: 0.71875rem; color: var(--text-secondary)">1 / 2</span>
|
|
126
|
+
</template>
|
|
127
|
+
</SectionCard>
|
|
128
|
+
</div>
|
|
129
|
+
</Variant>
|
|
130
|
+
|
|
131
|
+
<Variant title="Fill with pinned toolbar">
|
|
132
|
+
<div style="padding: 2rem; height: 420px; display: flex;">
|
|
133
|
+
<SectionCard title="All experiments" :count="42" divider="light" fill>
|
|
134
|
+
<template #toolbar>
|
|
135
|
+
<div style="display: flex; gap: 0.5rem; padding: 0.6875rem 1rem; border-bottom: 1px solid var(--border-color)">
|
|
136
|
+
<span style="font-size: 0.78125rem; color: var(--text-secondary)">Toolbar stays put while the rows scroll</span>
|
|
137
|
+
</div>
|
|
138
|
+
</template>
|
|
139
|
+
<div class="mint-section-card__rows">
|
|
140
|
+
<div v-for="n in 20" :key="n" class="mint-section-card__row">
|
|
141
|
+
<span style="flex: 1; font-size: 0.8125rem; color: var(--text-primary)">Row {{ n }}</span>
|
|
142
|
+
<span style="font-family: 'Fira Code', monospace; font-size: 0.71875rem; color: var(--text-muted)">MM-2026-{{ String(n).padStart(3, '0') }}</span>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
<template #footer>
|
|
146
|
+
<span style="font-size: 0.71875rem; color: var(--text-muted)">Showing 1–20 of 42</span>
|
|
147
|
+
<span style="font-size: 0.71875rem; color: var(--text-secondary)">1 / 3</span>
|
|
148
|
+
</template>
|
|
149
|
+
</SectionCard>
|
|
150
|
+
</div>
|
|
151
|
+
</Variant>
|
|
152
|
+
|
|
153
|
+
<Variant title="No icon, plain title">
|
|
154
|
+
<div style="padding: 2rem; max-width: 600px;">
|
|
155
|
+
<SectionCard title="Design data" count="96 samples · 4 timepoints" divider="light">
|
|
156
|
+
<div style="padding: 1rem; font-size: 0.8125rem; color: var(--text-secondary)">
|
|
157
|
+
Card body content sits directly on the card surface — no nested card.
|
|
158
|
+
</div>
|
|
159
|
+
</SectionCard>
|
|
160
|
+
</div>
|
|
161
|
+
</Variant>
|
|
162
|
+
</Story>
|
|
163
|
+
</template>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/** One-card page region from the MINT 1.1 card system: 12px radius, the
|
|
3
|
+
* single --card-shadow token, a header strip (icon chip, title, mono count,
|
|
4
|
+
* actions), and hairline-separated content — no card-in-card. */
|
|
5
|
+
import { computed } from 'vue'
|
|
6
|
+
import type { SectionCardAccent, SectionCardDivider } from '../types'
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
title: string
|
|
10
|
+
/** Monospace meta shown next to the title, e.g. "14 ongoing · 42 total". */
|
|
11
|
+
count?: string | number
|
|
12
|
+
/** SVG path(s) for the 22px icon chip; omit to render no chip. */
|
|
13
|
+
icon?: string | string[]
|
|
14
|
+
/** Tint of the icon chip. */
|
|
15
|
+
accent?: SectionCardAccent
|
|
16
|
+
/** Uppercase category heading (home cards) vs plain heading (detail cards). */
|
|
17
|
+
uppercase?: boolean
|
|
18
|
+
/** Header hairline: 'strong' = --border-color, 'light' = --border-light. */
|
|
19
|
+
divider?: SectionCardDivider
|
|
20
|
+
/**
|
|
21
|
+
* Stretch to fill the height its parent offers, scrolling the body and
|
|
22
|
+
* pinning the header and footer. Requires a parent that gives the card a
|
|
23
|
+
* definite height (a flex or grid track).
|
|
24
|
+
*/
|
|
25
|
+
fill?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
29
|
+
count: undefined,
|
|
30
|
+
icon: undefined,
|
|
31
|
+
accent: 'primary',
|
|
32
|
+
uppercase: false,
|
|
33
|
+
divider: 'strong',
|
|
34
|
+
fill: false,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const iconPaths = computed(() =>
|
|
38
|
+
props.icon === undefined ? [] : Array.isArray(props.icon) ? props.icon : [props.icon],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const headerClasses = computed(() => [
|
|
42
|
+
'mint-section-card__header',
|
|
43
|
+
props.divider === 'light' ? 'mint-section-card__header--divider-light' : '',
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
const titleClasses = computed(() => [
|
|
47
|
+
'mint-section-card__title',
|
|
48
|
+
props.uppercase ? 'mint-section-card__title--uppercase' : '',
|
|
49
|
+
])
|
|
50
|
+
|
|
51
|
+
const cardClasses = computed(() => [
|
|
52
|
+
'mint-section-card',
|
|
53
|
+
props.fill ? 'mint-section-card--fill' : '',
|
|
54
|
+
])
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<template>
|
|
58
|
+
<section :class="cardClasses">
|
|
59
|
+
<div :class="headerClasses">
|
|
60
|
+
<span
|
|
61
|
+
v-if="iconPaths.length"
|
|
62
|
+
:class="['mint-section-card__icon-chip', `mint-section-card__icon-chip--${accent}`]"
|
|
63
|
+
>
|
|
64
|
+
<svg
|
|
65
|
+
class="mint-section-card__icon"
|
|
66
|
+
viewBox="0 0 24 24"
|
|
67
|
+
fill="none"
|
|
68
|
+
stroke="currentColor"
|
|
69
|
+
stroke-width="2"
|
|
70
|
+
stroke-linecap="round"
|
|
71
|
+
stroke-linejoin="round"
|
|
72
|
+
aria-hidden="true"
|
|
73
|
+
>
|
|
74
|
+
<path v-for="(d, i) in iconPaths" :key="i" :d="d" />
|
|
75
|
+
</svg>
|
|
76
|
+
</span>
|
|
77
|
+
<h2 :class="titleClasses">{{ title }}</h2>
|
|
78
|
+
<span v-if="count !== undefined" class="mint-section-card__count">{{ count }}</span>
|
|
79
|
+
<div class="mint-section-card__spacer"></div>
|
|
80
|
+
<slot name="actions" />
|
|
81
|
+
</div>
|
|
82
|
+
<!-- Controls belong to the card's chrome, so they sit outside the body and
|
|
83
|
+
stay put while the body scrolls. -->
|
|
84
|
+
<div v-if="$slots.toolbar" class="mint-section-card__toolbar">
|
|
85
|
+
<slot name="toolbar" />
|
|
86
|
+
</div>
|
|
87
|
+
<div class="mint-section-card__body">
|
|
88
|
+
<slot />
|
|
89
|
+
</div>
|
|
90
|
+
<div v-if="$slots.footer" class="mint-section-card__footer">
|
|
91
|
+
<slot name="footer" />
|
|
92
|
+
</div>
|
|
93
|
+
</section>
|
|
94
|
+
</template>
|
|
95
|
+
|
|
96
|
+
<style>
|
|
97
|
+
@import '../styles/components/section-card.css';
|
|
98
|
+
</style>
|
package/src/components/index.ts
CHANGED
|
@@ -126,6 +126,7 @@ export { default as DateTimePicker } from './DateTimePicker.vue'
|
|
|
126
126
|
export { default as TimeRangeInput } from './TimeRangeInput.vue'
|
|
127
127
|
export { default as ScheduleCalendar } from './ScheduleCalendar.vue'
|
|
128
128
|
export { default as ResourceCard } from './ResourceCard.vue'
|
|
129
|
+
export { default as SectionCard } from './SectionCard.vue'
|
|
129
130
|
|
|
130
131
|
// Experiment / analysis components
|
|
131
132
|
export { default as ExperimentSelectorModal } from './ExperimentSelectorModal.vue'
|
|
@@ -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,
|
|
@@ -12,6 +12,12 @@ export function getInjectedPlatformContext(): PlatformContext | undefined {
|
|
|
12
12
|
return (window as unknown as { __MINT_PLATFORM__?: PlatformContext }).__MINT_PLATFORM__
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/** Return the platform's canonical plugin identity for the mounted frontend. */
|
|
16
|
+
export function resolveCurrentPluginId(): string | undefined {
|
|
17
|
+
const pluginId = getInjectedPlatformContext()?.plugin?.id
|
|
18
|
+
return typeof pluginId === 'string' && pluginId ? pluginId : undefined
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
export function getInjectedExperimentContext(): ExperimentAwarePlatformContext | undefined {
|
|
16
22
|
return getInjectedPlatformContext() as ExperimentAwarePlatformContext | undefined
|
|
17
23
|
}
|
|
@@ -38,10 +44,11 @@ export function currentExperimentIdFromContext(
|
|
|
38
44
|
|
|
39
45
|
export function currentExperimentIdFromUrl(): number | undefined {
|
|
40
46
|
if (typeof window === 'undefined') return undefined
|
|
41
|
-
const
|
|
47
|
+
const location = window.location
|
|
48
|
+
const params = new URLSearchParams(location?.search ?? '')
|
|
42
49
|
return parseExperimentId(params.get('experimentId') ?? params.get('experiment_id'))
|
|
43
|
-
?? experimentIdFromPath(
|
|
44
|
-
?? experimentIdFromPath(
|
|
50
|
+
?? experimentIdFromPath(location?.pathname ?? '')
|
|
51
|
+
?? experimentIdFromPath((location?.hash ?? '').replace(/^#\/?/, '/'))
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
export function resolveCurrentExperimentId(
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
AnalysisArtifactMetadataUpdate,
|
|
20
20
|
AnalysisArtifactStatusFilter,
|
|
21
21
|
AnalysisArtifactSummary,
|
|
22
|
+
AnalysisArtifactTarget,
|
|
22
23
|
} from '../types/analysisArtifactTypes'
|
|
23
24
|
|
|
24
25
|
export { ANALYSIS_FILE_ARTIFACT_SCHEMA } from '../types/analysisArtifactTypes'
|
|
@@ -67,6 +68,8 @@ export interface UseAnalysisArtifactsReturn {
|
|
|
67
68
|
refresh: () => Promise<AnalysisArtifactSummary[]>
|
|
68
69
|
/** Fetch one artifact with its result payload; null when it fails or is superseded. */
|
|
69
70
|
getDetail: (artifactId: number) => Promise<AnalysisArtifactDetail | null>
|
|
71
|
+
/** Resolve and fetch the exact stable artifact identity. */
|
|
72
|
+
resolveTarget: (target: AnalysisArtifactTarget) => Promise<AnalysisArtifactDetail | null>
|
|
70
73
|
/** Update display_name and/or note; resolves to the updated summary or null. */
|
|
71
74
|
updateMetadata: (
|
|
72
75
|
artifactId: number,
|
|
@@ -206,6 +209,50 @@ export function useAnalysisArtifacts(
|
|
|
206
209
|
}
|
|
207
210
|
}
|
|
208
211
|
|
|
212
|
+
async function resolveTarget(
|
|
213
|
+
target: AnalysisArtifactTarget,
|
|
214
|
+
): Promise<AnalysisArtifactDetail | null> {
|
|
215
|
+
const experimentId = currentExperimentId.value
|
|
216
|
+
if (experimentId !== target.experimentId) {
|
|
217
|
+
request.setError(
|
|
218
|
+
`Analysis artifact target belongs to experiment ${target.experimentId}, not `
|
|
219
|
+
+ `${experimentId ?? 'the current selection'}.`,
|
|
220
|
+
)
|
|
221
|
+
return null
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const available = await list()
|
|
225
|
+
const summary = available.find((artifact) => (
|
|
226
|
+
artifact.plugin_id === target.artifactPluginId
|
|
227
|
+
&& artifact.artifact_key === target.artifactKey
|
|
228
|
+
&& artifact.status === 'active'
|
|
229
|
+
))
|
|
230
|
+
if (!summary) {
|
|
231
|
+
request.setError(
|
|
232
|
+
`Analysis artifact ${target.artifactPluginId}/${target.artifactKey} is not available.`,
|
|
233
|
+
)
|
|
234
|
+
return null
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const detail = await getDetail(summary.id)
|
|
238
|
+
if (
|
|
239
|
+
detail
|
|
240
|
+
&& (
|
|
241
|
+
detail.experiment_id !== target.experimentId
|
|
242
|
+
|| detail.plugin_id !== target.artifactPluginId
|
|
243
|
+
|| detail.artifact_key !== target.artifactKey
|
|
244
|
+
)
|
|
245
|
+
) {
|
|
246
|
+
selectedDetail.value = null
|
|
247
|
+
request.setError(
|
|
248
|
+
`Analysis artifact ${target.artifactPluginId}/${target.artifactKey} did not `
|
|
249
|
+
+ 'match the requested identity.',
|
|
250
|
+
)
|
|
251
|
+
return null
|
|
252
|
+
}
|
|
253
|
+
return detail
|
|
254
|
+
}
|
|
255
|
+
|
|
209
256
|
async function mutate(
|
|
210
257
|
action: string,
|
|
211
258
|
operation: (experimentId: number) => Promise<AnalysisArtifactSummary>,
|
|
@@ -296,6 +343,7 @@ export function useAnalysisArtifacts(
|
|
|
296
343
|
list,
|
|
297
344
|
refresh: list,
|
|
298
345
|
getDetail,
|
|
346
|
+
resolveTarget,
|
|
299
347
|
updateMetadata,
|
|
300
348
|
archive,
|
|
301
349
|
restore,
|
|
@@ -75,7 +75,10 @@ export interface UsePluginJobsReturn extends PluginJobCenterSource {
|
|
|
75
75
|
status: ShallowRef<'idle' | 'initializing' | 'ready' | 'paused' | 'error' | 'disposed'>
|
|
76
76
|
initialize: () => Promise<void>
|
|
77
77
|
ready: () => Promise<void>
|
|
78
|
-
uploadPath: (
|
|
78
|
+
uploadPath: (
|
|
79
|
+
input: PluginJobPathInput,
|
|
80
|
+
definitionId?: string,
|
|
81
|
+
) => Promise<PluginJobPathUpload>
|
|
79
82
|
uploadStagedPart: <Metadata>(
|
|
80
83
|
input: PluginJobStagedPartInput<Metadata>,
|
|
81
84
|
) => Promise<PluginJobStagedPartUploadResult<Metadata>>
|
|
@@ -279,7 +282,7 @@ function createPluginJobs(
|
|
|
279
282
|
jobInput = await submitToSession(
|
|
280
283
|
session.value.id,
|
|
281
284
|
clientRequestId,
|
|
282
|
-
await stagePathInputs(inputs, session.value.id),
|
|
285
|
+
await stagePathInputs(inputs, session.value.id, definitionId),
|
|
283
286
|
)
|
|
284
287
|
} catch (cause) {
|
|
285
288
|
if (!isSessionNotFoundError(cause)) throw cause
|
|
@@ -287,7 +290,7 @@ function createPluginJobs(
|
|
|
287
290
|
jobInput = await submitToSession(
|
|
288
291
|
session.value.id,
|
|
289
292
|
clientRequestId,
|
|
290
|
-
await stagePathInputs(inputs, session.value.id),
|
|
293
|
+
await stagePathInputs(inputs, session.value.id, definitionId),
|
|
291
294
|
)
|
|
292
295
|
}
|
|
293
296
|
const job = upsertJob(jobInput, definitionId)
|
|
@@ -317,9 +320,12 @@ function createPluginJobs(
|
|
|
317
320
|
}
|
|
318
321
|
}
|
|
319
322
|
|
|
320
|
-
async function uploadPath(
|
|
323
|
+
async function uploadPath(
|
|
324
|
+
input: PluginJobPathInput,
|
|
325
|
+
definitionId?: string,
|
|
326
|
+
): Promise<PluginJobPathUpload> {
|
|
321
327
|
if (!session.value) throw new Error('Generated analysis session is not initialized')
|
|
322
|
-
return uploadPathForSession(input, session.value.id)
|
|
328
|
+
return uploadPathForSession(input, session.value.id, definitionId)
|
|
323
329
|
}
|
|
324
330
|
|
|
325
331
|
async function uploadStagedPart<Metadata>(
|
|
@@ -375,6 +381,7 @@ function createPluginJobs(
|
|
|
375
381
|
async function uploadPathForSession(
|
|
376
382
|
input: PluginJobPathInput,
|
|
377
383
|
sessionId: string,
|
|
384
|
+
definitionId?: string,
|
|
378
385
|
): Promise<PluginJobPathUpload> {
|
|
379
386
|
if (!input.files.length || input.files.length !== input.relativePaths.length) {
|
|
380
387
|
throw new Error('Path upload requires one relative path for each file')
|
|
@@ -386,40 +393,51 @@ function createPluginJobs(
|
|
|
386
393
|
body.append('files', input.files[index]!)
|
|
387
394
|
body.append('relative_paths', input.relativePaths[index]!)
|
|
388
395
|
}
|
|
396
|
+
const headers: Record<string, string> = {
|
|
397
|
+
'X-Mint-Job-Session': sessionId,
|
|
398
|
+
}
|
|
399
|
+
if (definitionId) headers['X-Mint-Job-Definition'] = definitionId
|
|
389
400
|
return request<PluginJobPathUpload>('/jobs/uploads', {
|
|
390
401
|
method: 'POST',
|
|
391
402
|
body,
|
|
392
403
|
signal: input.signal,
|
|
393
|
-
headers
|
|
404
|
+
headers,
|
|
394
405
|
})
|
|
395
406
|
}
|
|
396
407
|
|
|
397
408
|
async function stagePathInputs(
|
|
398
409
|
inputs: Record<string, unknown>,
|
|
399
410
|
sessionId: string,
|
|
411
|
+
definitionId: string,
|
|
400
412
|
): Promise<Record<string, unknown>> {
|
|
401
|
-
return await stagePathValue(inputs, sessionId) as Record<string, unknown>
|
|
413
|
+
return await stagePathValue(inputs, sessionId, definitionId) as Record<string, unknown>
|
|
402
414
|
}
|
|
403
415
|
|
|
404
|
-
async function stagePathValue(
|
|
416
|
+
async function stagePathValue(
|
|
417
|
+
value: unknown,
|
|
418
|
+
sessionId: string,
|
|
419
|
+
definitionId: string,
|
|
420
|
+
): Promise<unknown> {
|
|
405
421
|
if (typeof File !== 'undefined' && value instanceof File) {
|
|
406
422
|
const upload = await uploadPathForSession({
|
|
407
423
|
kind: 'path',
|
|
408
424
|
files: [value],
|
|
409
425
|
relativePaths: [value.name],
|
|
410
|
-
}, sessionId)
|
|
426
|
+
}, sessionId, definitionId)
|
|
411
427
|
return { token: upload.token }
|
|
412
428
|
}
|
|
413
429
|
if (isPluginJobPathInput(value)) {
|
|
414
|
-
const upload = await uploadPathForSession(value, sessionId)
|
|
430
|
+
const upload = await uploadPathForSession(value, sessionId, definitionId)
|
|
415
431
|
return { token: upload.token }
|
|
416
432
|
}
|
|
417
433
|
if (Array.isArray(value)) {
|
|
418
|
-
return await Promise.all(value.map(
|
|
434
|
+
return await Promise.all(value.map(
|
|
435
|
+
item => stagePathValue(item, sessionId, definitionId),
|
|
436
|
+
))
|
|
419
437
|
}
|
|
420
438
|
if (value && typeof value === 'object') {
|
|
421
439
|
const entries = await Promise.all(Object.entries(value).map(async ([key, child]) => (
|
|
422
|
-
[key, await stagePathValue(child, sessionId)] as const
|
|
440
|
+
[key, await stagePathValue(child, sessionId, definitionId)] as const
|
|
423
441
|
)))
|
|
424
442
|
return Object.fromEntries(entries)
|
|
425
443
|
}
|