Link to relevant page from status bar warnings / errors (#11140)

* Use hash state for system pages

* Add links to items

* Add stats to other types

* Link on mobile as well

* Use link

* Cleanup using util
This commit is contained in:
Nicolas Mowen 2024-04-28 16:59:03 -06:00 committed by GitHub
parent c2c6113299
commit acf37f9920
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 79 additions and 24 deletions

View File

@ -9,6 +9,7 @@ import { useContext, useEffect, useMemo } from "react";
import { FaCheck } from "react-icons/fa"; import { FaCheck } from "react-icons/fa";
import { IoIosWarning } from "react-icons/io"; import { IoIosWarning } from "react-icons/io";
import { MdCircle } from "react-icons/md"; import { MdCircle } from "react-icons/md";
import { Link } from "react-router-dom";
import useSWR from "swr"; import useSWR from "swr";
export default function Statusbar() { export default function Statusbar() {
@ -43,7 +44,13 @@ export default function Statusbar() {
useEffect(() => { useEffect(() => {
clearMessages("stats"); clearMessages("stats");
potentialProblems.forEach((problem) => { potentialProblems.forEach((problem) => {
addMessage("stats", problem.text, problem.color); addMessage(
"stats",
problem.text,
problem.color,
undefined,
problem.relevantLink,
);
}); });
}, [potentialProblems, addMessage, clearMessages]); }, [potentialProblems, addMessage, clearMessages]);
@ -110,14 +117,25 @@ export default function Statusbar() {
) : ( ) : (
Object.entries(messages).map(([key, messageArray]) => ( Object.entries(messages).map(([key, messageArray]) => (
<div key={key} className="h-full flex items-center gap-2"> <div key={key} className="h-full flex items-center gap-2">
{messageArray.map(({ id, text, color }: StatusMessage) => ( {messageArray.map(({ id, text, color, link }: StatusMessage) => {
<div key={id} className="flex items-center text-sm gap-2"> const message = (
<IoIosWarning <div
className={`size-5 ${color || "text-danger"}`} key={id}
/> className={`flex items-center text-sm gap-2 ${link ? "hover:underline cursor-pointer" : ""}`}
{text} >
</div> <IoIosWarning
))} className={`size-5 ${color || "text-danger"}`}
/>
{text}
</div>
);
if (link) {
return <Link to={link}>{message}</Link>;
} else {
return message;
}
})}
</div> </div>
)) ))
)} )}

View File

@ -139,7 +139,7 @@ export default function GeneralSettings({ className }: GeneralSettings) {
<DropdownMenuLabel>System</DropdownMenuLabel> <DropdownMenuLabel>System</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuGroup className={isDesktop ? "" : "flex flex-col"}> <DropdownMenuGroup className={isDesktop ? "" : "flex flex-col"}>
<Link to="/system"> <Link to="/system#general">
<MenuItem <MenuItem
className={ className={
isDesktop isDesktop

View File

@ -13,6 +13,7 @@ import {
StatusBarMessagesContext, StatusBarMessagesContext,
StatusMessage, StatusMessage,
} from "@/context/statusbar-provider"; } from "@/context/statusbar-provider";
import { Link } from "react-router-dom";
function Bottombar() { function Bottombar() {
const navItems = useNavigation("secondary"); const navItems = useNavigation("secondary");
@ -51,7 +52,13 @@ function StatusAlertNav() {
useEffect(() => { useEffect(() => {
clearMessages("stats"); clearMessages("stats");
potentialProblems.forEach((problem) => { potentialProblems.forEach((problem) => {
addMessage("stats", problem.text, problem.color); addMessage(
"stats",
problem.text,
problem.color,
undefined,
problem.relevantLink,
);
}); });
}, [potentialProblems, addMessage, clearMessages]); }, [potentialProblems, addMessage, clearMessages]);
@ -68,14 +75,22 @@ function StatusAlertNav() {
<div className="w-full h-auto py-4 overflow-y-auto overflow-x-hidden flex flex-col items-center gap-2"> <div className="w-full h-auto py-4 overflow-y-auto overflow-x-hidden flex flex-col items-center gap-2">
{Object.entries(messages).map(([key, messageArray]) => ( {Object.entries(messages).map(([key, messageArray]) => (
<div key={key} className="w-full flex items-center gap-2"> <div key={key} className="w-full flex items-center gap-2">
{messageArray.map(({ id, text, color }: StatusMessage) => ( {messageArray.map(({ id, text, color, link }: StatusMessage) => {
<div key={id} className="flex items-center text-xs gap-2"> const message = (
<IoIosWarning <div key={id} className="flex items-center text-xs gap-2">
className={`size-5 ${color || "text-danger"}`} <IoIosWarning
/> className={`size-5 ${color || "text-danger"}`}
{text} />
</div> {text}
))} </div>
);
if (link) {
return <Link to={link}>{message}</Link>;
} else {
return message;
}
})}
</div> </div>
))} ))}
</div> </div>

View File

@ -10,6 +10,7 @@ export type StatusMessage = {
id: string; id: string;
text: string; text: string;
color?: string; color?: string;
link?: string;
}; };
export type StatusMessagesState = { export type StatusMessagesState = {
@ -27,6 +28,7 @@ type StatusBarMessagesContextValue = {
message: string, message: string,
color?: string, color?: string,
messageId?: string, messageId?: string,
link?: string,
) => string; ) => string;
removeMessage: (key: string, messageId: string) => void; removeMessage: (key: string, messageId: string) => void;
clearMessages: (key: string) => void; clearMessages: (key: string) => void;
@ -43,14 +45,20 @@ export function StatusBarMessagesProvider({
const messages = useMemo(() => messagesState, [messagesState]); const messages = useMemo(() => messagesState, [messagesState]);
const addMessage = useCallback( const addMessage = useCallback(
(key: string, message: string, color?: string, messageId?: string) => { (
key: string,
message: string,
color?: string,
messageId?: string,
link?: string,
) => {
const id = messageId || Date.now().toString(); const id = messageId || Date.now().toString();
const msgColor = color || "text-danger"; const msgColor = color || "text-danger";
setMessagesState((prevMessages) => ({ setMessagesState((prevMessages) => ({
...prevMessages, ...prevMessages,
[key]: [ [key]: [
...(prevMessages[key] || []), ...(prevMessages[key] || []),
{ id, text: message, color: msgColor }, { id, text: message, color: msgColor, link },
], ],
})); }));
return id; return id;

View File

@ -34,11 +34,13 @@ export default function useStats(stats: FrigateStats | undefined) {
problems.push({ problems.push({
text: `${capitalizeFirstLetter(key)} is very slow (${det["inference_speed"]} ms)`, text: `${capitalizeFirstLetter(key)} is very slow (${det["inference_speed"]} ms)`,
color: "text-danger", color: "text-danger",
relevantLink: "/system#general",
}); });
} else if (det["inference_speed"] > InferenceThreshold.warning) { } else if (det["inference_speed"] > InferenceThreshold.warning) {
problems.push({ problems.push({
text: `${capitalizeFirstLetter(key)} is slow (${det["inference_speed"]} ms)`, text: `${capitalizeFirstLetter(key)} is slow (${det["inference_speed"]} ms)`,
color: "text-orange-400", color: "text-orange-400",
relevantLink: "/system#general",
}); });
} }
}); });
@ -53,6 +55,7 @@ export default function useStats(stats: FrigateStats | undefined) {
problems.push({ problems.push({
text: `${capitalizeFirstLetter(name.replaceAll("_", " "))} is offline`, text: `${capitalizeFirstLetter(name.replaceAll("_", " "))} is offline`,
color: "text-danger", color: "text-danger",
relevantLink: "logs",
}); });
} }
}); });
@ -70,6 +73,7 @@ export default function useStats(stats: FrigateStats | undefined) {
problems.push({ problems.push({
text: `${capitalizeFirstLetter(name.replaceAll("_", " "))} has high FFMPEG CPU usage (${ffmpegAvg}%)`, text: `${capitalizeFirstLetter(name.replaceAll("_", " "))} has high FFMPEG CPU usage (${ffmpegAvg}%)`,
color: "text-danger", color: "text-danger",
relevantLink: "/system#cameras",
}); });
} }
@ -77,6 +81,7 @@ export default function useStats(stats: FrigateStats | undefined) {
problems.push({ problems.push({
text: `${capitalizeFirstLetter(name.replaceAll("_", " "))} has high detect CPU usage (${detectAvg}%)`, text: `${capitalizeFirstLetter(name.replaceAll("_", " "))} has high detect CPU usage (${detectAvg}%)`,
color: "text-danger", color: "text-danger",
relevantLink: "/system#cameras",
}); });
} }
}); });

