Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelogs/unreleased/7027-shared-filter-components.yml
Original file line number Diff line number Diff line change
@@ -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: {}
2 changes: 1 addition & 1 deletion cypress/e2e/scenario-4-desired-state.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ describe("Scenario 4 Desired State", () => {

// Open filter drawer and clear the filter
cy.contains("button", "Filters", { timeout: 30000 }).click();
cy.contains("button", "Clear all").click();
cy.contains("button", "Reset Filters").click();

cy.wait("@FILTER_UPDATE");

Expand Down
209 changes: 46 additions & 163 deletions src/Slices/Agents/UI/FilterWidget/FilterWidgetComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,14 @@
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 { toggleValueInList } from "@/Core";
import { uniq } from "@/Core/Language/collection";
import React from "react";
import { Divider, Form, FormGroup, Stack, StackItem } from "@patternfly/react-core";
import { Filter } from "@/Slices/Agents/Core/Types";
import { FilterDrawerPanelContent, MultiTextSelect } from "@/UI/Components";
import {
ActiveFilterGroup,
ActiveFilters,
AddableTextInput,
FilterDrawerPanelContent,
MultiTextSelect,
getFilterActions,
} from "@/UI/Components";
import { words } from "@/UI/words";
import { AgentStatus } from "@S/Agents/Core/Domain";

Expand All @@ -46,51 +33,13 @@ interface Props {
* @returns {React.ReactElement} The rendered filter widget.
*/
export const FilterWidgetComponent: React.FC<Props> = ({ filter, setFilter, onClose }) => {
const [nameInput, setNameInput] = useState("");
const { addString, toggleString, removeStringChip, clearStringGroup } = getFilterActions(
filter,
setFilter
);

const agentStatuses = Object.values(AgentStatus);

// --- Name ---
const applyNameFilter = () => {
const trimmed = nameInput.trim();

if (!trimmed) {
return;
}

setFilter({ ...filter, name: uniq([...(filter.name ?? []), trimmed]) });
setNameInput("");
};

const removeNameChip = (value: string) => {
const updated = (filter.name ?? []).filter((name) => name !== value);

setFilter({ ...filter, name: updated.length > 0 ? updated : undefined });
};

const clearNameFilters = () => setFilter({ ...filter, name: undefined });

// --- Status ---
const handleStatusSelect = (selection: string | ((prev: string[]) => string[])) => {
if (typeof selection !== "string") {
return;
}

const updated = uniq(toggleValueInList(selection, filter.status ?? [])) as AgentStatus[];

setFilter({ ...filter, status: updated.length > 0 ? updated : undefined });
};

const removeStatusChip = (value: string) => {
const updated = (filter.status ?? []).filter((status) => status !== value);

setFilter({ ...filter, status: updated.length > 0 ? updated : undefined });
};

const clearStatusFilters = () => setFilter({ ...filter, status: undefined });

const clearAllFilters = () => setFilter({});

const hasActiveFilters = (filter.name?.length ?? 0) > 0 || (filter.status?.length ?? 0) > 0;

return (
Expand All @@ -106,117 +55,51 @@ export const FilterWidgetComponent: React.FC<Props> = ({ filter, setFilter, onCl
children: status,
isSelected: (filter.status ?? []).includes(status),
}))}
setSelected={handleStatusSelect}
setSelected={(selection) => {
if (typeof selection === "string") {
toggleString("status", selection);
}
}}
placeholderText={words("agents.filters.status.placeholder")}
selected={filter.status ?? []}
/>
</FormGroup>
</StackItem>

<StackItem>
<FormGroup label={words("agents.columns.name")}>
<InputGroup>
<InputGroupItem isFill>
<TextInput
value={nameInput}
onChange={(_event, value) => setNameInput(value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
applyNameFilter();
}
}}
type="search"
placeholder={words("agents.filters.name.placeholder")}
aria-label="NameFilterInput"
/>
</InputGroupItem>
<InputGroupItem>
<Button
variant="control"
onClick={applyNameFilter}
isDisabled={!nameInput.trim()}
aria-label="Apply name filter"
>
<PlusIcon />
</Button>
</InputGroupItem>
</InputGroup>
</FormGroup>
<AddableTextInput
label={words("agents.columns.name")}
placeholder={words("agents.filters.name.placeholder")}
onAdd={(value) => addString("name", value)}
type="search"
/>
</StackItem>
</Form>

<Divider />

<StackItem>
<Flex
justifyContent={{ default: "justifyContentSpaceBetween" }}
alignItems={{ default: "alignItemsCenter" }}
>
<FlexItem>
<Title headingLevel="h3" size="md">
{words("resources.filters.active.title")}
</Title>
</FlexItem>
<FlexItem>
<Button variant="link" isInline onClick={clearAllFilters}>
{words("resources.filters.active.resetFilters")}
</Button>
</FlexItem>
</Flex>

{hasActiveFilters ? (
<Stack hasGutter style={{ padding: "1rem 0" }}>
{(filter.name?.length ?? 0) > 0 && (
<StackItem>
<LabelGroup
categoryName={words("agents.columns.name")}
isCompact
isClosable
isEditable
onClick={clearNameFilters}
closeBtnAriaLabel={words("resources.filters.active.group.close")(
words("agents.columns.name")
)}
>
{(filter.name ?? []).map((name) => (
<Label key={name} color="grey" onClose={() => removeNameChip(name)}>
{name}
</Label>
))}
</LabelGroup>
</StackItem>
)}
{(filter.status?.length ?? 0) > 0 && (
<StackItem>
<LabelGroup
categoryName={words("agents.columns.status")}
isCompact
isClosable
isEditable
onClick={clearStatusFilters}
closeBtnAriaLabel={words("resources.filters.active.group.close")(
words("agents.columns.status")
)}
>
{(filter.status ?? []).map((status) => (
<Label key={status} color="grey" onClose={() => removeStatusChip(status)}>
{status}
</Label>
))}
</LabelGroup>
</StackItem>
)}
</Stack>
) : (
<EmptyState variant="xs">
<Title headingLevel="h4" size="md">
{words("resources.filters.active.empty.title")}
</Title>
<EmptyStateBody>{words("resources.filters.active.empty.body.noTabs")}</EmptyStateBody>
</EmptyState>
<ActiveFilters hasActiveFilters={hasActiveFilters} onClear={() => setFilter({})}>
{(filter.name?.length ?? 0) > 0 && (
<StackItem>
<ActiveFilterGroup
title={words("agents.columns.name")}
values={filter.name}
onRemove={(value) => removeStringChip("name", value)}
onRemoveGroup={() => clearStringGroup("name")}
/>
</StackItem>
)}
{(filter.status?.length ?? 0) > 0 && (
<StackItem>
<ActiveFilterGroup
title={words("agents.columns.status")}
values={filter.status}
onRemove={(value) => removeStringChip("status", value)}
onRemoveGroup={() => clearStringGroup("status")}
/>
</StackItem>
)}
</StackItem>
</ActiveFilters>
</Stack>
</FilterDrawerPanelContent>
);
Expand Down
Loading