mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 21:44:13 +03:00
consolidate probe dialog
This commit is contained in:
parent
5c41107fcd
commit
c54ada65dc
@ -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",
|
||||
|
||||
@ -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,21 +95,32 @@ 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 (
|
||||
<div className="space-y-4">
|
||||
{/* Probe success header (green check + text) */}
|
||||
<Alert variant="destructive">
|
||||
<CiCircleAlert className="size-5" />
|
||||
<AlertTitle>{t("cameraWizard.step2.noRtspCandidates")}</AlertTitle>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
{probeResult?.success && (
|
||||
<div className="mb-3 flex flex-row items-center gap-2 text-sm text-success">
|
||||
<FaCircleCheck className="size-4" />
|
||||
<span>{t("cameraWizard.step2.probeSuccessful")}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm">{t("cameraWizard.step2.deviceInfo")}</div>
|
||||
<Card>
|
||||
<CardTitle className="border-b p-4 text-sm">
|
||||
{t("cameraWizard.step2.deviceInfo")}
|
||||
</CardTitle>
|
||||
<CardContent className="space-y-2 p-4 text-sm">
|
||||
{probeResult.manufacturer && (
|
||||
<div>
|
||||
@ -127,7 +137,9 @@ export default function OnvifProbeResults({
|
||||
<span className="text-muted-foreground">
|
||||
{t("cameraWizard.step2.model")}:
|
||||
</span>{" "}
|
||||
<span className="text-primary-variant">{probeResult.model}</span>
|
||||
<span className="text-primary-variant">
|
||||
{probeResult.model}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{probeResult.firmware_version && (
|
||||
@ -185,10 +197,16 @@ export default function OnvifProbeResults({
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{rtspCandidates.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm">{t("cameraWizard.step2.rtspCandidates")}</h3>
|
||||
<div className="mt-5 space-y-2">
|
||||
<div className="text-sm">
|
||||
{t("cameraWizard.step2.rtspCandidates")}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t("cameraWizard.step2.rtspCandidatesDescription")}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{rtspCandidates.map((candidate, idx) => {
|
||||
@ -215,18 +233,17 @@ export default function OnvifProbeResults({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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,14 +270,14 @@ function CandidateItem({
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
<Card
|
||||
className={cn(
|
||||
"rounded-lg bg-card",
|
||||
isSelected &&
|
||||
"outline outline-[3px] -outline-offset-[2.8px] outline-selected duration-200",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col space-y-4 p-4">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-medium">
|
||||
@ -334,11 +351,15 @@ function CandidateItem({
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={isTesting}
|
||||
onClick={() => testCandidate?.(candidate.uri)}
|
||||
className="h-8 px-3 text-sm"
|
||||
>
|
||||
{isTesting ? (
|
||||
<ActivityIndicator className="size-3" />
|
||||
<>
|
||||
<ActivityIndicator className="mr-2 size-4" />{" "}
|
||||
{t("cameraWizard.step2.testConnection")}
|
||||
</>
|
||||
) : (
|
||||
t("cameraWizard.step2.testConnection")
|
||||
)}
|
||||
@ -357,6 +378,7 @@ function CandidateItem({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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<string, boolean>;
|
||||
};
|
||||
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-h-[90%] max-w-3xl overflow-y-auto xl:max-h-[80%]">
|
||||
<StepIndicator
|
||||
steps={STEPS}
|
||||
currentStep={0}
|
||||
variant="dots"
|
||||
className="mb-4 justify-start"
|
||||
/>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("cameraWizard.title")}</DialogTitle>
|
||||
<DialogDescription>{t("cameraWizard.description")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="p-0">
|
||||
<OnvifProbeResults
|
||||
isLoading={isLoading}
|
||||
isError={isError}
|
||||
error={error}
|
||||
probeResult={probeResult || undefined}
|
||||
onSelectCandidate={onSelectCandidate}
|
||||
onRetry={onRetry}
|
||||
selectedUris={selectedCandidateUris}
|
||||
testCandidate={testCandidate}
|
||||
candidateTests={candidateTests}
|
||||
testingCandidates={testingCandidates}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
{isTesting && testStatus && (
|
||||
<div className="mb-3 flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<ActivityIndicator className="size-4" />
|
||||
<span>{testStatus}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-3 pt-3 sm:flex-row sm:justify-end sm:gap-4">
|
||||
<Button
|
||||
className="flex items-center justify-center gap-2 sm:flex-1"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
{t("button.back", { ns: "common" })}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="flex items-center justify-center gap-2 sm:flex-1"
|
||||
onClick={testAllSelectedCandidates}
|
||||
disabled={
|
||||
!(selectedCandidateUris && selectedCandidateUris.length > 0) ||
|
||||
isLoading ||
|
||||
isTesting
|
||||
}
|
||||
variant="select"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
{isTesting && <ActivityIndicator className="size-4" />}
|
||||
<span>{t("cameraWizard.step1.testConnection")}</span>
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@ -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<OnvifProbeResponse | null>(
|
||||
null,
|
||||
);
|
||||
const [probeDialogOpen, setProbeDialogOpen] = useState(false);
|
||||
const [testingCandidates, setTestingCandidates] = useState<
|
||||
Record<string, boolean>
|
||||
>({} as Record<string, boolean>);
|
||||
const [selectedCandidateUris, setSelectedCandidateUris] = useState<string[]>(
|
||||
[],
|
||||
);
|
||||
const [candidateTests, setCandidateTests] = useState<CandidateTestMap>(
|
||||
{} as CandidateTestMap,
|
||||
);
|
||||
const [testingCandidates, setTestingCandidates] = useState<
|
||||
Record<string, boolean>
|
||||
>({} as Record<string, boolean>);
|
||||
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
{probeMode ? (
|
||||
// Probe mode: show probe dialog
|
||||
// Probe mode: show probe results directly
|
||||
<>
|
||||
{probeResult && (
|
||||
<div className="p-4">
|
||||
<ProbeDialog
|
||||
open={probeDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setProbeDialogOpen(open);
|
||||
// If dialog is being closed (open=false), go back
|
||||
if (!open) {
|
||||
onBack();
|
||||
}
|
||||
}}
|
||||
<div className="space-y-4">
|
||||
<OnvifProbeResults
|
||||
isLoading={isProbing}
|
||||
isError={!!probeError}
|
||||
error={probeError || undefined}
|
||||
probeResult={probeResult}
|
||||
onSelectCandidate={handleSelectCandidate}
|
||||
onRetry={probeCamera}
|
||||
selectedCandidateUris={selectedCandidateUris}
|
||||
testAllSelectedCandidates={testAllSelectedCandidates}
|
||||
isTesting={isTesting}
|
||||
testStatus={testStatus}
|
||||
selectedUris={selectedCandidateUris}
|
||||
testCandidate={testCandidate}
|
||||
candidateTests={candidateTests}
|
||||
testingCandidates={testingCandidates}
|
||||
@ -489,31 +475,15 @@ export default function Step2ProbeOrSnapshot({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isProbing && !probeResult && (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<ActivityIndicator className="size-4" />
|
||||
{t("cameraWizard.step2.probing")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{probeError && !probeResult && (
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm text-destructive">{probeError}</div>
|
||||
<div className="flex flex-col gap-3 pt-3 sm:flex-row sm:justify-end sm:gap-4">
|
||||
<Button type="button" onClick={onBack} className="sm:flex-1">
|
||||
{t("button.back", { ns: "common" })}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={probeCamera}
|
||||
variant="select"
|
||||
className="flex items-center justify-center gap-2 sm:flex-1"
|
||||
>
|
||||
{t("cameraWizard.step2.retry")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ProbeFooterButtons
|
||||
isProbing={isProbing}
|
||||
probeError={probeError}
|
||||
onBack={onBack}
|
||||
onTestAll={testAllSelectedCandidates}
|
||||
onRetry={probeCamera}
|
||||
isTesting={isTesting}
|
||||
selectedCount={selectedCandidateUris.length}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
// Manual mode: show snapshot and stream details
|
||||
@ -566,32 +536,19 @@ export default function Step2ProbeOrSnapshot({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-3 pt-3 sm:flex-row sm:justify-end sm:gap-4">
|
||||
<Button type="button" onClick={onBack} className="sm:flex-1">
|
||||
{t("button.back", { ns: "common" })}
|
||||
</Button>
|
||||
{testResult?.success ? (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleContinue}
|
||||
variant="select"
|
||||
className="flex items-center justify-center gap-2 sm:flex-1"
|
||||
>
|
||||
{t("button.continue", { ns: "common" })}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={testConnection}
|
||||
disabled={isTesting}
|
||||
variant="select"
|
||||
className="flex items-center justify-center gap-2 sm:flex-1"
|
||||
>
|
||||
{isTesting && <ActivityIndicator className="size-4" />}
|
||||
{t("cameraWizard.step2.retry")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<ProbeFooterButtons
|
||||
mode="manual"
|
||||
isProbing={false}
|
||||
probeError={null}
|
||||
onBack={onBack}
|
||||
onTestAll={testAllSelectedCandidates}
|
||||
onRetry={probeCamera}
|
||||
isTesting={isTesting}
|
||||
selectedCount={selectedCandidateUris.length}
|
||||
manualTestSuccess={!!testResult?.success}
|
||||
onContinue={handleContinue}
|
||||
onManualTest={testConnection}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@ -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 (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<ActivityIndicator className="size-4" />
|
||||
{t("cameraWizard.step2.probing")}
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 pt-3 sm:flex-row sm:justify-end sm:gap-4">
|
||||
<Button type="button" onClick={onBack} disabled className="sm:flex-1">
|
||||
{t("button.back", { ns: "common" })}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
disabled
|
||||
variant="select"
|
||||
className="flex items-center justify-center gap-2 sm:flex-1"
|
||||
>
|
||||
<ActivityIndicator className="size-4" />
|
||||
{t("cameraWizard.step2.probing")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Error footer
|
||||
if (probeError) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm text-destructive">{probeError}</div>
|
||||
<div className="flex flex-col gap-3 pt-3 sm:flex-row sm:justify-end sm:gap-4">
|
||||
<Button type="button" onClick={onBack} className="sm:flex-1">
|
||||
{t("button.back", { ns: "common" })}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onRetry}
|
||||
variant="select"
|
||||
className="flex items-center justify-center gap-2 sm:flex-1"
|
||||
>
|
||||
{t("cameraWizard.step2.retry")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="flex flex-col gap-3 pt-3 sm:flex-row sm:justify-end sm:gap-4">
|
||||
<Button type="button" onClick={onBack} className="sm:flex-1">
|
||||
{t("button.back", { ns: "common" })}
|
||||
</Button>
|
||||
{manualTestSuccess ? (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onContinue}
|
||||
variant="select"
|
||||
className="flex items-center justify-center gap-2 sm:flex-1"
|
||||
>
|
||||
{t("button.continue", { ns: "common" })}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onManualTest}
|
||||
disabled={isTesting}
|
||||
variant="select"
|
||||
className="flex items-center justify-center gap-2 sm:flex-1"
|
||||
>
|
||||
{isTesting && <ActivityIndicator className="size-4" />}
|
||||
{t("cameraWizard.step2.retry")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Default probe footer
|
||||
return (
|
||||
<div className="flex flex-col gap-3 pt-3 sm:flex-row sm:justify-end sm:gap-4">
|
||||
<Button type="button" onClick={onBack} className="sm:flex-1">
|
||||
{t("button.back", { ns: "common" })}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onTestAll}
|
||||
disabled={isTesting || selectedCount === 0}
|
||||
variant="select"
|
||||
className="flex items-center justify-center gap-2 sm:flex-1"
|
||||
>
|
||||
{isTesting && <ActivityIndicator className="size-4" />}
|
||||
{t("cameraWizard.step2.testConnection")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user