mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-22 08:08:22 +03:00
fix sections and live validation
This commit is contained in:
parent
b1cd5432bf
commit
55c6c50c97
@ -1,6 +1,10 @@
|
|||||||
{
|
{
|
||||||
"label": "Global Audio events configuration",
|
"label": "Global Audio events configuration",
|
||||||
"description": "Global settings for audio-based event detection; camera-level settings can override these.",
|
"description": "Global settings for audio-based event detection; camera-level settings can override these.",
|
||||||
|
"groups": {
|
||||||
|
"detection": "Detection",
|
||||||
|
"sensitivity": "Sensitivity"
|
||||||
|
},
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"label": "Enable audio events",
|
"label": "Enable audio events",
|
||||||
"description": "Enable or disable audio event detection globally. Can be overridden per camera."
|
"description": "Enable or disable audio event detection globally. Can be overridden per camera."
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
{
|
{
|
||||||
"label": "Object tracking",
|
"label": "Object tracking",
|
||||||
"description": "Settings for the detection/detect role used to run object detection and initialize trackers.",
|
"description": "Settings for the detection/detect role used to run object detection and initialize trackers.",
|
||||||
|
"groups": {
|
||||||
|
"resolution": "Resolution",
|
||||||
|
"tracking": "Tracking"
|
||||||
|
},
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"label": "Detection Enabled",
|
"label": "Detection Enabled",
|
||||||
"description": "Enable or disable object detection for this camera. Detection must be enabled for object tracking to run."
|
"description": "Enable or disable object detection for this camera. Detection must be enabled for object tracking to run."
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
{
|
{
|
||||||
"label": "Global motion detection configuration",
|
"label": "Global motion detection configuration",
|
||||||
"description": "Default motion detection settings applied to cameras unless overridden per-camera.",
|
"description": "Default motion detection settings applied to cameras unless overridden per-camera.",
|
||||||
|
"groups": {
|
||||||
|
"sensitivity": "Sensitivity",
|
||||||
|
"algorithm": "Algorithm"
|
||||||
|
},
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"label": "Enable motion detection",
|
"label": "Enable motion detection",
|
||||||
"description": "Enable or disable motion detection globally; per-camera settings can override this."
|
"description": "Enable or disable motion detection globally; per-camera settings can override this."
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
{
|
{
|
||||||
"label": "Global object configuration",
|
"label": "Global object configuration",
|
||||||
"description": "Global object tracking defaults including which labels to track and per-object filters.",
|
"description": "Global object tracking defaults including which labels to track and per-object filters.",
|
||||||
|
"groups": {
|
||||||
|
"tracking": "Tracking",
|
||||||
|
"filtering": "Filtering"
|
||||||
|
},
|
||||||
"track": {
|
"track": {
|
||||||
"label": "Objects to track",
|
"label": "Objects to track",
|
||||||
"description": "List of object labels to track globally; camera configs can override this."
|
"description": "List of object labels to track globally; camera configs can override this."
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
{
|
{
|
||||||
"label": "Global record configuration",
|
"label": "Global record configuration",
|
||||||
"description": "Global recording and retention settings applied to cameras unless overridden per-camera.",
|
"description": "Global recording and retention settings applied to cameras unless overridden per-camera.",
|
||||||
|
"groups": {
|
||||||
|
"retention": "Retention",
|
||||||
|
"events": "Events"
|
||||||
|
},
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"label": "Enable record on all cameras",
|
"label": "Enable record on all cameras",
|
||||||
"description": "Enable or disable recording globally; individual cameras can override this."
|
"description": "Enable or disable recording globally; individual cameras can override this."
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
{
|
{
|
||||||
"label": "Global snapshots configuration",
|
"label": "Global snapshots configuration",
|
||||||
"description": "Global settings for saved JPEG snapshots of tracked objects; can be overridden per-camera.",
|
"description": "Global settings for saved JPEG snapshots of tracked objects; can be overridden per-camera.",
|
||||||
|
"groups": {
|
||||||
|
"display": "Display"
|
||||||
|
},
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"label": "Snapshots enabled",
|
"label": "Snapshots enabled",
|
||||||
"description": "Enable or disable saving snapshots globally."
|
"description": "Enable or disable saving snapshots globally."
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
{
|
{
|
||||||
"label": "Global timestamp style configuration",
|
"label": "Global timestamp style configuration",
|
||||||
"description": "Global styling options for in-feed timestamps applied to recordings and snapshots.",
|
"description": "Global styling options for in-feed timestamps applied to recordings and snapshots.",
|
||||||
|
"groups": {
|
||||||
|
"appearance": "Appearance"
|
||||||
|
},
|
||||||
"position": {
|
"position": {
|
||||||
"label": "Timestamp position",
|
"label": "Timestamp position",
|
||||||
"description": "Position of the timestamp on the image (tl/tr/bl/br)."
|
"description": "Position of the timestamp on the image (tl/tr/bl/br)."
|
||||||
|
|||||||
@ -27,6 +27,8 @@ export interface ConfigFormProps {
|
|||||||
uiSchema?: UiSchema;
|
uiSchema?: UiSchema;
|
||||||
/** Field ordering */
|
/** Field ordering */
|
||||||
fieldOrder?: string[];
|
fieldOrder?: string[];
|
||||||
|
/** Field groups for layout */
|
||||||
|
fieldGroups?: Record<string, string[]>;
|
||||||
/** Fields to hide */
|
/** Fields to hide */
|
||||||
hiddenFields?: string[];
|
hiddenFields?: string[];
|
||||||
/** Fields marked as advanced (collapsed by default) */
|
/** Fields marked as advanced (collapsed by default) */
|
||||||
@ -55,6 +57,7 @@ export function ConfigForm({
|
|||||||
onError,
|
onError,
|
||||||
uiSchema: customUiSchema,
|
uiSchema: customUiSchema,
|
||||||
fieldOrder,
|
fieldOrder,
|
||||||
|
fieldGroups,
|
||||||
hiddenFields,
|
hiddenFields,
|
||||||
advancedFields,
|
advancedFields,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
@ -100,12 +103,13 @@ export function ConfigForm({
|
|||||||
const finalUiSchema = useMemo(
|
const finalUiSchema = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
...generatedUiSchema,
|
...generatedUiSchema,
|
||||||
|
"ui:groups": fieldGroups,
|
||||||
...customUiSchema,
|
...customUiSchema,
|
||||||
"ui:submitButtonOptions": showSubmit
|
"ui:submitButtonOptions": showSubmit
|
||||||
? { norender: false }
|
? { norender: false }
|
||||||
: { norender: true },
|
: { norender: true },
|
||||||
}),
|
}),
|
||||||
[generatedUiSchema, customUiSchema, showSubmit],
|
[generatedUiSchema, customUiSchema, showSubmit, fieldGroups],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create error transformer for user-friendly error messages
|
// Create error transformer for user-friendly error messages
|
||||||
|
|||||||
@ -45,6 +45,8 @@ export interface SectionConfig {
|
|||||||
advancedFields?: string[];
|
advancedFields?: string[];
|
||||||
/** Fields to compare for override detection */
|
/** Fields to compare for override detection */
|
||||||
overrideFields?: string[];
|
overrideFields?: string[];
|
||||||
|
/** Whether to enable live validation */
|
||||||
|
liveValidate?: boolean;
|
||||||
/** Additional uiSchema overrides */
|
/** Additional uiSchema overrides */
|
||||||
uiSchema?: UiSchema;
|
uiSchema?: UiSchema;
|
||||||
}
|
}
|
||||||
@ -466,8 +468,10 @@ export function createConfigSection({
|
|||||||
formData={pendingData || formData}
|
formData={pendingData || formData}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
fieldOrder={sectionConfig.fieldOrder}
|
fieldOrder={sectionConfig.fieldOrder}
|
||||||
|
fieldGroups={sectionConfig.fieldGroups}
|
||||||
hiddenFields={sectionConfig.hiddenFields}
|
hiddenFields={sectionConfig.hiddenFields}
|
||||||
advancedFields={sectionConfig.advancedFields}
|
advancedFields={sectionConfig.advancedFields}
|
||||||
|
liveValidate={sectionConfig.liveValidate}
|
||||||
uiSchema={sectionConfig.uiSchema}
|
uiSchema={sectionConfig.uiSchema}
|
||||||
disabled={disabled || isSaving}
|
disabled={disabled || isSaving}
|
||||||
readonly={readonly}
|
readonly={readonly}
|
||||||
|
|||||||
@ -9,31 +9,111 @@ import {
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { LuChevronDown, LuChevronRight } from "react-icons/lu";
|
import { LuChevronDown, LuChevronRight } from "react-icons/lu";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
||||||
const { title, description, properties } = props;
|
const { title, description, properties, uiSchema } = props;
|
||||||
|
const formContext = (props as Record<string, unknown>).formContext as
|
||||||
|
| Record<string, unknown>
|
||||||
|
| undefined;
|
||||||
|
|
||||||
// Check if this is a root-level object
|
// Check if this is a root-level object
|
||||||
const isRoot = !title;
|
const isRoot = !title;
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
|
|
||||||
|
const { t } = useTranslation([
|
||||||
|
(formContext?.i18nNamespace as string | undefined) || "common",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const groupDefinitions =
|
||||||
|
(uiSchema?.["ui:groups"] as Record<string, string[]> | undefined) || {};
|
||||||
|
|
||||||
|
const isHiddenProp = (prop: (typeof properties)[number]) =>
|
||||||
|
prop.content.props.uiSchema?.["ui:widget"] === "hidden";
|
||||||
|
|
||||||
|
const visibleProps = properties.filter((prop) => !isHiddenProp(prop));
|
||||||
|
|
||||||
// Check for advanced section grouping
|
// Check for advanced section grouping
|
||||||
const advancedProps = properties.filter(
|
const advancedProps = visibleProps.filter(
|
||||||
(p) => p.content.props.uiSchema?.["ui:options"]?.advanced === true,
|
(p) => p.content.props.uiSchema?.["ui:options"]?.advanced === true,
|
||||||
);
|
);
|
||||||
const regularProps = properties.filter(
|
const regularProps = visibleProps.filter(
|
||||||
(p) => p.content.props.uiSchema?.["ui:options"]?.advanced !== true,
|
(p) => p.content.props.uiSchema?.["ui:options"]?.advanced !== true,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||||
|
|
||||||
|
const toTitle = (value: string) =>
|
||||||
|
value.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
||||||
|
|
||||||
|
const renderGroupedFields = (items: (typeof properties)[number][]) => {
|
||||||
|
if (!items.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const grouped = new Set<string>();
|
||||||
|
const groups = Object.entries(groupDefinitions)
|
||||||
|
.map(([groupKey, fields]) => {
|
||||||
|
const ordered = fields
|
||||||
|
.map((field) => items.find((item) => item.name === field))
|
||||||
|
.filter(Boolean) as (typeof properties)[number][];
|
||||||
|
|
||||||
|
if (ordered.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ordered.forEach((item) => grouped.add(item.name));
|
||||||
|
|
||||||
|
const label = t(`groups.${groupKey}`, {
|
||||||
|
defaultValue: toTitle(groupKey),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: groupKey,
|
||||||
|
label,
|
||||||
|
items: ordered,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean) as Array<{
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
items: (typeof properties)[number][];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const ungrouped = items.filter((item) => !grouped.has(item.name));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{groups.map((group) => (
|
||||||
|
<div key={group.key} className="space-y-4">
|
||||||
|
<div className="text-sm font-medium text-muted-foreground">
|
||||||
|
{group.label}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{group.items.map((element) => (
|
||||||
|
<div key={element.name}>{element.content}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{ungrouped.length > 0 && (
|
||||||
|
<div className={cn("space-y-4", groups.length > 0 && "pt-2")}>
|
||||||
|
{ungrouped.map((element) => (
|
||||||
|
<div key={element.name}>{element.content}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Root level renders children directly
|
// Root level renders children directly
|
||||||
if (isRoot) {
|
if (isRoot) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{regularProps.map((element) => (
|
{renderGroupedFields(regularProps)}
|
||||||
<div key={element.name}>{element.content}</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{advancedProps.length > 0 && (
|
{advancedProps.length > 0 && (
|
||||||
<Collapsible open={showAdvanced} onOpenChange={setShowAdvanced}>
|
<Collapsible open={showAdvanced} onOpenChange={setShowAdvanced}>
|
||||||
@ -48,9 +128,7 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent className="space-y-4 pt-2">
|
<CollapsibleContent className="space-y-4 pt-2">
|
||||||
{advancedProps.map((element) => (
|
{renderGroupedFields(advancedProps)}
|
||||||
<div key={element.name}>{element.content}</div>
|
|
||||||
))}
|
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
)}
|
)}
|
||||||
@ -83,9 +161,7 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
|||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<CardContent className="space-y-4 pt-0">
|
<CardContent className="space-y-4 pt-0">
|
||||||
{regularProps.map((element) => (
|
{renderGroupedFields(regularProps)}
|
||||||
<div key={element.name}>{element.content}</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{advancedProps.length > 0 && (
|
{advancedProps.length > 0 && (
|
||||||
<Collapsible open={showAdvanced} onOpenChange={setShowAdvanced}>
|
<Collapsible open={showAdvanced} onOpenChange={setShowAdvanced}>
|
||||||
@ -104,9 +180,7 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent className="space-y-4 pt-2">
|
<CollapsibleContent className="space-y-4 pt-2">
|
||||||
{advancedProps.map((element) => (
|
{renderGroupedFields(advancedProps)}
|
||||||
<div key={element.name}>{element.content}</div>
|
|
||||||
))}
|
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -66,6 +66,7 @@ const globalSectionConfigs: Record<
|
|||||||
fieldOrder?: string[];
|
fieldOrder?: string[];
|
||||||
hiddenFields?: string[];
|
hiddenFields?: string[];
|
||||||
advancedFields?: string[];
|
advancedFields?: string[];
|
||||||
|
liveValidate?: boolean;
|
||||||
i18nNamespace: string;
|
i18nNamespace: string;
|
||||||
}
|
}
|
||||||
> = {
|
> = {
|
||||||
@ -114,8 +115,8 @@ const globalSectionConfigs: Record<
|
|||||||
"trusted_proxies",
|
"trusted_proxies",
|
||||||
"hash_iterations",
|
"hash_iterations",
|
||||||
"roles",
|
"roles",
|
||||||
"admin_first_time_login",
|
|
||||||
],
|
],
|
||||||
|
hiddenFields: ["admin_first_time_login"],
|
||||||
advancedFields: [
|
advancedFields: [
|
||||||
"cookie_name",
|
"cookie_name",
|
||||||
"cookie_secure",
|
"cookie_secure",
|
||||||
@ -148,6 +149,7 @@ const globalSectionConfigs: Record<
|
|||||||
"separator",
|
"separator",
|
||||||
],
|
],
|
||||||
advancedFields: ["header_map", "auth_secret", "separator"],
|
advancedFields: ["header_map", "auth_secret", "separator"],
|
||||||
|
liveValidate: true,
|
||||||
},
|
},
|
||||||
ui: {
|
ui: {
|
||||||
i18nNamespace: "config/ui",
|
i18nNamespace: "config/ui",
|
||||||
@ -502,6 +504,7 @@ const GlobalConfigSection = memo(function GlobalConfigSection({
|
|||||||
fieldOrder={sectionConfig.fieldOrder}
|
fieldOrder={sectionConfig.fieldOrder}
|
||||||
hiddenFields={sectionConfig.hiddenFields}
|
hiddenFields={sectionConfig.hiddenFields}
|
||||||
advancedFields={sectionConfig.advancedFields}
|
advancedFields={sectionConfig.advancedFields}
|
||||||
|
liveValidate={sectionConfig.liveValidate}
|
||||||
showSubmit={false}
|
showSubmit={false}
|
||||||
i18nNamespace={sectionConfig.i18nNamespace}
|
i18nNamespace={sectionConfig.i18nNamespace}
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user