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