• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

michaelmbugua-me / DataAnalyticsPlatform / #6

28 Aug 2025 06:06AM UTC coverage: 22.047% (+1.3%) from 20.788%
#6

push

github

michaelmbugua-me
feat: FilterManagerComponent

- Add a dialog before deleting a configuration.
- Also add a toastr notification after user action.

74 of 616 branches covered (12.01%)

Branch coverage included in aggregate %.

331 of 1221 relevant lines covered (27.11%)

2.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

13.61
/src/app/features/DataExplorerModule/RawEvents/RawEventsComponent.ts
1
import {Component, inject, OnInit, signal, computed, Signal, ChangeDetectionStrategy} from '@angular/core';
1✔
2
import {AgGridAngular} from 'ag-grid-angular';
1✔
3
import {ColDef, GridOptions, GridReadyEvent, ValueFormatterParams, CellClassParams} from 'ag-grid-community';
4
import {Button, ButtonDirective, ButtonIcon, ButtonLabel} from 'primeng/button';
1✔
5
import {Select} from 'primeng/select';
1✔
6
import {FormsModule} from '@angular/forms';
1✔
7
import {DataService} from '../../../core/services/DataService';
1✔
8
import {FilterDrawerComponent} from '../../shared/components/filter-drawer';
1✔
9
import {
10
  RawEvent,
11
  AnalyticsEvent,
12
  PerformanceEvent,
13
  CrashEvent,
14
  EventSource,
15
  Platform,
16
  Country,
17
  ReleaseChannel
18
} from '../../../core/models/DataModels';
19
import { getSourceCellStyle, getReleaseChannelStyle, getDurationCellStyle } from '../../shared/utils/gridCellStyles';
1✔
20
import { AllCommunityModule, ModuleRegistry } from 'ag-grid-community';
1✔
21
import {DataExportationService} from '../../../core/services/DataExportationService';
1✔
22
import { FiltersService } from '../../../core/services/FiltersService';
1✔
23
import { evaluateQuery } from '../../../core/utils/query';
1✔
24

25
// Register AG Grid modules lazily for this feature chunk
26
ModuleRegistry.registerModules([AllCommunityModule]);
1✔
27

