diff --git a/ui/components/ClusterOverview/components/FilterByBroker.stories.tsx b/ui/components/ClusterOverview/components/FilterByBroker.stories.tsx new file mode 100644 index 000000000..e849f9291 --- /dev/null +++ b/ui/components/ClusterOverview/components/FilterByBroker.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from "@storybook/nextjs"; +import { FilterByBroker } from "./FilterByBroker"; + +const meta: Meta = { + component: FilterByBroker, + args: { + selectedBroker: "Broker1", + brokerList: ["Broker1", "Broker2", "Broker3"], + }, +} as Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/ui/components/ClusterOverview/components/FilterByBroker.tsx b/ui/components/ClusterOverview/components/FilterByBroker.tsx new file mode 100644 index 000000000..84e1a94f9 --- /dev/null +++ b/ui/components/ClusterOverview/components/FilterByBroker.tsx @@ -0,0 +1,77 @@ +import { + MenuToggle, + MenuToggleElement, + Select, + SelectList, + SelectOption, + ToolbarItem, +} from "@/libs/patternfly/react-core"; +import { useTranslations } from "next-intl"; +import { useState } from "react"; + +export function FilterByBroker({ + selectedBroker, + brokerList, + onSetSelectedBroker, + disableToolbar, +}: { + selectedBroker: string | undefined; + brokerList: string[]; + onSetSelectedBroker: (value: string | undefined) => void; + disableToolbar: boolean; +}) { + const t = useTranslations("metrics"); + const [isBrokerSelectOpen, setIsBrokerSelectOpen] = useState(false); + + const onToggleClick = () => setIsBrokerSelectOpen((prev) => !prev); + + const onBrokerSelect = ( + _event: React.MouseEvent | undefined, + value: string | number | undefined, + ) => { + if (value === t("all_brokers")) { + onSetSelectedBroker(undefined); + } else { + onSetSelectedBroker(value as string); + } + setIsBrokerSelectOpen(false); + }; + + // Define the toggle (new API pattern) + const toggle = (toggleRef: React.Ref) => ( + + {selectedBroker || t("all_brokers")} + + ); + + return ( + + + + ); +} diff --git a/ui/components/ClusterOverview/components/FilterByTime.stories.tsx b/ui/components/ClusterOverview/components/FilterByTime.stories.tsx new file mode 100644 index 000000000..de15a308f --- /dev/null +++ b/ui/components/ClusterOverview/components/FilterByTime.stories.tsx @@ -0,0 +1,17 @@ +import type { Meta, StoryObj } from "@storybook/nextjs"; +import { FilterByTime } from "./FilterByTime"; + +const meta: Meta = { + component: FilterByTime, + args: { + keyText: "string", + disableToolbar: false, + ariaLabel: "the aria label", + duration: 60, + }, +} as Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/ui/components/ClusterOverview/components/FilterByTime.tsx b/ui/components/ClusterOverview/components/FilterByTime.tsx new file mode 100644 index 000000000..4b068b199 --- /dev/null +++ b/ui/components/ClusterOverview/components/FilterByTime.tsx @@ -0,0 +1,103 @@ +import { + MenuToggle, + MenuToggleElement, + Select, + SelectList, + SelectOption, + ToolbarItem, +} from "@/libs/patternfly/react-core"; +import { useState } from "react"; + +export enum DurationOptions { + Last5minutes = 5, + Last15minutes = 15, + Last30minutes = 30, + Last1hour = 60, + Last3hours = 3 * 60, + Last6hours = 6 * 60, + Last12hours = 12 * 60, + Last24hours = 24 * 60, + Last2days = 2 * 24 * 60, + Last7days = 7 * 24 * 60, +} + +export const DurationOptionsMap = { + [DurationOptions.Last5minutes]: "Last 5 minutes", + [DurationOptions.Last15minutes]: "Last 15 minutes", + [DurationOptions.Last30minutes]: "Last 30 minutes", + [DurationOptions.Last1hour]: "Last 1 hour", + [DurationOptions.Last3hours]: "Last 3 hours", + [DurationOptions.Last6hours]: "Last 6 hours", + [DurationOptions.Last12hours]: "Last 12 hours", + [DurationOptions.Last24hours]: "Last 24 hours", + [DurationOptions.Last2days]: "Last 2 days", + [DurationOptions.Last7days]: "Last 7 days", +} as const; + +export function FilterByTime({ + duration, + keyText, + ariaLabel, + disableToolbar, + onDurationChange, +}: { + duration: DurationOptions; + onDurationChange: (value: DurationOptions) => void; + keyText: string; + ariaLabel: string; + disableToolbar: boolean; +}) { + const [isTimeSelectOpen, setIsTimeSelectOpen] = useState(false); + + const onToggleClick = () => setIsTimeSelectOpen((prev) => !prev); + + const onTimeSelect = ( + _event: React.MouseEvent | undefined, + value: string | number | undefined, + ) => { + const mapping = Object.entries(DurationOptionsMap).find( + ([, label]) => label === value, + ); + if (mapping) { + onDurationChange(parseInt(mapping[0], 10) as DurationOptions); + } + setIsTimeSelectOpen(false); + }; + + const toggle = (toggleRef: React.Ref) => ( + + {DurationOptionsMap[duration]} + + ); + + return ( + + + + + ); +} diff --git a/ui/components/ClusterOverview/components/FilterByTopic.stories.tsx b/ui/components/ClusterOverview/components/FilterByTopic.stories.tsx new file mode 100644 index 000000000..176f8aeb0 --- /dev/null +++ b/ui/components/ClusterOverview/components/FilterByTopic.stories.tsx @@ -0,0 +1,44 @@ +import type { Meta, StoryObj } from "@storybook/nextjs"; +import { FilterByTopic } from "./FilterByTopic"; + +const meta: Meta = { + component: FilterByTopic, + args: { + selectedTopic: undefined, + topicList: ["lorem", "dolor", "ipsum"], + disableToolbar: false, + }, +} as Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; +Default.args = {}; + +export const Disabled: Story = {}; +Disabled.args = { + disableToolbar: true, + selectedTopic: "lorem", +}; + +export const NoTopics: Story = {}; +NoTopics.args = { + topicList: undefined, +}; + +export const MultipleTopicsWithCommonWords: Story = {}; +MultipleTopicsWithCommonWords.args = { + topicList: ["lorem dolor", "lorem ipsum", "lorem foo", "dolor", "ipsum"], +}; + +export const DoesNotBreakWithLongWords: Story = {}; +DoesNotBreakWithLongWords.args = { + topicList: [ + "lorem dolor lorem dolor lorem dolor lorem dolor lorem dolor lorem dolor", + "lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum ", + "lorem foo", + "dolor", + "ipsum", + ], +}; diff --git a/ui/components/ClusterOverview/components/FilterByTopic.tsx b/ui/components/ClusterOverview/components/FilterByTopic.tsx new file mode 100644 index 000000000..af6086a30 --- /dev/null +++ b/ui/components/ClusterOverview/components/FilterByTopic.tsx @@ -0,0 +1,132 @@ +import { + Menu, + MenuList, + MenuContent, + MenuSearch, + MenuSearchInput, + SearchInput, + SelectOption, + MenuToggle, + MenuContainer, + ToolbarItem, + SelectGroup, +} from "@/libs/patternfly/react-core"; +import { FilterIcon } from "@/libs/patternfly/react-icons"; +import { useTranslations } from "next-intl"; +import { useRef, useState } from "react"; + +export function FilterByTopic({ + selectedTopic, + topicList = [], + disableToolbar, + onSetSelectedTopic, +}: { + selectedTopic: string | undefined; + topicList: string[]; + disableToolbar: boolean; + onSetSelectedTopic: (value: string | undefined) => void; +}) { + const t = useTranslations("metrics"); + const allTopicsLabel = t("all_topics"); + + const [isOpen, setIsOpen] = useState(false); + const [filter, setFilter] = useState(""); + + const toggleRef = useRef(); + const menuRef = useRef(); + + const filteredTopics = topicList.filter((topic) => + topic.toLowerCase().includes(filter.toLowerCase()), + ); + + const onSelect = (_event: any, itemId: string | number | undefined) => { + if (itemId === "all-topics") { + onSetSelectedTopic(undefined); + } else { + onSetSelectedTopic(itemId as string); + } + setIsOpen(false); + }; + + const menuItems = [ + + {allTopicsLabel} + , + + {filteredTopics.map((topic, index) => ( + + {topic} + + ))} + , + ]; + + if (filter && menuItems.length === 1) { + menuItems.push( + + {t("common:no_results_found")} + , + ); + } + + const toggle = ( + setIsOpen(!isOpen)} + isExpanded={isOpen} + isDisabled={disableToolbar || topicList.length === 0} + className="appserv-metrics-filterbytopic" + > + {selectedTopic || allTopicsLabel} + + ); + + const menu = ( + + + + setFilter(val)} + onClear={(evt) => { + evt.stopPropagation(); + setFilter(""); + }} + /> + + + + + {menuItems} + + + ); + + return ( + + + + ); +} diff --git a/ui/messages/en.json b/ui/messages/en.json index 861aebe24..ad04ce5f5 100644 --- a/ui/messages/en.json +++ b/ui/messages/en.json @@ -414,6 +414,10 @@ "no_data": "No clusters available.", "not_available": "n/a" }, + "metrics": { + "all_brokers": "All Brokers", + "all_topics": "All topics" + }, "ColumnsModal": { "title": "Manage columns", "description": "Chosen fields will be displayed in the table.",