Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<th mat-header-cell>
<div class="header-filter-container">
<span
*ngIf="hasSorting"
mat-sort-header
[sortActionDescription]="sortActionDescription"
>{{ headerText }}</span
>
<span *ngIf="!hasSorting">{{ headerText }}</span>
<ng-container
*ngTemplateOutlet="
filterButton;
context: {
name: filterName,
filterOpened: filterOpened,
activeFilter: activeFilter,
}
">
</ng-container>
</div>
</th>

<ng-template
#filterButton
let-name="name"
let-filterOpened="filterOpened"
let-activeFilter="activeFilter">
<button
(click)="openFilter($event, name, filterOpened)"
(keydown.enter)="openFilter($event, name, filterOpened)"
(keydown.space)="openFilter($event, name, filterOpened)"
class="filter-button filter-button-{{ name }}"
[ngClass]="filterOpened && activeFilter === name ? 'active' : ''"
attr.aria-label="filter by {{ name }}">
<mat-icon fontSet="material-icons-outlined" class="filter-icon"
>filter_list</mat-icon
>
</button>
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
@use 'node_modules/@angular/material/index' as mat;
@use 'src/theming/m3-theme' as *;
@use 'colors';

:host {
display: contents;
}

th {
height: var(--mat-table-header-container-height);
vertical-align: middle;
}

.header-filter-container {
display: flex;
align-items: center;
}

.filter-button {
display: flex;
width: 32px;
height: 32px;
justify-content: center;
align-items: center;
flex-shrink: 0;
margin: 0 2px 0 38px;
padding: 0;
border: none;
background: colors.$white;
cursor: pointer;
border-radius: 50%;
&:hover {
background-color: rgba(0, 0, 0, 0.04);
}
&:active,
&.active {
background-color: colors.$secondary-container;
color: colors.$on-secondary-container;
&:hover {
filter: brightness(90%);
}
}
}

.filter-button.active .mat-icon {
color: mat.get-theme-color($light-theme, primary, 35);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FilterHeaderComponent } from './filter-header.component';
import { MatIconModule } from '@angular/material/icon';
import { MatSortModule } from '@angular/material/sort';
import { MatButtonModule } from '@angular/material/button';
import { MatTableModule } from '@angular/material/table';
import { CommonModule } from '@angular/common';
import { By } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@Component({
selector: 'app-dummy-table',
template: `
<table mat-table matSort [dataSource]="data">
<ng-container matColumnDef="testColumn">
<app-filter-header
*matHeaderCellDef
[filterName]="'testFilter'"
[filterOpened]="false"
[activeFilter]="'testFilter'"
[headerText]="'Test Header'"
[hasSorting]="true"
[sortActionDescription]="'Sort by test'">
</app-filter-header>
<td mat-cell *matCellDef="let element">{{ element }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
`,
standalone: false,
})
export class DummyTableComponent {
data = ['Row 1', 'Row 2', 'Row 3'];
displayedColumns = ['testColumn'];
}

describe('FilterHeaderComponent within mat-table', () => {
let fixture: ComponentFixture<DummyTableComponent>;
let compiled: HTMLElement;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DummyTableComponent],
imports: [
BrowserAnimationsModule,
FilterHeaderComponent,
MatIconModule,
MatSortModule,
MatButtonModule,
MatTableModule,
CommonModule,
],
}).compileComponents();

fixture = TestBed.createComponent(DummyTableComponent);
fixture.detectChanges();
compiled = fixture.nativeElement as HTMLElement;
});

it('should have the filter header component', () => {
const filterHeader = compiled.querySelector(
'app-filter-header'
) as HTMLElement;
expect(filterHeader).toBeTruthy();
const headerText = filterHeader?.querySelector(
'.header-filter-container span'
) as HTMLSpanElement;
expect(headerText?.textContent?.trim()).toBe('Test Header');
});

