Skip to content

Commit

Permalink
Add status bar provider (#11066)
Browse files Browse the repository at this point in the history
  • Loading branch information
hawkeye217 authored Apr 22, 2024
1 parent acadfb6 commit ba3930a
Show file tree
Hide file tree
Showing 11 changed files with 218 additions and 38 deletions.
11 changes: 9 additions & 2 deletions web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"idb-keyval": "^6.2.1",
"immer": "^10.0.4",
"konva": "^9.3.6",
"lodash": "^4.17.21",
"lucide-react": "^0.372.0",
"monaco-yaml": "^5.1.1",
"next-themes": "^0.3.0",
Expand Down Expand Up @@ -76,6 +77,7 @@
"devDependencies": {
"@tailwindcss/forms": "^0.5.7",
"@testing-library/jest-dom": "^6.1.5",
"@types/lodash": "^4.17.0",
"@types/node": "^20.12.7",
"@types/react": "^18.2.79",
"@types/react-dom": "^18.2.25",
Expand Down
44 changes: 35 additions & 9 deletions web/src/components/Statusbar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { useFrigateStats } from "@/api/ws";
import {
StatusBarMessagesContext,
StatusMessage,
} from "@/context/statusbar-provider";
import useStats from "@/hooks/use-stats";
import { FrigateStats } from "@/types/stats";
import { useMemo } from "react";
import { useContext, useEffect, useMemo } from "react";
import { FaCheck } from "react-icons/fa";
import { IoIosWarning } from "react-icons/io";
import { MdCircle } from "react-icons/md";
import useSWR from "swr";
Expand All @@ -11,6 +16,10 @@ export default function Statusbar() {
revalidateOnFocus: false,
});
const { payload: latestStats } = useFrigateStats();
const { messages, addMessage, clearMessages } = useContext(
StatusBarMessagesContext,
)!;

const stats = useMemo(() => {
if (latestStats) {
return latestStats;
Expand All @@ -31,6 +40,13 @@ export default function Statusbar() {

const { potentialProblems } = useStats(stats);

useEffect(() => {
clearMessages("stats");
potentialProblems.forEach((problem) => {
addMessage("stats", problem.text, problem.color);
});
}, [potentialProblems, addMessage, clearMessages]);

return (
<div className="absolute left-0 bottom-0 right-0 w-full h-8 flex justify-between items-center px-4 bg-background_alt z-10 dark:text-secondary-foreground border-t border-secondary-highlight">
<div className="h-full flex items-center gap-2">
Expand Down Expand Up @@ -86,15 +102,25 @@ export default function Statusbar() {
})}
</div>
<div className="h-full flex items-center gap-2">
{potentialProblems.map((prob) => (
<div
key={prob.text}
className="flex items-center text-sm gap-2 capitalize"
>
<IoIosWarning className={`size-5 ${prob.color}`} />
{prob.text}
{Object.entries(messages).length === 0 ? (
<div className="flex items-center text-sm gap-2">
<FaCheck className="size-3 text-green-500" />
System is healthy
</div>
))}
) : (
Object.entries(messages).map(([key, messageArray]) => (
<div key={key} className="h-full flex items-center gap-2">
{messageArray.map(({ id, text, color }: StatusMessage) => (
<div key={id} className="flex items-center text-sm gap-2">
<IoIosWarning
className={`size-5 ${color || "text-danger"}`}
/>
{text}
</div>
))}
</div>
))
)}
</div>
</div>
);
Expand Down
37 changes: 28 additions & 9 deletions web/src/components/navigation/Bottombar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
import useSWR from "swr";
import { FrigateStats } from "@/types/stats";
import { useFrigateStats } from "@/api/ws";
import { useMemo } from "react";
import { useContext, useEffect, useMemo } from "react";
import useStats from "@/hooks/use-stats";
import GeneralSettings from "../menu/GeneralSettings";
import AccountSettings from "../menu/AccountSettings";
import useNavigation from "@/hooks/use-navigation";
import {
StatusBarMessagesContext,
StatusMessage,
} from "@/context/statusbar-provider";

function Bottombar() {
const navItems = useNavigation("secondary");
Expand All @@ -30,6 +34,11 @@ function StatusAlertNav() {
revalidateOnFocus: false,
});
const { payload: latestStats } = useFrigateStats();

const { messages, addMessage, clearMessages } = useContext(
StatusBarMessagesContext,
)!;

const stats = useMemo(() => {
if (latestStats) {
return latestStats;
Expand All @@ -39,7 +48,14 @@ function StatusAlertNav() {
}, [initialStats, latestStats]);
const { potentialProblems } = useStats(stats);

if (!potentialProblems || potentialProblems.length == 0) {
useEffect(() => {
clearMessages("stats");
potentialProblems.forEach((problem) => {
addMessage("stats", problem.text, problem.color);
});
}, [potentialProblems, addMessage, clearMessages]);

if (!messages || Object.keys(messages).length === 0) {
return;
}

Expand All @@ -50,13 +66,16 @@ function StatusAlertNav() {
</DrawerTrigger>
<DrawerContent className="max-h-[75dvh] px-2 mx-1 rounded-t-2xl overflow-hidden">
<div className="w-full h-auto py-4 overflow-y-auto overflow-x-hidden flex flex-col items-center gap-2">
{potentialProblems.map((prob) => (
<div
key={prob.text}
className="w-full flex items-center text-xs gap-2 capitalize"
>
<IoIosWarning className={`size-5 ${prob.color}`} />
{prob.text}
{Object.entries(messages).map(([key, messageArray]) => (
<div key={key} className="w-full flex items-center gap-2">
{messageArray.map(({ id, text, color }: StatusMessage) => (
<div key={id} className="flex items-center text-xs gap-2">
<IoIosWarning
className={`size-5 ${color || "text-danger"}`}
/>
{text}
</div>
))}
</div>
))}
</div>
Expand Down
15 changes: 13 additions & 2 deletions web/src/components/settings/MasksAndZones.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { FrigateConfig } from "@/types/frigateConfig";
import useSWR from "swr";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { PolygonCanvas } from "./PolygonCanvas";
import { Polygon, PolygonType } from "@/types/canvas";
import { interpolatePoints, parseCoordinates } from "@/utils/canvasUtil";
Expand All @@ -25,6 +32,7 @@ import ObjectMaskEditPane from "./ObjectMaskEditPane";
import PolygonItem from "./PolygonItem";
import { Link } from "react-router-dom";
import { isDesktop } from "react-device-detect";
import { StatusBarMessagesContext } from "@/context/statusbar-provider";

type MasksAndZoneProps = {
selectedCamera: string;
Expand All @@ -50,6 +58,8 @@ export default function MasksAndZones({
const containerRef = useRef<HTMLDivElement | null>(null);
const [editPane, setEditPane] = useState<PolygonType | undefined>(undefined);

const { addMessage } = useContext(StatusBarMessagesContext)!;

const cameraConfig = useMemo(() => {
if (config && selectedCamera) {
return config.cameras[selectedCamera];
Expand Down Expand Up @@ -167,7 +177,8 @@ export default function MasksAndZones({
setAllPolygons([...(editingPolygons ?? [])]);
setHoveredPolygonIndex(null);
setUnsavedChanges(false);
}, [editingPolygons, setUnsavedChanges]);
addMessage("masks_zones", "Restart required (masks/zones changed)");
}, [editingPolygons, setUnsavedChanges, addMessage]);

useEffect(() => {
if (isLoading) {
Expand Down
16 changes: 14 additions & 2 deletions web/src/components/settings/MotionTuner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import useSWR from "swr";
import axios from "axios";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import AutoUpdatingCameraImage from "@/components/camera/AutoUpdatingCameraImage";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { Slider } from "@/components/ui/slider";
import { Label } from "@/components/ui/label";
import {
Expand All @@ -20,6 +20,7 @@ import { toast } from "sonner";
import { Separator } from "../ui/separator";
import { Link } from "react-router-dom";
import { LuExternalLink } from "react-icons/lu";
import { StatusBarMessagesContext } from "@/context/statusbar-provider";

type MotionTunerProps = {
selectedCamera: string;
Expand All @@ -41,6 +42,8 @@ export default function MotionTuner({
const [changedValue, setChangedValue] = useState(false);
const [isLoading, setIsLoading] = useState(false);

const { addMessage, clearMessages } = useContext(StatusBarMessagesContext)!;

const { send: sendMotionThreshold } = useMotionThreshold(selectedCamera);
const { send: sendMotionContourArea } = useMotionContourArea(selectedCamera);
const { send: sendImproveContrast } = useImproveContrast(selectedCamera);
Expand Down Expand Up @@ -145,7 +148,16 @@ export default function MotionTuner({
const onCancel = useCallback(() => {
setMotionSettings(origMotionSettings);
setChangedValue(false);
}, [origMotionSettings]);
clearMessages("motion_tuner");
}, [origMotionSettings, clearMessages]);

useEffect(() => {
if (changedValue) {
addMessage("motion_tuner", "Unsaved motion tuner changes");
} else {
clearMessages("motion_tuner");
}
}, [changedValue, addMessage, clearMessages]);

if (!cameraConfig && !selectedCamera) {
return <ActivityIndicator />;
Expand Down
3 changes: 2 additions & 1 deletion web/src/context/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { RecoilRoot } from "recoil";
import { ApiProvider } from "@/api";
import { IconContext } from "react-icons";
import { TooltipProvider } from "@/components/ui/tooltip";
import { StatusBarMessagesProvider } from "@/context/statusbar-provider";

type TProvidersProps = {
children: ReactNode;
Expand All @@ -16,7 +17,7 @@ function providers({ children }: TProvidersProps) {
<ThemeProvider defaultTheme="system" storageKey="frigate-ui-theme">
<TooltipProvider>
<IconContext.Provider value={{ size: "20" }}>
{children}
<StatusBarMessagesProvider>{children}</StatusBarMessagesProvider>
</IconContext.Provider>
</TooltipProvider>
</ThemeProvider>
Expand Down
83 changes: 83 additions & 0 deletions web/src/context/statusbar-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {
createContext,
useState,
ReactNode,
useCallback,
useMemo,
} from "react";

export type StatusMessage = {
id: string;
text: string;
color?: string;
};

export type StatusMessagesState = {
[key: string]: StatusMessage[];
};

type StatusBarMessagesProviderProps = {
children: ReactNode;
};

type StatusBarMessagesContextValue = {
messages: StatusMessagesState;
addMessage: (
key: string,
message: string,
color?: string,
messageId?: string,
) => string;
removeMessage: (key: string, messageId: string) => void;
clearMessages: (key: string) => void;
};

export const StatusBarMessagesContext =
createContext<StatusBarMessagesContextValue | null>(null);

export function StatusBarMessagesProvider({
children,
}: StatusBarMessagesProviderProps) {
const [messagesState, setMessagesState] = useState<StatusMessagesState>({});

const messages = useMemo(() => messagesState, [messagesState]);

const addMessage = useCallback(
(key: string, message: string, color?: string, messageId?: string) => {
const id = messageId || Date.now().toString();
const msgColor = color || "text-danger";
setMessagesState((prevMessages) => ({
...prevMessages,
[key]: [
...(prevMessages[key] || []),
{ id, text: message, color: msgColor },
],
}));
return id;
},
[],
);

const removeMessage = useCallback((key: string, messageId: string) => {
setMessagesState((prevMessages) => ({
...prevMessages,
[key]: prevMessages[key].filter((msg) => msg.id !== messageId),
}));
}, []);

const clearMessages = useCallback((key: string) => {
setMessagesState((prevMessages) => {
const updatedMessages = { ...prevMessages };
delete updatedMessages[key];
return updatedMessages;
});
}, []);

return (
<StatusBarMessagesContext.Provider
value={{ messages, addMessage, removeMessage, clearMessages }}
>
{children}
</StatusBarMessagesContext.Provider>
);
}
12 changes: 12 additions & 0 deletions web/src/hooks/use-deep-memo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useRef } from "react";
import { isEqual } from "lodash";

export default function useDeepMemo<T>(value: T) {
const ref = useRef<T | undefined>(undefined);

if (!isEqual(ref.current, value)) {
ref.current = value;
}

return ref.current;
}
Loading

0 comments on commit ba3930a

Please sign in to comment.