1+ <template lang="pug">
2+ div
3+ b-row
4+ b-col( cols ="12" , md ="6" ) .mb-2
5+ b-form-group( label ="Watcher (bucket)" )
6+ b-form-select(
7+ v-model ="selectedBucketId" ,
8+ :options ="bucketOptions" ,
9+ :disabled ="bucketOptions.length === 0 || loading"
10+ )
11+ b-col( cols ="12" , md ="6" ) .mb-2
12+ b-form-group
13+ template( #label )
14+ span Field in event data
15+ span.info-icon (
16+ title ="Field names come from event data. Dot notation is supported (e.g., data.title)."
17+ ) i
18+ b-form-select(
19+ v-model ="selectedField" ,
20+ :options ="fieldSelectOptions" ,
21+ :disabled ="!selectedBucketId || loading"
22+ )
23+ b-form-input.mt-2 (
24+ v-if ="selectedField === '__custom' || fieldOptions.length === 0" ,
25+ v-model ="customField" ,
26+ placeholder ="e.g. data.title" ,
27+ :disabled ="loading"
28+ )
29+ b-alert.mt-2 ( v-if ="error" , show , variant ="danger" ) {{ error }}
30+ b-alert.mt-2 ( v-else-if ="!selectedBucketId" show variant ="info" )
31+ | Select a watcher to load events for this period.
32+ b-alert.mt-2 ( v-else-if ="!loading && aggregated.length === 0" show variant ="warning" )
33+ | No events found for this watcher and time range.
34+
35+ div.mt-2
36+ div.text-center.py-4 ( v-if ="loading" )
37+ b-spinner( small type ="grow" label ="Loading" )
38+ span.ml-2 Loading events...
39+ aw-summary(
40+ v-else-if ="aggregated.length" ,
41+ :fields ="aggregated" ,
42+ :namefunc ="namefunc" ,
43+ :hoverfunc ="hoverfunc" ,
44+ :colorfunc ="colorfunc" ,
45+ with_limit
46+ )
47+ div.text-muted.text-center.py-4 ( v-else )
48+ | Pick a field to see results.
49+ </template >
50+
51+ <script lang="ts">
52+ import _ from ' lodash' ;
53+ import moment from ' moment' ;
54+ import { useActivityStore } from ' ~/stores/activity' ;
55+ import { useBucketsStore } from ' ~/stores/buckets' ;
56+ import { getClient } from ' ~/util/awclient' ;
57+ interface AggregatedEvent {
58+ duration: number ;
59+ data: Record <string , any >;
60+ }
61+ function formatValue(value : unknown ): string {
62+ if (Array .isArray (value )) return value .join (' > ' );
63+ if (value === null || value === undefined ) return ' Unknown' ;
64+ if (typeof value === ' object' ) return JSON .stringify (value );
65+ return String (value );
66+ }
67+ export default {
68+ name: ' aw-watcher-columns' ,
69+ props: {
70+ initialBucketId: { type: String , default: ' ' },
71+ initialField: { type: String , default: ' ' },
72+ initialCustomField: { type: String , default: ' ' },
73+ },
74+ data() {
75+ return {
76+ bucketsStore: useBucketsStore (),
77+ activityStore: useActivityStore (),
78+ selectedBucketId: this .initialBucketId || ' ' ,
79+ selectedField: this .initialField || ' ' ,
80+ customField: this .initialCustomField || ' ' ,
81+ fieldOptions: [] as string [],
82+ events: [] as any [],
83+ aggregated: [] as AggregatedEvent [],
84+ loading: false ,
85+ error: ' ' ,
86+ };
87+ },
88+ computed: {
89+ bucketOptions(): { value: string ; text: string }[] {
90+ return this .bucketsStore .buckets .map (b => ({
91+ value: b .id ,
92+ text: ` ${b .id } (${b .type || ' unknown' }) ` ,
93+ }));
94+ },
95+ fieldSelectOptions(): { value: string ; text: string }[] {
96+ const options = this .fieldOptions .map (f => ({ value: f , text: f }));
97+ options .push ({ value: ' __custom' , text: ' Custom field…' });
98+ return options ;
99+ },
100+ selectedFieldValue(): string {
101+ if (this .selectedField === ' __custom' ) return this .customField ;
102+ return this .selectedField ;
103+ },
104+ timeRange(): { start: string ; end: string } | null {
105+ const opts = this .activityStore .query_options ;
106+ if (! opts || ! opts .timeperiod ) return null ;
107+ const start = opts .timeperiod .start ;
108+ const end = moment (start )
109+ .add (... opts .timeperiod .length )
110+ .toISOString ();
111+ return { start , end };
112+ },
113+ namefunc(): (e : AggregatedEvent ) => string {
114+ return e => e .data .display ;
115+ },
116+ hoverfunc(): (e : AggregatedEvent ) => string {
117+ return e => e .data .display ;
118+ },
119+ colorfunc(): (e : AggregatedEvent ) => string {
120+ return e => e .data .colorKey ;
121+ },
122+ },
123+ watch: {
124+ selectedBucketId : function () {
125+ this .emitSelection ();
126+ this .loadEvents ();
127+ },
128+ selectedField : function () {
129+ this .emitSelection ();
130+ this .aggregate ();
131+ },
132+ customField : function () {
133+ if (this .selectedField === ' __custom' ) {
134+ this .emitSelection ();
135+ this .aggregate ();
136+ }
137+ },
138+ timeRange: {
139+ handler() {
140+ this .loadEvents ();
141+ },
142+ deep: true ,
143+ },
144+ },
145+ async mounted() {
146+ await this .bucketsStore .ensureLoaded ();
147+ this .setDefaultBucket ();
148+ this .emitSelection ();
149+ if (this .selectedBucketId ) {
150+ await this .loadEvents ();
151+ }
152+ },
153+ methods: {
154+ setDefaultBucket() {
155+ if (this .selectedBucketId ) return ;
156+ const host = this .activityStore .query_options && this .activityStore .query_options .host ;
157+ const byHost = host ? this .bucketsStore .buckets .filter (b => b .hostname === host ) : [];
158+ if (byHost .length > 0 ) {
159+ this .selectedBucketId = byHost [0 ].id ;
160+ } else if (this .bucketsStore .buckets .length > 0 ) {
161+ this .selectedBucketId = this .bucketsStore .buckets [0 ].id ;
162+ }
163+ },
164+ async loadEvents() {
165+ if (! this .selectedBucketId || ! this .timeRange ) return ;
166+ this .loading = true ;
167+ this .error = ' ' ;
168+ this .aggregated = [];
169+ try {
170+ this .events = await getClient ().getEvents (this .selectedBucketId , {
171+ start: this .timeRange .start ,
172+ end: this .timeRange .end ,
173+ limit: - 1 ,
174+ });
175+ this .fieldOptions = this .extractFields (this .events );
176+ if (! this .selectedField && this .fieldOptions .length > 0 ) {
177+ this .selectedField = this .fieldOptions [0 ];
178+ } else if (! this .selectedField && this .fieldOptions .length === 0 ) {
179+ this .selectedField = ' __custom' ;
180+ }
181+ this .aggregate ();
182+ } catch (err ) {
183+ console .error (err );
184+ this .events = [];
185+ this .fieldOptions = [];
186+ this .error = err ?.message || ' Failed to load events for the selected watcher.' ;
187+ } finally {
188+ this .loading = false ;
189+ }
190+ },
191+ extractFields(events : any []): string [] {
192+ const keys = new Set <string >();
193+ events .slice (0 , 50 ).forEach (e => {
194+ Object .keys (e .data || {}).forEach (k => keys .add (k ));
195+ });
196+ return Array .from (keys ).sort ();
197+ },
198+ aggregate() {
199+ if (! this .selectedFieldValue ) {
200+ this .aggregated = [];
201+ return ;
202+ }
203+ const path = this .selectedFieldValue .startsWith (' data.' )
204+ ? this .selectedFieldValue
205+ : ` data.${this .selectedFieldValue } ` ;
206+ const grouped = new Map <string , AggregatedEvent >();
207+ this .events .forEach (e => {
208+ const value = _ .get (e , path );
209+ const display = formatValue (value );
210+ const key = Array .isArray (value ) ? JSON .stringify (value ) : String (display );
211+ if (! grouped .has (key )) {
212+ grouped .set (key , {
213+ duration: 0 ,
214+ data: { display , raw: value , colorKey: display },
215+ });
216+ }
217+ const entry = grouped .get (key );
218+ entry .duration += e .duration || 0 ;
219+ });
220+ this .aggregated = Array .from (grouped .values ()).sort ((a , b ) => b .duration - a .duration );
221+ },
222+ emitSelection() {
223+ this .$emit (' update-props' , {
224+ bucketId: this .selectedBucketId ,
225+ field: this .selectedField === ' __custom' ? ' __custom' : this .selectedField ,
226+ customField: this .selectedField === ' __custom' ? this .customField : ' ' ,
227+ });
228+ },
229+ },
230+ };
231+ </script >
232+
233+ <style scoped>
234+ .info-icon {
235+ display : inline-flex ;
236+ align-items : center ;
237+ justify-content : center ;
238+ margin-left : 0.25rem ;
239+ width : 1.15em ;
240+ height : 1.15em ;
241+ border : 1px solid #888 ;
242+ border-radius : 50% ;
243+ font-size : 0.75em ;
244+ color : #555 ;
245+ cursor : help ;
246+ }
247+ </style >
0 commit comments