@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
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
import { createGeneratedAnalysisTransport } from '../composables/useGeneratedAnalysis'
|
|
5
|
+
import {
|
|
6
|
+
getInjectedPlatformContext,
|
|
7
|
+
resolveCurrentExperimentId,
|
|
8
|
+
} from '../composables/platformContextHelpers'
|
|
9
|
+
import type { AnalysisArtifactDetail } from '../types/analysisArtifactTypes'
|
|
10
|
+
import type { PluginJobPathInput } from '../types/generated-ui'
|
|
11
|
+
import AlertBox from './AlertBox.vue'
|
|
12
|
+
import ArtifactSelectorModal from './ArtifactSelectorModal.vue'
|
|
13
|
+
import BaseButton from './BaseButton.vue'
|
|
14
|
+
import FileUploader from './FileUploader.vue'
|
|
15
|
+
|
|
16
|
+
interface ArtifactFileReference {
|
|
17
|
+
key: string
|
|
18
|
+
plugin_id?: string
|
|
19
|
+
experiment_id?: number
|
|
20
|
+
filename?: string
|
|
21
|
+
content_type?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const props = defineProps<{
|
|
25
|
+
modelValue?: PluginJobPathInput | PluginJobPathInput[] | null
|
|
26
|
+
disabled?: boolean
|
|
27
|
+
multiple?: boolean
|
|
28
|
+
}>()
|
|
29
|
+
|
|
30
|
+
const emit = defineEmits<{
|
|
31
|
+
'update:modelValue': [value: PluginJobPathInput | PluginJobPathInput[] | null]
|
|
32
|
+
}>()
|
|
33
|
+
|
|
34
|
+
const mode = ref<'file' | 'folder'>('file')
|
|
35
|
+
const artifactOpen = ref(false)
|
|
36
|
+
const artifactLoading = ref(false)
|
|
37
|
+
const artifactError = ref('')
|
|
38
|
+
const platform = getInjectedPlatformContext()
|
|
39
|
+
const experimentId = resolveCurrentExperimentId(platform)
|
|
40
|
+
const canSelectArtifact = computed(() => Boolean(platform?.isIntegrated && experimentId))
|
|
41
|
+
const transport = createGeneratedAnalysisTransport()
|
|
42
|
+
|
|
43
|
+
function selectMode(nextMode: 'file' | 'folder'): void {
|
|
44
|
+
mode.value = nextMode
|
|
45
|
+
emit('update:modelValue', null)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function selectFiles(
|
|
49
|
+
files: File[],
|
|
50
|
+
rootName?: string,
|
|
51
|
+
relativePaths?: string[],
|
|
52
|
+
): void {
|
|
53
|
+
const paths = relativePaths ?? files.map(file => file.name)
|
|
54
|
+
const selections: PluginJobPathInput[] = props.multiple && !rootName
|
|
55
|
+
? files.map((file, index) => ({
|
|
56
|
+
kind: 'path',
|
|
57
|
+
files: [file],
|
|
58
|
+
relativePaths: [paths[index] ?? file.name],
|
|
59
|
+
}))
|
|
60
|
+
: [{
|
|
61
|
+
kind: 'path',
|
|
62
|
+
files,
|
|
63
|
+
relativePaths: paths,
|
|
64
|
+
...(rootName ? { rootName } : {}),
|
|
65
|
+
}]
|
|
66
|
+
if (props.multiple) {
|
|
67
|
+
const existing = Array.isArray(props.modelValue) ? props.modelValue : []
|
|
68
|
+
emit('update:modelValue', [...existing, ...selections])
|
|
69
|
+
} else {
|
|
70
|
+
emit('update:modelValue', selections[0] ?? null)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function removeFiles(files: File[]): void {
|
|
75
|
+
const removed = new Set(files)
|
|
76
|
+
const current = Array.isArray(props.modelValue)
|
|
77
|
+
? props.modelValue
|
|
78
|
+
: props.modelValue
|
|
79
|
+
? [props.modelValue]
|
|
80
|
+
: []
|
|
81
|
+
const remaining = current.flatMap((selection) => {
|
|
82
|
+
const indexes = selection.files
|
|
83
|
+
.map((file, index) => removed.has(file) ? -1 : index)
|
|
84
|
+
.filter(index => index >= 0)
|
|
85
|
+
if (!indexes.length) return []
|
|
86
|
+
return [{
|
|
87
|
+
...selection,
|
|
88
|
+
files: indexes.map(index => selection.files[index]!),
|
|
89
|
+
relativePaths: indexes.map(index => selection.relativePaths[index]!),
|
|
90
|
+
}]
|
|
91
|
+
})
|
|
92
|
+
emit(
|
|
93
|
+
'update:modelValue',
|
|
94
|
+
props.multiple ? remaining : remaining[0] ?? null,
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function selectArtifact(detail: AnalysisArtifactDetail): Promise<void> {
|
|
99
|
+
artifactLoading.value = true
|
|
100
|
+
artifactError.value = ''
|
|
101
|
+
try {
|
|
102
|
+
const reference = artifactFileReference(detail)
|
|
103
|
+
const resolvedExperimentId = reference.experiment_id ?? detail.experiment_id
|
|
104
|
+
const pluginId = reference.plugin_id ?? detail.plugin_id
|
|
105
|
+
const baseUrl = (platform?.platformApiUrl ?? '/api').replace(/\/$/, '')
|
|
106
|
+
const objectPath = reference.key.split('/').map(encodeURIComponent).join('/')
|
|
107
|
+
const url = `${baseUrl}/experiments/${resolvedExperimentId}/objects/${objectPath}`
|
|
108
|
+
+ `?plugin_id=${encodeURIComponent(pluginId)}&format=raw`
|
|
109
|
+
const response = await transport(url, { method: 'GET' })
|
|
110
|
+
if (!response.ok) throw new Error(`Artifact download failed (${response.status})`)
|
|
111
|
+
const resultFilename = String(detail.result.filename ?? '')
|
|
112
|
+
const keySegments = reference.key.split('/')
|
|
113
|
+
const filename = reference.filename
|
|
114
|
+
|| resultFilename
|
|
115
|
+
|| keySegments[keySegments.length - 1]
|
|
116
|
+
|| 'artifact.bin'
|
|
117
|
+
const contentType = reference.content_type
|
|
118
|
+
?? response.headers.get('Content-Type')
|
|
119
|
+
?? 'application/octet-stream'
|
|
120
|
+
const file = new File([await response.blob()], filename, { type: contentType })
|
|
121
|
+
selectFiles([file], undefined, [filename])
|
|
122
|
+
} catch (cause) {
|
|
123
|
+
artifactError.value = cause instanceof Error ? cause.message : 'Failed to load artifact'
|
|
124
|
+
} finally {
|
|
125
|
+
artifactLoading.value = false
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function artifactFileReference(detail: AnalysisArtifactDetail): ArtifactFileReference {
|
|
130
|
+
if (detail.result.schema_version !== 'mint.analysis_file.v1') {
|
|
131
|
+
throw new Error('Only file-backed analysis artifacts can be used as a path')
|
|
132
|
+
}
|
|
133
|
+
const reference = detail.result.file
|
|
134
|
+
if (!reference || typeof reference !== 'object' || !('key' in reference)) {
|
|
135
|
+
throw new Error('The selected artifact has no file reference')
|
|
136
|
+
}
|
|
137
|
+
const candidate = reference as Record<string, unknown>
|
|
138
|
+
if (typeof candidate.key !== 'string' || !candidate.key) {
|
|
139
|
+
throw new Error('The selected artifact has no object key')
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
key: candidate.key,
|
|
143
|
+
...(typeof candidate.plugin_id === 'string' ? { plugin_id: candidate.plugin_id } : {}),
|
|
144
|
+
...(typeof candidate.experiment_id === 'number'
|
|
145
|
+
? { experiment_id: candidate.experiment_id }
|
|
146
|
+
: {}),
|
|
147
|
+
...(typeof candidate.filename === 'string' ? { filename: candidate.filename } : {}),
|
|
148
|
+
...(typeof candidate.content_type === 'string'
|
|
149
|
+
? { content_type: candidate.content_type }
|
|
150
|
+
: {}),
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
</script>
|
|
154
|
+
|
|
155
|
+
<template>
|
|
156
|
+
<div class="mint-generated-path-input">
|
|
157
|
+
<div class="mint-generated-path-input__modes" role="group" aria-label="Path type">
|
|
158
|
+
<BaseButton
|
|
159
|
+
size="sm"
|
|
160
|
+
:variant="mode === 'file' ? 'primary' : 'secondary'"
|
|
161
|
+
:disabled="disabled"
|
|
162
|
+
@click="selectMode('file')"
|
|
163
|
+
>
|
|
164
|
+
File
|
|
165
|
+
</BaseButton>
|
|
166
|
+
<BaseButton
|
|
167
|
+
size="sm"
|
|
168
|
+
:variant="mode === 'folder' ? 'primary' : 'secondary'"
|
|
169
|
+
:disabled="disabled"
|
|
170
|
+
@click="selectMode('folder')"
|
|
171
|
+
>
|
|
172
|
+
Folder
|
|
173
|
+
</BaseButton>
|
|
174
|
+
<BaseButton
|
|
175
|
+
v-if="canSelectArtifact"
|
|
176
|
+
size="sm"
|
|
177
|
+
variant="secondary"
|
|
178
|
+
:disabled="disabled"
|
|
179
|
+
:loading="artifactLoading"
|
|
180
|
+
@click="artifactOpen = true"
|
|
181
|
+
>
|
|
182
|
+
Existing artifact
|
|
183
|
+
</BaseButton>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<FileUploader
|
|
187
|
+
:key="mode"
|
|
188
|
+
:mode="mode"
|
|
189
|
+
:multiple="multiple && mode === 'file'"
|
|
190
|
+
:disabled="disabled"
|
|
191
|
+
@upload="selectFiles"
|
|
192
|
+
@remove="removeFiles"
|
|
193
|
+
/>
|
|
194
|
+
|
|
195
|
+
<AlertBox v-if="artifactError" type="error" :message="artifactError" />
|
|
196
|
+
<ArtifactSelectorModal
|
|
197
|
+
v-if="canSelectArtifact"
|
|
198
|
+
v-model="artifactOpen"
|
|
199
|
+
:experiment-id="experimentId"
|
|
200
|
+
:fetch-detail-on-select="true"
|
|
201
|
+
title="Select a file artifact"
|
|
202
|
+
@select-detail="selectArtifact"
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
</template>
|
|
206
|
+
|
|
207
|
+
<style scoped>
|
|
208
|
+
.mint-generated-path-input {
|
|
209
|
+
display: grid;
|
|
210
|
+
gap: 0.75rem;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.mint-generated-path-input__modes {
|
|
214
|
+
display: flex;
|
|
215
|
+
flex-wrap: wrap;
|
|
216
|
+
gap: 0.5rem;
|
|
217
|
+
}
|
|
218
|
+
</style>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
3
|
+
import type * as PlotlyType from 'plotly.js'
|
|
4
|
+
|
|
5
|
+
import ChartContainer from './ChartContainer.vue'
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
spec: Record<string, unknown>
|
|
9
|
+
fallback?: { media_type: string; data: string }
|
|
10
|
+
}>()
|
|
11
|
+
|
|
12
|
+
const chartElement = ref<HTMLDivElement | null>(null)
|
|
13
|
+
const failed = ref(false)
|
|
14
|
+
let plotly: typeof PlotlyType | null = null
|
|
15
|
+
|
|
16
|
+
async function loadPlotly(): Promise<typeof PlotlyType> {
|
|
17
|
+
const module = await import('plotly.js-dist-min')
|
|
18
|
+
return ('default' in module ? module.default : module) as unknown as typeof PlotlyType
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function renderChart(): Promise<void> {
|
|
22
|
+
if (!chartElement.value) return
|
|
23
|
+
failed.value = false
|
|
24
|
+
await nextTick()
|
|
25
|
+
try {
|
|
26
|
+
const renderer = plotly ?? await loadPlotly()
|
|
27
|
+
plotly = renderer
|
|
28
|
+
const data = Array.isArray(props.spec.data) ? props.spec.data : []
|
|
29
|
+
const layout = props.spec.layout && typeof props.spec.layout === 'object'
|
|
30
|
+
? props.spec.layout
|
|
31
|
+
: {}
|
|
32
|
+
await renderer.react(
|
|
33
|
+
chartElement.value,
|
|
34
|
+
data as PlotlyType.Data[],
|
|
35
|
+
layout as Partial<PlotlyType.Layout>,
|
|
36
|
+
{ responsive: true, displaylogo: false },
|
|
37
|
+
)
|
|
38
|
+
failed.value = false
|
|
39
|
+
} catch {
|
|
40
|
+
failed.value = true
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
onMounted(renderChart)
|
|
45
|
+
watch(() => props.spec, async () => {
|
|
46
|
+
await nextTick()
|
|
47
|
+
await renderChart()
|
|
48
|
+
})
|
|
49
|
+
onBeforeUnmount(() => {
|
|
50
|
+
if (plotly && chartElement.value) plotly.purge(chartElement.value)
|
|
51
|
+
})
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
<ChartContainer title="Interactive chart">
|
|
56
|
+
<img
|
|
57
|
+
v-if="failed && fallback"
|
|
58
|
+
class="mint-generated-result__image"
|
|
59
|
+
:src="`data:${fallback.media_type};base64,${fallback.data}`"
|
|
60
|
+
alt="Static Plotly chart fallback"
|
|
61
|
+
>
|
|
62
|
+
<p
|
|
63
|
+
v-else-if="failed"
|
|
64
|
+
class="mint-generated-result__plotly-error"
|
|
65
|
+
role="alert"
|
|
66
|
+
>
|
|
67
|
+
Unable to display this chart.
|
|
68
|
+
</p>
|
|
69
|
+
<div
|
|
70
|
+
ref="chartElement"
|
|
71
|
+
v-show="!failed"
|
|
72
|
+
class="mint-generated-result__plotly"
|
|
73
|
+
aria-label="Interactive Plotly chart"
|
|
74
|
+
/>
|
|
75
|
+
</ChartContainer>
|
|
76
|
+
</template>
|
|
77
|
+
|
|
78
|
+
<style scoped>
|
|
79
|
+
.mint-generated-result__plotly-error {
|
|
80
|
+
margin: 0;
|
|
81
|
+
padding: 1rem;
|
|
82
|
+
border: 1px solid var(--color-danger-border, var(--color-border));
|
|
83
|
+
border-radius: 0.75rem;
|
|
84
|
+
background: var(--color-danger-bg, var(--color-bg-secondary));
|
|
85
|
+
color: var(--color-danger-text, var(--color-text-primary));
|
|
86
|
+
}
|
|
87
|
+
</style>
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
|
3
|
+
|
|
4
|
+
import { generatedJobDefaults, generatedJobFormSchema, normalizeGeneratedJobInput } from '../generated/schema'
|
|
5
|
+
import type { FormSchema } from '../types/form-builder'
|
|
6
|
+
import type {
|
|
7
|
+
GeneratedAnalysisJob,
|
|
8
|
+
GeneratedAnalysisResult,
|
|
9
|
+
GeneratedJobDefinition,
|
|
10
|
+
} from '../types/generated-ui'
|
|
11
|
+
import type { PillNavItem, SidebarToolSection } from '../types'
|
|
12
|
+
import {
|
|
13
|
+
useGeneratedAnalysis,
|
|
14
|
+
type GeneratedAnalysisTransport,
|
|
15
|
+
} from '../composables/useGeneratedAnalysis'
|
|
16
|
+
import AlertBox from './AlertBox.vue'
|
|
17
|
+
import EmptyState from './EmptyState.vue'
|
|
18
|
+
import GeneratedResultRenderer from './GeneratedResultRenderer.vue'
|
|
19
|
+
import JobsStatusTray from './JobsStatusTray.vue'
|
|
20
|
+
import LoadingSpinner from './LoadingSpinner.vue'
|
|
21
|
+
import PluginWorkspaceView from './PluginWorkspaceView.vue'
|
|
22
|
+
|
|
23
|
+
const props = withDefaults(defineProps<{
|
|
24
|
+
baseUrl: string
|
|
25
|
+
showStandaloneLabel?: boolean
|
|
26
|
+
transport?: GeneratedAnalysisTransport
|
|
27
|
+
}>(), {
|
|
28
|
+
showStandaloneLabel: false,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const generated = useGeneratedAnalysis({
|
|
32
|
+
baseUrl: props.baseUrl,
|
|
33
|
+
transport: props.transport,
|
|
34
|
+
})
|
|
35
|
+
const activeDefinitionId = ref('')
|
|
36
|
+
const selectedJobId = ref('')
|
|
37
|
+
const valuesByDefinition = reactive<Record<string, Record<string, unknown>>>({})
|
|
38
|
+
const definitions = computed(() => generated.manifest.value?.jobs ?? [])
|
|
39
|
+
const activeDefinition = computed(() => (
|
|
40
|
+
definitions.value.find(definition => definition.id === activeDefinitionId.value) ?? null
|
|
41
|
+
))
|
|
42
|
+
|
|
43
|
+
const pillNav = computed<PillNavItem[]>(() => definitions.value.map(definition => ({
|
|
44
|
+
id: definition.id,
|
|
45
|
+
label: definition.title,
|
|
46
|
+
})))
|
|
47
|
+
|
|
48
|
+
const panels = computed<Record<string, SidebarToolSection[]>>(() => Object.fromEntries(
|
|
49
|
+
definitions.value.map(definition => [definition.id, [{
|
|
50
|
+
id: definition.id,
|
|
51
|
+
label: 'Parameters',
|
|
52
|
+
subtitle: resourceSummary(definition),
|
|
53
|
+
defaultOpen: true,
|
|
54
|
+
}]]),
|
|
55
|
+
))
|
|
56
|
+
|
|
57
|
+
const forms = computed<Record<string, FormSchema>>(() => Object.fromEntries(
|
|
58
|
+
definitions.value.map(definition => [definition.id, generatedJobFormSchema(definition)]),
|
|
59
|
+
))
|
|
60
|
+
|
|
61
|
+
const currentValues = computed(() => valuesByDefinition[activeDefinitionId.value] ?? {})
|
|
62
|
+
const currentJob = computed(() => {
|
|
63
|
+
const currentSessionId = generated.session.value?.id
|
|
64
|
+
const currentDefinitionId = activeDefinitionId.value
|
|
65
|
+
const selected = generated.jobs.value.find(candidate => (
|
|
66
|
+
candidate.job_id === selectedJobId.value
|
|
67
|
+
&& candidate.status === 'completed'
|
|
68
|
+
&& candidate.metadata.definition_id === currentDefinitionId
|
|
69
|
+
&& candidate.metadata.has_result === true
|
|
70
|
+
))
|
|
71
|
+
return selected ?? generated.jobs.value.find(candidate => (
|
|
72
|
+
candidate.session_id === currentSessionId
|
|
73
|
+
&& candidate.status === 'completed'
|
|
74
|
+
&& candidate.metadata.definition_id === currentDefinitionId
|
|
75
|
+
&& candidate.metadata.has_result === true
|
|
76
|
+
))
|
|
77
|
+
})
|
|
78
|
+
const currentResult = computed<GeneratedAnalysisResult | null>(() => (
|
|
79
|
+
currentJob.value ? generated.resultForJob(currentJob.value.job_id) : null
|
|
80
|
+
))
|
|
81
|
+
|
|
82
|
+
watch(definitions, (nextDefinitions) => {
|
|
83
|
+
for (const definition of nextDefinitions) {
|
|
84
|
+
valuesByDefinition[definition.id] ??= generatedJobDefaults(definition)
|
|
85
|
+
}
|
|
86
|
+
if (!nextDefinitions.some(definition => definition.id === activeDefinitionId.value)) {
|
|
87
|
+
activeDefinitionId.value = nextDefinitions[0]?.id ?? ''
|
|
88
|
+
}
|
|
89
|
+
}, { immediate: true })
|
|
90
|
+
|
|
91
|
+
watch(currentJob, (job) => {
|
|
92
|
+
if (job) void generated.loadResult(job)
|
|
93
|
+
}, { immediate: true })
|
|
94
|
+
|
|
95
|
+
function updateValues(values: Record<string, unknown>): void {
|
|
96
|
+
if (activeDefinitionId.value) valuesByDefinition[activeDefinitionId.value] = { ...values }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function submitJob(sectionId: string, values: Record<string, unknown>): Promise<void> {
|
|
100
|
+
const definition = definitions.value.find(candidate => candidate.id === sectionId)
|
|
101
|
+
if (!definition) return
|
|
102
|
+
selectedJobId.value = ''
|
|
103
|
+
valuesByDefinition[definition.id] = { ...values }
|
|
104
|
+
await generated.submit(definition.id, normalizeGeneratedJobInput(definition, values))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function selectJob(job: GeneratedAnalysisJob): void {
|
|
108
|
+
selectedJobId.value = job.job_id
|
|
109
|
+
activeDefinitionId.value = String(job.metadata.definition_id ?? activeDefinitionId.value)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function resourceSummary(definition: GeneratedJobDefinition): string {
|
|
113
|
+
if (definition.lifecycle === 'service' || definition.profile?.kind === 'service') {
|
|
114
|
+
return 'Host service'
|
|
115
|
+
}
|
|
116
|
+
if (definition.lifecycle === 'staged' || definition.profile?.kind === 'staged') {
|
|
117
|
+
return 'Staged upload'
|
|
118
|
+
}
|
|
119
|
+
return `${definition.cpu} CPU`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
onMounted(() => {
|
|
123
|
+
void generated.initialize().catch(() => undefined)
|
|
124
|
+
})
|
|
125
|
+
</script>
|
|
126
|
+
|
|
127
|
+
<template>
|
|
128
|
+
<PluginWorkspaceView
|
|
129
|
+
:title="generated.manifest.value?.ui.title ?? 'Analysis'"
|
|
130
|
+
:subtitle="generated.manifest.value?.ui.description"
|
|
131
|
+
:show-standalone-label="showStandaloneLabel"
|
|
132
|
+
:show-settings="false"
|
|
133
|
+
:pill-nav="pillNav"
|
|
134
|
+
:active-view="activeDefinitionId"
|
|
135
|
+
:panels="panels"
|
|
136
|
+
:forms="forms"
|
|
137
|
+
sidebar-title="Parameters"
|
|
138
|
+
sidebar-position="left"
|
|
139
|
+
:values="currentValues"
|
|
140
|
+
:show-form-actions="true"
|
|
141
|
+
:form-loading="generated.isSubmitting.value"
|
|
142
|
+
:form-disabled="generated.isSubmitting.value"
|
|
143
|
+
@update:active-view="activeDefinitionId = $event"
|
|
144
|
+
@update:values="updateValues"
|
|
145
|
+
@form-submit="submitJob"
|
|
146
|
+
>
|
|
147
|
+
<main class="mint-generated-workspace">
|
|
148
|
+
<div v-if="generated.isLoading.value" class="mint-generated-workspace__loading">
|
|
149
|
+
<LoadingSpinner size="lg" />
|
|
150
|
+
<span>Loading analysis workspace</span>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<template v-else>
|
|
154
|
+
<AlertBox
|
|
155
|
+
v-if="generated.error.value"
|
|
156
|
+
type="error"
|
|
157
|
+
:message="generated.error.value"
|
|
158
|
+
/>
|
|
159
|
+
|
|
160
|
+
<header v-if="activeDefinition" class="mint-generated-workspace__header">
|
|
161
|
+
<div>
|
|
162
|
+
<p class="mint-generated-workspace__eyebrow">Analysis result</p>
|
|
163
|
+
<h1>{{ activeDefinition.title }}</h1>
|
|
164
|
+
<p v-if="activeDefinition.description">{{ activeDefinition.description }}</p>
|
|
165
|
+
</div>
|
|
166
|
+
<div class="mint-generated-workspace__resources" aria-label="Job resource requirements">
|
|
167
|
+
<span>{{ resourceSummary(activeDefinition) }}</span>
|
|
168
|
+
</div>
|
|
169
|
+
</header>
|
|
170
|
+
|
|
171
|
+
<section class="mint-generated-workspace__result">
|
|
172
|
+
<GeneratedResultRenderer
|
|
173
|
+
v-if="currentResult"
|
|
174
|
+
:result="currentResult"
|
|
175
|
+
/>
|
|
176
|
+
<EmptyState
|
|
177
|
+
v-else
|
|
178
|
+
title="No result yet"
|
|
179
|
+
description="Set the parameters and run the analysis. Jobs continue independently if you switch tabs."
|
|
180
|
+
size="md"
|
|
181
|
+
/>
|
|
182
|
+
</section>
|
|
183
|
+
</template>
|
|
184
|
+
|
|
185
|
+
<JobsStatusTray
|
|
186
|
+
:source="generated"
|
|
187
|
+
:teleport-to="false"
|
|
188
|
+
@select="selectJob"
|
|
189
|
+
/>
|
|
190
|
+
</main>
|
|
191
|
+
</PluginWorkspaceView>
|
|
192
|
+
</template>
|
|
193
|
+
|
|
194
|
+
<style scoped>
|
|
195
|
+
.mint-generated-workspace {
|
|
196
|
+
position: relative;
|
|
197
|
+
min-width: 0;
|
|
198
|
+
min-height: calc(100vh - 5rem);
|
|
199
|
+
padding: clamp(1rem, 2.5vw, 2.5rem);
|
|
200
|
+
background: var(--color-bg-primary);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.mint-generated-workspace__loading {
|
|
204
|
+
display: flex;
|
|
205
|
+
min-height: 24rem;
|
|
206
|
+
align-items: center;
|
|
207
|
+
justify-content: center;
|
|
208
|
+
gap: 0.75rem;
|
|
209
|
+
color: var(--color-text-secondary);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.mint-generated-workspace__header {
|
|
213
|
+
display: flex;
|
|
214
|
+
align-items: flex-start;
|
|
215
|
+
justify-content: space-between;
|
|
216
|
+
gap: 1.5rem;
|
|
217
|
+
max-width: 76rem;
|
|
218
|
+
margin: 0 auto 1.25rem;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.mint-generated-workspace__header h1 {
|
|
222
|
+
margin: 0;
|
|
223
|
+
color: var(--color-text-primary);
|
|
224
|
+
font-size: clamp(1.35rem, 2vw, 1.8rem);
|
|
225
|
+
line-height: 1.2;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.mint-generated-workspace__header p:not(.mint-generated-workspace__eyebrow) {
|
|
229
|
+
margin: 0.4rem 0 0;
|
|
230
|
+
color: var(--color-text-secondary);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.mint-generated-workspace__eyebrow {
|
|
234
|
+
margin: 0 0 0.35rem;
|
|
235
|
+
color: var(--color-text-muted);
|
|
236
|
+
font-size: 0.72rem;
|
|
237
|
+
font-weight: 700;
|
|
238
|
+
letter-spacing: 0.08em;
|
|
239
|
+
text-transform: uppercase;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.mint-generated-workspace__resources {
|
|
243
|
+
display: flex;
|
|
244
|
+
flex-wrap: wrap;
|
|
245
|
+
justify-content: flex-end;
|
|
246
|
+
gap: 0.4rem;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.mint-generated-workspace__resources span {
|
|
250
|
+
padding: 0.3rem 0.55rem;
|
|
251
|
+
border: 1px solid var(--color-border);
|
|
252
|
+
border-radius: 999px;
|
|
253
|
+
background: var(--color-bg-secondary);
|
|
254
|
+
color: var(--color-text-secondary);
|
|
255
|
+
font-size: 0.75rem;
|
|
256
|
+
font-weight: 600;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.mint-generated-workspace__result {
|
|
260
|
+
max-width: 76rem;
|
|
261
|
+
min-height: 24rem;
|
|
262
|
+
margin: 0 auto;
|
|
263
|
+
padding: clamp(1rem, 2vw, 1.75rem);
|
|
264
|
+
border: 1px solid var(--color-border);
|
|
265
|
+
border-radius: 1rem;
|
|
266
|
+
background: var(--color-bg-secondary);
|
|
267
|
+
box-shadow: var(--shadow-sm);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@media (max-width: 640px) {
|
|
271
|
+
.mint-generated-workspace__header {
|
|
272
|
+
flex-direction: column;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.mint-generated-workspace__resources {
|
|
276
|
+
justify-content: flex-start;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
</style>
|