@morscherlab/mld-sdk 0.9.3 → 0.9.5
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/components/ExperimentSelectorModal.vue.d.ts +6 -0
- package/dist/components/ExperimentSelectorModal.vue.js +284 -35
- package/dist/components/ExperimentSelectorModal.vue.js.map +1 -1
- package/dist/components/ResourceCard.vue.d.ts +1 -1
- package/dist/components/StatusIndicator.vue.d.ts +1 -1
- package/dist/composables/experiment-utils.d.ts +4 -1
- package/dist/composables/experiment-utils.js +22 -0
- package/dist/composables/experiment-utils.js.map +1 -1
- package/dist/composables/index.d.ts +1 -1
- package/dist/composables/index.js +4 -1
- package/dist/composables/useExperimentSelector.d.ts +6 -1
- package/dist/composables/useExperimentSelector.js +76 -7
- package/dist/composables/useExperimentSelector.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -1
- package/dist/stores/auth.js +21 -14
- package/dist/stores/auth.js.map +1 -1
- package/dist/styles.css +277 -6
- package/dist/types/components.d.ts +10 -0
- package/dist/types/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/ExperimentSelectorModal.story.vue +170 -23
- package/src/components/ExperimentSelectorModal.vue +205 -14
- package/src/composables/experiment-utils.ts +23 -1
- package/src/composables/index.ts +3 -0
- package/src/composables/useExperimentSelector.ts +113 -9
- package/src/index.ts +3 -0
- package/src/stores/auth.ts +24 -16
- package/src/styles/components/experiment-selector-modal.css +152 -3
- package/src/types/components.ts +11 -0
- package/src/types/index.ts +3 -0
package/src/composables/index.ts
CHANGED
|
@@ -95,9 +95,12 @@ export {
|
|
|
95
95
|
} from './useExperimentSelector'
|
|
96
96
|
export {
|
|
97
97
|
formatExperimentDate,
|
|
98
|
+
datePresetToISO,
|
|
98
99
|
EXPERIMENT_STATUS_OPTIONS,
|
|
99
100
|
EXPERIMENT_STATUS_VARIANT_MAP,
|
|
100
101
|
EXPERIMENT_STATUS_LABELS,
|
|
102
|
+
DATE_PRESET_OPTIONS,
|
|
103
|
+
SORT_OPTIONS,
|
|
101
104
|
} from './experiment-utils'
|
|
102
105
|
export {
|
|
103
106
|
useExperimentData,
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { ref, reactive, computed, watch, onScopeDispose, type Ref, type ComputedRef } from 'vue'
|
|
2
2
|
import { useApi } from './useApi'
|
|
3
|
-
import
|
|
3
|
+
import { datePresetToISO } from './experiment-utils'
|
|
4
|
+
import type {
|
|
5
|
+
ExperimentSummary,
|
|
6
|
+
ExperimentListResponse,
|
|
7
|
+
ExperimentFilters,
|
|
8
|
+
ExperimentTypeOption,
|
|
9
|
+
ExperimentSortField,
|
|
10
|
+
PlatformContext,
|
|
11
|
+
SelectOption,
|
|
12
|
+
} from '../types'
|
|
4
13
|
|
|
5
14
|
function getPlatformContext(): PlatformContext | undefined {
|
|
6
15
|
if (typeof window === 'undefined') return undefined
|
|
@@ -27,11 +36,20 @@ export interface UseExperimentSelectorReturn {
|
|
|
27
36
|
error: Ref<string | null>
|
|
28
37
|
page: Ref<number>
|
|
29
38
|
hasMore: ComputedRef<boolean>
|
|
39
|
+
// Sort
|
|
40
|
+
sortKey: Ref<string>
|
|
41
|
+
// Filter options
|
|
42
|
+
experimentTypes: Ref<ExperimentTypeOption[]>
|
|
43
|
+
projects: Ref<SelectOption<string>[]>
|
|
44
|
+
// Grouped view
|
|
45
|
+
groupedByProject: ComputedRef<[string, ExperimentSummary[]][]>
|
|
46
|
+
// Methods
|
|
30
47
|
fetch: () => Promise<void>
|
|
31
48
|
loadMore: () => Promise<void>
|
|
32
49
|
reset: () => void
|
|
33
50
|
select: (experiment: ExperimentSummary) => void
|
|
34
51
|
clear: () => void
|
|
52
|
+
fetchFilterOptions: () => Promise<void>
|
|
35
53
|
}
|
|
36
54
|
|
|
37
55
|
export function useExperimentSelector(
|
|
@@ -48,26 +66,62 @@ export function useExperimentSelector(
|
|
|
48
66
|
const error = ref<string | null>(null)
|
|
49
67
|
const page = ref(0)
|
|
50
68
|
|
|
69
|
+
// Sort: combined key like "created_at:desc"
|
|
70
|
+
const sortKey = ref<string>('created_at:desc')
|
|
71
|
+
|
|
72
|
+
// Filter option data (fetched once, cached)
|
|
73
|
+
const experimentTypes = ref<ExperimentTypeOption[]>([])
|
|
74
|
+
const projects = ref<SelectOption<string>[]>([])
|
|
75
|
+
let filterOptionsFetched = false
|
|
76
|
+
|
|
51
77
|
const hasMore = computed(() => experiments.value.length < total.value)
|
|
52
78
|
|
|
79
|
+
const filters: ExperimentFilters = reactive({
|
|
80
|
+
search: undefined,
|
|
81
|
+
status: undefined,
|
|
82
|
+
project: undefined,
|
|
83
|
+
experimentType: undefined,
|
|
84
|
+
datePreset: undefined,
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
function parseSortKey(): { sortBy: ExperimentSortField; sortOrder: 'asc' | 'desc' } {
|
|
88
|
+
const [field, order] = sortKey.value.split(':')
|
|
89
|
+
return {
|
|
90
|
+
sortBy: (field || 'created_at') as ExperimentSortField,
|
|
91
|
+
sortOrder: (order || 'desc') as 'asc' | 'desc',
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
53
95
|
async function fetchExperiments(): Promise<void> {
|
|
54
96
|
isLoading.value = true
|
|
55
97
|
error.value = null
|
|
56
98
|
try {
|
|
57
99
|
const params = new URLSearchParams()
|
|
58
|
-
// Priority: explicit option > platform context (single type) > no filter
|
|
100
|
+
// Priority: explicit option > platform context (single type) > filter dropdown > no filter
|
|
59
101
|
const allowedTypes = getPlatformContext()?.allowedExperimentTypes
|
|
60
102
|
const effectiveType = experimentType
|
|
61
103
|
?? (allowedTypes?.length === 1 ? allowedTypes[0] : undefined)
|
|
104
|
+
?? filters.experimentType
|
|
105
|
+
?? undefined
|
|
62
106
|
if (effectiveType) params.set('experiment_type', effectiveType)
|
|
63
107
|
if (filters.status) params.set('status', filters.status)
|
|
64
108
|
if (filters.search) params.set('search', filters.search)
|
|
65
109
|
if (filters.project) params.set('project', filters.project)
|
|
110
|
+
|
|
111
|
+
// Sort params
|
|
112
|
+
const { sortBy, sortOrder } = parseSortKey()
|
|
113
|
+
params.set('sort_by', sortBy)
|
|
114
|
+
params.set('sort_order', sortOrder)
|
|
115
|
+
|
|
116
|
+
// Date preset → created_after
|
|
117
|
+
if (filters.datePreset) {
|
|
118
|
+
params.set('created_after', datePresetToISO(filters.datePreset))
|
|
119
|
+
}
|
|
120
|
+
|
|
66
121
|
params.set('limit', String(limit))
|
|
67
122
|
params.set('skip', String(page.value * limit))
|
|
68
123
|
|
|
69
124
|
const query = params.toString()
|
|
70
|
-
// Use absolute platform URL in integrated mode to bypass plugin's baseURL
|
|
71
125
|
const base = platformBase ?? '/api'
|
|
72
126
|
const url = `${base}/experiments${query ? `?${query}` : ''}`
|
|
73
127
|
const data = await api.get<ExperimentListResponse>(url)
|
|
@@ -96,6 +150,32 @@ export function useExperimentSelector(
|
|
|
96
150
|
}
|
|
97
151
|
}
|
|
98
152
|
|
|
153
|
+
async function fetchFilterOptions(): Promise<void> {
|
|
154
|
+
if (filterOptionsFetched) return
|
|
155
|
+
filterOptionsFetched = true
|
|
156
|
+
|
|
157
|
+
const base = platformBase ?? '/api'
|
|
158
|
+
const [typesRes, projectsRes] = await Promise.allSettled([
|
|
159
|
+
api.get<Array<{ name: string; color?: string }>>(`${base}/experiments/experiment-types`),
|
|
160
|
+
api.get<Array<{ id: number; name: string }>>(`${base}/projects`),
|
|
161
|
+
])
|
|
162
|
+
|
|
163
|
+
if (typesRes.status === 'fulfilled' && Array.isArray(typesRes.value)) {
|
|
164
|
+
experimentTypes.value = typesRes.value.map(t => ({
|
|
165
|
+
value: t.name,
|
|
166
|
+
label: t.name,
|
|
167
|
+
color: t.color,
|
|
168
|
+
}))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (projectsRes.status === 'fulfilled' && Array.isArray(projectsRes.value)) {
|
|
172
|
+
projects.value = projectsRes.value.map(p => ({
|
|
173
|
+
value: p.name,
|
|
174
|
+
label: p.name,
|
|
175
|
+
}))
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
99
179
|
async function loadMore(): Promise<void> {
|
|
100
180
|
if (!hasMore.value || isLoading.value) return
|
|
101
181
|
page.value++
|
|
@@ -118,13 +198,30 @@ export function useExperimentSelector(
|
|
|
118
198
|
filters.search = undefined
|
|
119
199
|
filters.status = undefined
|
|
120
200
|
filters.project = undefined
|
|
201
|
+
filters.experimentType = undefined
|
|
202
|
+
filters.datePreset = undefined
|
|
203
|
+
sortKey.value = 'created_at:desc'
|
|
121
204
|
page.value = 0
|
|
122
205
|
}
|
|
123
206
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
207
|
+
// Group experiments by project (client-side)
|
|
208
|
+
const groupedByProject = computed<[string, ExperimentSummary[]][]>(() => {
|
|
209
|
+
const groups = new Map<string, ExperimentSummary[]>()
|
|
210
|
+
for (const exp of experiments.value) {
|
|
211
|
+
const key = exp.project_name ?? exp.project ?? 'No project'
|
|
212
|
+
const list = groups.get(key)
|
|
213
|
+
if (list) {
|
|
214
|
+
list.push(exp)
|
|
215
|
+
} else {
|
|
216
|
+
groups.set(key, [exp])
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Sort alphabetically, "No project" last
|
|
220
|
+
return [...groups.entries()].sort(([a], [b]) => {
|
|
221
|
+
if (a === 'No project') return 1
|
|
222
|
+
if (b === 'No project') return -1
|
|
223
|
+
return a.localeCompare(b)
|
|
224
|
+
})
|
|
128
225
|
})
|
|
129
226
|
|
|
130
227
|
// Debounced watch on search filter
|
|
@@ -140,10 +237,12 @@ export function useExperimentSelector(
|
|
|
140
237
|
},
|
|
141
238
|
)
|
|
142
239
|
|
|
143
|
-
// Immediate watch on
|
|
240
|
+
// Immediate watch on discrete filters and sort (cancel any pending search debounce)
|
|
144
241
|
watch(
|
|
145
|
-
() => [filters.status, filters.project],
|
|
242
|
+
() => [filters.status, filters.project, filters.experimentType, filters.datePreset, sortKey.value],
|
|
146
243
|
() => {
|
|
244
|
+
if (debounceTimer) clearTimeout(debounceTimer)
|
|
245
|
+
debounceTimer = null
|
|
147
246
|
page.value = 0
|
|
148
247
|
fetchExperiments()
|
|
149
248
|
},
|
|
@@ -166,10 +265,15 @@ export function useExperimentSelector(
|
|
|
166
265
|
error,
|
|
167
266
|
page,
|
|
168
267
|
hasMore,
|
|
268
|
+
sortKey,
|
|
269
|
+
experimentTypes,
|
|
270
|
+
projects,
|
|
271
|
+
groupedByProject,
|
|
169
272
|
fetch: fetchExperiments,
|
|
170
273
|
loadMore,
|
|
171
274
|
reset,
|
|
172
275
|
select,
|
|
173
276
|
clear,
|
|
277
|
+
fetchFilterOptions,
|
|
174
278
|
}
|
|
175
279
|
}
|
package/src/index.ts
CHANGED
|
@@ -153,9 +153,12 @@ export {
|
|
|
153
153
|
type UseExperimentSelectorReturn,
|
|
154
154
|
// Experiment utilities
|
|
155
155
|
formatExperimentDate,
|
|
156
|
+
datePresetToISO,
|
|
156
157
|
EXPERIMENT_STATUS_OPTIONS,
|
|
157
158
|
EXPERIMENT_STATUS_VARIANT_MAP,
|
|
158
159
|
EXPERIMENT_STATUS_LABELS,
|
|
160
|
+
DATE_PRESET_OPTIONS,
|
|
161
|
+
SORT_OPTIONS,
|
|
159
162
|
// Experiment data
|
|
160
163
|
useExperimentData,
|
|
161
164
|
type UseExperimentDataOptions,
|
package/src/stores/auth.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type { AuthConfig, UserInfo } from '../types'
|
|
|
5
5
|
const AUTH_TOKEN_KEY = 'mld-auth-token'
|
|
6
6
|
const AUTH_EXPIRES_KEY = 'mld-auth-expires'
|
|
7
7
|
|
|
8
|
+
const hasLocalStorage = typeof localStorage !== 'undefined' && typeof localStorage.getItem === 'function'
|
|
9
|
+
|
|
8
10
|
export const useAuthStore = defineStore('mld-auth', () => {
|
|
9
11
|
// State
|
|
10
12
|
const token = ref<string | null>(null)
|
|
@@ -50,18 +52,20 @@ export const useAuthStore = defineStore('mld-auth', () => {
|
|
|
50
52
|
|
|
51
53
|
// Actions
|
|
52
54
|
function initialize() {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
55
|
+
if (hasLocalStorage) {
|
|
56
|
+
const storedToken = localStorage.getItem(AUTH_TOKEN_KEY)
|
|
57
|
+
const storedExpires = localStorage.getItem(AUTH_EXPIRES_KEY)
|
|
58
|
+
|
|
59
|
+
if (storedToken) {
|
|
60
|
+
token.value = storedToken
|
|
61
|
+
|
|
62
|
+
if (storedExpires) {
|
|
63
|
+
const expires = new Date(storedExpires)
|
|
64
|
+
if (expires > new Date()) {
|
|
65
|
+
tokenExpires.value = expires
|
|
66
|
+
} else {
|
|
67
|
+
clearToken()
|
|
68
|
+
}
|
|
65
69
|
}
|
|
66
70
|
}
|
|
67
71
|
}
|
|
@@ -74,8 +78,10 @@ export const useAuthStore = defineStore('mld-auth', () => {
|
|
|
74
78
|
const expires = new Date(Date.now() + expiresIn * 1000)
|
|
75
79
|
tokenExpires.value = expires
|
|
76
80
|
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
if (hasLocalStorage) {
|
|
82
|
+
localStorage.setItem(AUTH_TOKEN_KEY, accessToken)
|
|
83
|
+
localStorage.setItem(AUTH_EXPIRES_KEY, expires.toISOString())
|
|
84
|
+
}
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
function clearToken() {
|
|
@@ -84,8 +90,10 @@ export const useAuthStore = defineStore('mld-auth', () => {
|
|
|
84
90
|
username.value = null
|
|
85
91
|
userInfo.value = null
|
|
86
92
|
|
|
87
|
-
|
|
88
|
-
|
|
93
|
+
if (hasLocalStorage) {
|
|
94
|
+
localStorage.removeItem(AUTH_TOKEN_KEY)
|
|
95
|
+
localStorage.removeItem(AUTH_EXPIRES_KEY)
|
|
96
|
+
}
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
function setUserInfo(info: UserInfo) {
|
|
@@ -6,7 +6,16 @@
|
|
|
6
6
|
gap: 0.75rem;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
/*
|
|
9
|
+
/* Shared filter row layout */
|
|
10
|
+
.mld-experiment-selector__filters-row,
|
|
11
|
+
.mld-experiment-selector__filters-advanced {
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
gap: 0.5rem;
|
|
15
|
+
flex-wrap: wrap;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* Keep old class name working for backwards compat */
|
|
10
19
|
.mld-experiment-selector__filters {
|
|
11
20
|
display: flex;
|
|
12
21
|
gap: 0.5rem;
|
|
@@ -14,14 +23,76 @@
|
|
|
14
23
|
|
|
15
24
|
.mld-experiment-selector__search {
|
|
16
25
|
flex: 1;
|
|
17
|
-
min-width:
|
|
26
|
+
min-width: 10rem;
|
|
18
27
|
}
|
|
19
28
|
|
|
20
|
-
.mld-experiment-selector__status-filter
|
|
29
|
+
.mld-experiment-selector__status-filter,
|
|
30
|
+
.mld-experiment-selector__filter-select {
|
|
21
31
|
flex-shrink: 0;
|
|
22
32
|
width: 10rem;
|
|
23
33
|
}
|
|
24
34
|
|
|
35
|
+
/* "Filters" toggle button */
|
|
36
|
+
.mld-experiment-selector__filters-toggle {
|
|
37
|
+
display: inline-flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
gap: 0.375rem;
|
|
40
|
+
padding: 0.375rem 0.625rem;
|
|
41
|
+
border: 1px solid var(--border-color);
|
|
42
|
+
border-radius: var(--mld-radius-sm);
|
|
43
|
+
background: var(--bg-primary);
|
|
44
|
+
color: var(--text-secondary);
|
|
45
|
+
font-size: 0.8125rem;
|
|
46
|
+
cursor: pointer;
|
|
47
|
+
white-space: nowrap;
|
|
48
|
+
position: relative;
|
|
49
|
+
transition: border-color 0.15s ease, color 0.15s ease;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.mld-experiment-selector__filters-toggle:hover {
|
|
53
|
+
border-color: var(--color-primary);
|
|
54
|
+
color: var(--text-primary);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.mld-experiment-selector__filters-toggle--active {
|
|
58
|
+
color: var(--color-primary);
|
|
59
|
+
border-color: var(--color-primary);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Active filter badge dot */
|
|
63
|
+
.mld-experiment-selector__filters-dot {
|
|
64
|
+
width: 6px;
|
|
65
|
+
height: 6px;
|
|
66
|
+
border-radius: 50%;
|
|
67
|
+
background-color: var(--color-primary, #3b82f6);
|
|
68
|
+
flex-shrink: 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Advanced filters row */
|
|
72
|
+
.mld-experiment-selector__filters-advanced {
|
|
73
|
+
padding-top: 0.25rem;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Group toggle checkbox label */
|
|
77
|
+
.mld-experiment-selector__group-toggle {
|
|
78
|
+
display: inline-flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
gap: 0.375rem;
|
|
81
|
+
font-size: 0.8125rem;
|
|
82
|
+
color: var(--text-secondary);
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
white-space: nowrap;
|
|
85
|
+
user-select: none;
|
|
86
|
+
margin-left: auto;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.mld-experiment-selector__group-checkbox {
|
|
90
|
+
width: 14px;
|
|
91
|
+
height: 14px;
|
|
92
|
+
accent-color: var(--color-primary, #3b82f6);
|
|
93
|
+
cursor: pointer;
|
|
94
|
+
}
|
|
95
|
+
|
|
25
96
|
/* Loading skeleton */
|
|
26
97
|
.mld-experiment-selector__skeleton {
|
|
27
98
|
display: flex;
|
|
@@ -66,6 +137,60 @@
|
|
|
66
137
|
border-radius: var(--mld-radius);
|
|
67
138
|
}
|
|
68
139
|
|
|
140
|
+
/* Project group header */
|
|
141
|
+
.mld-experiment-selector__group-header {
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
gap: 0.5rem;
|
|
145
|
+
padding: 0.5rem 1rem;
|
|
146
|
+
background-color: var(--bg-secondary, var(--bg-primary));
|
|
147
|
+
border: none;
|
|
148
|
+
cursor: pointer;
|
|
149
|
+
width: 100%;
|
|
150
|
+
text-align: left;
|
|
151
|
+
font-size: 0.75rem;
|
|
152
|
+
font-weight: 600;
|
|
153
|
+
color: var(--text-secondary);
|
|
154
|
+
text-transform: uppercase;
|
|
155
|
+
letter-spacing: 0.03em;
|
|
156
|
+
transition: background-color 0.15s ease;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.mld-experiment-selector__group-header:hover {
|
|
160
|
+
background-color: var(--bg-hover);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.mld-experiment-selector__group-chevron {
|
|
164
|
+
flex-shrink: 0;
|
|
165
|
+
transition: transform 0.15s ease;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.mld-experiment-selector__group-chevron--collapsed {
|
|
169
|
+
transform: rotate(-90deg);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.mld-experiment-selector__group-name {
|
|
173
|
+
flex: 1;
|
|
174
|
+
min-width: 0;
|
|
175
|
+
overflow: hidden;
|
|
176
|
+
text-overflow: ellipsis;
|
|
177
|
+
white-space: nowrap;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.mld-experiment-selector__group-count {
|
|
181
|
+
display: inline-flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
justify-content: center;
|
|
184
|
+
min-width: 1.25rem;
|
|
185
|
+
height: 1.25rem;
|
|
186
|
+
padding: 0 0.375rem;
|
|
187
|
+
border-radius: 999px;
|
|
188
|
+
background-color: var(--bg-tertiary, var(--border-color));
|
|
189
|
+
font-size: 0.6875rem;
|
|
190
|
+
font-weight: 500;
|
|
191
|
+
color: var(--text-muted);
|
|
192
|
+
}
|
|
193
|
+
|
|
69
194
|
/* Row */
|
|
70
195
|
.mld-experiment-selector__row {
|
|
71
196
|
display: flex;
|
|
@@ -134,3 +259,27 @@
|
|
|
134
259
|
color: var(--text-muted);
|
|
135
260
|
margin-top: 0.125rem;
|
|
136
261
|
}
|
|
262
|
+
|
|
263
|
+
/* Footer with clear selection */
|
|
264
|
+
.mld-experiment-selector__footer {
|
|
265
|
+
display: flex;
|
|
266
|
+
justify-content: flex-start;
|
|
267
|
+
padding-top: 0.25rem;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.mld-experiment-selector__clear-btn {
|
|
271
|
+
padding: 0.25rem 0;
|
|
272
|
+
border: none;
|
|
273
|
+
background: none;
|
|
274
|
+
color: var(--text-muted);
|
|
275
|
+
font-size: 0.8125rem;
|
|
276
|
+
cursor: pointer;
|
|
277
|
+
text-decoration: underline;
|
|
278
|
+
text-decoration-color: transparent;
|
|
279
|
+
transition: color 0.15s ease, text-decoration-color 0.15s ease;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.mld-experiment-selector__clear-btn:hover {
|
|
283
|
+
color: var(--color-primary);
|
|
284
|
+
text-decoration-color: var(--color-primary);
|
|
285
|
+
}
|
package/src/types/components.ts
CHANGED
|
@@ -604,6 +604,14 @@ export interface ResourceSpec {
|
|
|
604
604
|
|
|
605
605
|
// Experiment selector types
|
|
606
606
|
export type ExperimentStatus = 'planned' | 'ongoing' | 'completed'
|
|
607
|
+
export type DatePreset = 'last_7_days' | 'last_30_days' | 'last_90_days'
|
|
608
|
+
export type ExperimentSortField = 'created_at' | 'updated_at' | 'name' | 'status'
|
|
609
|
+
|
|
610
|
+
export interface ExperimentTypeOption {
|
|
611
|
+
value: string
|
|
612
|
+
label: string
|
|
613
|
+
color?: string
|
|
614
|
+
}
|
|
607
615
|
|
|
608
616
|
export interface ExperimentSummary {
|
|
609
617
|
id: number
|
|
@@ -613,6 +621,7 @@ export interface ExperimentSummary {
|
|
|
613
621
|
experiment_type: string
|
|
614
622
|
project?: string
|
|
615
623
|
project_id?: number
|
|
624
|
+
project_name?: string
|
|
616
625
|
notes?: string
|
|
617
626
|
tags?: Record<string, unknown>
|
|
618
627
|
created_at: string
|
|
@@ -629,6 +638,8 @@ export interface ExperimentFilters {
|
|
|
629
638
|
search?: string
|
|
630
639
|
status?: ExperimentStatus | null
|
|
631
640
|
project?: string | null
|
|
641
|
+
experimentType?: string | null
|
|
642
|
+
datePreset?: DatePreset | null
|
|
632
643
|
}
|
|
633
644
|
|
|
634
645
|
// FitPanel types
|
package/src/types/index.ts
CHANGED