From e10bc349623978efea8e11ad2b575eaf4c7be7a7 Mon Sep 17 00:00:00 2001 From: Test Date: Sun, 7 Apr 2024 16:05:20 +0300 Subject: [PATCH 01/16] fix(ui): truncate alert name in history panel (#1060) --- keep-ui/app/alerts/alert-history.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keep-ui/app/alerts/alert-history.tsx b/keep-ui/app/alerts/alert-history.tsx index 0577d535d..33a0db974 100644 --- a/keep-ui/app/alerts/alert-history.tsx +++ b/keep-ui/app/alerts/alert-history.tsx @@ -49,8 +49,8 @@ const AlertHistoryPanel = ({ return ( -
- History of: {alertsHistoryWithDate.at(0)?.name} +
+ History of: {alertsHistoryWithDate.at(0)?.name} Showing: {alertsHistoryWithDate.length} alerts (1000 maximum) From d791925f316f51838fa37fe5b5b6b236c03e8548 Mon Sep 17 00:00:00 2001 From: Shahar Glazner Date: Sun, 7 Apr 2024 19:14:08 +0300 Subject: [PATCH 02/16] feat: some enhancments to the search bar (#1057) Co-authored-by: Test --- keep-ui/app/alerts/ColumnSelection.tsx | 1 + keep-ui/app/alerts/ThemeSelection.tsx | 123 ++++++++++++++++ keep-ui/app/alerts/TitleAndFilters.tsx | 12 +- keep-ui/app/alerts/ViewAlertModal.tsx | 40 ++++++ keep-ui/app/alerts/alert-menu.tsx | 23 +++ keep-ui/app/alerts/alert-severity.tsx | 4 +- keep-ui/app/alerts/alert-table-headers.tsx | 98 +++++++++---- keep-ui/app/alerts/alert-table-tab-panel.tsx | 3 + keep-ui/app/alerts/alert-table-utils.tsx | 25 ++++ keep-ui/app/alerts/alert-table.tsx | 26 +++- keep-ui/app/alerts/alerts-rules-builder.tsx | 140 ++++++++++++++++++- keep-ui/app/alerts/alerts-table-body.tsx | 53 ++++--- keep-ui/app/alerts/alerts.tsx | 9 +- keep-ui/app/alerts/models.tsx | 4 +- keep-ui/package-lock.json | 10 ++ keep-ui/package.json | 1 + 16 files changed, 505 insertions(+), 67 deletions(-) create mode 100644 keep-ui/app/alerts/ThemeSelection.tsx create mode 100644 keep-ui/app/alerts/ViewAlertModal.tsx diff --git a/keep-ui/app/alerts/ColumnSelection.tsx b/keep-ui/app/alerts/ColumnSelection.tsx index 80df6122d..772e6ad04 100644 --- a/keep-ui/app/alerts/ColumnSelection.tsx +++ b/keep-ui/app/alerts/ColumnSelection.tsx @@ -83,6 +83,7 @@ export default function ColumnSelection({ closePopover(); }; + return ( {({ close }) => ( diff --git a/keep-ui/app/alerts/ThemeSelection.tsx b/keep-ui/app/alerts/ThemeSelection.tsx new file mode 100644 index 000000000..f91c2c7a0 --- /dev/null +++ b/keep-ui/app/alerts/ThemeSelection.tsx @@ -0,0 +1,123 @@ +import React, { useState, Fragment, useRef, FormEvent } from 'react'; +import { Popover } from '@headlessui/react'; +import { Button, Tab, TabGroup, TabList, TabPanels, TabPanel } from "@tremor/react"; +import { IoColorPaletteOutline } from 'react-icons/io5'; +import { FloatingArrow, arrow, offset, useFloating } from "@floating-ui/react"; + +const predefinedThemes = { + Transparent: { + critical: 'bg-white', + high: 'bg-white', + warning: 'bg-white', + low: 'bg-white', + info: 'bg-white' + }, + Keep: { + critical: 'bg-orange-400', // Highest opacity for critical + high: 'bg-orange-300', + warning: 'bg-orange-200', + low: 'bg-orange-100', + info: 'bg-orange-50' // Lowest opacity for info + }, + Basic: { + critical: 'bg-red-200', + high: 'bg-orange-200', + warning: 'bg-yellow-200', + low: 'bg-green-200', + info: 'bg-blue-200' + } +}; + +const themeKeyMapping = { + 0: 'Transparent', + 1: 'Keep', + 2: 'Basic' +}; +type ThemeName = keyof typeof predefinedThemes; + +export const ThemeSelection = ({ onThemeChange }: { onThemeChange: (theme: any) => void }) => { + const arrowRef = useRef(null); + const [selectedTab, setSelectedTab] = useState('Transparent'); + + const { refs, floatingStyles, context } = useFloating({ + strategy: "fixed", + placement: "bottom-end", + middleware: [offset({ mainAxis: 10 }), arrow({ element: arrowRef })], + }); + + const handleThemeChange = (event: any) => { + const themeIndex = event as 0 | 1 | 2; + handleApplyTheme(themeIndex as 0 | 1 | 2); + }; + + + + + const handleApplyTheme = (themeKey: keyof typeof themeKeyMapping) => { + const themeName = themeKeyMapping[themeKey]; + setSelectedTab(themeName as ThemeName); +}; + + + + const onApplyTheme = (close: () => void) => { + // themeName is now assured to be a key of predefinedThemes + const themeName: ThemeName = selectedTab; + const newTheme = predefinedThemes[themeName]; // This should now be error-free + onThemeChange(newTheme); + setSelectedTab('Transparent'); // Assuming 'Transparent' is a valid key + close(); // Close the popover + }; + + return ( + + {({ close }) => ( + <> + + + + + Set theme colors + + + Transparent + Keep + Basic + + + {Object.keys(predefinedThemes).map(themeName => ( + + {Object.entries(predefinedThemes[themeName as keyof typeof predefinedThemes]).map(([severity, color]) => ( +
+ {severity.charAt(0).toUpperCase() + severity.slice(1).toLowerCase()} +
+
+ ))} +
+ ))} +
+
+ +
+ + )} +
+ ); +}; diff --git a/keep-ui/app/alerts/TitleAndFilters.tsx b/keep-ui/app/alerts/TitleAndFilters.tsx index 15cead0b7..4f7c8cfdb 100644 --- a/keep-ui/app/alerts/TitleAndFilters.tsx +++ b/keep-ui/app/alerts/TitleAndFilters.tsx @@ -3,17 +3,24 @@ import { DateRangePicker, DateRangePickerValue, Title } from "@tremor/react"; import { AlertDto } from "./models"; import ColumnSelection from "./ColumnSelection"; import { LastRecieved } from "./LastReceived"; +import { ThemeSelection } from './ThemeSelection'; + +type Theme = { + [key: string]: string; +}; type TableHeaderProps = { presetName: string; alerts: AlertDto[]; table: Table; + onThemeChange: (newTheme: Theme) => void; }; export const TitleAndFilters = ({ presetName, alerts, table, + onThemeChange, }: TableHeaderProps) => { const onDateRangePickerChange = ({ from: start, @@ -46,7 +53,10 @@ export const TitleAndFilters = ({ onValueChange={onDateRangePickerChange} enableYearNavigation /> - +
+ + +
diff --git a/keep-ui/app/alerts/ViewAlertModal.tsx b/keep-ui/app/alerts/ViewAlertModal.tsx new file mode 100644 index 000000000..a04adb0d1 --- /dev/null +++ b/keep-ui/app/alerts/ViewAlertModal.tsx @@ -0,0 +1,40 @@ +import { AlertDto } from "./models"; // Adjust the import path as needed +import Modal from "@/components/ui/Modal"; // Ensure this path matches your project structure +import { Button } from "@tremor/react"; +import { toast } from "react-toastify"; + +interface ViewAlertModalProps { + alert: AlertDto | null | undefined; + handleClose: () => void; +} + +export const ViewAlertModal: React.FC = ({ alert, handleClose }) => { + const isOpen = !!alert; + + const handleCopy = async () => { + if (alert) { + try { + await navigator.clipboard.writeText(JSON.stringify(alert, null, 2)); + toast.success("Alert copied to clipboard!"); + } catch (err) { + toast.error("Failed to copy alert."); + } + } + }; + + return ( + +
+

Alert Details

+ +
+ {alert && ( +
+          {JSON.stringify(alert, null, 2)}
+        
+ )} +
+ ); +}; diff --git a/keep-ui/app/alerts/alert-menu.tsx b/keep-ui/app/alerts/alert-menu.tsx index fbcfb0ade..ea9766358 100644 --- a/keep-ui/app/alerts/alert-menu.tsx +++ b/keep-ui/app/alerts/alert-menu.tsx @@ -8,6 +8,7 @@ import { TrashIcon, UserPlusIcon, PlayIcon, + EyeIcon } from "@heroicons/react/24/outline"; import { IoNotificationsOffOutline } from "react-icons/io5"; @@ -28,6 +29,7 @@ interface Props { setRunWorkflowModalAlert?: (alert: AlertDto) => void; setDismissModalAlert?: (alert: AlertDto) => void; presetName: string; + setViewAlertModal?: (alert: AlertDto) => void; } export default function AlertMenu({ @@ -37,6 +39,7 @@ export default function AlertMenu({ setRunWorkflowModalAlert, setDismissModalAlert, presetName, + setViewAlertModal, }: Props) { const router = useRouter(); @@ -282,6 +285,26 @@ export default function AlertMenu({ )} )} + {/*View the alert */} + + {({ active }) => ( + + )} + {provider?.methods && provider?.methods?.length > 0 && (
diff --git a/keep-ui/app/alerts/alert-severity.tsx b/keep-ui/app/alerts/alert-severity.tsx index c788e4d9e..a7bdd634d 100644 --- a/keep-ui/app/alerts/alert-severity.tsx +++ b/keep-ui/app/alerts/alert-severity.tsx @@ -32,10 +32,10 @@ export default function AlertSeverity({ severity }: Props) { color = "orange"; severityText = Severity.High.toString(); break; - case "medium": + case "warning": color = "yellow"; icon = ArrowRightIcon; - severityText = Severity.Medium.toString(); + severityText = Severity.Warning.toString(); break; case "low": icon = ArrowDownRightIcon; diff --git a/keep-ui/app/alerts/alert-table-headers.tsx b/keep-ui/app/alerts/alert-table-headers.tsx index c22bc4ab5..84384a36f 100644 --- a/keep-ui/app/alerts/alert-table-headers.tsx +++ b/keep-ui/app/alerts/alert-table-headers.tsx @@ -1,6 +1,6 @@ // culled from https://github.com/cpvalente/ontime/blob/master/apps/client/src/features/cuesheet/cuesheet-table-elements/CuesheetHeader.tsx -import { CSSProperties, ReactNode } from "react"; +import { CSSProperties, ReactNode, useEffect } from "react"; import { closestCenter, DndContext, @@ -28,6 +28,8 @@ import { AlertDto } from "./models"; import { useLocalStorage } from "utils/hooks/useLocalStorage"; import { getColumnsIds } from "./alert-table-utils"; import classnames from "classnames"; +import { FaArrowUp, FaArrowDown, FaArrowRight } from 'react-icons/fa'; + interface DraggableHeaderCellProps { header: Header; @@ -65,33 +67,65 @@ const DraggableHeaderCell = ({ : "grab", }; + return ( -
- {children} -
- {column.getIsPinned() === false && ( -
+
{/* Flex container */} + {column.getCanSort() && ( // Sorting icon to the left + <> + { + console.log("clicked for sorting"); + event.stopPropagation(); + const toggleSorting = header.column.getToggleSortingHandler(); + if (toggleSorting) toggleSorting(event); + }} + title={ + column.getNextSortingOrder() === "asc" + ? "Sort ascending" + : column.getNextSortingOrder() === "desc" + ? "Sort descending" + : "Clear sort" + } + > + {/* Icon logic */} + {column.getIsSorted() ? ( + column.getIsSorted() === "asc" ? : + ) : ( + )} - onMouseDown={getResizeHandler()} - /> + + {/* Custom styled vertical line separator */} +
+ + )} + + {children} {/* Column name or text */} +
+ + {column.getIsPinned() === false && ( +
+ onMouseDown={getResizeHandler()} + /> + )} + + ); }; - interface Props { columns: ColumnDef[]; table: Table; @@ -108,17 +142,18 @@ export default function AlertsTableHeaders({ getColumnsIds(columns) ); + const sensors = useSensors( - useSensor(PointerSensor, { + useSensor(PointerSensor, { activationConstraint: { - delay: 100, - tolerance: 50, + delay: 250, // Adjust delay to prevent drag on quick clicks + tolerance: 5, // Adjust tolerance based on needs }, }), useSensor(TouchSensor, { activationConstraint: { - delay: 100, - tolerance: 50, + delay: 250, + tolerance: 5, }, }) ); @@ -162,13 +197,16 @@ export default function AlertsTableHeaders({ - {header.isPlaceholder - ? null - : flexRender( + {header.isPlaceholder ? null : ( +
+ {flexRender( header.column.columnDef.header, header.getContext() )} +
+ )}
); })} diff --git a/keep-ui/app/alerts/alert-table-tab-panel.tsx b/keep-ui/app/alerts/alert-table-tab-panel.tsx index 1db678c6c..a3ab3adce 100644 --- a/keep-ui/app/alerts/alert-table-tab-panel.tsx +++ b/keep-ui/app/alerts/alert-table-tab-panel.tsx @@ -30,6 +30,7 @@ interface Props { setNoteModalAlert: (alert: AlertDto | null) => void; setRunWorkflowModalAlert: (alert: AlertDto | null) => void; setDismissModalAlert: (alert: AlertDto | null) => void; + setViewAlertModal: (alert: AlertDto) => void; } export default function AlertTableTabPanel({ @@ -40,6 +41,7 @@ export default function AlertTableTabPanel({ setNoteModalAlert, setRunWorkflowModalAlert, setDismissModalAlert, + setViewAlertModal, }: Props) { const sortedPresetAlerts = alerts .filter((alert) => getPresetAlerts(alert, preset.name)) @@ -62,6 +64,7 @@ export default function AlertTableTabPanel({ setNoteModalAlert: setNoteModalAlert, setRunWorkflowModalAlert: setRunWorkflowModalAlert, setDismissModalAlert: setDismissModalAlert, + setViewAlertModal: setViewAlertModal, presetName: preset.name, }); diff --git a/keep-ui/app/alerts/alert-table-utils.tsx b/keep-ui/app/alerts/alert-table-utils.tsx index eac2d8eda..302707ee4 100644 --- a/keep-ui/app/alerts/alert-table-utils.tsx +++ b/keep-ui/app/alerts/alert-table-utils.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { ColumnDef, FilterFn, + Row, RowSelectionState, VisibilityState, createColumnHelper, @@ -17,6 +18,7 @@ import AlertAssignee from "./alert-assignee"; import AlertExtraPayload from "./alert-extra-payload"; import AlertMenu from "./alert-menu"; import { isSameDay, isValid, isWithinInterval, startOfDay } from "date-fns"; +import { Severity, severityMapping } from "./models"; export const DEFAULT_COLS = [ "checkbox", @@ -80,6 +82,24 @@ export const isDateWithinRange: FilterFn = (row, columnId, value) => { const columnHelper = createColumnHelper(); +const invertedSeverityMapping = Object.entries(severityMapping).reduce<{ [key: string]: number }>((acc, [key, value]) => { + acc[value as keyof typeof acc] = Number(key); + return acc; +}, {}); + + +const customSeveritySortFn = (rowA: any, rowB: any) => { + // Assuming rowA and rowB contain the data in a property (like 'original' or directly) + // Adjust the way to access severity values according to your data structure + const severityValueA = rowA.original?.severity; // or rowA.severity; + const severityValueB = rowB.original?.severity; // or rowB.severity; + + // Use the inverted mapping to get ranks + const rankA = invertedSeverityMapping[severityValueA] || 0; + const rankB = invertedSeverityMapping[severityValueB] || 0; + + return rankA > rankB ? 1 : rankA < rankB ? -1 : 0; +}; interface GenerateAlertTableColsArg { additionalColsToGenerate?: string[]; isCheckboxDisplayed?: boolean; @@ -89,6 +109,7 @@ interface GenerateAlertTableColsArg { setRunWorkflowModalAlert?: (alert: AlertDto) => void; setDismissModalAlert?: (alert: AlertDto) => void; presetName: string; + setViewAlertModal?: (alert: AlertDto) => void; } export const useAlertTableCols = ( @@ -101,6 +122,7 @@ export const useAlertTableCols = ( setRunWorkflowModalAlert, setDismissModalAlert, presetName, + setViewAlertModal, }: GenerateAlertTableColsArg = { presetName: "feed" } ) => { const [expandedToggles, setExpandedToggles] = useState({}); @@ -164,6 +186,8 @@ export const useAlertTableCols = ( header: "Severity", minSize: 100, cell: (context) => , + sortingFn: customSeveritySortFn, + }), columnHelper.display({ id: "name", @@ -271,6 +295,7 @@ export const useAlertTableCols = ( setIsMenuOpen={setCurrentOpenMenu} setRunWorkflowModalAlert={setRunWorkflowModalAlert} setDismissModalAlert={setDismissModalAlert} + setViewAlertModal={setViewAlertModal} /> ), }), diff --git a/keep-ui/app/alerts/alert-table.tsx b/keep-ui/app/alerts/alert-table.tsx index e367a6297..679a457f1 100644 --- a/keep-ui/app/alerts/alert-table.tsx +++ b/keep-ui/app/alerts/alert-table.tsx @@ -11,6 +11,8 @@ import { VisibilityState, ColumnSizingState, getFilteredRowModel, + SortingState, + getSortedRowModel, } from "@tanstack/react-table"; import AlertPagination from "./alert-pagination"; @@ -26,6 +28,8 @@ import AlertActions from "./alert-actions"; import AlertPresets from "./alert-presets"; import { evalWithContext } from "./alerts-rules-builder"; import { TitleAndFilters } from "./TitleAndFilters"; +import { severityMapping } from "./models"; +import { useState } from "react"; interface Props { alerts: AlertDto[]; @@ -43,6 +47,14 @@ export function AlertTable({ presetName, isRefreshAllowed = true, }: Props) { + const [theme, setTheme] = useLocalStorage('alert-table-theme', + Object.values(severityMapping).reduce<{ [key: string]: string }>((acc, severity) => { + acc[severity] = 'bg-white'; + return acc; + }, {}) + ); + + const columnsIds = getColumnsIds(columns); const [columnOrder] = useLocalStorage( @@ -60,6 +72,12 @@ export function AlertTable({ {} ); + const handleThemeChange = (newTheme: any) => { + setTheme(newTheme); + }; + + const [sorting, setSorting] = useState([]); + const table = useReactTable({ data: alerts, columns: columns, @@ -71,7 +89,10 @@ export function AlertTable({ left: ["checkbox"], right: ["alertMenu"], }, + sorting: sorting, }, + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), initialState: { pagination: { pageSize: 10 }, }, @@ -86,6 +107,7 @@ export function AlertTable({ columnResizeMode: "onChange", autoResetPageIndex: false, enableGlobalFilter: true, + enableSorting: true, }); const selectedRowIds = Object.entries( @@ -96,7 +118,7 @@ export function AlertTable({ return ( <> - + {selectedRowIds.length ? ( - + diff --git a/keep-ui/app/alerts/alerts-rules-builder.tsx b/keep-ui/app/alerts/alerts-rules-builder.tsx index a3e66dad7..57f1c1b04 100644 --- a/keep-ui/app/alerts/alerts-rules-builder.tsx +++ b/keep-ui/app/alerts/alerts-rules-builder.tsx @@ -16,6 +16,100 @@ import { AlertDto, Preset, severityMapping } from "./models"; import { XMarkIcon, TrashIcon } from "@heroicons/react/24/outline"; import { FiSave } from "react-icons/fi"; import { TbDatabaseImport } from "react-icons/tb"; +import Select, { components, MenuListProps } from 'react-select'; + +import { IoSearchOutline } from 'react-icons/io5'; +import { FiExternalLink } from 'react-icons/fi'; + + +const staticOptions = [ + { value: 'severity > 1', label: 'severity > info' }, + { value: 'status=="firing"', label: 'status is firing' }, + { value: 'source=="grafana"', label: 'source is grafana' }, + { value: 'message.contains("CPU")', label: 'cpu is high' }, +]; + + +const CustomOption = (props: any) => { + return ( + +
+ + {props.children} +
+
+ ); +}; + +const kbdStyle = { + background: '#eee', + borderRadius: '3px', + padding: '2px 4px', + margin: '0 2px', + fontWeight: 'bold', +}; + +// Custom MenuList with a static line at the end +const CustomMenuList = (props: MenuListProps<{}>) => { + return ( + + {props.children} +
+ Wildcard: source.contains("") + OR: || + AND: && + Enter to update query + + See Syntax Documentation + +
+
+ ); +}; + + +const customComponents = { + Control: () => null, // This hides the input field control + DropdownIndicator: null, // Optionally, hides the dropdown indicator if desired + IndicatorSeparator: null, + Option: CustomOption, + MenuList: CustomMenuList, +}; + +// Define the styles for react-select +const customStyles = { + option: (provided: any, state: any) => ({ + ...provided, + color: state.isFocused ? 'black' : 'black', + backgroundColor: state.isFocused ? 'rgba(255, 165, 0, 0.4)' : 'white', // Orange with opacity + cursor: 'pointer', + display: 'flex', + alignItems: 'center', // Align items in the center vertically + }), + menu: (provided: any) => ({ + ...provided, + margin: 0, // Remove the margin around the dropdown menu + borderRadius: '0', // Optional: Align with the border-radius of the Textarea if necessary + }), + // You can add more style customizations for other parts of the Select here if needed +}; + // Culled from: https://stackoverflow.com/a/54372020/12627235 const getAllMatches = (pattern: RegExp, string: string) => @@ -27,7 +121,7 @@ const sanitizeCELIntoJS = (celExpression: string): string => { let jsExpression = celExpression.replace(/contains/g, "includes"); // Replace severity comparisons with mapped values jsExpression = jsExpression.replace( - /severity\s*([<>=]+)\s*(\d)/g, + /severity\s*([<>]=?|==)\s*(\d)/g, (match, operator, number) => { const severityValue = severityMapping[number]; if (!severityValue) { @@ -144,9 +238,22 @@ export const AlertsRulesBuilder = ({ const [sqlError, setSqlError] = useState(null); const textAreaRef = useRef(null); + const wrapperRef = useRef(null); const isFirstRender = useRef(true); + const [showSuggestions, setShowSuggestions] = useState(false); + + const toggleSuggestions = () => { + setShowSuggestions(!showSuggestions); + }; + + const handleSelectChange = (selectedOption: any) => { + setCELRules(selectedOption.value); + toggleSuggestions(); + onApplyFilter(); + }; + const constructCELRules = (preset?: Preset) => { // Check if selectedPreset is defined and has options if (preset && preset.options) { @@ -173,6 +280,19 @@ export const AlertsRulesBuilder = ({ return ""; // Default to empty string if no preset or options are found }; + useEffect(() => { + function handleClickOutside(event: any) { + if (wrapperRef.current && !wrapperRef.current.contains(event.target)) { + setShowSuggestions(false); + } + } + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + useEffect(() => { // Use the constructCELRules function to set the initial value of celRules const initialCELRules = constructCELRules(selectedPreset); @@ -332,6 +452,8 @@ export const AlertsRulesBuilder = ({
+ + {/* Import SQL */} { @@ -359,6 +481,7 @@ export const AlertsRulesBuilder = ({
+ {/* Docs */}
{/* CEL badge and (i) icon container */} @@ -381,7 +504,7 @@ export const AlertsRulesBuilder = ({
{/* Textarea and error message container */} -
+