Skip to content

Commit 3b9b747

Browse files
committed
feat: add label selection to filter
Signed-off-by: Olivier Vernin <olivier@vernin.me>
1 parent d7e0e53 commit 3b9b747

1 file changed

Lines changed: 200 additions & 1 deletion

File tree

src/components/scm/_filter.vue

Lines changed: 200 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,50 @@
2828
:disabled="!isRepositoryBranchesData()"
2929
></v-select>
3030

31-
<!-- Date Range Slider -->
31+
<!-- Label Key and Value Selection -->
32+
<div v-for="(label, index) in selectedLabels" :key="index">
33+
<v-row class="align-center">
34+
<v-col cols="12" sm="5">
35+
<v-select
36+
label="Label Key (Optional)"
37+
:items="labelKeys"
38+
prepend-inner-icon="mdi-label"
39+
v-model="label.key"
40+
clearable
41+
@update:model-value="onLabelKeyChange(index)"
42+
></v-select>
43+
</v-col>
44+
<v-col cols="12" sm="5">
45+
<v-select
46+
label="Label Value (Optional)"
47+
:items="getLabelValuesForIndex(index)"
48+
prepend-inner-icon="mdi-label-multiple"
49+
v-model="label.value"
50+
clearable
51+
:disabled="!label.key"
52+
@update:model-value="onLabelValueChange(index)"
53+
></v-select>
54+
</v-col>
55+
<v-col cols="12" sm="2" class="d-flex gap-2">
56+
<v-btn
57+
icon="mdi-plus"
58+
size="small"
59+
@click="addLabelRow"
60+
v-if="index === selectedLabels.length - 1"
61+
:disabled="!canAddNewLabelRow()"
62+
></v-btn>
63+
<v-btn
64+
icon="mdi-delete"
65+
size="small"
66+
color="error"
67+
@click="removeLabelRow(index)"
68+
v-if="selectedLabels.length > 1"
69+
></v-btn>
70+
</v-col>
71+
</v-row>
72+
</div>
73+
74+
<!-- Date Range Slider -->
3275
<v-range-slider
3376
v-model="dateRange"
3477
:reverse="false"
@@ -120,10 +163,15 @@ export default {
120163
branch : "",
121164
restrictedSCM: "",
122165
dateRange: [0, 24], // [6 hours ago, now] by default
166+
labelKeys: [],
167+
labelValuesByKey: {}, // Map to store label values for each key
168+
selectedLabels: [{ key: null, value: null }], // Array of label selections
169+
debounceTimer: null,
123170
}),
124171
125172
beforeUnmount() {
126173
this.cancelAutoUpdate();
174+
clearTimeout(this.debounceTimer);
127175
},
128176
129177
computed: {
@@ -228,6 +276,71 @@ export default {
228276
return this.restrictedSCM != ""
229277
},
230278
279+
async getLabelKeys() {
280+
try {
281+
const auth_enabled = process.env.VUE_APP_AUTH_ENABLED === 'true';
282+
let query = `${getApiBaseURL()}/pipeline/labels?keyonly=true&start_time=${encodeURIComponent(this.formattedStartTime)}&end_time=${encodeURIComponent(this.formattedEndTime)}`;
283+
284+
if (auth_enabled) {
285+
const token = await this.$auth0.getAccessTokenSilently();
286+
const response = await fetch(query, {
287+
headers: {
288+
Authorization: `Bearer ${token}`
289+
}
290+
});
291+
const data = await response.json();
292+
this.labelKeys = data.labels || [];
293+
} else {
294+
const response = await fetch(query);
295+
const data = await response.json();
296+
this.labelKeys = data.labels || [];
297+
}
298+
} catch (error) {
299+
console.error('Error fetching label keys:', error);
300+
this.labelKeys = [];
301+
}
302+
},
303+
304+
async getLabelValues(labelKey) {
305+
try {
306+
if (!labelKey) {
307+
return [];
308+
}
309+
310+
// Return cached values if available
311+
if (this.labelValuesByKey[labelKey]) {
312+
return this.labelValuesByKey[labelKey];
313+
}
314+
315+
const auth_enabled = process.env.VUE_APP_AUTH_ENABLED === 'true';
316+
let query = `${getApiBaseURL()}/pipeline/labels?key=${encodeURIComponent(labelKey)}&start_time=${encodeURIComponent(this.formattedStartTime)}&end_time=${encodeURIComponent(this.formattedEndTime)}`;
317+
318+
if (auth_enabled) {
319+
const token = await this.$auth0.getAccessTokenSilently();
320+
const response = await fetch(query, {
321+
headers: {
322+
Authorization: `Bearer ${token}`
323+
}
324+
});
325+
const data = await response.json();
326+
// Extract unique values from the labels array
327+
const uniqueValues = [...new Set(data.labels.map(label => label.value))];
328+
this.labelValuesByKey[labelKey] = uniqueValues || [];
329+
return this.labelValuesByKey[labelKey];
330+
} else {
331+
const response = await fetch(query);
332+
const data = await response.json();
333+
// Extract unique values from the labels array
334+
const uniqueValues = [...new Set(data.labels.map(label => label.value))];
335+
this.labelValuesByKey[labelKey] = uniqueValues || [];
336+
return this.labelValuesByKey[labelKey];
337+
}
338+
} catch (error) {
339+
console.error('Error fetching label values:', error);
340+
return [];
341+
}
342+
},
343+
231344
resetRestrictedSCM() {
232345
const queryParams = { ...router.currentRoute.query }
233346
delete queryParams.scmid
@@ -263,6 +376,24 @@ export default {
263376
endTime: this.formattedEndTime,
264377
}
265378
379+
// Build labels as map[string]string as expected by the API.
380+
const labels = {};
381+
const seen = new Set();
382+
for (const label of this.selectedLabels) {
383+
if (label.key && label.value) {
384+
const pairKey = `${label.key}::${label.value}`;
385+
if (seen.has(pairKey)) {
386+
continue;
387+
}
388+
389+
seen.add(pairKey);
390+
labels[label.key] = label.value;
391+
}
392+
}
393+
if (Object.keys(labels).length > 0) {
394+
newFilter.labels = labels;
395+
}
396+
266397
this.$emit('update-filter', newFilter)
267398
},
268399
@@ -354,6 +485,65 @@ export default {
354485
355486
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
356487
},
488+
489+
getLabelValuesForIndex(index) {
490+
const labelKey = this.selectedLabels[index].key;
491+
const values = this.labelValuesByKey[labelKey] || [];
492+
493+
// Filter out values that are already selected with this key in other rows
494+
return values.filter(value => {
495+
return !this.selectedLabels.some((label, i) => {
496+
if (i === index) return false; // Don't compare with itself
497+
return label.key === labelKey && label.value === value;
498+
});
499+
});
500+
},
501+
502+
async onLabelKeyChange(index) {
503+
const labelKey = this.selectedLabels[index].key;
504+
if (labelKey) {
505+
await this.getLabelValues(labelKey);
506+
}
507+
// Clear value when key changes
508+
this.selectedLabels[index].value = null;
509+
},
510+
511+
onLabelValueChange(index) {
512+
const label = this.selectedLabels[index];
513+
if (!label.key || !label.value) {
514+
return;
515+
}
516+
517+
const isDuplicatePair = this.selectedLabels.some((selectedLabel, selectedIndex) => {
518+
if (selectedIndex === index) {
519+
return false;
520+
}
521+
522+
return selectedLabel.key === label.key && selectedLabel.value === label.value;
523+
});
524+
525+
if (isDuplicatePair) {
526+
this.selectedLabels[index].value = null;
527+
}
528+
},
529+
530+
canAddNewLabelRow() {
531+
// Don't allow adding a new row if the last row is incomplete
532+
const lastLabel = this.selectedLabels[this.selectedLabels.length - 1];
533+
return lastLabel.key && lastLabel.value;
534+
},
535+
536+
addLabelRow() {
537+
if (this.canAddNewLabelRow()) {
538+
this.selectedLabels.push({ key: null, value: null });
539+
}
540+
},
541+
542+
removeLabelRow(index) {
543+
if (this.selectedLabels.length > 1) {
544+
this.selectedLabels.splice(index, 1);
545+
}
546+
},
357547
},
358548
359549
watch: {
@@ -378,6 +568,13 @@ export default {
378568
const startTime = this.stepToISO(val[0])
379569
const endTime = this.stepToISO(val[1])
380570
this.$emit('date-range-changed', { startTime, endTime })
571+
572+
// Debounce label refresh to avoid an API call on every slider tick
573+
clearTimeout(this.debounceTimer)
574+
this.debounceTimer = setTimeout(() => {
575+
this.labelValuesByKey = {}
576+
this.getLabelKeys()
577+
}, 500)
381578
},
382579
repository (val) {
383580
var newRepositoryBranches = []
@@ -412,6 +609,8 @@ export default {
412609
} else {
413610
this.getSCMSData()
414611
}
612+
// Load label keys on component creation
613+
await this.getLabelKeys();
415614
} catch (error) {
416615
console.log(error);
417616
}

0 commit comments

Comments
 (0)