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
9 changes: 4 additions & 5 deletions AMW_angular/io/src/app/deployment/deployment-filter.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { ComparatorFilterOption } from './comparator-filter-option';
import { DateTimeModel } from '../shared/date-time-picker/date-time.model';

export type FilterValue = string | number | boolean | DateTimeModel;

export interface DeploymentFilter {
name: string;
comp: string;
val: any;
type: string;
compOptions: ComparatorFilterOption[];
valOptions: string[];
val: FilterValue;
}
7 changes: 7 additions & 0 deletions AMW_angular/io/src/app/deployment/filter-type.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export enum FilterType {
BOOLEAN = 'booleanType',
DATE = 'DateType',
SPECIAL = 'SpecialFilterType',
ENUM = 'ENUM_TYPE',
STRING = 'StringType',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<div class="form-group row">
<div class="col-sm-4 offset-1">
<input class="form-control" disabled type="text" [value]="filter().name" />
</div>
@if (type() !== FilterType.SPECIAL) {
<div class="col-sm-2">
<select
id="selectFilterComp"
class="form-control"
[attr.disabled]="compOptions.length === 1 ? '' : null"
[(ngModel)]="filter().comp"
>
@for (compOption of compOptions(); track compOption) {
<option [ngValue]="compOption.name">
{{ compOption.displayName }}
</option>
}
</select>
</div>
@if (type() !== FilterType.DATE) {
@if (type() !== FilterType.BOOLEAN && type() !== FilterType.ENUM) {
<div class="col-sm-4">
<input class="form-control" [attr.list]="'list' + index()" type="text" [(ngModel)]="filter().val" />
<datalist [attr.id]="'list' + index()">
@for (filterValueOption of valOptions(); track filterValueOption) {
<option>{{ filterValueOption }}</option>
}
</datalist>
</div>
}
@if (type() === FilterType.BOOLEAN || type() === FilterType.ENUM) {
<div class="col-sm-4">
<select class="form-control" [(ngModel)]="filter().val">
@for (filterValueOption of valOptions(); track filterValueOption) {
<option>{{ filterValueOption }}</option>
}
</select>
</div>
}
}
@if (type() === FilterType.DATE) {
<div class="col-sm-4">
<div class="input-group date">
<app-date-time-picker
[(ngModel)]="filter().val"
name="dateDeployment"
id="datetimepicker"
class="w-100"
></app-date-time-picker>
</div>
</div>
}
}
<div class="col-sm-1" [ngClass]="{ 'ms-auto': type() === FilterType.SPECIAL }">
<app-button title="Remove filter" [variant]="'link'" (click)="onRemove()"
><app-icon icon="trash"></app-icon
></app-button>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { DeploymentFilterComponent } from './deployment-filter.component';
import { DeploymentService } from '../../deployment/deployment.service';
import { DeploymentFilter } from '../../deployment/deployment-filter';
import { of } from 'rxjs';

describe('DeploymentFilterComponent', () => {
let component: DeploymentFilterComponent;
let fixture: ComponentFixture<DeploymentFilterComponent>;
let deploymentService: DeploymentService;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DeploymentFilterComponent],
providers: [DeploymentService, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()],
}).compileComponents();

fixture = TestBed.createComponent(DeploymentFilterComponent);
component = fixture.componentInstance;
deploymentService = TestBed.inject(DeploymentService);

fixture.componentRef.setInput('filter', { name: 'Test Filter', comp: 'eq', val: 'test' } as DeploymentFilter);
fixture.componentRef.setInput('index', 0);
fixture.componentRef.setInput('type', 'StringType');
fixture.componentRef.setInput('compOptions', [{ name: 'eq', displayName: 'is' }]);
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should load options for ENUM_TYPE filters and mark for check', () => {
const filter: DeploymentFilter = {
name: 'State',
comp: 'eq',
val: 'failed',
} as DeploymentFilter;

fixture.componentRef.setInput('filter', filter);
fixture.componentRef.setInput('index', 0);
fixture.componentRef.setInput('type', 'ENUM_TYPE');
fixture.componentRef.setInput('compOptions', [{ name: 'eq', displayName: 'is' }]);

