From 82e83b736895b0dbe60a4f7f8f88110bb56eae20 Mon Sep 17 00:00:00 2001 From: Shahar Glazner Date: Thu, 17 Oct 2024 11:42:14 +0300 Subject: [PATCH] feat: searchable columns + nested (#2225) --- keep-ui/app/alerts/ColumnSelection.tsx | 68 ++++--- keep-ui/app/alerts/alert-table-tab-panel.tsx | 203 ++++++++++--------- keep-ui/app/alerts/alert-table-utils.tsx | 15 +- pyproject.toml | 2 +- 4 files changed, 165 insertions(+), 123 deletions(-) diff --git a/keep-ui/app/alerts/ColumnSelection.tsx b/keep-ui/app/alerts/ColumnSelection.tsx index 772e6ad04..a65088e77 100644 --- a/keep-ui/app/alerts/ColumnSelection.tsx +++ b/keep-ui/app/alerts/ColumnSelection.tsx @@ -1,11 +1,11 @@ -import { FormEvent, Fragment, useRef } from "react"; +import { FormEvent, Fragment, useRef, useState } from "react"; import { Table } from "@tanstack/table-core"; -import { Button } from "@tremor/react"; +import { Button, TextInput } from "@tremor/react"; import { useLocalStorage } from "utils/hooks/useLocalStorage"; import { VisibilityState, ColumnOrderState } from "@tanstack/react-table"; import { FloatingArrow, arrow, offset, useFloating } from "@floating-ui/react"; import { Popover } from "@headlessui/react"; -import { FiSettings } from "react-icons/fi"; +import { FiSettings, FiSearch } from "react-icons/fi"; import { DEFAULT_COLS, DEFAULT_COLS_VISIBILITY } from "./alert-table-utils"; import { AlertDto } from "./models"; @@ -31,16 +31,19 @@ export default function ColumnSelection({ }); const tableColumns = table.getAllColumns(); - const [, setColumnVisibility] = useLocalStorage( - `column-visibility-${presetName}`, - DEFAULT_COLS_VISIBILITY - ); + const [columnVisibility, setColumnVisibility] = + useLocalStorage( + `column-visibility-${presetName}`, + DEFAULT_COLS_VISIBILITY + ); const [columnOrder, setColumnOrder] = useLocalStorage( `column-order-${presetName}`, DEFAULT_COLS ); + const [searchTerm, setSearchTerm] = useState(""); + const columnsOptions = tableColumns .filter((col) => col.getIsPinned() === false) .map((col) => col.id); @@ -49,6 +52,10 @@ export default function ColumnSelection({ .filter((col) => col.getIsVisible() && col.getIsPinned() === false) .map((col) => col.id); + const filteredColumns = columnsOptions.filter((column) => + column.toLowerCase().includes(searchTerm.toLowerCase()) + ); + const onMultiSelectChange = ( event: FormEvent, closePopover: VoidFunction @@ -56,34 +63,32 @@ export default function ColumnSelection({ event.preventDefault(); const formData = new FormData(event.currentTarget); - const valueKeys = Object.keys(Object.fromEntries(formData.entries())); + const selectedColumnIds = Object.keys( + Object.fromEntries(formData.entries()) + ); - const newColumnVisibility = columnsOptions.reduce( - (acc, key) => { - if (valueKeys.includes(key)) { - return { ...acc, [key]: true }; - } + // Update visibility only for the currently visible (filtered) columns. + const newColumnVisibility = { ...columnVisibility }; + filteredColumns.forEach((column) => { + newColumnVisibility[column] = selectedColumnIds.includes(column); + }); - return { ...acc, [key]: false }; - }, - {} - ); + // Create a new order array with all existing columns and newly selected columns + const updatedOrder = [ + ...columnOrder, + ...selectedColumnIds.filter((id) => !columnOrder.includes(id)), + ]; - const originalColsOrder = columnOrder.filter((columnId) => - valueKeys.includes(columnId) - ); - const newlyAddedCols = valueKeys.filter( - (columnId) => !columnOrder.includes(columnId) + // Remove any columns that are no longer selected + const finalOrder = updatedOrder.filter( + (id) => selectedColumnIds.includes(id) || !filteredColumns.includes(id) ); - const newColumnOrder = [...originalColsOrder, ...newlyAddedCols]; - setColumnVisibility(newColumnVisibility); - setColumnOrder(newColumnOrder); + setColumnOrder(finalOrder); closePopover(); }; - return ( {({ close }) => ( @@ -109,15 +114,22 @@ export default function ColumnSelection({ context={context} /> Set table fields + setSearchTerm(e.target.value)} + className="mt-2" + />
    - {columnsOptions.map((column) => ( + {filteredColumns.map((column) => (
  • diff --git a/keep-ui/app/alerts/alert-table-tab-panel.tsx b/keep-ui/app/alerts/alert-table-tab-panel.tsx index 7ad5e3e96..a481655ec 100644 --- a/keep-ui/app/alerts/alert-table-tab-panel.tsx +++ b/keep-ui/app/alerts/alert-table-tab-panel.tsx @@ -1,108 +1,125 @@ - import { AlertTable } from "./alert-table"; - import { useAlertTableCols } from "./alert-table-utils"; - import { AlertDto, AlertKnownKeys, Preset, getTabsFromPreset } from "./models"; +import { AlertTable } from "./alert-table"; +import { useAlertTableCols } from "./alert-table-utils"; +import { AlertDto, AlertKnownKeys, Preset, getTabsFromPreset } from "./models"; - const getPresetAlerts = (alert: AlertDto, presetName: string): boolean => { - if (presetName === "deleted") { - return alert.deleted === true; - } +const getPresetAlerts = (alert: AlertDto, presetName: string): boolean => { + if (presetName === "deleted") { + return alert.deleted === true; + } - if (presetName === "groups") { - return alert.group === true; - } + if (presetName === "groups") { + return alert.group === true; + } - if (presetName === "feed") { - return alert.deleted === false && alert.dismissed === false; - } + if (presetName === "feed") { + return alert.deleted === false && alert.dismissed === false; + } - if (presetName === "dismissed") { - return alert.dismissed === true; - } + if (presetName === "dismissed") { + return alert.dismissed === true; + } - if (presetName === "without-incident") { - return alert.incident === null; - } + if (presetName === "without-incident") { + return alert.incident === null; + } + return true; +}; - return true; - }; +interface Props { + alerts: AlertDto[]; + preset: Preset; + isAsyncLoading: boolean; + setTicketModalAlert: (alert: AlertDto | null) => void; + setNoteModalAlert: (alert: AlertDto | null) => void; + setRunWorkflowModalAlert: (alert: AlertDto | null) => void; + setDismissModalAlert: (alert: AlertDto[] | null) => void; + setChangeStatusAlert: (alert: AlertDto | null) => void; + mutateAlerts: () => void; +} - interface Props { - alerts: AlertDto[]; - preset: Preset; - isAsyncLoading: boolean; - setTicketModalAlert: (alert: AlertDto | null) => void; - setNoteModalAlert: (alert: AlertDto | null) => void; - setRunWorkflowModalAlert: (alert: AlertDto | null) => void; - setDismissModalAlert: (alert: AlertDto[] | null) => void; - setChangeStatusAlert: (alert: AlertDto | null) => void; - mutateAlerts: () => void; - } +export default function AlertTableTabPanel({ + alerts, + preset, + isAsyncLoading, + setTicketModalAlert, + setNoteModalAlert, + setRunWorkflowModalAlert, + setDismissModalAlert, + setChangeStatusAlert, + mutateAlerts, +}: Props) { + const sortedPresetAlerts = alerts + .filter((alert) => getPresetAlerts(alert, preset.name)) + .sort((a, b) => { + // Shahar: we want noise alert first. If no noisy (most of the cases) we want the most recent first. + const noisyA = a.isNoisy && a.status == "firing" ? 1 : 0; + const noisyB = b.isNoisy && b.status == "firing" ? 1 : 0; - export default function AlertTableTabPanel({ - alerts, - preset, - isAsyncLoading, - setTicketModalAlert, - setNoteModalAlert, - setRunWorkflowModalAlert, - setDismissModalAlert, - setChangeStatusAlert, - mutateAlerts - }: Props) { - const sortedPresetAlerts = alerts - .filter((alert) => getPresetAlerts(alert, preset.name)) - .sort((a, b) => { - // Shahar: we want noise alert first. If no noisy (most of the cases) we want the most recent first. - const noisyA = (a.isNoisy && a.status == "firing") ? 1 : 0; - const noisyB = (b.isNoisy && b.status == "firing") ? 1 : 0; + // Primary sort based on noisy flag (true first) + if (noisyA !== noisyB) { + return noisyB - noisyA; + } - // Primary sort based on noisy flag (true first) - if (noisyA !== noisyB) { - return noisyB - noisyA; - } - - // Secondary sort based on time (most recent first) - return b.lastReceived.getTime() - a.lastReceived.getTime(); + // Secondary sort based on time (most recent first) + return b.lastReceived.getTime() - a.lastReceived.getTime(); }); - const additionalColsToGenerate = [ - ...new Set( - alerts - .flatMap((alert) => Object.keys(alert)) - .filter((key) => AlertKnownKeys.includes(key) === false) - ), - ]; + const additionalColsToGenerate = [ + ...new Set( + alerts.flatMap((alert) => { + const keys = Object.keys(alert).filter( + (key) => !AlertKnownKeys.includes(key) + ); + return keys.flatMap((key) => { + if ( + typeof alert[key as keyof AlertDto] === "object" && + alert[key as keyof AlertDto] !== null + ) { + return Object.keys(alert[key as keyof AlertDto] as object).map( + (subKey) => `${key}.${subKey}` + ); + } + return key; + }); + }) + ), + ]; - const alertTableColumns = useAlertTableCols({ - additionalColsToGenerate: additionalColsToGenerate, - isCheckboxDisplayed: - preset.name !== "deleted" && preset.name !== "dismissed", - isMenuDisplayed: true, - setTicketModalAlert: setTicketModalAlert, - setNoteModalAlert: setNoteModalAlert, - setRunWorkflowModalAlert: setRunWorkflowModalAlert, - setDismissModalAlert: setDismissModalAlert, - setChangeStatusAlert: setChangeStatusAlert, - presetName: preset.name, - presetNoisy: preset.is_noisy, - }); + const alertTableColumns = useAlertTableCols({ + additionalColsToGenerate: additionalColsToGenerate, + isCheckboxDisplayed: + preset.name !== "deleted" && preset.name !== "dismissed", + isMenuDisplayed: true, + setTicketModalAlert: setTicketModalAlert, + setNoteModalAlert: setNoteModalAlert, + setRunWorkflowModalAlert: setRunWorkflowModalAlert, + setDismissModalAlert: setDismissModalAlert, + setChangeStatusAlert: setChangeStatusAlert, + presetName: preset.name, + presetNoisy: preset.is_noisy, + }); - const presetTabs = getTabsFromPreset(preset); + const presetTabs = getTabsFromPreset(preset); - return ( - - ); - } + return ( + + ); +} diff --git a/keep-ui/app/alerts/alert-table-utils.tsx b/keep-ui/app/alerts/alert-table-utils.tsx index b1fb285ac..9d33102c5 100644 --- a/keep-ui/app/alerts/alert-table-utils.tsx +++ b/keep-ui/app/alerts/alert-table-utils.tsx @@ -139,7 +139,20 @@ export const useAlertTableCols = ( header: colName, minSize: 100, cell: (context) => { - const alertValue = context.row.original[colName as keyof AlertDto]; + const keys = colName.split("."); + let alertValue: any = context.row.original; + for (const key of keys) { + if ( + alertValue && + typeof alertValue === "object" && + key in alertValue + ) { + alertValue = alertValue[key as keyof typeof alertValue]; + } else { + alertValue = undefined; + break; + } + } if (typeof alertValue === "object" && alertValue !== null) { return ( diff --git a/pyproject.toml b/pyproject.toml index 2e269dea8..1dfcc6d45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "keep" -version = "0.25.4" +version = "0.26.0" description = "Alerting. for developers, by developers." authors = ["Keep Alerting LTD"] readme = "README.md"