Skip to content

Commit

Permalink
feat: searchable columns + nested (#2225)
Browse files Browse the repository at this point in the history
  • Loading branch information
shahargl authored Oct 17, 2024
1 parent 0da791e commit 82e83b7
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 123 deletions.
68 changes: 40 additions & 28 deletions keep-ui/app/alerts/ColumnSelection.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -31,16 +31,19 @@ export default function ColumnSelection({
});
const tableColumns = table.getAllColumns();

const [, setColumnVisibility] = useLocalStorage<VisibilityState>(
`column-visibility-${presetName}`,
DEFAULT_COLS_VISIBILITY
);
const [columnVisibility, setColumnVisibility] =
useLocalStorage<VisibilityState>(
`column-visibility-${presetName}`,
DEFAULT_COLS_VISIBILITY
);

const [columnOrder, setColumnOrder] = useLocalStorage<ColumnOrderState>(
`column-order-${presetName}`,
DEFAULT_COLS
);

const [searchTerm, setSearchTerm] = useState("");

const columnsOptions = tableColumns
.filter((col) => col.getIsPinned() === false)
.map((col) => col.id);
Expand All @@ -49,41 +52,43 @@ 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<HTMLFormElement>,
closePopover: VoidFunction
) => {
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<VisibilityState>(
(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 (
<Popover as={Fragment}>
{({ close }) => (
Expand All @@ -109,15 +114,22 @@ export default function ColumnSelection({
context={context}
/>
<span className="text-gray-400 text-sm">Set table fields</span>
<TextInput
icon={FiSearch}
placeholder="Search fields..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="mt-2"
/>
<ul className="space-y-1 mt-3 max-h-96 overflow-auto">
{columnsOptions.map((column) => (
{filteredColumns.map((column) => (
<li key={column}>
<label className="cursor-pointer p-2">
<input
className="mr-2"
name={column}
type="checkbox"
defaultChecked={selectedColumns.includes(column)}
defaultChecked={columnVisibility[column]}
/>
{column}
</label>
Expand Down
203 changes: 110 additions & 93 deletions keep-ui/app/alerts/alert-table-tab-panel.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<AlertTable
alerts={sortedPresetAlerts}
columns={alertTableColumns}
setDismissedModalAlert={setDismissModalAlert}
isAsyncLoading={isAsyncLoading}
presetName={preset.name}
presetPrivate={preset.is_private}
presetNoisy={preset.is_noisy}
presetStatic={preset.name === "feed" || preset.name === "groups" || preset.name === "dismissed" || preset.name === "without-incident"}
presetId={preset.id}
presetTabs={presetTabs}
mutateAlerts={mutateAlerts}
/>
);
}
return (
<AlertTable
alerts={sortedPresetAlerts}
columns={alertTableColumns}
setDismissedModalAlert={setDismissModalAlert}
isAsyncLoading={isAsyncLoading}
presetName={preset.name}
presetPrivate={preset.is_private}
presetNoisy={preset.is_noisy}
presetStatic={
preset.name === "feed" ||
preset.name === "groups" ||
preset.name === "dismissed" ||
preset.name === "without-incident"
}
presetId={preset.id}
presetTabs={presetTabs}
mutateAlerts={mutateAlerts}
/>
);
}
15 changes: 14 additions & 1 deletion keep-ui/app/alerts/alert-table-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down

0 comments on commit 82e83b7

Please sign in to comment.