diff --git a/AMW_angular/io/src/app/deployment/deployment-filter.ts b/AMW_angular/io/src/app/deployment/deployment-filter.ts index 7ea3a27bb..becea7163 100644 --- a/AMW_angular/io/src/app/deployment/deployment-filter.ts +++ b/AMW_angular/io/src/app/deployment/deployment-filter.ts @@ -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; } diff --git a/AMW_angular/io/src/app/deployment/filter-type.enum.ts b/AMW_angular/io/src/app/deployment/filter-type.enum.ts new file mode 100644 index 000000000..66141592c --- /dev/null +++ b/AMW_angular/io/src/app/deployment/filter-type.enum.ts @@ -0,0 +1,7 @@ +export enum FilterType { + BOOLEAN = 'booleanType', + DATE = 'DateType', + SPECIAL = 'SpecialFilterType', + ENUM = 'ENUM_TYPE', + STRING = 'StringType', +} diff --git a/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.html b/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.html new file mode 100644 index 000000000..f0fc8d8ff --- /dev/null +++ b/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.html @@ -0,0 +1,59 @@ +
+
+ +
+ @if (type() !== FilterType.SPECIAL) { +
+ +
+ @if (type() !== FilterType.DATE) { + @if (type() !== FilterType.BOOLEAN && type() !== FilterType.ENUM) { +
+ + + @for (filterValueOption of valOptions(); track filterValueOption) { + + } + +
+ } + @if (type() === FilterType.BOOLEAN || type() === FilterType.ENUM) { +
+ +
+ } + } + @if (type() === FilterType.DATE) { +
+
+ +
+
+ } + } +
+ +
+
diff --git a/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.spec.ts b/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.spec.ts new file mode 100644 index 000000000..cbc335024 --- /dev/null +++ b/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.spec.ts @@ -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; + 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); + }); +}); diff --git a/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.ts b/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.ts new file mode 100644 index 000000000..14e7139ad --- /dev/null +++ b/AMW_angular/io/src/app/deployments/deployment-filter/deployment-filter.component.ts @@ -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(); + index = input.required(); + type = input.required(); + compOptions = input([]); + remove = output(); + + valOptions = signal([]); + + 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()); + } +} diff --git a/AMW_angular/io/src/app/deployments/deployments.component.html b/AMW_angular/io/src/app/deployments/deployments.component.html index 11d52f47d..beade6ee5 100644 --- a/AMW_angular/io/src/app/deployments/deployments.component.html +++ b/AMW_angular/io/src/app/deployments/deployments.component.html @@ -21,77 +21,23 @@ [(ngModel)]="selectedFilterType" (change)="addFilter()" > - @for (filterType of filterTypes; track filterType.name) { + @for (filterType of filterTypes(); track filterType.name) { } - @if (filters.length > 0) { - @for (filter of filters; track $index; let i = $index) { -
-
- -
- @if (filter.type === 'SpecialFilterType') { -
- } - @if (filter.type !== 'SpecialFilterType') { -
- -
- @if (filter.type !== 'DateType') { - @if (filter.type !== 'booleanType' && filter.type !== 'ENUM_TYPE') { -
- - - @for (filterValueOption of filter.valOptions; track filterValueOption) { - - } - -
- } - @if (filter.type === 'booleanType' || filter.type === 'ENUM_TYPE') { -
- -
- } - } - @if (filter.type === 'DateType') { -
-
- -
-
- } - } -
- -
-
+ @if (filters().length > 0) { + @for (filter of filters(); track $index; let i = $index) { + + } } @@ -103,7 +49,7 @@ Clear filters Clipboard @@ -168,7 +114,7 @@ [deployments]="deployments()" [sortCol]="sortCol" [sortDirection]="sortDirection" - [filtersForParam]="filtersForParam" + [filtersForParam]="filters()" (doConfirmDeployment)="confirmDeployment($event)" (doCancelDeployment)="cancelDeployment($event)" (doRejectDeployment)="rejectDeployment($event)" diff --git a/AMW_angular/io/src/app/deployments/deployments.component.spec.ts b/AMW_angular/io/src/app/deployments/deployments.component.spec.ts index 2ceecaf1d..f64e0be20 100644 --- a/AMW_angular/io/src/app/deployments/deployments.component.spec.ts +++ b/AMW_angular/io/src/app/deployments/deployments.component.spec.ts @@ -9,6 +9,7 @@ import { DeploymentService } from '../deployment/deployment.service'; import { DeploymentsListComponent } from './deployments-list.component'; import { DeploymentsEditModalComponent } from './deployments-edit-modal.component'; import { DeploymentFilterType } from '../deployment/deployment-filter-type'; +import { FilterType } from '../deployment/filter-type.enum'; import { ComparatorFilterOption } from '../deployment/comparator-filter-option'; import { DeploymentFilter } from '../deployment/deployment-filter'; import { Deployment } from '../deployment/deployment'; @@ -81,7 +82,7 @@ describe('DeploymentsComponent (with query params)', () => { expect(deploymentService.canRequestDeployments).toHaveBeenCalled(); }); - it('should enhance filters with the right comparator and comparator options on ngOnInit', () => { + it('should enhance filters with the right comparator on ngOnInit', () => { // given const deploymentFilters: DeploymentFilterType[] = [ { name: 'Application', type: 'StringType' }, @@ -101,13 +102,13 @@ describe('DeploymentsComponent (with query params)', () => { activatedRouteStub.queryParams.next({ filters: filter }); // then - expect(component.paramFilters[0].compOptions.length).toEqual(1); - expect(component.paramFilters[1].compOptions.length).toEqual(3); + expect(component.paramFilters[0].name).toEqual('Application'); expect(component.paramFilters[0].comp).toEqual('eq'); + expect(component.paramFilters[1].name).toEqual('Confirmed on'); expect(component.paramFilters[1].comp).toEqual('lt'); }); - it('should enhance filters with the right option values on ngOnInit', () => { + it('should enhance filters with correct filter types on ngOnInit', () => { // given const deploymentFilters: DeploymentFilterType[] = [ { name: 'Application', type: 'StringType' }, @@ -115,15 +116,6 @@ describe('DeploymentsComponent (with query params)', () => { ]; vi.spyOn(deploymentService, 'getAllDeploymentFilterTypes').mockReturnValue(of(deploymentFilters)); vi.spyOn(deploymentService, 'getAllComparatorFilterOptions').mockReturnValue(of([])); - vi.spyOn(deploymentService, 'getFilterOptionValues').mockImplementation((param: string) => { - const optionValues: { - [key: string]: string[]; - } = { - Application: ['app1', 'app2'], - 'Confirmed on': [], - }; - return of(optionValues[param]); - }); vi.spyOn(deploymentService, 'canRequestDeployments').mockReturnValue(of(true)); // when @@ -131,8 +123,36 @@ describe('DeploymentsComponent (with query params)', () => { activatedRouteStub.queryParams.next({ filters: filter }); // then - expect(component.paramFilters[0].valOptions.length).toEqual(2); - expect(component.paramFilters[1].valOptions.length).toEqual(0); + expect(component.paramFilters[0].name).toEqual('Application'); + expect(component.getFilterType('Application')).toEqual(FilterType.STRING); + expect(component.paramFilters[1].name).toEqual('Confirmed on'); + expect(component.getFilterType('Confirmed on')).toEqual(FilterType.DATE); + }); + + it('should set URL filter value immediately while component loads options asynchronously', () => { + // given + const stateFilter = JSON.stringify([{ name: 'State', comp: 'eq', val: 'failed' }]); + const deploymentFilters: DeploymentFilterType[] = [{ name: 'State', type: 'ENUM_TYPE' }]; + + vi.spyOn(deploymentService, 'getAllDeploymentFilterTypes').mockReturnValue(of(deploymentFilters)); + vi.spyOn(deploymentService, 'getAllComparatorFilterOptions').mockReturnValue(of([])); + vi.spyOn(deploymentService, 'canRequestDeployments').mockReturnValue(of(true)); + vi.spyOn(deploymentService, 'getFilteredDeployments').mockReturnValue(of({ deployments: [], total: 0 })); + + // when + component.ngOnInit(); + activatedRouteStub.queryParams.next({ filters: stateFilter }); + + // then + expect(component.filters().length).toBe(1); + const filterResult = component.filters()[0]; + + // The filter value should be preserved as 'failed' immediately + expect(filterResult.val).toEqual('failed'); + // Filter should have the correct type in filterTypes + expect(component.getFilterType('State')).toEqual('ENUM_TYPE'); + // Component will load options asynchronously + expect(filterResult.name).toEqual('State'); }); it('should apply filters ngOnInit ', () => { @@ -165,7 +185,7 @@ describe('DeploymentsComponent (with query params)', () => { // then expect(deploymentService.getFilteredDeploymentsForCsvExport).toHaveBeenCalledWith( - JSON.stringify(component.filtersForBackend), + JSON.stringify([]), 'd.deploymentDate', 'DESC', ); @@ -378,14 +398,14 @@ describe('DeploymentsComponent (without query params)', () => { it('should remove filter and reset offset on removeFilter', () => { // given component.offset = 10; - component.filters = [ + component.filters.set([ { name: 'Confirmed', comp: 'eq', val: 'true', type: 'booleanType', } as DeploymentFilter, - ]; + ]); const deploymentFilters: DeploymentFilterType[] = [ { name: 'Application', type: 'StringType' }, { name: 'Confirmed on', type: 'DateType' }, @@ -399,24 +419,24 @@ describe('DeploymentsComponent (without query params)', () => { vi.spyOn(deploymentService, 'getAllComparatorFilterOptions').mockReturnValue(of(comparatorOptions)); // when - component.removeFilter(component.filters[0]); + component.removeFilter(component.filters()[0]); // then - expect(component.filters.length).toEqual(0); + expect(component.filters().length).toEqual(0); expect(component.offset).toEqual(0); }); it('should reset offset on setMaxResultsPerPage', () => { // given component.offset = 10; - component.filters = [ + component.filters.set([ { name: 'Confirmed', comp: 'eq', val: 'true', type: 'booleanType', } as DeploymentFilter, - ]; + ]); // when component.setMaxResultsPerPage(20); @@ -486,7 +506,7 @@ describe('DeploymentsComponent (without query params)', () => { ]; const deploymentFilters: DeploymentFilterType[] = [ { name: 'Application', type: 'StringType' }, - { name: 'Confirmed on', type: 'DateType' }, + { name: 'Confirmed', type: 'booleanType' }, ]; const comparatorOptions: ComparatorFilterOption[] = [ { name: 'lt', displayName: '<' }, @@ -497,21 +517,19 @@ describe('DeploymentsComponent (without query params)', () => { vi.spyOn(deploymentService, 'getAllComparatorFilterOptions').mockReturnValue(of(comparatorOptions)); vi.spyOn(deploymentService, 'getFilteredDeployments').mockReturnValue(of({ deployments: [], total: 0 })); - // given - component.filters = [ + component.filterTypes.set(deploymentFilters); + component.filters.set([ { name: 'Confirmed', comp: 'eq', val: 'true', - type: 'booleanType', } as DeploymentFilter, { name: 'Application', comp: 'eq', val: 'TestApp', - type: 'StringType', } as DeploymentFilter, - ]; + ]); // when component.applyFilters(); @@ -538,7 +556,7 @@ describe('DeploymentsComponent (without query params)', () => { const deploymentFilters: DeploymentFilterType[] = [ { name: 'Application', type: 'StringType' }, { name: 'Application Server', type: 'StringType' }, - { name: 'Confirmed on', type: 'DateType' }, + { name: 'Confirmed', type: 'booleanType' }, { name: 'Latest deployment', type: 'SpecialFilterType' }, ]; const comparatorOptions: ComparatorFilterOption[] = [ @@ -550,38 +568,35 @@ describe('DeploymentsComponent (without query params)', () => { vi.spyOn(deploymentService, 'getAllComparatorFilterOptions').mockReturnValue(of(comparatorOptions)); vi.spyOn(deploymentService, 'getFilteredDeployments').mockReturnValue(of({ deployments: [], total: 0 })); - component.filters = [ + component.filterTypes.set(deploymentFilters); + component.filters.set([ { name: 'Confirmed', comp: 'eq', val: 'false', - type: 'booleanType', } as DeploymentFilter, { name: 'Application', comp: 'eq', val: 'TestApp', - type: 'StringType', } as DeploymentFilter, { name: 'Application Server', comp: 'eq', val: '', - type: 'StringType', } as DeploymentFilter, { name: 'Latest deployment', comp: 'eq', val: '', - type: 'SpecialFilterType', } as DeploymentFilter, - ]; + ]); // when component.applyFilters(); // then - expect(component.filters.length).toEqual(3); + expect(component.filters().length).toEqual(3); expect(sessionStorage.getItem('deploymentFilters')).toEqual(JSON.stringify(expectedFilters)); expect(deploymentService.getFilteredDeployments).toHaveBeenCalledWith( JSON.stringify(expectedFilters), @@ -609,7 +624,7 @@ describe('DeploymentsComponent (without query params)', () => { { name: 'Confirmed', comp: 'eq', val: 'true' } as DeploymentFilter, { name: 'Application', comp: 'eq', val: 'TestApp' } as DeploymentFilter, ]; - component.filters = [ + component.filters.set([ { name: 'Confirmed', comp: 'eq', @@ -622,7 +637,7 @@ describe('DeploymentsComponent (without query params)', () => { val: 'TestApp', type: 'StringType', } as DeploymentFilter, - ]; + ]); const deploymentFilters: DeploymentFilterType[] = [ { name: 'Application', type: 'StringType' }, { name: 'Confirmed on', type: 'DateType' }, diff --git a/AMW_angular/io/src/app/deployments/deployments.component.ts b/AMW_angular/io/src/app/deployments/deployments.component.ts index 6f281502e..f4f53a50d 100644 --- a/AMW_angular/io/src/app/deployments/deployments.component.ts +++ b/AMW_angular/io/src/app/deployments/deployments.component.ts @@ -1,11 +1,12 @@ import { Location } from '@angular/common'; -import { ChangeDetectionStrategy, Component, OnInit, ViewChild, inject, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit, ViewChild, computed, inject, signal } from '@angular/core'; import { FormsModule, NgModel } from '@angular/forms'; import { ActivatedRoute, Params } from '@angular/router'; import * as _ from 'lodash-es'; import * as datefns from 'date-fns'; import { Subscription, timer } from 'rxjs'; import { DeploymentFilter } from '../deployment/deployment-filter'; +import { FilterType } from '../deployment/filter-type.enum'; import { DeploymentFilterType } from '../deployment/deployment-filter-type'; import { ComparatorFilterOption } from '../deployment/comparator-filter-option'; import { Deployment } from '../deployment/deployment'; @@ -16,12 +17,12 @@ import { DateTimeModel } from '../shared/date-time-picker/date-time.model'; import { PaginationComponent } from '../shared/pagination/pagination.component'; import { DeploymentsListComponent } from './deployments-list.component'; import { IconComponent } from '../shared/icon/icon.component'; -import { DateTimePickerComponent } from '../shared/date-time-picker/date-time-picker.component'; import { NotificationComponent } from '../shared/elements/notification/notification.component'; import { LoadingIndicatorComponent } from '../shared/elements/loading-indicator.component'; import { PageComponent } from '../layout/page/page.component'; import { ToastService } from '../shared/elements/toast/toast.service'; import { ButtonComponent } from '../shared/button/button.component'; +import { DeploymentFilterComponent } from './deployment-filter/deployment-filter.component'; @Component({ selector: 'app-deployments', @@ -31,12 +32,12 @@ import { ButtonComponent } from '../shared/button/button.component'; LoadingIndicatorComponent, NotificationComponent, FormsModule, - DateTimePickerComponent, IconComponent, DeploymentsListComponent, PaginationComponent, PageComponent, ButtonComponent, + DeploymentFilterComponent, ], }) export class DeploymentsComponent implements OnInit { @@ -52,29 +53,32 @@ export class DeploymentsComponent implements OnInit { paramFilters: DeploymentFilter[] = []; autoload = true; - // enhanced filters for deployment service - filtersForBackend: DeploymentFilter[] = []; - // value of filters parameter. Used to pass as json object to the logView.xhtml - filtersForParam: DeploymentFilter[] = []; - // valid for all, loaded once - filterTypes: DeploymentFilterType[] = []; + filterTypes = signal([]); + private filterTypeMap = computed(() => { + const map = new Map(); + this.filterTypes().forEach(filterType => { + const validFilterTypes = Object.values(FilterType); + if (validFilterTypes.includes(filterType.type as FilterType)) { + map.set(filterType.name, filterType.type as FilterType); + } + }); + return map; + }); comparatorOptions: ComparatorFilterOption[] = []; comparatorOptionsMap: { [key: string]: string } = {}; + singleComparatorOption: ComparatorFilterOption[] = [{ name: 'eq', displayName: 'is' }]; hasPermissionToRequestDeployments = false; csvSeparator = ''; // available edit actions deploymentDate: number; // for deployment date change - // available filterValues (if any) - filterValueOptions: { [key: string]: string[] } = {}; - // to be added selectedFilterType: DeploymentFilterType; // already set - filters: DeploymentFilter[] = []; + filters = signal([]); // filtered deployments deployments = signal([]); @@ -129,11 +133,8 @@ export class DeploymentsComponent implements OnInit { const newFilter: DeploymentFilter = {} as DeploymentFilter; newFilter.name = this.selectedFilterType.name; newFilter.comp = this.defaultComparator; - newFilter.val = this.selectedFilterType.type === 'booleanType' ? 'true' : ''; - newFilter.type = this.selectedFilterType.type; - newFilter.compOptions = this.comparatorOptionsForType(this.selectedFilterType.type); - this.setValueOptionsForFilter(newFilter); - this.filters.push(newFilter); + newFilter.val = this.selectedFilterType.type === FilterType.BOOLEAN ? 'true' : ''; + this.filters.update((filters) => [...filters, newFilter]); this.offset = 0; this.selectedFilterType = null; this.selectModel.reset(null); @@ -141,63 +142,44 @@ export class DeploymentsComponent implements OnInit { } removeFilter(filter: DeploymentFilter) { - const i: number = _.findIndex(this.filters, { + const i: number = _.findIndex(this.filters(), { name: filter.name, comp: filter.comp, val: filter.val, }); if (i !== -1) { - this.filters.splice(i, 1); + this.filters.update((filters) => { + const newFilters = [...filters]; + newFilters.splice(i, 1); + return newFilters; + }); } this.offset = 0; } clearFilters() { - this.filters = []; + this.filters.set([]); sessionStorage.setItem('deploymentFilters', null); this.updateFiltersInURL(null); } applyFilters() { - this.filtersForBackend = []; - this.filtersForParam = []; const filtersToBeRemoved: DeploymentFilter[] = []; this.errorMessage = ''; - this.filters.forEach((filter) => { - if (filter.val || filter.type === 'SpecialFilterType') { - this.filtersForParam.push({ - name: filter.name, - comp: filter.comp, - val: filter.val, - } as DeploymentFilter); - if (filter.type === 'DateType') { - if (!filter.val) { - this.errorMessage = 'Invalid date'; - } - this.filtersForBackend.push({ - name: filter.name, - comp: filter.comp, - val: filter.val.toEpoch().toString(), - } as DeploymentFilter); - } else { - this.filtersForBackend.push({ - name: filter.name, - comp: filter.comp, - val: filter.val, - } as DeploymentFilter); - } - } else { + // Remove empty filters first + this.filters().forEach((filter) => { + const filterType = this.getFilterType(filter.name); + if (!filter.val && filterType !== 'SpecialFilterType') { filtersToBeRemoved.push(filter); + } else if (filterType === FilterType.DATE && !filter.val) { + this.errorMessage = 'Invalid date'; } }); filtersToBeRemoved.forEach((filter) => this.removeFilter(filter)); if (!this.errorMessage) { - this.getFilteredDeployments(JSON.stringify(this.filtersForBackend)); - let filterString: string = null; - if (this.filtersForParam.length > 0) { - filterString = JSON.stringify(this.filtersForParam); - } + this.getFilteredDeployments(this.buildBackendFilters()); + const filterString = this.filters().length > 0 ? JSON.stringify(this.filters()) : null; sessionStorage.setItem('deploymentFilters', filterString); this.updateFiltersInURL(filterString); } @@ -277,7 +259,7 @@ export class DeploymentsComponent implements OnInit { exportCSV() { this.isLoading.set(true); this.errorMessage = 'Generating your CSV.
Please hold on, depending on the requested data this may take a while'; - this.getFilteredDeploymentsForCsvExport(JSON.stringify(this.filtersForBackend)); + this.getFilteredDeploymentsForCsvExport(this.buildBackendFilters()); } async copyURL() { @@ -323,16 +305,20 @@ export class DeploymentsComponent implements OnInit { autoRefresh() { if (this.refreshInterval > 0 && !this.timerSubscription) { this.timerSubscription = timer(this.refreshInterval * 1000).subscribe(() => { - this.getFilteredDeployments(JSON.stringify(this.filtersForBackend)); + this.getFilteredDeployments(this.buildBackendFilters()); this.timerSubscription = null; }); } } + getFilterType(filterName: string): FilterType | undefined { + return this.filterTypeMap().get(filterName); + } + private canFilterBeAdded(): boolean { return ( this.selectedFilterType.name !== 'Latest deployment job for App Server and Env' || - _.findIndex(this.filters, { name: this.selectedFilterType.name }) === -1 + _.findIndex(this.filters(), { name: this.selectedFilterType.name }) === -1 ); } @@ -373,22 +359,28 @@ export class DeploymentsComponent implements OnInit { } private comparatorOptionsForType(filterType: string) { - if (filterType === 'booleanType' || filterType === 'StringType' || filterType === 'ENUM_TYPE') { - return [{ name: 'eq', displayName: 'is' }]; + if (filterType === FilterType.BOOLEAN || filterType === FilterType.STRING || filterType === FilterType.ENUM) { + return this.singleComparatorOption; } else { return this.comparatorOptions; } } - private setValueOptionsForFilter(filter: DeploymentFilter) { - if (!this.filterValueOptions[filter.name]) { - if (filter.type === 'booleanType') { - filter.valOptions = this.filterValueOptions[filter.name] = ['true', 'false']; - } else { - this.getAndSetFilterOptionValues(filter); - } - } - filter.valOptions = this.filterValueOptions[filter.name]; + comparatorOptionsForFilterType(filterType: string) { + return this.comparatorOptionsForType(filterType); + } + + private buildBackendFilters(): string { + const filters = this.filters().map( + (filter) => + ({ + name: filter.name, + comp: filter.comp, + val: this.getFilterType(filter.name) === FilterType.DATE + && filter.val instanceof DateTimeModel ? filter.val.toEpoch().toString() : filter.val, + }) as DeploymentFilter, + ); + return JSON.stringify(filters); } private mapStates() { @@ -422,7 +414,7 @@ export class DeploymentsComponent implements OnInit { private initTypeAndOptions() { this.isLoading.set(true); this.deploymentService.getAllDeploymentFilterTypes().subscribe({ - next: (r) => (this.filterTypes = _.sortBy(r, 'name')), + next: (r) => this.filterTypes.set(_.sortBy(r, 'name')), error: (e) => (this.errorMessage = e), complete: () => { this.getAllComparatorOptions(); @@ -441,14 +433,6 @@ export class DeploymentsComponent implements OnInit { }); } - private getAndSetFilterOptionValues(filter: DeploymentFilter) { - this.deploymentService.getFilterOptionValues(filter.name).subscribe({ - next: (r) => (this.filterValueOptions[filter.name] = r), - error: (e) => (this.errorMessage = e), - complete: () => (filter.valOptions = this.filterValueOptions[filter.name]), - }); - } - private getFilteredDeployments(filterString: string) { this.isLoading.set(true); this.deploymentService @@ -490,31 +474,34 @@ export class DeploymentsComponent implements OnInit { } private enhanceParamFilter() { - if (this.paramFilters) { + if (this.paramFilters && this.paramFilters.length > 0) { this.clearFilters(); + const enhancedFilters: DeploymentFilter[] = []; + this.paramFilters.forEach((filter) => { - const i: number = _.findIndex(this.filterTypes, ['name', filter.name]); - if (i >= 0) { - filter.type = this.filterTypes[i].type; - filter.compOptions = this.comparatorOptionsForType(filter.type); + const filterType = this.getFilterType(filter.name); + if (filterType) { filter.comp = !filter.comp ? this.defaultComparator : filter.comp; - this.parseDateTime(filter); - this.setValueOptionsForFilter(filter); - this.filters.push(filter); + this.parseDateTime(filter, filterType); + enhancedFilters.push(filter); } else { this.errorMessage = 'Error parsing filter'; } }); - } - if (this.autoload) { + + this.filters.set(enhancedFilters); + if (this.autoload) { + this.applyFilters(); + } + } else if (this.autoload) { this.applyFilters(); } } // parse string from json back to DateTimeModel - private parseDateTime(filter: DeploymentFilter) { - if (filter.type === 'DateType') { - filter.val = DateTimeModel.fromLocalString(filter.val); + private parseDateTime(filter: DeploymentFilter, filterType: string) { + if (filterType === FilterType.DATE) { + filter.val = DateTimeModel.fromLocalString(filter.val as string); } }