From c54ada65dc3211c2c75c5df172f2897b40a996a6 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 10 Nov 2025 12:33:07 -0600 Subject: [PATCH] consolidate probe dialog --- web/public/locales/en/views/settings.json | 2 + .../settings/wizard/OnvifProbeResults.tsx | 426 +++++++++--------- .../settings/wizard/ProbeDialog.tsx | 126 ------ .../settings/wizard/Step2ProbeOrSnapshot.tsx | 233 +++++++--- 4 files changed, 386 insertions(+), 401 deletions(-) delete mode 100644 web/src/components/settings/wizard/ProbeDialog.tsx diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 7714a00e7..37d2e95b1 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -217,6 +217,7 @@ "presets": "Presets", "deviceInfo": "Device Information", "rtspCandidates": "RTSP Candidates", + "noRtspCandidates": "No RTSP URLs were found from the camera. Your credentials may be incorrect, or the camera may not support ONVIF or the method used to retrieve RTSP URLs. Go back and enter the RTSP URL manually.", "candidateStreamTitle": "Candidate {{number}}", "useCandidate": "Use", "uriCopy": "Copy", @@ -265,6 +266,7 @@ "autotrackingSupport": "Autotracking Support", "presets": "Presets", "rtspCandidates": "RTSP Candidates", + "rtspCandidatesDescription": "The following RTSP URLs were found from the camera probe. Select one or more to use for this camera in Frigate.", "candidateStreamTitle": "Candidate {{number}}", "useCandidate": "Use", "uriCopy": "Copy", diff --git a/web/src/components/settings/wizard/OnvifProbeResults.tsx b/web/src/components/settings/wizard/OnvifProbeResults.tsx index 63bbd0d96..014dfde29 100644 --- a/web/src/components/settings/wizard/OnvifProbeResults.tsx +++ b/web/src/components/settings/wizard/OnvifProbeResults.tsx @@ -1,7 +1,6 @@ import { useTranslation } from "react-i18next"; -import { Card, CardContent, CardTitle } from "@/components/ui/card"; +import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -// Input removed: URL shown as text import ActivityIndicator from "@/components/indicators/activity-indicator"; import { FaCopy, FaCheck } from "react-icons/fa"; import { LuX } from "react-icons/lu"; @@ -96,137 +95,155 @@ export default function OnvifProbeResults({ ); } - const rtspCandidates = probeResult.rtsp_candidates || []; + const rtspCandidates = (probeResult.rtsp_candidates || []).filter( + (c) => c.source === "GetStreamUri", + ); + + if (probeResult?.success && rtspCandidates.length === 0) { + return ( +
+ + + {t("cameraWizard.step2.noRtspCandidates")} + +
+ ); + } return ( -
- {/* Probe success header (green check + text) */} - {probeResult?.success && ( -
- - {t("cameraWizard.step2.probeSuccessful")} -
- )} - - - {t("cameraWizard.step2.deviceInfo")} - - - {probeResult.manufacturer && ( -
- - {t("cameraWizard.step2.manufacturer")}: - {" "} - - {probeResult.manufacturer} - -
- )} - {probeResult.model && ( -
- - {t("cameraWizard.step2.model")}: - {" "} - {probeResult.model} -
- )} - {probeResult.firmware_version && ( -
- - {t("cameraWizard.step2.firmware")}: - {" "} - - {probeResult.firmware_version} - -
- )} - {probeResult.profiles_count !== undefined && ( -
- - {t("cameraWizard.step2.profiles")}: - {" "} - - {probeResult.profiles_count} - -
- )} - {probeResult.ptz_supported !== undefined && ( -
- - {t("cameraWizard.step2.ptzSupport")}: - {" "} - - {probeResult.ptz_supported - ? t("yes", { ns: "common" }) - : t("no", { ns: "common" })} - -
- )} - {probeResult.ptz_supported && probeResult.autotrack_supported && ( -
- - {t("cameraWizard.step2.autotrackingSupport")}: - {" "} - - {t("yes", { ns: "common" })} - -
- )} - {probeResult.ptz_supported && - probeResult.presets_count !== undefined && ( + <> +
+ {probeResult?.success && ( +
+ + {t("cameraWizard.step2.probeSuccessful")} +
+ )} +
{t("cameraWizard.step2.deviceInfo")}
+ + + {probeResult.manufacturer && (
- {t("cameraWizard.step2.presets")}: + {t("cameraWizard.step2.manufacturer")}: {" "} - {probeResult.presets_count} + {probeResult.manufacturer}
)} -
-
+ {probeResult.model && ( +
+ + {t("cameraWizard.step2.model")}: + {" "} + + {probeResult.model} + +
+ )} + {probeResult.firmware_version && ( +
+ + {t("cameraWizard.step2.firmware")}: + {" "} + + {probeResult.firmware_version} + +
+ )} + {probeResult.profiles_count !== undefined && ( +
+ + {t("cameraWizard.step2.profiles")}: + {" "} + + {probeResult.profiles_count} + +
+ )} + {probeResult.ptz_supported !== undefined && ( +
+ + {t("cameraWizard.step2.ptzSupport")}: + {" "} + + {probeResult.ptz_supported + ? t("yes", { ns: "common" }) + : t("no", { ns: "common" })} + +
+ )} + {probeResult.ptz_supported && probeResult.autotrack_supported && ( +
+ + {t("cameraWizard.step2.autotrackingSupport")}: + {" "} + + {t("yes", { ns: "common" })} + +
+ )} + {probeResult.ptz_supported && + probeResult.presets_count !== undefined && ( +
+ + {t("cameraWizard.step2.presets")}: + {" "} + + {probeResult.presets_count} + +
+ )} + + +
+
+ {rtspCandidates.length > 0 && ( +
+
+ {t("cameraWizard.step2.rtspCandidates")} +
+
+ {t("cameraWizard.step2.rtspCandidatesDescription")} +
- {rtspCandidates.length > 0 && ( -
-

{t("cameraWizard.step2.rtspCandidates")}

+
+ {rtspCandidates.map((candidate, idx) => { + const isSelected = !!selectedUris?.includes(candidate.uri); + const candidateTest = candidateTests?.[candidate.uri]; + const isTesting = testingCandidates?.[candidate.uri]; -
- {rtspCandidates.map((candidate, idx) => { - const isSelected = !!selectedUris?.includes(candidate.uri); - const candidateTest = candidateTests?.[candidate.uri]; - const isTesting = testingCandidates?.[candidate.uri]; - - return ( - handleCopyUri(candidate.uri)} - onUse={() => onSelectCandidate(candidate.uri)} - isSelected={isSelected} - testCandidate={testCandidate} - candidateTest={candidateTest} - isTesting={isTesting} - /> - ); - })} + return ( + handleCopyUri(candidate.uri)} + onUse={() => onSelectCandidate(candidate.uri)} + isSelected={isSelected} + testCandidate={testCandidate} + candidateTest={candidateTest} + isTesting={isTesting} + /> + ); + })} +
-
- )} -
+ )} +
+ ); } type CandidateItemProps = { candidate: OnvifRtspCandidate; index?: number; - // isTested?: boolean; (unused) copiedUri: string | null; onCopy: () => void; onUse: () => void; isSelected?: boolean; - // onTest?: () => void; (unused) testCandidate?: (uri: string) => void; candidateTest?: TestResult | { success: false; error: string }; isTesting?: boolean; @@ -253,110 +270,115 @@ function CandidateItem({ }; return ( -
-
-
-
-

- {t("cameraWizard.step2.candidateStreamTitle", { - number: (index ?? 0) + 1, - })} -

- {candidateTest?.success && ( -
- {[ - candidateTest.resolution, - candidateTest.fps - ? `${candidateTest.fps} ${t( - "cameraWizard.testResultLabels.fps", - )}` - : null, - candidateTest.videoCodec, - candidateTest.audioCodec, - ] - .filter(Boolean) - .join(" · ")} -
- )} -
- -
- {candidateTest?.success && ( -
- - - {t("cameraWizard.step2.connected")} - -
- )} - - {candidateTest && !candidateTest.success && ( -
- - - {t("cameraWizard.step2.notConnected")} - -
- )} -
-
- -
-

setShowFull((s) => !s)} - title={t("cameraWizard.step2.toggleUriView")} - > - {showFull ? candidate.uri : maskUri(candidate.uri)} -

