]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: carbonize table filters 68990/head
authorNizamudeen A <nia@redhat.com>
Tue, 19 May 2026 04:40:08 +0000 (10:10 +0530)
committerNizamudeen A <nia@redhat.com>
Mon, 8 Jun 2026 07:55:01 +0000 (13:25 +0530)
Fixes: https://tracker.ceph.com/issues/76687
Signed-off-by: Nizamudeen A <nia@redhat.com>
src/pybind/mgr/dashboard/frontend/cypress/e2e/page-helper.po.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/datatable.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.scss
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-column-filter.ts

index 99f00bc0640264319a1f7f5577fff429bfdbd172..d1ad20b7a47fce8c4ccaec9c7c0589aa025e4c7b 100644 (file)
@@ -290,8 +290,13 @@ export abstract class PageHelper {
 
   filterTable(name: string, option: string) {
     this.waitDataTableToLoad();
-    cy.get('select#filter_name').select(name);
-    cy.get('select#filter_option').select(option);
+    cy.get('[data-testid=filter-button]').click();
+    cy.get('cds-popover-content')
+      .should('be.visible')
+      .within(() => {
+        cy.get(`[data-testid="filter-select-${name}"]`).find('select').select(option);
+        cy.get('[data-testid="apply-filters"]').click();
+      });
   }
 
   setPageSize(size: string) {
index 7aa8949ad47a29aaf84024079eacea4ca5aa042d..c9342c4a4d92c87de74d78e92815973957c58e0d 100644 (file)
@@ -19,7 +19,9 @@ import {
   InputModule,
   GridModule,
   LayoutModule,
-  InlineLoadingModule
+  InlineLoadingModule,
+  PopoverModule,
+  TooltipModule
 } from 'carbon-components-angular';
 import AddIcon from '@carbon/icons/es/add/16';
 import FilterIcon from '@carbon/icons/es/filter/16';
@@ -109,7 +111,9 @@ import { TableDetailDirective } from './directives/table-detail.directive';
     InputModule,
     GridModule,
     LayoutModule,
-    InlineLoadingModule
+    InlineLoadingModule,
+    PopoverModule,
+    TooltipModule
   ],
   declarations: [
     TableComponent,
index ebace42a6b7499ac81c6c1cc5f134cb11161ad7e..1460f9ca77a8da3861435d9b6c98e70afba08721 100644 (file)
       </cds-table-toolbar-search>
       <!-- end search -->
       <!-- column filters -->
-      <ng-container *ngIf="columnFilters.length !== 0">
-        <div class="filter-wrapper">
-          <svg [cdsIcon]="icons.filter"
-               [size]="icons.size16"
-               class="cds--toolbar-action__icon"></svg>
-          <cds-select (valueChange)="onSelectFilter($event)"
-                      display="inline"
-                      id="filter_name">
-            <ng-container *ngFor="let filter of columnFilters">
-              <option [value]="filter.column.name"
-                      [selected]="filter.column.name === selectedFilter.column.name">{{ filter.column.name }}</option>
-            </ng-container>
-          </cds-select>
-          <cds-select (valueChange)="onChangeFilter($event)"
-                      display="inline"
-                      id="filter_option">
-            <option *ngIf="!selectedFilter.value"
-                    i18n>Any</option>
-            <ng-container *ngFor="let option of selectedFilter.options">
-              <option [value]="option.raw"
-                      [selected]="option.raw === selectedFilter?.value?.raw">{{ option.formatted }}</option>
-            </ng-container>
-          </cds-select>
+       @if (columnFilters.length !== 0) {
+        <div cdsPopover
+             [isOpen]="openFilterPopover"
+             [autoAlign]="true"
+             [caret]="false">
+          <cds-icon-button
+            kind="ghost"
+            class="toolbar-action"
+            (click)="toggleFilterPopover()"
+            data-testid="filter-button">
+            <svg [cdsIcon]="icons.filter"
+                 [size]="icons.size16"
+                 class="cds--toolbar-action__icon"></svg>
+          </cds-icon-button>
+          <cds-popover-content [cdsLayer]="0">
+            <div
+              cdsGrid
+              [fullWidth]="true"
+              [condensed]="true"
+              [narrow]="true">
+            @for (filter of columnFilters; track filter.column.name; let i = $index) {
+            <div
+              cdsRow
+              [cdsLayer]="2"
+              class="cds-mt-4">
+
+              <!-- filter names column -->
+              <div
+                cdsCol
+                [columnNumbers]="{sm: 4, md: 4}">
+                <cds-text-label>Filter
+                  <input cdsText
+                         readonly
+                         size="sm"
+                         [id]="'filter_key_' + i"
+                         [name]="'filter_key_' + i"
+                         [value]="filter.column.name">
+                </cds-text-label>
+              </div>
+
+              <!-- filter values column -->
+              <div
+                cdsCol
+                [columnNumbers]="{sm: 4, md: 4}">
+                <cds-select (valueChange)="onChangeFilter($event, filter)"
+                            [id]="'filter_value_' + i"
+                            label="Value"
+                            size="sm"
+                            [attr.data-testid]="'filter-select-' + filter.column.name"
+                            i18n-label>
+                  <option i18n>Any</option>
+                  @for (option of filter.options; track option.raw) {
+                    <option [value]="option.raw"
+                            [selected]="option.raw === selectedFilters[filter.column.name]">
+                      {{ option.formatted }}
+                    </option>
+                  }
+
+                </cds-select>
+              </div>
+            </div>
+          }
+          <div cdsRow
+               class="cds-mt-4 cds-mb-4">
+
+            <button cdsButton="primary"
+                    size="sm"
+                    data-testid="apply-filters"
+                    (click)="onSubmitFilter()">
+              <ng-container i18n>Apply filters</ng-container>
+            </button>
+          </div>
+          </div>
+          </cds-popover-content>
         </div>
-      </ng-container>
+      }
       <!-- end column filters -->
       <!-- refresh button -->
       <cds-icon-button
     </cds-table-toolbar-content>
   </cds-table-toolbar>
   <!-- filter chips for column filters -->
-  <div class="d-flex justify-content-end align-items-center filter-tags"
-       *ngIf="toolHeader && columnFiltered">
-  <div class="d-flex gap-2">
-    <ng-container *ngFor="let filter of columnFilters">
-      <cds-tag *ngIf="filter.value"
-               type="outline"
-               class="align-self-center">
-        <span class="me-2">{{ filter.column.name }}: {{ filter.value.formatted }}</span>
-        <button class="cds--tag__close-icon"
-                (click)="onChangeFilter(filter)">
-          <svg [cdsIcon]="icons.destroy"
-               [size]="icons.size16"></svg>
+   @if (toolHeader && columnFiltered) {
+    <div class="filter-tags">
+      <div
+        class="align-items-center"
+        cdsStack="horizontal"
+        [gap]="2">
+        @for (filter of activeFilters | slice:0:3; track filter.column.name) {
+          @if (filter.value) {
+            <cds-tag-filter
+              closeButtonLabel="Clear"
+              i18n-closeButtonLabel
+              (close)="onRemoveFilter(filter)">
+              {{ filter.column.name }}={{ filter.value.formatted }}
+            </cds-tag-filter>
+          }
+        }
+        @if (activeFilters.length > 3) {
+          <cds-tooltip [description]="remainingFiltersTpl">
+            <cds-tag>
+              +{{ activeFilters.length - 3 }}
+            </cds-tag>
+
+            <ng-template #remainingFiltersTpl>
+              <div
+                cdsStack="vertical"
+                [gap]="2">
+              @for (filter of activeFilters | slice:3; track filter.column.name) {
+                <cds-tag-filter
+                  closeButtonLabel="Clear"
+                  i18n-closeButtonLabel
+                  (close)="onRemoveFilter(filter)">
+                  {{ filter.column.name }}={{ filter.value.formatted }}
+                </cds-tag-filter>
+              }
+              </div>
+            </ng-template>
+          </cds-tooltip>
+        }
+        <button cdsButton="ghost"
+                (click)="onClearFilters($event)">
+          <ng-container i18n>Clear filters</ng-container>
         </button>
-      </cds-tag>
-    </ng-container>
-    <button cdsButton="ghost"
-            (click)="onClearFilters($event)">
-      <ng-container i18n>Clear filters</ng-container>
-    </button>
-  </div>
-  </div>
+      </div>
+    </div>
+  }
   <!-- end filter chips for column filters -->
   <table cdsTable
          [sortable]="sortable"
index 89a816426fcf4ab258b658b1a0b0aa9a8caeb7d9..611a3b9e93f6a957a9e67fbc2a51cc05ee86e1d9 100644 (file)
@@ -64,8 +64,8 @@
 }
 
 .filter-tags {
-  background-color: var(--cds-layer-accent);
-  border-bottom: 1px solid var(--cds-layer-active);
+  background-color: var(--cds-layer-01);
+  border-top: var(--cds-spacing-01) solid var(--cds-layer-02);
   color: var(--cds-text-primary);
 }
 
index 949ba54007297edc4506e70369e14ff063667453..889439dcc6e9a29e0646604f8f66eba2eb78128c 100644 (file)
@@ -134,8 +134,8 @@ describe('TableComponent', () => {
     ) => {
       component.search = search;
       _.forEach(changes, (change) => {
-        component.onSelectFilter(change.filter.column.name);
-        component.onChangeFilter(change.value || undefined);
+        component.onChangeFilter(change.value || undefined, change.filter);
+        component.onSubmitFilter();
       });
       expect(component.rows).toEqual(results);
       component.onClearSearch();
index 3ee596f212e172c154f5923fbb6ae56a0a3e498d..7d4c8284784f5f708543fc1d43a5ea3c98f0d975 100644 (file)
@@ -25,7 +25,12 @@ import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
 import { Icons, IconSize, EMPTY_STATE_IMAGE } from '~/app/shared/enum/icons.enum';
 
 import { CdTableColumn } from '~/app/shared/models/cd-table-column';
-import { CdTableColumnFilter } from '~/app/shared/models/cd-table-column-filter';
+import {
+  CdTableColumnFilter,
+  CdTableColumnFilterOption,
+  CdTableColumnSelectedFilter,
+  CdTableColumnStagedFilter
+} from '~/app/shared/models/cd-table-column-filter';
 import { CdTableColumnFiltersChange } from '~/app/shared/models/cd-table-column-filters-change';
 import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
@@ -434,6 +439,14 @@ export class TableComponent implements AfterViewInit, OnInit, OnChanges, OnDestr
   editStates: EditState = {};
   formGroup: CdFormGroup = new CdFormGroup({});
 
+  openFilterPopover = false;
+  stagedFilters: CdTableColumnStagedFilter = {};
+  selectedFilters: CdTableColumnSelectedFilter = {};
+
+  get activeFilters() {
+    return this.columnFilters.filter((filter) => filter.value);
+  }
+
   constructor(
     // private ngZone: NgZone,
     private cdRef: ChangeDetectorRef,
@@ -754,6 +767,10 @@ export class TableComponent implements AfterViewInit, OnInit, OnChanges, OnDestr
     this.tableColumns = this.localColumns;
   }
 
+  toggleFilterPopover() {
+    this.openFilterPopover = !this.openFilterPopover;
+  }
+
   initColumnFilters() {
     let filterableColumns = _.filter(this.localColumns, { filterable: true });
     filterableColumns = [...filterableColumns, ...this.extraFilterableColumns];
@@ -767,6 +784,13 @@ export class TableComponent implements AfterViewInit, OnInit, OnChanges, OnDestr
       };
     });
     this.selectedFilter = _.first(this.columnFilters);
+    this.initSelectedColumnFilters();
+  }
+
+  private initSelectedColumnFilters() {
+    this.columnFilters.forEach((filter) => {
+      this.selectedFilters[filter.column.name] = filter.value?.raw;
+    });
   }
 
   private createColumnFilterOption(
@@ -805,14 +829,43 @@ export class TableComponent implements AfterViewInit, OnInit, OnChanges, OnDestr
     });
   }
 
