mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-29 11:26:49 +03:00
fixes
This commit is contained in:
@@ -2,7 +2,9 @@
|
||||
* Shared setup-wizard e2e helpers.
|
||||
*
|
||||
* 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";
|
||||
@@ -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();
|
||||
|
||||
// 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 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();
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ test.describe("setup wizard account @high @mobile", () => {
|
||||
frigateApp,
|
||||
page,
|
||||
}) => {
|
||||
const addCamera = await installFirstRun(frigateApp, page);
|
||||
await installFirstRun(frigateApp, page);
|
||||
await captureUserCalls(page);
|
||||
|
||||
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 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 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 page.getByRole("button", { name: "Skip" }).click();
|
||||
|
||||
// every remaining step is passed without writing config: Skip on the
|
||||
// detector, then Auto on hwaccel, which has nothing to derive and so
|
||||
|
||||
@@ -61,7 +61,7 @@ test.describe("setup wizard hardware @high @mobile", () => {
|
||||
frigateApp,
|
||||
page,
|
||||
}) => {
|
||||
const addCamera = await installFirstRun(frigateApp, page, {
|
||||
await installFirstRun(frigateApp, page, {
|
||||
hwaccel: {
|
||||
recommended: "vaapi",
|
||||
available: [
|
||||
@@ -79,7 +79,7 @@ test.describe("setup wizard hardware @high @mobile", () => {
|
||||
const saves = await captureSaves(page);
|
||||
|
||||
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
|
||||
await expect(
|
||||
@@ -124,13 +124,13 @@ test.describe("setup wizard hardware @high @mobile", () => {
|
||||
frigateApp,
|
||||
page,
|
||||
}) => {
|
||||
const addCamera = await installFirstRun(frigateApp, page, {
|
||||
await installFirstRun(frigateApp, page, {
|
||||
hardware: NVIDIA_HARDWARE,
|
||||
});
|
||||
const saves = await captureSaves(page);
|
||||
|
||||
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
|
||||
await gotoDetectorStep(page, addCamera);
|
||||
await gotoDetectorStep(page);
|
||||
|
||||
await expect(
|
||||
page.getByRole("radio", { name: /NVIDIA GeForce RTX 3060/ }),
|
||||
@@ -165,7 +165,7 @@ test.describe("setup wizard hardware @high @mobile", () => {
|
||||
frigateApp,
|
||||
page,
|
||||
}) => {
|
||||
const addCamera = await installFirstRun(frigateApp, page, {
|
||||
await installFirstRun(frigateApp, page, {
|
||||
hardware: NVIDIA_HARDWARE,
|
||||
hwaccel: {
|
||||
recommended: "nvidia",
|
||||
@@ -175,7 +175,7 @@ test.describe("setup wizard hardware @high @mobile", () => {
|
||||
await captureSaves(page);
|
||||
|
||||
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
|
||||
await gotoDetectorStep(page, addCamera);
|
||||
await gotoDetectorStep(page);
|
||||
await page
|
||||
.getByRole("button", { name: "Continue without detection" })
|
||||
.click();
|
||||
@@ -201,7 +201,7 @@ test.describe("setup wizard hardware @high @mobile", () => {
|
||||
frigateApp,
|
||||
page,
|
||||
}) => {
|
||||
const addCamera = await installFirstRun(frigateApp, page, {
|
||||
await installFirstRun(frigateApp, page, {
|
||||
hwaccel: {
|
||||
recommended: "jetson",
|
||||
available: [
|
||||
@@ -218,7 +218,7 @@ test.describe("setup wizard hardware @high @mobile", () => {
|
||||
const saves = await captureSaves(page);
|
||||
|
||||
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
|
||||
await gotoDetectorStep(page, addCamera);
|
||||
await gotoDetectorStep(page);
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await expect(page.getByText("Hardware Acceleration")).toBeVisible();
|
||||
|
||||
@@ -238,11 +238,11 @@ test.describe("setup wizard hardware @high @mobile", () => {
|
||||
frigateApp,
|
||||
page,
|
||||
}) => {
|
||||
const addCamera = await installFirstRun(frigateApp, page);
|
||||
await installFirstRun(frigateApp, page);
|
||||
const saves = await captureSaves(page);
|
||||
|
||||
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
|
||||
await gotoDetectorStep(page, addCamera);
|
||||
await gotoDetectorStep(page);
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
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 Statusbar from "./components/Statusbar";
|
||||
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 { cn } from "./lib/utils";
|
||||
import { isPWA } from "./utils/isPWA";
|
||||
@@ -15,6 +15,8 @@ import useSWR from "swr";
|
||||
import { FrigateConfig } from "./types/frigateConfig";
|
||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
import { isRedirectingToLogin } from "@/api/auth-redirect";
|
||||
import { AuthContext } from "@/context/auth-context";
|
||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||
import { isSetupDismissed } from "@/utils/setupWizard";
|
||||
|
||||
const Live = lazy(() => import("@/pages/Live"));
|
||||
@@ -57,14 +59,20 @@ function DefaultAppView() {
|
||||
// decided once per load: adding the first camera part way through the
|
||||
// wizard must not pull the wizard out from under the user
|
||||
const [showWizard, setShowWizard] = useState<boolean>();
|
||||
const { auth } = useContext(AuthContext);
|
||||
const isAdmin = useIsAdmin();
|
||||
|
||||
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(
|
||||
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
|
||||
// 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 { useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useSWR from "swr";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { FaCircleCheck } from "react-icons/fa6";
|
||||
|
||||
type SetupCameraProps = {
|
||||
@@ -19,30 +17,18 @@ export default function SetupCamera({ onNext, onBack }: SetupCameraProps) {
|
||||
const [showWizard, setShowWizard] = useState(false);
|
||||
const [addedCameras, setAddedCameras] = useState<string[]>([]);
|
||||
const [detectCodecs, setDetectCodecs] = useState<Record<string, string>>({});
|
||||
const { mutate: mutateConfig } = useSWR<FrigateConfig>("config", {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
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(
|
||||
({ name, detectCodec }: { name: string; detectCodec?: string }) => {
|
||||
setAddedCameras((previous) =>
|
||||
previous.includes(name) ? previous : [...previous, name],
|
||||
);
|
||||
|
||||
if (detectCodec) {
|
||||
setDetectCodecs((previous) => ({ ...previous, [name]: detectCodec }));
|
||||
}
|
||||
@@ -51,9 +37,13 @@ export default function SetupCamera({ onNext, onBack }: SetupCameraProps) {
|
||||
);
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
onNext(addedCameras.length > 0 ? addedCameras : undefined, detectCodecs);
|
||||
onNext(addedCameras, detectCodecs);
|
||||
}, [onNext, addedCameras, detectCodecs]);
|
||||
|
||||
const handleSkip = useCallback(() => {
|
||||
onNext();
|
||||
}, [onNext]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-4 py-4">
|
||||
@@ -97,10 +87,14 @@ export default function SetupCamera({ onNext, onBack }: SetupCameraProps) {
|
||||
{t("setupWizard.actions.back")}
|
||||
</Button>
|
||||
<div className="flex flex-1 justify-end gap-3">
|
||||
{addedCameras.length > 0 && (
|
||||
{addedCameras.length > 0 ? (
|
||||
<Button type="button" variant="select" onClick={handleNext}>
|
||||
{t("setupWizard.actions.next")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="button" variant="outline" onClick={handleSkip}>
|
||||
{t("setupWizard.actions.skip")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user