- -
- +
+
+ {candidateTest?.success && ( +
+ + + {t("cameraWizard.step2.connected")} + +
+ )} + + {candidateTest && !candidateTest.success && ( +
+ + + {t("cameraWizard.step2.notConnected")} + +
+ )} +
+
+ +
+

setShowFull((s) => !s)} + title={t("cameraWizard.step2.toggleUriView")} + > + {showFull ? candidate.uri : maskUri(candidate.uri)} +

+ +
+ + + +
+
+ +
- -
- -
-
-
+ + ); } diff --git a/web/src/components/settings/wizard/ProbeDialog.tsx b/web/src/components/settings/wizard/ProbeDialog.tsx deleted file mode 100644 index c340426d0..000000000 --- a/web/src/components/settings/wizard/ProbeDialog.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, -} from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import OnvifProbeResults from "./OnvifProbeResults"; -import {} from "@/components/ui/card"; -import ActivityIndicator from "@/components/indicators/activity-indicator"; -import type { - OnvifProbeResponse, - CandidateTestMap, -} from "@/types/cameraWizard"; -import StepIndicator from "@/components/indicators/StepIndicator"; - -type ProbeDialogProps = { - open: boolean; - onOpenChange: (open: boolean) => void; - isLoading: boolean; - isError: boolean; - error?: string; - probeResult?: OnvifProbeResponse | null; - onSelectCandidate: (uri: string) => void; - onRetry: () => void; - selectedCandidateUris?: string[]; - testAllSelectedCandidates?: () => void; - isTesting?: boolean; - testStatus?: string; - testCandidate?: (uri: string) => void; - candidateTests?: CandidateTestMap; - testingCandidates?: Record; -}; - -export default function ProbeDialog({ - open, - onOpenChange, - isLoading, - isError, - error, - probeResult, - onSelectCandidate, - onRetry, - selectedCandidateUris, - testAllSelectedCandidates, - isTesting, - testStatus, - testCandidate, - candidateTests, - testingCandidates, -}: ProbeDialogProps) { - const { t } = useTranslation(["views/settings"]); - - const STEPS = [ - "cameraWizard.steps.nameAndConnection", - "cameraWizard.steps.streamConfiguration", - "cameraWizard.steps.validationAndTesting", - ]; - - return ( - - - - - {t("cameraWizard.title")} - {t("cameraWizard.description")} - - -
- -
- -
- {isTesting && testStatus && ( -
- - {testStatus} -
- )} -
- - - -
-
-
-
- ); -} diff --git a/web/src/components/settings/wizard/Step2ProbeOrSnapshot.tsx b/web/src/components/settings/wizard/Step2ProbeOrSnapshot.tsx index 8e3234f9e..b16ddca28 100644 --- a/web/src/components/settings/wizard/Step2ProbeOrSnapshot.tsx +++ b/web/src/components/settings/wizard/Step2ProbeOrSnapshot.tsx @@ -17,7 +17,7 @@ import type { } from "@/types/cameraWizard"; import { FaCircleCheck } from "react-icons/fa6"; import { Card, CardContent, CardTitle } from "../../ui/card"; -import ProbeDialog from "./ProbeDialog"; +import OnvifProbeResults from "./OnvifProbeResults"; import { CAMERA_BRANDS } from "@/types/cameraWizard"; import { detectReolinkCamera } from "@/utils/cameraUtil"; @@ -45,16 +45,15 @@ export default function Step2ProbeOrSnapshot({ const [probeResult, setProbeResult] = useState( null, ); - const [probeDialogOpen, setProbeDialogOpen] = useState(false); + const [testingCandidates, setTestingCandidates] = useState< + Record + >({} as Record); const [selectedCandidateUris, setSelectedCandidateUris] = useState( [], ); const [candidateTests, setCandidateTests] = useState( {} as CandidateTestMap, ); - const [testingCandidates, setTestingCandidates] = useState< - Record - >({} as Record); const handleSelectCandidate = useCallback((uri: string) => { setSelectedCandidateUris((s) => { @@ -205,7 +204,6 @@ export default function Step2ProbeOrSnapshot({ if (response.data && response.data.success) { setProbeResult(response.data); - setProbeDialogOpen(true); } else { setProbeError(response.data?.message || "Probe failed"); } @@ -266,7 +264,6 @@ export default function Step2ProbeOrSnapshot({ if (streamConfigs.length > 0) { onNext({ streams: streamConfigs }); toast.success(t("cameraWizard.step2.testSuccess")); - setProbeDialogOpen(false); } else { toast.error( t("cameraWizard.commonErrors.testFailed", { @@ -459,29 +456,18 @@ export default function Step2ProbeOrSnapshot({ return (
{probeMode ? ( - // Probe mode: show probe dialog + // Probe mode: show probe results directly <> {probeResult && ( -
- { - setProbeDialogOpen(open); - // If dialog is being closed (open=false), go back - if (!open) { - onBack(); - } - }} +
+ )} - {isProbing && !probeResult && ( -
- - {t("cameraWizard.step2.probing")} -
- )} - - {probeError && !probeResult && ( -
-
{probeError}
-
- - -
-
- )} + ) : ( // Manual mode: show snapshot and stream details @@ -566,32 +536,19 @@ export default function Step2ProbeOrSnapshot({
)} -
- - {testResult?.success ? ( - - ) : ( - - )} -
+ )}
@@ -638,3 +595,133 @@ function StreamDetails({ testResult }: { testResult: TestResult }) { ); } + +type ProbeFooterProps = { + isProbing: boolean; + probeError: string | null; + onBack: () => void; + onTestAll: () => void; + onRetry: () => void; + isTesting: boolean; + selectedCount: number; + mode?: "probe" | "manual"; + manualTestSuccess?: boolean; + onContinue?: () => void; + onManualTest?: () => void; +}; + +function ProbeFooterButtons({ + isProbing, + probeError, + onBack, + onTestAll, + onRetry, + isTesting, + selectedCount, + mode = "probe", + manualTestSuccess, + onContinue, + onManualTest, +}: ProbeFooterProps) { + const { t } = useTranslation(["views/settings"]); + + // Loading footer + if (isProbing) { + return ( +
+
+ + {t("cameraWizard.step2.probing")} +
+
+ + +
+
+ ); + } + + // Error footer + if (probeError) { + return ( +
+
{probeError}
+
+ + +
+
+ ); + } + + // Default footer: show back + test (test disabled if none selected or testing) + // If manual mode, show Continue when test succeeded, otherwise show Test (calls onManualTest) + if (mode === "manual") { + return ( +
+ + {manualTestSuccess ? ( + + ) : ( + + )} +
+ ); + } + + // Default probe footer + return ( +
+ + +
+ ); +}