-
Notifications
You must be signed in to change notification settings - Fork 700
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into fix/2462-use-config-double-fetch
- Loading branch information
Showing
12 changed files
with
782 additions
and
427 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,269 @@ | ||
import React from "react"; | ||
import { | ||
AlertFacetsProps, | ||
FacetValue, | ||
FacetFilters, | ||
} from "./alert-table-facet-types"; | ||
import { Facet } from "./alert-table-facet"; | ||
import { | ||
getFilteredAlertsForFacet, | ||
getSeverityOrder, | ||
} from "./alert-table-facet-utils"; | ||
import { useLocalStorage } from "utils/hooks/useLocalStorage"; | ||
import { AlertDto } from "./models"; | ||
import { | ||
DynamicFacet, | ||
DynamicFacetWrapper, | ||
AddFacetModal, | ||
} from "./alert-table-facet-dynamic"; | ||
import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline"; | ||
|
||
export const AlertFacets: React.FC<AlertFacetsProps> = ({ | ||
alerts, | ||
facetFilters, | ||
setFacetFilters, | ||
dynamicFacets, | ||
setDynamicFacets, | ||
onDelete, | ||
className, | ||
table, | ||
}) => { | ||
const presetName = window.location.pathname.split("/").pop() || "default"; | ||
|
||
const [isModalOpen, setIsModalOpen] = useLocalStorage<boolean>( | ||
`addFacetModalOpen-${presetName}`, | ||
false | ||
); | ||
|
||
const handleSelect = ( | ||
facetKey: string, | ||
value: string, | ||
exclusive: boolean, | ||
isAllOnly: boolean | ||
) => { | ||
const newFilters = { ...facetFilters }; | ||
|
||
if (isAllOnly) { | ||
if (exclusive) { | ||
newFilters[facetKey] = [value]; | ||
} else { | ||
delete newFilters[facetKey]; | ||
} | ||
} else { | ||
if (exclusive) { | ||
newFilters[facetKey] = [value]; | ||
} else { | ||
const currentValues = newFilters[facetKey] || []; | ||
if (currentValues.includes(value)) { | ||
newFilters[facetKey] = currentValues.filter((v) => v !== value); | ||
if (newFilters[facetKey].length === 0) { | ||
delete newFilters[facetKey]; | ||
} | ||
} else { | ||
newFilters[facetKey] = [...currentValues, value]; | ||
} | ||
} | ||
} | ||
|
||
setFacetFilters(newFilters); | ||
}; | ||
|
||
const getFacetValues = (key: keyof AlertDto | string): FacetValue[] => { | ||
const filteredAlerts = getFilteredAlertsForFacet(alerts, facetFilters, key); | ||
const valueMap = new Map<string, number>(); | ||
let nullCount = 0; | ||
|
||
filteredAlerts.forEach((alert) => { | ||
let value; | ||
|
||
// Handle nested keys like "labels.host" | ||
if (typeof key === "string" && key.includes(".")) { | ||
const [parentKey, childKey] = key.split("."); | ||
const parentValue = alert[parentKey as keyof AlertDto]; | ||
|
||
if ( | ||
typeof parentValue === "object" && | ||
parentValue !== null && | ||
!Array.isArray(parentValue) && | ||
!(parentValue instanceof Date) | ||
) { | ||
value = (parentValue as Record<string, unknown>)[childKey]; | ||
} else { | ||
value = undefined; | ||
} | ||
} else { | ||
value = alert[key as keyof AlertDto]; | ||
} | ||
|
||
if (Array.isArray(value)) { | ||
if (value.length === 0) { | ||
nullCount++; | ||
} else { | ||
value.forEach((v) => { | ||
valueMap.set(v, (valueMap.get(v) || 0) + 1); | ||
}); | ||
} | ||
} else if (value !== undefined && value !== null) { | ||
const strValue = String(value); | ||
valueMap.set(strValue, (valueMap.get(strValue) || 0) + 1); | ||
} else { | ||
nullCount++; | ||
} | ||
}); | ||
|
||
let values = Array.from(valueMap.entries()).map(([label, count]) => ({ | ||
label, | ||
count, | ||
isSelected: | ||
facetFilters[key]?.includes(label) || !facetFilters[key]?.length, | ||
})); | ||
|
||
if (["assignee", "incident"].includes(key as string) && nullCount > 0) { | ||
values.push({ | ||
label: "n/a", | ||
count: nullCount, | ||
isSelected: | ||
facetFilters[key]?.includes("n/a") || !facetFilters[key]?.length, | ||
}); | ||
} | ||
|
||
if (key === "severity") { | ||
values.sort((a, b) => { | ||
if (a.label === "n/a") return 1; | ||
if (b.label === "n/a") return -1; | ||
const orderDiff = getSeverityOrder(a.label) - getSeverityOrder(b.label); | ||
if (orderDiff !== 0) return orderDiff; | ||
return b.count - a.count; | ||
}); | ||
} else { | ||
values.sort((a, b) => { | ||
if (a.label === "n/a") return 1; | ||
if (b.label === "n/a") return -1; | ||
return b.count - a.count; | ||
}); | ||
} | ||
|
||
return values; | ||
}; | ||
|
||
const staticFacets = [ | ||
"severity", | ||
"status", | ||
"source", | ||
"assignee", | ||
"dismissed", | ||
"incident", | ||
]; | ||
|
||
const handleAddFacet = (column: string) => { | ||
setDynamicFacets([ | ||
...dynamicFacets, | ||
{ | ||
key: column, | ||
name: column.charAt(0).toUpperCase() + column.slice(1), | ||
}, | ||
]); | ||
}; | ||
|
||
const handleDeleteFacet = (facetKey: string) => { | ||
setDynamicFacets(dynamicFacets.filter((df) => df.key !== facetKey)); | ||
const newFilters = { ...facetFilters }; | ||
delete newFilters[facetKey]; | ||
setFacetFilters(newFilters); | ||
}; | ||
|
||
return ( | ||
<div className={className}> | ||
<div className="space-y-2"> | ||
{/* Facet button */} | ||
<button | ||
onClick={() => setIsModalOpen(true)} | ||
className="w-full mt-2 px-2 py-1 text-sm text-gray-600 hover:bg-gray-100 rounded flex items-center gap-2" | ||
> | ||
<PlusIcon className="h-4 w-4" /> | ||
Add Facet | ||
</button> | ||
<Facet | ||
name="Severity" | ||
values={getFacetValues("severity")} | ||
onSelect={(value, exclusive, isAllOnly) => | ||
handleSelect("severity", value, exclusive, isAllOnly) | ||
} | ||
facetKey="severity" | ||
facetFilters={facetFilters} | ||
/> | ||
<Facet | ||
name="Status" | ||
values={getFacetValues("status")} | ||
onSelect={(value, exclusive, isAllOnly) => | ||
handleSelect("status", value, exclusive, isAllOnly) | ||
} | ||
facetKey="status" | ||
facetFilters={facetFilters} | ||
/> | ||
<Facet | ||
name="Source" | ||
values={getFacetValues("source")} | ||
onSelect={(value, exclusive, isAllOnly) => | ||
handleSelect("source", value, exclusive, isAllOnly) | ||
} | ||
facetKey="source" | ||
facetFilters={facetFilters} | ||
/> | ||
<Facet | ||
name="Assignee" | ||
values={getFacetValues("assignee")} | ||
onSelect={(value, exclusive, isAllOnly) => | ||
handleSelect("assignee", value, exclusive, isAllOnly) | ||
} | ||
facetKey="assignee" | ||
facetFilters={facetFilters} | ||
/> | ||
<Facet | ||
name="Dismissed" | ||
values={getFacetValues("dismissed")} | ||
onSelect={(value, exclusive, isAllOnly) => | ||
handleSelect("dismissed", value, exclusive, isAllOnly) | ||
} | ||
facetKey="dismissed" | ||
facetFilters={facetFilters} | ||
/> | ||
<Facet | ||
name="Incident Related" | ||
facetKey="incident" | ||
values={getFacetValues("incident")} | ||
onSelect={(value, exclusive, isAllOnly) => | ||
handleSelect("incident", value, exclusive, isAllOnly) | ||
} | ||
facetFilters={facetFilters} | ||
/> | ||
{/* Dynamic facets */} | ||
{dynamicFacets.map((facet) => ( | ||
<DynamicFacetWrapper | ||
key={facet.key} | ||
name={facet.name} | ||
values={getFacetValues(facet.key as keyof AlertDto)} | ||
onSelect={(value, exclusive, isAllOnly) => | ||
handleSelect(facet.key, value, exclusive, isAllOnly) | ||
} | ||
facetKey={facet.key} | ||
facetFilters={facetFilters} | ||
onDelete={() => handleDeleteFacet(facet.key)} | ||
/> | ||
))} | ||
|
||
{/* Facet Modal */} | ||
<AddFacetModal | ||
isOpen={isModalOpen} | ||
onClose={() => setIsModalOpen(false)} | ||
table={table} | ||
onAddFacet={handleAddFacet} | ||
existingFacets={[ | ||
...staticFacets, | ||
...dynamicFacets.map((df) => df.key), | ||
]} | ||
/> | ||
</div> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import React, { useState } from "react"; | ||
import { TextInput } from "@tremor/react"; | ||
import { TrashIcon } from "@heroicons/react/24/outline"; | ||
import { FacetProps, FacetFilters } from "./alert-table-facet-types"; | ||
import { AlertDto } from "./models"; | ||
import { Facet } from "./alert-table-facet"; | ||
import Modal from "@/components/ui/Modal"; | ||
import { Table } from "@tanstack/table-core"; | ||
import { FiSearch } from "react-icons/fi"; | ||
|
||
export interface DynamicFacet { | ||
key: string; | ||
name: string; | ||
} | ||
|
||
interface AddFacetModalProps { | ||
isOpen: boolean; | ||
onClose: () => void; | ||
table: Table<AlertDto>; | ||
onAddFacet: (column: string) => void; | ||
existingFacets: string[]; | ||
} | ||
|
||
export const AddFacetModal: React.FC<AddFacetModalProps> = ({ | ||
isOpen, | ||
onClose, | ||
table, | ||
onAddFacet, | ||
existingFacets, | ||
}) => { | ||
const [searchTerm, setSearchTerm] = useState(""); | ||
|
||
const availableColumns = table | ||
.getAllColumns() | ||
.filter( | ||
(col) => | ||
// Filter out pinned columns and existing facets | ||
!col.getIsPinned() && | ||
!existingFacets.includes(col.id) && | ||
// Filter by search term | ||
col.id.toLowerCase().includes(searchTerm.toLowerCase()) | ||
) | ||
.map((col) => col.id); | ||
|
||
return ( | ||
<Modal | ||
isOpen={isOpen} | ||
onClose={onClose} | ||
title="Add New Facet" | ||
className="w-[400px]" | ||
> | ||
<div className="p-6"> | ||
<TextInput | ||
icon={FiSearch} | ||
placeholder="Search columns..." | ||
value={searchTerm} | ||
onChange={(e) => setSearchTerm(e.target.value)} | ||
className="mb-4" | ||
/> | ||
<div className="max-h-96 overflow-auto space-y-1"> | ||
{availableColumns.map((column) => ( | ||
<button | ||
key={column} | ||
onClick={() => { | ||
onAddFacet(column); | ||
onClose(); | ||
}} | ||
className="w-full text-left px-4 py-2 hover:bg-gray-100 rounded" | ||
> | ||
{column} | ||
</button> | ||
))} | ||
</div> | ||
</div> | ||
</Modal> | ||
); | ||
}; | ||
|
||
export interface DynamicFacetProps extends FacetProps { | ||
onDelete: () => void; | ||
} | ||
|
||
export const DynamicFacetWrapper: React.FC<DynamicFacetProps> = ({ | ||
onDelete, | ||
...facetProps | ||
}) => { | ||
return ( | ||
<div className="relative"> | ||
<button | ||
onClick={onDelete} | ||
className="absolute right-2 top-2 p-1 text-gray-400 hover:text-gray-600" | ||
> | ||
<TrashIcon className="h-4 w-4" /> | ||
</button> | ||
<Facet showIcon={false} {...facetProps} /> | ||
</div> | ||
); | ||
}; |
Oops, something went wrong.