@morscherlab/mint-sdk 1.0.56 → 1.0.57
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/dist/__tests__/components/ArtifactSaveDialog.test.d.ts +1 -0
- package/dist/__tests__/components/ArtifactSelectorModal.test.d.ts +1 -0
- package/dist/__tests__/composables/useAnalysisArtifacts.test.d.ts +1 -0
- package/dist/analysisArtifactTypes-Cu40dzSG.js +7 -0
- package/dist/analysisArtifactTypes-Cu40dzSG.js.map +1 -0
- package/dist/components/ArtifactSaveDialog.vue.d.ts +56 -0
- package/dist/components/ArtifactSelectorModal.vue.d.ts +41 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +2 -2
- package/dist/{components-BQwOeyiy.js → components-PC3SVsaa.js} +1743 -1144
- package/dist/components-PC3SVsaa.js.map +1 -0
- package/dist/composables/index.d.ts +1 -0
- package/dist/composables/index.js +4 -3
- package/dist/composables/useAnalysisArtifacts.d.ts +59 -0
- package/dist/{composables-TpXyhRZZ.js → composables-OYTD0Y3r.js} +2 -2
- package/dist/{composables-TpXyhRZZ.js.map → composables-OYTD0Y3r.js.map} +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/install.js +1 -1
- package/dist/styles.css +412 -0
- package/dist/types/analysisArtifactTypes.d.ts +100 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +2 -0
- package/dist/{useJobsStatusTray-CVecN6Ao.js → useAnalysisArtifacts-Ob8UtVwl.js} +157 -2
- package/dist/useAnalysisArtifacts-Ob8UtVwl.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/components/ArtifactSaveDialog.test.ts +516 -0
- package/src/__tests__/components/ArtifactSelectorModal.test.ts +178 -0
- package/src/__tests__/composables/useAnalysisArtifacts.test.ts +667 -0
- package/src/components/ArtifactSaveDialog.story.vue +293 -0
- package/src/components/ArtifactSaveDialog.vue +383 -0
- package/src/components/ArtifactSelectorModal.story.vue +295 -0
- package/src/components/ArtifactSelectorModal.vue +326 -0
- package/src/components/index.ts +2 -0
- package/src/composables/index.ts +6 -0
- package/src/composables/useAnalysisArtifacts.ts +305 -0
- package/src/styles/components/artifact-save-dialog.css +17 -0
- package/src/styles/components/artifact-selector-modal.css +216 -0
- package/src/styles/index.css +2 -0
- package/src/types/analysisArtifactTypes.ts +119 -0
- package/src/types/index.ts +17 -0
- package/dist/components-BQwOeyiy.js.map +0 -1
- package/dist/useJobsStatusTray-CVecN6Ao.js.map +0 -1
- /package/dist/{ExperimentPopover-8A4Rhffp.js → ExperimentPopover-Ryusjrhv.js} +0 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
ref,
|
|
4
|
+
toValue,
|
|
5
|
+
watch,
|
|
6
|
+
type ComputedRef,
|
|
7
|
+
type MaybeRefOrGetter,
|
|
8
|
+
type Ref,
|
|
9
|
+
} from 'vue'
|
|
10
|
+
import { useApi } from './useApi'
|
|
11
|
+
import { useRequestSyncState } from './useRequestSyncState'
|
|
12
|
+
import {
|
|
13
|
+
getInjectedPlatformContext,
|
|
14
|
+
resolveCurrentExperimentId,
|
|
15
|
+
} from './platformContextHelpers'
|
|
16
|
+
import type {
|
|
17
|
+
AnalysisArtifactDetail,
|
|
18
|
+
AnalysisArtifactListResponse,
|
|
19
|
+
AnalysisArtifactMetadataUpdate,
|
|
20
|
+
AnalysisArtifactStatusFilter,
|
|
21
|
+
AnalysisArtifactSummary,
|
|
22
|
+
} from '../types/analysisArtifactTypes'
|
|
23
|
+
|
|
24
|
+
export { ANALYSIS_FILE_ARTIFACT_SCHEMA } from '../types/analysisArtifactTypes'
|
|
25
|
+
|
|
26
|
+
export interface UseAnalysisArtifactsOptions {
|
|
27
|
+
/** Experiment id; defaults to platform injection or URL. Refetches when it changes. */
|
|
28
|
+
experimentId?: MaybeRefOrGetter<number | undefined>
|
|
29
|
+
/** Override API base URL. */
|
|
30
|
+
apiBaseUrl?: string
|
|
31
|
+
/** Scope to a single owning plugin; omit or null to keep all plugins. */
|
|
32
|
+
pluginId?: MaybeRefOrGetter<string | null | undefined>
|
|
33
|
+
/** Always request archived artifacts alongside active ones. */
|
|
34
|
+
includeArchived?: MaybeRefOrGetter<boolean | undefined>
|
|
35
|
+
/** Fetch as soon as an experiment id is available. */
|
|
36
|
+
immediate?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface UseAnalysisArtifactsReturn {
|
|
40
|
+
/** Fetched artifacts, scoped to pluginId when set. */
|
|
41
|
+
artifacts: ComputedRef<AnalysisArtifactSummary[]>
|
|
42
|
+
/** Artifacts after applying statusFilter, pluginFilter, and search. */
|
|
43
|
+
visibleArtifacts: ComputedRef<AnalysisArtifactSummary[]>
|
|
44
|
+
/** Detail loaded by the last successful getDetail call, or null. */
|
|
45
|
+
selectedDetail: Ref<AnalysisArtifactDetail | null>
|
|
46
|
+
/** Resolved experiment id from options, platform injection, or URL. */
|
|
47
|
+
currentExperimentId: ComputedRef<number | undefined>
|
|
48
|
+
/** Whether an experiment id is available. */
|
|
49
|
+
hasCurrentExperiment: ComputedRef<boolean>
|
|
50
|
+
/** Whether any list or detail fetch is in progress. */
|
|
51
|
+
isLoading: ComputedRef<boolean>
|
|
52
|
+
/** Whether any archive/restore/metadata mutation is in progress. */
|
|
53
|
+
isMutating: ComputedRef<boolean>
|
|
54
|
+
/** Error message from the last failed operation, or null. */
|
|
55
|
+
error: Ref<string | null>
|
|
56
|
+
/** Timestamp of the last successful load, or null. */
|
|
57
|
+
lastLoadedAt: Ref<Date | null>
|
|
58
|
+
/** Case-insensitive filter over display_name and artifact_key. */
|
|
59
|
+
search: Ref<string>
|
|
60
|
+
/** Lifecycle filter; widening past "active" refetches with include_archived. */
|
|
61
|
+
statusFilter: Ref<AnalysisArtifactStatusFilter>
|
|
62
|
+
/** Plugin filter applied to visibleArtifacts, or null for all plugins. */
|
|
63
|
+
pluginFilter: Ref<string | null>
|
|
64
|
+
/** Fetch the artifact list for the current experiment. */
|
|
65
|
+
list: () => Promise<AnalysisArtifactSummary[]>
|
|
66
|
+
/** Alias of list. */
|
|
67
|
+
refresh: () => Promise<AnalysisArtifactSummary[]>
|
|
68
|
+
/** Fetch one artifact with its result payload; null when it fails or is superseded. */
|
|
69
|
+
getDetail: (artifactId: number) => Promise<AnalysisArtifactDetail | null>
|
|
70
|
+
/** Update display_name and/or note; resolves to the updated summary or null. */
|
|
71
|
+
updateMetadata: (
|
|
72
|
+
artifactId: number,
|
|
73
|
+
updates: AnalysisArtifactMetadataUpdate,
|
|
74
|
+
) => Promise<AnalysisArtifactSummary | null>
|
|
75
|
+
/** Archive an artifact; resolves to the updated summary or null. */
|
|
76
|
+
archive: (artifactId: number) => Promise<AnalysisArtifactSummary | null>
|
|
77
|
+
/** Restore an archived artifact; resolves to the updated summary or null. */
|
|
78
|
+
restore: (artifactId: number) => Promise<AnalysisArtifactSummary | null>
|
|
79
|
+
/** Whether a key exists in the loaded artifacts (no network call). */
|
|
80
|
+
hasArtifactKey: (artifactKey: string) => boolean
|
|
81
|
+
/** Reset artifacts, selection, and error. */
|
|
82
|
+
clear: () => void
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Lists and manages analysis artifacts through the user-facing platform API. */
|
|
86
|
+
export function useAnalysisArtifacts(
|
|
87
|
+
options: UseAnalysisArtifactsOptions = {},
|
|
88
|
+
): UseAnalysisArtifactsReturn {
|
|
89
|
+
const injectedContext = getInjectedPlatformContext()
|
|
90
|
+
const api = useApi({ baseUrl: options.apiBaseUrl ?? injectedContext?.platformApiUrl })
|
|
91
|
+
|
|
92
|
+
const currentExperimentId = computed<number | undefined>(
|
|
93
|
+
() => toValue(options.experimentId) ?? resolveCurrentExperimentId(injectedContext),
|
|
94
|
+
)
|
|
95
|
+
const hasCurrentExperiment = computed(() => currentExperimentId.value !== undefined)
|
|
96
|
+
const scopePluginId = computed(() => toValue(options.pluginId) ?? null)
|
|
97
|
+
|
|
98
|
+
const request = useRequestSyncState('Analysis artifact request failed.')
|
|
99
|
+
const fetchedArtifacts = ref<AnalysisArtifactSummary[]>([])
|
|
100
|
+
const selectedDetail = ref<AnalysisArtifactDetail | null>(null)
|
|
101
|
+
|
|
102
|
+
// Counters, not booleans: overlapping requests must not clear each other's flag.
|
|
103
|
+
const loadingCount = ref(0)
|
|
104
|
+
const mutatingCount = ref(0)
|
|
105
|
+
const isLoading = computed(() => loadingCount.value > 0)
|
|
106
|
+
const isMutating = computed(() => mutatingCount.value > 0)
|
|
107
|
+
|
|
108
|
+
const search = ref('')
|
|
109
|
+
const statusFilter = ref<AnalysisArtifactStatusFilter>('active')
|
|
110
|
+
const pluginFilter = ref<string | null>(null)
|
|
111
|
+
|
|
112
|
+
const needsArchived = computed(
|
|
113
|
+
() => Boolean(toValue(options.includeArchived)) || statusFilter.value !== 'active',
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
const artifacts = computed(() => (
|
|
117
|
+
scopePluginId.value
|
|
118
|
+
? fetchedArtifacts.value.filter((artifact) => artifact.plugin_id === scopePluginId.value)
|
|
119
|
+
: fetchedArtifacts.value
|
|
120
|
+
))
|
|
121
|
+
|
|
122
|
+
const visibleArtifacts = computed(() => {
|
|
123
|
+
const query = search.value.trim().toLowerCase()
|
|
124
|
+
return artifacts.value.filter((artifact) => {
|
|
125
|
+
if (statusFilter.value !== 'all' && artifact.status !== statusFilter.value) return false
|
|
126
|
+
if (pluginFilter.value && artifact.plugin_id !== pluginFilter.value) return false
|
|
127
|
+
if (!query) return true
|
|
128
|
+
return (
|
|
129
|
+
artifact.display_name.toLowerCase().includes(query)
|
|
130
|
+
|| artifact.artifact_key.toLowerCase().includes(query)
|
|
131
|
+
)
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// Generation tokens: a superseded response must commit nothing — not rows, not the
|
|
136
|
+
// shared error, not lastLoadedAt — or a late failure would bury fresh results.
|
|
137
|
+
let listToken = 0
|
|
138
|
+
let detailToken = 0
|
|
139
|
+
const hasRequestedList = ref(false)
|
|
140
|
+
|
|
141
|
+
function currentExperimentIdOrError(action: string): number | undefined {
|
|
142
|
+
const id = currentExperimentId.value
|
|
143
|
+
if (id === undefined) {
|
|
144
|
+
request.setError(`No current experiment is selected for ${action}`)
|
|
145
|
+
return undefined
|
|
146
|
+
}
|
|
147
|
+
return id
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function replaceArtifact(summary: AnalysisArtifactSummary): void {
|
|
151
|
+
const index = fetchedArtifacts.value.findIndex((artifact) => artifact.id === summary.id)
|
|
152
|
+
if (index >= 0) {
|
|
153
|
+
fetchedArtifacts.value.splice(index, 1, summary)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function list(): Promise<AnalysisArtifactSummary[]> {
|
|
158
|
+
const experimentId = currentExperimentIdOrError('listing analysis artifacts')
|
|
159
|
+
if (experimentId === undefined) {
|
|
160
|
+
fetchedArtifacts.value = []
|
|
161
|
+
return []
|
|
162
|
+
}
|
|
163
|
+
const token = ++listToken
|
|
164
|
+
hasRequestedList.value = true
|
|
165
|
+
loadingCount.value += 1
|
|
166
|
+
request.clearError()
|
|
167
|
+
try {
|
|
168
|
+
const data = await api.get<AnalysisArtifactListResponse>(
|
|
169
|
+
`/experiments/${experimentId}/analysis-artifacts?include_archived=${needsArchived.value}`,
|
|
170
|
+
)
|
|
171
|
+
if (token !== listToken) return artifacts.value
|
|
172
|
+
// Copy: replaceArtifact splices in place, and the response array is not ours.
|
|
173
|
+
fetchedArtifacts.value = [...(data?.artifacts ?? [])]
|
|
174
|
+
request.markLoaded()
|
|
175
|
+
return artifacts.value
|
|
176
|
+
} catch (err) {
|
|
177
|
+
if (token !== listToken) return []
|
|
178
|
+
request.setError(err)
|
|
179
|
+
fetchedArtifacts.value = []
|
|
180
|
+
return []
|
|
181
|
+
} finally {
|
|
182
|
+
loadingCount.value -= 1
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function getDetail(artifactId: number): Promise<AnalysisArtifactDetail | null> {
|
|
187
|
+
const experimentId = currentExperimentIdOrError('loading an analysis artifact')
|
|
188
|
+
if (experimentId === undefined) return null
|
|
189
|
+
const token = ++detailToken
|
|
190
|
+
loadingCount.value += 1
|
|
191
|
+
request.clearError()
|
|
192
|
+
try {
|
|
193
|
+
const detail = await api.get<AnalysisArtifactDetail>(
|
|
194
|
+
`/experiments/${experimentId}/analysis-artifacts/${artifactId}`,
|
|
195
|
+
)
|
|
196
|
+
if (token !== detailToken) return null
|
|
197
|
+
selectedDetail.value = detail
|
|
198
|
+
request.markLoaded()
|
|
199
|
+
return detail
|
|
200
|
+
} catch (err) {
|
|
201
|
+
if (token !== detailToken) return null
|
|
202
|
+
request.setError(err)
|
|
203
|
+
return null
|
|
204
|
+
} finally {
|
|
205
|
+
loadingCount.value -= 1
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function mutate(
|
|
210
|
+
action: string,
|
|
211
|
+
operation: (experimentId: number) => Promise<AnalysisArtifactSummary>,
|
|
212
|
+
): Promise<AnalysisArtifactSummary | null> {
|
|
213
|
+
const experimentId = currentExperimentIdOrError(action)
|
|
214
|
+
if (experimentId === undefined) return null
|
|
215
|
+
mutatingCount.value += 1
|
|
216
|
+
request.clearError()
|
|
217
|
+
try {
|
|
218
|
+
const summary = await operation(experimentId)
|
|
219
|
+
replaceArtifact(summary)
|
|
220
|
+
request.markSaved()
|
|
221
|
+
return summary
|
|
222
|
+
} catch (err) {
|
|
223
|
+
request.setError(err)
|
|
224
|
+
return null
|
|
225
|
+
} finally {
|
|
226
|
+
mutatingCount.value -= 1
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function updateMetadata(
|
|
231
|
+
artifactId: number,
|
|
232
|
+
updates: AnalysisArtifactMetadataUpdate,
|
|
233
|
+
): Promise<AnalysisArtifactSummary | null> {
|
|
234
|
+
return mutate('updating artifact metadata', (experimentId) =>
|
|
235
|
+
api.patch<AnalysisArtifactSummary>(
|
|
236
|
+
`/experiments/${experimentId}/analysis-artifacts/${artifactId}`,
|
|
237
|
+
updates,
|
|
238
|
+
))
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function archive(artifactId: number): Promise<AnalysisArtifactSummary | null> {
|
|
242
|
+
return mutate('archiving an analysis artifact', (experimentId) =>
|
|
243
|
+
api.post<AnalysisArtifactSummary>(
|
|
244
|
+
`/experiments/${experimentId}/analysis-artifacts/${artifactId}/archive`,
|
|
245
|
+
))
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function restore(artifactId: number): Promise<AnalysisArtifactSummary | null> {
|
|
249
|
+
return mutate('restoring an analysis artifact', (experimentId) =>
|
|
250
|
+
api.post<AnalysisArtifactSummary>(
|
|
251
|
+
`/experiments/${experimentId}/analysis-artifacts/${artifactId}/restore`,
|
|
252
|
+
))
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function hasArtifactKey(artifactKey: string): boolean {
|
|
256
|
+
return artifacts.value.some((artifact) => artifact.artifact_key === artifactKey)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Drop experiment-scoped state and orphan every in-flight response. */
|
|
260
|
+
function clear(): void {
|
|
261
|
+
listToken += 1
|
|
262
|
+
detailToken += 1
|
|
263
|
+
fetchedArtifacts.value = []
|
|
264
|
+
selectedDetail.value = null
|
|
265
|
+
request.clearError()
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Refetch when the scope changes or archived rows are newly needed. `immediate`
|
|
269
|
+
// covers a late-resolving experiment id; otherwise stay quiet until something
|
|
270
|
+
// has asked for a list at least once.
|
|
271
|
+
watch([currentExperimentId, needsArchived], ([experimentId], [previousExperimentId]) => {
|
|
272
|
+
if (experimentId !== previousExperimentId) {
|
|
273
|
+
// Scope changed: the previous experiment's rows and any in-flight reply for
|
|
274
|
+
// them must not survive into the new one.
|
|
275
|
+
clear()
|
|
276
|
+
}
|
|
277
|
+
if (experimentId === undefined) return
|
|
278
|
+
if (options.immediate || hasRequestedList.value) {
|
|
279
|
+
void list()
|
|
280
|
+
}
|
|
281
|
+
}, { immediate: options.immediate })
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
artifacts,
|
|
285
|
+
visibleArtifacts,
|
|
286
|
+
selectedDetail,
|
|
287
|
+
currentExperimentId,
|
|
288
|
+
hasCurrentExperiment,
|
|
289
|
+
isLoading,
|
|
290
|
+
isMutating,
|
|
291
|
+
error: request.error,
|
|
292
|
+
lastLoadedAt: request.lastLoadedAt,
|
|
293
|
+
search,
|
|
294
|
+
statusFilter,
|
|
295
|
+
pluginFilter,
|
|
296
|
+
list,
|
|
297
|
+
refresh: list,
|
|
298
|
+
getDetail,
|
|
299
|
+
updateMetadata,
|
|
300
|
+
archive,
|
|
301
|
+
restore,
|
|
302
|
+
hasArtifactKey,
|
|
303
|
+
clear,
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.mint-artifact-save-dialog {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: 1rem;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.mint-artifact-save-dialog__immutable {
|
|
8
|
+
margin: 0;
|
|
9
|
+
font-size: 0.8125rem;
|
|
10
|
+
color: var(--text-muted);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.mint-artifact-save-dialog__immutable code {
|
|
14
|
+
font-family: var(--font-mono, 'Fira Code', monospace);
|
|
15
|
+
font-size: 0.75rem;
|
|
16
|
+
color: var(--text-secondary);
|
|
17
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/* ArtifactSelectorModal Component Styles */
|
|
2
|
+
|
|
3
|
+
.mint-artifact-selector {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
gap: 0.75rem;
|
|
7
|
+
min-height: 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* Filter row */
|
|
11
|
+
.mint-artifact-selector__filters-row {
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
gap: 0.5rem;
|
|
15
|
+
flex-wrap: wrap;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.mint-artifact-selector__search {
|
|
19
|
+
flex: 1;
|
|
20
|
+
min-width: 10rem;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.mint-artifact-selector__filter-select {
|
|
24
|
+
flex-shrink: 0;
|
|
25
|
+
width: 10rem;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Loading skeleton */
|
|
29
|
+
.mint-artifact-selector__skeleton {
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
border: 1px solid var(--border-color);
|
|
33
|
+
border-radius: var(--radius-md);
|
|
34
|
+
overflow: hidden;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.mint-artifact-selector__skeleton-row {
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
gap: 0.75rem;
|
|
41
|
+
padding: 0.75rem 1rem;
|
|
42
|
+
background-color: var(--bg-primary);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.mint-artifact-selector__skeleton-content {
|
|
46
|
+
flex: 1;
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-direction: column;
|
|
49
|
+
gap: 6px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Error */
|
|
53
|
+
.mint-artifact-selector__error {
|
|
54
|
+
padding: 0.75rem;
|
|
55
|
+
border: 1px solid color-mix(in srgb, var(--mint-error) 30%, transparent);
|
|
56
|
+
border-radius: var(--radius-md);
|
|
57
|
+
background-color: var(--mint-error-bg);
|
|
58
|
+
color: var(--mint-error);
|
|
59
|
+
font-size: 0.875rem;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Scrollable list */
|
|
63
|
+
.mint-artifact-selector__list {
|
|
64
|
+
max-height: min(28rem, calc(100vh - 16rem));
|
|
65
|
+
overflow-y: auto;
|
|
66
|
+
display: flex;
|
|
67
|
+
flex-direction: column;
|
|
68
|
+
border: 1px solid var(--border-color);
|
|
69
|
+
border-radius: var(--radius-md);
|
|
70
|
+
background-color: var(--bg-primary);
|
|
71
|
+
scrollbar-width: thin;
|
|
72
|
+
overscroll-behavior: contain;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.mint-artifact-selector__list::-webkit-scrollbar {
|
|
76
|
+
width: 0.5rem;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.mint-artifact-selector__list::-webkit-scrollbar-track {
|
|
80
|
+
background: transparent;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.mint-artifact-selector__list::-webkit-scrollbar-thumb {
|
|
84
|
+
background-color: color-mix(in srgb, var(--text-muted) 30%, transparent);
|
|
85
|
+
border: 2px solid transparent;
|
|
86
|
+
border-radius: 9999px;
|
|
87
|
+
background-clip: content-box;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* Row */
|
|
91
|
+
.mint-artifact-selector__row {
|
|
92
|
+
position: relative;
|
|
93
|
+
display: flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
gap: 0.75rem;
|
|
96
|
+
min-height: 4.125rem;
|
|
97
|
+
padding: 0.75rem 1rem 0.75rem 1.125rem;
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
border-left: 0 solid transparent;
|
|
100
|
+
border-bottom: 1px solid var(--border-light);
|
|
101
|
+
background-color: var(--bg-primary);
|
|
102
|
+
transition:
|
|
103
|
+
background-color 0.15s ease,
|
|
104
|
+
box-shadow 0.15s ease;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.mint-artifact-selector__row:hover {
|
|
108
|
+
background-color: var(--bg-hover);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.mint-artifact-selector__row--active {
|
|
112
|
+
box-shadow: inset 3px 0 0 var(--color-primary);
|
|
113
|
+
background-color: var(--color-primary-soft);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.mint-artifact-selector__row--active:hover {
|
|
117
|
+
background-color: var(--color-primary-soft);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* Keyboard focused row */
|
|
121
|
+
.mint-artifact-selector__row--focused {
|
|
122
|
+
background-color: var(--bg-hover);
|
|
123
|
+
outline: none;
|
|
124
|
+
box-shadow:
|
|
125
|
+
inset 0 0 0 2px var(--color-primary),
|
|
126
|
+
inset 3px 0 0 var(--color-primary);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.mint-artifact-selector__row--active.mint-artifact-selector__row--focused {
|
|
130
|
+
background-color: var(--color-primary-soft);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.mint-artifact-selector__row--archived .mint-artifact-selector__name,
|
|
134
|
+
.mint-artifact-selector__row--archived .mint-artifact-selector__meta {
|
|
135
|
+
opacity: var(--mint-disabled-opacity, 0.6);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* Row content */
|
|
139
|
+
.mint-artifact-selector__row-content {
|
|
140
|
+
flex: 1;
|
|
141
|
+
min-width: 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.mint-artifact-selector__name {
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
gap: 0.5rem;
|
|
148
|
+
font-size: 0.875rem;
|
|
149
|
+
font-weight: 500;
|
|
150
|
+
color: var(--text-primary);
|
|
151
|
+
min-width: 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.mint-artifact-selector__key {
|
|
155
|
+
font-family: var(--font-mono, 'Fira Code', monospace);
|
|
156
|
+
font-size: 0.6875rem;
|
|
157
|
+
padding: 0.0625rem 0.375rem;
|
|
158
|
+
border-radius: var(--radius-sm);
|
|
159
|
+
background-color: var(--bg-secondary);
|
|
160
|
+
color: var(--text-secondary);
|
|
161
|
+
white-space: nowrap;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.mint-artifact-selector__meta {
|
|
165
|
+
display: flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
gap: 0.5rem;
|
|
168
|
+
flex-wrap: wrap;
|
|
169
|
+
font-size: 0.75rem;
|
|
170
|
+
color: var(--text-muted);
|
|
171
|
+
margin-top: 0.125rem;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.mint-artifact-selector__plugin {
|
|
175
|
+
font-family: var(--font-mono, 'Fira Code', monospace);
|
|
176
|
+
color: var(--text-secondary);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* Status pill + row actions */
|
|
180
|
+
.mint-artifact-selector__row-side {
|
|
181
|
+
display: flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
gap: 0.5rem;
|
|
184
|
+
flex-shrink: 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.mint-artifact-selector__action {
|
|
188
|
+
padding: 0.25rem 0.5rem;
|
|
189
|
+
border: 1px solid var(--border-color);
|
|
190
|
+
border-radius: var(--radius-sm);
|
|
191
|
+
background: var(--bg-primary);
|
|
192
|
+
color: var(--text-secondary);
|
|
193
|
+
font-size: 0.75rem;
|
|
194
|
+
cursor: pointer;
|
|
195
|
+
white-space: nowrap;
|
|
196
|
+
transition:
|
|
197
|
+
background-color 0.15s ease,
|
|
198
|
+
border-color 0.15s ease,
|
|
199
|
+
color 0.15s ease;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.mint-artifact-selector__action:hover:not(:disabled) {
|
|
203
|
+
border-color: var(--color-primary);
|
|
204
|
+
color: var(--color-primary);
|
|
205
|
+
background-color: var(--bg-hover);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.mint-artifact-selector__action:focus-visible {
|
|
209
|
+
outline: none;
|
|
210
|
+
box-shadow: var(--focus-ring);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.mint-artifact-selector__action:disabled {
|
|
214
|
+
opacity: var(--mint-disabled-opacity, 0.6);
|
|
215
|
+
cursor: not-allowed;
|
|
216
|
+
}
|
package/src/styles/index.css
CHANGED
|
@@ -85,5 +85,7 @@
|
|
|
85
85
|
@import './components/experiment-data-viewer.css';
|
|
86
86
|
@import './components/experiment-code-badge.css';
|
|
87
87
|
@import './components/experiment-selector-modal.css';
|
|
88
|
+
@import './components/artifact-selector-modal.css';
|
|
89
|
+
@import './components/artifact-save-dialog.css';
|
|
88
90
|
@import './components/experiment-popover.css';
|
|
89
91
|
@import './components/fit-panel.css';
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { TreeNode } from './componentLabTypes'
|
|
2
|
+
import type { SummaryData } from './componentWorkflowTypes'
|
|
3
|
+
|
|
4
|
+
/** Result schema identifier for file-backed analysis artifacts. */
|
|
5
|
+
export const ANALYSIS_FILE_ARTIFACT_SCHEMA = 'mint.analysis_file.v1'
|
|
6
|
+
|
|
7
|
+
/** Known artifact lifecycle states; kept open for forward compatibility. */
|
|
8
|
+
export type AnalysisArtifactStatus = 'active' | 'archived' | (string & {})
|
|
9
|
+
|
|
10
|
+
/** Status filter vocabulary for artifact lists. */
|
|
11
|
+
export type AnalysisArtifactStatusFilter = 'active' | 'archived' | 'all'
|
|
12
|
+
|
|
13
|
+
/** Summary row returned by GET /experiments/{id}/analysis-artifacts. */
|
|
14
|
+
export interface AnalysisArtifactSummary {
|
|
15
|
+
id: number
|
|
16
|
+
experiment_id: number
|
|
17
|
+
plugin_id: string
|
|
18
|
+
artifact_key: string
|
|
19
|
+
display_name: string
|
|
20
|
+
note?: string | null
|
|
21
|
+
status: AnalysisArtifactStatus
|
|
22
|
+
result_keys: string[]
|
|
23
|
+
created_at?: string | null
|
|
24
|
+
updated_at?: string | null
|
|
25
|
+
archived_at?: string | null
|
|
26
|
+
archived_by?: number | null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Full artifact returned by GET /experiments/{id}/analysis-artifacts/{artifactId}. */
|
|
30
|
+
export interface AnalysisArtifactDetail extends AnalysisArtifactSummary {
|
|
31
|
+
result: Record<string, unknown>
|
|
32
|
+
tree: TreeNode[]
|
|
33
|
+
summary: SummaryData | null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Response of GET /experiments/{id}/analysis-artifacts. */
|
|
37
|
+
export interface AnalysisArtifactListResponse {
|
|
38
|
+
artifacts: AnalysisArtifactSummary[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Body of PATCH /experiments/{id}/analysis-artifacts/{artifactId}. */
|
|
42
|
+
export interface AnalysisArtifactMetadataUpdate {
|
|
43
|
+
display_name?: string
|
|
44
|
+
/** undefined = keep, string = set, null = clear. */
|
|
45
|
+
note?: string | null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** How a save payload should be applied by the plugin backend. */
|
|
49
|
+
export type ArtifactSaveMode = 'create' | 'upsert' | 'update'
|
|
50
|
+
|
|
51
|
+
interface ArtifactSavePayloadBase {
|
|
52
|
+
experimentId: number
|
|
53
|
+
artifactKey: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* JSON artifact save — handler maps to save_analysis_artifact, which upserts by key.
|
|
58
|
+
* That call only assigns display_name/note when they are provided, and cannot clear
|
|
59
|
+
* either; use the metadata PATCH (useAnalysisArtifacts.updateMetadata) for that.
|
|
60
|
+
*/
|
|
61
|
+
export interface JsonArtifactSavePayload extends ArtifactSavePayloadBase {
|
|
62
|
+
kind: 'json'
|
|
63
|
+
mode: 'create' | 'upsert'
|
|
64
|
+
result: Record<string, unknown>
|
|
65
|
+
displayName?: string
|
|
66
|
+
note?: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** File artifact create — handler maps to save_analysis_file_artifact (create-only: a reused key conflicts). */
|
|
70
|
+
export interface FileArtifactCreatePayload extends ArtifactSavePayloadBase {
|
|
71
|
+
kind: 'file'
|
|
72
|
+
mode: 'create'
|
|
73
|
+
file: File
|
|
74
|
+
/** Maps to the Python `kind=` argument (defaults to "file" backend-side). */
|
|
75
|
+
fileKind: string
|
|
76
|
+
filename?: string
|
|
77
|
+
displayName?: string
|
|
78
|
+
note?: string
|
|
79
|
+
contentType?: string
|
|
80
|
+
metadata?: Record<string, unknown>
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* File artifact replacement — handler maps to update_analysis_file_artifact
|
|
85
|
+
* (transactional CAS replace). `kind` and `filename` are immutable backend-side, so
|
|
86
|
+
* `fileKind` echoes the artifact's existing kind and no filename is carried. `note`
|
|
87
|
+
* is three-state: omitted preserves, null clears, a string sets. `metadata` REPLACES
|
|
88
|
+
* the previous result metadata rather than merging, so it always carries the full
|
|
89
|
+
* intended map — omitting it clears whatever was there.
|
|
90
|
+
*/
|
|
91
|
+
export interface FileArtifactUpdatePayload extends ArtifactSavePayloadBase {
|
|
92
|
+
kind: 'file'
|
|
93
|
+
mode: 'update'
|
|
94
|
+
file: File
|
|
95
|
+
fileKind: string
|
|
96
|
+
note?: string | null
|
|
97
|
+
contentType?: string
|
|
98
|
+
metadata?: Record<string, unknown>
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type ArtifactSavePayload =
|
|
102
|
+
| JsonArtifactSavePayload
|
|
103
|
+
| FileArtifactCreatePayload
|
|
104
|
+
| FileArtifactUpdatePayload
|
|
105
|
+
|
|
106
|
+
/** Optional handler result; cleanupPending surfaces AnalysisFileArtifactUpdate.cleanup_pending. */
|
|
107
|
+
export interface ArtifactSaveResult {
|
|
108
|
+
artifact?: AnalysisArtifactSummary
|
|
109
|
+
cleanupPending?: boolean
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Plugin-supplied save handler. The dialog never writes to the platform;
|
|
114
|
+
* the handler forwards the payload to the plugin backend, which calls
|
|
115
|
+
* save_analysis_artifact / save_analysis_file_artifact / update_analysis_file_artifact.
|
|
116
|
+
*/
|
|
117
|
+
export type ArtifactSaveHandler = (
|
|
118
|
+
payload: ArtifactSavePayload,
|
|
119
|
+
) => Promise<ArtifactSaveResult | void>
|
package/src/types/index.ts
CHANGED
|
@@ -176,6 +176,23 @@ export type {
|
|
|
176
176
|
TreeNode,
|
|
177
177
|
} from './components'
|
|
178
178
|
|
|
179
|
+
export type {
|
|
180
|
+
AnalysisArtifactStatus,
|
|
181
|
+
AnalysisArtifactStatusFilter,
|
|
182
|
+
AnalysisArtifactSummary,
|
|
183
|
+
AnalysisArtifactDetail,
|
|
184
|
+
AnalysisArtifactListResponse,
|
|
185
|
+
AnalysisArtifactMetadataUpdate,
|
|
186
|
+
ArtifactSaveMode,
|
|
187
|
+
JsonArtifactSavePayload,
|
|
188
|
+
FileArtifactCreatePayload,
|
|
189
|
+
FileArtifactUpdatePayload,
|
|
190
|
+
ArtifactSavePayload,
|
|
191
|
+
ArtifactSaveResult,
|
|
192
|
+
ArtifactSaveHandler,
|
|
193
|
+
} from './analysisArtifactTypes'
|
|
194
|
+
export { ANALYSIS_FILE_ARTIFACT_SCHEMA } from './analysisArtifactTypes'
|
|
195
|
+
|
|
179
196
|
export type {
|
|
180
197
|
JobListPayload,
|
|
181
198
|
JobProgress,
|