const mockOptions = ['success', 'failed', 'canceled'];
vi.spyOn(deploymentService, 'getFilterOptionValues').mockImplementation(() => {
// Before async emission, the component should pre-seed valOptions with the current value
expect(component.valOptions()).toEqual(['failed']);
return of(mockOptions);
});

component.ngOnInit();

expect(deploymentService.getFilterOptionValues).toHaveBeenCalledWith('State');
expect(component.valOptions()).toEqual(mockOptions);
});

it('should set boolean options for booleanType filters and mark for check', () => {
const filter: DeploymentFilter = {
name: 'Confirmed',
comp: 'eq',
val: 'true',
} as DeploymentFilter;

fixture.componentRef.setInput('filter', filter);
fixture.componentRef.setInput('index', 0);
fixture.componentRef.setInput('type', 'booleanType');
fixture.componentRef.setInput('compOptions', [{ name: 'eq', displayName: 'is' }]);

component.ngOnInit();

expect(component.valOptions()).toEqual(['true', 'false']);
});

it('should not load options for SpecialFilterType', () => {
const filter: DeploymentFilter = {
name: 'Special',
comp: 'eq',
val: '',
} as DeploymentFilter;

fixture.componentRef.setInput('filter', filter);
fixture.componentRef.setInput('index', 0);
fixture.componentRef.setInput('type', 'SpecialFilterType');
fixture.componentRef.setInput('compOptions', []);

const spy = vi.spyOn(deploymentService, 'getFilterOptionValues');

component.ngOnInit();

expect(spy).not.toHaveBeenCalled();
expect(component.valOptions()).toEqual([]);
});

it('should not load options for DateType filters', () => {
const filter: DeploymentFilter = {
name: 'Date',
comp: 'eq',
val: '',
} as DeploymentFilter;

fixture.componentRef.setInput('filter', filter);
fixture.componentRef.setInput('index', 0);
fixture.componentRef.setInput('type', 'DateType');
fixture.componentRef.setInput('compOptions', []);

const spy = vi.spyOn(deploymentService, 'getFilterOptionValues');

component.ngOnInit();

expect(spy).not.toHaveBeenCalled();
expect(component.valOptions()).toEqual([]);
});

