mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-28 19:18:22 +03:00
combine review sections
This commit is contained in:
parent
f82a721477
commit
bbd62129ca
@ -1,9 +1,15 @@
|
|||||||
import type { ComponentType } from "react";
|
import type { ComponentType } from "react";
|
||||||
import SemanticSearchReindex from "./SemanticSearchReindex.tsx";
|
import SemanticSearchReindex from "./SemanticSearchReindex.tsx";
|
||||||
|
import CameraReviewSettingsView from "@/views/settings/CameraReviewSettingsView.tsx";
|
||||||
|
|
||||||
export type RendererComponent = ComponentType<
|
// Props that will be injected into all section renderers
|
||||||
Record<string, unknown> | undefined
|
export type SectionRendererProps = {
|
||||||
>;
|
selectedCamera?: string;
|
||||||
|
setUnsavedChanges?: (hasChanges: boolean) => void;
|
||||||
|
[key: string]: unknown; // Allow additional props from uiSchema
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RendererComponent = ComponentType<SectionRendererProps>;
|
||||||
|
|
||||||
export type SectionRenderers = Record<
|
export type SectionRenderers = Record<
|
||||||
string,
|
string,
|
||||||
@ -16,10 +22,26 @@ export type SectionRenderers = Record<
|
|||||||
// names to React components. These names are referenced from `uiSchema`
|
// names to React components. These names are referenced from `uiSchema`
|
||||||
// descriptors (e.g., `{ "ui:after": { render: "SemanticSearchReindex" } }`) and
|
// descriptors (e.g., `{ "ui:after": { render: "SemanticSearchReindex" } }`) and
|
||||||
// are resolved by `FieldTemplate` through `formContext.renderers`.
|
// are resolved by `FieldTemplate` through `formContext.renderers`.
|
||||||
|
//
|
||||||
|
// RUNTIME PROPS INJECTION:
|
||||||
|
// All renderers automatically receive the following props from BaseSection:
|
||||||
|
// - selectedCamera?: string - The current camera name (camera-level only)
|
||||||
|
// - setUnsavedChanges?: (hasChanges: boolean) => void - Callback to signal unsaved state
|
||||||
|
//
|
||||||
|
// Additional static props can be passed via uiSchema:
|
||||||
|
// { "ui:after": { render: "MyRenderer", props: { customProp: "value" } } }
|
||||||
|
//
|
||||||
|
// ADDING NEW RENDERERS:
|
||||||
|
// 1. Create your component accepting SectionRendererProps
|
||||||
|
// 2. Import and add it to the appropriate section in this registry
|
||||||
|
// 3. Reference it in your section's uiSchema using the { render: "ComponentName" } syntax
|
||||||
export const sectionRenderers: SectionRenderers = {
|
export const sectionRenderers: SectionRenderers = {
|
||||||
semantic_search: {
|
semantic_search: {
|
||||||
SemanticSearchReindex,
|
SemanticSearchReindex,
|
||||||
},
|
},
|
||||||
|
review: {
|
||||||
|
CameraReviewSettingsView,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default sectionRenderers;
|
export default sectionRenderers;
|
||||||
|
|||||||
@ -53,6 +53,7 @@ import {
|
|||||||
import { applySchemaDefaults } from "@/lib/config-schema";
|
import { applySchemaDefaults } from "@/lib/config-schema";
|
||||||
import { isJsonObject } from "@/lib/utils";
|
import { isJsonObject } from "@/lib/utils";
|
||||||
import { ConfigSectionData, JsonObject, JsonValue } from "@/types/configForm";
|
import { ConfigSectionData, JsonObject, JsonValue } from "@/types/configForm";
|
||||||
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
|
|
||||||
export interface SectionConfig {
|
export interface SectionConfig {
|
||||||
/** Field ordering within the section */
|
/** Field ordering within the section */
|
||||||
@ -513,7 +514,7 @@ export function ConfigSection({
|
|||||||
|
|
||||||
const needsRestart = requiresRestartForOverrides(overrides);
|
const needsRestart = requiresRestartForOverrides(overrides);
|
||||||
|
|
||||||
await axios.put("config/sett", {
|
await axios.put("config/set", {
|
||||||
requires_restart: needsRestart ? 1 : 0,
|
requires_restart: needsRestart ? 1 : 0,
|
||||||
update_topic: updateTopic,
|
update_topic: updateTopic,
|
||||||
config_data: {
|
config_data: {
|
||||||
@ -607,7 +608,7 @@ export function ConfigSection({
|
|||||||
|
|
||||||
const configData = level === "global" ? effectiveSchemaDefaults : "";
|
const configData = level === "global" ? effectiveSchemaDefaults : "";
|
||||||
|
|
||||||
await axios.put("config/sett", {
|
await axios.put("config/set", {
|
||||||
requires_restart: requiresRestart ? 0 : 1,
|
requires_restart: requiresRestart ? 0 : 1,
|
||||||
update_topic: updateTopic,
|
update_topic: updateTopic,
|
||||||
config_data: {
|
config_data: {
|
||||||
@ -688,6 +689,42 @@ export function ConfigSection({
|
|||||||
);
|
);
|
||||||
}, [sectionConfig.customValidate, sectionValidation]);
|
}, [sectionConfig.customValidate, sectionValidation]);
|
||||||
|
|
||||||
|
// Wrap renderers with runtime props (selectedCamera, setUnsavedChanges, etc.)
|
||||||
|
const wrappedRenderers = useMemo(() => {
|
||||||
|
const baseRenderers =
|
||||||
|
sectionConfig?.renderers ?? sectionRenderers?.[sectionPath];
|
||||||
|
if (!baseRenderers) return undefined;
|
||||||
|
|
||||||
|
// Create wrapper that injects runtime props
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(baseRenderers).map(([key, RendererComponent]) => [
|
||||||
|
key,
|
||||||
|
(staticProps: Record<string, unknown> = {}) => (
|
||||||
|
<RendererComponent
|
||||||
|
{...staticProps}
|
||||||
|
selectedCamera={cameraName}
|
||||||
|
setUnsavedChanges={(hasChanges: boolean) => {
|
||||||
|
// Translate setUnsavedChanges to pending data state
|
||||||
|
if (hasChanges && !pendingData) {
|
||||||
|
// Component signaled changes but we don't have pending data yet
|
||||||
|
// This can happen when the component manages its own state
|
||||||
|
} else if (!hasChanges && pendingData) {
|
||||||
|
// Component signaled no changes, clear pending
|
||||||
|
setPendingData(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
sectionConfig?.renderers,
|
||||||
|
sectionPath,
|
||||||
|
cameraName,
|
||||||
|
pendingData,
|
||||||
|
setPendingData,
|
||||||
|
]);
|
||||||
|
|
||||||
if (!modifiedSchema) {
|
if (!modifiedSchema) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -749,8 +786,7 @@ export function ConfigSection({
|
|||||||
// section prefix to templates so they can attempt `${section}.${field}` lookups.
|
// section prefix to templates so they can attempt `${section}.${field}` lookups.
|
||||||
sectionI18nPrefix: sectionPath,
|
sectionI18nPrefix: sectionPath,
|
||||||
t,
|
t,
|
||||||
renderers:
|
renderers: wrappedRenderers,
|
||||||
sectionConfig?.renderers ?? sectionRenderers?.[sectionPath],
|
|
||||||
sectionDocs: sectionConfig.sectionDocs,
|
sectionDocs: sectionConfig.sectionDocs,
|
||||||
fieldDocs: sectionConfig.fieldDocs,
|
fieldDocs: sectionConfig.fieldDocs,
|
||||||
}}
|
}}
|
||||||
@ -775,7 +811,7 @@ export function ConfigSection({
|
|||||||
onClick={() => setIsResetDialogOpen(true)}
|
onClick={() => setIsResetDialogOpen(true)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={isSaving || disabled}
|
disabled={isSaving || disabled}
|
||||||
className="gap-2"
|
className="flex flex-1 gap-2"
|
||||||
>
|
>
|
||||||
<LuRotateCcw className="h-4 w-4" />
|
<LuRotateCcw className="h-4 w-4" />
|
||||||
{level === "global"
|
{level === "global"
|
||||||
@ -794,7 +830,7 @@ export function ConfigSection({
|
|||||||
onClick={handleReset}
|
onClick={handleReset}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={isSaving || disabled}
|
disabled={isSaving || disabled}
|
||||||
className="gap-2"
|
className="flex min-w-36 flex-1 gap-2"
|
||||||
>
|
>
|
||||||
{t("undo", { ns: "common", defaultValue: "Undo" })}
|
{t("undo", { ns: "common", defaultValue: "Undo" })}
|
||||||
</Button>
|
</Button>
|
||||||
@ -803,12 +839,19 @@ export function ConfigSection({
|
|||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
variant="select"
|
variant="select"
|
||||||
disabled={!hasChanges || isSaving || disabled}
|
disabled={!hasChanges || isSaving || disabled}
|
||||||
className="gap-2"
|
className="flex min-w-36 flex-1 gap-2"
|
||||||
>
|
>
|
||||||
<LuSave className="h-4 w-4" />
|
{isSaving ? (
|
||||||
{isSaving
|
<>
|
||||||
? t("saving", { ns: "common", defaultValue: "Saving..." })
|
<ActivityIndicator className="h-4 w-4" />
|
||||||
: t("save", { ns: "common", defaultValue: "Save" })}
|
{t("saving", { ns: "common", defaultValue: "Saving..." })}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<LuSave className="h-4 w-4" />
|
||||||
|
{t("save", { ns: "common", defaultValue: "Save" })}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -33,7 +33,6 @@ import useSWR from "swr";
|
|||||||
import FilterSwitch from "@/components/filter/FilterSwitch";
|
import FilterSwitch from "@/components/filter/FilterSwitch";
|
||||||
import { ZoneMaskFilterButton } from "@/components/filter/ZoneMaskFilter";
|
import { ZoneMaskFilterButton } from "@/components/filter/ZoneMaskFilter";
|
||||||
import { PolygonType } from "@/types/canvas";
|
import { PolygonType } from "@/types/canvas";
|
||||||
import CameraReviewSettingsView from "@/views/settings/CameraReviewSettingsView";
|
|
||||||
import CameraManagementView from "@/views/settings/CameraManagementView";
|
import CameraManagementView from "@/views/settings/CameraManagementView";
|
||||||
import MotionTunerView from "@/views/settings/MotionTunerView";
|
import MotionTunerView from "@/views/settings/MotionTunerView";
|
||||||
import MasksAndZonesView from "@/views/settings/MasksAndZonesView";
|
import MasksAndZonesView from "@/views/settings/MasksAndZonesView";
|
||||||
@ -80,6 +79,7 @@ import {
|
|||||||
MobilePageHeader,
|
MobilePageHeader,
|
||||||
MobilePageTitle,
|
MobilePageTitle,
|
||||||
} from "@/components/mobile/MobilePage";
|
} from "@/components/mobile/MobilePage";
|
||||||
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
|
||||||
const allSettingsViews = [
|
const allSettingsViews = [
|
||||||
"profileSettings",
|
"profileSettings",
|
||||||
@ -119,7 +119,7 @@ const allSettingsViews = [
|
|||||||
"cameraSnapshots",
|
"cameraSnapshots",
|
||||||
"cameraMotion",
|
"cameraMotion",
|
||||||
"cameraObjects",
|
"cameraObjects",
|
||||||
"cameraConfigReview",
|
"cameraReview",
|
||||||
"cameraAudioEvents",
|
"cameraAudioEvents",
|
||||||
"cameraAudioTranscription",
|
"cameraAudioTranscription",
|
||||||
"cameraNotifications",
|
"cameraNotifications",
|
||||||
@ -132,7 +132,6 @@ const allSettingsViews = [
|
|||||||
"cameraUi",
|
"cameraUi",
|
||||||
"cameraTimestampStyle",
|
"cameraTimestampStyle",
|
||||||
"cameraManagement",
|
"cameraManagement",
|
||||||
"cameraReview",
|
|
||||||
"masksAndZones",
|
"masksAndZones",
|
||||||
"motionTuner",
|
"motionTuner",
|
||||||
"enrichments",
|
"enrichments",
|
||||||
@ -224,7 +223,7 @@ const CameraRecordingSettingsPage = createSectionPage("record", "camera");
|
|||||||
const CameraSnapshotsSettingsPage = createSectionPage("snapshots", "camera");
|
const CameraSnapshotsSettingsPage = createSectionPage("snapshots", "camera");
|
||||||
const CameraMotionSettingsPage = createSectionPage("motion", "camera");
|
const CameraMotionSettingsPage = createSectionPage("motion", "camera");
|
||||||
const CameraObjectsSettingsPage = createSectionPage("objects", "camera");
|
const CameraObjectsSettingsPage = createSectionPage("objects", "camera");
|
||||||
const CameraConfigReviewSettingsPage = createSectionPage("review", "camera");
|
const CameraReviewSettingsPage = createSectionPage("review", "camera");
|
||||||
const CameraAudioEventsSettingsPage = createSectionPage("audio", "camera");
|
const CameraAudioEventsSettingsPage = createSectionPage("audio", "camera");
|
||||||
const CameraAudioTranscriptionSettingsPage = createSectionPage(
|
const CameraAudioTranscriptionSettingsPage = createSectionPage(
|
||||||
"audio_transcription",
|
"audio_transcription",
|
||||||
@ -290,7 +289,7 @@ const settingsGroups = [
|
|||||||
{ key: "cameraSnapshots", component: CameraSnapshotsSettingsPage },
|
{ key: "cameraSnapshots", component: CameraSnapshotsSettingsPage },
|
||||||
{ key: "cameraMotion", component: CameraMotionSettingsPage },
|
{ key: "cameraMotion", component: CameraMotionSettingsPage },
|
||||||
{ key: "cameraObjects", component: CameraObjectsSettingsPage },
|
{ key: "cameraObjects", component: CameraObjectsSettingsPage },
|
||||||
{ key: "cameraConfigReview", component: CameraConfigReviewSettingsPage },
|
{ key: "cameraReview", component: CameraReviewSettingsPage },
|
||||||
{ key: "cameraAudioEvents", component: CameraAudioEventsSettingsPage },
|
{ key: "cameraAudioEvents", component: CameraAudioEventsSettingsPage },
|
||||||
{
|
{
|
||||||
key: "cameraAudioTranscription",
|
key: "cameraAudioTranscription",
|
||||||
@ -318,7 +317,6 @@ const settingsGroups = [
|
|||||||
component: CameraTimestampStyleSettingsPage,
|
component: CameraTimestampStyleSettingsPage,
|
||||||
},
|
},
|
||||||
{ key: "cameraManagement", component: CameraManagementView },
|
{ key: "cameraManagement", component: CameraManagementView },
|
||||||
{ key: "cameraReview", component: CameraReviewSettingsView },
|
|
||||||
{ key: "masksAndZones", component: MasksAndZonesView },
|
{ key: "masksAndZones", component: MasksAndZonesView },
|
||||||
{ key: "motionTuner", component: MotionTunerView },
|
{ key: "motionTuner", component: MotionTunerView },
|
||||||
],
|
],
|
||||||
@ -412,7 +410,7 @@ const CAMERA_SELECT_BUTTON_PAGES = [
|
|||||||
"cameraSnapshots",
|
"cameraSnapshots",
|
||||||
"cameraMotion",
|
"cameraMotion",
|
||||||
"cameraObjects",
|
"cameraObjects",
|
||||||
"cameraConfigReview",
|
"cameraReview",
|
||||||
"cameraAudioEvents",
|
"cameraAudioEvents",
|
||||||
"cameraAudioTranscription",
|
"cameraAudioTranscription",
|
||||||
"cameraNotifications",
|
"cameraNotifications",
|
||||||
@ -424,7 +422,6 @@ const CAMERA_SELECT_BUTTON_PAGES = [
|
|||||||
"cameraOnvif",
|
"cameraOnvif",
|
||||||
"cameraUi",
|
"cameraUi",
|
||||||
"cameraTimestampStyle",
|
"cameraTimestampStyle",
|
||||||
"cameraReview",
|
|
||||||
"masksAndZones",
|
"masksAndZones",
|
||||||
"motionTuner",
|
"motionTuner",
|
||||||
"triggers",
|
"triggers",
|
||||||
@ -440,7 +437,7 @@ const CAMERA_SECTION_MAPPING: Record<string, SettingsType> = {
|
|||||||
snapshots: "cameraSnapshots",
|
snapshots: "cameraSnapshots",
|
||||||
motion: "cameraMotion",
|
motion: "cameraMotion",
|
||||||
objects: "cameraObjects",
|
objects: "cameraObjects",
|
||||||
review: "cameraConfigReview",
|
review: "cameraReview",
|
||||||
audio: "cameraAudioEvents",
|
audio: "cameraAudioEvents",
|
||||||
audio_transcription: "cameraAudioTranscription",
|
audio_transcription: "cameraAudioTranscription",
|
||||||
notifications: "cameraNotifications",
|
notifications: "cameraNotifications",
|
||||||
@ -788,6 +785,7 @@ export default function Settings() {
|
|||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Toaster position="top-center" />
|
||||||
{!contentMobileOpen && (
|
{!contentMobileOpen && (
|
||||||
<div className="flex size-full flex-col">
|
<div className="flex size-full flex-col">
|
||||||
<div className="sticky -top-2 z-50 mb-2 bg-background p-4">
|
<div className="sticky -top-2 z-50 mb-2 bg-background p-4">
|
||||||
@ -925,7 +923,8 @@ export default function Settings() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div className="flex min-h-16 items-center justify-between border-b border-secondary p-3">
|
<Toaster position="top-center" />
|
||||||
|
<div className="flex items-center justify-between border-b border-secondary p-3">
|
||||||
<Heading as="h3" className="mb-0">
|
<Heading as="h3" className="mb-0">
|
||||||
{t("menu.settings", { ns: "common" })}
|
{t("menu.settings", { ns: "common" })}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|||||||
@ -43,7 +43,7 @@ import { formatList } from "@/utils/stringUtil";
|
|||||||
|
|
||||||
type CameraReviewSettingsViewProps = {
|
type CameraReviewSettingsViewProps = {
|
||||||
selectedCamera: string;
|
selectedCamera: string;
|
||||||
setUnsavedChanges: React.Dispatch<React.SetStateAction<boolean>>;
|
setUnsavedChanges?: (hasChanges: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CameraReviewSettingsValueType = {
|
type CameraReviewSettingsValueType = {
|
||||||
@ -249,7 +249,7 @@ export default function CameraReviewSettingsView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setChangedValue(false);
|
setChangedValue(false);
|
||||||
setUnsavedChanges(false);
|
setUnsavedChanges?.(false);
|
||||||
removeMessage(
|
removeMessage(
|
||||||
"camera_settings",
|
"camera_settings",
|
||||||
`review_classification_settings_${selectedCamera}`,
|
`review_classification_settings_${selectedCamera}`,
|
||||||
@ -746,6 +746,7 @@ export default function CameraReviewSettingsView({
|
|||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Separator className="my-2 flex bg-secondary" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user