mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-13 18:22:49 +03:00
fixes
This commit is contained in:
@@ -2,7 +2,9 @@
|
|||||||
* Shared setup-wizard e2e helpers.
|
* Shared setup-wizard e2e helpers.
|
||||||
*
|
*
|
||||||
* The wizard shows when config has no cameras, so a first run is mocked by
|
* The wizard shows when config has no cameras, so a first run is mocked by
|
||||||
* serving a camera-less config until the returned callback is fired.
|
* serving a camera-less config until the returned callback is fired. Firing
|
||||||
|
* it is only needed by tests that care what the rest of the app sees; the
|
||||||
|
* wizard itself tracks added cameras from the camera dialog's own callback.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Page } from "@playwright/test";
|
import type { Page } from "@playwright/test";
|
||||||
@@ -32,7 +34,7 @@ export async function installFirstRun(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function gotoDetectorStep(page: Page, addCamera: () => void) {
|
export async function gotoDetectorStep(page: Page) {
|
||||||
await page.getByRole("button", { name: "Get Started" }).click();
|
await page.getByRole("button", { name: "Get Started" }).click();
|
||||||
|
|
||||||
// the account step sits between welcome and camera whenever auth is on,
|
// the account step sits between welcome and camera whenever auth is on,
|
||||||
@@ -43,17 +45,7 @@ export async function gotoDetectorStep(page: Page, addCamera: () => void) {
|
|||||||
await page.getByRole("button", { name: "Skip" }).click();
|
await page.getByRole("button", { name: "Skip" }).click();
|
||||||
|
|
||||||
await expect(page.getByText("Add Your First Camera")).toBeVisible();
|
await expect(page.getByText("Add Your First Camera")).toBeVisible();
|
||||||
|
await page.getByRole("button", { name: "Skip" }).click();
|
||||||
|
|
||||||
// the camera step's Next only renders once its local addedCameras fills,
|
|
||||||
// which SetupCamera does by refetching config when the dialog closes, so
|
|
||||||
// opening and cancelling the dialog is what reveals Next
|
|
||||||
await page.getByRole("button", { name: "Add Camera" }).click();
|
|
||||||
addCamera();
|
|
||||||
await page.getByRole("button", { name: "Cancel" }).click();
|
|
||||||
|
|
||||||
await expect(page.getByRole("button", { name: "Next" })).toBeVisible({
|
|
||||||
timeout: 10_000,
|
|
||||||
});
|
|
||||||
await page.getByRole("button", { name: "Next" }).click();
|
|
||||||
await expect(page.getByText("Object Detection")).toBeVisible();
|
await expect(page.getByText("Object Detection")).toBeVisible();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ test.describe("setup wizard account @high @mobile", () => {
|
|||||||
frigateApp,
|
frigateApp,
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const addCamera = await installFirstRun(frigateApp, page);
|
await installFirstRun(frigateApp, page);
|
||||||
await captureUserCalls(page);
|
await captureUserCalls(page);
|
||||||
|
|
||||||
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
|
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
|
||||||
@@ -173,16 +173,8 @@ test.describe("setup wizard account @high @mobile", () => {
|
|||||||
await expect(page.getByText("Password set")).toBeVisible();
|
await expect(page.getByText("Password set")).toBeVisible();
|
||||||
await page.getByRole("button", { name: "Next" }).click();
|
await page.getByRole("button", { name: "Next" }).click();
|
||||||
|
|
||||||
// the camera step has no Skip, so it is advanced the way the detector
|
|
||||||
// helper does, by opening and cancelling the add dialog
|
|
||||||
await expect(page.getByText("Add Your First Camera")).toBeVisible();
|
await expect(page.getByText("Add Your First Camera")).toBeVisible();
|
||||||
await page.getByRole("button", { name: "Add Camera" }).click();
|
await page.getByRole("button", { name: "Skip" }).click();
|
||||||
addCamera();
|
|
||||||
await page.getByRole("button", { name: "Cancel" }).click();
|
|
||||||
await expect(page.getByRole("button", { name: "Next" })).toBeVisible({
|
|
||||||
timeout: 10_000,
|
|
||||||
});
|
|
||||||
await page.getByRole("button", { name: "Next" }).click();
|
|
||||||
|
|
||||||
// every remaining step is passed without writing config: Skip on the
|
// every remaining step is passed without writing config: Skip on the
|
||||||
// detector, then Auto on hwaccel, which has nothing to derive and so
|
// detector, then Auto on hwaccel, which has nothing to derive and so
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ test.describe("setup wizard hardware @high @mobile", () => {
|
|||||||
frigateApp,
|
frigateApp,
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const addCamera = await installFirstRun(frigateApp, page, {
|
await installFirstRun(frigateApp, page, {
|
||||||
hwaccel: {
|
hwaccel: {
|
||||||
recommended: "vaapi",
|
recommended: "vaapi",
|
||||||
available: [
|
available: [
|
||||||
@@ -79,7 +79,7 @@ test.describe("setup wizard hardware @high @mobile", () => {
|
|||||||
const saves = await captureSaves(page);
|
const saves = await captureSaves(page);
|
||||||
|
|
||||||
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
|
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
|
||||||
await gotoDetectorStep(page, addCamera);
|
await gotoDetectorStep(page);
|
||||||
|
|
||||||
// the default hardware mock reports two Corals, an Intel GPU, and the CPU
|
// the default hardware mock reports two Corals, an Intel GPU, and the CPU
|
||||||
await expect(
|
await expect(
|
||||||
@@ -124,13 +124,13 @@ test.describe("setup wizard hardware @high @mobile", () => {
|
|||||||
frigateApp,
|
frigateApp,
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const addCamera = await installFirstRun(frigateApp, page, {
|
await installFirstRun(frigateApp, page, {
|
||||||
hardware: NVIDIA_HARDWARE,
|
hardware: NVIDIA_HARDWARE,
|
||||||
});
|
});
|
||||||
const saves = await captureSaves(page);
|
const saves = await captureSaves(page);
|
||||||
|
|
||||||
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
|
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
|
||||||
await gotoDetectorStep(page, addCamera);
|
await gotoDetectorStep(page);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole("radio", { name: /NVIDIA GeForce RTX 3060/ }),
|
page.getByRole("radio", { name: /NVIDIA GeForce RTX 3060/ }),
|
||||||
@@ -165,7 +165,7 @@ test.describe("setup wizard hardware @high @mobile", () => {
|
|||||||
frigateApp,
|
frigateApp,
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const addCamera = await installFirstRun(frigateApp, page, {
|
await installFirstRun(frigateApp, page, {
|
||||||
hardware: NVIDIA_HARDWARE,
|
hardware: NVIDIA_HARDWARE,
|
||||||
hwaccel: {
|
hwaccel: {
|
||||||
recommended: "nvidia",
|
recommended: "nvidia",
|
||||||
@@ -175,7 +175,7 @@ test.describe("setup wizard hardware @high @mobile", () => {
|
|||||||
await captureSaves(page);
|
await captureSaves(page);
|
||||||
|
|
||||||
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
|
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
|
||||||
await gotoDetectorStep(page, addCamera);
|
await gotoDetectorStep(page);
|
||||||
await page
|
await page
|
||||||
.getByRole("button", { name: "Continue without detection" })
|
.getByRole("button", { name: "Continue without detection" })
|
||||||
.click();
|
.click();
|
||||||
@@ -201,7 +201,7 @@ test.describe("setup wizard hardware @high @mobile", () => {
|
|||||||
frigateApp,
|
frigateApp,
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const addCamera = await installFirstRun(frigateApp, page, {
|
await installFirstRun(frigateApp, page, {
|
||||||
hwaccel: {
|
hwaccel: {
|
||||||
recommended: "jetson",
|
recommended: "jetson",
|
||||||
available: [
|
available: [
|
||||||
@@ -218,7 +218,7 @@ test.describe("setup wizard hardware @high @mobile", () => {
|
|||||||
const saves = await captureSaves(page);
|
const saves = await captureSaves(page);
|
||||||
|
|
||||||
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
|
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
|
||||||
await gotoDetectorStep(page, addCamera);
|
await gotoDetectorStep(page);
|
||||||
await page.getByRole("button", { name: "Next" }).click();
|
await page.getByRole("button", { name: "Next" }).click();
|
||||||
await expect(page.getByText("Hardware Acceleration")).toBeVisible();
|
await expect(page.getByText("Hardware Acceleration")).toBeVisible();
|
||||||
|
|
||||||
@@ -238,11 +238,11 @@ test.describe("setup wizard hardware @high @mobile", () => {
|
|||||||
frigateApp,
|
frigateApp,
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const addCamera = await installFirstRun(frigateApp, page);
|
await installFirstRun(frigateApp, page);
|
||||||
const saves = await captureSaves(page);
|
const saves = await captureSaves(page);
|
||||||
|
|
||||||
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
|
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
|
||||||
await gotoDetectorStep(page, addCamera);
|
await gotoDetectorStep(page);
|
||||||
await page.getByRole("button", { name: "Next" }).click();
|
await page.getByRole("button", { name: "Next" }).click();
|
||||||
await expect(page.getByText("Hardware Acceleration")).toBeVisible();
|
await expect(page.getByText("Hardware Acceleration")).toBeVisible();
|
||||||
|
|
||||||
|
|||||||
+12
-4
@@ -6,7 +6,7 @@ import Sidebar from "@/components/navigation/Sidebar";
|
|||||||
import { isDesktop, isMobile } from "react-device-detect";
|
import { isDesktop, isMobile } from "react-device-detect";
|
||||||
import Statusbar from "./components/Statusbar";
|
import Statusbar from "./components/Statusbar";
|
||||||
import Bottombar from "./components/navigation/Bottombar";
|
import Bottombar from "./components/navigation/Bottombar";
|
||||||
import { Suspense, lazy, useEffect, useState } from "react";
|
import { Suspense, lazy, useContext, useEffect, useState } from "react";
|
||||||
import { Redirect } from "./components/navigation/Redirect";
|
import { Redirect } from "./components/navigation/Redirect";
|
||||||
import { cn } from "./lib/utils";
|
import { cn } from "./lib/utils";
|
||||||
import { isPWA } from "./utils/isPWA";
|
import { isPWA } from "./utils/isPWA";
|
||||||
@@ -15,6 +15,8 @@ import useSWR from "swr";
|
|||||||
import { FrigateConfig } from "./types/frigateConfig";
|
import { FrigateConfig } from "./types/frigateConfig";
|
||||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
import { isRedirectingToLogin } from "@/api/auth-redirect";
|
import { isRedirectingToLogin } from "@/api/auth-redirect";
|
||||||
|
import { AuthContext } from "@/context/auth-context";
|
||||||
|
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||||
import { isSetupDismissed } from "@/utils/setupWizard";
|
import { isSetupDismissed } from "@/utils/setupWizard";
|
||||||
|
|
||||||
const Live = lazy(() => import("@/pages/Live"));
|
const Live = lazy(() => import("@/pages/Live"));
|
||||||
@@ -57,14 +59,20 @@ function DefaultAppView() {
|
|||||||
// decided once per load: adding the first camera part way through the
|
// decided once per load: adding the first camera part way through the
|
||||||
// wizard must not pull the wizard out from under the user
|
// wizard must not pull the wizard out from under the user
|
||||||
const [showWizard, setShowWizard] = useState<boolean>();
|
const [showWizard, setShowWizard] = useState<boolean>();
|
||||||
|
const { auth } = useContext(AuthContext);
|
||||||
|
const isAdmin = useIsAdmin();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (config && showWizard === undefined) {
|
// every step writes through admin only endpoints, and the role isn't
|
||||||
|
// known until the profile resolves
|
||||||
|
if (config && !auth.isLoading && showWizard === undefined) {
|
||||||
setShowWizard(
|
setShowWizard(
|
||||||
Object.keys(config.cameras ?? {}).length === 0 && !isSetupDismissed(),
|
isAdmin &&
|
||||||
|
Object.keys(config.cameras ?? {}).length === 0 &&
|
||||||
|
!isSetupDismissed(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [config, showWizard]);
|
}, [config, auth.isLoading, isAdmin, showWizard]);
|
||||||
|
|
||||||
// Compute required roles for main routes, ensuring we have config first
|
// Compute required roles for main routes, ensuring we have config first
|
||||||
// to prevent race condition where custom roles are temporarily unavailable
|
// to prevent race condition where custom roles are temporarily unavailable
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import CameraWizardDialog from "@/components/settings/CameraWizardDialog";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import useSWR from "swr";
|
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
|
||||||
import { FaCircleCheck } from "react-icons/fa6";
|
import { FaCircleCheck } from "react-icons/fa6";
|
||||||
|
|
||||||
type SetupCameraProps = {
|
type SetupCameraProps = {
|
||||||
@@ -19,30 +17,18 @@ export default function SetupCamera({ onNext, onBack }: SetupCameraProps) {
|
|||||||
const [showWizard, setShowWizard] = useState(false);
|
const [showWizard, setShowWizard] = useState(false);
|
||||||
const [addedCameras, setAddedCameras] = useState<string[]>([]);
|
const [addedCameras, setAddedCameras] = useState<string[]>([]);
|
||||||
const [detectCodecs, setDetectCodecs] = useState<Record<string, string>>({});
|
const [detectCodecs, setDetectCodecs] = useState<Record<string, string>>({});
|
||||||
const { mutate: mutateConfig } = useSWR<FrigateConfig>("config", {
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
setShowWizard(false);
|
setShowWizard(false);
|
||||||
// the dialog's config save and go2rtc setup need a moment to land, and
|
}, []);
|
||||||
// the SWR cache would serve the pre-save config
|
|
||||||
setTimeout(() => {
|
|
||||||
fetch(`${window.baseUrl || ""}api/config`)
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((freshConfig: FrigateConfig) => {
|
|
||||||
const cameraNames = Object.keys(freshConfig.cameras || {});
|
|
||||||
if (cameraNames.length > addedCameras.length) {
|
|
||||||
mutateConfig(freshConfig, { revalidate: false });
|
|
||||||
setAddedCameras(cameraNames);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}, 1000);
|
|
||||||
}, [mutateConfig, addedCameras]);
|
|
||||||
|
|
||||||
|
// the dialog fires this once its config write has succeeded, which is the
|
||||||
|
// only reliable signal that a camera was added
|
||||||
const handleCameraAdded = useCallback(
|
const handleCameraAdded = useCallback(
|
||||||
({ name, detectCodec }: { name: string; detectCodec?: string }) => {
|
({ name, detectCodec }: { name: string; detectCodec?: string }) => {
|
||||||
|
setAddedCameras((previous) =>
|
||||||
|
previous.includes(name) ? previous : [...previous, name],
|
||||||
|
);
|
||||||
|
|
||||||
if (detectCodec) {
|
if (detectCodec) {
|
||||||
setDetectCodecs((previous) => ({ ...previous, [name]: detectCodec }));
|
setDetectCodecs((previous) => ({ ...previous, [name]: detectCodec }));
|
||||||
}
|
}
|
||||||
@@ -51,9 +37,13 @@ export default function SetupCamera({ onNext, onBack }: SetupCameraProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleNext = useCallback(() => {
|
const handleNext = useCallback(() => {
|
||||||
onNext(addedCameras.length > 0 ? addedCameras : undefined, detectCodecs);
|
onNext(addedCameras, detectCodecs);
|
||||||
}, [onNext, addedCameras, detectCodecs]);
|
}, [onNext, addedCameras, detectCodecs]);
|
||||||
|
|
||||||
|
const handleSkip = useCallback(() => {
|
||||||
|
onNext();
|
||||||
|
}, [onNext]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-4 py-4">
|
<div className="flex flex-col gap-4 py-4">
|
||||||
@@ -97,10 +87,14 @@ export default function SetupCamera({ onNext, onBack }: SetupCameraProps) {
|
|||||||
{t("setupWizard.actions.back")}
|
{t("setupWizard.actions.back")}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex flex-1 justify-end gap-3">
|
<div className="flex flex-1 justify-end gap-3">
|
||||||
{addedCameras.length > 0 && (
|
{addedCameras.length > 0 ? (
|
||||||
<Button type="button" variant="select" onClick={handleNext}>
|
<Button type="button" variant="select" onClick={handleNext}>
|
||||||
{t("setupWizard.actions.next")}
|
{t("setupWizard.actions.next")}
|
||||||
</Button>
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button type="button" variant="outline" onClick={handleSkip}>
|
||||||
|
{t("setupWizard.actions.skip")}
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user