@morscherlab/mint-sdk 1.0.0-beta.7 → 1.0.0-rc.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 +9 -1
- package/dist/__tests__/components/LcmsSequenceTable.test.d.ts +1 -0
- package/dist/__tests__/components/ProgressBar.test.d.ts +1 -0
- package/dist/__tests__/components/RackEditor.test.d.ts +1 -0
- package/dist/__tests__/components/SequenceProgressBar.test.d.ts +1 -0
- package/dist/__tests__/composables/useExperimentSamples.test.d.ts +1 -0
- package/dist/__tests__/composables/useProtocolTemplates.test.d.ts +1 -0
- package/dist/__tests__/stores/settings.test.d.ts +1 -0
- package/dist/__tests__/utils/instrument.test.d.ts +1 -0
- package/dist/__tests__/utils/lcms.test.d.ts +1 -0
- package/dist/__tests__/utils/permissions.test.d.ts +1 -0
- package/dist/__tests__/utils/rack.test.d.ts +1 -0
- package/dist/{auth-QQj2kkze.js → auth-B7g4J4ZF.js} +148 -24
- package/dist/auth-B7g4J4ZF.js.map +1 -0
- package/dist/components/AutoGroupModal.vue.d.ts +1 -1
- package/dist/components/BaseCheckbox.vue.d.ts +1 -1
- package/dist/components/BaseToggle.vue.d.ts +2 -2
- package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +1 -1
- package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +1 -1
- package/dist/components/BioTemplatePresetWorkspaceView.vue.d.ts +1 -1
- package/dist/components/DoseDesignWorkspaceView.vue.d.ts +1 -1
- package/dist/components/FormulaInput.vue.d.ts +1 -1
- package/dist/components/InstrumentAlertLog.vue.d.ts +22 -0
- package/dist/components/InstrumentStateBadge.vue.d.ts +11 -0
- package/dist/components/InstrumentStatusCard.vue.d.ts +13 -0
- package/dist/components/LcmsSequenceTable.vue.d.ts +26 -0
- package/dist/components/ProgressBar.vue.d.ts +1 -0
- package/dist/components/RackEditor.vue.d.ts +41 -3
- package/dist/components/ReagentList.vue.d.ts +1 -1
- package/dist/components/SampleSelector.vue.d.ts +5 -2
- package/dist/components/SegmentedControl.vue.d.ts +2 -0
- package/dist/components/SequenceInput.vue.d.ts +1 -1
- package/dist/components/SequenceProgressBar.vue.d.ts +15 -0
- package/dist/components/SettingsModal.vue.d.ts +8 -1
- package/dist/components/TagsInput.vue.d.ts +1 -1
- package/dist/components/WellPlate.vue.d.ts +42 -3
- package/dist/components/index.d.ts +5 -0
- package/dist/components/index.js +3 -3
- package/dist/{components-DihbSJjU.js → components-BhK-dW99.js} +2135 -1075
- package/dist/components-BhK-dW99.js.map +1 -0
- package/dist/composables/experimentDesignData.d.ts +17 -0
- package/dist/composables/index.d.ts +2 -0
- package/dist/composables/index.js +4 -4
- package/dist/composables/useControlSchema.d.ts +11 -0
- package/dist/composables/useExperimentData.d.ts +11 -3
- package/dist/composables/useExperimentSamples.d.ts +42 -0
- package/dist/composables/usePlatformContext.d.ts +54 -0
- package/dist/{composables-BcgZ6diz.js → composables-Bg7CFuNz.js} +5 -3
- package/dist/composables-Bg7CFuNz.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +168 -6
- package/dist/index.js.map +1 -0
- package/dist/install.js +2 -2
- package/dist/instrument.d.ts +7 -0
- package/dist/lcms.d.ts +27 -0
- package/dist/permissions.d.ts +46 -0
- package/dist/stores/auth.d.ts +74 -2
- package/dist/stores/index.js +1 -1
- package/dist/styles.css +3186 -1070
- package/dist/templates/builders.d.ts +7 -3
- package/dist/templates/index.d.ts +2 -2
- package/dist/templates/index.js +2 -2
- package/dist/templates/presets.d.ts +12 -0
- package/dist/templates/types.d.ts +16 -1
- package/dist/{templates-Cyt0Suwf.js → templates-BorLR_7p.js} +324 -10
- package/dist/templates-BorLR_7p.js.map +1 -0
- package/dist/types/auth.d.ts +2 -0
- package/dist/types/components.d.ts +32 -3
- package/dist/types/form-builder.d.ts +2 -1
- package/dist/types/index.d.ts +4 -1
- package/dist/types/instrument.d.ts +56 -0
- package/dist/types/platform.d.ts +3 -0
- package/dist/{useExperimentData-CM6Y0u5L.js → useProtocolTemplates-n6AJqSqv.js} +627 -380
- package/dist/useProtocolTemplates-n6AJqSqv.js.map +1 -0
- package/dist/utils/rack.d.ts +47 -0
- package/package.json +1 -1
- package/src/__tests__/components/AppTopBar.test.ts +15 -0
- package/src/__tests__/components/BaseTabs.test.ts +15 -0
- package/src/__tests__/components/GroupAssigner.test.ts +18 -0
- package/src/__tests__/components/LcmsSequenceTable.test.ts +57 -0
- package/src/__tests__/components/ProgressBar.test.ts +18 -0
- package/src/__tests__/components/RackEditor.test.ts +125 -0
- package/src/__tests__/components/SampleSelector.test.ts +25 -0
- package/src/__tests__/components/SegmentedControl.test.ts +45 -0
- package/src/__tests__/components/SequenceProgressBar.test.ts +39 -0
- package/src/__tests__/components/SettingsModal.test.ts +83 -2
- package/src/__tests__/composables/useApi.test.ts +45 -0
- package/src/__tests__/composables/useAuth.test.ts +20 -0
- package/src/__tests__/composables/useControlSchema.test.ts +4 -0
- package/src/__tests__/composables/useExperimentData.test.ts +23 -0
- package/src/__tests__/composables/useExperimentSamples.test.ts +91 -0
- package/src/__tests__/composables/useProtocolTemplates.test.ts +64 -0
- package/src/__tests__/stores/settings.test.ts +78 -0
- package/src/__tests__/templates/templates.test.ts +86 -0
- package/src/__tests__/utils/instrument.test.ts +47 -0
- package/src/__tests__/utils/lcms.test.ts +73 -0
- package/src/__tests__/utils/permissions.test.ts +50 -0
- package/src/__tests__/utils/rack.test.ts +120 -0
- package/src/components/AppAvatarMenu.vue +6 -3
- package/src/components/AppTopBar.vue +16 -10
- package/src/components/AuditTrail.vue +1 -1
- package/src/components/BaseTabs.vue +22 -1
- package/src/components/Calendar.vue +6 -2
- package/src/components/ConcentrationInput.vue +3 -2
- package/src/components/GroupAssigner.vue +8 -3
- package/src/components/InstrumentAlertLog.vue +191 -0
- package/src/components/InstrumentStateBadge.vue +50 -0
- package/src/components/InstrumentStatusCard.vue +188 -0
- package/src/components/LcmsSequenceTable.vue +191 -0
- package/src/components/NumberInput.vue +5 -3
- package/src/components/ProgressBar.vue +3 -0
- package/src/components/RackEditor.vue +73 -2
- package/src/components/SampleHierarchyTree.vue +3 -2
- package/src/components/SampleSelector.vue +28 -9
- package/src/components/SegmentedControl.story.vue +17 -0
- package/src/components/SegmentedControl.vue +14 -3
- package/src/components/SequenceProgressBar.vue +71 -0
- package/src/components/SettingsModal.vue +49 -2
- package/src/components/UnitInput.vue +6 -2
- package/src/components/WellPlate.vue +145 -24
- package/src/components/index.ts +5 -0
- package/src/components/internal/WellEditPopupInternal.vue +1 -0
- package/src/composables/experimentDesignData.ts +182 -0
- package/src/composables/index.ts +14 -0
- package/src/composables/useApi.ts +113 -16
- package/src/composables/useAuth.ts +4 -0
- package/src/composables/useAutoGroup.ts +18 -9
- package/src/composables/useControlSchema.ts +21 -0
- package/src/composables/useExperimentData.ts +57 -16
- package/src/composables/useExperimentSamples.ts +142 -0
- package/src/composables/useProtocolTemplates.ts +13 -1
- package/src/composables/useRackEditor.ts +3 -2
- package/src/index.ts +27 -0
- package/src/instrument.ts +90 -0
- package/src/lcms.ts +108 -0
- package/src/permissions.ts +143 -0
- package/src/stores/auth.ts +79 -26
- package/src/stores/settings.ts +10 -0
- package/src/styles/components/instrument-monitor.css +478 -0
- package/src/styles/components/lcms-sequence-table.css +189 -0
- package/src/styles/components/sequence-progress-bar.css +63 -0
- package/src/styles/components/settings-modal.css +9 -0
- package/src/styles/components/tabs.css +9 -0
- package/src/styles/components/well-edit-popup.css +7 -1
- package/src/styles/components/well-plate.css +5 -0
- package/src/styles/index.css +3 -0
- package/src/templates/builders.ts +201 -0
- package/src/templates/controlSchemas.ts +68 -0
- package/src/templates/index.ts +2 -0
- package/src/templates/presets.ts +23 -0
- package/src/templates/types.ts +17 -0
- package/src/types/auth.ts +3 -0
- package/src/types/components.ts +45 -3
- package/src/types/form-builder.ts +2 -1
- package/src/types/index.ts +35 -0
- package/src/types/instrument.ts +61 -0
- package/src/types/platform.ts +4 -0
- package/src/utils/rack.ts +209 -0
- package/dist/auth-QQj2kkze.js.map +0 -1
- package/dist/components-DihbSJjU.js.map +0 -1
- package/dist/composables-BcgZ6diz.js.map +0 -1
- package/dist/templates-Cyt0Suwf.js.map +0 -1
- package/dist/useExperimentData-CM6Y0u5L.js.map +0 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
createLcmsControlWellEditData,
|
|
4
|
+
getLcmsDefaultControlWellId,
|
|
5
|
+
lcmsPlateCellsToRacks,
|
|
6
|
+
rackToLcmsPlateCells,
|
|
7
|
+
} from '../../utils/rack'
|
|
8
|
+
import type { Rack } from '../../types'
|
|
9
|
+
|
|
10
|
+
describe('rack LCMS adapters', () => {
|
|
11
|
+
it('converts LCMS plate cells into rack wells grouped by slot', () => {
|
|
12
|
+
const racks = lcmsPlateCellsToRacks([
|
|
13
|
+
{
|
|
14
|
+
row: 'A',
|
|
15
|
+
column: 1,
|
|
16
|
+
sample_name: 'S1',
|
|
17
|
+
sample_type: 'sample',
|
|
18
|
+
injection_volume: 7,
|
|
19
|
+
slot: 'G',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
row: 'H',
|
|
23
|
+
column: 10,
|
|
24
|
+
sample_name: 'IQC',
|
|
25
|
+
sample_type: 'iqc',
|
|
26
|
+
injection_count: 2,
|
|
27
|
+
slot: 'R',
|
|
28
|
+
},
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
expect(racks.map(rack => rack.slot)).toEqual(['R', 'G'])
|
|
32
|
+
expect(racks[0]).toMatchObject({
|
|
33
|
+
id: 'rack-R',
|
|
34
|
+
format: 96,
|
|
35
|
+
wells: {
|
|
36
|
+
H10: {
|
|
37
|
+
sampleType: 'iqc',
|
|
38
|
+
metadata: {
|
|
39
|
+
label: 'IQC',
|
|
40
|
+
injectionVolume: 5,
|
|
41
|
+
injectionCount: 2,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
expect(racks[1].wells.A1?.metadata).toMatchObject({
|
|
47
|
+
label: 'S1',
|
|
48
|
+
injectionVolume: 7,
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('flattens rack wells back to LCMS plate cells with slot metadata', () => {
|
|
53
|
+
const rack: Rack = {
|
|
54
|
+
id: 'rack-R',
|
|
55
|
+
name: 'Rack R',
|
|
56
|
+
format: 54,
|
|
57
|
+
slot: 'R',
|
|
58
|
+
injectionVolume: 5,
|
|
59
|
+
wells: {
|
|
60
|
+
A1: {
|
|
61
|
+
id: 'A1',
|
|
62
|
+
state: 'filled',
|
|
63
|
+
sampleType: 'sample',
|
|
64
|
+
metadata: {
|
|
65
|
+
label: 'S1',
|
|
66
|
+
injectionVolume: 7,
|
|
67
|
+
injectionCount: 3,
|
|
68
|
+
customMethod: 'C:\\Methods\\custom.meth',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
F7: {
|
|
72
|
+
id: 'F7',
|
|
73
|
+
state: 'filled',
|
|
74
|
+
sampleType: 'iqc',
|
|
75
|
+
metadata: {
|
|
76
|
+
label: 'IQC',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
expect(rackToLcmsPlateCells(rack)).toEqual([
|
|
83
|
+
{
|
|
84
|
+
row: 'A',
|
|
85
|
+
column: 1,
|
|
86
|
+
sample_name: 'S1',
|
|
87
|
+
sample_type: 'sample',
|
|
88
|
+
injection_volume: 7,
|
|
89
|
+
injection_count: 3,
|
|
90
|
+
custom_method: 'C:\\Methods\\custom.meth',
|
|
91
|
+
slot: 'R',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
row: 'F',
|
|
95
|
+
column: 7,
|
|
96
|
+
sample_name: 'IQC',
|
|
97
|
+
sample_type: 'iqc',
|
|
98
|
+
injection_volume: 5,
|
|
99
|
+
injection_count: 1,
|
|
100
|
+
custom_method: null,
|
|
101
|
+
slot: 'R',
|
|
102
|
+
},
|
|
103
|
+
])
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('exposes LCMS default control well edit data', () => {
|
|
107
|
+
expect(getLcmsDefaultControlWellId('54vial', 'iqc')).toBe('F7')
|
|
108
|
+
expect(createLcmsControlWellEditData('qc', {
|
|
109
|
+
plateType: '96well',
|
|
110
|
+
injectionVolume: 8,
|
|
111
|
+
})).toEqual({
|
|
112
|
+
wellId: 'H11',
|
|
113
|
+
label: 'QC',
|
|
114
|
+
sampleType: 'qc',
|
|
115
|
+
injectionVolume: 8,
|
|
116
|
+
injectionCount: 1,
|
|
117
|
+
customMethod: '',
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
})
|
|
@@ -35,9 +35,12 @@ const { isOpen, rootRef, close, toggle } = useDropdownState({
|
|
|
35
35
|
const initials = computed(() => {
|
|
36
36
|
if (props.userInitial) return props.userInitial.slice(0, 2).toUpperCase()
|
|
37
37
|
if (props.userName) {
|
|
38
|
-
const parts = props.userName.trim().split(/\s+/)
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
const parts = props.userName.trim().split(/\s+/).filter(Boolean)
|
|
39
|
+
const first = parts[0]
|
|
40
|
+
if (!first) return 'U'
|
|
41
|
+
const second = parts[1]
|
|
42
|
+
if (second) return (first.charAt(0) + second.charAt(0)).toUpperCase()
|
|
43
|
+
return first.slice(0, 2).toUpperCase()
|
|
41
44
|
}
|
|
42
45
|
return 'U'
|
|
43
46
|
})
|
|
@@ -117,7 +117,12 @@ const { isIntegrated, plugin } = usePlatformContext()
|
|
|
117
117
|
const isStandalone = computed(() => !isIntegrated.value)
|
|
118
118
|
const appExperiment = inject(APP_EXPERIMENT_KEY, null)
|
|
119
119
|
|
|
120
|
-
const
|
|
120
|
+
const pluginIcon = computed(() => {
|
|
121
|
+
const currentPlugin = plugin.value
|
|
122
|
+
return currentPlugin?.icon
|
|
123
|
+
? { icon: currentPlugin.icon, color: currentPlugin.color }
|
|
124
|
+
: null
|
|
125
|
+
})
|
|
121
126
|
|
|
122
127
|
const profileInitial = computed(() => {
|
|
123
128
|
if (props.userInitial) return props.userInitial
|
|
@@ -225,10 +230,10 @@ function currentItemIdFromLocation(pages: Array<Pick<PageSelectorItem, 'id' | 't
|
|
|
225
230
|
>
|
|
226
231
|
<slot name="icon">
|
|
227
232
|
<PluginIcon
|
|
228
|
-
v-if="
|
|
233
|
+
v-if="pluginIcon"
|
|
229
234
|
class="mint-topbar__plugin-icon"
|
|
230
|
-
:icon="
|
|
231
|
-
:tone="
|
|
235
|
+
:icon="pluginIcon.icon"
|
|
236
|
+
:tone="pluginIcon.color"
|
|
232
237
|
size="md"
|
|
233
238
|
variant="solid"
|
|
234
239
|
/>
|
|
@@ -248,10 +253,10 @@ function currentItemIdFromLocation(pages: Array<Pick<PageSelectorItem, 'id' | 't
|
|
|
248
253
|
>
|
|
249
254
|
<slot name="icon">
|
|
250
255
|
<PluginIcon
|
|
251
|
-
v-if="
|
|
256
|
+
v-if="pluginIcon"
|
|
252
257
|
class="mint-topbar__plugin-icon"
|
|
253
|
-
:icon="
|
|
254
|
-
:tone="
|
|
258
|
+
:icon="pluginIcon.icon"
|
|
259
|
+
:tone="pluginIcon.color"
|
|
255
260
|
size="md"
|
|
256
261
|
variant="solid"
|
|
257
262
|
/>
|
|
@@ -267,10 +272,10 @@ function currentItemIdFromLocation(pages: Array<Pick<PageSelectorItem, 'id' | 't
|
|
|
267
272
|
<template v-else>
|
|
268
273
|
<slot name="icon">
|
|
269
274
|
<PluginIcon
|
|
270
|
-
v-if="
|
|
275
|
+
v-if="pluginIcon"
|
|
271
276
|
class="mint-topbar__plugin-icon"
|
|
272
|
-
:icon="
|
|
273
|
-
:tone="
|
|
277
|
+
:icon="pluginIcon.icon"
|
|
278
|
+
:tone="pluginIcon.color"
|
|
274
279
|
size="md"
|
|
275
280
|
variant="solid"
|
|
276
281
|
/>
|
|
@@ -446,6 +451,7 @@ function currentItemIdFromLocation(pages: Array<Pick<PageSelectorItem, 'id' | 't
|
|
|
446
451
|
:control-options="settingsConfig?.controlOptions"
|
|
447
452
|
:values="settingsConfig?.values"
|
|
448
453
|
:enhancements="settingsConfig?.enhancements"
|
|
454
|
+
:user-type="settingsConfig?.userType"
|
|
449
455
|
@update:values="emit('settings-values-change', $event)"
|
|
450
456
|
>
|
|
451
457
|
<template v-for="tab in normalizedSettingsTabs" :key="tab.id" #[`tab-${tab.id}`]>
|
|
@@ -32,7 +32,7 @@ const uniqueTypes = computed(() => {
|
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
const uniqueUsers = computed(() => {
|
|
35
|
-
const users = new Set(props.entries.
|
|
35
|
+
const users = new Set(props.entries.flatMap(e => e.user ? [e.user] : []))
|
|
36
36
|
return Array.from(users)
|
|
37
37
|
})
|
|
38
38
|
|
|
@@ -21,6 +21,11 @@ const emit = defineEmits<{
|
|
|
21
21
|
const activeTab = computed(() => props.modelValue)
|
|
22
22
|
const normalizedTabs = computed<TabItem[]>(() => props.tabs.map(normalizeItemInput))
|
|
23
23
|
|
|
24
|
+
function isSvgIcon(icon: TabItem['icon']): icon is string | string[] {
|
|
25
|
+
if (!icon) return false
|
|
26
|
+
return Array.isArray(icon) || icon.trim().startsWith('M') || icon.trim().startsWith('m')
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
function selectTab(tabId: string) {
|
|
25
30
|
const tab = normalizedTabs.value.find(t => t.id === tabId)
|
|
26
31
|
if (tab && !tab.disabled) {
|
|
@@ -46,7 +51,23 @@ function selectTab(tabId: string) {
|
|
|
46
51
|
@click="selectTab(tab.id)"
|
|
47
52
|
>
|
|
48
53
|
<span class="mint-tab__content">
|
|
49
|
-
<
|
|
54
|
+
<svg
|
|
55
|
+
v-if="isSvgIcon(tab.icon)"
|
|
56
|
+
class="mint-tab__icon mint-tab__icon--svg"
|
|
57
|
+
viewBox="0 0 24 24"
|
|
58
|
+
fill="none"
|
|
59
|
+
stroke="currentColor"
|
|
60
|
+
stroke-width="2"
|
|
61
|
+
stroke-linecap="round"
|
|
62
|
+
stroke-linejoin="round"
|
|
63
|
+
aria-hidden="true"
|
|
64
|
+
>
|
|
65
|
+
<template v-if="Array.isArray(tab.icon)">
|
|
66
|
+
<path v-for="(d, index) in tab.icon" :key="index" :d="d" />
|
|
67
|
+
</template>
|
|
68
|
+
<path v-else :d="tab.icon" />
|
|
69
|
+
</svg>
|
|
70
|
+
<span v-else-if="tab.icon" class="mint-tab__icon">{{ tab.icon }}</span>
|
|
50
71
|
{{ tab.label }}
|
|
51
72
|
<span v-if="tab.badge !== undefined" class="mint-tab__badge">
|
|
52
73
|
{{ tab.badge }}
|
|
@@ -134,8 +134,12 @@ const markerMap = computed(() => {
|
|
|
134
134
|
for (const marker of props.markers) {
|
|
135
135
|
const date = parseDate(marker.date)
|
|
136
136
|
const key = toDateKey(date)
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
const markers = map.get(key)
|
|
138
|
+
if (markers) {
|
|
139
|
+
markers.push(marker)
|
|
140
|
+
} else {
|
|
141
|
+
map.set(key, [marker])
|
|
142
|
+
}
|
|
139
143
|
}
|
|
140
144
|
return map
|
|
141
145
|
})
|
|
@@ -37,13 +37,14 @@ const { unitCategories, getConversionHint } = useConcentrationUnits()
|
|
|
37
37
|
|
|
38
38
|
// Filter categories based on allowedUnits
|
|
39
39
|
const filteredCategories = computed(() => {
|
|
40
|
-
|
|
40
|
+
const allowedUnits = props.allowedUnits
|
|
41
|
+
if (!allowedUnits || allowedUnits.length === 0) {
|
|
41
42
|
return unitCategories.value
|
|
42
43
|
}
|
|
43
44
|
return unitCategories.value
|
|
44
45
|
.map(cat => ({
|
|
45
46
|
label: cat.label,
|
|
46
|
-
units: cat.units.filter(u =>
|
|
47
|
+
units: cat.units.filter(u => allowedUnits.includes(u)),
|
|
47
48
|
}))
|
|
48
49
|
.filter(cat => cat.units.length > 0)
|
|
49
50
|
})
|
|
@@ -42,9 +42,12 @@ const assignment = useGroupAssignment({
|
|
|
42
42
|
const { unassignedGroups, zone1Groups, zone2Groups, zone1Count, zone2Count, validationMessage } = assignment
|
|
43
43
|
|
|
44
44
|
function handleDragStart(event: DragEvent, groupName: string) {
|
|
45
|
+
const transfer = event.dataTransfer
|
|
46
|
+
if (!transfer) return
|
|
47
|
+
|
|
45
48
|
draggingGroup.value = groupName
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
transfer.setData('groupName', groupName)
|
|
50
|
+
transfer.effectAllowed = 'move'
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
function handleDragEnd() {
|
|
@@ -54,7 +57,9 @@ function handleDragEnd() {
|
|
|
54
57
|
|
|
55
58
|
function handleDragOver(event: DragEvent, zone: GroupAssignmentZone) {
|
|
56
59
|
event.preventDefault()
|
|
57
|
-
event.dataTransfer
|
|
60
|
+
if (event.dataTransfer) {
|
|
61
|
+
event.dataTransfer.dropEffect = 'move'
|
|
62
|
+
}
|
|
58
63
|
dragOverZone.value = zone
|
|
59
64
|
}
|
|
60
65
|
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/** Filterable instrument alert/event list with optional acknowledge actions. */
|
|
3
|
+
import { computed, ref } from 'vue'
|
|
4
|
+
import type { InstrumentAlert, InstrumentAlertLevel } from '../types'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
alerts?: InstrumentAlert[]
|
|
8
|
+
title?: string
|
|
9
|
+
acknowledgeable?: boolean
|
|
10
|
+
emptyMessage?: string
|
|
11
|
+
filteredEmptyMessage?: string
|
|
12
|
+
locale?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
16
|
+
alerts: () => [],
|
|
17
|
+
title: 'Event Log',
|
|
18
|
+
acknowledgeable: true,
|
|
19
|
+
emptyMessage: 'No alerts received yet',
|
|
20
|
+
filteredEmptyMessage: 'No alerts match current filters',
|
|
21
|
+
locale: undefined,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const emit = defineEmits<{
|
|
25
|
+
acknowledge: [alert: InstrumentAlert]
|
|
26
|
+
}>()
|
|
27
|
+
|
|
28
|
+
const levelConfig: Record<InstrumentAlertLevel, { label: string; cssClass: string; color: string }> = {
|
|
29
|
+
critical: { label: 'Critical', cssClass: 'mint-instrument-alert--critical', color: '#ef4444' },
|
|
30
|
+
warning: { label: 'Warning', cssClass: 'mint-instrument-alert--warning', color: '#eab308' },
|
|
31
|
+
info: { label: 'Info', cssClass: 'mint-instrument-alert--info', color: '#3b82f6' },
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const levels: InstrumentAlertLevel[] = ['critical', 'warning', 'info']
|
|
35
|
+
const activeLevels = ref<Set<InstrumentAlertLevel>>(new Set(levels))
|
|
36
|
+
const activeInstrument = ref<string | null>(null)
|
|
37
|
+
|
|
38
|
+
const instruments = computed(() => {
|
|
39
|
+
const counts = new Map<string, number>()
|
|
40
|
+
for (const alert of props.alerts) {
|
|
41
|
+
const name = instrumentName(alert)
|
|
42
|
+
counts.set(name, (counts.get(name) ?? 0) + 1)
|
|
43
|
+
}
|
|
44
|
+
return [...counts.entries()].map(([name, count]) => ({ name, count }))
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const alertCounts = computed(() => {
|
|
48
|
+
const counts: Record<InstrumentAlertLevel, number> = { critical: 0, warning: 0, info: 0 }
|
|
49
|
+
for (const alert of props.alerts) {
|
|
50
|
+
if (alert.level in counts) counts[alert.level]++
|
|
51
|
+
}
|
|
52
|
+
return counts
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const filteredAlerts = computed(() =>
|
|
56
|
+
[...props.alerts]
|
|
57
|
+
.filter(alert => activeLevels.value.has(alert.level))
|
|
58
|
+
.filter(alert => !activeInstrument.value || instrumentName(alert) === activeInstrument.value)
|
|
59
|
+
.sort((a, b) => alertTime(b) - alertTime(a))
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
function toggleLevel(level: InstrumentAlertLevel) {
|
|
63
|
+
const next = new Set(activeLevels.value)
|
|
64
|
+
if (next.has(level)) {
|
|
65
|
+
if (next.size > 1) next.delete(level)
|
|
66
|
+
} else {
|
|
67
|
+
next.add(level)
|
|
68
|
+
}
|
|
69
|
+
activeLevels.value = next
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function toggleInstrument(name: string) {
|
|
73
|
+
activeInstrument.value = activeInstrument.value === name ? null : name
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function instrumentName(alert: InstrumentAlert): string {
|
|
77
|
+
return alert.instrument_name || alert.instrument_id
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function alertTime(alert: InstrumentAlert): number {
|
|
81
|
+
if (!alert.timestamp) return 0
|
|
82
|
+
const date = alert.timestamp instanceof Date ? alert.timestamp : new Date(alert.timestamp)
|
|
83
|
+
return Number.isNaN(date.getTime()) ? 0 : date.getTime()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function timeAgo(value: string | Date | undefined): string {
|
|
87
|
+
if (!value) return ''
|
|
88
|
+
const date = value instanceof Date ? value : new Date(value)
|
|
89
|
+
if (Number.isNaN(date.getTime())) return ''
|
|
90
|
+
const seconds = Math.floor((Date.now() - date.getTime()) / 1000)
|
|
91
|
+
if (seconds < 60) return 'just now'
|
|
92
|
+
const minutes = Math.floor(seconds / 60)
|
|
93
|
+
if (minutes < 60) return `${minutes}m ago`
|
|
94
|
+
const hours = Math.floor(minutes / 60)
|
|
95
|
+
if (hours < 24) return `${hours}h ago`
|
|
96
|
+
const days = Math.floor(hours / 24)
|
|
97
|
+
if (days < 7) return `${days}d ago`
|
|
98
|
+
return date.toLocaleDateString(props.locale, { month: 'short', day: 'numeric' })
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function alertDetail(alert: InstrumentAlert): string {
|
|
102
|
+
return alert.body?.detail ?? alert.message ?? alert.rule ?? ''
|
|
103
|
+
}
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<template>
|
|
107
|
+
<div class="mint-instrument-alert-log">
|
|
108
|
+
<div class="mint-instrument-alert-log__header">
|
|
109
|
+
<span class="mint-instrument-alert-log__title">{{ title }}</span>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div class="mint-instrument-alert-log__filters">
|
|
113
|
+
<div class="mint-instrument-alert-log__filter-row">
|
|
114
|
+
<button
|
|
115
|
+
v-for="level in levels"
|
|
116
|
+
:key="level"
|
|
117
|
+
type="button"
|
|
118
|
+
class="mint-instrument-alert-log__filter"
|
|
119
|
+
:class="{ 'mint-instrument-alert-log__filter--active': activeLevels.has(level) }"
|
|
120
|
+
:style="activeLevels.has(level) ? { '--filter-color': levelConfig[level].color } : undefined"
|
|
121
|
+
@click="toggleLevel(level)"
|
|
122
|
+
>
|
|
123
|
+
<span
|
|
124
|
+
class="mint-instrument-alert-log__filter-dot"
|
|
125
|
+
:style="{ backgroundColor: levelConfig[level].color }"
|
|
126
|
+
/>
|
|
127
|
+
{{ levelConfig[level].label }}
|
|
128
|
+
<span class="mint-instrument-alert-log__filter-count">{{ alertCounts[level] }}</span>
|
|
129
|
+
</button>
|
|
130
|
+
</div>
|
|
131
|
+
<div v-if="instruments.length > 1" class="mint-instrument-alert-log__filter-row">
|
|
132
|
+
<button
|
|
133
|
+
v-for="instrument in instruments"
|
|
134
|
+
:key="instrument.name"
|
|
135
|
+
type="button"
|
|
136
|
+
class="mint-instrument-alert-log__filter"
|
|
137
|
+
:class="{ 'mint-instrument-alert-log__filter--active': activeInstrument === instrument.name }"
|
|
138
|
+
@click="toggleInstrument(instrument.name)"
|
|
139
|
+
>
|
|
140
|
+
{{ instrument.name }}
|
|
141
|
+
<span class="mint-instrument-alert-log__filter-count">{{ instrument.count }}</span>
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<TransitionGroup name="mint-instrument-alert" tag="div" class="mint-instrument-alert-log__list">
|
|
147
|
+
<div
|
|
148
|
+
v-for="alert in filteredAlerts"
|
|
149
|
+
:key="alert.id ?? `${alert.instrument_id}-${alert.rule ?? alert.message}-${alert.timestamp ?? ''}`"
|
|
150
|
+
class="mint-instrument-alert"
|
|
151
|
+
:class="[
|
|
152
|
+
levelConfig[alert.level]?.cssClass,
|
|
153
|
+
{ 'mint-instrument-alert--acknowledged': alert.acknowledged },
|
|
154
|
+
]"
|
|
155
|
+
>
|
|
156
|
+
<div class="mint-instrument-alert__header">
|
|
157
|
+
<span class="mint-instrument-alert__instrument">{{ instrumentName(alert) }}</span>
|
|
158
|
+
<span v-if="alert.body?.source" class="mint-instrument-alert__source">
|
|
159
|
+
{{ alert.body.source }}
|
|
160
|
+
</span>
|
|
161
|
+
<span class="mint-instrument-alert__time">{{ timeAgo(alert.timestamp) }}</span>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="mint-instrument-alert__detail">{{ alertDetail(alert) }}</div>
|
|
164
|
+
<div
|
|
165
|
+
v-if="alert.body?.code != null || (acknowledgeable && !alert.acknowledged && alert.level !== 'info')"
|
|
166
|
+
class="mint-instrument-alert__footer"
|
|
167
|
+
>
|
|
168
|
+
<span v-if="alert.body?.code != null" class="mint-instrument-alert__code">
|
|
169
|
+
Code {{ alert.body.code }}
|
|
170
|
+
</span>
|
|
171
|
+
<button
|
|
172
|
+
v-if="acknowledgeable && !alert.acknowledged && alert.level !== 'info'"
|
|
173
|
+
type="button"
|
|
174
|
+
class="mint-instrument-alert__ack"
|
|
175
|
+
@click="emit('acknowledge', alert)"
|
|
176
|
+
>
|
|
177
|
+
Acknowledge
|
|
178
|
+
</button>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div v-if="filteredAlerts.length === 0" key="empty" class="mint-instrument-alert-log__empty">
|
|
183
|
+
{{ alerts.length === 0 ? emptyMessage : filteredEmptyMessage }}
|
|
184
|
+
</div>
|
|
185
|
+
</TransitionGroup>
|
|
186
|
+
</div>
|
|
187
|
+
</template>
|
|
188
|
+
|
|
189
|
+
<style>
|
|
190
|
+
@import '../styles/components/instrument-monitor.css';
|
|
191
|
+
</style>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/** Status badge for shared instrument monitor states. */
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import type { InstrumentState } from '../types'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
state: InstrumentState | string
|
|
8
|
+
label?: string
|
|
9
|
+
pulseWhenRunning?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
13
|
+
label: undefined,
|
|
14
|
+
pulseWhenRunning: true,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const stateConfig: Record<InstrumentState, { label: string; tone: string }> = {
|
|
18
|
+
running: { label: 'RUNNING', tone: 'success' },
|
|
19
|
+
standby: { label: 'STANDBY', tone: 'warning' },
|
|
20
|
+
idle: { label: 'IDLE', tone: 'muted' },
|
|
21
|
+
error: { label: 'ERROR', tone: 'error' },
|
|
22
|
+
connected: { label: 'CONNECTED', tone: 'info' },
|
|
23
|
+
disconnected: { label: 'OFFLINE', tone: 'muted' },
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const normalizedState = computed(() => props.state.toLowerCase())
|
|
27
|
+
const config = computed(() => {
|
|
28
|
+
const match = stateConfig[normalizedState.value as InstrumentState]
|
|
29
|
+
return match ?? { label: props.state.toUpperCase(), tone: 'muted' }
|
|
30
|
+
})
|
|
31
|
+
const shouldPulse = computed(() => props.pulseWhenRunning && normalizedState.value === 'running')
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<span
|
|
36
|
+
class="mint-instrument-state"
|
|
37
|
+
:class="[
|
|
38
|
+
`mint-instrument-state--${config.tone}`,
|
|
39
|
+
{ 'mint-instrument-state--pulse': shouldPulse },
|
|
40
|
+
{ 'mint-instrument-state--offline': normalizedState === 'disconnected' },
|
|
41
|
+
]"
|
|
42
|
+
>
|
|
43
|
+
<span v-if="shouldPulse" class="mint-instrument-state__dot" />
|
|
44
|
+
{{ label ?? config.label }}
|
|
45
|
+
</span>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<style>
|
|
49
|
+
@import '../styles/components/instrument-monitor.css';
|
|
50
|
+
</style>
|