it('should emit an event when filter button is clicked in filter header', () => {
const filterHeader = fixture.debugElement.query(
By.css('app-filter-header')
);
const filterHeaderComponent =
filterHeader.componentInstance as FilterHeaderComponent;

spyOn(filterHeaderComponent.emitOpenFilter, 'emit');

const button = filterHeader.query(By.css('.filter-button'))
.nativeElement as HTMLButtonElement;
button.click();

expect(filterHeaderComponent.emitOpenFilter.emit).toHaveBeenCalledWith({
event: new PointerEvent('event'),
filter: 'testFilter',
filterOpened: false,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { CommonModule, NgIf } from '@angular/common';
import { MatTableModule } from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort';

export interface OpenFilterEvent {
event: Event;
filter: string;
filterOpened: boolean;
}

@Component({
selector: 'app-filter-header',
standalone: true,
imports: [
MatIconModule,
MatButtonModule,
CommonModule,
MatTableModule,
MatSortModule,
NgIf,
],
templateUrl: './filter-header.component.html',
styleUrl: './filter-header.component.scss',
})
export class FilterHeaderComponent {
@Output() emitOpenFilter = new EventEmitter<OpenFilterEvent>();
@Input({ required: true }) filterName!: string;
@Input({ required: true }) filterOpened!: boolean;
@Input() hasSorting: boolean = true;
@Input({ required: true }) activeFilter!: string;
@Input() sortActionDescription: string = '';
@Input({ required: true }) headerText!: string;

openFilter(event: Event, filter: string, filterOpened: boolean): void {
this.emitOpenFilter.emit({
event,
filter,
filterOpened,
});
}
}
116 changes: 32 additions & 84 deletions modules/ui/src/app/pages/reports/reports.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,15 @@ <h2 class="title" tabindex="-1">Reports</h2>
tabindex="0">
<!-- Started Column -->
<ng-container matColumnDef="started">
<th
<app-filter-header
*matHeaderCellDef
mat-header-cell
mat-sort-header
sortActionDescription="Sort by started date">
Started
<ng-container
*ngTemplateOutlet="
filterButton;
context: {
name: FilterName.Started,
filterOpened: vm.filterOpened,
activeFilter: vm.activeFilter,
}
">
</ng-container>
</th>
sortActionDescription="Sort by started date"
headerText="Started"
[filterName]="FilterName.Started"
[filterOpened]="vm.filterOpened"
[activeFilter]="vm.activeFilter"
(emitOpenFilter)="openFilter($event)">
</app-filter-header>
<td *matCellDef="let data" class="text-nowrap" mat-cell>
{{ getFormattedDateString(data.started) }}
</td>
Expand All @@ -70,45 +62,29 @@ <h2 class="title" tabindex="-1">Reports</h2>

<!-- Device Column -->
<ng-container matColumnDef="deviceInfo">
<th
<app-filter-header
*matHeaderCellDef
mat-header-cell
mat-sort-header
sortActionDescription="Sort by device">
Device
<ng-container
*ngTemplateOutlet="
filterButton;
context: {
name: FilterName.DeviceInfo,
filterOpened: vm.filterOpened,
activeFilter: vm.activeFilter,
}
">
</ng-container>
</th>
sortActionDescription="Sort by device"
headerText="Device"
[filterName]="FilterName.DeviceInfo"
[filterOpened]="vm.filterOpened"
[activeFilter]="vm.activeFilter"
(emitOpenFilter)="openFilter($event)">
</app-filter-header>
<td *matCellDef="let data" mat-cell>{{ data.deviceInfo }}</td>
</ng-container>

<!-- Firmware Column -->
<ng-container matColumnDef="deviceFirmware">
<th
<app-filter-header
*matHeaderCellDef
mat-header-cell
mat-sort-header
sortActionDescription="Sort by firmware">
Firmware
<ng-container
*ngTemplateOutlet="
filterButton;
context: {
name: FilterName.DeviceFirmware,
filterOpened: vm.filterOpened,
activeFilter: vm.activeFilter,
}
">
</ng-container>
</th>
sortActionDescription="Sort by firmware"
headerText="Firmware"
[filterName]="FilterName.DeviceFirmware"
[filterOpened]="vm.filterOpened"
[activeFilter]="vm.activeFilter"
(emitOpenFilter)="openFilter($event)">
</app-filter-header>
<td *matCellDef="let data" mat-cell>{{ data.deviceFirmware }}</td>
</ng-container>

Expand All @@ -128,23 +104,15 @@ <h2 class="title" tabindex="-1">Reports</h2>

<!-- Result Column -->
<ng-container matColumnDef="status">
<th
<app-filter-header
*matHeaderCellDef
mat-header-cell
mat-sort-header
sortActionDescription="Sort by result">
Result
<ng-container
*ngTemplateOutlet="
filterButton;
context: {
name: FilterName.Results,
filterOpened: vm.filterOpened,
activeFilter: vm.activeFilter,
}
">
</ng-container>
</th>
sortActionDescription="Sort by result"
headerText="Result"
[filterName]="FilterName.Results"
[filterOpened]="vm.filterOpened"
[activeFilter]="vm.activeFilter"
(emitOpenFilter)="openFilter($event)">
</app-filter-header>
<td *matCellDef="let data" mat-cell>
<span
[ngClass]="getResultClass(data.status)"
Expand Down Expand Up @@ -255,26 +223,6 @@ <h2 class="title" tabindex="-1">Reports</h2>
</div>
</ng-template>

<ng-template
#filterButton
let-name="name"
let-filterOpened="filterOpened"
let-activeFilter="activeFilter">
<button
mat-button
(click)="openFilter($event, name, filterOpened)"
(keydown.enter)="openFilter($event, name, filterOpened)"
(keydown.space)="openFilter($event, name, filterOpened)"
class="filter-button filter-button-{{ name }}"
[ngClass]="filterOpened && activeFilter === name ? 'active' : ''"
attr.aria-label="filter by {{ name }}"
mat-button>
<mat-icon fontSet="material-icons-outlined" class="filter-icon"
>filter_list</mat-icon
>
</button>
</ng-template>

<ng-template #emptyMessage let-header="header" let-message="message">
<div class="results-content-empty-message">
<div class="results-content-empty-message-img"></div>
Expand Down
2 changes: 1 addition & 1 deletion modules/ui/src/app/pages/reports/reports.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
justify-content: center;
align-items: center;
flex-shrink: 0;
margin: 0 2px 0 8px;
margin: 0 2px 0 12px;
padding: 0;
border: none;
background: colors.$white;
Expand Down
Loading