diff --git a/packages/components/package.json b/packages/components/package.json index a99475c364a..6d925b0c996 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -154,6 +154,7 @@ "./components/hds/advanced-table/th-button-sort.js": "./dist/_app_/components/hds/advanced-table/th-button-sort.js", "./components/hds/advanced-table/th-button-tooltip.js": "./dist/_app_/components/hds/advanced-table/th-button-tooltip.js", "./components/hds/advanced-table/th-context-menu.js": "./dist/_app_/components/hds/advanced-table/th-context-menu.js", + "./components/hds/advanced-table/th-filter-menu.js": "./dist/_app_/components/hds/advanced-table/th-filter-menu.js", "./components/hds/advanced-table/th-reorder-drop-target.js": "./dist/_app_/components/hds/advanced-table/th-reorder-drop-target.js", "./components/hds/advanced-table/th-reorder-handle.js": "./dist/_app_/components/hds/advanced-table/th-reorder-handle.js", "./components/hds/advanced-table/th-resize-handle.js": "./dist/_app_/components/hds/advanced-table/th-resize-handle.js", diff --git a/packages/components/src/components/hds/advanced-table/index.hbs b/packages/components/src/components/hds/advanced-table/index.hbs index 8f8bae33e58..87569ba1513 100644 --- a/packages/components/src/components/hds/advanced-table/index.hbs +++ b/packages/components/src/components/hds/advanced-table/index.hbs @@ -3,6 +3,16 @@ SPDX-License-Identifier: MPL-2.0 }} +{{#if this.hasActiveFilters}} + +{{/if}}
{{column.label}} diff --git a/packages/components/src/components/hds/advanced-table/index.ts b/packages/components/src/components/hds/advanced-table/index.ts index 4be54225400..e6da28fec8f 100644 --- a/packages/components/src/components/hds/advanced-table/index.ts +++ b/packages/components/src/components/hds/advanced-table/index.ts @@ -29,6 +29,8 @@ import type { HdsAdvancedTableModel, HdsAdvancedTableExpandState, HdsAdvancedTableColumnReorderCallback, + HdsAdvancedTableFilter, + HdsAdvancedTableFilters, } from './types.ts'; import type HdsAdvancedTableColumnType from './models/column.ts'; import type { HdsFormCheckboxBaseSignature } from '../form/checkbox/base.ts'; @@ -149,12 +151,15 @@ export interface HdsAdvancedTableSignature { hasStickyFirstColumn?: boolean; childrenKey?: string; maxHeight?: string; + filters?: HdsAdvancedTableFilters; + isLiveFilter?: boolean; onColumnReorder?: HdsAdvancedTableColumnReorderCallback; onColumnResize?: (columnKey: string, newWidth?: string) => void; onSelectionChange?: ( selection: HdsAdvancedTableOnSelectionChangeSignature ) => void; onSort?: (sortBy: string, sortOrder: HdsAdvancedTableThSortOrder) => void; + onFilter?: (filters: HdsAdvancedTableFilters) => void; }; Blocks: { body?: [ @@ -222,6 +227,8 @@ export default class HdsAdvancedTable extends Component 0; constructor(owner: Owner, args: HdsAdvancedTableSignature['Args']) { super(owner, args); @@ -698,6 +705,30 @@ export default class HdsAdvancedTable extends Component 0; + } } diff --git a/packages/components/src/components/hds/advanced-table/models/column.ts b/packages/components/src/components/hds/advanced-table/models/column.ts index c5c038874aa..48cf3ad2ecb 100644 --- a/packages/components/src/components/hds/advanced-table/models/column.ts +++ b/packages/components/src/components/hds/advanced-table/models/column.ts @@ -13,7 +13,9 @@ import type { HdsDropdownToggleButtonSignature } from '../../dropdown/toggle/but import type { HdsAdvancedTableCell, HdsAdvancedTableHorizontalAlignment, + HdsAdvancedTableFilterOption, HdsAdvancedTableColumn as HdsAdvancedTableColumnType, + HdsAdvancedTableFilterType, } from '../types'; export const DEFAULT_WIDTH = '1fr'; // default to '1fr' to allow flexible width @@ -51,6 +53,8 @@ export default class HdsAdvancedTableColumn { @tracked thContextMenuToggleElement?: HdsDropdownToggleButtonSignature['Element'] = undefined; + @tracked filterOptions?: HdsAdvancedTableFilterOption[] = undefined; + @tracked filterType?: HdsAdvancedTableFilterType = undefined; // width properties @tracked transientWidth?: `${number}px` = undefined; // used for transient width changes @@ -165,6 +169,8 @@ export default class HdsAdvancedTableColumn { this.tooltip = column.tooltip; this._setWidthValues(column); this.sortingFunction = column.sortingFunction; + this.filterOptions = column.filterOptions; + this.filterType = column.filterType; } // main collection function diff --git a/packages/components/src/components/hds/advanced-table/th-filter-menu.hbs b/packages/components/src/components/hds/advanced-table/th-filter-menu.hbs new file mode 100644 index 00000000000..abb39eaaca7 --- /dev/null +++ b/packages/components/src/components/hds/advanced-table/th-filter-menu.hbs @@ -0,0 +1,44 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + + + {{#each @column.filterOptions as |option|}} + {{#if (eq @column.filterType "radio")}} + + {{option.label}} + + {{else}} + + {{option.label}} + + {{/if}} + {{/each}} + {{#unless @isLiveFilter}} + + + + + + + {{/unless}} + \ No newline at end of file diff --git a/packages/components/src/components/hds/advanced-table/th-filter-menu.ts b/packages/components/src/components/hds/advanced-table/th-filter-menu.ts new file mode 100644 index 00000000000..0e5300f3520 --- /dev/null +++ b/packages/components/src/components/hds/advanced-table/th-filter-menu.ts @@ -0,0 +1,135 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import { modifier } from 'ember-modifier'; + +import type HdsAdvancedTableColumn from './models/column.ts'; +import type { HdsDropdownSignature } from '../dropdown/index.ts'; +import type { + HdsAdvancedTableFilter, + HdsAdvancedTableFilters, +} from './types.ts'; + +export interface HdsAdvancedTableThFilterMenuSignature { + Args: { + column: HdsAdvancedTableColumn; + filters?: HdsAdvancedTableFilters; + isLiveFilter?: boolean; + onFilter?: ( + key: string, + keyFilter?: HdsAdvancedTableFilter | HdsAdvancedTableFilter[] + ) => void; + }; + Element: HdsDropdownSignature['Element']; +} + +export default class HdsAdvancedTableThFilterMenu extends Component { + @tracked internalFilters: + | HdsAdvancedTableFilter[] + | HdsAdvancedTableFilter + | undefined = []; + @tracked hasActiveFilters: boolean = this.keyFilter !== undefined; + + private _updateInternalFilters = modifier(() => { + this.internalFilters = this.keyFilter; + }); + + get keyFilter(): + | HdsAdvancedTableFilter[] + | HdsAdvancedTableFilter + | undefined { + const { filters, column } = this.args; + + if (!filters || !column) { + return undefined; + } + return filters[column.key]; + } + + @action + onFilter(event: Event): void { + const addFilter = (value: unknown): HdsAdvancedTableFilter[] => { + const newFilter = { + text: value as string, + value: value, + }; + if ( + Array.isArray(this.internalFilters) && + input.classList.contains('hds-form-checkbox') + ) { + this.internalFilters.push(newFilter); + return this.internalFilters; + } else { + return [newFilter]; + } + }; + + const removeFilter = (value: string): HdsAdvancedTableFilter[] => { + const newFilter = [] as HdsAdvancedTableFilter[]; + if (Array.isArray(this.internalFilters)) { + this.internalFilters.forEach((filter) => { + if (filter.value != value) { + newFilter.push(filter); + } + }); + } + return newFilter; + }; + + const input = event.target as HTMLInputElement; + + let newFilter = [] as HdsAdvancedTableFilter[]; + + if (input.checked) { + newFilter = addFilter(input.value); + } else { + newFilter = removeFilter(input.value); + } + + this.internalFilters = newFilter; + + if (this.args.isLiveFilter) { + const { onFilter, column } = this.args; + if (onFilter && typeof onFilter === 'function') { + if (newFilter.length === 0) { + onFilter(column?.key, undefined); + } else { + onFilter(column?.key, newFilter); + } + this.hasActiveFilters = newFilter != undefined && newFilter.length > 0; + } + } + } + + @action + onApply(): void { + const { onFilter, column } = this.args; + if (onFilter && typeof onFilter === 'function') { + onFilter(column?.key, this.internalFilters); + } + } + + @action + onClear(): void { + this.internalFilters = []; + + const { onFilter, column } = this.args; + if (onFilter && typeof onFilter === 'function') { + onFilter(column?.key, this.internalFilters); + } + } + + private _isChecked = (value: string): boolean => { + if (Array.isArray(this.internalFilters)) { + return this.internalFilters.some((filter) => filter.value === value); + } else if (this.internalFilters && value) { + return this.internalFilters.value === value; + } + return false; + }; +} diff --git a/packages/components/src/components/hds/advanced-table/th-sort.hbs b/packages/components/src/components/hds/advanced-table/th-sort.hbs index 6606f130bc7..4c2b366e2bb 100644 --- a/packages/components/src/components/hds/advanced-table/th-sort.hbs +++ b/packages/components/src/components/hds/advanced-table/th-sort.hbs @@ -34,6 +34,9 @@ {{#if @tooltip}} {{/if}} + {{#if (gt this.numFilters 0)}} + + {{/if}}
@@ -44,6 +47,15 @@ /> {{#if @column}} + {{#if @column.filterOptions}} + + {{/if}} + void; onReorderDragEnd?: () => void; @@ -59,6 +63,10 @@ export interface HdsAdvancedTableThSortSignature { column: HdsAdvancedTableColumn, side: HdsAdvancedTableColumnReorderSide ) => void; + onFilter?: ( + key: string, + keyFilter?: HdsAdvancedTableFilter[] | HdsAdvancedTableFilter + ) => void; }; Blocks: { default?: []; @@ -113,6 +121,22 @@ export default class HdsAdvancedTableThSort extends Component item['project-name'])), + ).map((value) => ({ value, label: value })), + 'run-status': Array.from( + new Set(SAMPLE_MODEL.map((item) => item['run-status'])), + ).map((value) => ({ value, label: value })), + 'vcs-repo': Array.from( + new Set(SAMPLE_MODEL.map((item) => item['vcs-repo'])), + ).map((value) => ({ value, label: value })), + 'terraform-version': Array.from( + new Set(SAMPLE_MODEL.map((item) => item['terraform-version'])), + ).map((value) => ({ value, label: value })), + 'state-terraform-version': Array.from( + new Set(SAMPLE_MODEL.map((item) => item['state-terraform-version'])), + ).map((value) => ({ value, label: value })), +}; + +const SAMPLE_COLUMNS = [ + { + isSortable: true, + label: 'Name', + key: 'name', + width: 'max-content', + }, + { + label: 'Project name', + key: 'project-name', + isSortable: true, + width: 'max-content', + filterOptions: SAMPLE_MODEL_VALUES['project-name'], + filterType: 'checkbox', + }, + { + label: 'Current run ID', + key: 'current-run-id', + isSortable: true, + width: 'max-content', + }, + { + label: 'Run status', + key: 'run-status', + isSortable: true, + width: 'max-content', + filterOptions: SAMPLE_MODEL_VALUES['run-status'], + filterType: 'checkbox', + }, + { + label: 'Current run applied', + key: 'current-run-applied', + isSortable: true, + width: 'max-content', + }, + { + label: 'VCS repo', + key: 'vcs-repo', + isSortable: true, + width: 'max-content', + filterOptions: SAMPLE_MODEL_VALUES['vcs-repo'], + filterType: 'checkbox', + }, + { + label: 'Module count', + key: 'module-count', + isSortable: true, + width: 'max-content', + }, + { + label: 'Modules', + key: 'modules', + isSortable: true, + width: 'max-content', + }, + { + label: 'Provider count', + key: 'provider-count', + isSortable: true, + width: 'max-content', + }, + { + label: 'Providers', + key: 'providers', + isSortable: true, + width: 'max-content', + }, + { + label: 'Terraform version', + key: 'terraform-version', + isSortable: true, + width: 'max-content', + filterOptions: SAMPLE_MODEL_VALUES['terraform-version'], + filterType: 'radio', + }, + { + label: 'State terraform version', + key: 'state-terraform-version', + isSortable: true, + width: 'max-content', + filterOptions: SAMPLE_MODEL_VALUES['state-terraform-version'], + filterType: 'radio', + }, + { + label: 'Created', + key: 'created', + isSortable: true, + width: 'max-content', + }, + { + label: 'Updated', + key: 'updated', + isSortable: true, + width: 'max-content', + }, +]; + const updateModelWithSelectAllState = ( modelData: HdsAdvancedTableSignature['Args']['model'], selectAllState: boolean, @@ -499,6 +532,8 @@ export default class MockAppMainGenericAdvancedTable extends Component { + const filter = filters[name]; + if (!filter) return; + + if (Array.isArray(filter)) { + if (filter.length === 1) return filter[0]?.value; + return filter.map((f: HdsAdvancedTableSignature['Args']['filters'][]) => f.value); + } + return filter.value; + }; + + @action + onFilter(filters: HdsAdvancedTableSignature['Args']['filters']) { + this.filters = filters; + } + + get demoModelFilteredData() { + const filterItem = (item: HdsAdvancedTableSignature['Args']['filters']): boolean => { + if (Object.keys(this.filters).length === 0) return true; + let match = true; + Object.keys(this.filters).forEach((key) => { + const keyFilters = this.valuesFromFilter(this.filters, key); + if (Array.isArray(keyFilters)) { + if (!keyFilters.includes(item[key])) { + match = false; + } + } else if (item[key] !== keyFilters) { + match = false; + } + }); + return match; + }; + + return this.demoModel.filter(filterItem); + } + + @action + onLiveFilterToggle(event: Event) { + const target = event.target as HTMLInputElement; + this.isLiveFilter = target.checked; + } +