diff --git a/docs/providers/documentation/ilert-provider.mdx b/docs/providers/documentation/ilert-provider.mdx index f8676a395..cd53fce11 100644 --- a/docs/providers/documentation/ilert-provider.mdx +++ b/docs/providers/documentation/ilert-provider.mdx @@ -1,9 +1,9 @@ --- -title: "Ilert Provider" -sidebarTitle: "Ilert Provider" +title: "ilert Provider" +sidebarTitle: "ilert Provider" description: "The ilert provider enables the creation, updating, and resolution of events or incidents on ilert, leveraging both incident management and event notification capabilities for effective incident response." --- -# Ilert Provider +# ilert Provider ## Overview @@ -21,7 +21,7 @@ Depending on the `_type` specified, the provider will route the operation to the ### Incident Management - `summary`: A brief summary of the incident. This is required for creating a new incident. -- `status`: `IlertIncidentStatus` - The current status of the incident (e.g., INVESTIGATING, RESOLVED, MONITORING, IDENTIFIED). +- `status`: `ilertIncidentStatus` - The current status of the incident (e.g., INVESTIGATING, RESOLVED, MONITORING, IDENTIFIED). - `message`: A detailed message describing the incident or situation. Default is an empty string. - `affectedServices`: A JSON string representing the list of affected services and their statuses. Default is an empty array (`"[]"`). - `id`: The ID of the incident to update. If set to `"0"`, a new incident will be created. @@ -69,5 +69,5 @@ This provider is part of Keep's integration with ilert, designed to enhance oper ## Useful Links -- [ilert API Documentation](https://api.ilert.com/api-docs/) -- [ilert Alerting](https://www.ilert.com/product/reliable-actionable-alerting) +- [ilert API Documentation](https://api.ilert.com/api-docs/?utm_campaign=Keep&utm_source=integration&utm_medium=organic) +- [ilert Alerting](https://www.ilert.com/product/reliable-actionable-alerting?utm_campaign=Keep&utm_source=integration&utm_medium=organic) diff --git a/docs/providers/overview.mdx b/docs/providers/overview.mdx index 7a45033ce..49069603c 100644 --- a/docs/providers/overview.mdx +++ b/docs/providers/overview.mdx @@ -181,7 +181,7 @@ By leveraging Keep Providers, users are able to deeply integrate Keep with the t > diff --git a/keep-ui/app/dashboard/GridLayout.tsx b/keep-ui/app/dashboard/GridLayout.tsx index 9a33bfb0c..89ed7e4db 100644 --- a/keep-ui/app/dashboard/GridLayout.tsx +++ b/keep-ui/app/dashboard/GridLayout.tsx @@ -3,6 +3,7 @@ import { Responsive, WidthProvider, Layout } from "react-grid-layout"; import GridItemContainer from "./GridItemContainer"; import { LayoutItem, WidgetData } from "./types"; import "react-grid-layout/css/styles.css"; +import { Preset } from "app/alerts/models"; const ResponsiveGridLayout = WidthProvider(Responsive); @@ -12,9 +13,10 @@ interface GridLayoutProps { data: WidgetData[]; onEdit: (id: string) => void; onDelete: (id: string) => void; + presets: Preset[]; } -const GridLayout: React.FC = ({ layout, onLayoutChange, data, onEdit, onDelete }) => { +const GridLayout: React.FC = ({ layout, onLayoutChange, data, onEdit, onDelete, presets }) => { const layouts = { lg: layout }; return ( @@ -39,11 +41,15 @@ const GridLayout: React.FC = ({ layout, onLayoutChange, data, o compactType={null} draggableHandle=".grid-item__widget" > - {data.map((item) => ( -
- -
- ))} + {data.map((item) => { + //Fixing the static hardcode db value. + const preset = presets?.find(p => p?.id === item?.preset?.id); + item.preset = { ...item.preset,alerts_count: preset?.alerts_count ?? 0}; + return ( +
+ +
+ )})} ); }; diff --git a/keep-ui/app/dashboard/[id]/dashboard.tsx b/keep-ui/app/dashboard/[id]/dashboard.tsx index 535f06e64..f9d4009de 100644 --- a/keep-ui/app/dashboard/[id]/dashboard.tsx +++ b/keep-ui/app/dashboard/[id]/dashboard.tsx @@ -13,12 +13,21 @@ import { useDashboards } from 'utils/hooks/useDashboards'; import { getApiURL } from 'utils/apiUrl'; import './../styles.css'; import { toast } from 'react-toastify'; +import { GenericFilters } from '@/components/filters/GenericFilters'; +import { useDashboardPreset } from 'utils/hooks/useDashboardPresets'; + +const DASHBOARD_FILTERS = [ + { + type: "date", + key: "time_stamp", + value: "", + name: "Last received", + } +] const DashboardPage = () => { - const { useAllPresets, useStaticPresets } = usePresets(); - const { data: presets = [] } = useAllPresets(); - const { data: staticPresets = [] } = useStaticPresets(); - const { id } : any = useParams(); + const allPresets = useDashboardPreset(); + const { id }: any = useParams(); const { data: session } = useSession(); const { dashboards, isLoading, mutate: mutateDashboard } = useDashboards(); const [isModalOpen, setIsModalOpen] = useState(false); @@ -39,8 +48,6 @@ const DashboardPage = () => { } }, [id, dashboards, isLoading]); - const allPresets = [...presets, ...staticPresets]; - const openModal = () => { setEditingItem(null); // Ensure new modal opens without editing item context setIsModalOpen(true); @@ -163,6 +170,8 @@ const DashboardPage = () => { color="orange" /> +
+
+
{layout.length === 0 ? ( { data={widgetData} onEdit={handleEditWidget} onDelete={handleDeleteWidget} + presets={allPresets} /> )} diff --git a/keep-ui/app/mapping/create-or-edit-mapping.tsx b/keep-ui/app/mapping/create-or-edit-mapping.tsx index 870a81a53..8109c8061 100644 --- a/keep-ui/app/mapping/create-or-edit-mapping.tsx +++ b/keep-ui/app/mapping/create-or-edit-mapping.tsx @@ -329,9 +329,14 @@ export default function CreateOrEditMapping({ editRule, editCallback }: Props) { ... ) : ( attributes - .filter( - (attribute) => !selectedLookupAttributes.includes(attribute) - ) + .filter((attribute) => { + return !selectedLookupAttributes.some((lookupAttr) => { + const parts = lookupAttr + .split("&&") + .map((part) => part.trim()); + return parts.includes(attribute); + }); + }) .map((attribute) => ( {attribute} diff --git a/keep-ui/components/filters/GenericFilters.tsx b/keep-ui/components/filters/GenericFilters.tsx new file mode 100644 index 000000000..7d30e9f6c --- /dev/null +++ b/keep-ui/components/filters/GenericFilters.tsx @@ -0,0 +1,339 @@ +import GenericPopover from "@/components/popover/GenericPopover"; +import { Textarea, Badge, Button, Tab, TabGroup, TabList } from "@tremor/react"; +import moment from "moment"; +import { usePathname, useSearchParams, useRouter } from "next/navigation"; +import { useRef, useState, useEffect, ChangeEvent } from "react"; +import { GoPlusCircle } from "react-icons/go"; +import { DateRangePicker, DateRangePickerValue, Title } from "@tremor/react"; +import { MdOutlineDateRange } from "react-icons/md"; +import { IconType } from "react-icons"; +import { endOfDay } from "date-fns"; + + +type Filter = { + key: string; + value: string | string[] | Record; + type: string; + options?: { value: string; label: string }[]; + name: string; + icon?: IconType; +}; + +interface FiltersProps { + filters: Filter[]; +} + +interface PopoverContentProps { + filterRef: React.MutableRefObject; + filterKey: string; + type: string; +} + +function toArray(value: string | string[]) { + if (!value) return []; + + if (!Array.isArray(value) && value) { + return [value]; + } + return value; +} + +// TODO: Testing is needed +function CustomSelect({ + filter, + setLocalFilter, +}: { + filter: Filter | null; + setLocalFilter: (value: string | string[]) => void; +}) { + const filterKey = filter?.key || ""; + const [selectedOptions, setSelectedOptions] = useState>( + new Set() + ); + + useEffect(() => { + if (filter) { + setSelectedOptions(new Set(toArray(filter.value as string | string[]))); + } + }, [filter]); + + const handleCheckboxChange = (option: string, checked: boolean) => { + setSelectedOptions((prev) => { + const updatedOptions = new Set(prev); + if (checked) { + updatedOptions.add(option); + } else { + updatedOptions.delete(option); + } + if (filter) { + setLocalFilter(Array.from(updatedOptions)); + // setFilter((prev) => ({ ...prev, ...filter })); + } + return updatedOptions; + }); + }; + + if (!filter) { + return null; + } + + return ( + <> + + Select {filterKey?.charAt(0)?.toUpperCase() + filterKey?.slice(1)} + +
    + {filter.options?.map((option) => ( +
  • + +
  • + ))} +
+ + ); +} + +function getParsedValue(filter: Filter) { + const value = filter?.value as string; + if (!value) { + return 0; + } + if (typeof value !== "string") { + return 0; + } + try { + return JSON.parse(value) || {}; + } catch (e) { + return 0; + } +} + +function CustomDate({ + filter, + handleDate, +}: { + filter: Filter | null; + handleDate: (from?: Date, to?: Date) => void; +}) { + const [dateRange, setDateRange] = useState({ + from: undefined, + to: undefined, + }); + + const onDateRangePickerChange = ({ + from: start, + to: end, + }: DateRangePickerValue) => { + const endDate = end || start; + const endOfDayDate = endDate ? endOfDay(endDate) : end; + + + setDateRange({ from: start ?? undefined, to: endOfDayDate ?? undefined }); + handleDate(start, endOfDayDate); + }; + + useEffect(() => { + if (filter) { + const filterValue = getParsedValue(filter!); + const from = filterValue.start ? new Date(filterValue.start) : undefined; + const to = filterValue.end ? new Date(filterValue.end) : undefined; + onDateRangePickerChange({ from, to }); + } + }, [filter?.value]); + + if (!filter) return null; + + return ( +
+ +
+ ); +} + +const PopoverContent: React.FC = ({ + filterRef, + filterKey, + type, +}) => { + // Initialize local state for selected options + + const filter = filterRef.current?.find((filter) => filter.key === filterKey); + + const [localFilter, setLocalFilter] = useState(null); + + useEffect(() => { + if (filter) { + setLocalFilter({ ...filter }); + } + }, []); + + useEffect(() => { + if (localFilter && filter) { + filter.value = localFilter.value; + } + }, [localFilter?.value]); + + const handleLocalFilter = (value: string | string[]) => { + if (filter) { + filter.value = value; + } + setLocalFilter((prev) => { + if (prev) { + return { ...prev, value }; + } + return null; + }); + }; + + const handleDate = (start?: Date, end?: Date) => { + let newValue = "" + if (!start && !end) { + newValue = ""; + } else { + newValue = JSON.stringify({ + start: start, + end: end || start, + }); + } + if (filter) { + filter.value = newValue; + } + setLocalFilter((prev) => { + if (prev) { + return { ...prev, value: newValue }; + } + return null; + }); + }; + + // Return the appropriate content based on the selected type + switch (type) { + case "select": + return ( + + ); + case "date": + return ; + default: + return null; + } +}; + +export const GenericFilters: React.FC = ({ filters }) => { + // Initialize filterRef to store filter values + const filterRef = useRef(filters); + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const searchParamString = searchParams?.toString(); + const [apply, setApply] = useState(false); + + useEffect(() => { + if (apply && filterRef.current) { + const newParams = new URLSearchParams( + searchParams ? searchParams.toString() : "" + ); + const keys = filterRef.current.map((filter) => filter.key); + keys.forEach((key) => newParams.delete(key)); + for (const { key, value } of filterRef.current) { + if (Array.isArray(value)) { + for (const item of value) { + newParams.append(key, item); + } + } else if (value && typeof value === "string") { + newParams.append(key, value); + } else if (value && typeof value === "object") { + for (const [k, v] of Object.entries(value)) { + newParams.append(`$[${k}]`, v); + } + } + } + + router.push(`${pathname}?${newParams.toString()}`); + setApply(false); // Reset apply state + } + }, [apply]); + + useEffect(() => { + if (searchParams) { + // Convert URLSearchParams to a key-value pair object + const entries = Array.from(searchParams.entries()); + const params = entries.reduce((acc, [key, value]) => { + if (key in acc) { + if (Array.isArray(acc[key])) { + acc[key] = [...acc[key], value]; + return acc; + }else { + acc[key] = [acc[key] as string, value]; + } + return acc; + } + acc[key] = value; + return acc; + }, {} as Record); + + // Update filterRef.current with the new params + filterRef.current = filters.map((filter) => ({ + ...filter, + value: params[filter.key] || "", + })); + } + }, [searchParamString, filters]); + // Handle textarea value change + const onValueChange = (e: ChangeEvent) => { + //to do handle the value change + e.preventDefault(); + if (filterRef.current) { + } + }; + + // Handle key down event for textarea + const handleKeyDown = (e: any) => { + if (e.key === "Enter") { + e.preventDefault(); + setApply(true); + } + }; + + return ( +
+ {filters && + filters?.map(({ key, type, name, icon }) => { + //only type==select and date need popover i guess other text and textarea can be handled different. for now handling select and date + icon = icon ?? type === "date" ? MdOutlineDateRange : GoPlusCircle; + return ( +
+ + } + onApply={() => setApply(true)} + /> +
+ ); + })} + {/* TODO : Add clear filters functionality */} + {/* */} +
+ ); +}; diff --git a/keep-ui/components/popover/GenericPopover.tsx b/keep-ui/components/popover/GenericPopover.tsx index f7bb1df07..bc990d1ce 100644 --- a/keep-ui/components/popover/GenericPopover.tsx +++ b/keep-ui/components/popover/GenericPopover.tsx @@ -63,7 +63,7 @@ const GenericPopover: React.FC = ({ /> {content}