diff --git a/.changeset/dark-signs-enjoy.md b/.changeset/dark-signs-enjoy.md
new file mode 100644
index 00000000000..77af0799da9
--- /dev/null
+++ b/.changeset/dark-signs-enjoy.md
@@ -0,0 +1,16 @@
+---
+"@hashicorp/design-system-components": minor
+---
+
+
+`FilterBar` - Added new Filter Bar component
+
+
+
+`AdvancedTable` - Added support for filtering within the table with new `actions` named block and `FilterBar` contextual component
+
+
+
+`AdvancedTable` - Added argument `isEmpty` and named block `emptyState` for setting an empty state for the table
+
+
diff --git a/packages/components/package.json b/packages/components/package.json
index 6081e4dacc7..db08082ee95 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -235,6 +235,17 @@
"./components/hds/dropdown/toggle/button.js": "./dist/_app_/components/hds/dropdown/toggle/button.js",
"./components/hds/dropdown/toggle/chevron.js": "./dist/_app_/components/hds/dropdown/toggle/chevron.js",
"./components/hds/dropdown/toggle/icon.js": "./dist/_app_/components/hds/dropdown/toggle/icon.js",
+ "./components/hds/filter-bar/dropdown.js": "./dist/_app_/components/hds/filter-bar/dropdown.js",
+ "./components/hds/filter-bar/filter-group/checkbox.js": "./dist/_app_/components/hds/filter-bar/filter-group/checkbox.js",
+ "./components/hds/filter-bar/filter-group/date.js": "./dist/_app_/components/hds/filter-bar/filter-group/date.js",
+ "./components/hds/filter-bar/filter-group/generic.js": "./dist/_app_/components/hds/filter-bar/filter-group/generic.js",
+ "./components/hds/filter-bar/filter-group.js": "./dist/_app_/components/hds/filter-bar/filter-group.js",
+ "./components/hds/filter-bar/filter-group/numerical.js": "./dist/_app_/components/hds/filter-bar/filter-group/numerical.js",
+ "./components/hds/filter-bar/filter-group/radio.js": "./dist/_app_/components/hds/filter-bar/filter-group/radio.js",
+ "./components/hds/filter-bar.js": "./dist/_app_/components/hds/filter-bar.js",
+ "./components/hds/filter-bar/tabs.js": "./dist/_app_/components/hds/filter-bar/tabs.js",
+ "./components/hds/filter-bar/tabs/panel.js": "./dist/_app_/components/hds/filter-bar/tabs/panel.js",
+ "./components/hds/filter-bar/tabs/tab.js": "./dist/_app_/components/hds/filter-bar/tabs/tab.js",
"./components/hds/flyout.js": "./dist/_app_/components/hds/flyout.js",
"./components/hds/form/character-count.js": "./dist/_app_/components/hds/form/character-count.js",
"./components/hds/form/checkbox/base.js": "./dist/_app_/components/hds/form/checkbox/base.js",
diff --git a/packages/components/src/components.ts b/packages/components/src/components.ts
index fb1b5c4d9b8..e896eef62bb 100644
--- a/packages/components/src/components.ts
+++ b/packages/components/src/components.ts
@@ -130,6 +130,20 @@ export * from './components/hds/dropdown/list-item/types.ts';
export * from './components/hds/dropdown/toggle/types.ts';
export * from './components/hds/dropdown/types.ts';
+// FilterBar
+export { default as HdsFilterBar } from './components/hds/filter-bar/index.ts';
+export { default as HdsFilterBarDropdown } from './components/hds/filter-bar/dropdown.ts';
+export { default as HdsFilterBarFilterGroup } from './components/hds/filter-bar/filter-group/index.ts';
+export { default as HdsFilterBarFilterGroupCheckbox } from './components/hds/filter-bar/filter-group/checkbox.ts';
+export { default as HdsFilterBarFilterGroupDate } from './components/hds/filter-bar/filter-group/date.ts';
+export { default as HdsFilterBarFilterGroupGeneric } from './components/hds/filter-bar/filter-group/generic.ts';
+export { default as HdsFilterBarFilterGroupRadio } from './components/hds/filter-bar/filter-group/radio.ts';
+export { default as HdsFilterBarFilterGroupNumerical } from './components/hds/filter-bar/filter-group/numerical.ts';
+export { default as HdsFilterBarTabs } from './components/hds/filter-bar/tabs/index.ts';
+export { default as HdsFilterBarTabsPanel } from './components/hds/filter-bar/tabs/panel.ts';
+export { default as HdsFilterBarTabsTab } from './components/hds/filter-bar/tabs/tab.ts';
+export * from './components/hds/filter-bar/types.ts';
+
// Flyout
export { default as HdsFlyout } from './components/hds/flyout/index.ts';
export * from './components/hds/flyout/types.ts';
diff --git a/packages/components/src/components/hds/advanced-table/index.hbs b/packages/components/src/components/hds/advanced-table/index.hbs
index 45eb3875f77..3a8de93b50e 100644
--- a/packages/components/src/components/hds/advanced-table/index.hbs
+++ b/packages/components/src/components/hds/advanced-table/index.hbs
@@ -3,202 +3,236 @@
SPDX-License-Identifier: MPL-2.0
}}
-
\ No newline at end of file
diff --git a/packages/components/src/components/hds/advanced-table/index.ts b/packages/components/src/components/hds/advanced-table/index.ts
index a5bfae87784..61d49bf72a2 100644
--- a/packages/components/src/components/hds/advanced-table/index.ts
+++ b/packages/components/src/components/hds/advanced-table/index.ts
@@ -14,6 +14,7 @@ import HdsAdvancedTableTableModel from './models/table.ts';
import type Owner from '@ember/owner';
import type { WithBoundArgs } from '@glint/template';
+import type { ComponentLike } from '@glint/template';
import {
HdsAdvancedTableDensityValues,
HdsAdvancedTableVerticalAlignmentValues,
@@ -30,6 +31,7 @@ import type {
HdsAdvancedTableExpandState,
HdsAdvancedTableColumnReorderCallback,
} from './types.ts';
+import type { HdsFilterBarSignature } from '../filter-bar/index.ts';
import type HdsAdvancedTableColumnType from './models/column.ts';
import type { HdsFormCheckboxBaseSignature } from '../form/checkbox/base.ts';
import type HdsAdvancedTableTd from './td.ts';
@@ -149,6 +151,7 @@ export interface HdsAdvancedTableSignature {
hasStickyFirstColumn?: boolean;
childrenKey?: string;
maxHeight?: string;
+ isEmpty?: boolean;
onColumnReorder?: HdsAdvancedTableColumnReorderCallback;
onColumnResize?: (columnKey: string, newWidth?: string) => void;
onSelectionChange?: (
@@ -157,6 +160,11 @@ export interface HdsAdvancedTableSignature {
onSort?: (sortBy: string, sortOrder: HdsAdvancedTableThSortOrder) => void;
};
Blocks: {
+ actions?: [
+ {
+ FilterBar?: ComponentLike;
+ },
+ ];
body?: [
{
Td?: WithBoundArgs;
@@ -192,6 +200,7 @@ export interface HdsAdvancedTableSignature {
isOpen?: HdsAdvancedTableExpandState;
},
];
+ emptyState?: [];
};
Element: HTMLDivElement;
}
@@ -259,6 +268,14 @@ export default class HdsAdvancedTable extends Component
+
+
+
+ {{yield
+ (hash
+ FilterGroup=(component
+ "hds/filter-bar/filter-group" tab=T.Tab panel=T.Panel onChange=this.onFilter filters=this.internalFilters
+ )
+ close=D.close
+ )
+ }}
+
+
+
+
+ {{#unless this.isLiveFilter}}
+
+ {{/unless}}
+
+
+
+
\ No newline at end of file
diff --git a/packages/components/src/components/hds/filter-bar/dropdown.ts b/packages/components/src/components/hds/filter-bar/dropdown.ts
new file mode 100644
index 00000000000..3a0b53a958f
--- /dev/null
+++ b/packages/components/src/components/hds/filter-bar/dropdown.ts
@@ -0,0 +1,141 @@
+/**
+ * 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 Owner from '@ember/owner';
+import type { WithBoundArgs } from '@glint/template';
+
+import HdsFilterBarFilterGroup from './filter-group/index.ts';
+import type { HdsFilterBarFilters, HdsFilterBarFilter } from './types.ts';
+
+import type { HdsDropdownSignature } from '../dropdown/index.ts';
+
+export interface HdsFilterBarDropdownSignature {
+ Args: {
+ filters: HdsFilterBarFilters;
+ isLiveFilter: boolean;
+ onFilter?: (filters: HdsFilterBarFilters) => void;
+ };
+ Blocks: {
+ default: [
+ {
+ FilterGroup?: WithBoundArgs<
+ typeof HdsFilterBarFilterGroup,
+ 'tab' | 'panel' | 'filters' | 'onChange'
+ >;
+ close: HdsDropdownSignature['Blocks']['default'][0]['close'];
+ },
+ ];
+ };
+ Element: HTMLDivElement;
+}
+
+export default class HdsFilterBarDropdown extends Component {
+ @tracked internalFilters: HdsFilterBarFilters = {};
+
+ constructor(owner: Owner, args: HdsFilterBarDropdownSignature['Args']) {
+ super(owner, args);
+
+ const { filters } = this.args;
+
+ if (filters) {
+ this.internalFilters = { ...filters };
+ }
+ }
+
+ private _syncFilters = modifier(
+ (_element, [_filters]: [HdsFilterBarFilters | undefined]) => {
+ if (_filters) {
+ this.internalFilters = _filters;
+ }
+ }
+ );
+
+ get isLiveFilter(): boolean {
+ return this.args.isLiveFilter || false;
+ }
+
+ @action
+ onFilter(key: string, keyFilter?: HdsFilterBarFilter): void {
+ this.internalFilters = this._updateFilter(key, keyFilter);
+
+ if (this.isLiveFilter) {
+ this._applyFilters();
+ }
+ }
+
+ @action
+ onApply(closeDropdown?: () => void): void {
+ this._applyFilters(closeDropdown);
+ }
+
+ @action
+ onClear(closeDropdown?: () => void): void {
+ const { onFilter } = this.args;
+ this.internalFilters = {};
+
+ if (onFilter && typeof onFilter === 'function') {
+ onFilter(this.internalFilters);
+ }
+
+ if (closeDropdown && typeof closeDropdown === 'function') {
+ closeDropdown();
+ }
+ }
+
+ private _copyFilters = (
+ filters: HdsFilterBarFilters
+ ): HdsFilterBarFilters => {
+ const newFilters = {} as HdsFilterBarFilters;
+
+ Object.keys(filters).forEach((k) => {
+ newFilters[k] = JSON.parse(
+ JSON.stringify(filters[k])
+ ) as HdsFilterBarFilter;
+ });
+
+ return newFilters;
+ };
+
+ private _updateFilter(
+ key: string,
+ keyFilter?: HdsFilterBarFilter
+ ): HdsFilterBarFilters {
+ const newFilters = this._copyFilters(this.internalFilters);
+ if (
+ keyFilter === undefined ||
+ (Array.isArray(keyFilter) && keyFilter.length === 0)
+ ) {
+ delete newFilters[key];
+ } else {
+ Object.assign(newFilters, { [key]: keyFilter });
+ }
+
+ return { ...newFilters };
+ }
+
+ private _applyFilters = (closeDropdown?: () => void): void => {
+ const { onFilter } = this.args;
+ if (onFilter && typeof onFilter === 'function') {
+ onFilter(this.internalFilters);
+ }
+
+ if (closeDropdown && typeof closeDropdown === 'function') {
+ closeDropdown();
+ }
+ };
+
+ private _onClose = (): void => {
+ const { filters } = this.args;
+ if (filters) {
+ this.internalFilters = { ...filters };
+ } else {
+ this.internalFilters = {};
+ }
+ };
+}
diff --git a/packages/components/src/components/hds/filter-bar/filter-group/checkbox.hbs b/packages/components/src/components/hds/filter-bar/filter-group/checkbox.hbs
new file mode 100644
index 00000000000..b0c9522019d
--- /dev/null
+++ b/packages/components/src/components/hds/filter-bar/filter-group/checkbox.hbs
@@ -0,0 +1,9 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: MPL-2.0
+}}
+
+
+ {{@label}}
+
+
\ No newline at end of file
diff --git a/packages/components/src/components/hds/filter-bar/filter-group/checkbox.ts b/packages/components/src/components/hds/filter-bar/filter-group/checkbox.ts
new file mode 100644
index 00000000000..057b8ae8cbf
--- /dev/null
+++ b/packages/components/src/components/hds/filter-bar/filter-group/checkbox.ts
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+
+import type { HdsFilterBarFilter } from '../types.ts';
+
+export interface HdsFilterBarFilterGroupCheckboxSignature {
+ Args: {
+ value?: string;
+ label?: string;
+ keyFilter: HdsFilterBarFilter | undefined;
+ onChange?: (event: Event, label?: string) => void;
+ };
+ Blocks: {
+ default: [];
+ };
+ Element: HTMLDivElement;
+}
+
+export default class HdsFilterBarFilterGroupCheckbox extends Component {
+ @action
+ onChange(event: Event): void {
+ const { onChange, label } = this.args;
+ if (onChange && typeof onChange === 'function') {
+ onChange(event, label);
+ }
+ }
+
+ get isChecked(): boolean {
+ const { keyFilter, value } = this.args;
+ if (keyFilter && Array.isArray(keyFilter.data)) {
+ return keyFilter.data.some((filter) => filter.value === value);
+ }
+ return false;
+ }
+}
diff --git a/packages/components/src/components/hds/filter-bar/filter-group/date.hbs b/packages/components/src/components/hds/filter-bar/filter-group/date.hbs
new file mode 100644
index 00000000000..e561490034f
--- /dev/null
+++ b/packages/components/src/components/hds/filter-bar/filter-group/date.hbs
@@ -0,0 +1,65 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: MPL-2.0
+}}
+
+
+
+ {{this.selectorLabelText}}
+
+ {{hds-t
+ "hds.components.filter-bar.date.selector-input.default-value"
+ default="Pick a selector"
+ }}
+ {{#each this._selectorValues as |selectorValue|}}
+ {{this._getSelectorText
+ selectorValue
+ }}
+ {{/each}}
+
+
+ {{#if (eq this._selector "between")}}
+
+
+
+
+ {{else}}
+
+ {{/if}}
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/components/src/components/hds/filter-bar/filter-group/date.ts b/packages/components/src/components/hds/filter-bar/filter-group/date.ts
new file mode 100644
index 00000000000..46ad1ddc586
--- /dev/null
+++ b/packages/components/src/components/hds/filter-bar/filter-group/date.ts
@@ -0,0 +1,200 @@
+/**
+ * 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 type Owner from '@ember/owner';
+import { guidFor } from '@ember/object/internals';
+import { service } from '@ember/service';
+
+import type HdsIntlService from '../../../../services/hds-intl';
+import type { HdsFormTextInputTypes } from '../../form/text-input/types.ts';
+
+import type {
+ HdsFilterBarFilter,
+ HdsFilterBarDateFilterSelector,
+ HdsFilterBarDateFilterValue,
+} from '../types.ts';
+import { HdsFilterBarDateFilterSelectorValues } from '../types.ts';
+
+export const DATE_SELECTORS: HdsFilterBarDateFilterSelector[] = Object.values(
+ HdsFilterBarDateFilterSelectorValues
+);
+
+export const DATE_SELECTORS_TEXT: Record<
+ HdsFilterBarDateFilterSelector,
+ string
+> = {
+ [HdsFilterBarDateFilterSelectorValues.before]: 'before',
+ [HdsFilterBarDateFilterSelectorValues.exactly]: 'exactly',
+ [HdsFilterBarDateFilterSelectorValues.after]: 'after',
+ [HdsFilterBarDateFilterSelectorValues.between]: 'between',
+};
+
+export const DATE_SELECTORS_INPUT_TEXT: Record<
+ HdsFilterBarDateFilterSelector,
+ string
+> = {
+ [HdsFilterBarDateFilterSelectorValues.before]: 'Before',
+ [HdsFilterBarDateFilterSelectorValues.exactly]: 'Exactly',
+ [HdsFilterBarDateFilterSelectorValues.after]: 'After',
+ [HdsFilterBarDateFilterSelectorValues.between]: 'Between',
+};
+
+export interface HdsFilterBarFilterGroupDateSignature {
+ Args: {
+ keyFilter: HdsFilterBarFilter | undefined;
+ type?: 'date' | 'time' | 'datetime';
+ onChange?: (
+ selector?: HdsFilterBarDateFilterSelector,
+ value?: HdsFilterBarDateFilterValue
+ ) => void;
+ };
+ Blocks: {
+ default: [];
+ };
+ Element: HTMLDivElement;
+}
+
+export default class HdsFilterBarFilterGroupDate extends Component {
+ @service hdsIntl!: HdsIntlService;
+
+ @tracked private _selector: HdsFilterBarDateFilterSelector | undefined;
+ @tracked private _value: string | undefined;
+ @tracked private _betweenValueStart: string | undefined;
+ @tracked private _betweenValueEnd: string | undefined;
+
+ private _selectorValues = DATE_SELECTORS;
+ private _selectorInputId = 'selector-input-' + guidFor(this);
+ private _valueInputId = 'value-input-' + guidFor(this);
+ private _betweenValueStartInputId =
+ 'between-value-start-input-' + guidFor(this);
+ private _betweenValueEndInputId = 'between-value-end-input-' + guidFor(this);
+
+ constructor(
+ owner: Owner,
+ args: HdsFilterBarFilterGroupDateSignature['Args']
+ ) {
+ super(owner, args);
+
+ const { keyFilter } = this.args;
+ if (
+ keyFilter &&
+ (keyFilter.type === 'date' ||
+ keyFilter.type === 'time' ||
+ keyFilter.type === 'datetime')
+ ) {
+ const data = keyFilter.data;
+ this._selector = data.selector;
+ if (data.selector === 'between') {
+ if (
+ data.value &&
+ typeof data.value === 'object' &&
+ 'start' in data.value &&
+ 'end' in data.value
+ ) {
+ this._betweenValueStart = data.value.start;
+ this._betweenValueEnd = data.value.end;
+ }
+ } else {
+ this._value = data.value as string;
+ }
+ }
+ }
+
+ get type(): 'date' | 'time' | 'datetime' {
+ return this.args.type || 'date';
+ }
+
+ get inputType(): HdsFormTextInputTypes {
+ if (this.type === 'datetime') {
+ return 'datetime-local';
+ }
+ return this.type;
+ }
+
+ get selectorLabelText(): string {
+ return this.hdsIntl.t(`hds.components.filter-bar.date.${this.type}.label`, {
+ default: 'Date is',
+ });
+ }
+
+ @action
+ onSelectorChange(event: Event): void {
+ const select = event.target as HTMLSelectElement;
+ this._selector = select.value as HdsFilterBarDateFilterSelector;
+ if (this._selector === 'between') {
+ this._value = undefined;
+ } else {
+ this._betweenValueStart = undefined;
+ this._betweenValueEnd = undefined;
+ }
+ this._onChange();
+ }
+
+ @action
+ onValueChange(event: Event): void {
+ const input = event.target as HTMLInputElement;
+ this._value = input.value;
+ this._onChange();
+ }
+
+ @action
+ onBetweenValueStartChange(event: Event): void {
+ const input = event.target as HTMLInputElement;
+ this._betweenValueStart = input.value;
+ this._onChange();
+ }
+
+ @action
+ onBetweenValueEndChange(event: Event): void {
+ const input = event.target as HTMLInputElement;
+ this._betweenValueEnd = input.value;
+ this._onChange();
+ }
+
+ @action
+ onClear(): void {
+ this._resetInputValues();
+ this._onChange();
+ }
+
+ private _onChange(): void {
+ const { onChange } = this.args;
+ if (onChange && typeof onChange === 'function') {
+ if (
+ this._selector === 'between' &&
+ this._betweenValueStart !== undefined &&
+ this._betweenValueEnd !== undefined
+ ) {
+ onChange(this._selector, {
+ start: this._betweenValueStart,
+ end: this._betweenValueEnd,
+ });
+ } else {
+ onChange(this._selector, this._value);
+ }
+ }
+ }
+
+ private _getSelectorText = (
+ selector: HdsFilterBarDateFilterSelector
+ ): string => {
+ return this.hdsIntl.t(
+ `hds.components.filter-bar.date.selector-input.${selector}`,
+ {
+ default: DATE_SELECTORS_INPUT_TEXT[selector],
+ }
+ );
+ };
+
+ private _resetInputValues = (): void => {
+ this._selector = undefined;
+ this._value = undefined;
+ this._betweenValueStart = undefined;
+ this._betweenValueEnd = undefined;
+ };
+}
diff --git a/packages/components/src/components/hds/filter-bar/filter-group/generic.hbs b/packages/components/src/components/hds/filter-bar/filter-group/generic.hbs
new file mode 100644
index 00000000000..e3f9ebbd4dc
--- /dev/null
+++ b/packages/components/src/components/hds/filter-bar/filter-group/generic.hbs
@@ -0,0 +1,18 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: MPL-2.0
+}}
+
+
+ {{yield (hash updateFilter=this.updateFilter)}}
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/components/src/components/hds/filter-bar/filter-group/generic.ts b/packages/components/src/components/hds/filter-bar/filter-group/generic.ts
new file mode 100644
index 00000000000..69d53b1b30d
--- /dev/null
+++ b/packages/components/src/components/hds/filter-bar/filter-group/generic.ts
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+
+import type {
+ HdsFilterBarFilter,
+ HdsFilterBarGenericFilter,
+} from '../types.ts';
+
+export interface HdsFilterBarFilterGroupGenericSignature {
+ Args: {
+ keyFilter: HdsFilterBarFilter | undefined;
+ onChange?: (filter?: HdsFilterBarGenericFilter) => void;
+ };
+ Blocks: {
+ default: [
+ {
+ updateFilter: (filter: HdsFilterBarGenericFilter) => void;
+ },
+ ];
+ };
+ Element: HTMLDivElement;
+}
+
+export default class HdsFilterBarFilterGroupGeneric extends Component {
+ @action
+ updateFilter(filter: HdsFilterBarGenericFilter): void {
+ const { onChange } = this.args;
+ if (onChange && typeof onChange === 'function') {
+ onChange(filter);
+ }
+ }
+
+ @action
+ onClear(): void {
+ const { onChange } = this.args;
+ if (onChange && typeof onChange === 'function') {
+ onChange();
+ }
+ }
+}
diff --git a/packages/components/src/components/hds/filter-bar/filter-group/index.hbs b/packages/components/src/components/hds/filter-bar/filter-group/index.hbs
new file mode 100644
index 00000000000..646ca893518
--- /dev/null
+++ b/packages/components/src/components/hds/filter-bar/filter-group/index.hbs
@@ -0,0 +1,66 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: MPL-2.0
+}}
+{{#let @tab as |Tab|}}
+
+ {{@text}}
+
+{{/let}}
+{{#let @panel as |Panel|}}
+
+ {{#if @searchEnabled}}
+
+
+
+ {{/if}}
+ {{#if (eq @type "numerical")}}
+
+ {{else if (eq @type "date")}}
+
+ {{else if (eq @type "datetime")}}
+
+ {{else if (eq @type "time")}}
+
+ {{else if (eq @type "generic")}}
+ {{yield
+ (hash
+ Generic=(component
+ "hds/filter-bar/filter-group/generic" keyFilter=this.keyFilter onChange=this.onGenericChange
+ )
+ )
+ }}
+ {{else}}
+
+
+
+
+
+ {{yield
+ (hash
+ Checkbox=(component
+ "hds/filter-bar/filter-group/checkbox" keyFilter=this.keyFilter onChange=this.onSelectionChange
+ )
+ Radio=(component
+ "hds/filter-bar/filter-group/radio" keyFilter=this.keyFilter onChange=this.onSelectionChange
+ )
+ )
+ }}
+
+
+ {{/if}}
+
+{{/let}}
\ No newline at end of file
diff --git a/packages/components/src/components/hds/filter-bar/filter-group/index.ts b/packages/components/src/components/hds/filter-bar/filter-group/index.ts
new file mode 100644
index 00000000000..89dc866e417
--- /dev/null
+++ b/packages/components/src/components/hds/filter-bar/filter-group/index.ts
@@ -0,0 +1,283 @@
+/**
+ * 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 { WithBoundArgs } from '@glint/template';
+
+import HdsFilterBarTabsTab from '../tabs/tab.ts';
+import HdsFilterBarTabsPanel from '../tabs/panel.ts';
+import type { HdsTabsPanelSignature } from '../../tabs/panel.ts';
+
+import HdsFilterBarFilterGroupGeneric from './generic.ts';
+import HdsFilterBarFilterGroupCheckbox from './checkbox.ts';
+import HdsFilterBarFilterGroupRadio from './radio.ts';
+
+import type {
+ HdsFilterBarFilter,
+ HdsFilterBarFilters,
+ HdsFilterBarFilterType,
+ HdsFilterBarData,
+ HdsFilterBarGenericFilter,
+ HdsFilterBarGenericFilterData,
+ HdsFilterBarNumericalFilterData,
+ HdsFilterBarNumericalFilterSelector,
+ HdsFilterBarNumericalFilterValue,
+ HdsFilterBarDateFilterData,
+ HdsFilterBarDateFilterSelector,
+ HdsFilterBarDateFilterValue,
+} from '../types.ts';
+
+export interface HdsFilterBarFilterGroupSignature {
+ Args: {
+ tab?: WithBoundArgs;
+ panel?: WithBoundArgs;
+ key: string;
+ text: string;
+ type?: HdsFilterBarFilterType;
+ filters: HdsFilterBarFilters;
+ searchEnabled?: boolean;
+ onChange: (key: string, keyFilter?: HdsFilterBarFilter) => void;
+ };
+ Blocks: {
+ default: [
+ {
+ Generic?: WithBoundArgs<
+ typeof HdsFilterBarFilterGroupGeneric,
+ 'keyFilter'
+ >;
+ Checkbox?: WithBoundArgs<
+ typeof HdsFilterBarFilterGroupCheckbox,
+ 'keyFilter' | 'onChange'
+ >;
+ Radio?: WithBoundArgs<
+ typeof HdsFilterBarFilterGroupRadio,
+ 'keyFilter' | 'onChange'
+ >;
+ },
+ ];
+ };
+ Element: HdsTabsPanelSignature['Element'];
+}
+
+export default class HdsFilterBarFilterGroup extends Component {
+ @tracked internalFilters: HdsFilterBarData | undefined = [];
+
+ private _panelElement!: HdsTabsPanelSignature['Element'];
+
+ private _setUpFilterPanel = modifier(
+ (element: HdsTabsPanelSignature['Element']) => {
+ this._panelElement = element;
+
+ if (this.keyFilter) {
+ this.internalFilters = JSON.parse(
+ JSON.stringify(this.keyFilter.data)
+ ) as HdsFilterBarData;
+ }
+ }
+ );
+
+ get type(): HdsFilterBarFilterType {
+ const { type } = this.args;
+
+ if (!type) {
+ return 'multi-select';
+ }
+ return type;
+ }
+
+ get keyFilter(): HdsFilterBarFilter | undefined {
+ const { filters, key } = this.args;
+
+ if (!filters) {
+ return undefined;
+ }
+ return filters[key];
+ }
+
+ get numFilters(): number {
+ const { filters, key } = this.args;
+ if (filters && key in filters) {
+ const keyFilters = filters[key]?.data;
+ if (Array.isArray(keyFilters)) {
+ return keyFilters.length;
+ } else if (keyFilters) {
+ return 1;
+ }
+ }
+ return 0;
+ }
+
+ get formattedFilters(): HdsFilterBarFilter | undefined {
+ if (
+ this.internalFilters === undefined ||
+ (Array.isArray(this.internalFilters) && this.internalFilters.length === 0)
+ ) {
+ return undefined;
+ }
+ return {
+ type: this.type,
+ text: this.args.text,
+ data: this.internalFilters,
+ } as HdsFilterBarFilter;
+ }
+
+ @action
+ onSelectionChange(event: Event, label?: string): void {
+ const addFilter = (value: unknown): void => {
+ const newFilter = {
+ value: value,
+ label: label,
+ } as HdsFilterBarGenericFilterData;
+ if (this.type === 'single-select') {
+ this.internalFilters = newFilter;
+ } else {
+ if (Array.isArray(this.internalFilters)) {
+ this.internalFilters.push(newFilter);
+ } else {
+ this.internalFilters = [newFilter];
+ }
+ }
+ };
+
+ const removeFilter = (value: string): void => {
+ if (this.type === 'single-select') {
+ this.internalFilters = undefined;
+ } else {
+ if (Array.isArray(this.internalFilters)) {
+ const newFilter = [] as HdsFilterBarGenericFilterData[];
+ this.internalFilters.forEach((filter) => {
+ if (filter.value != value) {
+ newFilter.push(filter);
+ }
+ });
+ this.internalFilters = newFilter;
+ } else {
+ this.internalFilters = [];
+ }
+ }
+ };
+
+ const input = event.target as HTMLInputElement;
+
+ if (input.checked) {
+ addFilter(input.value);
+ } else {
+ removeFilter(input.value);
+ }
+
+ const { onChange } = this.args;
+ if (onChange && typeof onChange === 'function') {
+ onChange(this.args.key, this.formattedFilters);
+ }
+ }
+
+ @action
+ onNumericalChange(
+ selector?: HdsFilterBarNumericalFilterSelector,
+ value?: HdsFilterBarNumericalFilterValue
+ ): void {
+ const addFilter = (): HdsFilterBarData => {
+ const newFilter = {
+ selector: selector,
+ value: value,
+ } as HdsFilterBarNumericalFilterData;
+ return newFilter;
+ };
+
+ if (selector && value) {
+ this.internalFilters = addFilter();
+ } else {
+ this.internalFilters = undefined;
+ }
+
+ const { onChange } = this.args;
+ if (onChange && typeof onChange === 'function') {
+ onChange(this.args.key, this.formattedFilters);
+ }
+ }
+
+ @action
+ onDateChange(
+ selector?: HdsFilterBarDateFilterSelector,
+ value?: HdsFilterBarDateFilterValue
+ ): void {
+ const addFilter = (): HdsFilterBarData => {
+ const newFilter = {
+ selector: selector,
+ value: value,
+ } as HdsFilterBarDateFilterData;
+ return newFilter;
+ };
+
+ if (selector && value) {
+ this.internalFilters = addFilter();
+ } else {
+ this.internalFilters = undefined;
+ }
+
+ const { onChange } = this.args;
+ if (onChange && typeof onChange === 'function') {
+ onChange(this.args.key, this.formattedFilters);
+ }
+ }
+
+ @action
+ onGenericChange(filter?: HdsFilterBarGenericFilter): void {
+ if (filter) {
+ this.internalFilters = filter.data;
+ filter.text = this.args.text;
+ } else {
+ this.internalFilters = undefined;
+ }
+
+ const { onChange } = this.args;
+ if (onChange && typeof onChange === 'function') {
+ onChange(this.args.key, filter);
+ }
+ }
+
+ @action
+ onClear(): void {
+ this.internalFilters = undefined;
+
+ const { onChange } = this.args;
+ if (onChange && typeof onChange === 'function') {
+ onChange(this.args.key, this.formattedFilters);
+ }
+ }
+
+ get classNames(): string {
+ const classes = ['hds-filter-bar__filter-group'];
+
+ classes.push(`hds-filter-bar__dropdown--type-${this.type}`);
+
+ return classes.join(' ');
+ }
+
+ private onSearch = (event: Event) => {
+ const listItems = this._panelElement.querySelectorAll(
+ '.hds-filter-bar__filter-group__selection-option'
+ );
+ const input = event.target as HTMLInputElement;
+ listItems.forEach((item) => {
+ if (item.textContent) {
+ const text = item.textContent.toLowerCase();
+ const searchText = input.value.toLowerCase();
+ if (text.includes(searchText)) {
+ item.classList.remove(
+ 'hds-filter-bar__filter-group__selection-option--hidden'
+ );
+ } else {
+ item.classList.add(
+ 'hds-filter-bar__filter-group__selection-option--hidden'
+ );
+ }
+ }
+ });
+ };
+}
diff --git a/packages/components/src/components/hds/filter-bar/filter-group/numerical.hbs b/packages/components/src/components/hds/filter-bar/filter-group/numerical.hbs
new file mode 100644
index 00000000000..8f80af37cb0
--- /dev/null
+++ b/packages/components/src/components/hds/filter-bar/filter-group/numerical.hbs
@@ -0,0 +1,70 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: MPL-2.0
+}}
+
+
+
+
+ {{hds-t "hds.components.filter-bar.filter-group.numerical.label" default="Number is"}}
+
+
+ {{hds-t
+ "hds.components.filter-bar.filter-group.numerical.selector-input.default-value"
+ default="Pick a selector"
+ }}
+ {{#each this._selectorValues as |selectorValue|}}
+ {{this._getSelectorText
+ selectorValue
+ }}
+ {{/each}}
+
+
+ {{#if (eq this._selector "between")}}
+
+
+
+
+ {{else}}
+
+ {{/if}}
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/components/src/components/hds/filter-bar/filter-group/numerical.ts b/packages/components/src/components/hds/filter-bar/filter-group/numerical.ts
new file mode 100644
index 00000000000..e31d50b7236
--- /dev/null
+++ b/packages/components/src/components/hds/filter-bar/filter-group/numerical.ts
@@ -0,0 +1,191 @@
+/**
+ * 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 type Owner from '@ember/owner';
+import { guidFor } from '@ember/object/internals';
+import { service } from '@ember/service';
+
+import type HdsIntlService from '../../../../services/hds-intl';
+import type {
+ HdsFilterBarFilter,
+ HdsFilterBarNumericalFilterSelector,
+ HdsFilterBarNumericalFilterValue,
+} from '../types.ts';
+import { HdsFilterBarNumericalFilterSelectorValues } from '../types.ts';
+
+export const NUMERICAL_SELECTORS: HdsFilterBarNumericalFilterSelector[] =
+ Object.values(HdsFilterBarNumericalFilterSelectorValues);
+
+export const NUMERICAL_SELECTORS_TEXT: Record<
+ HdsFilterBarNumericalFilterSelector,
+ string
+> = {
+ [HdsFilterBarNumericalFilterSelectorValues.lessThan]: '<',
+ [HdsFilterBarNumericalFilterSelectorValues.lessThanOrEqualTo]: '≤',
+ [HdsFilterBarNumericalFilterSelectorValues.equalTo]: '=',
+ [HdsFilterBarNumericalFilterSelectorValues.greaterThanOrEqualTo]: '≥',
+ [HdsFilterBarNumericalFilterSelectorValues.greaterThan]: '>',
+ [HdsFilterBarNumericalFilterSelectorValues.between]: 'between',
+};
+
+export const NUMERICAL_SELECTORS_INPUT_TEXT: Record<
+ HdsFilterBarNumericalFilterSelector,
+ string
+> = {
+ [HdsFilterBarNumericalFilterSelectorValues.lessThan]: 'Less than (<)',
+ [HdsFilterBarNumericalFilterSelectorValues.lessThanOrEqualTo]:
+ 'Less than or equal to (≤)',
+ [HdsFilterBarNumericalFilterSelectorValues.equalTo]: 'Equal to (=)',
+ [HdsFilterBarNumericalFilterSelectorValues.greaterThanOrEqualTo]:
+ 'Greater than or equal to (≥)',
+ [HdsFilterBarNumericalFilterSelectorValues.greaterThan]: 'Greater than (>)',
+ [HdsFilterBarNumericalFilterSelectorValues.between]: 'Between',
+};
+
+export interface HdsFilterBarFilterGroupNumericalSignature {
+ Args: {
+ keyFilter: HdsFilterBarFilter | undefined;
+ onChange?: (
+ selector?: HdsFilterBarNumericalFilterSelector,
+ value?: HdsFilterBarNumericalFilterValue
+ ) => void;
+ };
+ Blocks: {
+ default: [];
+ };
+ Element: HTMLDivElement;
+}
+
+export default class HdsFilterBarFilterGroupNumerical extends Component {
+ @service hdsIntl!: HdsIntlService;
+
+ @tracked private _selector: HdsFilterBarNumericalFilterSelector | undefined;
+ @tracked private _value: number | undefined;
+ @tracked private _betweenValueStart: number | undefined;
+ @tracked private _betweenValueEnd: number | undefined;
+
+ private _selectorValues = NUMERICAL_SELECTORS;
+ private _selectorInputId = 'selector-input-' + guidFor(this);
+ private _valueInputId = 'value-input-' + guidFor(this);
+ private _betweenValueStartInputId =
+ 'between-value-start-input-' + guidFor(this);
+ private _betweenValueEndInputId = 'between-value-end-input-' + guidFor(this);
+
+ constructor(
+ owner: Owner,
+ args: HdsFilterBarFilterGroupNumericalSignature['Args']
+ ) {
+ super(owner, args);
+
+ const { keyFilter } = this.args;
+ if (keyFilter && keyFilter.type === 'numerical') {
+ const data = keyFilter.data;
+ this._selector = data?.selector;
+ if (data.selector === 'between') {
+ if (data.value && typeof data.value === 'object') {
+ this._betweenValueStart = Number(data.value.start);
+ this._betweenValueEnd = Number(data.value.end);
+ }
+ } else {
+ this._value = Number(data.value);
+ }
+ }
+ }
+
+ get stringValue(): string | undefined {
+ return this._value !== undefined ? this._value.toString() : undefined;
+ }
+
+ get stringBetweenValueStart(): string | undefined {
+ return this._betweenValueStart !== undefined
+ ? this._betweenValueStart.toString()
+ : undefined;
+ }
+
+ get stringBetweenValueEnd(): string | undefined {
+ return this._betweenValueEnd !== undefined
+ ? this._betweenValueEnd.toString()
+ : undefined;
+ }
+
+ @action
+ onSelectorChange(event: Event): void {
+ const select = event.target as HTMLSelectElement;
+ this._selector = select.value as HdsFilterBarNumericalFilterSelector;
+ if (this._selector === 'between') {
+ this._value = undefined;
+ } else {
+ this._betweenValueStart = undefined;
+ this._betweenValueEnd = undefined;
+ }
+ this._onChange();
+ }
+
+ @action
+ onValueChange(event: Event): void {
+ const input = event.target as HTMLInputElement;
+ this._value = parseFloat(input.value);
+ this._onChange();
+ }
+
+ @action
+ onBetweenValueStartChange(event: Event): void {
+ const input = event.target as HTMLInputElement;
+ this._betweenValueStart = parseFloat(input.value);
+ this._onChange();
+ }
+
+ @action
+ onBetweenValueEndChange(event: Event): void {
+ const input = event.target as HTMLInputElement;
+ this._betweenValueEnd = parseFloat(input.value);
+ this._onChange();
+ }
+
+ @action
+ onClear(): void {
+ this._resetInputValues();
+ this._onChange();
+ }
+
+ private _onChange(): void {
+ const { onChange } = this.args;
+ if (onChange && typeof onChange === 'function') {
+ if (
+ this._selector === 'between' &&
+ this._betweenValueStart !== undefined &&
+ this._betweenValueEnd !== undefined
+ ) {
+ onChange(this._selector, {
+ start: this._betweenValueStart,
+ end: this._betweenValueEnd,
+ });
+ } else {
+ onChange(this._selector, this._value);
+ }
+ }
+ }
+
+ private _getSelectorText = (
+ selector: HdsFilterBarNumericalFilterSelector
+ ): string => {
+ return this.hdsIntl.t(
+ `hds.components.filter-bar.filter-group.numerical.selector-input.${selector}`,
+ {
+ default: NUMERICAL_SELECTORS_INPUT_TEXT[selector],
+ }
+ );
+ };
+
+ private _resetInputValues = (): void => {
+ this._selector = undefined;
+ this._value = undefined;
+ this._betweenValueStart = undefined;
+ this._betweenValueEnd = undefined;
+ };
+}
diff --git a/packages/components/src/components/hds/filter-bar/filter-group/radio.hbs b/packages/components/src/components/hds/filter-bar/filter-group/radio.hbs
new file mode 100644
index 00000000000..319efa5cdb3
--- /dev/null
+++ b/packages/components/src/components/hds/filter-bar/filter-group/radio.hbs
@@ -0,0 +1,9 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: MPL-2.0
+}}
+
+
+ {{@label}}
+
+
\ No newline at end of file
diff --git a/packages/components/src/components/hds/filter-bar/filter-group/radio.ts b/packages/components/src/components/hds/filter-bar/filter-group/radio.ts
new file mode 100644
index 00000000000..00c7bc3106d
--- /dev/null
+++ b/packages/components/src/components/hds/filter-bar/filter-group/radio.ts
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+
+import type { HdsFilterBarFilter } from '../types.ts';
+
+export interface HdsFilterBarFilterGroupRadioSignature {
+ Args: {
+ value?: string;
+ label?: string;
+ keyFilter: HdsFilterBarFilter | undefined;
+ onChange?: (event: Event, label?: string) => void;
+ };
+ Blocks: {
+ default: [];
+ };
+ Element: HTMLDivElement;
+}
+
+export default class HdsFilterBarFilterGroupRadio extends Component {
+ @action
+ onChange(event: Event): void {
+ const { onChange, label } = this.args;
+ if (onChange && typeof onChange === 'function') {
+ onChange(event, label);
+ }
+ }
+
+ get isChecked(): boolean {
+ const { keyFilter, value } = this.args;
+ if (
+ keyFilter &&
+ keyFilter.type === 'single-select' &&
+ value &&
+ 'value' in keyFilter.data
+ ) {
+ return keyFilter.data.value === value;
+ }
+ return false;
+ }
+}
diff --git a/packages/components/src/components/hds/filter-bar/index.hbs b/packages/components/src/components/hds/filter-bar/index.hbs
new file mode 100644
index 00000000000..7bcf45daeba
--- /dev/null
+++ b/packages/components/src/components/hds/filter-bar/index.hbs
@@ -0,0 +1,98 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: MPL-2.0
+}}
+
+
+
+ {{yield
+ (hash
+ Dropdown=(component
+ "hds/filter-bar/dropdown" filters=@filters isLiveFilter=@isLiveFilter onFilter=this.onFilter
+ )
+ )
+ }}
+ {{#if @hasSearch}}
+
+ {{/if}}
+
+ {{yield (hash ActionsGeneric=(component "hds/yield"))}}
+ {{yield (hash ActionsDropdown=(component "hds/dropdown"))}}
+
+
+ {{#if this._isExpanded}}
+
+ {{#if this.hasActiveFilters}}
+ {{#each-in @filters as |key filter|}}
+ {{#if filter.data}}
+ {{#if (eq filter.type "single-select")}}
+
+ {{else if (eq filter.type "numerical")}}
+
+ {{else if (or (eq filter.type "date") (eq filter.type "datetime") (eq filter.type "time"))}}
+
+ {{else if (eq filter.type "search")}}
+
+ {{else if (eq filter.type "generic")}}
+
+ {{else if (eq filter.type "multi-select")}}
+ {{#each (this._getMultiSelectFilterData filter.data) as |item|}}
+
+ {{/each}}
+ {{/if}}
+ {{/if}}
+ {{/each-in}}
+
+ {{else}}
+
+ {{hds-t "hds.components.filter-bar.no-filters-applied" default="No filters applied"}}
+
+ {{/if}}
+
+ {{/if}}
+
\ No newline at end of file
diff --git a/packages/components/src/components/hds/filter-bar/index.ts b/packages/components/src/components/hds/filter-bar/index.ts
new file mode 100644
index 00000000000..6a3fce8c535
--- /dev/null
+++ b/packages/components/src/components/hds/filter-bar/index.ts
@@ -0,0 +1,302 @@
+/**
+ * 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 { service } from '@ember/service';
+import { isArray } from '@ember/array';
+import type { WithBoundArgs } from '@glint/template';
+
+import type HdsIntlService from '../../../services/hds-intl';
+import type {
+ HdsFilterBarFilters,
+ HdsFilterBarFilter,
+ HdsFilterBarFilterType,
+ HdsFilterBarData,
+ HdsFilterBarGenericFilterData,
+} from './types.ts';
+import HdsDropdown from '../dropdown/index.ts';
+import HdsYield from '../yield/index.ts';
+import HdsFilterBarDropdown from './dropdown.ts';
+
+import { NUMERICAL_SELECTORS_TEXT } from './filter-group/numerical.ts';
+import { DATE_SELECTORS_TEXT } from './filter-group/date.ts';
+
+export interface HdsFilterBarSignature {
+ Args: {
+ filters: HdsFilterBarFilters;
+ isLiveFilter?: boolean;
+ hasSearch?: boolean;
+ onFilter?: (filters: HdsFilterBarFilters) => void;
+ };
+ Blocks: {
+ default?: [
+ {
+ ActionsDropdown?: WithBoundArgs;
+ ActionsGeneric?: WithBoundArgs;
+ Dropdown?: WithBoundArgs<
+ typeof HdsFilterBarDropdown,
+ 'filters' | 'isLiveFilter' | 'onFilter'
+ >;
+ },
+ ];
+ };
+ Element: HTMLDivElement;
+}
+
+export default class HdsFilterBar extends Component {
+ @service hdsIntl!: HdsIntlService;
+
+ @tracked _isExpanded: boolean = this.hasActiveFilters;
+
+ get searchValue(): string {
+ const { filters } = this.args;
+ if (filters['search']) {
+ return this._getFilterValueText(filters['search']);
+ }
+ return '';
+ }
+
+ get hasActiveFilters(): boolean {
+ return Object.keys(this.args.filters).length > 0;
+ }
+
+ @action
+ onFilter(filters: HdsFilterBarFilters): void {
+ const { onFilter } = this.args;
+ if (onFilter && typeof onFilter === 'function') {
+ onFilter(filters);
+
+ if (Object.keys(filters).length > 0) {
+ this._isExpanded = true;
+ } else {
+ this._isExpanded = false;
+ }
+ }
+ }
+
+ @action
+ clearFilters(): void {
+ const { onFilter } = this.args;
+ if (onFilter && typeof onFilter === 'function') {
+ onFilter({});
+ this._isExpanded = false;
+ }
+ }
+
+ @action
+ onSearch(event: Event): void {
+ const { filters } = this.args;
+ const input = event.target as HTMLInputElement;
+ const value = input?.value;
+
+ const newFilters = this._copyFilters(filters);
+
+ if (value.length > 0) {
+ newFilters['search'] = {
+ type: 'search',
+ text: 'Search',
+ data: { value },
+ };
+ } else {
+ delete newFilters['search'];
+ }
+
+ this.onFilter({ ...newFilters });
+ }
+
+ @action
+ toggleExpand(): void {
+ this._isExpanded = !this._isExpanded;
+ }
+
+ private _copyFilters = (
+ filters: HdsFilterBarFilters
+ ): HdsFilterBarFilters => {
+ const newFilters = {} as HdsFilterBarFilters;
+
+ Object.keys(filters).forEach((k) => {
+ newFilters[k] = JSON.parse(
+ JSON.stringify(filters[k])
+ ) as HdsFilterBarFilter;
+ });
+
+ return newFilters;
+ };
+
+ private _onFilterDismiss = (key: string, filterValue?: unknown): void => {
+ const { filters } = this.args;
+ if (filters && filters[key]) {
+ const keyFilter: HdsFilterBarFilter = filters[key];
+ const newFilters = this._copyFilters(filters);
+
+ if (keyFilter.type === 'multi-select' && isArray(keyFilter.data)) {
+ const newKeyfilter = keyFilter.data?.filter(
+ (item) => item.value !== filterValue
+ );
+ if (newKeyfilter.length === 0) {
+ delete newFilters[key];
+ } else {
+ newFilters[key] = {
+ type: 'multi-select',
+ text: keyFilter.text,
+ data: newKeyfilter,
+ };
+ }
+ } else {
+ delete newFilters[key];
+ }
+
+ this.onFilter({ ...newFilters });
+ }
+ };
+
+ private _filterData = (
+ data: HdsFilterBarData
+ ): HdsFilterBarGenericFilterData => {
+ const result = {
+ value: '',
+ } as HdsFilterBarGenericFilterData;
+ if ('value' in data) {
+ result.value = data.value;
+ }
+ if ('label' in data) {
+ result.label = data.label;
+ }
+ return result;
+ };
+
+ private _getFilterValueText = (filter: HdsFilterBarFilter): string => {
+ const result = this._filterData(filter.data);
+ const resultLabel = result?.label as string;
+ const resultValue = result?.value as string;
+ return resultLabel ?? resultValue;
+ };
+
+ private _getFilterKeyText = (
+ key: string,
+ data: HdsFilterBarFilter
+ ): string => {
+ if (data.text) {
+ return data.text;
+ } else {
+ return key;
+ }
+ };
+
+ private _getNumericalFilterValueText = (
+ filter: HdsFilterBarFilter
+ ): string => {
+ const data = filter.data;
+
+ if (filter.type === 'numerical' && 'selector' in data && 'value' in data) {
+ const selector = data.selector as keyof typeof NUMERICAL_SELECTORS_TEXT;
+ if (
+ selector === 'between' &&
+ typeof data.value === 'object' &&
+ data.value !== null
+ ) {
+ const separatorText = this.hdsIntl.t(
+ 'hds.components.filter-bar.filter-text.numerical-filter.separator',
+ {
+ default: 'and',
+ }
+ );
+ return `${NUMERICAL_SELECTORS_TEXT[selector]} ${data.value.start} ${separatorText} ${data.value.end}`;
+ } else if (typeof data.value !== 'object') {
+ return `${NUMERICAL_SELECTORS_TEXT[selector]} ${data.value}`;
+ }
+ return '';
+ } else {
+ return '';
+ }
+ };
+
+ private _getDateFilterValueText = (filter: HdsFilterBarFilter): string => {
+ const data = filter.data;
+
+ if (
+ (filter.type === 'date' ||
+ filter.type === 'datetime' ||
+ filter.type === 'time') &&
+ 'selector' in data &&
+ 'value' in data
+ ) {
+ const selector = data.selector as keyof typeof DATE_SELECTORS_TEXT;
+ if (
+ selector === 'between' &&
+ typeof data.value === 'object' &&
+ data.value !== null
+ ) {
+ const separatorText = this.hdsIntl.t(
+ 'hds.components.filter-bar.filter-text.date-filter.separator',
+ {
+ default: 'and',
+ }
+ );
+ const startDateText = this._formatDateFilterText(
+ data.value.start as string,
+ filter.type
+ );
+ const endDateText = this._formatDateFilterText(
+ data.value.end as string,
+ filter.type
+ );
+ return `${DATE_SELECTORS_TEXT[selector]} ${startDateText} ${separatorText} ${endDateText}`;
+ } else if (data.value !== null && typeof data.value !== 'object') {
+ const dateText = this._formatDateFilterText(
+ data.value as string,
+ filter.type
+ );
+ return `${DATE_SELECTORS_TEXT[selector]} ${dateText}`;
+ }
+ return '';
+ } else {
+ return '';
+ }
+ };
+
+ private _formatDateFilterText = (
+ dateString: string,
+ filterType: HdsFilterBarFilterType
+ ): string => {
+ let date;
+ if (filterType === 'time') {
+ date = new Date(`1970-01-01T${dateString}`);
+ } else {
+ date = new Date(dateString);
+ }
+
+ let options = {};
+ if (filterType === 'date') {
+ options = { dateStyle: 'short' };
+ } else if (filterType === 'time') {
+ options = { timeStyle: 'short' };
+ } else {
+ options = { dateStyle: 'short', timeStyle: 'short' };
+ }
+
+ const newDate = new Intl.DateTimeFormat(undefined, options);
+ return newDate.format(date);
+ };
+
+ private _getGenericFilterValueText = (filter: HdsFilterBarFilter): string => {
+ if ('dismissTagText' in filter) {
+ return filter.dismissTagText ?? '';
+ } else {
+ return '';
+ }
+ };
+
+ private _getMultiSelectFilterData = (
+ data: HdsFilterBarData
+ ): { value: unknown; label?: string }[] => {
+ if (isArray(data)) {
+ return data.map((item) => this._filterData(item));
+ }
+ return [];
+ };
+}
diff --git a/packages/components/src/components/hds/filter-bar/tabs/index.hbs b/packages/components/src/components/hds/filter-bar/tabs/index.hbs
new file mode 100644
index 00000000000..4b58e4dd652
--- /dev/null
+++ b/packages/components/src/components/hds/filter-bar/tabs/index.hbs
@@ -0,0 +1,34 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: MPL-2.0
+}}
+
+
+ {{yield
+ (hash
+ Tab=(component
+ "hds/filter-bar/tabs/tab"
+ selectedTabIndex=this._selectedTabIndex
+ tabIds=this._tabIds
+ panelIds=this._panelIds
+ didInsertNode=this.didInsertTab
+ willDestroyNode=this.willDestroyTab
+ onClick=this.onClick
+ onKeyUp=this.onKeyUp
+ )
+ )
+ }}
+
+ {{yield
+ (hash
+ Panel=(component
+ "hds/filter-bar/tabs/panel"
+ selectedTabIndex=this._selectedTabIndex
+ tabIds=this._tabIds
+ panelIds=this._panelIds
+ didInsertNode=this.didInsertPanel
+ willDestroyNode=this.willDestroyPanel
+ )
+ )
+ }}
+
\ No newline at end of file
diff --git a/packages/components/src/components/hds/filter-bar/tabs/index.ts b/packages/components/src/components/hds/filter-bar/tabs/index.ts
new file mode 100644
index 00000000000..158c4da34f7
--- /dev/null
+++ b/packages/components/src/components/hds/filter-bar/tabs/index.ts
@@ -0,0 +1,185 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+import { schedule } from '@ember/runloop';
+import { modifier } from 'ember-modifier';
+import type { WithBoundArgs } from '@glint/template';
+import HdsFilterBarTabsTabComponent from './tab.ts';
+import HdsFilterBarTabsPanelComponent from './panel.ts';
+
+const TAB_ELEMENT_SELECTOR = '.hds-filter-bar__tabs__tab__button';
+const PANEL_ELEMENT_SELECTOR = '.hds-filter-bar__tabs__panel';
+
+export interface HdsFilterBarTabsSignature {
+ Args: {
+ selectedTabIndex?: number;
+ ariaLabel: string;
+ onClickTab?: (event: MouseEvent, tabIndex: number) => void;
+ };
+ Blocks: {
+ default: [
+ {
+ Tab?: WithBoundArgs<
+ typeof HdsFilterBarTabsTabComponent,
+ | 'selectedTabIndex'
+ | 'tabIds'
+ | 'panelIds'
+ | 'didInsertNode'
+ | 'willDestroyNode'
+ | 'onClick'
+ | 'onKeyUp'
+ >;
+ Panel?: WithBoundArgs<
+ typeof HdsFilterBarTabsPanelComponent,
+ | 'selectedTabIndex'
+ | 'tabIds'
+ | 'panelIds'
+ | 'didInsertNode'
+ | 'willDestroyNode'
+ >;
+ },
+ ];
+ };
+ Element: HTMLDivElement;
+}
+
+export default class HdsFilterBarTabs extends Component {
+ @tracked private _tabIds: string[] = [];
+ @tracked private _tabNodes: HTMLElement[] = [];
+ @tracked private _panelNodes: HTMLElement[] = [];
+ @tracked private _panelIds: string[] = [];
+ @tracked private _selectedTabIndex: number = 0;
+
+ private _element!: HTMLDivElement;
+
+ private _setUpFilterBarTabs = modifier((element: HTMLDivElement) => {
+ const { selectedTabIndex } = this.args;
+
+ if (selectedTabIndex) {
+ this._selectedTabIndex = selectedTabIndex;
+ }
+
+ this._element = element;
+
+ return () => {};
+ });
+
+ @action
+ didInsertTab(): void {
+ // eslint-disable-next-line ember/no-runloop
+ schedule('afterRender', (): void => {
+ this._updateTabs();
+ });
+ }
+
+ @action
+ willDestroyTab(element: HTMLElement): void {
+ // eslint-disable-next-line ember/no-runloop
+ schedule('afterRender', (): void => {
+ this._tabNodes = this._tabNodes.filter(
+ (node): boolean => node.id !== element.id
+ );
+ this._tabIds = this._tabIds.filter(
+ (tabId): boolean => tabId !== element.id
+ );
+ });
+ }
+
+ @action
+ didInsertPanel(): void {
+ // eslint-disable-next-line ember/no-runloop
+ schedule('afterRender', (): void => {
+ this._updatePanels();
+ });
+ }
+
+ @action
+ willDestroyPanel(element: HTMLElement): void {
+ // eslint-disable-next-line ember/no-runloop
+ schedule('afterRender', (): void => {
+ this._panelNodes = this._panelNodes.filter(
+ (node): boolean => node.id !== element.id
+ );
+ this._panelIds = this._panelIds.filter(
+ (panelId): boolean => panelId !== element.id
+ );
+ });
+ }
+
+ @action
+ onClick(event: MouseEvent, tabIndex: number): void {
+ this._selectedTabIndex = tabIndex;
+
+ // invoke the callback function if it's provided as argument
+ if (typeof this.args.onClickTab === 'function') {
+ this.args.onClickTab(event, tabIndex);
+ }
+ }
+
+ @action
+ onKeyUp(event: KeyboardEvent, tabIndex: number): void {
+ const leftArrow = 'ArrowLeft';
+ const rightArrow = 'ArrowRight';
+ const upArrow = 'ArrowUp';
+ const downArrow = 'ArrowDown';
+ const enterKey = 'Enter';
+ const spaceKey = ' ';
+
+ if (event.key === rightArrow || event.key === downArrow) {
+ const nextTabIndex = (tabIndex + 1) % this._tabIds.length;
+ this._focusTab(nextTabIndex, event);
+ } else if (event.key === leftArrow || event.key === upArrow) {
+ const prevTabIndex =
+ (tabIndex + this._tabIds.length - 1) % this._tabIds.length;
+ this._focusTab(prevTabIndex, event);
+ } else if (event.key === enterKey || event.key === spaceKey) {
+ this._selectedTabIndex = tabIndex;
+ }
+ // scroll selected tab into view (it may be out of view when activated using a keyboard with `prev/next`)
+ const parentNode = this._tabNodes[this._selectedTabIndex]?.parentNode;
+ if (parentNode instanceof HTMLElement) {
+ parentNode.scrollIntoView({
+ behavior: 'smooth',
+ block: 'nearest',
+ inline: 'nearest',
+ });
+ }
+ }
+
+ // Focus tab for keyboard & mouse navigation:
+ private _focusTab(tabIndex: number, event: KeyboardEvent): void {
+ event.preventDefault();
+ this._tabNodes[tabIndex]?.focus();
+ }
+
+ // Update the tab arrays based on how they are ordered in the DOM
+ private _updateTabs(): void {
+ const tabs = this._element.querySelectorAll(TAB_ELEMENT_SELECTOR);
+ let newTabIds: string[] = [];
+ let newTabNodes: HTMLElement[] = [];
+ tabs.forEach((tab) => {
+ newTabIds = [...newTabIds, tab.id];
+ newTabNodes = [...newTabNodes, tab as HTMLElement];
+ });
+ this._tabIds = newTabIds;
+ this._tabNodes = newTabNodes;
+ }
+
+ // Update the panel arrays based on how they are ordered in the DOM
+ private _updatePanels(): void {
+ const panels = this._element.querySelectorAll(PANEL_ELEMENT_SELECTOR);
+ let newPanelIds: string[] = [];
+ let newPanelNodes: HTMLElement[] = [];
+ panels.forEach((panel) => {
+ newPanelIds = [...newPanelIds, panel.id];
+ newPanelNodes = [...newPanelNodes, panel as HTMLElement];
+ });
+ this._panelIds = newPanelIds;
+ this._panelNodes = newPanelNodes;
+ }
+}
diff --git a/packages/components/src/components/hds/filter-bar/tabs/panel.hbs b/packages/components/src/components/hds/filter-bar/tabs/panel.hbs
new file mode 100644
index 00000000000..80f302512b8
--- /dev/null
+++ b/packages/components/src/components/hds/filter-bar/tabs/panel.hbs
@@ -0,0 +1,17 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: MPL-2.0
+}}
+
+ {{#if this.isVisible}}
+ {{yield}}
+ {{/if}}
+
\ No newline at end of file
diff --git a/packages/components/src/components/hds/filter-bar/tabs/panel.ts b/packages/components/src/components/hds/filter-bar/tabs/panel.ts
new file mode 100644
index 00000000000..276bd9f33dd
--- /dev/null
+++ b/packages/components/src/components/hds/filter-bar/tabs/panel.ts
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Component from '@glimmer/component';
+import { guidFor } from '@ember/object/internals';
+import { action } from '@ember/object';
+import { modifier } from 'ember-modifier';
+
+export interface HdsFilterBarTabsPanelSignature {
+ Args: {
+ selectedTabIndex?: number;
+ tabIds?: string[];
+ panelIds?: string[];
+ didInsertNode?: () => void;
+ willDestroyNode?: (element: HTMLElement) => void;
+ };
+ Blocks: {
+ default: [];
+ };
+ Element: HTMLElement;
+}
+
+export default class HdsFilterBarTabsPanel extends Component {
+ private _panelId = 'panel-' + guidFor(this);
+ private _elementId?: string;
+
+ private _setUpPanel = modifier(
+ (
+ element: HTMLElement,
+ [insertCallbackFunction, destroyCallbackFunction]
+ ) => {
+ if (typeof insertCallbackFunction === 'function') {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+ insertCallbackFunction(element);
+ }
+
+ return () => {
+ if (typeof destroyCallbackFunction === 'function') {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+ destroyCallbackFunction(element);
+ }
+ };
+ }
+ );
+
+ get nodeIndex(): number | undefined {
+ return this.args.panelIds?.indexOf(this._panelId);
+ }
+
+ get coupledTabId(): string | undefined {
+ return this.nodeIndex !== undefined
+ ? this.args.tabIds?.[this.nodeIndex]
+ : undefined;
+ }
+
+ get isVisible(): boolean {
+ return this.nodeIndex === this.args.selectedTabIndex;
+ }
+
+ @action
+ didInsertNode(element: HTMLElement): void {
+ const { didInsertNode } = this.args;
+
+ if (typeof didInsertNode === 'function') {
+ this._elementId = element.id;
+ didInsertNode();
+ }
+ }
+
+ @action
+ willDestroyNode(element: HTMLElement): void {
+ const { willDestroyNode } = this.args;
+
+ if (typeof willDestroyNode === 'function') {
+ willDestroyNode(element);
+ }
+ }
+}
diff --git a/packages/components/src/components/hds/filter-bar/tabs/tab.hbs b/packages/components/src/components/hds/filter-bar/tabs/tab.hbs
new file mode 100644
index 00000000000..8872757795a
--- /dev/null
+++ b/packages/components/src/components/hds/filter-bar/tabs/tab.hbs
@@ -0,0 +1,30 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: MPL-2.0
+}}
+{{! template-lint-disable require-context-role no-invalid-role }}
+{{! template-lint-disable require-presentational-children }}
+
+
+ {{yield}}
+ {{#if (gt @numFilters 0)}}
+
+ {{@numFilters}}
+
+ {{/if}}
+
+
+
+{{! template-lint-enable require-presentational-children }}
+{{! template-lint-enable require-context-role no-invalid-role }}
\ No newline at end of file
diff --git a/packages/components/src/components/hds/filter-bar/tabs/tab.ts b/packages/components/src/components/hds/filter-bar/tabs/tab.ts
new file mode 100644
index 00000000000..80d2234eecb
--- /dev/null
+++ b/packages/components/src/components/hds/filter-bar/tabs/tab.ts
@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Component from '@glimmer/component';
+import { guidFor } from '@ember/object/internals';
+import { action } from '@ember/object';
+import { modifier } from 'ember-modifier';
+
+export interface HdsFilterBarTabsTabSignature {
+ Args: {
+ selectedTabIndex?: number;
+ tabIds?: string[];
+ panelIds?: string[];
+ numFilters?: number;
+ didInsertNode?: () => void;
+ willDestroyNode?: (element: HTMLButtonElement) => void;
+ onClick?: (event: MouseEvent, nodeIndex: number) => void;
+ onKeyUp?: (event: KeyboardEvent, nodeIndex: number) => void;
+ };
+ Blocks: {
+ default: [];
+ };
+ Element: HTMLElement;
+}
+
+export default class HdsFilterBarTabsTab extends Component {
+ private _tabId = 'tab-' + guidFor(this);
+ private _elementId?: string;
+
+ private _setUpTab = modifier(
+ (
+ element: HTMLElement,
+ [insertCallbackFunction, destroyCallbackFunction]
+ ) => {
+ if (typeof insertCallbackFunction === 'function') {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+ insertCallbackFunction(element);
+ }
+
+ return () => {
+ if (typeof destroyCallbackFunction === 'function') {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+ destroyCallbackFunction(element);
+ }
+ };
+ }
+ );
+
+ get nodeIndex(): number | undefined {
+ return this.args.tabIds?.indexOf(this._tabId);
+ }
+
+ get coupledPanelId(): string | undefined {
+ return this.nodeIndex !== undefined
+ ? this.args.panelIds?.[this.nodeIndex]
+ : undefined;
+ }
+
+ get isSelected(): boolean {
+ return (
+ this.nodeIndex !== undefined &&
+ this.nodeIndex === this.args.selectedTabIndex
+ );
+ }
+
+ @action
+ didInsertNode(element: HTMLButtonElement): void {
+ const { didInsertNode } = this.args;
+
+ if (typeof didInsertNode === 'function') {
+ this._elementId = element.id;
+ didInsertNode();
+ }
+ }
+
+ @action
+ willDestroyNode(element: HTMLButtonElement): void {
+ const { willDestroyNode } = this.args;
+
+ if (typeof willDestroyNode === 'function') {
+ willDestroyNode(element);
+ }
+ }
+
+ @action
+ onClick(event: MouseEvent): false | undefined {
+ const { onClick } = this.args;
+
+ if (this.nodeIndex !== undefined && typeof onClick === 'function') {
+ onClick(event, this.nodeIndex);
+ } else {
+ return false;
+ }
+ }
+
+ @action
+ onKeyUp(event: KeyboardEvent): void {
+ const { onKeyUp } = this.args;
+
+ if (this.nodeIndex !== undefined && typeof onKeyUp === 'function') {
+ onKeyUp(event, this.nodeIndex);
+ }
+ }
+
+ get classNames(): string {
+ const classes = ['hds-filter-bar__tabs__tab'];
+
+ if (this.isSelected) {
+ classes.push(`hds-filter-bar__tabs__tab--is-selected`);
+ }
+
+ return classes.join(' ');
+ }
+}
diff --git a/packages/components/src/components/hds/filter-bar/types.ts b/packages/components/src/components/hds/filter-bar/types.ts
new file mode 100644
index 00000000000..af230923a81
--- /dev/null
+++ b/packages/components/src/components/hds/filter-bar/types.ts
@@ -0,0 +1,117 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+export enum HdsFilterBarFilterTypeValues {
+ multiSelect = 'multi-select',
+ singleSelect = 'single-select',
+ numerical = 'numerical',
+ date = 'date',
+ time = 'time',
+ datetime = 'datetime',
+ generic = 'generic',
+ search = 'search',
+}
+
+export type HdsFilterBarFilterType = `${HdsFilterBarFilterTypeValues}`;
+
+export interface HdsFilterBarGenericFilterData {
+ value: unknown;
+ label?: string;
+}
+
+export enum HdsFilterBarNumericalFilterSelectorValues {
+ lessThan = 'less-than',
+ lessThanOrEqualTo = 'less-than-or-equal-to',
+ equalTo = 'equal-to',
+ greaterThanOrEqualTo = 'greater-than-or-equal-to',
+ greaterThan = 'greater-than',
+ between = 'between',
+}
+
+export type HdsFilterBarNumericalFilterSelector =
+ `${HdsFilterBarNumericalFilterSelectorValues}`;
+
+export type HdsFilterBarNumericalFilterValue =
+ | number
+ | { start?: number; end?: number };
+
+export interface HdsFilterBarNumericalFilterData {
+ selector: HdsFilterBarNumericalFilterSelector;
+ value: HdsFilterBarNumericalFilterValue;
+}
+
+export enum HdsFilterBarDateFilterSelectorValues {
+ before = 'before',
+ exactly = 'exactly',
+ after = 'after',
+ between = 'between',
+}
+
+export type HdsFilterBarDateFilterSelector =
+ `${HdsFilterBarDateFilterSelectorValues}`;
+
+export type HdsFilterBarDateFilterValue =
+ | string
+ | { start?: string; end?: string };
+
+export interface HdsFilterBarDateFilterData {
+ selector: HdsFilterBarDateFilterSelector;
+ value: HdsFilterBarDateFilterValue;
+}
+
+export type HdsFilterBarData =
+ | HdsFilterBarGenericFilterData[]
+ | HdsFilterBarGenericFilterData
+ | HdsFilterBarNumericalFilterData
+ | HdsFilterBarDateFilterData;
+
+export interface HdsFilterBarMultiSelectFilter {
+ type: 'multi-select';
+ text?: string;
+ data: HdsFilterBarGenericFilterData[];
+}
+
+export interface HdsFilterBarSingleSelectFilter {
+ type: 'single-select';
+ text?: string;
+ data: HdsFilterBarGenericFilterData;
+}
+
+export interface HdsFilterBarNumericalFilter {
+ type: 'numerical';
+ text?: string;
+ data: HdsFilterBarNumericalFilterData;
+}
+
+export interface HdsFilterBarDateFilter {
+ type: 'date' | 'time' | 'datetime';
+ text?: string;
+ data: HdsFilterBarDateFilterData;
+}
+
+export interface HdsFilterBarGenericFilter {
+ type: 'generic';
+ text?: string;
+ dismissTagText?: string;
+ data: HdsFilterBarGenericFilterData | HdsFilterBarGenericFilterData[];
+}
+
+export interface HdsFilterBarSearchFilter {
+ type: 'search';
+ text?: string;
+ data: HdsFilterBarGenericFilterData;
+}
+
+export type HdsFilterBarFilter =
+ | HdsFilterBarMultiSelectFilter
+ | HdsFilterBarSingleSelectFilter
+ | HdsFilterBarNumericalFilter
+ | HdsFilterBarDateFilter
+ | HdsFilterBarGenericFilter
+ | HdsFilterBarSearchFilter;
+
+export interface HdsFilterBarFilters {
+ [name: string]: HdsFilterBarFilter;
+}
diff --git a/packages/components/src/styles/@hashicorp/design-system-components.scss b/packages/components/src/styles/@hashicorp/design-system-components.scss
index ae3fb7b1912..fdc971e96eb 100644
--- a/packages/components/src/styles/@hashicorp/design-system-components.scss
+++ b/packages/components/src/styles/@hashicorp/design-system-components.scss
@@ -36,6 +36,7 @@
@use "../components/disclosure-primitive";
@use "../components/dismiss-button";
@use "../components/dropdown";
+@use "../components/filter-bar";
@use "../components/flyout";
@use "../components/form"; // multiple components
@use "../components/icon";
diff --git a/packages/components/src/styles/components/advanced-table.scss b/packages/components/src/styles/components/advanced-table.scss
index 75936fadddd..fdd3679bcc3 100644
--- a/packages/components/src/styles/components/advanced-table.scss
+++ b/packages/components/src/styles/components/advanced-table.scss
@@ -415,7 +415,8 @@ $hds-advanced-table-drag-preview-background-color: rgba(204, 227, 254, 30%);
}
}
-.hds-advanced-table__th-context-menu .hds-dropdown-toggle-icon {
+.hds-advanced-table__th-context-menu .hds-dropdown-toggle-icon,
+.hds-advanced-table__th-filter-menu .hds-dropdown-toggle-icon {
width: $hds-advanced-table-button-size;
height: $hds-advanced-table-button-size;
margin: -2px 0;
@@ -504,6 +505,25 @@ $hds-advanced-table-drag-preview-background-color: rgba(204, 227, 254, 30%);
align-self: flex-start;
}
+.hds-advanced-table__th-filter-menu--active {
+ position: relative;
+
+ &::before {
+ position: absolute;
+ top: -4px;
+ right: -4px;
+ width: 6px;
+ height: 6px;
+ background-color: var(--token-color-foreground-action);
+ border-radius: 50%;
+ content: "";
+ }
+}
+
+.hds-advanced-table__clear-filters-button {
+ margin-bottom: 16px;
+}
+
// ----------------------------------------------------------------
// TABLE BODY
@@ -745,3 +765,49 @@ $hds-advanced-table-drag-preview-background-color: rgba(204, 227, 254, 30%);
border-radius: var(--token-border-radius-medium);
box-shadow: var(--token-elevation-mid-box-shadow);
}
+
+// ----------------------------------------------------------------
+
+// FILTER BAR
+.hds-advanced-table__actions .hds-filter-bar {
+ border-bottom: none;
+ border-radius: $hds-advanced-table-border-radius $hds-advanced-table-border-radius 0 0;
+}
+
+.hds-advanced-table__actions + .hds-advanced-table__container {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+
+ .hds-advanced-table__thead .hds-advanced-table__tr:first-of-type .hds-advanced-table__th {
+ &:first-child {
+ border-top-left-radius: 0;
+ }
+
+ &:last-child {
+ border-top-right-radius: 0;
+ }
+ }
+}
+
+/// ----------------------------------------------------------------
+
+// EMPTY STATE
+.hds-advanced-table__empty-state {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 400px;
+ background-color: var(--token-color-surface-primary);
+ border: 1px solid var(--token-color-border-primary);
+ border-bottom-right-radius: $hds-advanced-table-border-radius;
+ border-bottom-left-radius: $hds-advanced-table-border-radius;
+}
+
+.hds-advanced-table__empty-state__content {
+ max-width: 450px;
+}
+
+.hds-advanced-table:not(:has(+ .hds-advanced-table__empty-state)) {
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0;
+}
diff --git a/packages/components/src/styles/components/filter-bar.scss b/packages/components/src/styles/components/filter-bar.scss
new file mode 100644
index 00000000000..df66864fcdf
--- /dev/null
+++ b/packages/components/src/styles/components/filter-bar.scss
@@ -0,0 +1,164 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+//
+// FILTER BAR
+//
+
+.hds-filter-bar {
+ display: grid;
+ gap: 8px;
+ padding: 8px;
+ background-color: var(--token-color-surface-faint);
+ border: 1px solid var(--token-color-border-primary);
+ border-radius: var(--token-border-radius-medium);
+}
+
+.hds-filter-bar__applied-filters-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px 12px;
+ align-items: end;
+ padding-top: 8px;
+ border-top: 1px solid var(--token-color-border-primary);
+}
+
+.hds-filter-bar__actions__right {
+ margin-left: auto;
+}
+
+.hds-filter-bar__search {
+ --token-form-control-padding: 3px;
+}
+
+// FILTER BAR > DROPDOWN
+
+.hds-filter-bar__dropdown .hds-dropdown__list,
+.hds-filter-bar__dropdown .hds-dropdown-list-item {
+ padding: 0;
+}
+
+// FILTER BAR > TABS
+
+.hds-filter-bar__tabs {
+ display: flex;
+}
+
+.hds-filter-bar__tabs__list {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ width: 50%;
+ padding: 8px;
+ list-style: none;
+ background-color: var(--token-color-surface-faint);
+ border-right: 1px solid var(--token-color-border-primary);
+ border-top-left-radius: var(--token-border-radius-medium);
+}
+
+.hds-filter-bar__tabs__tab__button {
+ display: flex;
+ gap: 16px;
+ align-items: center;
+ width: 100%;
+ padding: 8px 12px;
+ color: var(--token-color-foreground-primary);
+ text-align: left;
+ background-color: transparent;
+ border: none;
+ border-radius: var(--token-border-radius-small);
+
+ &.mock-hover,
+ &:hover {
+ background-color: var(--token-color-surface-interactive-hover);
+ cursor: pointer;
+ }
+
+ &.mock-active,
+ &:active {
+ background-color: var(--token-color-surface-interactive-active);
+ }
+}
+
+.hds-filter-bar__tabs__tab__text {
+ width: 100%;
+}
+
+.hds-filter-bar__tabs__tab--is-selected .hds-filter-bar__tabs__tab__button {
+ color: var(--token-color-foreground-action);
+ background-color: var(--token-color-surface-strong);
+
+ &.mock-hover,
+ &:hover {
+ background-color: var(--token-color-palette-neutral-200);
+ }
+
+ &.mock-active,
+ &:active {
+ background-color: var(--token-color-palette-neutral-300);
+ }
+}
+
+.hds-filter-bar__tabs__panel:not([hidden]) {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ width: 50%;
+ padding: 16px 0;
+ background-color: var(--token-color-surface-primary);
+ border-top-right-radius: var(--token-border-radius-medium);
+}
+
+// FILTER BAR > FILTER GROUP
+
+.hds-filter-bar__filter-group .hds-form-field--layout-flag {
+ padding: 8px 12px;
+}
+
+.hds-filter-bar__filter-group__search {
+ padding: 0 16px 16px 16px;
+ border-bottom: 1px solid var(--token-color-border-primary);
+}
+
+.hds-filter-bar__filter-group__list {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ margin: 0;
+ padding: 0;
+ overflow-y: auto;
+ list-style: none;
+ overscroll-behavior: contain;
+}
+
+.hds-filter-bar__filter-group__fields {
+ padding: 0 16px;
+
+ .hds-filter-bar__filter-group__field,
+ .hds-filter-bar__filter-group__field[type="date"],
+ .hds-filter-bar__filter-group__field[type="time"] {
+ width: 100%;
+ }
+}
+
+.hds-filter-bar__filter-group__numerical,
+.hds-filter-bar__filter-group__date {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.hds-filter-bar__filter-group__selection-option {
+ display: block;
+ padding: 8px 16px;
+
+ &--hidden {
+ display: none;
+ }
+}
+
+.hds-filter-bar__filter-group__clear {
+ padding: 4px;
+}
diff --git a/packages/components/src/template-registry.ts b/packages/components/src/template-registry.ts
index 4dee43a90a6..5569b8cc52d 100644
--- a/packages/components/src/template-registry.ts
+++ b/packages/components/src/template-registry.ts
@@ -97,6 +97,18 @@ import type HdsDropdownListItemTitleComponent from './components/hds/dropdown/li
import type HdsDropdownToggleButtonComponent from './components/hds/dropdown/toggle/button';
import type HdsDropdownToggleChevronComponent from './components/hds/dropdown/toggle/chevron';
import type HdsDropdownToggleIconComponent from './components/hds/dropdown/toggle/icon';
+
+import type HdsFilterBarComponent from './components/hds/filter-bar';
+import type HdsFilterBarDropdownComponent from './components/hds/filter-bar/dropdown';
+import type HdsFilterBarFilterGroupComponent from './components/hds/filter-bar/filter-group/index.ts';
+import type HdsFilterBarFilterGroupCheckboxComponent from './components/hds/filter-bar/filter-group/checkbox';
+import type HdsFilterBarFilterGroupDateComponent from './components/hds/filter-bar/filter-group/date';
+import type HdsFilterBarFilterGroupGenericComponent from './components/hds/filter-bar/filter-group/generic';
+import type HdsFilterBarFilterGroupRadioComponent from './components/hds/filter-bar/filter-group/radio';
+import type HdsFilterBarFilterGroupNumericalComponent from './components/hds/filter-bar/filter-group/numerical';
+import type HdsFilterBarTabsComponent from './components/hds/filter-bar/tabs';
+import type HdsFilterBarTabsPanelComponent from './components/hds/filter-bar/tabs/panel';
+import type HdsFilterBarTabsTabComponent from './components/hds/filter-bar/tabs/tab';
import type HdsFlyoutComponent from './components/hds/flyout';
import type HdsFormComponent from './components/hds/form';
@@ -554,6 +566,30 @@ export default interface HdsComponentsRegistry {
'Hds::Dropdown::Toggle::Icon': typeof HdsDropdownToggleIconComponent;
'hds/dropdown/toggle/icon': typeof HdsDropdownToggleIconComponent;
+ // Filter Bar
+ 'Hds::FilterBar': typeof HdsFilterBarComponent;
+ 'hds/filter-bar': typeof HdsFilterBarComponent;
+ 'Hds::FilterBar::Dropdown': typeof HdsFilterBarDropdownComponent;
+ 'hds/filter-bar/dropdown': typeof HdsFilterBarDropdownComponent;
+ 'Hds::FilterBar::FilterGroup': typeof HdsFilterBarFilterGroupComponent;
+ 'hds/filter-bar/filter-group': typeof HdsFilterBarFilterGroupComponent;
+ 'Hds::FilterBar::FilterGroup::Checkbox': typeof HdsFilterBarFilterGroupCheckboxComponent;
+ 'hds/filter-bar/filter-group/checkbox': typeof HdsFilterBarFilterGroupCheckboxComponent;
+ 'Hds::FilterBar::FilterGroup::Date': typeof HdsFilterBarFilterGroupDateComponent;
+ 'hds/filter-bar/filter-group/date': typeof HdsFilterBarFilterGroupDateComponent;
+ 'Hds::FilterBar::FilterGroup::Generic': typeof HdsFilterBarFilterGroupGenericComponent;
+ 'hds/filter-bar/filter-group/generic': typeof HdsFilterBarFilterGroupGenericComponent;
+ 'Hds::FilterBar::FilterGroup::Radio': typeof HdsFilterBarFilterGroupRadioComponent;
+ 'hds/filter-bar/filter-group/radio': typeof HdsFilterBarFilterGroupRadioComponent;
+ 'Hds::FilterBar::FilterGroup::Numerical': typeof HdsFilterBarFilterGroupNumericalComponent;
+ 'hds/filter-bar/filter-group/numerical': typeof HdsFilterBarFilterGroupNumericalComponent;
+ 'Hds::FilterBar::Tabs': typeof HdsFilterBarTabsComponent;
+ 'hds/filter-bar/tabs': typeof HdsFilterBarTabsComponent;
+ 'Hds::FilterBar::Tabs::Panel': typeof HdsFilterBarTabsPanelComponent;
+ 'hds/filter-bar/tabs/panel': typeof HdsFilterBarTabsPanelComponent;
+ 'Hds::FilterBar::Tabs::Tab': typeof HdsFilterBarTabsTabComponent;
+ 'hds/filter-bar/tabs/tab': typeof HdsFilterBarTabsTabComponent;
+
// Flyout
'Hds::Flyout': typeof HdsFlyoutComponent;
'hds/flyout': typeof HdsFlyoutComponent;
diff --git a/packages/components/translations/hds/components/advanced-table/en-us.yaml b/packages/components/translations/hds/components/advanced-table/en-us.yaml
index b6074f30b8b..7e5d5ac4348 100644
--- a/packages/components/translations/hds/components/advanced-table/en-us.yaml
+++ b/packages/components/translations/hds/components/advanced-table/en-us.yaml
@@ -1 +1,4 @@
reordered-message: Moved {columnLabel} column to position {newPosition}
+empty-state:
+ title: No data available
+ description: There is currently no data to display in the table.
diff --git a/packages/components/translations/hds/components/filter-bar/date/en-us.yaml b/packages/components/translations/hds/components/filter-bar/date/en-us.yaml
new file mode 100644
index 00000000000..832a7cf3e6f
--- /dev/null
+++ b/packages/components/translations/hds/components/filter-bar/date/en-us.yaml
@@ -0,0 +1,18 @@
+date:
+ label: Date is
+datetime:
+ label: Datetime is
+time:
+ label: Time is
+selector-input:
+ default-value: Pick a selector
+ before: Before
+ exactly: Exactly
+ after: After
+ between: Between
+value-input:
+ placeholder: Enter a date
+between-value-inputs:
+ start-placeholder: Start
+ end-placeholder: End
+clear: Clear filter
diff --git a/packages/components/translations/hds/components/filter-bar/dropdown/en-us.yaml b/packages/components/translations/hds/components/filter-bar/dropdown/en-us.yaml
new file mode 100644
index 00000000000..21cdddde856
--- /dev/null
+++ b/packages/components/translations/hds/components/filter-bar/dropdown/en-us.yaml
@@ -0,0 +1,9 @@
+apply: Apply filters
+clear: Clear all filters
+expand-collapse-button:
+ expand: Expand filters
+ collapse: Collapse filters
+selected-filters: Filters selected
+tabs:
+ aria-label: Filter bar tabs
+toggle-button: Filters
diff --git a/packages/components/translations/hds/components/filter-bar/en-us.yaml b/packages/components/translations/hds/components/filter-bar/en-us.yaml
new file mode 100644
index 00000000000..bde0ef6ae74
--- /dev/null
+++ b/packages/components/translations/hds/components/filter-bar/en-us.yaml
@@ -0,0 +1,9 @@
+search:
+ aria-label: Search filters
+ placeholder: Search
+filter-text:
+ numerical-filter:
+ separator: and
+ date-filter:
+ separator: and
+no-filters-applied: No filters applied
diff --git a/packages/components/translations/hds/components/filter-bar/filter-group/en-us.yaml b/packages/components/translations/hds/components/filter-bar/filter-group/en-us.yaml
new file mode 100644
index 00000000000..06a65bc3b91
--- /dev/null
+++ b/packages/components/translations/hds/components/filter-bar/filter-group/en-us.yaml
@@ -0,0 +1 @@
+clear: Clear selection
diff --git a/packages/components/translations/hds/components/filter-bar/filter-group/numerical/en-us.yaml b/packages/components/translations/hds/components/filter-bar/filter-group/numerical/en-us.yaml
new file mode 100644
index 00000000000..9e1eb4f1217
--- /dev/null
+++ b/packages/components/translations/hds/components/filter-bar/filter-group/numerical/en-us.yaml
@@ -0,0 +1,15 @@
+label: Number is
+selector-input:
+ default-value: Pick a selector
+ less-than: Less than (<)
+ less-than-or-equal-to: Less than or equal to (≤)
+ equal-to: Equal to (=)
+ greater-than-or-equal-to: Greater than or equal to (≥)
+ greater-than: Greater than (>)
+ between: Between
+value-input:
+ placeholder: Enter a value
+between-value-inputs:
+ start-placeholder: Start
+ end-placeholder: End
+clear: Clear filter
diff --git a/packages/components/translations/hds/components/filter-bar/filter-options/en-us.yaml b/packages/components/translations/hds/components/filter-bar/filter-options/en-us.yaml
new file mode 100644
index 00000000000..e94d2868354
--- /dev/null
+++ b/packages/components/translations/hds/components/filter-bar/filter-options/en-us.yaml
@@ -0,0 +1 @@
+search-input-placeholder: "Search"
diff --git a/showcase/app/components/mock/app/main/generic-advanced-table.gts b/showcase/app/components/mock/app/main/generic-advanced-table.gts
index c238ae7606e..68af63946b9 100644
--- a/showcase/app/components/mock/app/main/generic-advanced-table.gts
+++ b/showcase/app/components/mock/app/main/generic-advanced-table.gts
@@ -4,111 +4,43 @@
*/
import Component from '@glimmer/component';
import { action } from '@ember/object';
+import { tracked } from '@glimmer/tracking';
import { deepTracked } from 'ember-deep-tracked';
-import { get } from '@ember/helper';
+import { get, fn } from '@ember/helper';
+import { on } from '@ember/modifier';
+import style from 'ember-style-modifier/modifiers/style';
+
+import ShwPlaceholder from 'showcase/components/shw/placeholder';
// HDS components
import {
HdsAdvancedTable,
+ HdsButton,
+ HdsFilterBar,
+ HdsLayoutFlex,
HdsLinkInline,
HdsBadge,
HdsBadgeColorValues,
+ HdsFormToggleField,
+ HdsTextBody,
+ HdsTextDisplay,
type HdsAdvancedTableOnSelectionChangeSignature,
+ type HdsFilterBarNumericalFilter,
+ type HdsFilterBarDateFilter,
+ type HdsFilterBarSingleSelectFilter,
+ type HdsFilterBarMultiSelectFilter,
+ type HdsFilterBarSearchFilter,
+ type HdsFilterBarFilter,
+ type HdsFilterBarGenericFilter,
} from '@hashicorp/design-system-components/components';
import type { HdsAdvancedTableSignature } from '@hashicorp/design-system-components/components/hds/advanced-table/index';
+import type { HdsFilterBarSignature } from '@hashicorp/design-system-components/components/hds/filter-bar/index';
export interface MockAppMainGenericAdvancedTableSignature {
Element: HTMLDivElement;
}
-const SAMPLE_COLUMNS = [
- {
- isSortable: true,
- label: 'Name',
- key: 'name',
- width: 'max-content',
- },
- {
- label: 'Project name',
- key: 'project-name',
- isSortable: true,
- width: 'max-content',
- },
- {
- label: 'Current run ID',
- key: 'current-run-id',
- isSortable: true,
- width: 'max-content',
- },
- {
- label: 'Run status',
- key: 'run-status',
- isSortable: true,
- width: 'max-content',
- },
- {
- label: 'Current run applied',
- key: 'current-run-applied',
- isSortable: true,
- width: 'max-content',
- },
- {
- label: 'VCS repo',
- key: 'vcs-repo',
- isSortable: true,
- width: 'max-content',
- },
- {
- 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',
- },
- {
- label: 'State terraform version',
- key: 'state-terraform-version',
- isSortable: true,
- width: 'max-content',
- },
- {
- label: 'Created',
- key: 'created',
- isSortable: true,
- width: 'max-content',
- },
- {
- label: 'Updated',
- key: 'updated',
- isSortable: true,
- width: 'max-content',
- },
-];
-
const SAMPLE_MODEL = [
{
name: 'zoguve-guw-mannaz',
@@ -117,6 +49,7 @@ const SAMPLE_MODEL = [
'run-status': 'errored',
'run-status-color': HdsBadgeColorValues.Critical,
'current-run-applied': 'Mar 06, 2025 09:10:14 am',
+ 'creation-time': '09:13:13',
'vcs-repo': 'example/a))!hzfpKcBl0',
'module-count': 46,
modules: 'wad-bedzeaje-rogmejca',
@@ -134,6 +67,7 @@ const SAMPLE_MODEL = [
'run-status': 'applied',
'run-status-color': HdsBadgeColorValues.Success,
'current-run-applied': 'Mar 06, 2025 09:09:14 am',
+ 'creation-time': '22:22:45',
'vcs-repo': 'example/tp7Xe!mDHlI[70ZO1',
'module-count': 152,
modules: 'wad-bedzeaje-rogmejca',
@@ -151,7 +85,8 @@ const SAMPLE_MODEL = [
'run-status': 'applied',
'run-status-color': HdsBadgeColorValues.Success,
'current-run-applied': 'Mar 06, 2025 09:08:14 am',
- 'vcs-repo': 'example/sClKKTBbyCIzf@d8NxH2',
+ 'creation-time': '11:05:33',
+ 'vcs-repo': 'example/a))!hzfpKcBl0',
'module-count': 31,
modules: 'wad-bedzeaje-rogmejca',
'provider-count': 42,
@@ -168,7 +103,8 @@ const SAMPLE_MODEL = [
'run-status': 'planned',
'run-status-color': HdsBadgeColorValues.Warning,
'current-run-applied': 'Mar 06, 2025 09:07:14 am',
- 'vcs-repo': 'example/y0^(Nm*63',
+ 'creation-time': '20:44:21',
+ 'vcs-repo': 'example/a))!hzfpKcBl0',
'module-count': 58,
modules: 'wad-bedzeaje-rogmejca',
'provider-count': 140,
@@ -185,7 +121,8 @@ const SAMPLE_MODEL = [
'run-status': 'applied',
'run-status-color': HdsBadgeColorValues.Success,
'current-run-applied': 'Mar 06, 2025 09:06:14 am',
- 'vcs-repo': 'example/ljPWe[4',
+ 'creation-time': '07:59:59',
+ 'vcs-repo': 'example/a))!hzfpKcBl0',
'module-count': 32,
modules: 'wad-bedzeaje-rogmejca',
'provider-count': 50,
@@ -202,7 +139,8 @@ const SAMPLE_MODEL = [
'run-status': 'errored',
'run-status-color': HdsBadgeColorValues.Critical,
'current-run-applied': 'Mar 06, 2025 09:05:14 am',
- 'vcs-repo': 'example/E*fcS4mn@BoDgZu0O5',
+ 'creation-time': '20:30:00',
+ 'vcs-repo': 'example/a))!hzfpKcBl0',
'module-count': 94,
modules: 'wad-bedzeaje-rogmejca',
'provider-count': 113,
@@ -219,6 +157,7 @@ const SAMPLE_MODEL = [
'run-status': 'applied',
'run-status-color': HdsBadgeColorValues.Success,
'current-run-applied': 'Mar 06, 2025 09:04:14 am',
+ 'creation-time': '10:15:30',
'vcs-repo': 'example/&j[RmmtjpQX6',
'module-count': 117,
modules: 'wad-bedzeaje-rogmejca',
@@ -236,13 +175,14 @@ const SAMPLE_MODEL = [
'run-status': 'errored',
'run-status-color': HdsBadgeColorValues.Critical,
'current-run-applied': 'Mar 06, 2025 09:03:14 am',
- 'vcs-repo': 'example/(DCFjSEKcBuU44J8AB87',
+ 'creation-time': '09:45:00',
+ 'vcs-repo': 'example/&j[RmmtjpQX6',
'module-count': 114,
modules: 'wad-bedzeaje-rogmejca',
'provider-count': 107,
providers: 'susnup-da-zuw',
'terraform-version': '0.14.0',
- 'state-terraform-version': '0.15.0',
+ 'state-terraform-version': '0.16.0',
created: 'Feb 27 2025',
updated: 'Feb 27 2025',
},
@@ -253,13 +193,14 @@ const SAMPLE_MODEL = [
'run-status': 'planned',
'run-status-color': HdsBadgeColorValues.Warning,
'current-run-applied': 'Mar 06, 2025 09:02:14 am',
- 'vcs-repo': 'example/9YURY8',
+ 'creation-time': '10:30:00',
+ 'vcs-repo': 'example/&j[RmmtjpQX6',
'module-count': 106,
modules: 'wad-bedzeaje-rogmejca',
'provider-count': 185,
providers: 'susnup-da-zuw',
- 'terraform-version': '0.14.0',
- 'state-terraform-version': '0.15.0',
+ 'terraform-version': '0.14.5',
+ 'state-terraform-version': '0.16.0',
created: 'Feb 26 2025',
updated: 'Feb 26 2025',
},
@@ -270,13 +211,14 @@ const SAMPLE_MODEL = [
'run-status': 'errored',
'run-status-color': HdsBadgeColorValues.Critical,
'current-run-applied': 'Mar 06, 2025 09:01:14 am',
- 'vcs-repo': 'example/9YURY8',
+ 'creation-time': '11:00:00',
+ 'vcs-repo': 'example/&j[RmmtjpQX6',
'module-count': 124,
modules: 'wad-bedzeaje-rogmejca',
'provider-count': 175,
providers: 'susnup-da-zuw',
- 'terraform-version': '0.14.0',
- 'state-terraform-version': '0.15.0',
+ 'terraform-version': '0.14.5',
+ 'state-terraform-version': '0.16.0',
created: 'Feb 25 2025',
updated: 'Feb 25 2025',
},
@@ -287,13 +229,14 @@ const SAMPLE_MODEL = [
'run-status': 'applied',
'run-status-color': HdsBadgeColorValues.Success,
'current-run-applied': 'Mar 06, 2025 09:00:14 am',
- 'vcs-repo': 'example/d2s3B46I10',
+ 'creation-time': '10:45:00',
+ 'vcs-repo': 'example/&j[RmmtjpQX6',
'module-count': 70,
modules: 'wad-bedzeaje-rogmejca',
'provider-count': 168,
providers: 'susnup-da-zuw',
- 'terraform-version': '0.14.0',
- 'state-terraform-version': '0.15.0',
+ 'terraform-version': '0.14.5',
+ 'state-terraform-version': '0.16.0',
created: 'Feb 24 2025',
updated: 'Feb 24 2025',
},
@@ -304,13 +247,14 @@ const SAMPLE_MODEL = [
'run-status': 'applied',
'run-status-color': HdsBadgeColorValues.Success,
'current-run-applied': 'Mar 06, 2025 09:00:14 am',
+ 'creation-time': '10:45:00',
'vcs-repo': 'example/d2s3B46I10',
'module-count': 70,
modules: 'wad-bedzeaje-rogmejca',
'provider-count': 168,
providers: 'susnup-da-zuw',
- 'terraform-version': '0.14.0',
- 'state-terraform-version': '0.15.0',
+ 'terraform-version': '0.14.5',
+ 'state-terraform-version': '0.16.0',
created: 'Feb 23 2025',
updated: 'Feb 23 2025',
},
@@ -321,13 +265,14 @@ const SAMPLE_MODEL = [
'run-status': 'errored',
'run-status-color': HdsBadgeColorValues.Critical,
'current-run-applied': 'Mar 06, 2025 08:59:14 am',
- 'vcs-repo': 'example/v@C6&hBTou11',
+ 'creation-time': '09:50:00',
+ 'vcs-repo': 'example/d2s3B46I10',
'module-count': 106,
modules: 'wad-bedzeaje-rogmejca',
'provider-count': 61,
providers: 'susnup-da-zuw',
- 'terraform-version': '0.14.0',
- 'state-terraform-version': '0.15.0',
+ 'terraform-version': '0.14.5',
+ 'state-terraform-version': '0.16.0',
created: 'Feb 22 2025',
updated: 'Feb 22 2025',
},
@@ -338,12 +283,13 @@ const SAMPLE_MODEL = [
'run-status': 'applied',
'run-status-color': HdsBadgeColorValues.Success,
'current-run-applied': 'Mar 06, 2025 08:58:14 am',
- 'vcs-repo': 'example/@t23^12',
+ 'creation-time': '10:10:00',
+ 'vcs-repo': 'example/d2s3B46I10',
'module-count': 14,
modules: 'wad-bedzeaje-rogmejca',
'provider-count': 143,
providers: 'susnup-da-zuw',
- 'terraform-version': '0.14.0',
+ 'terraform-version': '0.14.5',
'state-terraform-version': '0.15.0',
created: 'Feb 21 2025',
updated: 'Feb 21 2025',
@@ -355,12 +301,13 @@ const SAMPLE_MODEL = [
'run-status': 'planned',
'run-status-color': HdsBadgeColorValues.Warning,
'current-run-applied': 'Mar 06, 2025 08:58:14 am',
- 'vcs-repo': 'example/@t23^12',
+ 'creation-time': '10:20:00',
+ 'vcs-repo': 'example/d2s3B46I10',
'module-count': 14,
modules: 'wad-bedzeaje-rogmejca',
'provider-count': 143,
providers: 'susnup-da-zuw',
- 'terraform-version': '0.14.0',
+ 'terraform-version': '0.14.5',
'state-terraform-version': '0.15.0',
created: 'Feb 20 2025',
updated: 'Feb 20 2025',
@@ -372,12 +319,13 @@ const SAMPLE_MODEL = [
'run-status': 'errored',
'run-status-color': HdsBadgeColorValues.Critical,
'current-run-applied': 'Mar 06, 2025 08:57:14 am',
+ 'creation-time': '09:30:00',
'vcs-repo': 'example/JUha^7zr14',
'module-count': 114,
modules: 'wad-bedzeaje-rogmejca',
'provider-count': 98,
providers: 'susnup-da-zuw',
- 'terraform-version': '0.14.0',
+ 'terraform-version': '0.14.5',
'state-terraform-version': '0.15.0',
created: 'Feb 19 2025',
updated: 'Feb 19 2025',
@@ -389,12 +337,13 @@ const SAMPLE_MODEL = [
'run-status': 'applied',
'run-status-color': HdsBadgeColorValues.Success,
'current-run-applied': 'Mar 06, 2025 08:56:14 am',
+ 'creation-time': '10:05:00',
'vcs-repo': 'example/JUha^7zr14',
'module-count': 99,
modules: 'wad-bedzeaje-rogmejca',
'provider-count': 170,
providers: 'susnup-da-zuw',
- 'terraform-version': '0.14.0',
+ 'terraform-version': '0.14.5',
'state-terraform-version': '0.15.0',
created: 'Feb 18 2025',
updated: 'Feb 18 2025',
@@ -406,12 +355,13 @@ const SAMPLE_MODEL = [
'run-status': 'planned',
'run-status-color': HdsBadgeColorValues.Warning,
'current-run-applied': 'Mar 06, 2025 08:57:14 am',
- 'vcs-repo': 'example/t*vN3@*BxJnG116',
+ 'creation-time': '09:55:00',
+ 'vcs-repo': 'example/d2s3B46I10',
'module-count': 139,
modules: 'wad-bedzeaje-rogmejca',
'provider-count': 170,
providers: 'susnup-da-zuw',
- 'terraform-version': '0.14.0',
+ 'terraform-version': '0.14.5',
'state-terraform-version': '0.15.0',
created: 'Feb 17 2025',
updated: 'Feb 17 2025',
@@ -423,12 +373,13 @@ const SAMPLE_MODEL = [
'run-status': 'planned',
'run-status-color': HdsBadgeColorValues.Warning,
'current-run-applied': 'Mar 06, 2025 08:57:14 am',
+ 'creation-time': '09:40:00',
'vcs-repo': 'example/8G3C81*u*q*O$17',
'module-count': 107,
modules: 'wad-bedzeaje-rogmejca',
'provider-count': 83,
providers: 'susnup-da-zuw',
- 'terraform-version': '0.14.0',
+ 'terraform-version': '0.14.5',
'state-terraform-version': '0.15.0',
created: 'Feb 16 2025',
updated: 'Feb 16 2025',
@@ -440,12 +391,13 @@ const SAMPLE_MODEL = [
'run-status': 'errored',
'run-status-color': HdsBadgeColorValues.Critical,
'current-run-applied': 'Mar 06, 2025 08:56:14 am',
+ 'creation-time': '09:15:00',
'vcs-repo': 'example/gt]5*c!N1*N%I!m)18',
'module-count': 80,
modules: 'wad-bedzeaje-rogmejca',
'provider-count': 152,
providers: 'susnup-da-zuw',
- 'terraform-version': '0.14.0',
+ 'terraform-version': '0.14.5',
'state-terraform-version': '0.15.0',
created: 'Feb 15 2025',
updated: 'Feb 15 2025',
@@ -457,18 +409,141 @@ const SAMPLE_MODEL = [
'run-status': 'applied',
'run-status-color': HdsBadgeColorValues.Success,
'current-run-applied': 'Mar 06, 2025 08:57:14 am',
+ 'creation-time': '10:00:00',
'vcs-repo': 'example/gt]5*c!N1*N%I!m)18',
'module-count': 158,
modules: 'wad-bedzeaje-rogmejca',
'provider-count': 11,
providers: 'susnup-da-zuw',
- 'terraform-version': '0.14.0',
+ 'terraform-version': '0.14.5',
'state-terraform-version': '0.15.0',
created: 'Feb 14 2025',
updated: 'Feb 14 2025',
},
];
+const SAMPLE_MODEL_VALUES = {
+ name: Array.from(new Set(SAMPLE_MODEL.map((item) => item['name']))).map(
+ (value) => ({ value, label: value }),
+ ),
+ 'project-name': Array.from(
+ new Set(SAMPLE_MODEL.map((item) => 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',
+ },
+ {
+ label: 'Current run ID',
+ key: 'current-run-id',
+ isSortable: true,
+ width: 'max-content',
+ },
+ {
+ label: 'Run status',
+ key: 'run-status',
+ isSortable: true,
+ width: 'max-content',
+ },
+ {
+ label: 'Current run applied',
+ key: 'current-run-applied',
+ isSortable: true,
+ width: 'max-content',
+ },
+ {
+ label: 'Creation time',
+ key: 'creation-time',
+ isSortable: true,
+ width: 'max-content',
+ },
+ {
+ label: 'VCS repo',
+ key: 'vcs-repo',
+ isSortable: true,
+ width: 'max-content',
+ },
+ {
+ 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',
+ },
+ {
+ label: 'State terraform version',
+ key: 'state-terraform-version',
+ isSortable: true,
+ width: 'max-content',
+ },
+ {
+ label: 'Created',
+ key: 'created',
+ isSortable: true,
+ width: 'max-content',
+ },
+ {
+ label: 'Updated',
+ key: 'updated',
+ isSortable: true,
+ width: 'max-content',
+ },
+];
+
+const CUSTOM_FILTER = {
+ type: 'generic',
+ dismissTagText: 'equals example/a))!hzfpKcBl0',
+ data: {
+ value: 'example/a))!hzfpKcBl0',
+ },
+} as HdsFilterBarGenericFilter;
+
const updateModelWithSelectAllState = (
modelData: HdsAdvancedTableSignature['Args']['model'],
selectAllState: boolean,
@@ -499,6 +574,9 @@ export default class MockAppMainGenericAdvancedTable extends Component {
+ console.log('onFilter called with filters: ', filters);
+ this.filters = filters;
+ };
+
+ get demoModelFilteredData() {
+ const filterItem = (item: Record): boolean => {
+ if (Object.keys(this.filters).length === 0) return true;
+ let match = true;
+ Object.keys(this.filters).forEach((key) => {
+ const filter = this.filters[key] as HdsFilterBarFilter;
+ if (filter) {
+ switch (filter.type) {
+ case 'date':
+ case 'datetime':
+ case 'time':
+ if (!this.isDateFilterMatch(item[key], filter)) {
+ match = false;
+ }
+ break;
+ case 'numerical':
+ if (!this.isNumericalFilterMatch(item[key], filter)) {
+ match = false;
+ }
+ break;
+ case 'single-select':
+ if (!this.isSingleSelectFilterMatch(item[key], filter)) {
+ match = false;
+ }
+ break;
+ case 'search':
+ if (!this.isSearchFilterMatch(item, filter)) {
+ match = false;
+ }
+ break;
+ case 'generic':
+ if (!this.isGenericFilterMatch(item[key], filter)) {
+ match = false;
+ }
+ break;
+ default:
+ if (!this.isMultiSelectFilterMatch(item[key], filter)) {
+ match = false;
+ }
+ }
+ }
+ });
+ return match;
+ };
+
+ const filteredData = this.demoModel.filter(filterItem);
+ return filteredData;
+ }
+
+ get noFilterData() {
+ return this.demoModelFilteredData.length === 0;
+ }
+
+ isNumericalFilterMatch(
+ itemValue: unknown,
+ filter: HdsFilterBarNumericalFilter,
+ ): boolean {
+ const filterData = filter.data;
+ const selector = filterData.selector;
+ const number = Number(itemValue);
+
+ const value = filterData.value;
+ const valueNumber = Number(value);
+
+ if (isNaN(number)) {
+ return false;
+ } else if (!isNaN(valueNumber)) {
+ switch (selector) {
+ case 'less-than':
+ return number < valueNumber;
+ case 'less-than-or-equal-to':
+ return number <= valueNumber;
+ case 'equal-to':
+ return number === valueNumber;
+ case 'greater-than-or-equal-to':
+ return number >= valueNumber;
+ case 'greater-than':
+ return number > valueNumber;
+ default:
+ return false;
+ }
+ } else if (selector === 'between' && typeof value === 'object') {
+ if (!value.start || !value.end) {
+ return false;
+ }
+ return number >= value.start && number <= value.end;
+ }
+
+ return false;
+ }
+
+ isDateFilterMatch(
+ itemValue: unknown,
+ filter: HdsFilterBarDateFilter,
+ ): boolean {
+ const filterData = filter.data;
+ const selector = filterData.selector;
+ const value = filterData.value;
+
+ const date = this.dateFromFilter(String(itemValue), filter.type);
+
+ if (selector === 'between' && typeof value === 'object') {
+ if (!value.start || !value.end) {
+ return false;
+ }
+ const startDate = this.dateFromFilter(value.start, filter.type);
+ const endDate = this.dateFromFilter(value.end, filter.type);
+ if (this.dateIsValid(startDate) && this.dateIsValid(endDate)) {
+ return (
+ date.getTime() >= startDate.getTime() &&
+ date.getTime() <= endDate.getTime()
+ );
+ } else {
+ return false;
+ }
+ } else if (typeof value === 'string') {
+ const valueDate = this.dateFromFilter(value, filter.type);
+ if (this.dateIsValid(valueDate)) {
+ switch (selector) {
+ case 'before':
+ return date.getTime() < valueDate.getTime();
+ case 'exactly':
+ return date.getTime() === valueDate.getTime();
+ case 'after':
+ return date.getTime() > valueDate.getTime();
+ default:
+ return false;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ isSingleSelectFilterMatch(
+ itemValue: unknown,
+ filter: HdsFilterBarSingleSelectFilter,
+ ): boolean {
+ return itemValue === filter.data.value;
+ }
+
+ isMultiSelectFilterMatch(
+ itemValue: unknown,
+ filter: HdsFilterBarMultiSelectFilter,
+ ): boolean {
+ const filterValues = filter.data.map((d) => d.value);
+ return filterValues.includes(itemValue);
+ }
+
+ isSearchFilterMatch(
+ item: Record,
+ filter: HdsFilterBarSearchFilter,
+ ): boolean {
+ let match = false;
+ Object.keys(item).forEach((key) => {
+ const itemValue = item[key];
+ const filterValue = filter.data.value;
+ if (
+ typeof itemValue === 'string' &&
+ typeof filterValue === 'string' &&
+ itemValue.toLowerCase().includes(filterValue.toLowerCase())
+ ) {
+ match = true;
+ }
+ });
+ return match;
+ }
+
+ isGenericFilterMatch(
+ itemValue: unknown,
+ filter: HdsFilterBarGenericFilter,
+ ): boolean {
+ if (Array.isArray(filter.data)) {
+ const filterValues = filter.data.map((d) => d.value);
+ return filterValues.includes(itemValue);
+ } else {
+ return itemValue === filter.data.value;
+ }
+ }
+
+ dateFromFilter = (dateString: string, filterType: string): Date => {
+ if (filterType === 'time') {
+ return new Date(`1970-01-01T${dateString}`);
+ }
+ return new Date(dateString);
+ };
+
+ dateIsValid = (date?: Date | string): date is Date =>
+ date instanceof Date && !isNaN(+date);
+
+ clearFilters = () => {
+ this.filters = {};
+ };
+
+ onSeparatedFilterBar = (event: Event) => {
+ const target = event.target as HTMLInputElement;
+ this.isSeparatedFilterBar = target.checked;
+ };
+
+ onLiveFilterToggle = (event: Event) => {
+ const target = event.target as HTMLInputElement;
+ this.isLiveFilter = target.checked;
+ };
+
+
+
+ Separated filter bar component
+
+
+
+
+
+ Live filtering
+
+
+
+ {{#if this.isSeparatedFilterBar}}
+
+
+
+
+
+
+ access
+ homework
+ discovery
+ memories
+
+
+
+ {{#each (get SAMPLE_MODEL_VALUES "name") as |option|}}
+
+ {{/each}}
+
+
+ {{#each (get SAMPLE_MODEL_VALUES "project-name") as |option|}}
+
+ {{/each}}
+
+
+ {{#each (get SAMPLE_MODEL_VALUES "run-status") as |option|}}
+
+ {{/each}}
+
+
+ {{#each (get SAMPLE_MODEL_VALUES "terraform-version") as |option|}}
+
+ {{/each}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{/if}}
+
+ <:actions as |A|>
+ {{#unless this.isSeparatedFilterBar}}
+
+
+
+
+
+
+ access
+ homework
+ discovery
+ memories
+
+
+
+ {{#each (get SAMPLE_MODEL_VALUES "name") as |option|}}
+
+ {{/each}}
+
+
+ {{#each (get SAMPLE_MODEL_VALUES "project-name") as |option|}}
+
+ {{/each}}
+
+
+ {{#each (get SAMPLE_MODEL_VALUES "run-status") as |option|}}
+
+ {{/each}}
+
+
+ {{#each
+ (get SAMPLE_MODEL_VALUES "terraform-version")
+ as |option|
+ }}
+
+ {{/each}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{/unless}}
+
<:body as |B|>
{{! @glint-expect-error }}
@@ -563,6 +1076,10 @@ export default class MockAppMainGenericAdvancedTable extends Component
+
+ {{! @glint-expect-error }}
+ {{get B.data "creation-time"}}
+
{{! @glint-expect-error }}
{{get B.data "vcs-repo"}}
@@ -607,6 +1124,23 @@ export default class MockAppMainGenericAdvancedTable extends Component
+ <:emptyState>
+ {{#if this.noFilterData}}
+
+ No data to display
+
+ No results were found with the selected filters. Please clear or
+ update the filters.
+
+
+
+
+
+ {{/if}}
+
}
diff --git a/showcase/app/components/page-components/filter-bar/index.gts b/showcase/app/components/page-components/filter-bar/index.gts
new file mode 100644
index 00000000000..1d226a56507
--- /dev/null
+++ b/showcase/app/components/page-components/filter-bar/index.gts
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import type { TemplateOnlyComponent } from '@ember/component/template-only';
+import { pageTitle } from 'ember-page-title';
+
+import ShwTextH1 from 'showcase/components/shw/text/h1';
+
+import SubSectionBasic from 'showcase/components/page-components/filter-bar/sub-sections/basic';
+
+const FilterBarIndex: TemplateOnlyComponent =
+ {{pageTitle "FilterBar Component"}}
+
+ Filter Bar
+
+
+ ;
+
+export default FilterBarIndex;
diff --git a/showcase/app/components/page-components/filter-bar/sub-sections/basic.gts b/showcase/app/components/page-components/filter-bar/sub-sections/basic.gts
new file mode 100644
index 00000000000..debaffc9d97
--- /dev/null
+++ b/showcase/app/components/page-components/filter-bar/sub-sections/basic.gts
@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Component from '@glimmer/component';
+
+import ShwDivider from 'showcase/components/shw/divider';
+import ShwPlaceholder from 'showcase/components/shw/placeholder';
+import ShwTextH2 from 'showcase/components/shw/text/h2';
+
+import { HdsFilterBar } from '@hashicorp/design-system-components/components';
+
+export default class SubSectionBasic extends Component {
+ private _filters = {};
+
+
+ Basic usage
+
+
+
+
+
+
+
+ Action 1
+ Action 2
+ Action 3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+}
diff --git a/showcase/app/router.ts b/showcase/app/router.ts
index d83532978cf..ee573d1cb93 100644
--- a/showcase/app/router.ts
+++ b/showcase/app/router.ts
@@ -50,6 +50,7 @@ Router.map(function () {
this.route('code-block');
this.route('code-editor');
this.route('dropdown');
+ this.route('filter-bar');
this.route('flyout');
this.route('form', function (): void {
this.route('frameless', function (): void {
diff --git a/showcase/app/routes/page-components/filter-bar.ts b/showcase/app/routes/page-components/filter-bar.ts
new file mode 100644
index 00000000000..9128e10b5a7
--- /dev/null
+++ b/showcase/app/routes/page-components/filter-bar.ts
@@ -0,0 +1,13 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Route from '@ember/routing/route';
+
+import type { ModelFrom } from 'showcase/utils/model-from-route';
+
+export type PageComponentsFilterBarModel =
+ ModelFrom;
+
+export default class PageComponentsFilterBarRoute extends Route {}
diff --git a/showcase/app/templates/index.gts b/showcase/app/templates/index.gts
index b5369de0bc8..0acf969458f 100644
--- a/showcase/app/templates/index.gts
+++ b/showcase/app/templates/index.gts
@@ -137,6 +137,11 @@ const Index: TemplateOnlyComponent =
Dropdown
+
+
+ FilterBar
+
+
Flyout
diff --git a/showcase/app/templates/page-components/filter-bar.gts b/showcase/app/templates/page-components/filter-bar.gts
new file mode 100644
index 00000000000..a3265fd8f25
--- /dev/null
+++ b/showcase/app/templates/page-components/filter-bar.gts
@@ -0,0 +1,14 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import type { TemplateOnlyComponent } from '@ember/component/template-only';
+
+import FilterBarIndex from 'showcase/components/page-components/filter-bar/index';
+
+const PageComponentsFilterBar: TemplateOnlyComponent =
+
+ ;
+
+export default PageComponentsFilterBar;
diff --git a/website/docs/components/filter-bar/index.js b/website/docs/components/filter-bar/index.js
new file mode 100644
index 00000000000..14325f981d9
--- /dev/null
+++ b/website/docs/components/filter-bar/index.js
@@ -0,0 +1,11 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+
+export default class Index extends Component {
+ @tracked filters = {};
+}
diff --git a/website/docs/components/filter-bar/index.md b/website/docs/components/filter-bar/index.md
new file mode 100644
index 00000000000..18d7b5d9be6
--- /dev/null
+++ b/website/docs/components/filter-bar/index.md
@@ -0,0 +1,38 @@
+---
+title: Filter Bar
+description: A composition of HDS components to support filtering a data set.
+caption: A composition of HDS components to support filtering a data set.
+links:
+ figma: >-
+ https://www.figma.com/design/iweq3r2Pi8xiJfD9e6lOhF/HDS-Components-v2.0?node-id=67385-76599&t=w8xQlWxzH7bwXLe2-1
+ github: >-
+ https://github.com/hashicorp/design-system/tree/main/packages/components/src/components/hds/filter-bar
+related:
+ - components/table/advanced-table
+ - components/table/table
+ - patterns/filter-patterns
+previewImage: assets/illustrations/components/filter-bar.jpg
+navigation:
+ keywords:
+ - filtering
+status:
+ added: 5.2.0
+---
+
+
+ @include "partials/guidelines/guidelines.md"
+
+
+
+ @include "partials/code/how-to-use.md"
+ @include "partials/code/component-api.md"
+
+
+
+ @include "partials/specifications/anatomy.md"
+ @include "partials/specifications/states.md"
+
+
+
+ @include "partials/accessibility/accessibility.md"
+
diff --git a/website/docs/components/filter-bar/partials/accessibility/accessibility.md b/website/docs/components/filter-bar/partials/accessibility/accessibility.md
new file mode 100644
index 00000000000..99e8e973dff
--- /dev/null
+++ b/website/docs/components/filter-bar/partials/accessibility/accessibility.md
@@ -0,0 +1,11 @@
+## Conformance Rating
+
+## Applicable WCAG Success Criteria
+
+This section is for reference only. This component intends to conform to the following WCAG Success Criteria:
+
+
+
+---
+
+
diff --git a/website/docs/components/filter-bar/partials/code/component-api.md b/website/docs/components/filter-bar/partials/code/component-api.md
new file mode 100644
index 00000000000..a42383d5793
--- /dev/null
+++ b/website/docs/components/filter-bar/partials/code/component-api.md
@@ -0,0 +1,11 @@
+## Component API
+
+### FilterBar
+
+
+
+ This component supports use of [`...attributes`](https://guides.emberjs.com/release/in-depth-topics/patterns-for-components/#toc_attribute-ordering).
+
+
+
+### Contextual components
diff --git a/website/docs/components/filter-bar/partials/code/how-to-use.md b/website/docs/components/filter-bar/partials/code/how-to-use.md
new file mode 100644
index 00000000000..eb6ea3b1674
--- /dev/null
+++ b/website/docs/components/filter-bar/partials/code/how-to-use.md
@@ -0,0 +1,28 @@
+## How to use this component
+
+```handlebars
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
diff --git a/website/docs/components/filter-bar/partials/guidelines/guidelines.md b/website/docs/components/filter-bar/partials/guidelines/guidelines.md
new file mode 100644
index 00000000000..da460c1e1d7
--- /dev/null
+++ b/website/docs/components/filter-bar/partials/guidelines/guidelines.md
@@ -0,0 +1,183 @@
+The Filter Bar is used to apply and display filters to a data set. It is most often used in conjunction with the [Advanced Table](/components/table/advanced-table), but is flexible enough to support different data sets and rendering methods such as in a list or grid of cards.
+
+The Filter Bar comes paired with a complex dropdown menu that displays available filter parameters (a parameter is often the equivalent of a column in a table), values within each parameter, support for numerical/date/time values, ranges of values, and actions to apply and clear filters from the data set.
+
+!!! Callout
+
+While the Filter Bar underwent rigorous research and testing, this component is relatively complex. If specific functionality or the filtering methods don't meet your needs, please [contact the Design Systems Team](/about/support) so we can provide support.
+!!!
+
+## Usage
+
+### When to use
+
+- When displaying relevant filters and filters that have been applied to a data set.
+- For common filter methods like multi-selection, single selection, numbers, dates, and times.
+- As a direct replacement for the HDS [Filter patterns](/patterns/filter-patterns) guidance.
+
+### When not to use
+
+- For complex query builder features.
+
+## Overlap with the Filter pattern guidance
+
+The Filter Bar component is a successor to the [Filter patterns](/patterns/filter-patterns) guidance and supports the vast majority of filtering experiences within HashiCorp applications out of the box. New experiences should use the Filter Bar by default instead of the adhering to the pattern guidance, while already delivered features should consider migrating to the Filter Bar component.
+
+## Type
+
+The Filter Bar supports two visual presentations, `attached` and `standalone`, to be used in different contexts and with different types of data sets.
+
+### Attached
+
+Use the `attached` variant with the [Advanced Table](/components/table/advanced-table) and standard [Table](/components/table/table).
+
+
+
+### Standalone
+
+Use the `standalone` variant when a data set is rendered in formats other than a table, e.g., a list or array of cards.
+
+
+
+## Expand & collapse
+
+The Filter Bar supports expanding and collapsing the applied filters section to help simplify the UI around the data set and bring focus to the data or content. This is especially helpful when many filters are applied or the data set is very complex.
+
+
+
+When no filters are applied, the Filter Bar is collapsed by default and displays an empty state message when expanded.
+
+
+
+When one or more filters are applied the Filter Bar is expanded by default.
+
+
+
+## Search
+
+Use the search input in the Filter Bar to apply a broad text/string-based filter across the entire data set.
+
+
+
+## Bulk actions
+
+The Filter Bar supports bulk actions corresponding with our recommendations for [multi-select](/patterns/table-multi-select) within a table, and can be used to perform actions across multiple results such as edit, delete, and different selection methods across the data set.
+
+
+
+## Generic content
+
+If custom functionality is needed for manipulating the view or contents of the data set, a generic block is grouped with the bulk actions in the Filter Bar. We aren't prescriptive about what can be passed to this generic block, but it should generally be limited to additional actions (as [Buttons](/components/button)) and [Dropdowns](/components/dropdown) with multiple grouped actions.
+
+
+
+## Applied filters
+
+Applied filters are represented by a [Tag](/components/tag) displaying the filter parameter (the category or column the filter corresponds to) and the filter value (corresponding with the specific cell content).
+
+
+
+The text rendered within the Tag uses a standardized format depending on the type of filter and the method:
+
+- Single and multiple selection filters group the parameter and value using a colon; e.g., "Region: AWS (us-east)".
+- Numerical filters group the parameter and value with an operator symbol; e.g., "Modules > 50".
+- Date and time filters group the parameter and value with natural language; e.g., "Creation time before 12:00 PM".
+
+
+
+For a full list of supported operators visit the [specifications](/components/filter-bar?tab=specifications#value-input-operators) page.
+
+### Custom applied filter text
+
+If necessary, the default formatting within the Tag can be overidden with custom text. This can be useful if the label of the parameter is an irregular plural, if the parameter reads more naturally with certain punctuation or grammar, or for product-specific reasons.
+
+
+
+
+
+## Filter dropdown
+
+The Filter Dropdown is responsible for the selection and application of filters and is broken two "panels":
+
+- The left panel displays the list of parameters (categories) that can be filtered upon.
+- The right panel conditionally displays either a list of options, or a grouping of input fields when filtering by a numerical, date, or time value.
+
+
+
+Which filtering method to use depends on the data type and context. In the Figma component, select the `type` from the nested FilterValue component. In Ember, pass the filter type and method to in the configuration.
+
+### Multi-selection
+
+Multi-selection supports the selection of multiple values in a list of options and is the most common method of filtering. It is suited for categorical data like statuses and IDs, but can also be used more generally to filter by a handful of similar values.
+
+
+
+### Single-selection
+
+Single selection supports a _mutually exclusive_ selection using radio buttons; e.g., the selection of a value where more than one selection will logically result in an empty state, therefore, only value can be selected.
+
+
+
+### Numbers, dates, and times
+
+Filtering by numerical values, dates, and times is handled through the combination of an operator (greater than, less than, before, etc.) and an input field or grouping of input fields. This filtering method is best suited for range-based filtering; e.g., filtering by results relationally compared to the value or range of values.
+
+
+
+
+
+
+
+For a full list of supported operators visit the [specifications](/components/filter-bar?tab=specifications#value-input-operators) page.
+
+### Search across filter values
+
+Use the integrated search (`hasSearch`) in the values panel to allow users to search by string across all values within a selected parameter. This can be useful if there are many filter values or if a unique naming convention is used to artificially group results together.
+
+
+
+## Applying Filters
+
+Depending on how the data set is fetched and updated, the Filter Bar supports different methods of applying filters:
+
+- **Per-filter:** selected filters are applied when the user confirms their selection with the "Apply filters" submit button. This is the most common method and generally requires a database or API call to update the data set with the corresponding filter parameters.
+- **Live filtering:** filters are applied immediately upon selection. This method generally requires updating the data set on the client.
+
+
+
+## Clearing filters
+
+Filters can be cleared either in bulk or individually from the Filter Bar or Dropdown.
+
+### Filter Bar
+
+Clear all filters with the Button in the Filter Bar.
+
+
+
+Clear filters individually via the dismiss button of the Tag.
+
+
+
+### Filter Dropdown
+
+Clear all filters via the Button in the footer of the Dropdown.
+
+
+
+Deselect all filter values within a parameter with the "Clear selection" Button, then confirm the changes with the "Apply filters" Button in the footer.
+
+
+
+Clear filter input fields with the "Clear filter" Button, then apply the changes with the "Apply filters" Button in the footer.
+
+
+
+## Empty state
+
+An empty state occurs if the filters applied don't match any results in the data set. This is supported out of the box in the [Advanced Table](/components/table/advanced-table/) and will display an informative message about the empty state and prompt users to either adjust the applied filters or clear all of them.
+
+
+
+If the data set is being rendered in a list or method outside of what is directly supported by HDS, use the [Application State](/components/application-state) to communicate the empty state result from the applied filters.
+
diff --git a/website/docs/components/filter-bar/partials/specifications/anatomy.md b/website/docs/components/filter-bar/partials/specifications/anatomy.md
new file mode 100644
index 00000000000..9e91128ede6
--- /dev/null
+++ b/website/docs/components/filter-bar/partials/specifications/anatomy.md
@@ -0,0 +1,76 @@
+## Anatomy
+
+### Filter Bar
+
+
+
+| Element | Usage |
+|------------------|------------------------|
+| Collapse/expand | Required; Toggles open and closed the applied filters. |
+| Filter toggle button | Required; Toggles open and closed the Filter Dropdown. |
+| Search | Optional; Searches based on string matching across the entire data set. |
+| Generic content | Optional |
+| Bulk actions | Optional; Supports performing bulk actions on the data set. |
+| Applied Filters List | Required; Displays the filters that have been applied to the data set, or an empty state. |
+| Applied Filter Tag | Required; Displays the filter as `Parameter: Value` within an HDS [Tag](/components/tag). |
+| Clear All Filters | Required; Clears all of the applied filters, sets the Filter Bar and the data set back to their default state. |
+
+### Filter Dropdown
+
+The Filter Dropdown anatomy is dependent on the filter method and is dependent on the filtering method:
+
+- Selection: multi-selection using checkboxes or single selection using radios.
+- Input: numerical, date, time, datetime values, or a range between a start and end value.
+
+
+
+| Element | Usage |
+|------------------|------------------------|
+| Parameters list | Required; Displays all of the available filter parameters, generally corresponding with columns in a table. |
+| Filter value(s) | Required; Displays available values within a parameter for selection, or allows the input of custom values. |
+| Apply Filters action | Required if the filtering method is "per-filter". Applies the filters selected in either the values list or the values input. |
+| Clear all filters action | Required; Clears the selection of filters across all parameters. |
+
+#### Filter value selection
+
+
+
+
+| Element | Usage |
+|------------------|------------------------|
+| Search | Optional; Searches across available filter parameter values using string matching. |
+| Clear selected values action | Required; De-selects all values in the list. |
+| Value input | Required; Supports filtering on numbers, strings, dates, or times. |
+
+#### Filter value input
+
+
+
+| Element | Usage |
+|------------------|------------------------|
+| Operator selection | Requried; Allows the user to select how the filter is applied. |
+| Value input | Required; Allows the user to input a value to filter upon such as a number, string, date, or time. |
+| Clear filter | Required; Clears the filter operator and input fields. |
+
+#### Filter value range input
+
+When applying a filter value via an input, if `Between` is selected in the operator list, the input fields will be broken into a `start` value and `end` value.
+
+
+
+| Element | Usage |
+|------------------|------------------------|
+| Operator selection | Required; Allows the user to select how the filter is applied. |
+| Value range input | Required; Allows the user to input a start and end value to filter upon. |
+| Clear filter fields action | Required; Clears the filter input field. |
+
+##### Value input operators
+
+Available operators are dependent on the type of data being filtered upon, but are generally either numerical, or date/time based.
+
+
+
+| Type | Options |
+|------|-----------|
+| Numerical | Less than (>), Less than or equal to (≤), Equal to (=), Not equal to (≠), Greater than or equal to (≥), Greater than (>), Between |
+| Date and time | Before, Exactly, After, Between |
\ No newline at end of file
diff --git a/website/docs/components/filter-bar/partials/specifications/states.md b/website/docs/components/filter-bar/partials/specifications/states.md
new file mode 100644
index 00000000000..87f80b3e867
--- /dev/null
+++ b/website/docs/components/filter-bar/partials/specifications/states.md
@@ -0,0 +1,5 @@
+## States
+
+### Parameter List Item
+
+
\ No newline at end of file
diff --git a/website/docs/patterns/filter-patterns/partials/guidelines/overview.md b/website/docs/patterns/filter-patterns/partials/guidelines/overview.md
index 30c93009836..a4246b86a78 100644
--- a/website/docs/patterns/filter-patterns/partials/guidelines/overview.md
+++ b/website/docs/patterns/filter-patterns/partials/guidelines/overview.md
@@ -1 +1,6 @@
+!!! Insight
+
+In version [5.2.0](/whats-new/release-notes#520) the [Filter Bar](/components/filter-bar) component was released to support direct filtering with HDS components. While this documentation is still relevant for filtering experiences that have been built around this guidance, new filtering features and experiences should use the Filter Bar component instead, while already delivered features should consider migrating.
+!!!
+
Filtering is used to limit the objects in a data set based on one or more parameters. It is commonly used in tandem with a [Table](/components/table/table) or [Advanced Table](/components/table/advanced-table), but the core concepts have a wide range of relevant use cases depending on the type of data set and the context within the application.
diff --git a/website/public/assets/components/filter-bar/filter-bar-anatomy.png b/website/public/assets/components/filter-bar/filter-bar-anatomy.png
new file mode 100644
index 00000000000..3114de32436
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-anatomy.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-bulk-actions.png b/website/public/assets/components/filter-bar/filter-bar-bulk-actions.png
new file mode 100644
index 00000000000..99af8238f43
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-bulk-actions.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-clear-all-filters.png b/website/public/assets/components/filter-bar/filter-bar-clear-all-filters.png
new file mode 100644
index 00000000000..cb97f139cdc
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-clear-all-filters.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-clear-individual-filter.png b/website/public/assets/components/filter-bar/filter-bar-clear-individual-filter.png
new file mode 100644
index 00000000000..c53bceca1c8
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-clear-individual-filter.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-collapsed.png b/website/public/assets/components/filter-bar/filter-bar-collapsed.png
new file mode 100644
index 00000000000..a84000d2bf4
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-collapsed.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-dropdown-anatomy-input-range.png b/website/public/assets/components/filter-bar/filter-bar-dropdown-anatomy-input-range.png
new file mode 100644
index 00000000000..479cd42e776
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-dropdown-anatomy-input-range.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-dropdown-anatomy-input.png b/website/public/assets/components/filter-bar/filter-bar-dropdown-anatomy-input.png
new file mode 100644
index 00000000000..0786803a3c3
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-dropdown-anatomy-input.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-dropdown-anatomy-selection.png b/website/public/assets/components/filter-bar/filter-bar-dropdown-anatomy-selection.png
new file mode 100644
index 00000000000..e44fa4502b6
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-dropdown-anatomy-selection.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-dropdown-anatomy.png b/website/public/assets/components/filter-bar/filter-bar-dropdown-anatomy.png
new file mode 100644
index 00000000000..8eaeb9d0f78
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-dropdown-anatomy.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-dropdown-clear-all-filters.png b/website/public/assets/components/filter-bar/filter-bar-dropdown-clear-all-filters.png
new file mode 100644
index 00000000000..0e696f6e1bc
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-dropdown-clear-all-filters.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-dropdown-clear-filter-input.png b/website/public/assets/components/filter-bar/filter-bar-dropdown-clear-filter-input.png
new file mode 100644
index 00000000000..661bf517028
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-dropdown-clear-filter-input.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-dropdown-clear-selection-filter.png b/website/public/assets/components/filter-bar/filter-bar-dropdown-clear-selection-filter.png
new file mode 100644
index 00000000000..53eee1ac76e
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-dropdown-clear-selection-filter.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-dropdown-date-filter.png b/website/public/assets/components/filter-bar/filter-bar-dropdown-date-filter.png
new file mode 100644
index 00000000000..ad02d3eeb6a
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-dropdown-date-filter.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-dropdown-multi-selection.png b/website/public/assets/components/filter-bar/filter-bar-dropdown-multi-selection.png
new file mode 100644
index 00000000000..b6bba8e34ae
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-dropdown-multi-selection.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-dropdown-numerical-filter.png b/website/public/assets/components/filter-bar/filter-bar-dropdown-numerical-filter.png
new file mode 100644
index 00000000000..58fe7fda1ca
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-dropdown-numerical-filter.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-dropdown-open.png b/website/public/assets/components/filter-bar/filter-bar-dropdown-open.png
new file mode 100644
index 00000000000..dd8453cfd83
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-dropdown-open.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-dropdown-operator-options.png b/website/public/assets/components/filter-bar/filter-bar-dropdown-operator-options.png
new file mode 100644
index 00000000000..b5ee5f969e6
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-dropdown-operator-options.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-dropdown-range-input.png b/website/public/assets/components/filter-bar/filter-bar-dropdown-range-input.png
new file mode 100644
index 00000000000..58a5c260388
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-dropdown-range-input.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-dropdown-search-values.png b/website/public/assets/components/filter-bar/filter-bar-dropdown-search-values.png
new file mode 100644
index 00000000000..979a393a6a2
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-dropdown-search-values.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-dropdown-single-selection.png b/website/public/assets/components/filter-bar/filter-bar-dropdown-single-selection.png
new file mode 100644
index 00000000000..28285b3a986
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-dropdown-single-selection.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-dropdown-time-filter.png b/website/public/assets/components/filter-bar/filter-bar-dropdown-time-filter.png
new file mode 100644
index 00000000000..90c3199cd26
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-dropdown-time-filter.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-empty-state.png b/website/public/assets/components/filter-bar/filter-bar-empty-state.png
new file mode 100644
index 00000000000..5fb2f302555
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-empty-state.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-expanded-empty-state.png b/website/public/assets/components/filter-bar/filter-bar-expanded-empty-state.png
new file mode 100644
index 00000000000..041133a4688
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-expanded-empty-state.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-expanded-with-filters.png b/website/public/assets/components/filter-bar/filter-bar-expanded-with-filters.png
new file mode 100644
index 00000000000..4b798cd4b57
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-expanded-with-filters.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-generic-content.png b/website/public/assets/components/filter-bar/filter-bar-generic-content.png
new file mode 100644
index 00000000000..0191920e8c2
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-generic-content.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-parameter-list-item-states.png b/website/public/assets/components/filter-bar/filter-bar-parameter-list-item-states.png
new file mode 100644
index 00000000000..ce496a3b183
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-parameter-list-item-states.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-search-filled.png b/website/public/assets/components/filter-bar/filter-bar-search-filled.png
new file mode 100644
index 00000000000..785df4c5fa1
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-search-filled.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-tag-example.png b/website/public/assets/components/filter-bar/filter-bar-tag-example.png
new file mode 100644
index 00000000000..3001251a16e
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-tag-example.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-tag-filter-methods.png b/website/public/assets/components/filter-bar/filter-bar-tag-filter-methods.png
new file mode 100644
index 00000000000..07f2f52bef2
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-tag-filter-methods.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-type-attached.png b/website/public/assets/components/filter-bar/filter-bar-type-attached.png
new file mode 100644
index 00000000000..605a6fce856
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-type-attached.png differ
diff --git a/website/public/assets/components/filter-bar/filter-bar-type-standalone.png b/website/public/assets/components/filter-bar/filter-bar-type-standalone.png
new file mode 100644
index 00000000000..47cd7fc8993
Binary files /dev/null and b/website/public/assets/components/filter-bar/filter-bar-type-standalone.png differ
diff --git a/website/public/assets/illustrations/components/filter-bar.jpg b/website/public/assets/illustrations/components/filter-bar.jpg
new file mode 100644
index 00000000000..fb002b9865a
Binary files /dev/null and b/website/public/assets/illustrations/components/filter-bar.jpg differ
diff --git a/website/tests/acceptance/components/filter-bar-test.js b/website/tests/acceptance/components/filter-bar-test.js
new file mode 100644
index 00000000000..bf64a15302c
--- /dev/null
+++ b/website/tests/acceptance/components/filter-bar-test.js
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import { module, test } from 'qunit';
+import { visit, currentURL } from '@ember/test-helpers';
+import { setupApplicationTest } from 'website/tests/helpers';
+import { a11yAudit } from 'ember-a11y-testing/test-support';
+
+module('Acceptance | components/filter-bar', function (hooks) {
+ setupApplicationTest(hooks);
+
+ test('visiting /components/filter-bar', async function (assert) {
+ await visit('/components/filter-bar');
+
+ assert.strictEqual(currentURL(), '/components/filter-bar');
+ });
+
+ test('components/card page passes automated a11y checks', async function (assert) {
+ await visit('/components/filter-bar');
+ await a11yAudit();
+ assert.ok(true, 'a11y automation audit passed');
+ });
+});