mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-26 13:48:59 +03:00
use react-jsonschema-form for UI config
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
// Hook to detect when camera config overrides global defaults
|
||||
import { useMemo } from "react";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import get from "lodash/get";
|
||||
import type { FrigateConfig } from "@/types/frigateConfig";
|
||||
|
||||
export interface OverrideStatus {
|
||||
/** Whether the field is overridden from global */
|
||||
isOverridden: boolean;
|
||||
/** The global default value */
|
||||
globalValue: unknown;
|
||||
/** The camera-specific value */
|
||||
cameraValue: unknown;
|
||||
}
|
||||
|
||||
export interface UseConfigOverrideOptions {
|
||||
/** Full Frigate config */
|
||||
config: FrigateConfig | undefined;
|
||||
/** Camera name for per-camera settings */
|
||||
cameraName?: string;
|
||||
/** Config section path (e.g., "detect", "record.events") */
|
||||
sectionPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to detect config overrides between global and camera level
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { isOverridden, getFieldOverride } = useConfigOverride({
|
||||
* config,
|
||||
* cameraName: "front_door",
|
||||
* sectionPath: "detect"
|
||||
* });
|
||||
*
|
||||
* // Check if entire section is overridden
|
||||
* if (isOverridden) {
|
||||
* // Show override indicator
|
||||
* }
|
||||
*
|
||||
* // Check specific field
|
||||
* const fpsOverride = getFieldOverride("fps");
|
||||
* ```
|
||||
*/
|
||||
export function useConfigOverride({
|
||||
config,
|
||||
cameraName,
|
||||
sectionPath,
|
||||
}: UseConfigOverrideOptions) {
|
||||
return useMemo(() => {
|
||||
if (!config) {
|
||||
return {
|
||||
isOverridden: false,
|
||||
globalValue: undefined,
|
||||
cameraValue: undefined,
|
||||
getFieldOverride: () => ({
|
||||
isOverridden: false,
|
||||
globalValue: undefined,
|
||||
cameraValue: undefined,
|
||||
}),
|
||||
resetToGlobal: () => undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// Get global value for the section
|
||||
const globalValue = get(config, sectionPath);
|
||||
|
||||
// If no camera specified, return global value info
|
||||
if (!cameraName) {
|
||||
return {
|
||||
isOverridden: false,
|
||||
globalValue,
|
||||
cameraValue: globalValue,
|
||||
getFieldOverride: (fieldPath: string): OverrideStatus => ({
|
||||
isOverridden: false,
|
||||
globalValue: get(globalValue, fieldPath),
|
||||
cameraValue: get(globalValue, fieldPath),
|
||||
}),
|
||||
resetToGlobal: () => globalValue,
|
||||
};
|
||||
}
|
||||
|
||||
// Get camera-specific value
|
||||
const cameraConfig = config.cameras?.[cameraName];
|
||||
if (!cameraConfig) {
|
||||
return {
|
||||
isOverridden: false,
|
||||
globalValue,
|
||||
cameraValue: undefined,
|
||||
getFieldOverride: () => ({
|
||||
isOverridden: false,
|
||||
globalValue: undefined,
|
||||
cameraValue: undefined,
|
||||
}),
|
||||
resetToGlobal: () => globalValue,
|
||||
};
|
||||
}
|
||||
|
||||
const cameraValue = get(cameraConfig, sectionPath);
|
||||
|
||||
// Check if the entire section is overridden
|
||||
const isOverridden = !isEqual(globalValue, cameraValue);
|
||||
|
||||
/**
|
||||
* Get override status for a specific field within the section
|
||||
*/
|
||||
const getFieldOverride = (fieldPath: string): OverrideStatus => {
|
||||
const globalFieldValue = get(globalValue, fieldPath);
|
||||
const cameraFieldValue = get(cameraValue, fieldPath);
|
||||
|
||||
return {
|
||||
isOverridden: !isEqual(globalFieldValue, cameraFieldValue),
|
||||
globalValue: globalFieldValue,
|
||||
cameraValue: cameraFieldValue,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the global value to reset camera override
|
||||
*/
|
||||
const resetToGlobal = (fieldPath?: string) => {
|
||||
if (fieldPath) {
|
||||
return get(globalValue, fieldPath);
|
||||
}
|
||||
return globalValue;
|
||||
};
|
||||
|
||||
return {
|
||||
isOverridden,
|
||||
globalValue,
|
||||
cameraValue,
|
||||
getFieldOverride,
|
||||
resetToGlobal,
|
||||
};
|
||||
}, [config, cameraName, sectionPath]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get all overridden fields for a camera
|
||||
*/
|
||||
export function useAllCameraOverrides(
|
||||
config: FrigateConfig | undefined,
|
||||
cameraName: string | undefined,
|
||||
) {
|
||||
return useMemo(() => {
|
||||
if (!config || !cameraName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const cameraConfig = config.cameras?.[cameraName];
|
||||
if (!cameraConfig) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const overriddenSections: string[] = [];
|
||||
|
||||
// Check each section that can be overridden
|
||||
const sectionsToCheck = [
|
||||
"detect",
|
||||
"record",
|
||||
"snapshots",
|
||||
"motion",
|
||||
"objects",
|
||||
"review",
|
||||
"audio",
|
||||
"notifications",
|
||||
"live",
|
||||
"timestamp_style",
|
||||
];
|
||||
|
||||
for (const section of sectionsToCheck) {
|
||||
const globalValue = get(config, section);
|
||||
const cameraValue = get(cameraConfig, section);
|
||||
|
||||
if (!isEqual(globalValue, cameraValue)) {
|
||||
overriddenSections.push(section);
|
||||
}
|
||||
}
|
||||
|
||||
return overriddenSections;
|
||||
}, [config, cameraName]);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// Hook for efficiently working with config schemas
|
||||
// Caches resolved section schemas to avoid repeated expensive resolution
|
||||
|
||||
import { useMemo } from "react";
|
||||
import useSWR from "swr";
|
||||
import type { RJSFSchema } from "@rjsf/utils";
|
||||
import { resolveAndCleanSchema } from "@/lib/config-schema";
|
||||
|
||||
// Cache for resolved section schemas - keyed by schema reference + section key
|
||||
const sectionSchemaCache = new WeakMap<RJSFSchema, Map<string, RJSFSchema>>();
|
||||
|
||||
/**
|
||||
* Extracts and resolves a section schema from the full config schema
|
||||
* Uses caching to avoid repeated expensive resolution
|
||||
*/
|
||||
function extractSectionSchema(
|
||||
schema: RJSFSchema,
|
||||
sectionPath: string,
|
||||
level: "global" | "camera",
|
||||
): RJSFSchema | null {
|
||||
// Create cache key
|
||||
const cacheKey = `${level}:${sectionPath}`;
|
||||
|
||||
// Check cache first (using WeakMap with schema as key for proper garbage collection)
|
||||
let schemaCache = sectionSchemaCache.get(schema);
|
||||
if (!schemaCache) {
|
||||
schemaCache = new Map<string, RJSFSchema>();
|
||||
sectionSchemaCache.set(schema, schemaCache);
|
||||
}
|
||||
|
||||
if (schemaCache.has(cacheKey)) {
|
||||
return schemaCache.get(cacheKey)!;
|
||||
}
|
||||
|
||||
const schemaObj = schema as Record<string, unknown>;
|
||||
const defs = (schemaObj.$defs || schemaObj.definitions || {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
|
||||
let sectionDef: Record<string, unknown> | null = null;
|
||||
|
||||
// For camera level, get section from CameraConfig in $defs
|
||||
if (level === "camera") {
|
||||
const cameraConfigDef = defs.CameraConfig as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
if (cameraConfigDef?.properties) {
|
||||
const props = cameraConfigDef.properties as Record<string, unknown>;
|
||||
const sectionProp = props[sectionPath];
|
||||
|
||||
if (sectionProp && typeof sectionProp === "object") {
|
||||
const refProp = sectionProp as Record<string, unknown>;
|
||||
if (refProp.$ref && typeof refProp.$ref === "string") {
|
||||
const refPath = (refProp.$ref as string)
|
||||
.replace(/^#\/\$defs\//, "")
|
||||
.replace(/^#\/definitions\//, "");
|
||||
sectionDef = defs[refPath] as Record<string, unknown>;
|
||||
} else {
|
||||
sectionDef = sectionProp as Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For global level, get from root properties
|
||||
if (schemaObj.properties) {
|
||||
const props = schemaObj.properties as Record<string, unknown>;
|
||||
const sectionProp = props[sectionPath];
|
||||
|
||||
if (sectionProp && typeof sectionProp === "object") {
|
||||
const refProp = sectionProp as Record<string, unknown>;
|
||||
if (refProp.$ref && typeof refProp.$ref === "string") {
|
||||
const refPath = (refProp.$ref as string)
|
||||
.replace(/^#\/\$defs\//, "")
|
||||
.replace(/^#\/definitions\//, "");
|
||||
sectionDef = defs[refPath] as Record<string, unknown>;
|
||||
} else {
|
||||
sectionDef = sectionProp as Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!sectionDef) return null;
|
||||
|
||||
// Include $defs for nested references and resolve them
|
||||
const schemaWithDefs = {
|
||||
...sectionDef,
|
||||
$defs: defs,
|
||||
} as RJSFSchema;
|
||||
|
||||
// Resolve all references and strip $defs from result
|
||||
const resolved = resolveAndCleanSchema(schemaWithDefs);
|
||||
|
||||
// Cache the result
|
||||
schemaCache.set(cacheKey, resolved);
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: Cache is automatically cleared when schema changes since we use WeakMap
|
||||
* with the schema object as key. No manual clearing needed.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Hook to get a resolved section schema
|
||||
* Efficiently caches resolved schemas to avoid repeated expensive operations
|
||||
*/
|
||||
export function useSectionSchema(
|
||||
sectionPath: string,
|
||||
level: "global" | "camera",
|
||||
): RJSFSchema | null {
|
||||
const { data: schema } = useSWR<RJSFSchema>("config/schema.json");
|
||||
|
||||
return useMemo(() => {
|
||||
if (!schema) return null;
|
||||
return extractSectionSchema(schema, sectionPath, level);
|
||||
}, [schema, sectionPath, level]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get the raw config schema
|
||||
*/
|
||||
export function useConfigSchema(): RJSFSchema | undefined {
|
||||
const { data: schema } = useSWR<RJSFSchema>("config/schema.json");
|
||||
return schema;
|
||||
}
|
||||
Reference in New Issue
Block a user