+++ /dev/null
-export interface InventoryDeviceAppliedFilter {
- label: string;
- prop: string;
- value: string;
- formatValue: string;
-}
+++ /dev/null
-import { PipeTransform } from '@angular/core';
-
-export interface InventoryDeviceFilter {
- label: string;
- prop: string;
- initValue: string;
- value: string;
- options: {
- value: string;
- formatValue: string;
- }[];
- pipe?: PipeTransform;
-}
+++ /dev/null
-import { InventoryDeviceAppliedFilter } from './inventory-device-applied-filters.interface';
-import { InventoryDevice } from './inventory-device.model';
-
-export interface InventoryDeviceFiltersChangeEvent {
- filters: InventoryDeviceAppliedFilter[];
- filterInDevices: InventoryDevice[];
- filterOutDevices: InventoryDevice[];
-}
-<cd-table [data]="filterInDevices"
+<cd-table [data]="devices"
[columns]="columns"
identifier="uid"
[forceIdentifier]="true"
columnMode="flex"
[autoReload]="false"
[searchField]="false"
- (updateSelection)="updateSelection($event)">
+ (updateSelection)="updateSelection($event)"
+ (columnFiltersChanged)="onColumnFiltersChanged($event)">
<cd-table-actions class="table-actions"
[permission]="permission"
[selection]="selection"
[tableActions]="tableActions">
</cd-table-actions>
- <div class="table-filters form-inline"
- *ngIf="filters.length !== 0">
- <div class="form-group filter tc_filter"
- *ngFor="let filter of filters">
- <label class="col-form-label"><span>{{ filter.label }}</span><span>: </span></label>
- <select class="custom-select"
- [(ngModel)]="filter.value"
- [ngModelOptions]="{standalone: true}"
- (ngModelChange)="onFilterChange()"
- [disabled]="filter.disabled">
- <option *ngFor="let opt of filter.options"
- [value]="opt.value">{{ opt.formatValue }}</option>
- </select>
- </div>
- <div class="widget-toolbar tc_refreshBtn"
- *ngIf="filters.length !== 0">
- <button type="button"
- title="Reset filters"
- class="btn btn-light"
- (click)="onFilterReset()">
- <span [ngClass]="[icons.stack]">
- <i [ngClass]="[icons.filter, icons.stack2x]"></i>
- <i [ngClass]="[icons.destroy, icons.stack1x]"></i>
- </span>
- </button>
- </div>
- </div>
</cd-table>
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
-import { getterForProp } from '@swimlane/ngx-datatable/release/utils';
import * as _ from 'lodash';
import { ToastrModule } from 'ngx-toastr';
-import {
- configureTestBed,
- FixtureHelper,
- i18nProviders
-} from '../../../../../testing/unit-test-helper';
+import { configureTestBed, i18nProviders } from '../../../../../testing/unit-test-helper';
import { SharedModule } from '../../../../shared/shared.module';
-import { InventoryDevice } from './inventory-device.model';
import { InventoryDevicesComponent } from './inventory-devices.component';
describe('InventoryDevicesComponent', () => {
let component: InventoryDevicesComponent;
let fixture: ComponentFixture<InventoryDevicesComponent>;
- let fixtureHelper: FixtureHelper;
- const devices: InventoryDevice[] = [
- {
- hostname: 'node0',
- uid: '1',
- path: 'sda',
- sys_api: {
- vendor: 'AAA',
- model: 'aaa',
- size: 1024,
- rotational: 'false',
- human_readable_size: '1 KB'
- },
- available: false,
- rejected_reasons: [''],
- device_id: 'AAA-aaa-id0',
- human_readable_type: 'nvme/ssd',
- osd_ids: []
- },
- {
- hostname: 'node0',
- uid: '2',
- path: 'sdb',
- sys_api: {
- vendor: 'AAA',
- model: 'aaa',
- size: 1024,
- rotational: 'false',
- human_readable_size: '1 KB'
- },
- available: true,
- rejected_reasons: [''],
- device_id: 'AAA-aaa-id1',
- human_readable_type: 'nvme/ssd',
- osd_ids: []
- },
- {
- hostname: 'node0',
- uid: '3',
- path: 'sdc',
- sys_api: {
- vendor: 'BBB',
- model: 'bbb',
- size: 2048,
- rotational: 'true',
- human_readable_size: '2 KB'
- },
- available: true,
- rejected_reasons: [''],
- device_id: 'BBB-bbbb-id0',
- human_readable_type: 'hdd',
- osd_ids: []
- },
- {
- hostname: 'node1',
- uid: '4',
- path: 'sda',
- sys_api: {
- vendor: 'CCC',
- model: 'ccc',
- size: 1024,
- rotational: 'true',
- human_readable_size: '1 KB'
- },
- available: false,
- rejected_reasons: [''],
- device_id: 'CCC-cccc-id0',
- human_readable_type: 'hdd',
- osd_ids: []
- }
- ];
configureTestBed({
imports: [FormsModule, HttpClientTestingModule, SharedModule, ToastrModule.forRoot()],
beforeEach(() => {
fixture = TestBed.createComponent(InventoryDevicesComponent);
- fixtureHelper = new FixtureHelper(fixture);
component = fixture.componentInstance;
+ fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
- describe('without device data', () => {
- beforeEach(() => {
- fixture.detectChanges();
- });
-
- it('should have columns that are sortable', () => {
- expect(component.columns.every((column) => Boolean(column.prop))).toBeTruthy();
- });
-
- it('should have filters', () => {
- const labelTexts = fixtureHelper.getTextAll('.tc_filter span:first-child');
- const filterLabels = _.map(component.filters, 'label');
- expect(labelTexts).toEqual(filterLabels);
-
- const optionTexts = fixtureHelper.getTextAll('.tc_filter option');
- expect(optionTexts).toEqual(_.map(component.filters, 'initValue'));
- });
- });
-
- describe('with device data', () => {
- beforeEach(() => {
- component.devices = devices;
- fixture.detectChanges();
- });
-
- it('should have filters', () => {
- for (let i = 0; i < component.filters.length; i++) {
- const optionTexts = fixtureHelper.getTextAll(`.tc_filter:nth-child(${i + 1}) option`);
- const optionTextsSet = new Set(optionTexts);
-
- const filter = component.filters[i];
- const columnValues = devices.map((device: InventoryDevice) => {
- const valueGetter = getterForProp(filter.prop);
- const value = valueGetter(device, filter.prop);
- const formatValue = filter.pipe ? filter.pipe.transform(value) : value;
- return `${formatValue}`;
- });
- const expectedOptionsSet = new Set(['*', ...columnValues]);
- expect(optionTextsSet).toEqual(expectedOptionsSet);
- }
- });
-
- it('should filter a single column', () => {
- spyOn(component.filterChange, 'emit');
- fixtureHelper.selectElement('.tc_filter:nth-child(1) select', 'node1');
- expect(component.filterInDevices.length).toBe(1);
- expect(component.filterInDevices[0]).toEqual(devices[3]);
- expect(component.filterChange.emit).toHaveBeenCalled();
- });
-
- it('should filter multiple columns', () => {
- spyOn(component.filterChange, 'emit');
- fixtureHelper.selectElement('.tc_filter:nth-child(2) select', 'hdd');
- fixtureHelper.selectElement('.tc_filter:nth-child(1) select', 'node0');
- expect(component.filterInDevices.length).toBe(1);
- expect(component.filterInDevices[0].uid).toBe('3');
- expect(component.filterChange.emit).toHaveBeenCalledTimes(2);
- });
+ it('should have columns that are sortable', () => {
+ expect(component.columns.every((column) => Boolean(column.prop))).toBeTruthy();
});
});
import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { I18n } from '@ngx-translate/i18n-polyfill';
-import { getterForProp } from '@swimlane/ngx-datatable/release/utils';
import * as _ from 'lodash';
import { BsModalService } from 'ngx-bootstrap/modal';
import { NotificationType } from '../../../../shared/enum/notification-type.enum';
import { CdTableAction } from '../../../../shared/models/cd-table-action';
import { CdTableColumn } from '../../../../shared/models/cd-table-column';
+import { CdTableColumnFiltersChange } from '../../../../shared/models/cd-table-column-filters-change';
import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
import { Permission } from '../../../../shared/models/permissions';
import { DimlessBinaryPipe } from '../../../../shared/pipes/dimless-binary.pipe';
import { AuthStorageService } from '../../../../shared/services/auth-storage.service';
import { NotificationService } from '../../../../shared/services/notification.service';
-import { InventoryDeviceFilter } from './inventory-device-filter.interface';
-import { InventoryDeviceFiltersChangeEvent } from './inventory-device-filters-change-event.interface';
import { InventoryDevice } from './inventory-device.model';
@Component({
// Device table row selection type
@Input() selectionType: string = undefined;
- @Output() filterChange = new EventEmitter<InventoryDeviceFiltersChangeEvent>();
-
- filterInDevices: InventoryDevice[] = [];
- filterOutDevices: InventoryDevice[] = [];
+ @Output() filterChange = new EventEmitter<CdTableColumnFiltersChange>();
icons = Icons;
columns: Array<CdTableColumn> = [];
- filters: InventoryDeviceFilter[] = [];
selection: CdTableSelection = new CdTableSelection();
permission: Permission;
tableActions: CdTableAction[];
return !this.hiddenColumns.includes(col.prop);
});
- // init filters
- this.filters = this.columns
- .filter((col: any) => {
- return this.filterColumns.includes(col.prop);
- })
- .map((col: any) => {
- return {
- label: col.name,
- prop: col.prop,
- initValue: '*',
- value: '*',
- options: [{ value: '*', formatValue: '*' }],
- pipe: col.pipe
- };
- });
-
- this.filterInDevices = [...this.devices];
- this.updateFilterOptions(this.devices);
- }
-
- ngOnChanges() {
- this.updateFilterOptions(this.devices);
- this.filterInDevices = [...this.devices];
- // TODO: apply filter, columns changes, filter changes
- }
-
- updateFilterOptions(devices: InventoryDevice[]) {
- // update filter options to all possible values in a column, might be time-consuming
- this.filters.forEach((filter) => {
- const values = _.sortedUniq(_.map(devices, filter.prop).sort());
- const options = values.map((v: string) => {
- return {
- value: v,
- formatValue: filter.pipe ? filter.pipe.transform(v) : v
- };
- });
- filter.options = [{ value: '*', formatValue: '*' }, ...options];
- });
- }
-
- doFilter() {
- this.filterOutDevices = [];
- const appliedFilters = [];
- let devices: any = [...this.devices];
- this.filters.forEach((filter) => {
- if (filter.value === filter.initValue) {
- return;
+ // init column filters
+ _.forEach(this.filterColumns, (prop) => {
+ const col = _.find(this.columns, { prop: prop });
+ if (col) {
+ col.filterable = true;
}
- appliedFilters.push({
- label: filter.label,
- prop: filter.prop,
- value: filter.value,
- formatValue: filter.pipe ? filter.pipe.transform(filter.value) : filter.value
- });
- // Separate devices to filter-in and filter-out parts.
- // Cast column value to string type because options are always string.
- const parts = _.partition(devices, (row) => {
- // use getter from ngx-datatable for props like 'sys_api.size'
- const valueGetter = getterForProp(filter.prop);
- return `${valueGetter(row, filter.prop)}` === filter.value;
- });
- devices = parts[0];
- this.filterOutDevices = [...this.filterOutDevices, ...parts[1]];
- });
- this.filterInDevices = devices;
- this.filterChange.emit({
- filters: appliedFilters,
- filterInDevices: this.filterInDevices,
- filterOutDevices: this.filterOutDevices
});
}
- onFilterChange() {
- this.doFilter();
+ ngOnChanges() {
+ this.devices = [...this.devices];
}
- onFilterReset() {
- this.filters.forEach((item) => {
- item.value = item.initValue;
- });
- this.filterInDevices = [...this.devices];
- this.filterOutDevices = [];
- this.filterChange.emit({
- filters: [],
- filterInDevices: this.filterInDevices,
- filterOutDevices: this.filterOutDevices
- });
+ onColumnFiltersChanged(event: CdTableColumnFiltersChange) {
+ this.filterChange.emit(event);
}
updateSelection(selection: CdTableSelection) {
-import { InventoryDeviceFiltersChangeEvent } from '../../inventory/inventory-devices/inventory-device-filters-change-event.interface';
+import { CdTableColumnFiltersChange } from '../../../../shared/models/cd-table-column-filters-change';
-export interface DevicesSelectionChangeEvent extends InventoryDeviceFiltersChangeEvent {
+export interface DevicesSelectionChangeEvent extends CdTableColumnFiltersChange {
type: string;
}
<ng-template #blockClearDevices>
<div class="pb-2 my-2 border-bottom">
<span *ngFor="let filter of appliedFilters">
- <span class="badge badge-dark mr-2">{{ filter.label }}: {{ filter.formatValue }}</span>
+ <span class="badge badge-dark mr-2">{{ filter.name }}: {{ filter.value.formatted }}</span>
</span>
<a class="tc_clearSelections"
href=""
import { BsModalService, ModalOptions } from 'ngx-bootstrap/modal';
import { Icons } from '../../../../shared/enum/icons.enum';
-import { InventoryDeviceFiltersChangeEvent } from '../../inventory/inventory-devices/inventory-device-filters-change-event.interface';
+import { CdTableColumnFiltersChange } from '../../../../shared/models/cd-table-column-filters-change';
import { InventoryDevice } from '../../inventory/inventory-devices/inventory-device.model';
import { OsdDevicesSelectionModalComponent } from '../osd-devices-selection-modal/osd-devices-selection-modal.component';
import { DevicesSelectionChangeEvent } from './devices-selection-change-event.interface';
}
};
const modalRef = this.bsModalService.show(OsdDevicesSelectionModalComponent, options);
- modalRef.content.submitAction.subscribe((result: InventoryDeviceFiltersChangeEvent) => {
- this.devices = result.filterInDevices;
+ modalRef.content.submitAction.subscribe((result: CdTableColumnFiltersChange) => {
+ this.devices = result.data;
this.appliedFilters = result.filters;
const event = _.assign({ type: this.type }, result);
this.selected.emit(event);
<div class="modal-footer">
<cd-submit-button (submitAction)="onSubmit()"
[form]="formGroup"
- [disabled]="!canSubmit || filterInDevices.length === 0">{{ action | titlecase }}</cd-submit-button>
+ [disabled]="!canSubmit || filteredDevices.length === 0">{{ action | titlecase }}</cd-submit-button>
<cd-back-button [back]="bsModalRef.hide"></cd-back-button>
</div>
</form>
import { ToastrModule } from 'ngx-toastr';
import { configureTestBed, i18nProviders } from '../../../../../testing/unit-test-helper';
+import { CdTableColumnFiltersChange } from '../../../../shared/models/cd-table-column-filters-change';
import { SharedModule } from '../../../../shared/shared.module';
-import { InventoryDeviceFiltersChangeEvent } from '../../inventory/inventory-devices/inventory-device-filters-change-event.interface';
import { InventoryDevice } from '../../inventory/inventory-devices/inventory-device.model';
import { InventoryDevicesComponent } from '../../inventory/inventory-devices/inventory-devices.component';
import { OsdDevicesSelectionModalComponent } from './osd-devices-selection-modal.component';
});
it('should enable submit button after filtering some devices', () => {
- const event: InventoryDeviceFiltersChangeEvent = {
+ const event: CdTableColumnFiltersChange = {
filters: [
{
- label: 'hostname',
+ name: 'hostname',
prop: 'hostname',
- value: 'node0',
- formatValue: 'node0'
+ value: { raw: 'node0', formatted: 'node0' }
},
{
- label: 'size',
+ name: 'size',
prop: 'size',
- value: '1024',
- formatValue: '1KiB'
+ value: { raw: '1024', formatted: '1KiB' }
}
],
- filterInDevices: devices,
- filterOutDevices: []
+ data: devices,
+ dataOut: []
};
component.onFilterChange(event);
fixture.detectChanges();
import { Icons } from '../../../../shared/enum/icons.enum';
import { CdFormBuilder } from '../../../../shared/forms/cd-form-builder';
import { CdFormGroup } from '../../../../shared/forms/cd-form-group';
-import { InventoryDeviceFiltersChangeEvent } from '../../inventory/inventory-devices/inventory-device-filters-change-event.interface';
+import { CdTableColumnFiltersChange } from '../../../../shared/models/cd-table-column-filters-change';
import { InventoryDevice } from '../../inventory/inventory-devices/inventory-device.model';
@Component({
})
export class OsdDevicesSelectionModalComponent {
@Output()
- submitAction = new EventEmitter<InventoryDeviceFiltersChangeEvent>();
+ submitAction = new EventEmitter<CdTableColumnFiltersChange>();
icons = Icons;
filterColumns: string[] = [];
action: string;
devices: InventoryDevice[] = [];
+ filteredDevices: InventoryDevice[] = [];
+ event: CdTableColumnFiltersChange;
canSubmit = false;
- filters = [];
- filterInDevices: InventoryDevice[] = [];
- filterOutDevices: InventoryDevice[] = [];
-
- isFiltered = false;
constructor(
private formBuilder: CdFormBuilder,
this.formGroup = this.formBuilder.group({});
}
- onFilterChange(event: InventoryDeviceFiltersChangeEvent) {
+ onFilterChange(event: CdTableColumnFiltersChange) {
this.canSubmit = false;
- this.filters = event.filters;
if (_.isEmpty(event.filters)) {
// filters are cleared
- this.filterInDevices = [];
- this.filterOutDevices = [];
+ this.filteredDevices = [];
+ this.event = undefined;
} else {
// at least one filter is required (except hostname)
- const filters = this.filters.filter((filter) => {
+ const filters = event.filters.filter((filter) => {
return filter.prop !== 'hostname';
});
this.canSubmit = !_.isEmpty(filters);
- this.filterInDevices = event.filterInDevices;
- this.filterOutDevices = event.filterOutDevices;
+ this.filteredDevices = event.data;
+ this.event = event;
}
}
onSubmit() {
- this.submitAction.emit({
- filters: this.filters,
- filterInDevices: this.filterInDevices,
- filterOutDevices: this.filterOutDevices
- });
+ this.submitAction.emit(this.event);
this.bsModalRef.hide();
}
}
+import { CdTableColumnFiltersChange } from '../../../../shared/models/cd-table-column-filters-change';
import { FormatterService } from '../../../../shared/services/formatter.service';
-import { InventoryDeviceAppliedFilter } from '../../inventory/inventory-devices/inventory-device-applied-filters.interface';
export class DriveGroup {
// DriveGroupSpec object.
this.spec['host_pattern'] = pattern;
}
- setDeviceSelection(type: string, appliedFilters: InventoryDeviceAppliedFilter[]) {
+ setDeviceSelection(type: string, appliedFilters: CdTableColumnFiltersChange['filters']) {
const key = `${type}_devices`;
this.spec[key] = {};
appliedFilters.forEach((filter) => {
const attr = this.deviceSelectionAttrs[filter.prop];
if (attr) {
const name = attr.name;
- this.spec[key][name] = attr.formatter ? attr.formatter(filter.value) : filter.value;
+ this.spec[key][name] = attr.formatter ? attr.formatter(filter.value.raw) : filter.value.raw;
}
});
}
const event: DevicesSelectionChangeEvent = {
type: type,
filters: [],
- filterInDevices: devices,
- filterOutDevices: []
+ data: devices,
+ dataOut: []
};
component.onDevicesSelected(event);
if (type === 'data') {
}
onDevicesSelected(event: DevicesSelectionChangeEvent) {
- this.availDevices = event.filterOutDevices;
+ this.availDevices = event.dataOut;
if (event.type === 'data') {
// If user selects data devices for a single host, make only remaining devices on
// that host as available.
const hostnameFilter = _.find(event.filters, { prop: 'hostname' });
if (hostnameFilter) {
- this.hostname = hostnameFilter.value;
- this.availDevices = event.filterOutDevices.filter((device: InventoryDevice) => {
+ this.hostname = hostnameFilter.value.raw;
+ this.availDevices = event.dataOut.filter((device: InventoryDevice) => {
return device.hostname === this.hostname;
});
this.driveGroup.setHostPattern(this.hostname);