Files
homarr/packages/widgets/src/wazuh-top-list/component.tsx
T
+12 fcd7c979df feat!: release v2 (#6545)
Co-authored-by: Artur <145776561+arturict@users.noreply.github.com>
Co-authored-by: arturict <arturict@users.noreply.github.com>
Co-authored-by: Thomas <thomas@ajnart.fr>
Co-authored-by: Afonso Jorge Ramos <afonsojorgeramos@gmail.com>
Co-authored-by: Danura30082 <danura30082+github@gmail.com>
Co-authored-by: Kushie <122986913+kushiemoon-dev@users.noreply.github.com>
Co-authored-by: ajnart <thomas@ajnart.fr>
Co-authored-by: ajnart <thomas@ajnart.dev>
Co-authored-by: chenjun <chenjun@foxmail.com>
Co-authored-by: Wikibear <wikibear@users.noreply.github.com>
Co-authored-by: Bergasha <bergasha@gmail.com>
Co-authored-by: leafyelin <43306196+leafyelin@users.noreply.github.com>
Co-authored-by: Lucas Araujo <lucas.lra@gmail.com>
Co-authored-by: GreenMonito <86085570+GreenMonito@users.noreply.github.com>
Co-authored-by: Nerdy-Technician <roffo@nerdy-technician.social>
Co-authored-by: Jun <34545896+jcstrong@users.noreply.github.com>
Co-authored-by: Edmond Abraham <54119952+gitedmond@users.noreply.github.com>
Co-authored-by: thrawnx <tight.bulb9384@fastmail.com>
Co-authored-by: Chris <58918656+nordada@users.noreply.github.com>
Co-authored-by: yi111 <153097222+Yi-111-a@users.noreply.github.com>
Co-authored-by: Yanis <naamaneyanis@gmail.com>
Co-authored-by: Wikibear <me@sej.de>
Co-authored-by: alessiottavo <93441886+alessiottavo@users.noreply.github.com>
2026-10-02 21:00:14 +02:00

202 lines
8.2 KiB
TypeScript

"use client";
import { Badge, Box, Center, Group, ScrollArea, Stack, Text, Tooltip } from "@mantine/core";
import { IconDeviceDesktop, IconListNumbers, IconTarget, IconWorld } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import type { WazuhTopListEntry, WazuhTopListKind } from "@homarr/integrations/types";
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
import type { TablerIcon } from "@homarr/ui";
import { WidgetEmptyState } from "../common/empty-state";
import { getUsableWidgetQueryData, isInitialWidgetQueryPending } from "../common/query-state";
import type { WidgetComponentProps } from "../definition";
import { isPrivateIp, WazuhChip, WazuhLevelBadge } from "../wazuh/_shared/badges";
import { WazuhErrorState } from "../wazuh/_shared/error-state";
import { WazuhPendingState, WazuhWidgetFrame } from "../wazuh/_shared/frame";
import type { WazuhDashboardLinks } from "../wazuh/_shared/links";
import { formatWazuhCount, getWazuhSeverityForLevel, wazuhSeverityChartColors } from "../wazuh/_shared/severity";
import { useWazuhLinks } from "../wazuh/_shared/use-wazuh-links";
import classes from "../wazuh/_shared/wazuh.module.css";
const kindIcons: Record<WazuhTopListKind, TablerIcon> = {
rules: IconListNumbers,
agents: IconDeviceDesktop,
sourceIps: IconWorld,
mitreTactics: IconTarget,
mitreTechniques: IconTarget,
};
const kindColors: Record<WazuhTopListKind, string> = {
rules: "blue",
agents: "teal",
sourceIps: "cyan",
mitreTactics: "violet",
mitreTechniques: "violet",
};
const getEntryHref = (
kind: WazuhTopListKind,
entry: WazuhTopListEntry,
links: WazuhDashboardLinks | null,
range: string,
): string | undefined => {
if (!links) return undefined;
switch (kind) {
case "rules":
return links.events({ query: `rule.id:${entry.key}`, from: `now-${range}` });
case "agents":
return entry.detail ? links.agent(entry.detail) : undefined;
case "sourceIps":
return links.events({ query: `data.srcip:"${entry.key}"`, from: `now-${range}` });
default:
return links.mitre;
}
};
export default function WazuhTopListWidget({
integrationIds,
options,
width,
height,
}: WidgetComponentProps<"wazuhTopList">) {
const t = useI18n("widget.wazuhTopList");
const tShared = useI18n("widget.wazuh");
const locale = useCurrentIntlLocale();
const integrationId = integrationIds[0] ?? "";
const query = clientApi.widget.wazuh.getTopList.useQuery(
{ integrationId, kind: options.kind, range: options.range, limit: options.limit, minLevel: options.minLevel },
{ staleTime: 60_000, refetchInterval: 2 * 60_000 },
);
const result = getUsableWidgetQueryData(query);
const links = useWazuhLinks(result?.dashboardUrl);
const compact = height < 170 || width < 240;
const title = t(`title.${options.kind}`, { range: tShared(`range.${options.range}`) });
const Icon = kindIcons[options.kind];
if (isInitialWidgetQueryPending(query)) {
return (
<WazuhWidgetFrame icon={Icon} title={title} compact={compact} iconColor={kindColors[options.kind]}>
<WazuhPendingState />
</WazuhWidgetFrame>
);
}
if (!result) return <WidgetEmptyState />;
if (result.error) return <WazuhErrorState error={result.error} compact={height < 160} />;
const list = result.data;
const max = Math.max(1, ...list.entries.map((entry) => entry.count));
const headerHref =
options.kind === "mitreTactics" || options.kind === "mitreTechniques"
? links?.mitre
: options.kind === "agents"
? links?.agents
: links?.events({ from: `now-${options.range}` });
const showDetailColumn = width >= 300;
const showAgentsColumn = width >= 380 && options.kind !== "agents";
return (
<WazuhWidgetFrame
icon={Icon}
title={title}
href={headerHref}
compact={compact}
iconColor={kindColors[options.kind]}
headerRight={
<Tooltip label={t("totalTooltip", { count: list.total.toLocaleString(locale) })} withArrow>
<Badge size="sm" variant="light" color="gray" radius="sm">
{formatWazuhCount(list.total, locale)}
</Badge>
</Tooltip>
}
>
{list.entries.length === 0 ? (
<Center h="100%">
<Text size="sm" c="dimmed" ta="center">
{t(`empty.${options.kind}`)}
</Text>
</Center>
) : (
<ScrollArea h="100%" type="auto" scrollbarSize={6} offsetScrollbars="y">
<Stack gap={1}>
{list.entries.map((entry, index) => {
const href = getEntryHref(options.kind, entry, links, options.range);
const severity = entry.maxLevel !== null ? getWazuhSeverityForLevel(entry.maxLevel) : "low";
const share = entry.count / max;
const isIp = options.kind === "sourceIps";
return (
<Box
key={entry.key}
component={href ? "a" : "div"}
{...(href ? { href, target: "_blank", rel: "noopener noreferrer" } : {})}
className={classes.row}
style={{ flexDirection: "column", alignItems: "stretch", gap: 3, paddingBlock: compact ? 2 : 4 }}
>
<Group gap={8} wrap="nowrap">
<Text size="xs" c="dimmed" w={14} ta="right" className={classes.numeric}>
{index + 1}
</Text>
{options.kind === "rules" && entry.maxLevel !== null && <WazuhLevelBadge level={entry.maxLevel} />}
<Text
size="xs"
fw={500}
truncate
style={{ flex: 1, minWidth: 0 }}
className={isIp ? classes.mono : undefined}
title={entry.label}
>
{entry.label}
</Text>
{isIp && width >= 260 && (
<WazuhChip color={isPrivateIp(entry.key) ? undefined : "red"}>
{isPrivateIp(entry.key) ? t("private") : t("public")}
</WazuhChip>
)}
{showDetailColumn && entry.detail && options.kind !== "agents" && (
<Text size="xs" c="dimmed" className={`${classes.mono} ${classes.nowrap}`}>
{options.kind === "rules" ? `#${entry.detail}` : entry.detail}
</Text>
)}
{showAgentsColumn && entry.agents !== null && (
<Tooltip label={t("agents", { count: String(entry.agents) })} withArrow openDelay={300}>
<Text size="xs" c="dimmed" className={classes.numeric}>
<IconDeviceDesktop size={11} style={{ verticalAlign: -1, marginRight: 2 }} />
{entry.agents}
</Text>
</Tooltip>
)}
<Text size="xs" fw={700} miw={40} ta="right" className={classes.numeric}>
{formatWazuhCount(entry.count, locale)}
</Text>
</Group>
{options.showBars && (
<Box pl={22}>
<div className={classes.bar}>
<div
className={classes.barFill}
style={{
width: `${Math.max(share * 100, 1)}%`,
background:
options.kind === "mitreTactics" || options.kind === "mitreTechniques"
? "var(--mantine-color-violet-5)"
: `var(--mantine-color-${wazuhSeverityChartColors[severity].replace(".", "-")})`,
}}
/>
</div>
</Box>
)}
</Box>
);
})}
{list.others > 0 && height >= 200 && (
<Text size="xs" c="dimmed" ta="center" mt={4}>
{t("others", { count: formatWazuhCount(list.others, locale) })}
</Text>
)}
</Stack>
</ScrollArea>
)}
</WazuhWidgetFrame>
);
}