-  onSelectFilter(filter: string) {
-    const value = this.columnFilters.find((x) => x.column.name === filter);
-    this.selectedFilter = value;
+  // saving the filters to a staged variable so they are not applied immediately
+  onChangeFilter(selectedValue: string, filter: CdTableColumnFilter) {
+    const filterName = filter.column.name;
+    const selectedFilter = this.selectedFilters[filterName];
+    const newSelectedFilter = selectedFilter === selectedValue ? undefined : selectedValue;
+
+    this.selectedFilters[filterName] = newSelectedFilter;
+
+    const option = filter.options.find(
+      (x: CdTableColumnFilterOption) => x.raw === newSelectedFilter
+    );
+    this.stagedFilters[filterName] = option;
+  }
+
+  onSubmitFilter() {
+    this.columnFilters.forEach((filter) => {
+      const filterName = filter.column.name;
+
+      if (this.stagedFilters.hasOwnProperty(filterName)) {
+        filter.value = this.stagedFilters[filterName];
+        this.selectedFilter = filter;
+      }
+    });
+
+    this.stagedFilters = {};
+    this.updateFilter();
+    this.openFilterPopover = false;
   }
 
-  onChangeFilter(filter: string) {
-    const option = this.selectedFilter.options.find((x) => x.raw === filter);
-    this.selectedFilter.value = _.isEqual(this.selectedFilter.value, option) ? undefined : option;
+  onRemoveFilter(filter: CdTableColumnFilter) {
+    const filterName = filter.column.name;
+    filter.value = undefined;
+    this.selectedFilters[filterName] = undefined;
+    delete this.stagedFilters[filterName];
+    if (this.selectedFilter?.column.name === filterName) {
+      this.selectedFilter = undefined;
+    }
     this.updateFilter();
   }
 
@@ -1288,6 +1341,7 @@ export class TableComponent implements AfterViewInit, OnInit, OnChanges, OnDestr
     });
     this.selectedFilter = _.first(this.columnFilters);
     this.updateFilter();
+    this.initSelectedColumnFilters();
   }
 
   updateFilter() {
index ccdbe82fc1b26d04fac26c1234621d632e8d37a7..d4d45ac91ba4c144defe928819d5823a476b846f 100644 (file)
@@ -2,6 +2,19 @@ import { CdTableColumn } from './cd-table-column';
 
 export interface CdTableColumnFilter {
   column: CdTableColumn;
-  options: { raw: string; formatted: string }[]; // possible options of a filter
-  value?: { raw: string; formatted: string }; // selected option
+  options: CdTableColumnFilterOption[]; // possible options of a filter
+  value?: CdTableColumnFilterOption; // selected option
+}
+
+export interface CdTableColumnStagedFilter {
+  [filterName: string]: CdTableColumnFilterOption;
+}
+
+export interface CdTableColumnSelectedFilter {
+  [filterName: string]: string | undefined;
+}
+
+export interface CdTableColumnFilterOption {
+  raw: string;
+  formatted: string;
 }