From de32eca3b6248c201703f527c4658c1e7b832837 Mon Sep 17 00:00:00 2001 From: AronH99 Date: Wed, 15 Jul 2026 10:31:58 +0200 Subject: [PATCH 1/7] Move filter building blocks to shared UI components and reuse across drawers Co-Authored-By: Claude Opus 4.8 --- .../7027-shared-filter-components.yml | 5 + .../UI/FilterWidget/FilterWidgetComponent.tsx | 168 ++------ .../FilterWidget/FilterWidgetComponent.tsx | 214 +++-------- .../DesiredStateDetailsFilterWidget.tsx | 88 ++++- .../UI/FilterWidget/FilterWidgetComponent.tsx | 363 ++++-------------- .../UI/FilterWidget/FilterWidgetComponent.tsx | 320 +++------------ .../FilterWidget/ActiveFiltersSection.tsx | 125 +++--- .../FilterWidget/ResourceFilterForm.tsx | 19 +- .../Components/FilterWidget/index.ts | 3 - .../DiscoveredResourcesFilterWidget.tsx | 92 ++++- .../AddableSelectInput.test.tsx | 85 ++-- .../AddableSelectInput.tsx | 26 +- src/UI/Components/AddableSelectInput/index.ts | 1 + .../AddableTextInput.test.tsx | 64 +-- .../AddableTextInput}/AddableTextInput.tsx | 20 +- src/UI/Components/AddableTextInput/index.ts | 1 + .../FilterDrawer}/ActiveFilterGroup.test.tsx | 1 - .../FilterDrawer}/ActiveFilterGroup.tsx | 6 +- .../Components/FilterDrawer/ActiveFilters.tsx | 75 ++++ src/UI/Components/FilterDrawer/index.ts | 3 + .../utils.test.ts | 0 .../{FilterWidget => FilterDrawer}/utils.ts | 0 .../FilterWidgetComponent.test.tsx | 210 ---------- .../FilterWidget/FilterWidgetComponent.tsx | 169 -------- src/UI/Components/FilterWidget/index.ts | 2 - src/UI/Components/Filters/IntRangeFilter.tsx | 135 ------- src/UI/Components/Filters/TimestampFilter.tsx | 2 + src/UI/Components/Filters/TimestampPicker.tsx | 1 - .../Filters/TimestampRangeFilter.tsx | 111 ++++++ src/UI/Components/Filters/index.ts | 2 +- src/UI/Components/index.ts | 3 +- src/UI/words.tsx | 1 - 32 files changed, 725 insertions(+), 1590 deletions(-) create mode 100644 changelogs/unreleased/7027-shared-filter-components.yml rename src/{Slices/Resource/UI/ResourcesPage/Components/FilterWidget => UI/Components/AddableSelectInput}/AddableSelectInput.test.tsx (54%) rename src/{Slices/Resource/UI/ResourcesPage/Components/FilterWidget => UI/Components/AddableSelectInput}/AddableSelectInput.tsx (91%) create mode 100644 src/UI/Components/AddableSelectInput/index.ts rename src/{Slices/Resource/UI/ResourcesPage/Components/FilterWidget => UI/Components/AddableTextInput}/AddableTextInput.test.tsx (58%) rename src/{Slices/Resource/UI/ResourcesPage/Components/FilterWidget => UI/Components/AddableTextInput}/AddableTextInput.tsx (80%) create mode 100644 src/UI/Components/AddableTextInput/index.ts rename src/{Slices/Resource/UI/ResourcesPage/Components/FilterWidget => UI/Components/FilterDrawer}/ActiveFilterGroup.test.tsx (98%) rename src/{Slices/Resource/UI/ResourcesPage/Components/FilterWidget => UI/Components/FilterDrawer}/ActiveFilterGroup.tsx (88%) create mode 100644 src/UI/Components/FilterDrawer/ActiveFilters.tsx rename src/UI/Components/{FilterWidget => FilterDrawer}/utils.test.ts (100%) rename src/UI/Components/{FilterWidget => FilterDrawer}/utils.ts (100%) delete mode 100644 src/UI/Components/FilterWidget/FilterWidgetComponent.test.tsx delete mode 100644 src/UI/Components/FilterWidget/FilterWidgetComponent.tsx delete mode 100644 src/UI/Components/FilterWidget/index.ts delete mode 100644 src/UI/Components/Filters/IntRangeFilter.tsx create mode 100644 src/UI/Components/Filters/TimestampRangeFilter.tsx diff --git a/changelogs/unreleased/7027-shared-filter-components.yml b/changelogs/unreleased/7027-shared-filter-components.yml new file mode 100644 index 0000000000..962945e9e2 --- /dev/null +++ b/changelogs/unreleased/7027-shared-filter-components.yml @@ -0,0 +1,5 @@ +description: Moved AddableTextInput, AddableSelectInput and ActiveFilterGroup into shared UI components, extracted shared ActiveFilters and TimestampRangeFilter building blocks, reused them across the filter drawers, and removed the config-driven FilterWidgetComponent. +issue-nr: 7027 +change-type: patch +destination-branches: [master] +sections: {} diff --git a/src/Slices/Agents/UI/FilterWidget/FilterWidgetComponent.tsx b/src/Slices/Agents/UI/FilterWidget/FilterWidgetComponent.tsx index a92d6ba210..4cfce29d72 100644 --- a/src/Slices/Agents/UI/FilterWidget/FilterWidgetComponent.tsx +++ b/src/Slices/Agents/UI/FilterWidget/FilterWidgetComponent.tsx @@ -1,27 +1,15 @@ -import React, { useState } from "react"; -import { - Button, - Divider, - EmptyState, - EmptyStateBody, - Flex, - FlexItem, - Form, - FormGroup, - InputGroup, - InputGroupItem, - Label, - LabelGroup, - Stack, - StackItem, - TextInput, - Title, -} from "@patternfly/react-core"; -import { PlusIcon } from "@patternfly/react-icons"; +import React from "react"; +import { Divider, Form, FormGroup, Stack, StackItem } from "@patternfly/react-core"; import { toggleValueInList } from "@/Core"; import { uniq } from "@/Core/Language/collection"; import { Filter } from "@/Slices/Agents/Core/Types"; -import { FilterDrawerPanelContent, MultiTextSelect } from "@/UI/Components"; +import { + ActiveFilterGroup, + ActiveFilters, + AddableTextInput, + FilterDrawerPanelContent, + MultiTextSelect, +} from "@/UI/Components"; import { words } from "@/UI/words"; import { AgentStatus } from "@S/Agents/Core/Domain"; @@ -46,21 +34,11 @@ interface Props { * @returns {React.ReactElement} The rendered filter widget. */ export const FilterWidgetComponent: React.FC = ({ filter, setFilter, onClose }) => { - const [nameInput, setNameInput] = useState(""); - const agentStatuses = Object.values(AgentStatus); // --- Name --- - const applyNameFilter = () => { - const trimmed = nameInput.trim(); - - if (!trimmed) { - return; - } - - setFilter({ ...filter, name: uniq([...(filter.name ?? []), trimmed]) }); - setNameInput(""); - }; + const addName = (value: string) => + setFilter({ ...filter, name: uniq([...(filter.name ?? []), value]) }); const removeNameChip = (value: string) => { const updated = (filter.name ?? []).filter((name) => name !== value); @@ -114,109 +92,39 @@ export const FilterWidgetComponent: React.FC = ({ filter, setFilter, onCl - - - - setNameInput(value)} - onKeyDown={(event) => { - if (event.key === "Enter") { - event.preventDefault(); - applyNameFilter(); - } - }} - type="search" - placeholder={words("agents.filters.name.placeholder")} - aria-label="NameFilterInput" - /> - - - - - - + - - - - - {words("resources.filters.active.title")} - - - - - - - - {hasActiveFilters ? ( - - {(filter.name?.length ?? 0) > 0 && ( - - - {(filter.name ?? []).map((name) => ( - - ))} - - - )} - {(filter.status?.length ?? 0) > 0 && ( - - - {(filter.status ?? []).map((status) => ( - - ))} - - - )} - - ) : ( - - - {words("resources.filters.active.empty.title")} - - {words("resources.filters.active.empty.body.noTabs")} - + + {(filter.name?.length ?? 0) > 0 && ( + + + + )} + {(filter.status?.length ?? 0) > 0 && ( + + + )} - + ); diff --git a/src/Slices/DesiredState/UI/Components/FilterWidget/FilterWidgetComponent.tsx b/src/Slices/DesiredState/UI/Components/FilterWidget/FilterWidgetComponent.tsx index 1182f3d715..2062037ff0 100644 --- a/src/Slices/DesiredState/UI/Components/FilterWidget/FilterWidgetComponent.tsx +++ b/src/Slices/DesiredState/UI/Components/FilterWidget/FilterWidgetComponent.tsx @@ -2,28 +2,28 @@ import React, { useState } from "react"; import { Button, Divider, - EmptyState, - EmptyStateBody, Flex, FlexItem, Form, FormGroup, InputGroup, InputGroupItem, - Label, - LabelGroup, Stack, StackItem, TextInput, - Title, } from "@patternfly/react-core"; import { PlusIcon } from "@patternfly/react-icons"; import { RangeOperator, toggleValueInList } from "@/Core"; import { uniq } from "@/Core/Language/collection"; import { Filter } from "@/Slices/DesiredState/Core/Types"; import { CustomDatePresenter } from "@/UI"; -import { FilterDrawerPanelContent, MultiTextSelect } from "@/UI/Components"; -import { TimestampPicker } from "@/UI/Components/Filters"; +import { + ActiveFilterGroup, + ActiveFilters, + FilterDrawerPanelContent, + MultiTextSelect, +} from "@/UI/Components"; +import { TimestampRangeFilter } from "@/UI/Components/Filters"; import { words } from "@/UI/words"; import { DesiredStateVersionStatus } from "@S/DesiredState/Core/Domain"; @@ -36,8 +36,6 @@ interface Props { } export const FilterWidgetComponent: React.FC = ({ filter, setFilter, onClose }) => { - const [dateFrom, setDateFrom] = useState(); - const [dateTo, setDateTo] = useState(); const [versionFrom, setVersionFrom] = useState(""); const [versionTo, setVersionTo] = useState(""); @@ -68,32 +66,6 @@ export const FilterWidgetComponent: React.FC = ({ filter, setFilter, onCl setFilter({ ...filter, status: undefined, disregardDefault: true }); // --- Date --- - const applyDateFromFilter = () => { - if (!dateFrom) { - return; - } - const updated = [ - ...(filter.date ?? []).filter((d) => d.operator !== RangeOperator.Operator.From), - { date: dateFrom, operator: RangeOperator.Operator.From }, - ]; - - setFilter({ ...filter, date: updated }); - setDateFrom(undefined); - }; - - const applyDateToFilter = () => { - if (!dateTo) { - return; - } - const updated = [ - ...(filter.date ?? []).filter((d) => d.operator !== RangeOperator.Operator.To), - { date: dateTo, operator: RangeOperator.Operator.To }, - ]; - - setFilter({ ...filter, date: updated }); - setDateTo(undefined); - }; - const removeDateChip = (chip: string) => { const operator = chip.split("|")[0].trim() as RangeOperator.Operator; const updated = (filter.date ?? []).filter((d) => d.operator !== operator); @@ -176,52 +148,13 @@ export const FilterWidgetComponent: React.FC = ({ filter, setFilter, onCl - - - - - - - - } - /> - - - - - - - - } - /> - - - - + setFilter({ ...filter, date })} + /> @@ -284,95 +217,38 @@ export const FilterWidgetComponent: React.FC = ({ filter, setFilter, onCl - - - - - {words("resources.filters.active.title")} - - - - - - - - {hasActiveFilters ? ( - - {(filter.status ?? []).length > 0 && ( - - - {(filter.status ?? []).map((s) => ( - - ))} - - - )} - {dateChips.length > 0 && ( - - - {dateChips.map((chip) => ( - - ))} - - - )} - {versionChips.length > 0 && ( - - - {versionChips.map((chip) => ( - - ))} - - - )} - - ) : ( - - - {words("resources.filters.active.empty.title")} - - {words("resources.filters.active.empty.body.noTabs")} - + + {(filter.status ?? []).length > 0 && ( + + + + )} + {dateChips.length > 0 && ( + + + + )} + {versionChips.length > 0 && ( + + + )} - + ); diff --git a/src/Slices/DesiredStateDetails/UI/Controls/FilterWidget/DesiredStateDetailsFilterWidget.tsx b/src/Slices/DesiredStateDetails/UI/Controls/FilterWidget/DesiredStateDetailsFilterWidget.tsx index e1736c0d4d..211c8b22ca 100644 --- a/src/Slices/DesiredStateDetails/UI/Controls/FilterWidget/DesiredStateDetailsFilterWidget.tsx +++ b/src/Slices/DesiredStateDetails/UI/Controls/FilterWidget/DesiredStateDetailsFilterWidget.tsx @@ -1,38 +1,47 @@ import React, { memo } from "react"; +import { Divider, Form, Stack, StackItem, Title } from "@patternfly/react-core"; import { Resource } from "@/Core"; import { useUrlStateWithFilter } from "@/Data"; -import { FilterField, FilterWidgetComponent } from "@/UI/Components"; +import { + ActiveFilterGroup, + ActiveFilters, + AddableTextInput, + FilterDrawerPanelContent, +} from "@/UI/Components"; import { words } from "@/UI/words"; interface DesiredStateDetailsFilterWidgetProps { onClose: () => void; } -const FIELDS: FilterField[] = [ +type TextFilterKey = "type" | "agent" | "value"; + +const FIELDS: { key: TextFilterKey; label: string; placeholder: string }[] = [ { + key: "type", label: words("resources.filters.resource.type.label"), placeholder: words("resources.filters.resource.type.placeholder"), - filterKey: "type", }, { + key: "agent", label: words("resources.filters.resource.agent.label"), placeholder: words("resources.filters.resource.agent.placeholder"), - filterKey: "agent", }, { + key: "value", label: words("resources.filters.resource.value.label"), placeholder: words("resources.filters.resource.value.placeholder"), - filterKey: "value", }, ]; /** * The DesiredStateDetailsFilterWidget component. * - * A memoized wrapper around FilterWidgetComponent that owns the desired state details - * filter state via URL state management. By managing the filter state internally, - * this component avoids re-rendering when the parent page re-renders due to other - * state changes. + * A memoized side-panel drawer owning the desired state details filter state via URL + * state management. Renders a free-text filter per resource identifier (type, agent, + * value) followed by an active filters chip section. By managing the filter state + * internally, this component avoids re-rendering when the parent page re-renders due + * to other state changes. * * @Props {DesiredStateDetailsFilterWidgetProps} - Component props. * @prop {() => void} onClose - Callback executed when the filter drawer should be closed. @@ -45,14 +54,61 @@ export const DesiredStateDetailsFilterWidget: React.FC + setFilter({ ...filter, [key]: [...(filter[key] ?? []), value] }); + + const removeChip = (key: TextFilterKey, value: string) => { + const updated = (filter[key] ?? []).filter((v) => v !== value); + + setFilter({ ...filter, [key]: updated.length > 0 ? updated : undefined }); + }; + + const clearGroup = (key: TextFilterKey) => setFilter({ ...filter, [key]: undefined }); + + const clearAllFilters = () => setFilter({}); + + const hasActiveFilters = FIELDS.some(({ key }) => (filter[key]?.length ?? 0) > 0); + return ( - + + +
e.preventDefault()}> + + + {words("resources.filters.resource.sectionTitle")} + + + {FIELDS.map(({ key, label, placeholder }) => ( + + addValue(key, value)} + /> + + ))} +
+ + + + + {FIELDS.map(({ key, label }) => + (filter[key]?.length ?? 0) > 0 ? ( + + removeChip(key, value)} + onRemoveGroup={() => clearGroup(key)} + /> + + ) : null + )} + +
+
); } ); + +DesiredStateDetailsFilterWidget.displayName = "DesiredStateDetailsFilterWidget"; diff --git a/src/Slices/Events/UI/FilterWidget/FilterWidgetComponent.tsx b/src/Slices/Events/UI/FilterWidget/FilterWidgetComponent.tsx index 4a66b5d7d1..c7c0d59677 100644 --- a/src/Slices/Events/UI/FilterWidget/FilterWidgetComponent.tsx +++ b/src/Slices/Events/UI/FilterWidget/FilterWidgetComponent.tsx @@ -1,28 +1,16 @@ -import React, { useState } from "react"; -import { - Button, - Divider, - EmptyState, - EmptyStateBody, - Flex, - FlexItem, - Form, - FormGroup, - InputGroup, - InputGroupItem, - Label, - LabelGroup, - Stack, - StackItem, - TextInput, - Title, -} from "@patternfly/react-core"; -import { PlusIcon } from "@patternfly/react-icons"; +import React from "react"; +import { Divider, Form, FormGroup, Stack, StackItem } from "@patternfly/react-core"; import { EventType, RangeOperator, toggleValueInList } from "@/Core"; import { uniq } from "@/Core/Language/collection"; import { Filter } from "@/Slices/Events/Core/Types"; -import { FilterDrawerPanelContent, MultiTextSelect } from "@/UI/Components"; -import { TimestampPicker } from "@/UI/Components/Filters"; +import { + ActiveFilterGroup, + ActiveFilters, + AddableTextInput, + FilterDrawerPanelContent, + MultiTextSelect, +} from "@/UI/Components"; +import { TimestampRangeFilter } from "@/UI/Components/Filters"; import { CustomDatePresenter } from "@/UI/Utils"; import { words } from "@/UI/words"; @@ -52,10 +40,6 @@ interface Props { * @returns {React.ReactElement} The rendered filter widget. */ export const FilterWidgetComponent: React.FC = ({ filter, setFilter, states, onClose }) => { - const [versionInput, setVersionInput] = useState(""); - const [from, setFrom] = useState(); - const [to, setTo] = useState(); - const eventTypes = Object.values(EventType); // --- Event type --- @@ -116,16 +100,8 @@ export const FilterWidgetComponent: React.FC = ({ filter, setFilter, stat const clearDestinationFilters = () => setFilter({ ...filter, destination: undefined }); // --- Version --- - const applyVersionFilter = () => { - const trimmed = versionInput.trim(); - - if (!trimmed) { - return; - } - - setFilter({ ...filter, version: uniq([...(filter.version ?? []), trimmed]) }); - setVersionInput(""); - }; + const addVersion = (value: string) => + setFilter({ ...filter, version: uniq([...(filter.version ?? []), value]) }); const removeVersionChip = (value: string) => { const updated = (filter.version ?? []).filter((version) => version !== value); @@ -136,34 +112,6 @@ export const FilterWidgetComponent: React.FC = ({ filter, setFilter, stat const clearVersionFilters = () => setFilter({ ...filter, version: undefined }); // --- Date --- - const applyDateFromFilter = () => { - if (!from) { - return; - } - - const updated = [ - ...(filter.timestamp ?? []).filter((entry) => entry.operator !== RangeOperator.Operator.From), - { date: from, operator: RangeOperator.Operator.From }, - ]; - - setFilter({ ...filter, timestamp: updated }); - setFrom(undefined); - }; - - const applyDateToFilter = () => { - if (!to) { - return; - } - - const updated = [ - ...(filter.timestamp ?? []).filter((entry) => entry.operator !== RangeOperator.Operator.To), - { date: to, operator: RangeOperator.Operator.To }, - ]; - - setFilter({ ...filter, timestamp: updated }); - setTo(undefined); - }; - const removeTimestampChip = (operator: RangeOperator.Operator) => { const updated = (filter.timestamp ?? []).filter((entry) => entry.operator !== operator); @@ -179,6 +127,14 @@ export const FilterWidgetComponent: React.FC = ({ filter, setFilter, stat label: `${entry.operator} | ${datePresenter.getFull(entry.date.toISOString())}`, })); + const removeTimestampChipByLabel = (label: string) => { + const chip = timestampChips.find((entry) => entry.label === label); + + if (chip) { + removeTimestampChip(chip.operator); + } + }; + const hasActiveFilters = (filter.event_type?.length ?? 0) > 0 || (filter.source?.length ?? 0) > 0 || @@ -239,230 +195,79 @@ export const FilterWidgetComponent: React.FC = ({ filter, setFilter, stat
- - - - setVersionInput(value)} - onKeyDown={(event) => { - if (event.key === "Enter") { - event.preventDefault(); - applyVersionFilter(); - } - }} - type="number" - placeholder={words("events.filters.version.placeholder")} - aria-label="VersionFilterInput" - /> - - - - - - + - - - - - - - - } - /> - - - - - - - - } - /> - - - - + setFilter({ ...filter, timestamp })} + /> - - - - - {words("resources.filters.active.title")} - - - - - - - - {hasActiveFilters ? ( - - {(filter.event_type?.length ?? 0) > 0 && ( - - - {(filter.event_type ?? []).map((eventType) => ( - - ))} - - - )} - {(filter.source?.length ?? 0) > 0 && ( - - - {(filter.source ?? []).map((source) => ( - - ))} - - - )} - {(filter.destination?.length ?? 0) > 0 && ( - - - {(filter.destination ?? []).map((destination) => ( - - ))} - - - )} - {(filter.version?.length ?? 0) > 0 && ( - - - {(filter.version ?? []).map((version) => ( - - ))} - - - )} - {timestampChips.length > 0 && ( - - - {timestampChips.map((chip) => ( - - ))} - - - )} - - ) : ( - - - {words("resources.filters.active.empty.title")} - - {words("resources.filters.active.empty.body.noTabs")} - + + {(filter.event_type?.length ?? 0) > 0 && ( + + + + )} + {(filter.source?.length ?? 0) > 0 && ( + + + + )} + {(filter.destination?.length ?? 0) > 0 && ( + + + + )} + {(filter.version?.length ?? 0) > 0 && ( + + + + )} + {timestampChips.length > 0 && ( + + chip.label)} + onRemove={removeTimestampChipByLabel} + onRemoveGroup={clearTimestampFilters} + /> + )} - + ); diff --git a/src/Slices/Parameters/UI/FilterWidget/FilterWidgetComponent.tsx b/src/Slices/Parameters/UI/FilterWidget/FilterWidgetComponent.tsx index d547c3ebef..7c8b552a81 100644 --- a/src/Slices/Parameters/UI/FilterWidget/FilterWidgetComponent.tsx +++ b/src/Slices/Parameters/UI/FilterWidget/FilterWidgetComponent.tsx @@ -1,29 +1,16 @@ -import React, { useState } from "react"; -import { - Button, - Divider, - EmptyState, - EmptyStateBody, - Flex, - FlexItem, - Form, - FormGroup, - InputGroup, - InputGroupItem, - Label, - LabelGroup, - Stack, - StackItem, - TextInput, - Title, -} from "@patternfly/react-core"; -import { PlusIcon } from "@patternfly/react-icons"; +import React from "react"; +import { Divider, Form, Stack, StackItem } from "@patternfly/react-core"; import { RangeOperator } from "@/Core"; import { uniq } from "@/Core/Language/collection"; import { Filter } from "@/Slices/Parameters/Core/Types"; import { CustomDatePresenter } from "@/UI"; -import { FilterDrawerPanelContent } from "@/UI/Components"; -import { TimestampPicker } from "@/UI/Components/Filters"; +import { + ActiveFilterGroup, + ActiveFilters, + AddableTextInput, + FilterDrawerPanelContent, +} from "@/UI/Components"; +import { TimestampRangeFilter } from "@/UI/Components/Filters"; import { words } from "@/UI/words"; const datePresenter = new CustomDatePresenter(); @@ -49,21 +36,9 @@ interface Props { * @returns {React.ReactElement} The rendered filter widget. */ export const FilterWidgetComponent: React.FC = ({ filter, setFilter, onClose }) => { - const [nameInput, setNameInput] = useState(""); - const [sourceInput, setSourceInput] = useState(""); - const [dateFrom, setDateFrom] = useState(); - const [dateTo, setDateTo] = useState(); - // --- Name --- - const addName = () => { - const value = nameInput.trim(); - - if (!value) { - return; - } + const addName = (value: string) => setFilter({ ...filter, name: uniq([...(filter.name ?? []), value]) }); - setNameInput(""); - }; const removeNameChip = (value: string) => { const updated = (filter.name ?? []).filter((n) => n !== value); @@ -74,15 +49,8 @@ export const FilterWidgetComponent: React.FC = ({ filter, setFilter, onCl const clearNameFilters = () => setFilter({ ...filter, name: undefined }); // --- Source --- - const addSource = () => { - const value = sourceInput.trim(); - - if (!value) { - return; - } + const addSource = (value: string) => setFilter({ ...filter, source: uniq([...(filter.source ?? []), value]) }); - setSourceInput(""); - }; const removeSourceChip = (value: string) => { const updated = (filter.source ?? []).filter((s) => s !== value); @@ -93,32 +61,6 @@ export const FilterWidgetComponent: React.FC = ({ filter, setFilter, onCl const clearSourceFilters = () => setFilter({ ...filter, source: undefined }); // --- Updated (date range) --- - const applyDateFromFilter = () => { - if (!dateFrom) { - return; - } - const updated = [ - ...(filter.updated ?? []).filter((d) => d.operator !== RangeOperator.Operator.From), - { date: dateFrom, operator: RangeOperator.Operator.From }, - ]; - - setFilter({ ...filter, updated }); - setDateFrom(undefined); - }; - - const applyDateToFilter = () => { - if (!dateTo) { - return; - } - const updated = [ - ...(filter.updated ?? []).filter((d) => d.operator !== RangeOperator.Operator.To), - { date: dateTo, operator: RangeOperator.Operator.To }, - ]; - - setFilter({ ...filter, updated }); - setDateTo(undefined); - }; - const removeUpdatedChip = (chip: string) => { const operator = chip.split("|")[0].trim() as RangeOperator.Operator; const updated = (filter.updated ?? []).filter((d) => d.operator !== operator); @@ -142,210 +84,66 @@ export const FilterWidgetComponent: React.FC = ({ filter, setFilter, onCl
e.preventDefault()}> - - - - setNameInput(val)} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); - addName(); - } - }} - placeholder={words("parameters.filters.name.placeholder")} - aria-label="Name filter" - /> - - - - - - + - - - - setSourceInput(val)} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); - addSource(); - } - }} - placeholder={words("parameters.filters.source.placeholder")} - aria-label="Source filter" - /> - - - - - - + - - - - - - - - } - /> - - - - - - - - } - /> - - - - + setFilter({ ...filter, updated })} + />
- - - - - {words("resources.filters.active.title")} - - - - - - - - {hasActiveFilters ? ( - - {(filter.name ?? []).length > 0 && ( - - - {(filter.name ?? []).map((name) => ( - - ))} - - - )} - {(filter.source ?? []).length > 0 && ( - - - {(filter.source ?? []).map((source) => ( - - ))} - - - )} - {updatedChips.length > 0 && ( - - - {updatedChips.map((chip) => ( - - ))} - - - )} - - ) : ( - - - {words("resources.filters.active.empty.title")} - - {words("resources.filters.active.empty.body.noTabs")} - + + {(filter.name?.length ?? 0) > 0 && ( + + + + )} + {(filter.source?.length ?? 0) > 0 && ( + + + + )} + {updatedChips.length > 0 && ( + + + )} - +
); diff --git a/src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/ActiveFiltersSection.tsx b/src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/ActiveFiltersSection.tsx index 0fb3965cf6..3bf8152e53 100644 --- a/src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/ActiveFiltersSection.tsx +++ b/src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/ActiveFiltersSection.tsx @@ -1,17 +1,8 @@ import React from "react"; -import { - Button, - EmptyState, - EmptyStateBody, - Flex, - FlexItem, - Stack, - StackItem, - Title, -} from "@patternfly/react-core"; +import { StackItem } from "@patternfly/react-core"; import { Resource } from "@/Core"; +import { ActiveFilterGroup, ActiveFilters } from "@/UI/Components"; import { words } from "@/UI/words"; -import { ActiveFilterGroup } from "./ActiveFilterGroup"; export interface ActiveFiltersSectionProps { filter: Resource.FilterWithDefaultHandling; @@ -65,73 +56,51 @@ export const ActiveFiltersSection: React.FC = ({ (filter.status && filter.status.length > 0); return ( - - - - - {words("resources.filters.active.title")} - - - - - - - {hasActiveFilters ? ( - - {filter.type && filter.type.length > 0 && ( - - - - )} - {filter.agent && filter.agent.length > 0 && ( - - - - )} - {filter.value && filter.value.length > 0 && ( - - - - )} - {filter.status && filter.status.length > 0 && ( - - - - )} - - ) : ( - - - {words("resources.filters.active.empty.title")} - - {words("resources.filters.active.empty.body")} - + + {filter.type && filter.type.length > 0 && ( + + + )} - + {filter.agent && filter.agent.length > 0 && ( + + + + )} + {filter.value && filter.value.length > 0 && ( + + + + )} + {filter.status && filter.status.length > 0 && ( + + + + )} + ); }; diff --git a/src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/ResourceFilterForm.tsx b/src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/ResourceFilterForm.tsx index 0cd03ef978..9c00ebea45 100644 --- a/src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/ResourceFilterForm.tsx +++ b/src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/ResourceFilterForm.tsx @@ -3,10 +3,15 @@ import { FormGroup, Stack, StackItem, Title } from "@patternfly/react-core"; import { Resource } from "@/Core"; import { useGetAgents } from "@/Data/Queries"; import { useDebounce } from "@/UI"; -import { OptionalToggleGroup, excludeIcons, includeIcons } from "@/UI/Components"; +import { + AddableSelectInput, + AddableSelectOption, + AddableTextInput, + OptionalToggleGroup, + excludeIcons, + includeIcons, +} from "@/UI/Components"; import { words } from "@/UI/words"; -import { AddableSelectInput, SelectOption } from "./AddableSelectInput"; -import { AddableTextInput } from "./AddableTextInput"; export interface ResourceFilterFormProps { onAddType: (type: string) => void; @@ -45,7 +50,7 @@ export const ResourceFilterForm: React.FC = ({ filter: debouncedSearch ? { name: [debouncedSearch] } : undefined, }); - const agentOptions = useMemo(() => { + const agentOptions = useMemo(() => { if (!data?.pages) { return []; } @@ -101,6 +106,9 @@ export const ResourceFilterForm: React.FC = ({ }} isLoading={isLoading || isFetchingNextPage} onToggleInputMode={() => setInputMode("text")} + toggleLabel={words("resources.filters.resource.agent.selectInfoLabel")} + loadingLabel={words("resources.filters.resource.agent.loading")} + emptyLabel={words("agents.empty.message")} /> ) : ( = ({ placeholder={words("resources.filters.resource.agent.placeholder")} onAdd={onAddAgent} onToggleInputMode={() => setInputMode("select")} - - /* hint={words("resources.filters.resource.agent.hint")} */ + toggleLabel={words("resources.filters.resource.agent.textInfoLabel")} /> )} diff --git a/src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/index.ts b/src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/index.ts index 780e4d4c41..5574e1e700 100644 --- a/src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/index.ts +++ b/src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/index.ts @@ -1,8 +1,5 @@ export * from "./FilterWidgetComponent"; export * from "./ConnectedFilterWidget"; export * from "./ActiveFiltersSection"; -export * from "./ActiveFilterGroup"; export * from "./StatusFilterSelect"; export * from "./ResourceFilterForm"; -export * from "./AddableTextInput"; -export * from "./AddableSelectInput"; diff --git a/src/Slices/ResourceDiscovery/UI/Components/FilterWidget/DiscoveredResourcesFilterWidget.tsx b/src/Slices/ResourceDiscovery/UI/Components/FilterWidget/DiscoveredResourcesFilterWidget.tsx index eb1c54a946..a159f7c8be 100644 --- a/src/Slices/ResourceDiscovery/UI/Components/FilterWidget/DiscoveredResourcesFilterWidget.tsx +++ b/src/Slices/ResourceDiscovery/UI/Components/FilterWidget/DiscoveredResourcesFilterWidget.tsx @@ -1,40 +1,49 @@ import React, { memo } from "react"; +import { Divider, Form, Stack, StackItem, Title } from "@patternfly/react-core"; import { useUrlStateWithFilter } from "@/Data"; import { Filter } from "@/Data/Queries"; -import { FilterField, FilterWidgetComponent } from "@/UI/Components"; +import { + ActiveFilterGroup, + ActiveFilters, + AddableTextInput, + FilterDrawerPanelContent, +} from "@/UI/Components"; import { words } from "@/UI/words"; interface DiscoveredResourcesFilterWidgetProps { onClose: () => void; } -const FIELDS: FilterField[] = [ +type DiscoveredResourcesFilter = Omit; + +type TextFilterKey = "type" | "agent" | "value"; + +const FIELDS: { key: TextFilterKey; label: string; placeholder: string }[] = [ { + key: "type", label: words("resources.filters.resource.type.label"), placeholder: words("resources.filters.resource.type.placeholder"), - filterKey: "type", }, { + key: "agent", label: words("resources.filters.resource.agent.label"), placeholder: words("resources.filters.resource.agent.placeholder"), - filterKey: "agent", }, { + key: "value", label: words("resources.filters.resource.value.label"), placeholder: words("resources.filters.resource.value.placeholder"), - filterKey: "value", }, ]; -type DiscoveredResourcesFilter = Omit; - /** * The DiscoveredResourcesFilterWidget component. * - * A memoized wrapper around FilterWidgetComponent that owns the discovered resources - * filter state via URL state management. By managing the filter state internally, - * this component avoids re-rendering when the parent page re-renders due to other - * state changes. + * A memoized side-panel drawer owning the discovered resources filter state via URL + * state management. Renders a free-text filter per resource identifier (type, agent, + * value) followed by an active filters chip section. By managing the filter state + * internally, this component avoids re-rendering when the parent page re-renders due + * to other state changes. * * @Props {DiscoveredResourcesFilterWidgetProps} - Component props. * @prop {() => void} onClose - Callback executed when the filter drawer should be closed. @@ -47,14 +56,61 @@ export const DiscoveredResourcesFilterWidget: React.FC + setFilter({ ...filter, [key]: [...(filter[key] ?? []), value] }); + + const removeChip = (key: TextFilterKey, value: string) => { + const updated = (filter[key] ?? []).filter((v) => v !== value); + + setFilter({ ...filter, [key]: updated.length > 0 ? updated : undefined }); + }; + + const clearGroup = (key: TextFilterKey) => setFilter({ ...filter, [key]: undefined }); + + const clearAllFilters = () => setFilter({}); + + const hasActiveFilters = FIELDS.some(({ key }) => (filter[key]?.length ?? 0) > 0); + return ( - + + +
e.preventDefault()}> + + + {words("resources.filters.resource.sectionTitle")} + + + {FIELDS.map(({ key, label, placeholder }) => ( + + addValue(key, value)} + /> + + ))} +
+ + + + + {FIELDS.map(({ key, label }) => + (filter[key]?.length ?? 0) > 0 ? ( + + removeChip(key, value)} + onRemoveGroup={() => clearGroup(key)} + /> + + ) : null + )} + +
+
); } ); + +DiscoveredResourcesFilterWidget.displayName = "DiscoveredResourcesFilterWidget"; diff --git a/src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/AddableSelectInput.test.tsx b/src/UI/Components/AddableSelectInput/AddableSelectInput.test.tsx similarity index 54% rename from src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/AddableSelectInput.test.tsx rename to src/UI/Components/AddableSelectInput/AddableSelectInput.test.tsx index 103b978710..46a3aa7f2a 100644 --- a/src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/AddableSelectInput.test.tsx +++ b/src/UI/Components/AddableSelectInput/AddableSelectInput.test.tsx @@ -9,24 +9,26 @@ const options = [ const getInput = () => screen.getByTestId("search-input").querySelector("input") as HTMLInputElement; +const renderInput = (overrides = {}) => + render( + + ); + describe("AddableSelectInput", () => { it("adds typed value when add button is clicked", () => { const onAdd = vi.fn(); - const onFilter = vi.fn(); - const onReachEnd = vi.fn(); - const onToggleInputMode = vi.fn(); - render( - - ); + renderInput({ onAdd }); const input = getInput(); const button = screen.getByTestId("add-button"); @@ -39,22 +41,9 @@ describe("AddableSelectInput", () => { }); it("calls onFilter when typing", () => { - const onAdd = vi.fn(); const onFilter = vi.fn(); - const onReachEnd = vi.fn(); - const onToggleInputMode = vi.fn(); - render( - - ); + renderInput({ onFilter }); const input = getInput(); @@ -64,22 +53,9 @@ describe("AddableSelectInput", () => { }); it("clears input when clear button is used", () => { - const onAdd = vi.fn(); const onFilter = vi.fn(); - const onReachEnd = vi.fn(); - const onToggleInputMode = vi.fn(); - render( - - ); + renderInput({ onFilter }); const input = getInput(); @@ -92,21 +68,8 @@ describe("AddableSelectInput", () => { it("adds value when pressing Enter", () => { const onAdd = vi.fn(); - const onFilter = vi.fn(); - const onReachEnd = vi.fn(); - const onToggleInputMode = vi.fn(); - render( - - ); + renderInput({ onAdd }); const input = getInput(); @@ -116,4 +79,14 @@ describe("AddableSelectInput", () => { expect(onAdd).toHaveBeenCalledWith("enter-value"); expect(input.value).toBe(""); }); + + it("calls onToggleInputMode when the toggle link is clicked", () => { + const onToggleInputMode = vi.fn(); + + renderInput({ onToggleInputMode }); + + fireEvent.click(screen.getByRole("button", { name: "Use text input" })); + + expect(onToggleInputMode).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/AddableSelectInput.tsx b/src/UI/Components/AddableSelectInput/AddableSelectInput.tsx similarity index 91% rename from src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/AddableSelectInput.tsx rename to src/UI/Components/AddableSelectInput/AddableSelectInput.tsx index d3581f06c6..ffb13e6a0d 100644 --- a/src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/AddableSelectInput.tsx +++ b/src/UI/Components/AddableSelectInput/AddableSelectInput.tsx @@ -16,9 +16,8 @@ import { TextInputGroupUtilities, } from "@patternfly/react-core"; import { PlusIcon, TimesIcon } from "@patternfly/react-icons"; -import { words } from "@/UI"; -export interface SelectOption { +export interface AddableSelectOption { value: string; label: string; } @@ -26,12 +25,15 @@ export interface SelectOption { export interface AddableSelectInputProps { label: string; placeholder?: string; - options: SelectOption[]; + options: AddableSelectOption[]; onAdd: (value: string) => void; onFilter: (value: string) => void; onReachEnd: () => void; onToggleInputMode: () => void; + toggleLabel: string; isLoading: boolean; + loadingLabel?: string; + emptyLabel?: string; } /** @@ -40,19 +42,22 @@ export interface AddableSelectInputProps { * Provides a typeahead select input paired with a control button to append values to a filter category. * Clicking the input opens the full options list; typing filters it via the onFilter callback. * Scrolling to the bottom of the list triggers onReachEnd for paginated option loading. + * Shared building block for the filter drawers; all display text is supplied through props. * * @Props {AddableSelectInputProps} - Component props. * @prop {string} label - Label shown above the select field. * @prop {string} [placeholder] - Optional placeholder text shown in the input when no value is entered. - * @prop {SelectOption[]} options - The list of selectable options. + * @prop {AddableSelectOption[]} options - The list of selectable options. * @prop {(value: string) => void} onAdd - Callback executed with the current input value when the add action is triggered. * @prop {(value: string) => void} onFilter - Callback executed when the search input value changes, used to filter options externally. * @prop {() => void} onReachEnd - Callback executed when the menu scroll reaches near the end, used to load more options. * @prop {() => void} onToggleInputMode - Callback executed whenever we press on the labelInfo of the FormGroup. + * @prop {string} toggleLabel - Label for the input-mode toggle link. * @prop {boolean} isLoading - Whether options are currently being loaded; shows a spinner entry at the bottom of the list. + * @prop {string} [loadingLabel] - Text shown next to the spinner while options load. + * @prop {string} [emptyLabel] - Text shown when there are no options and nothing is loading. * @returns {React.ReactElement} The rendered addable select input. */ - export const AddableSelectInput: React.FC = ({ label, placeholder, @@ -61,7 +66,10 @@ export const AddableSelectInput: React.FC = ({ onFilter, onReachEnd, onToggleInputMode, + toggleLabel, isLoading, + loadingLabel, + emptyLabel, }) => { const [filterValue, setFilterValue] = useState(""); const [isOpen, setIsOpen] = useState(false); @@ -214,7 +222,7 @@ export const AddableSelectInput: React.FC = ({ onFilter(""); }} > - {words("resources.filters.resource.agent.selectInfoLabel")} + {toggleLabel} } > @@ -254,7 +262,7 @@ export const AddableSelectInput: React.FC = ({ {isLoading && ( - {words("resources.filters.resource.agent.loading")} + {loadingLabel} @@ -262,7 +270,7 @@ export const AddableSelectInput: React.FC = ({ {options.length === 0 && !isLoading && ( - {words("agents.empty.message")} + {emptyLabel} )} @@ -274,7 +282,7 @@ export const AddableSelectInput: React.FC = ({ onClick={handleAdd} isDisabled={!filterValue} data-testid="add-button" - aria-label={`${words("resources.filters.filter")}-${label}`} + aria-label={`Add filter-${label}`} > diff --git a/src/UI/Components/AddableSelectInput/index.ts b/src/UI/Components/AddableSelectInput/index.ts new file mode 100644 index 0000000000..7451e035bd --- /dev/null +++ b/src/UI/Components/AddableSelectInput/index.ts @@ -0,0 +1 @@ +export * from "./AddableSelectInput"; diff --git a/src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/AddableTextInput.test.tsx b/src/UI/Components/AddableTextInput/AddableTextInput.test.tsx similarity index 58% rename from src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/AddableTextInput.test.tsx rename to src/UI/Components/AddableTextInput/AddableTextInput.test.tsx index 12aae5e267..6a5be83f51 100644 --- a/src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/AddableTextInput.test.tsx +++ b/src/UI/Components/AddableTextInput/AddableTextInput.test.tsx @@ -1,21 +1,14 @@ import { fireEvent, render, screen } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; -import { words } from "@/UI"; import { AddableTextInput } from "./AddableTextInput"; describe("AddableTextInput", () => { it("adds a trimmed value when the add button is clicked", async () => { const handleAdd = vi.fn(); - render( - - ); + render(); - const input = screen.getByPlaceholderText(words("resources.filters.resource.type.placeholder")); + const input = screen.getByPlaceholderText("Type..."); await userEvent.type(input, " example "); @@ -27,24 +20,20 @@ describe("AddableTextInput", () => { expect(input).toHaveValue(""); }); - it("does not call onAdd for empty or whitespace-only values", async () => { + it("disables the add button and does not call onAdd for empty or whitespace-only values", async () => { const handleAdd = vi.fn(); - render( - - ); + render(); - const input = screen.getByPlaceholderText( - words("resources.filters.resource.agent.placeholder") - ); + const input = screen.getByPlaceholderText("Agent..."); + + expect(screen.getByTestId("add-button")).toBeDisabled(); await userEvent.type(input, " "); - const addButton = screen.getByTestId("add-button"); - await userEvent.click(addButton); + + expect(screen.getByTestId("add-button")).toBeDisabled(); + + fireEvent.keyPress(input, { key: "Enter", code: "Enter", charCode: 13 }); expect(handleAdd).not.toHaveBeenCalled(); expect(input).toHaveValue(" "); @@ -53,17 +42,9 @@ describe("AddableTextInput", () => { it("adds the current value when the enter key is pressed", async () => { const handleAdd = vi.fn(); - render( - - ); + render(); - const input = screen.getByPlaceholderText( - words("resources.filters.resource.value.placeholder") - ); + const input = screen.getByPlaceholderText("Value..."); await userEvent.type(input, "to-add"); @@ -93,4 +74,23 @@ describe("AddableTextInput", () => { const hint = await screen.findByText("This is a helpful hint"); expect(hint).toBeInTheDocument(); }); + + it("renders the toggle link and calls onToggleInputMode when clicked", async () => { + const handleToggle = vi.fn(); + + render( + + ); + + const toggle = screen.getByRole("button", { name: "Use select input" }); + await userEvent.click(toggle); + + expect(handleToggle).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/AddableTextInput.tsx b/src/UI/Components/AddableTextInput/AddableTextInput.tsx similarity index 80% rename from src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/AddableTextInput.tsx rename to src/UI/Components/AddableTextInput/AddableTextInput.tsx index 45f766bb7e..99beda3bea 100644 --- a/src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/AddableTextInput.tsx +++ b/src/UI/Components/AddableTextInput/AddableTextInput.tsx @@ -10,13 +10,14 @@ import { TextInput, } from "@patternfly/react-core"; import { PlusIcon } from "@patternfly/react-icons"; -import { words } from "@/UI"; export interface AddableTextInputProps { label: string; placeholder: string; onAdd: (value: string) => void; hint?: string; + toggleLabel?: string; + type?: React.ComponentProps["type"]; onToggleInputMode?: (event: React.MouseEvent) => void; } @@ -24,22 +25,24 @@ export interface AddableTextInputProps { * The AddableTextInput component. * * Provides a text input paired with a control button to append values to a filter category. + * Shared building block for the filter drawers; all display text is supplied through props. * * @Props {AddableTextInputProps} - Component props. * @prop {string} label - Label shown above the input field. * @prop {string} placeholder - Placeholder text displayed within the input. * @prop {(value: string) => void} onAdd - Callback executed with the trimmed value when the add action is triggered. - * @prop {string} hint - Hint displayed on hover of the help label. - * @prop {(event) => React.MouseEvent} onToggleInputMode - * - Callback executed whenever we press on the labelInfo of the FormGroup + * @prop {string} [hint] - Hint displayed on hover of the help label. + * @prop {string} [toggleLabel] - Label for the input-mode toggle link; only rendered when onToggleInputMode is provided. + * @prop {(event) => void} [onToggleInputMode] - Callback executed whenever we press on the labelInfo of the FormGroup. * @returns {React.ReactElement} The rendered addable text input. */ - export const AddableTextInput: React.FC = ({ label, placeholder, onAdd, hint, + toggleLabel, + type = "text", onToggleInputMode, }) => { const [value, setValue] = useState(""); @@ -79,7 +82,7 @@ export const AddableTextInput: React.FC = ({ labelInfo={ onToggleInputMode && ( ) } @@ -88,7 +91,7 @@ export const AddableTextInput: React.FC = ({ setValue(nextValue)} @@ -100,8 +103,9 @@ export const AddableTextInput: React.FC = ({ diff --git a/src/UI/Components/AddableTextInput/index.ts b/src/UI/Components/AddableTextInput/index.ts new file mode 100644 index 0000000000..78e6ef3b43 --- /dev/null +++ b/src/UI/Components/AddableTextInput/index.ts @@ -0,0 +1 @@ +export * from "./AddableTextInput"; diff --git a/src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/ActiveFilterGroup.test.tsx b/src/UI/Components/FilterDrawer/ActiveFilterGroup.test.tsx similarity index 98% rename from src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/ActiveFilterGroup.test.tsx rename to src/UI/Components/FilterDrawer/ActiveFilterGroup.test.tsx index 177e97c3fd..8c3054d836 100644 --- a/src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/ActiveFilterGroup.test.tsx +++ b/src/UI/Components/FilterDrawer/ActiveFilterGroup.test.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { render, screen } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; import { ActiveFilterGroup } from "./ActiveFilterGroup"; diff --git a/src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/ActiveFilterGroup.tsx b/src/UI/Components/FilterDrawer/ActiveFilterGroup.tsx similarity index 88% rename from src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/ActiveFilterGroup.tsx rename to src/UI/Components/FilterDrawer/ActiveFilterGroup.tsx index 4f86d73602..01524c4bad 100644 --- a/src/Slices/Resource/UI/ResourcesPage/Components/FilterWidget/ActiveFilterGroup.tsx +++ b/src/UI/Components/FilterDrawer/ActiveFilterGroup.tsx @@ -1,6 +1,5 @@ import React from "react"; import { Label, LabelGroup } from "@patternfly/react-core"; -import { words } from "@/UI/words"; export interface ActiveFilterGroupProps { title: string; @@ -13,6 +12,7 @@ export interface ActiveFilterGroupProps { * The ActiveFilterGroup component. * * This component is responsible of rendering the active values of a single filter category as a dismissible label group. + * Values prefixed with "!" (exclusions) are rendered in red. Shared building block for the filter drawers. * * @Props {ActiveFilterGroupProps} - Component props. * @prop {string} title - Display name for the category heading. @@ -39,9 +39,7 @@ export const ActiveFilterGroup: React.FC = ({ isClosable={Boolean(onRemoveGroup)} onClick={onRemoveGroup} isEditable - closeBtnAriaLabel={ - onRemoveGroup ? words("resources.filters.active.group.close")(title) : undefined - } + closeBtnAriaLabel={onRemoveGroup ? `Remove ${title} filters` : undefined} > {values.map((value) => (