it('should emit remove event', () => {
const filter: DeploymentFilter = {
name: 'State',
comp: 'eq',
val: 'failed',
} as DeploymentFilter;

fixture.componentRef.setInput('filter', filter);
fixture.componentRef.setInput('index', 0);
fixture.componentRef.setInput('type', 'ENUM_TYPE');

let emittedFilter: DeploymentFilter | undefined;
component.remove.subscribe((f) => (emittedFilter = f));

component.onRemove();

expect(emittedFilter).toBe(filter);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {
ChangeDetectionStrategy,
Component,
inject,
input,
OnInit,
output,
signal,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { DeploymentFilter } from '../../deployment/deployment-filter';
import { ComparatorFilterOption } from '../../deployment/comparator-filter-option';
import { DeploymentService } from '../../deployment/deployment.service';
import { FilterType } from '../../deployment/filter-type.enum';
import { ButtonComponent } from '../../shared/button/button.component';
import { IconComponent } from '../../shared/icon/icon.component';
import { DateTimePickerComponent } from '../../shared/date-time-picker/date-time-picker.component';

@Component({
selector: 'app-deployment-filter',
templateUrl: './deployment-filter.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, FormsModule, ButtonComponent, IconComponent, DateTimePickerComponent],
})
export class DeploymentFilterComponent implements OnInit {
private deploymentService = inject(DeploymentService);
protected readonly FilterType = FilterType;

filter = input.required<DeploymentFilter>();
index = input.required<number>();
type = input.required<string>();
compOptions = input<ComparatorFilterOption[]>([]);
remove = output<DeploymentFilter>();

valOptions = signal<string[]>([]);

ngOnInit() {
// Pre-seed options with the current value so the select can render it before async options load
if (this.filter()?.val) {
this.valOptions.set([String(this.filter().val)]);
}
this.loadOptions();
}

private loadOptions() {
if (this.type() === FilterType.BOOLEAN) {
this.valOptions.set(['true', 'false']);
} else if (this.type() !== FilterType.SPECIAL && this.type() !== FilterType.DATE) {
this.deploymentService.getFilterOptionValues(this.filter().name).subscribe({
next: (options) => {
this.valOptions.set(options);
},
});
}
}

onRemove() {
this.remove.emit(this.filter());
}
}
82 changes: 14 additions & 68 deletions AMW_angular/io/src/app/deployments/deployments.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,77 +21,23 @@
[(ngModel)]="selectedFilterType"
(change)="addFilter()"
>
@for (filterType of filterTypes; track filterType.name) {
@for (filterType of filterTypes(); track filterType.name) {
<option [ngValue]="filterType">{{ filterType.name }}</option>
}
</select>
</div>
</div>

@if (filters.length > 0) {
@for (filter of filters; track $index; let i = $index) {
<div class="form-group row">
<div class="col-sm-4 offset-1">
<input class="form-control" disabled type="text" value="{{ filter.name }}" />
</div>
@if (filter.type === 'SpecialFilterType') {
<div class="col-sm-6"></div>
}
@if (filter.type !== 'SpecialFilterType') {
<div class="col-sm-2">
<select
id="selectFilterComp"
class="form-control"
[attr.disabled]="filter.compOptions.length === 1 ? '' : null"
[(ngModel)]="filter.comp"
>
@for (compOption of filter.compOptions; track compOption) {
<option [ngValue]="compOption.name">
{{ compOption.displayName }}
</option>
}
</select>
</div>
@if (filter.type !== 'DateType') {
@if (filter.type !== 'booleanType' && filter.type !== 'ENUM_TYPE') {
<div class="col-sm-4">
<input class="form-control" attr.list="list{{ i }}" type="text" [(ngModel)]="filter.val" />
<datalist attr.id="list{{ i }}">
@for (filterValueOption of filter.valOptions; track filterValueOption) {
<option>{{ filterValueOption }}</option>
}
</datalist>
</div>
}
@if (filter.type === 'booleanType' || filter.type === 'ENUM_TYPE') {
<div class="col-sm-4">
<select class="form-control" [(ngModel)]="filter.val">
@for (filterValueOption of filter.valOptions; track filterValueOption) {
<option>{{ filterValueOption }}</option>
}
</select>
</div>
}
}
@if (filter.type === 'DateType') {
<div class="col-sm-4">
<div class="input-group date">
<app-date-time-picker
[(ngModel)]="filter.val"
name="dateDeployment"
id="datetimepicker"
class="w-100"
></app-date-time-picker>
</div>
</div>
}
}
<div class="col-sm-1">
<app-button title="Remove filter" [variant]="'link'" (click)="removeFilter(filter)"
><app-icon icon="trash"></app-icon
></app-button>
</div>
</div>
@if (filters().length > 0) {
@for (filter of filters(); track $index; let i = $index) {
<app-deployment-filter
[filter]="filter"
[index]="i"
[type]="getFilterType(filter.name) || ''"
[compOptions]="comparatorOptionsForFilterType(getFilterType(filter.name) || '')"
(remove)="removeFilter($event)"
>
</app-deployment-filter>
}
}

Expand All @@ -103,15 +49,15 @@
<app-button
[variant]="'danger'"
[additionalClasses]="'mb-2 me-2'"
[disabled]="filters.length < 1"
[disabled]="filters().length < 1"
(click)="clearFilters()"
>
Clear filters</app-button
>
<app-button
[variant]="'secondary'"
[additionalClasses]="'mb-2 me-2'"
[disabled]="filters.length < 1"
[disabled]="filters().length < 1"
(click)="copyURL()"
><app-icon icon="clipboard"></app-icon> Clipboard</app-button
>
Expand Down Expand Up @@ -168,7 +114,7 @@
[deployments]="deployments()"
[sortCol]="sortCol"
[sortDirection]="sortDirection"
[filtersForParam]="filtersForParam"
[filtersForParam]="filters()"
(doConfirmDeployment)="confirmDeployment($event)"
(doCancelDeployment)="cancelDeployment($event)"
(doRejectDeployment)="rejectDeployment($event)"
Expand Down
Loading