Copilot commented on code in PR #4240:
URL: https://github.com/apache/streampipes/pull/4240#discussion_r2918032553


##########
ui/src/app/chart-shared/components/charts/table/table-widget.component.ts:
##########
@@ -325,131 +666,195 @@ export class TableWidgetComponent extends 
BaseDataExplorerWidgetDirective<TableW
     }
 
     private filterRows(rows: TableRow[]): TableRow[] {
-        const searchTerm = (
+        let result = [...rows];
+        const search = (
             this.dataExplorerWidget.visualizationConfig.searchValue ?? ''
         )
             .trim()
             .toLowerCase();
-
-        if (!searchTerm) {
-            return [...rows];
+        if (search) {
+            result = result.filter(row =>
+                this.columnNames.some(c =>
+                    String(this.formatCellValue(c, row[c]))
+                        .toLowerCase()
+                        .includes(search),
+                ),
+            );
         }
-
-        return rows.filter(row =>
-            this.columnNames.some(column =>
-                String(this.formatCellValue(column, row[column]))
-                    .toLowerCase()
-                    .includes(searchTerm),
-            ),
-        );
+        for (const col of this.columnNames) {
+            const f = this.columnFilters[col];
+            if (f && f.size < this.getAllUniqueValues(col).length) {
+                result = result.filter(row =>
+                    f.has(this.formatForFilter(row, col)),
+                );
+            }
+            const adv = this.advancedFilters[col];
+            if (adv) result = this.applyAdvancedFilterToRows(result, col, adv);
+        }
+        return result;
     }
 
-    private sortRows(rows: TableRow[]): TableRow[] {
-        if (!this.sortColumn || this.sortDirection === '') {
-            return [...rows];
+    private applyAdvancedFilterToRows(
+        rows: TableRow[],
+        column: string,
+        adv: AdvancedFilter,
+    ): TableRow[] {
+        if (adv.type === 'Top 10') {
+            const top = rows
+                .map(r => ({ r, n: this.toNumber(r[column]) }))
+                .filter(
+                    (e): e is { r: TableRow; n: number } => e.n !== undefined,
+                )
+                .sort((a, b) => b.n - a.n)
+                .slice(0, 10);
+            const set = new Set(top.map(e => e.r));
+            return rows.filter(r => set.has(r));
         }
+        if (adv.type === 'Above average' || adv.type === 'Below average') {
+            const nums = rows
+                .map(r => this.toNumber(r[column]))
+                .filter((n): n is number => n !== undefined);
+            if (!nums.length) return rows;
+            const avg = nums.reduce((s, n) => s + n, 0) / nums.length;
+            return rows.filter(r => {
+                const n = this.toNumber(r[column]);
+                return (
+                    n !== undefined &&
+                    (adv.type === 'Above average' ? n > avg : n < avg)
+                );
+            });
+        }
+        return rows.filter(r => this.passesAdvancedFilter(r, column, adv));
+    }
 
-        const directionMultiplier = this.sortDirection === 'asc' ? 1 : -1;
-        return [...rows].sort((rowA, rowB) => {
-            const comparison = this.compareValues(
-                rowA[this.sortColumn],
-                rowB[this.sortColumn],
-                this.sortColumn,
-            );
+    private passesAdvancedFilter(
+        row: TableRow,
+        column: string,
+        adv: AdvancedFilter,
+    ): boolean {
+        const raw = row[column];
+        if (this.isNumericColumn(column) || column === 'time') {
+            const n =
+                column === 'time'
+                    ? new Date(raw as string | number).getTime()
+                    : this.toNumber(raw);
+            const t1 = Number(adv.value);
+            const t2 = Number(adv.value2);
+            switch (adv.type) {
+                case 'Equals':
+                    return n === t1;
+                case 'Does not equal':
+                    return n !== t1;
+                case 'Greater than':
+                    return n !== undefined && n > t1;
+                case 'Greater than or equal to':
+                    return n !== undefined && n >= t1;
+                case 'Less than':
+                    return n !== undefined && n < t1;
+                case 'Less than or equal to':
+                    return n !== undefined && n <= t1;
+                case 'Between':
+                    return (
+                        n !== undefined &&

Review Comment:
   In numeric/time advanced filters, `n` can be `undefined` (non-numeric) or 
`NaN` (invalid date from `new Date(...).getTime()`). The current `Does not 
equal` case (`return n !== t1`) will pass rows with `undefined`/`NaN`, and 
`Equals` will silently fail with `NaN`, which is likely not intended. Consider 
normalizing invalid numbers to `undefined` (e.g., treat `NaN` as `undefined`) 
and making `Equals`/`Does not equal` consistent about whether blanks should 
match.
   ```suggestion
               const nRaw =
                   column === 'time'
                       ? new Date(raw as string | number).getTime()
                       : this.toNumber(raw);
               const n = Number.isFinite(nRaw) ? nRaw : undefined;
               const t1Raw = Number(adv.value);
               const t1 = Number.isFinite(t1Raw) ? t1Raw : undefined;
               const t2Raw = Number(adv.value2);
               const t2 = Number.isFinite(t2Raw) ? t2Raw : undefined;
               switch (adv.type) {
                   case 'Equals':
                       return n !== undefined && t1 !== undefined && n === t1;
                   case 'Does not equal':
                       return n !== undefined && t1 !== undefined && n !== t1;
                   case 'Greater than':
                       return n !== undefined && t1 !== undefined && n > t1;
                   case 'Greater than or equal to':
                       return n !== undefined && t1 !== undefined && n >= t1;
                   case 'Less than':
                       return n !== undefined && t1 !== undefined && n < t1;
                   case 'Less than or equal to':
                       return n !== undefined && t1 !== undefined && n <= t1;
                   case 'Between':
                       return (
                           n !== undefined &&
                           t1 !== undefined &&
                           t2 !== undefined &&
   ```



##########
ui/src/app/chart-shared/components/charts/table/table-widget.component.ts:
##########
@@ -325,131 +666,195 @@ export class TableWidgetComponent extends 
BaseDataExplorerWidgetDirective<TableW
     }
 
     private filterRows(rows: TableRow[]): TableRow[] {
-        const searchTerm = (
+        let result = [...rows];
+        const search = (
             this.dataExplorerWidget.visualizationConfig.searchValue ?? ''
         )
             .trim()
             .toLowerCase();
-
-        if (!searchTerm) {
-            return [...rows];
+        if (search) {
+            result = result.filter(row =>
+                this.columnNames.some(c =>
+                    String(this.formatCellValue(c, row[c]))
+                        .toLowerCase()
+                        .includes(search),
+                ),
+            );
         }
-
-        return rows.filter(row =>
-            this.columnNames.some(column =>
-                String(this.formatCellValue(column, row[column]))
-                    .toLowerCase()
-                    .includes(searchTerm),
-            ),
-        );
+        for (const col of this.columnNames) {
+            const f = this.columnFilters[col];
+            if (f && f.size < this.getAllUniqueValues(col).length) {
+                result = result.filter(row =>
+                    f.has(this.formatForFilter(row, col)),
+                );
+            }
+            const adv = this.advancedFilters[col];
+            if (adv) result = this.applyAdvancedFilterToRows(result, col, adv);
+        }
+        return result;
     }
 
-    private sortRows(rows: TableRow[]): TableRow[] {
-        if (!this.sortColumn || this.sortDirection === '') {
-            return [...rows];
+    private applyAdvancedFilterToRows(
+        rows: TableRow[],
+        column: string,
+        adv: AdvancedFilter,
+    ): TableRow[] {
+        if (adv.type === 'Top 10') {
+            const top = rows
+                .map(r => ({ r, n: this.toNumber(r[column]) }))
+                .filter(
+                    (e): e is { r: TableRow; n: number } => e.n !== undefined,
+                )
+                .sort((a, b) => b.n - a.n)
+                .slice(0, 10);
+            const set = new Set(top.map(e => e.r));
+            return rows.filter(r => set.has(r));
         }
+        if (adv.type === 'Above average' || adv.type === 'Below average') {
+            const nums = rows
+                .map(r => this.toNumber(r[column]))
+                .filter((n): n is number => n !== undefined);
+            if (!nums.length) return rows;
+            const avg = nums.reduce((s, n) => s + n, 0) / nums.length;
+            return rows.filter(r => {
+                const n = this.toNumber(r[column]);
+                return (
+                    n !== undefined &&
+                    (adv.type === 'Above average' ? n > avg : n < avg)
+                );
+            });
+        }

Review Comment:
   Advanced filter options treat the `time` column as numeric, but `Top 
10`/`Above average`/`Below average` are implemented using 
`toNumber(r[column])`. For typical timestamp values (ISO strings, Date, etc.) 
`toNumber` returns `undefined`, so these filters effectively do nothing for 
`time` even though they are offered in the UI. Consider handling `time` 
explicitly (convert to `Date.getTime()` consistently) in these branches.



##########
ui/src/app/chart-shared/components/charts/table/table-widget.component.html:
##########
@@ -56,22 +57,46 @@
                                             isHighlightedColumn(column),
                                     }"
                                 >
-                                    <button
-                                        type="button"
-                                        class="sort-trigger"
-                                        (click)="sortBy(column)"
-                                    >
-                                        <span>
-                                            @if (column === 'time') {
-                                                {{ 'Time' | translate }}
-                                            } @else {
-                                                {{ headerLabel(column) }}
-                                            }
-                                        </span>
-                                        <mat-icon class="sort-icon">
-                                            {{ sortIcon(column) }}
-                                        </mat-icon>
-                                    </button>
+                                    <div class="th-inner">
+                                        <button
+                                            type="button"
+                                            class="sort-trigger"
+                                            (click)="sortBy(column)"
+                                        >
+                                            <span>
+                                                @if (column === 'time') {
+                                                    {{ 'Time' | translate }}
+                                                } @else {
+                                                    {{ headerLabel(column) }}
+                                                }
+                                            </span>
+                                            <mat-icon class="sort-icon">
+                                                {{ sortIcon(column) }}
+                                            </mat-icon>
+                                        </button>
+                                        <button
+                                            type="button"
+                                            class="column-filter-trigger"
+                                            [ngClass]="{
+                                                'filter-active':
+                                                    hasActiveFilter(column),
+                                                'filter-open':
+                                                    isColumnFilterOpen(column),
+                                            }"
+                                            (click)="
+                                                toggleColumnFilter(
+                                                    column,
+                                                    $event
+                                                )
+                                            "
+                                        >
+                                            <mat-icon
+                                                class="column-filter-icon"
+                                            >
+                                                filter_list
+                                            </mat-icon>
+                                        </button>

Review Comment:
   The filter icon button is icon-only and currently has no accessible name. 
Add an `aria-label`/`[attr.aria-label]` (and ideally a tooltip) so screen 
readers can announce what the button does (e.g., "Filter column").



##########
ui/src/app/chart-shared/components/charts/table/table-widget.component.scss:
##########
@@ -47,11 +47,10 @@
 }
 
 .analytics-table th {
-    position: sticky;
-    top: 0;
-    z-index: 2;
+    position: relative;
     padding: 0;
     background: var(--color-bg-0);
+    overflow: visible;
 }

Review Comment:
   Changing `.analytics-table th` from `position: sticky; top: 0; z-index: ...` 
to `position: relative` removes the sticky table header behavior. Since the 
table body is inside an overflow scroll container, this is likely a regression 
in usability (header no longer stays visible while scrolling). Consider 
restoring `position: sticky` and adjusting stacking/overflow rules so the 
dropdown can render correctly without sacrificing the sticky header.



##########
ui/src/app/chart-shared/components/charts/table/table-widget.component.ts:
##########
@@ -161,14 +228,304 @@ export class TableWidgetComponent extends 
BaseDataExplorerWidgetDirective<TableW
             this.sortDirection = 'asc';
         } else if (this.sortDirection === 'asc') {
             this.sortDirection = 'desc';
-        } else if (this.sortDirection === 'desc') {
+        } else {
             this.sortDirection = '';
             this.sortColumn = '';
+        }
+        this.applyTableState(false);
+    }
+
+    toggleColumnFilter(column: string, event: MouseEvent): void {
+        event.stopPropagation();
+        if (this.openFilterColumn === column) {
+            this.closeFilter();
+            return;
+        }
+        this.openFilterColumn = column;
+        this.showAdvancedPanel = null;
+        this.columnSearchTerms[column] ??= '';
+        const rect = (
+            event.currentTarget as HTMLElement
+        ).getBoundingClientRect();
+        const root = this.elRef.nativeElement.getBoundingClientRect();
+        const leftRel = rect.left - root.left;
+        const finalLeft =
+            root.right - rect.left >= DROPDOWN_MAX_WIDTH + 
DROPDOWN_EDGE_PADDING
+                ? leftRel
+                : Math.max(
+                      0,
+                      leftRel -
+                          (DROPDOWN_MAX_WIDTH - rect.width) -
+                          DROPDOWN_EDGE_PADDING,
+                  );
+        this.dropdownStyle = {
+            'position': 'absolute',
+            'top': `${rect.bottom - root.top}px`,
+            'left': `${finalLeft}px`,
+            'z-index': '9999',
+        };
+    }
+
+    isColumnFilterOpen = (column: string): boolean =>
+        this.openFilterColumn === column;
+
+    hasActiveFilter(column: string): boolean {
+        if (this.advancedFilters[column]) return true;
+        const f = this.columnFilters[column];
+        return !!f && f.size < this.getAllUniqueValues(column).length;
+    }
+
+    getAllUniqueValues = (column: string): string[] =>
+        this.extractUniqueValues(this.rows, column);
+
+    getVisibleUniqueValues(column: string): string[] {
+        let baseRows = this.getRowsFilteredByOtherColumns(column);
+        const adv = this.advancedFilters[column];
+        if (adv)
+            baseRows = this.applyAdvancedFilterToRows(baseRows, column, adv);
+        return this.extractUniqueValues(baseRows, column);
+    }
+
+    getLivePreviewValues(column: string): string[] {
+        let baseRows = this.getRowsFilteredByOtherColumns(column);
+        const adv = this.advancedFilters[column];
+        if (adv)
+            baseRows = this.applyAdvancedFilterToRows(baseRows, column, adv);
+        if (
+            this.showAdvancedPanel === column &&
+            this.selectedAdvancedType &&
+            (this.advancedInputValue ||
+                !this.needsInput(this.selectedAdvancedType))
+        ) {
+            baseRows = this.applyAdvancedFilterToRows(baseRows, column, {
+                type: this.selectedAdvancedType,
+                value: this.advancedInputValue,
+                value2: this.advancedInputValue2,
+            });
+        }
+        return this.extractUniqueValues(baseRows, column);
+    }
+
+    getFilteredUniqueValues(column: string): string[] {
+        const term = (this.columnSearchTerms[column] ?? '')
+            .trim()
+            .toLowerCase();
+        const values =
+            this.showAdvancedPanel === column
+                ? this.getLivePreviewValues(column)
+                : this.getVisibleUniqueValues(column);
+        return term
+            ? values.filter(v => v.toLowerCase().includes(term))
+            : values;
+    }
+
+    isValueChecked = (column: string, value: string): boolean =>
+        this.columnFilters[column]?.has(value) ?? true;
+
+    toggleValue(column: string, value: string): void {
+        this.ensureColumnFilter(column);
+        const f = this.columnFilters[column];
+        if (f.has(value)) {
+            f.delete(value);
         } else {
-            this.sortDirection = 'asc';
+            f.add(value);
         }
+        this.applyTableState(true);
+    }
 
-        this.applyTableState(false);
+    areAllValuesSelected(column: string): boolean {
+        const f = this.columnFilters[column];
+        if (!f) return true;
+        return this.getAllUniqueValues(column).every(v => f.has(v));
+    }
+
+    toggleAllValues(column: string): void {
+        this.ensureColumnFilter(column);
+        const f = this.columnFilters[column];
+        const all = this.getAllUniqueValues(column);
+        const allSelected = all.every(v => f.has(v));
+        all.forEach(v => (allSelected ? f.delete(v) : f.add(v)));
+        this.applyTableState(true);
+    }
+
+    hasSearchOrAdvanced = (column: string): boolean =>
+        !!this.columnSearchTerms[column]?.trim() ||
+        this.showAdvancedPanel === column;
+
+    areDisplayedValuesSelected(column: string): boolean {
+        const f = this.columnFilters[column];
+        if (!f) return true;
+        return this.getFilteredUniqueValues(column).every(v => f.has(v));
+    }
+
+    toggleDisplayedValues(column: string): void {
+        this.ensureColumnFilter(column);
+        const f = this.columnFilters[column];
+        const displayed = this.getFilteredUniqueValues(column);
+        displayed.forEach(v => f.add(v));
+        this.applyTableState(true);
+    }
+
+    onColumnSearchChange(column: string, term: string): void {
+        this.columnSearchTerms[column] = term;
+    }
+
+    clearColumnFilter(column: string): void {
+        delete this.columnFilters[column];
+        delete this.advancedFilters[column];
+        this.columnSearchTerms[column] = '';
+        this.showAdvancedPanel = null;
+        this.applyTableState(true);
+    }
+
+    onSearchKeydown(event: KeyboardEvent): void {
+        if (event.key === 'Enter') {
+            event.preventDefault();
+            this.closeFilter();
+        }
+    }
+
+    onAdvancedInputKeydown(
+        event: KeyboardEvent,
+        column: string,
+        inputIndex: number,
+    ): void {
+        if (event.key !== 'Enter') return;
+        event.preventDefault();
+        if (
+            inputIndex === 1 &&
+            this.needsSecondInput(this.selectedAdvancedType)
+        ) {
+            this.elRef.nativeElement
+                .querySelectorAll('.advanced-input')?.[1]
+                ?.focus();
+        } else {
+            this.applyAdvancedFilter(column);
+        }
+    }
+
+    onFilterDropdownClick = (event: MouseEvent): void =>
+        event.stopPropagation();
+
+    getAdvancedFilterOptions = (column: string): string[] =>
+        this.isNumericColumn(column) || column === 'time'
+            ? NUMERIC_FILTER_OPTIONS
+            : TEXT_FILTER_OPTIONS;
+
+    getAdvancedFilterLabel = (column: string): string =>
+        this.isNumericColumn(column) || column === 'time'
+            ? 'Number Filters'
+            : 'Text Filters';
+
+    toggleAdvancedPanel(column: string, event: MouseEvent): void {
+        event.stopPropagation();
+        if (this.showAdvancedPanel === column) {
+            this.showAdvancedPanel = null;
+            return;
+        }
+        this.showAdvancedPanel = column;
+        const existing = this.advancedFilters[column];
+        this.selectedAdvancedType = existing?.type ?? '';
+        this.advancedInputValue = existing?.value ?? '';
+        this.advancedInputValue2 = existing?.value2 ?? '';
+    }
+
+    selectAdvancedType(type: string): void {
+        this.selectedAdvancedType = type;
+        this.advancedInputValue = '';
+        this.advancedInputValue2 = '';
+    }
+
+    needsInput = (type: string): boolean => !NO_INPUT_TYPES.has(type);
+    needsSecondInput = (type: string): boolean => type === 'Between';
+
+    applyAdvancedFilter(column: string): void {
+        this.advancedFilters[column] = {
+            type: this.selectedAdvancedType,
+            value: this.advancedInputValue,
+            value2: this.advancedInputValue2,
+        };
+        this.showAdvancedPanel = null;
+        this.applyTableState(true);
+    }

Review Comment:
   `applyAdvancedFilter` stores the selected type/value without validating 
required inputs. For numeric filters (including `Between`) an empty 
`advancedInputValue`/`advancedInputValue2` will be coerced to `0` via 
`Number('')`, leading to incorrect filtering and showing an active filter even 
though the user didn't enter a value. Consider disabling the OK button until 
required inputs are present, or validating and not applying the filter when 
inputs are missing/invalid.



##########
ui/package-lock.json:
##########
@@ -377,40 +377,6 @@
         }
       }
     },
-    "node_modules/@angular-devkit/architect/node_modules/chokidar": {
-      "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz";,
-      "integrity": 
"sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "peer": true,
-      "dependencies": {
-        "readdirp": "^4.0.1"
-      },
-      "engines": {
-        "node": ">= 14.16.0"
-      },
-      "funding": {
-        "url": "https://paulmillr.com/funding/";
-      }
-    },
-    "node_modules/@angular-devkit/architect/node_modules/readdirp": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz";,
-      "integrity": 
"sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "peer": true,
-      "engines": {
-        "node": ">= 14.18.0"
-      },
-      "funding": {
-        "type": "individual",
-        "url": "https://paulmillr.com/funding/";
-      }
-    },
     "node_modules/@angular-devkit/schematics": {

Review Comment:
   `ui/package-lock.json` has a large set of dependency entry removals, but 
this PR doesn't appear to change any npm dependencies (the feature uses 
existing Angular/Material modules). This makes the diff noisy and can cause 
unnecessary lockfile churn/merge conflicts. Consider reverting the lockfile 
changes or documenting why a lockfile regeneration is required (and ensuring it 
was generated with the repo's expected npm version).



##########
ui/src/app/chart-shared/components/charts/table/table-widget.component.ts:
##########
@@ -161,14 +228,304 @@ export class TableWidgetComponent extends 
BaseDataExplorerWidgetDirective<TableW
             this.sortDirection = 'asc';
         } else if (this.sortDirection === 'asc') {
             this.sortDirection = 'desc';
-        } else if (this.sortDirection === 'desc') {
+        } else {
             this.sortDirection = '';
             this.sortColumn = '';
+        }
+        this.applyTableState(false);
+    }
+
+    toggleColumnFilter(column: string, event: MouseEvent): void {
+        event.stopPropagation();
+        if (this.openFilterColumn === column) {
+            this.closeFilter();
+            return;
+        }
+        this.openFilterColumn = column;
+        this.showAdvancedPanel = null;
+        this.columnSearchTerms[column] ??= '';
+        const rect = (
+            event.currentTarget as HTMLElement
+        ).getBoundingClientRect();
+        const root = this.elRef.nativeElement.getBoundingClientRect();
+        const leftRel = rect.left - root.left;
+        const finalLeft =
+            root.right - rect.left >= DROPDOWN_MAX_WIDTH + 
DROPDOWN_EDGE_PADDING
+                ? leftRel
+                : Math.max(
+                      0,
+                      leftRel -
+                          (DROPDOWN_MAX_WIDTH - rect.width) -
+                          DROPDOWN_EDGE_PADDING,
+                  );
+        this.dropdownStyle = {
+            'position': 'absolute',
+            'top': `${rect.bottom - root.top}px`,
+            'left': `${finalLeft}px`,
+            'z-index': '9999',
+        };
+    }
+
+    isColumnFilterOpen = (column: string): boolean =>
+        this.openFilterColumn === column;
+
+    hasActiveFilter(column: string): boolean {
+        if (this.advancedFilters[column]) return true;
+        const f = this.columnFilters[column];
+        return !!f && f.size < this.getAllUniqueValues(column).length;
+    }
+
+    getAllUniqueValues = (column: string): string[] =>
+        this.extractUniqueValues(this.rows, column);
+

Review Comment:
   `getAllUniqueValues` recomputes and sorts unique values from `this.rows` 
each time it's called. It's invoked from template bindings (`hasActiveFilter`, 
`areAllValuesSelected`) and inside filtering loops, which can become expensive 
on large datasets (repeated O(rows * columns) scans during change detection). 
Consider caching unique values/lengths per column when `rows` change (e.g., in 
`onDataReceived`) and reusing the cached results.



##########
ui/src/app/chart-shared/components/charts/table/table-widget.component.html:
##########
@@ -154,4 +179,170 @@
             </mat-paginator>
         </div>
     }
+    @if (openFilterColumn) {
+        <div
+            class="column-filter-dropdown"
+            [ngStyle]="dropdownStyle"
+            (click)="onFilterDropdownClick($event)"
+        >
+            @if (hasActiveFilter(openFilterColumn)) {
+                <button
+                    type="button"
+                    class="clear-filter-btn"
+                    (click)="clearColumnFilter(openFilterColumn)"
+                >
+                    <mat-icon class="clear-filter-icon"
+                        >filter_list_off</mat-icon
+                    >
+                    Clear Filter
+                </button>
+            }
+            <button
+                type="button"
+                class="advanced-filter-btn"
+                (click)="toggleAdvancedPanel(openFilterColumn, $event)"
+            >
+                {{ getAdvancedFilterLabel(openFilterColumn) }}

Review Comment:
   Several new UI strings are hard-coded and bypass i18n (e.g., "Clear Filter", 
"Search...", "(Select All)", "OK", "Cancel", "Clear", "Value..."). This 
component already uses `TranslatePipe` for other text, so these should also be 
moved to translation keys/pipes (including placeholders/aria-labels) to keep 
localization consistent.



##########
ui/src/app/chart-shared/components/charts/table/table-widget.component.ts:
##########
@@ -161,14 +228,304 @@ export class TableWidgetComponent extends 
BaseDataExplorerWidgetDirective<TableW
             this.sortDirection = 'asc';
         } else if (this.sortDirection === 'asc') {
             this.sortDirection = 'desc';
-        } else if (this.sortDirection === 'desc') {
+        } else {
             this.sortDirection = '';
             this.sortColumn = '';
+        }
+        this.applyTableState(false);
+    }
+
+    toggleColumnFilter(column: string, event: MouseEvent): void {
+        event.stopPropagation();
+        if (this.openFilterColumn === column) {
+            this.closeFilter();
+            return;
+        }
+        this.openFilterColumn = column;
+        this.showAdvancedPanel = null;
+        this.columnSearchTerms[column] ??= '';
+        const rect = (
+            event.currentTarget as HTMLElement
+        ).getBoundingClientRect();
+        const root = this.elRef.nativeElement.getBoundingClientRect();
+        const leftRel = rect.left - root.left;
+        const finalLeft =
+            root.right - rect.left >= DROPDOWN_MAX_WIDTH + 
DROPDOWN_EDGE_PADDING
+                ? leftRel
+                : Math.max(
+                      0,
+                      leftRel -
+                          (DROPDOWN_MAX_WIDTH - rect.width) -
+                          DROPDOWN_EDGE_PADDING,
+                  );
+        this.dropdownStyle = {
+            'position': 'absolute',
+            'top': `${rect.bottom - root.top}px`,
+            'left': `${finalLeft}px`,
+            'z-index': '9999',
+        };
+    }
+
+    isColumnFilterOpen = (column: string): boolean =>
+        this.openFilterColumn === column;
+
+    hasActiveFilter(column: string): boolean {
+        if (this.advancedFilters[column]) return true;
+        const f = this.columnFilters[column];
+        return !!f && f.size < this.getAllUniqueValues(column).length;
+    }
+
+    getAllUniqueValues = (column: string): string[] =>
+        this.extractUniqueValues(this.rows, column);
+
+    getVisibleUniqueValues(column: string): string[] {
+        let baseRows = this.getRowsFilteredByOtherColumns(column);
+        const adv = this.advancedFilters[column];
+        if (adv)
+            baseRows = this.applyAdvancedFilterToRows(baseRows, column, adv);
+        return this.extractUniqueValues(baseRows, column);
+    }
+
+    getLivePreviewValues(column: string): string[] {
+        let baseRows = this.getRowsFilteredByOtherColumns(column);
+        const adv = this.advancedFilters[column];
+        if (adv)
+            baseRows = this.applyAdvancedFilterToRows(baseRows, column, adv);
+        if (
+            this.showAdvancedPanel === column &&
+            this.selectedAdvancedType &&
+            (this.advancedInputValue ||
+                !this.needsInput(this.selectedAdvancedType))
+        ) {
+            baseRows = this.applyAdvancedFilterToRows(baseRows, column, {
+                type: this.selectedAdvancedType,
+                value: this.advancedInputValue,
+                value2: this.advancedInputValue2,
+            });
+        }
+        return this.extractUniqueValues(baseRows, column);
+    }
+
+    getFilteredUniqueValues(column: string): string[] {
+        const term = (this.columnSearchTerms[column] ?? '')
+            .trim()
+            .toLowerCase();
+        const values =
+            this.showAdvancedPanel === column
+                ? this.getLivePreviewValues(column)
+                : this.getVisibleUniqueValues(column);
+        return term
+            ? values.filter(v => v.toLowerCase().includes(term))
+            : values;
+    }
+
+    isValueChecked = (column: string, value: string): boolean =>
+        this.columnFilters[column]?.has(value) ?? true;
+
+    toggleValue(column: string, value: string): void {
+        this.ensureColumnFilter(column);
+        const f = this.columnFilters[column];
+        if (f.has(value)) {
+            f.delete(value);
         } else {
-            this.sortDirection = 'asc';
+            f.add(value);
         }
+        this.applyTableState(true);
+    }
 
-        this.applyTableState(false);
+    areAllValuesSelected(column: string): boolean {
+        const f = this.columnFilters[column];
+        if (!f) return true;
+        return this.getAllUniqueValues(column).every(v => f.has(v));
+    }
+
+    toggleAllValues(column: string): void {
+        this.ensureColumnFilter(column);
+        const f = this.columnFilters[column];
+        const all = this.getAllUniqueValues(column);
+        const allSelected = all.every(v => f.has(v));
+        all.forEach(v => (allSelected ? f.delete(v) : f.add(v)));
+        this.applyTableState(true);
+    }
+
+    hasSearchOrAdvanced = (column: string): boolean =>
+        !!this.columnSearchTerms[column]?.trim() ||
+        this.showAdvancedPanel === column;
+
+    areDisplayedValuesSelected(column: string): boolean {
+        const f = this.columnFilters[column];
+        if (!f) return true;
+        return this.getFilteredUniqueValues(column).every(v => f.has(v));
+    }
+
+    toggleDisplayedValues(column: string): void {
+        this.ensureColumnFilter(column);
+        const f = this.columnFilters[column];
+        const displayed = this.getFilteredUniqueValues(column);
+        displayed.forEach(v => f.add(v));

Review Comment:
   `toggleDisplayedValues` only adds the currently displayed values to the 
filter set. Since the checkbox in the UI behaves like a toggle, clicking it 
when all displayed values are already selected should also be able to 
deselect/uncheck them (e.g., remove the displayed values or toggle based on 
`areDisplayedValuesSelected`).
   ```suggestion
           const allDisplayedSelected = displayed.every(v => f.has(v));
           displayed.forEach(v => (allDisplayedSelected ? f.delete(v) : 
f.add(v)));
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to