3838 td
3939 b-form-checkbox( v-model ="filter_afk" size ="sm" switch )
4040 | Filter AFK
41+ tr
42+ th.pt-2.pr-3
43+ label Categories:
44+ td
45+ select( @change ="onCategorySelect($event)" , :value ="''" )
46+ option( value ="" disabled ) {{ filter_categories.length > 0 ? 'Add category...' : 'All' }}
47+ option( v-for ="cat in category_options" , :key ="cat.text" , :value ="cat.text" ) {{ cat.text }}
48+ div.mt-1 ( v-if ="filter_categories.length > 0" )
49+ span.badge.badge-info.mr-1 ( v-for ="(cat, idx) in filter_categories" , :key ="idx" )
50+ | {{ cat.join(' > ') }}
51+ button.ml-1.close.small ( @click ="removeCategory(idx)" , type ="button" , style ="font-size: 0.8rem" ) ×
4152 div.d-inline-block.border.rounded.p-2.mr-2 ( v-if ="num_events !== 0" )
4253 | Events shown: {{ num_events }}
4354 b-alert.d-inline-block.p-2.mb-0.mt-2 ( v-if ="num_events === 0" , variant ="warning" , show )
@@ -80,6 +91,9 @@ import { useSettingsStore } from '~/stores/settings';
8091import { useBucketsStore } from ' ~/stores/buckets' ;
8192import { getClient } from ' ~/util/awclient' ;
8293import { canonicalEvents } from ' ~/queries' ;
94+ import { useCategoryStore } from ' ~/stores/categories' ;
95+ import { matchString } from ' ~/util/classes' ;
96+ import { getCategorizationStringFromEvent } from ' ~/util/color' ;
8397import { seconds_to_duration } from ' ~/util/time' ;
8498
8599export default {
@@ -96,6 +110,7 @@ export default {
96110 filter_client: null ,
97111 filter_duration: null ,
98112 filter_afk: false ,
113+ filter_categories: [],
99114 swimlane: null ,
100115 updateTimelineWindow: true ,
101116 };
@@ -110,6 +125,10 @@ export default {
110125 num_events() {
111126 return _ .sumBy (this .buckets , ' events.length' );
112127 },
128+ category_options() {
129+ const categoryStore = useCategoryStore ();
130+ return categoryStore .allCategoriesSelect ;
131+ },
113132 filter_summary() {
114133 const desc = [];
115134 if (this .filter_hostname ) {
@@ -124,6 +143,13 @@ export default {
124143 if (this .filter_afk ) {
125144 desc .push (' AFK filtered' );
126145 }
146+ if (this .filter_categories .length > 0 ) {
147+ desc .push (
148+ this .filter_categories .length +
149+ ' categor' +
150+ (this .filter_categories .length === 1 ? ' y' : ' ies' )
151+ );
152+ }
127153
128154 if (desc .length > 0 ) {
129155 return desc .join (' , ' );
@@ -152,12 +178,28 @@ export default {
152178 this .updateTimelineWindow = false ;
153179 this .getBuckets ();
154180 },
181+ filter_categories() {
182+ this .updateTimelineWindow = false ;
183+ this .getBuckets ();
184+ },
155185 swimlane() {
156186 this .updateTimelineWindow = false ;
157187 this .getBuckets ();
158188 },
159189 },
160190 methods: {
191+ onCategorySelect(event ) {
192+ const text = event .target .value ;
193+ if (! text ) return ;
194+ const cat = this .category_options .find (c => c .text === text );
195+ if (cat && ! this .filter_categories .some (fc => _ .isEqual (fc , cat .value ))) {
196+ this .filter_categories = [... this .filter_categories , cat .value ];
197+ }
198+ event .target .value = ' ' ;
199+ },
200+ removeCategory(idx ) {
201+ this .filter_categories = this .filter_categories .filter ((_cat , i ) => i !== idx );
202+ },
161203 getBuckets : async function () {
162204 if (this .daterange == null ) return ;
163205
@@ -189,6 +231,26 @@ export default {
189231 }
190232 }
191233
234+ if (this .filter_categories .length > 0 ) {
235+ const categoryStore = useCategoryStore ();
236+ const allCats = categoryStore .classes ;
237+ for (const bucket of buckets ) {
238+ // Skip AFK buckets — they don't have meaningful categorization
239+ if (bucket .type === ' afkstatus' ) continue ;
240+ bucket .events = _ .filter (bucket .events , e => {
241+ const str = getCategorizationStringFromEvent (bucket , e );
242+ if (str === null ) return true ; // Keep events from unknown bucket types
243+ const matched = matchString (str , allCats );
244+ const eventCat = matched ? matched .name : [' Uncategorized' ];
245+ // Check if the event's category matches any selected filter category
246+ // (including parent matches: selecting "Work" also shows "Work > Programming")
247+ return this .filter_categories .some (filterCat =>
248+ _ .isEqual (eventCat .slice (0 , filterCat .length ), filterCat )
249+ );
250+ });
251+ }
252+ }
253+
192254 // AFK filtering: use query engine to filter window events by AFK status
193255 if (this .filter_afk ) {
194256 buckets = await this ._applyAfkFilter (buckets );
0 commit comments