diff --git a/keep-ui/features/filter/facets-panel.tsx b/keep-ui/features/filter/facets-panel.tsx index 2b26120fb..e7448d40b 100644 --- a/keep-ui/features/filter/facets-panel.tsx +++ b/keep-ui/features/filter/facets-panel.tsx @@ -1,10 +1,16 @@ import React, { useEffect, useMemo, useState } from "react"; import { Facet } from "./facet"; -import { CreateFacetDto, FacetDto, FacetOptionDto, FacetOptionsQueries } from "./models"; +import { + CreateFacetDto, + FacetDto, + FacetOptionDto, + FacetOptionsQueries, +} from "./models"; import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { useLocalStorage } from "@/utils/hooks/useLocalStorage"; import { AddFacetModal } from "./add-facet-modal"; -import 'react-loading-skeleton/dist/skeleton.css'; +import "react-loading-skeleton/dist/skeleton.css"; +import clsx from "clsx"; /** * It's facets state. Key is the facet id, and value is Set of unselected options. @@ -12,34 +18,40 @@ import 'react-loading-skeleton/dist/skeleton.css'; */ type FacetState = { [facetId: string]: Set; -} +}; -function buildCel(facets: FacetDto[], facetOptions: { [key: string]: FacetOptionDto[] }, facetsState: FacetState): string { +function buildCel( + facets: FacetDto[], + facetOptions: { [key: string]: FacetOptionDto[] }, + facetsState: FacetState +): string { const cel = Object.values(facets) - .filter((facet) => facet.id in facetsState) - .map((facet) => { - const notSelectedOptions = Object.values(facetOptions[facet.id]) - .filter((facetOption) => facetsState[facet.id]?.has(facetOption.display_name)) - .map((option) => { - if (typeof option.value === 'string') { - return `'${option.value}'`; - } else if (option.value == null) { - return 'null'; - } - - return option.value; - }); - - if (!notSelectedOptions.length) { - return; + .filter((facet) => facet.id in facetsState) + .map((facet) => { + const notSelectedOptions = Object.values(facetOptions[facet.id]) + .filter((facetOption) => + facetsState[facet.id]?.has(facetOption.display_name) + ) + .map((option) => { + if (typeof option.value === "string") { + return `'${option.value}'`; + } else if (option.value == null) { + return "null"; } - return `!(${facet.property_path} in [${notSelectedOptions.join(", ")}])`; - }) - .filter((query) => query) - .map((facetCel) => `${facetCel}`) - .map((query) => query) - .join(" && "); + return option.value; + }); + + if (!notSelectedOptions.length) { + return; + } + + return `!(${facet.property_path} in [${notSelectedOptions.join(", ")}])`; + }) + .filter((query) => query) + .map((facetCel) => `${facetCel}`) + .map((query) => query) + .join(" && "); return cel; } @@ -52,13 +64,19 @@ export interface FacetsPanelProps { areFacetOptionsLoading?: boolean; /** Token to clear filters related to facets */ clearFiltersToken?: string | null; - /** + /** * Object with facets that should be unchecked by default. * Key is the facet name, value is the list of option values to uncheck. **/ uncheckedByDefaultOptionValues?: { [key: string]: string[] }; - renderFacetOptionLabel?: (facetName: string, optionDisplayName: string) => JSX.Element | string | undefined; - renderFacetOptionIcon?: (facetName: string, optionDisplayName: string) => JSX.Element | undefined; + renderFacetOptionLabel?: ( + facetName: string, + optionDisplayName: string + ) => JSX.Element | string | undefined; + renderFacetOptionIcon?: ( + facetName: string, + optionDisplayName: string + ) => JSX.Element | undefined; onCelChange: (cel: string) => void; onAddFacet: (createFacet: CreateFacetDto) => void; onDeleteFacet: (facetId: string) => void; @@ -93,12 +111,18 @@ export const FacetsPanel: React.FC = ({ const [celState, setCelState] = useState(""); function getFacetState(facetId: string): Set { - if (!defaultStateHandledForFacetIds.has(facetId) && uncheckedByDefaultOptionValues && Object.keys(uncheckedByDefaultOptionValues).length) { + if ( + !defaultStateHandledForFacetIds.has(facetId) && + uncheckedByDefaultOptionValues && + Object.keys(uncheckedByDefaultOptionValues).length + ) { const facetState = new Set(...(facetsState[facetId] || [])); const facet = facets.find((f) => f.id === facetId); if (facet) { - uncheckedByDefaultOptionValues[facet?.name]?.forEach((optionValue) => facetState.add(optionValue)); + uncheckedByDefaultOptionValues[facet?.name]?.forEach((optionValue) => + facetState.add(optionValue) + ); defaultStateHandledForFacetIds.add(facetId); } @@ -110,7 +134,7 @@ export const FacetsPanel: React.FC = ({ const isOptionSelected = (facet_id: string, option_id: string) => { return !facetsState[facet_id] || !facetsState[facet_id].has(option_id); - } + }; function calculateFacetsState(newFacetsState: FacetState): void { setFacetsState(newFacetsState); @@ -125,10 +149,14 @@ export const FacetsPanel: React.FC = ({ facets.forEach((facet) => { const otherFacets = facets.filter((f) => f.id !== facet.id); - facetOptionQueries[facet.id] = buildCel(otherFacets, facetOptions, newFacetsState); - }) + facetOptionQueries[facet.id] = buildCel( + otherFacets, + facetOptions, + newFacetsState + ); + }); - onReloadFacetOptions && onReloadFacetOptions(facetOptionQueries) + onReloadFacetOptions && onReloadFacetOptions(facetOptionQueries); } function toggleFacetOption(facetId: string, value: string) { @@ -136,9 +164,9 @@ export const FacetsPanel: React.FC = ({ const facetState = getFacetState(facetId); if (isOptionSelected(facetId, value)) { - facetState.add(value) + facetState.add(value); } else { - facetState.delete(value) + facetState.delete(value); } calculateFacetsState({ ...facetsState, [facetId]: facetState }); @@ -148,14 +176,14 @@ export const FacetsPanel: React.FC = ({ setClickedFacetId(facetId); const facetState = getFacetState(facetId); - facetOptions[facetId].forEach(facetOption => { + facetOptions[facetId].forEach((facetOption) => { if (facetOption.display_name === optionValue) { facetState.delete(optionValue); return; } facetState.add(facetOption.display_name); - }) + }); calculateFacetsState({ ...facetsState, @@ -167,8 +195,9 @@ export const FacetsPanel: React.FC = ({ setClickedFacetId(facetId); const facetState = getFacetState(facetId); - Object.values(facetOptions[facetId]) - .forEach((option) => (facetState.delete(option.display_name))); + Object.values(facetOptions[facetId]).forEach((option) => + facetState.delete(option.display_name) + ); calculateFacetsState({ ...facetsState, @@ -180,15 +209,21 @@ export const FacetsPanel: React.FC = ({ calculateFacetsState({}); } - useEffect(function clearFiltersWhenTokenChange(): void { - if (clearFiltersToken) { - clearFilters(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [clearFiltersToken]); + useEffect( + function clearFiltersWhenTokenChange(): void { + if (clearFiltersToken) { + clearFilters(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, + [clearFiltersToken] + ); return ( -
+
{/* Facet button */} @@ -197,17 +232,17 @@ export const FacetsPanel: React.FC = ({ className="p-1 pr-2 text-sm text-gray-600 hover:bg-gray-100 rounded flex items-center gap-1" > - Add Facet + Add facet
- + {facets?.map((facet, index) => ( = ({ isStatic={facet.is_static} options={facetOptions?.[facet.id]} optionsLoading={!facetOptions?.[facet.id]} - optionsReloading={areFacetOptionsLoading && !!facet.id && clickedFacetId !== facet.id} + optionsReloading={ + areFacetOptionsLoading && + !!facet.id && + clickedFacetId !== facet.id + } onSelect={(value) => toggleFacetOption(facet.id, value)} onSelectOneOption={(value) => selectOneFacetOption(facet.id, value)} onSelectAllOptions={() => selectAllFacetOptions(facet.id)} facetState={getFacetState(facet.id)} facetKey={facet.id} - renderOptionLabel={(optionDisplayName) => renderFacetOptionLabel && renderFacetOptionLabel(facet.name, optionDisplayName)} - renderIcon={(optionDisplayName) => renderFacetOptionIcon && renderFacetOptionIcon(facet.name, optionDisplayName)} - onLoadOptions={() => onLoadFacetOptions && onLoadFacetOptions(facet.id)} + renderOptionLabel={(optionDisplayName) => + renderFacetOptionLabel && + renderFacetOptionLabel(facet.name, optionDisplayName) + } + renderIcon={(optionDisplayName) => + renderFacetOptionIcon && + renderFacetOptionIcon(facet.name, optionDisplayName) + } + onLoadOptions={() => + onLoadFacetOptions && onLoadFacetOptions(facet.id) + } onDelete={() => onDeleteFacet && onDeleteFacet(facet.id)} /> ))} @@ -232,7 +279,9 @@ export const FacetsPanel: React.FC = ({ setIsModalOpen(false)} - onAddFacet={(createFacet) => onAddFacet ? onAddFacet(createFacet) : null} + onAddFacet={(createFacet) => + onAddFacet ? onAddFacet(createFacet) : null + } />
diff --git a/keep-ui/features/incident-list/ui/incident-list.tsx b/keep-ui/features/incident-list/ui/incident-list.tsx index 5a2a1ab75..ebe8288db 100644 --- a/keep-ui/features/incident-list/ui/incident-list.tsx +++ b/keep-ui/features/incident-list/ui/incident-list.tsx @@ -25,7 +25,7 @@ import { getStatusIcon, getStatusColor } from "@/shared/lib/status-utils"; import { useUser } from "@/entities/users/model/useUser"; import { severityMapping } from "@/entities/alerts/model"; import { IncidentsNotFoundPlaceholder } from "./incidents-not-found"; -import { v4 as uuidV4 } from 'uuid'; +import { v4 as uuidV4 } from "uuid"; const AssigneeLabel = ({ email }: { email: string }) => { const user = useUser(email); @@ -81,13 +81,17 @@ export function IncidentList({ null ); - const [clearFiltersToken, setClearFiltersToken] = useState(null); - const [filterRevalidationToken, setFilterRevalidationToken] = useState(null); + const [clearFiltersToken, setClearFiltersToken] = useState( + null + ); + const [filterRevalidationToken, setFilterRevalidationToken] = useState< + string | null + >(null); const [isFormOpen, setIsFormOpen] = useState(false); useEffect(() => { setFilterRevalidationToken(incidentChangeToken); - }, [incidentChangeToken]) + }, [incidentChangeToken]); const handleCloseForm = () => { setIsFormOpen(false); @@ -129,7 +133,14 @@ export function IncidentList({ ); } if (facetName === "severity") { - return ; + return ( + + ); } if (facetName === "assignee") { return ; @@ -162,7 +173,7 @@ export function IncidentList({ const renderFacetOptionLabel = useCallback( (facetName: string, facetOptionName: string) => { facetName = facetName.toLowerCase(); - + switch (facetName) { case "assignee": if (!facetOptionName) { @@ -172,8 +183,9 @@ export function IncidentList({ case "dismissed": return facetOptionName === "true" ? "Dismissed" : "Not dismissed"; case "severity": { - const label = severityMapping[Number(facetOptionName)] || facetOptionName; - return {label}; + const label = + severityMapping[Number(facetOptionName)] || facetOptionName; + return {label}; } default: return {facetOptionName}; @@ -198,7 +210,9 @@ export function IncidentList({ if (filterCel && incidents?.items.length === 0) { return ( - setClearFiltersToken(uuidV4())} /> + setClearFiltersToken(uuidV4())} + /> ); } @@ -212,7 +226,7 @@ export function IncidentList({ } const uncheckedFacetOptionsByDefault: Record = { - "Status": ["resolved", "deleted"], + Status: ["resolved", "deleted"], }; return ( @@ -253,31 +267,29 @@ export function IncidentList({
- { - incidentsError ? ( - - ) : null - } - { - incidentsError ? null : ( -
- setFilterCel(cel)} - renderFacetOptionIcon={renderFacetOptionIcon} - renderFacetOptionLabel={renderFacetOptionLabel} - revalidationToken={filterRevalidationToken} - /> -
- {renderIncidents()} -
+ {incidentsError ? ( + + ) : null} + {incidentsError ? null : ( +
+ setFilterCel(cel)} + renderFacetOptionIcon={renderFacetOptionIcon} + renderFacetOptionLabel={renderFacetOptionLabel} + revalidationToken={filterRevalidationToken} + /> +
+ {renderIncidents()}
- ) - } +
+ )}
diff --git a/keep-ui/features/incident-list/ui/incident-table-component.tsx b/keep-ui/features/incident-list/ui/incident-table-component.tsx index 326fbe7f1..e090f83e9 100644 --- a/keep-ui/features/incident-list/ui/incident-table-component.tsx +++ b/keep-ui/features/incident-list/ui/incident-table-component.tsx @@ -21,15 +21,25 @@ interface Props { interface SortableHeaderCellProps { header: Header; children: ReactNode; + className?: string; } -const SortableHeaderCell = ({ header, children }: SortableHeaderCellProps) => { +const SortableHeaderCell = ({ + header, + children, + className, +}: SortableHeaderCellProps) => { const { column } = header; - const { style, className } = getCommonPinningStylesAndClassNames(column); + const { style, className: commonClassName } = + getCommonPinningStylesAndClassNames(column); return (
@@ -87,6 +97,7 @@ export const IncidentTableComponent = (props: Props) => { {flexRender( header.column.columnDef.header, @@ -113,6 +124,7 @@ export const IncidentTableComponent = (props: Props) => { key={cell.id} style={style} className={clsx( + cell.column.columnDef.meta?.tdClassName, className, "bg-white", cell.column.id === "actions" ? "p-1" : "" diff --git a/keep-ui/features/incident-list/ui/incidents-table.tsx b/keep-ui/features/incident-list/ui/incidents-table.tsx index 60a1240b3..27f3187bb 100644 --- a/keep-ui/features/incident-list/ui/incidents-table.tsx +++ b/keep-ui/features/incident-list/ui/incidents-table.tsx @@ -30,12 +30,13 @@ import { IncidentDropdownMenu } from "./incident-dropdown-menu"; import clsx from "clsx"; import { IncidentChangeStatusSelect } from "@/features/change-incident-status/"; import { useIncidentActions } from "@/entities/incidents/model"; -import { IncidentSeverityBadge } from "@/entities/incidents/ui"; import { getIncidentName } from "@/entities/incidents/lib/utils"; import { DateTimeField, TableIndeterminateCheckbox, TablePagination, + TableSeverityCell, + UISeverity, } from "@/shared/ui"; import { UserStatefulAvatar } from "@/entities/users/ui"; import { DynamicImageProviderIcon } from "@/components/ui"; @@ -121,6 +122,22 @@ export default function IncidentsTable({ }, [incidents.limit, incidents.offset, pagination, setPagination]); const columns = [ + columnHelper.display({ + id: "severity", + header: () => <>, + cell: ({ row }) => ( + + ), + size: 4, + minSize: 4, + maxSize: 4, + meta: { + tdClassName: "p-0", + thClassName: "p-0", + }, + }), columnHelper.display({ id: "selected", minSize: 32, @@ -179,13 +196,6 @@ export default function IncidentsTable({ id: "alerts_count", header: "Alerts", }), - columnHelper.accessor("severity", { - id: "severity", - header: "Severity", - cell: ({ row }) => ( - - ), - }), columnHelper.display({ id: "alert_sources", header: "Sources", @@ -266,7 +276,7 @@ export default function IncidentsTable({ pagination, sorting, columnPinning: { - left: ["selected"], + left: ["severity", "selected"], right: ["actions"], }, },