View File

@ -11,6 +11,8 @@ import { FaVideo } from "react-icons/fa";
import Logo from "@/components/Logo"; import Logo from "@/components/Logo";
import useOptimisticState from "@/hooks/use-optimistic-state"; import useOptimisticState from "@/hooks/use-optimistic-state";
import CameraMetrics from "@/views/system/CameraMetrics"; import CameraMetrics from "@/views/system/CameraMetrics";
import { useHashState } from "@/hooks/use-overlay-state";
import { capitalizeFirstLetter } from "@/utils/stringUtil";
const metrics = ["general", "storage", "cameras"] as const; const metrics = ["general", "storage", "cameras"] as const;
type SystemMetric = (typeof metrics)[number]; type SystemMetric = (typeof metrics)[number];
@ -18,12 +20,18 @@ type SystemMetric = (typeof metrics)[number];
function System() { function System() {
// stats page // stats page
const [page, setPage] = useState<SystemMetric>("general"); const [page, setPage] = useHashState<SystemMetric>();
const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100); const [pageToggle, setPageToggle] = useOptimisticState(
page ?? "general",
setPage,
100,
);
const [lastUpdated, setLastUpdated] = useState<number>(Date.now() / 1000); const [lastUpdated, setLastUpdated] = useState<number>(Date.now() / 1000);
useEffect(() => { useEffect(() => {
document.title = `${pageToggle[0].toUpperCase()}${pageToggle.substring(1)} Stats - Frigate`; if (pageToggle) {
document.title = `${capitalizeFirstLetter(pageToggle)} Stats - Frigate`;
}
}, [pageToggle]); }, [pageToggle]);
// stats collection // stats collection

View File

@ -62,6 +62,7 @@ export type StorageStats = {
export type PotentialProblem = { export type PotentialProblem = {
text: string; text: string;
color: string; color: string;
relevantLink?: string;
}; };
export type Vainfo = { export type Vainfo = {