28
@Component({
29
  selector: 'app-raw-events-component',
30
  templateUrl: './RawEventsComponent.html',
31
  imports: [
32
    AgGridAngular,
33
    ButtonDirective,
34
    ButtonIcon,
35
    ButtonLabel,
36
    FormsModule,
37
    Button,
38
    FilterDrawerComponent,
39
    Select
40
  ],
41
  providers: [],
42
  styleUrls: ['./RawEventsComponent.scss'],
43
  standalone: true,
44
  changeDetection: ChangeDetectionStrategy.OnPush,
45
})
46
export class RawEventsComponent implements OnInit {
1✔
47

48
  filtersService = inject(FiltersService);
1✔
49

50
  // Facet selection signals for easier user interaction
51
  selectedSource = signal<string | null>(null);
1✔
52
  selectedPlatform = signal<string | null>(null);
1✔
53
  selectedCountry = signal<string | null>(null);
1✔
54
  selectedReleaseChannel = signal<string | null>(null);
1✔
55

56
  // Options for dropdowns derived from current data
57
  sourceOptions = computed(() => uniqSorted(this.data().map(r => r.source).filter(Boolean)));
1✔
58
  platformOptions = computed(() => uniqSorted(this.data().map(r => r.platform).filter(Boolean)));
1✔
59
  countryOptions = computed(() => uniqSorted(this.data().map(r => r.country).filter(Boolean)));
1✔
60
  releaseChannelOptions = computed(() => uniqSorted(this.data().map(r => r.release_channel).filter(Boolean)));
1✔
61

62
  // Saved config names
63
  savedConfigNames = computed(() => (this.filtersService.savedConfigs() || []).map(c => c.name));
1!
64

65
  // Derived filtered data based on search + custom query + facet dropdowns
66
  public viewData: Signal<RawEvent[]> = computed(() => {
1✔
67
    const base = this.data();
×
68
    const search = (this.filtersService.searchText() || '').toLowerCase();
×
69
    const query = this.filtersService.customQuery() || '';
×
70

71
    const src = this.selectedSource();
×
72
    const plat = this.selectedPlatform();
×
73
    const ctry = this.selectedCountry();
×
74
    const rel = this.selectedReleaseChannel();
×
75

76
    const bySearch = (row: RawEvent) => {
×
77
      if (!search) return true;
×
78
      // simple: stringify limited fields for performance
79
      const hay = [row.id, row.event_name, row.platform, row.country, row.app_id, row.source, row.release_channel]
×
80
        .map(v => String(v ?? '')).join(' ').toLowerCase();
×
81
      return hay.includes(search);
×
82
    };
83

84
    const byQuery = (row: RawEvent) => {
×
85
      if (!query.trim()) return true;
×
86
      try { return evaluateQuery(row as any, query); } catch { return false; }
×
87
    };
88

89
    const byFacets = (row: RawEvent) =>
×
90
      (!src || row.source === src) &&
×
91
      (!plat || row.platform === plat) &&
92
      (!ctry || row.country === ctry) &&
93
      (!rel || row.release_channel === rel);
94

95
    return base.filter(r => bySearch(r) && byQuery(r) && byFacets(r));
×
96
  });
97

98
  private dataService = inject(DataService);
1✔
99
  private dataExportationService = inject(DataExportationService);
×
100

101
  // Type-safe data with proper typing
102
  public data: Signal<RawEvent[]> = this.dataService.filteredRawData;
×
103
  error: Signal<string | null> = this.dataService.error;
×
104

105
  filters!: Filter[];
106
  visible = signal(false);
×
107

108
  // Computed signals for different event types (optional - for additional functionality)
109
  analyticsEvents = computed(() =>
×
110
    this.data()?.filter((event): event is AnalyticsEvent => event.source === 'analytics') ?? []
×
111
  );
112

113
  performanceEvents = computed(() =>
×
114
    this.data()?.filter((event): event is PerformanceEvent => event.source === 'performance') ?? []
×
115
  );
116

117
  crashEvents = computed(() =>
×
118
    this.data()?.filter((event): event is CrashEvent => event.source === 'crash') ?? []
×
119
  );
120

121
  // Enhanced column definitions with type-aware formatters
122
  columnDefs: ColDef<RawEvent>[] = [
×
123
    {
124
      field: 'id',
125
      headerName: 'ID',
126
      filter: 'agTextColumnFilter',
127
      minWidth: 200
128
    },
129
    {
130
      field: 'timestamp',
131
      headerName: 'Timestamp',
132
      filter: 'agDateColumnFilter',
133
      minWidth: 180,
134
      valueFormatter: (params: any) =>
135
        new Date(params.value).toLocaleString()
×
136
    },
137
    {
138
      field: 'day',
139
      headerName: 'Day',
140
      filter: 'agDateColumnFilter',
141
      minWidth: 120
142
    },
143
    {
144
      field: 'hour',
145
      headerName: 'Hour',
146
      filter: 'agNumberColumnFilter',
147
      minWidth: 80
148
    },
149
    {
150
      field: 'source',
151
      headerName: 'Source',
152
      filter: 'agTextColumnFilter',
153
      minWidth: 160,
154
      cellStyle: (params: CellClassParams) => getSourceCellStyle(params.value as EventSource)
×
155
    },
156
    {
157
      field: 'event_name',
158
      headerName: 'Event Name',
159
      filter: 'agTextColumnFilter',
160
      minWidth: 150
161
    },
162
    {
163
      field: 'platform',
164
      headerName: 'Platform',
165
      filter: 'agTextColumnFilter',
166
      minWidth: 100,
167
      valueFormatter: (params: any) => this.getPlatformIcon(params.value)
×
168
    },
169
    {
170
      field: 'app_id',
171
      headerName: 'App ID',
172
      filter: 'agTextColumnFilter',
173
      minWidth: 140
174
    },
175
    {
176
      field: 'country',
177
      headerName: 'Country',
178
      filter: 'agTextColumnFilter',
179
      minWidth: 100
180
    },
181
    {
182
      field: 'release_channel',
183
      headerName: 'Release',
184
      filter: 'agTextColumnFilter',
185
      minWidth: 100,
186
      cellStyle: (params: CellClassParams) => getReleaseChannelStyle(params.value as ReleaseChannel)
×
187
    },
188
    {
189
      field: 'session_id',
190
      headerName: 'Session ID',
191
      filter: 'agTextColumnFilter',
192
      minWidth: 160
193
    },
194
    {
195
      field: 'user_pseudo_id',
196
      headerName: 'User ID',
197
      filter: 'agTextColumnFilter',
198
      minWidth: 160
199
    },
200
    // Conditional columns based on event type
201
    {
202
      field: 'analytics_event',
203
      headerName: 'Analytics Event',
204
      filter: 'agTextColumnFilter',
205
      minWidth: 150,
206
      hide: false // Show/hide based on filters
207
    },
208
    {
209
      field: 'duration_ms',
210
      headerName: 'Duration (ms)',
211
      filter: 'agNumberColumnFilter',
212
      minWidth: 160,
213
      valueFormatter: (params: ValueFormatterParams<RawEvent, number | undefined>) =>
214
        params.value ? `${params.value}ms` : '-',
×
215
      cellStyle: (params: CellClassParams) => getDurationCellStyle(params.value as number)
×
216
    },
217
    {
218
      field: 'status_code',
219
      headerName: 'Status',
220
      filter: 'agNumberColumnFilter',
221
      minWidth: 100,
222
      cellStyle: (params: CellClassParams) => this.getStatusCodeStyle(params.value as number)
×
223
    },
224
    {
225
      field: 'crash_type',
226
      headerName: 'Crash Type',
227
      filter: 'agTextColumnFilter',
228
      minWidth: 160,
229
      cellStyle: { color: '#dc3545', fontWeight: 'bold' }
230
    }
231
  ];
232

233
  public defaultColDef: ColDef<RawEvent> = {
×
234
    sortable: true,
235
    resizable: true,
236
    filter: true
237
  };
238

239
  public gridOptions: GridOptions<RawEvent> = {
×
240
    rowModelType: 'clientSide',
241
    pagination: true,
242
    paginationPageSize: 100,
243
    enableCellTextSelection: true,
244
    ensureDomOrder: true,
245
    animateRows: true,
246
    suppressMenuHide: true,
247
    domLayout: 'normal'
248
  };
249

250
  async ngOnInit() {
251
    // restore selected config application on init if needed
252
    const sel = this.filtersService.selectedConfigName();
×
253
    if (sel) this.applySelectedConfig(sel);
×
254
    this.filters = [
×
255
      {name: 'Today\'s records', code: 'TODAY'},
256
      {name: 'This week\'s records', code: 'WEEK'},
257
      {name: 'This month\'s records', code: 'MONTH'},
258
      {name: 'This year\'s records', code: 'YEAR'}
259
    ];
260
  }
261

262
  onGridReady(params: GridReadyEvent<RawEvent>) {
263
    console.log('Grid is ready');
×
264
    params.api.sizeColumnsToFit();
×
265

266
    // Apply initial column visibility based on data
267
    this.updateColumnVisibility(params);
×
268
  }
269

270
  toggleFilterVisibility() {
271
    this.visible.update(v => !v);
×
272
  }
273

274
  // Type-safe helper methods using the model types
275
  private getPlatformIcon(platform: Platform): string {
276
    const icons = {
×
277
      ios: 'iOS',
278
      android: 'Android',
279
      web: 'Web'
280
    };
281
    return icons[platform] || platform;
×
282
  }
283

284
  private getStatusCodeStyle(statusCode: number): Record<string, string> {
285
    if (!statusCode) return {};
×
286

287
    if (statusCode >= 500) return { backgroundColor: '#d32f2f', color: 'white' };
×
288
    if (statusCode >= 400) return { backgroundColor: '#ff9800', color: 'white' };
×
289
    if (statusCode >= 200 && statusCode < 300) return { backgroundColor: '#4caf50', color: 'white' };
×
290
    return {};
×
291
  }
292

293
  private updateColumnVisibility(params: GridReadyEvent<RawEvent>) {
294
    const data = this.data();
×
295
    if (!data || data.length === 0) return;
×
296

297
    const hasAnalytics = data.some(event => event.source === 'analytics');
×
298
    const hasPerformance = data.some(event => event.source === 'performance');
×
299
    const hasCrashes = data.some(event => event.source === 'crash');
×
300

301
    params.api.applyColumnState({
×
302
      state: [
303
        { colId: 'analytics_event', hide: !hasAnalytics },
304
        { colId: 'duration_ms', hide: !hasPerformance },
305
        { colId: 'status_code', hide: !hasPerformance },
306
        { colId: 'crash_type', hide: !hasCrashes }
307
      ]
308
    });
309
  }
310

311
  exportRecords() {
312

313

314
    let cols: string[] = this.columnDefs.map((item: any) => {
×
315
      if(item['headerName'].toLowerCase() !== 'actions'){
×
316
        return item['field'].toUpperCase()
×
317
      } else {
318
        return ''
×
319
      }
320
    })
321

322
    cols = cols.filter(item => item !== '')
×
323

324
    let rowKeys: string[] = Object.keys(this.data()[0]);
×
325
    let arr: string[][]= []
×
326

327
    this.data().forEach((row: any) => {
×
328
      let temp: string[] = []
×
329
      cols.forEach(colKey => {
×
330
        rowKeys.forEach(key => {
×
331
          if(colKey == key.toUpperCase()){
×
332
            temp.push(row[key])
×
333
          }
334
        })
335
      })
336
      arr.push(temp)
×
337
    })
338

339
    this.dataExportationService.exportToPdf(cols, arr, 'raw_events.pdf')
×
340
  }
341

342
  // Type-safe filter methods
343
  filterBySource(source: EventSource) {
344
    // Implementation for filtering by event source
345
    console.log(`Filtering by source: ${source}`);
×
346
  }
347

348
  filterByPlatform(platform: Platform) {
349
    // Implementation for filtering by platform
350
    console.log(`Filtering by platform: ${platform}`);
×
351
  }
352

353
  filterByCountry(country: Country) {
354
    // Implementation for filtering by country
355
    console.log(`Filtering by country: ${country}`);
×
356
  }
357

358
  // Export functionality with proper typing
359
  exportAnalyticsEvents() {
360
    const analyticsData = this.analyticsEvents();
×
361
    console.log('Exporting analytics events:', analyticsData.length);
×
362
    // Implementation for export
363
  }
364

365
  exportPerformanceEvents() {
366
    const performanceData = this.performanceEvents();
×
367
    console.log('Exporting performance events:', performanceData.length);
×
368
    // Implementation for export
369
  }
370

371
  exportCrashEvents() {
372
    const crashData = this.crashEvents();
×
373
    console.log('Exporting crash events:', crashData.length);
×
374
    // Implementation for export
375
  }
376

377
  saveCurrentConfig() {
378
    const name = prompt('Save current filters as (name):');
×
379
    if (!name) return;
×
380
    this.filtersService.saveConfig(name, this.dataService.dateRange());
×
381
  }
382

383
  applySelectedConfig(name: string) {
384
    const cfg = this.filtersService.loadConfig(name);
×
385
    if (cfg?.dateRange) {
×
386
      try {
×
387
        const from = new Date(cfg.dateRange.from);
×
388
        const to = new Date(cfg.dateRange.to);
×
389
        if (!isNaN(from.getTime()) && !isNaN(to.getTime())) this.dataService.setDateRange({ from, to });
×
390
      } catch {}
391
    }
392
  }
393

394
  clearAllFilters() {
395
    this.filtersService.searchText.set('');
×
396
    this.filtersService.customQuery.set('');
×
397
    this.selectedSource.set(null);
×
398
    this.selectedPlatform.set(null);
×
399
    this.selectedCountry.set(null);
×
400
    this.selectedReleaseChannel.set(null);
×
401
    this.filtersService.selectedConfigName.set(null);
×
402
  }
403

404
  resetDateRangeToThisMonth() {
405
    this.dataService.resetToThisMonth();
×
406
  }
407
}
408

409
function uniqSorted(arr: (string | null | undefined)[]): string[] {
410
  const set = new Set<string>();
×
411
  for (const v of arr) { if (v != null) set.add(String(v)); }
×
412
  return Array.from(set).sort((a,b) => a.localeCompare(b));
×
413
}
414

415
interface Filter {
416
  name: string;
417
  code: string;
418
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc