mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-26 14:28:58 +03:00
* Notice and status bar improvements Status bar problems added in the same pass got the same `Date.now()` id and overwrote each other, so usually only one showed. Messages now fall back to their text as the id. The desktop status bar shows the most severe message with a count of the rest that opens a popover listing all of them, and the mobile drawer stacks them vertically instead of placing them side by side. Dismissing a notice hid it for good, so a detector that restarted again after a dismissal was never shown. Dismiss is replaced by acknowledge, which hides a notice until it happens again, and mute, which hides it permanently. Kinds that never repeat (config and stream checks, the update notice) can only be muted. `reopen_at_count` is removed since acknowledge covers the failed login case. * move camera CPU warnings to notices High ffmpeg and detect CPU warnings sat in the status bar with no way to dismiss them. They're now `ffmpeg_high_cpu` and `detect_high_cpu` notices, raised per episode by the same tracker as skipped detections. Also stop failed login attempts held from before an acknowledgement from reopening the notice. * fix mypy and handle missing cpu stats in notices
211 lines
6.4 KiB
TypeScript
211 lines
6.4 KiB
TypeScript
import { Link } from "react-router-dom";
|
|
import { useTranslation } from "react-i18next";
|
|
import { FaTriangleExclamation } from "react-icons/fa6";
|
|
import {
|
|
LuBell,
|
|
LuBellOff,
|
|
LuCheck,
|
|
LuExternalLink,
|
|
LuEye,
|
|
LuInfo,
|
|
LuSlidersHorizontal,
|
|
LuX,
|
|
} from "react-icons/lu";
|
|
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from "@/components/ui/tooltip";
|
|
import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
|
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
|
import { useDocDomain } from "@/hooks/use-doc-domain";
|
|
import type { HealthProblem } from "@/types/health";
|
|
|
|
type HealthProblemRowProps = {
|
|
problem: HealthProblem;
|
|
};
|
|
|
|
const ICON_BUTTON_CLASS =
|
|
"size-6 shrink-0 text-muted-foreground hover:text-primary";
|
|
|
|
function RowAction({
|
|
label,
|
|
children,
|
|
}: {
|
|
label: string;
|
|
children: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
|
<TooltipPortal>
|
|
<TooltipContent>{label}</TooltipContent>
|
|
</TooltipPortal>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
export default function HealthProblemRow({ problem }: HealthProblemRowProps) {
|
|
const { t } = useTranslation(["views/system", "common"]);
|
|
const { getLocaleDocUrl } = useDocDomain();
|
|
const hasDetails = problem.scope || problem.meta;
|
|
const hasActions =
|
|
problem.link ||
|
|
problem.docLink ||
|
|
problem.externalLink ||
|
|
problem.onAcknowledge ||
|
|
problem.onMute ||
|
|
problem.onUnhide;
|
|
const unhideLabel =
|
|
problem.hidden === "muted"
|
|
? t("health.notices.unmute")
|
|
: t("health.notices.showAgain");
|
|
|
|
return (
|
|
<div
|
|
className="flex items-center gap-2 border-b border-border px-1 py-2 text-sm last:border-b-0"
|
|
data-testid={`health-problem-${problem.id}`}
|
|
data-severity={problem.severity}
|
|
>
|
|
<div className="flex shrink-0 self-start pt-0.5">
|
|
{problem.pending ? (
|
|
<ActivityIndicator className="" size={16} />
|
|
) : (
|
|
<>
|
|
{problem.severity === "error" && (
|
|
<LuX className="size-4 text-danger" />
|
|
)}
|
|
{problem.severity === "warning" && (
|
|
<FaTriangleExclamation className="size-4 text-yellow-500" />
|
|
)}
|
|
{problem.severity === "info" && (
|
|
<LuInfo className="size-4 text-selected" />
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<div>{problem.text}</div>
|
|
{hasDetails && (
|
|
<div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
|
|
{problem.scope && (
|
|
<span className="rounded bg-secondary px-1.5 py-0.5 text-xs text-primary-variant">
|
|
{problem.scopeIsCamera ? (
|
|
<CameraNameLabel camera={problem.scope} />
|
|
) : (
|
|
problem.scope
|
|
)}
|
|
</span>
|
|
)}
|
|
{problem.meta && <span>{problem.meta}</span>}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{hasActions && (
|
|
<div className="ml-auto flex shrink-0 items-center gap-1">
|
|
{problem.link && (
|
|
<RowAction label={t("health.notices.openSettings")}>
|
|
<Button
|
|
asChild
|
|
variant="ghost"
|
|
size="icon"
|
|
className={ICON_BUTTON_CLASS}
|
|
>
|
|
<Link
|
|
to={problem.link}
|
|
aria-label={t("health.notices.openSettings")}
|
|
>
|
|
<LuSlidersHorizontal className="size-3.5" />
|
|
</Link>
|
|
</Button>
|
|
</RowAction>
|
|
)}
|
|
{problem.docLink && (
|
|
<RowAction label={t("readTheDocumentation", { ns: "common" })}>
|
|
<Button
|
|
asChild
|
|
variant="ghost"
|
|
size="icon"
|
|
className={ICON_BUTTON_CLASS}
|
|
>
|
|
<a
|
|
href={getLocaleDocUrl(problem.docLink)}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
aria-label={t("readTheDocumentation", { ns: "common" })}
|
|
>
|
|
<LuExternalLink className="size-3.5" />
|
|
</a>
|
|
</Button>
|
|
</RowAction>
|
|
)}
|
|
{problem.externalLink && (
|
|
<RowAction label={t("health.notices.openLink")}>
|
|
<Button
|
|
asChild
|
|
variant="ghost"
|
|
size="icon"
|
|
className={ICON_BUTTON_CLASS}
|
|
>
|
|
<a
|
|
href={problem.externalLink}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
aria-label={t("health.notices.openLink")}
|
|
>
|
|
<LuExternalLink className="size-3.5" />
|
|
</a>
|
|
</Button>
|
|
</RowAction>
|
|
)}
|
|
{problem.onAcknowledge && (
|
|
<RowAction label={t("health.notices.acknowledgeHint")}>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className={ICON_BUTTON_CLASS}
|
|
aria-label={t("health.notices.acknowledge")}
|
|
onClick={problem.onAcknowledge}
|
|
>
|
|
<LuCheck className="size-3.5" />
|
|
</Button>
|
|
</RowAction>
|
|
)}
|
|
{problem.onMute && (
|
|
<RowAction label={t("health.notices.muteHint")}>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className={ICON_BUTTON_CLASS}
|
|
aria-label={t("health.notices.mute")}
|
|
onClick={problem.onMute}
|
|
>
|
|
<LuBellOff className="size-3.5" />
|
|
</Button>
|
|
</RowAction>
|
|
)}
|
|
{problem.onUnhide && (
|
|
<RowAction label={unhideLabel}>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className={ICON_BUTTON_CLASS}
|
|
aria-label={unhideLabel}
|
|
onClick={problem.onUnhide}
|
|
>
|
|
{problem.hidden === "muted" ? (
|
|
<LuBell className="size-3.5" />
|
|
) : (
|
|
<LuEye className="size-3.5" />
|
|
)}
|
|
</Button>
|
|
</RowAction>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|