mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-27 02:28:22 +03:00
add array field item template and fix ffmpeg section
This commit is contained in:
parent
8c65cbce22
commit
40b59c5d0c
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"label": "Object tracking",
|
"label": "Object Detection",
|
||||||
"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": {
|
"groups": {
|
||||||
"resolution": "Resolution",
|
"resolution": "Resolution",
|
||||||
|
|||||||
@ -38,6 +38,100 @@ export const FfmpegSection = createConfigSection({
|
|||||||
"apple_compatibility",
|
"apple_compatibility",
|
||||||
"gpu",
|
"gpu",
|
||||||
],
|
],
|
||||||
|
uiSchema: {
|
||||||
|
global_args: {
|
||||||
|
"ui:widget": "ArrayAsTextWidget",
|
||||||
|
"ui:options": {
|
||||||
|
suppressMultiSchema: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hwaccel_args: {
|
||||||
|
"ui:widget": "ArrayAsTextWidget",
|
||||||
|
"ui:options": {
|
||||||
|
suppressMultiSchema: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
input_args: {
|
||||||
|
"ui:widget": "ArrayAsTextWidget",
|
||||||
|
"ui:options": {
|
||||||
|
suppressMultiSchema: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
output_args: {
|
||||||
|
"ui:widget": "ArrayAsTextWidget",
|
||||||
|
"ui:options": {
|
||||||
|
suppressMultiSchema: true,
|
||||||
|
},
|
||||||
|
detect: {
|
||||||
|
"ui:widget": "ArrayAsTextWidget",
|
||||||
|
"ui:options": {
|
||||||
|
suppressMultiSchema: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
record: {
|
||||||
|
"ui:widget": "ArrayAsTextWidget",
|
||||||
|
"ui:options": {
|
||||||
|
suppressMultiSchema: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
detect: {
|
||||||
|
"ui:widget": "ArrayAsTextWidget",
|
||||||
|
"ui:options": {
|
||||||
|
suppressMultiSchema: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
record: {
|
||||||
|
"ui:widget": "ArrayAsTextWidget",
|
||||||
|
"ui:options": {
|
||||||
|
suppressMultiSchema: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputs: {
|
||||||
|
items: {
|
||||||
|
global_args: {
|
||||||
|
"ui:widget": "ArrayAsTextWidget",
|
||||||
|
"ui:options": {
|
||||||
|
suppressMultiSchema: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hwaccel_args: {
|
||||||
|
"ui:widget": "ArrayAsTextWidget",
|
||||||
|
"ui:options": {
|
||||||
|
suppressMultiSchema: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
input_args: {
|
||||||
|
"ui:widget": "ArrayAsTextWidget",
|
||||||
|
"ui:options": {
|
||||||
|
suppressMultiSchema: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
output_args: {
|
||||||
|
"ui:widget": "ArrayAsTextWidget",
|
||||||
|
"ui:options": {
|
||||||
|
suppressMultiSchema: true,
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
detect: {
|
||||||
|
"ui:widget": "ArrayAsTextWidget",
|
||||||
|
"ui:options": {
|
||||||
|
suppressMultiSchema: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
record: {
|
||||||
|
"ui:widget": "ArrayAsTextWidget",
|
||||||
|
"ui:options": {
|
||||||
|
suppressMultiSchema: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import type {
|
|||||||
RegistryFieldsType,
|
RegistryFieldsType,
|
||||||
TemplatesType,
|
TemplatesType,
|
||||||
} from "@rjsf/utils";
|
} from "@rjsf/utils";
|
||||||
import { getDefaultRegistry } from "@rjsf/core";
|
|
||||||
|
|
||||||
import { SwitchWidget } from "./widgets/SwitchWidget";
|
import { SwitchWidget } from "./widgets/SwitchWidget";
|
||||||
import { SelectWidget } from "./widgets/SelectWidget";
|
import { SelectWidget } from "./widgets/SelectWidget";
|
||||||
@ -27,6 +26,7 @@ import { ArrayAsTextWidget } from "./widgets/ArrayAsTextWidget";
|
|||||||
import { FieldTemplate } from "./templates/FieldTemplate";
|
import { FieldTemplate } from "./templates/FieldTemplate";
|
||||||
import { ObjectFieldTemplate } from "./templates/ObjectFieldTemplate";
|
import { ObjectFieldTemplate } from "./templates/ObjectFieldTemplate";
|
||||||
import { ArrayFieldTemplate } from "./templates/ArrayFieldTemplate";
|
import { ArrayFieldTemplate } from "./templates/ArrayFieldTemplate";
|
||||||
|
import { ArrayFieldItemTemplate } from "./templates/ArrayFieldItemTemplate";
|
||||||
import { BaseInputTemplate } from "./templates/BaseInputTemplate";
|
import { BaseInputTemplate } from "./templates/BaseInputTemplate";
|
||||||
import { DescriptionFieldTemplate } from "./templates/DescriptionFieldTemplate";
|
import { DescriptionFieldTemplate } from "./templates/DescriptionFieldTemplate";
|
||||||
import { TitleFieldTemplate } from "./templates/TitleFieldTemplate";
|
import { TitleFieldTemplate } from "./templates/TitleFieldTemplate";
|
||||||
@ -39,11 +39,8 @@ export interface FrigateTheme {
|
|||||||
fields: RegistryFieldsType;
|
fields: RegistryFieldsType;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultRegistry = getDefaultRegistry();
|
|
||||||
|
|
||||||
export const frigateTheme: FrigateTheme = {
|
export const frigateTheme: FrigateTheme = {
|
||||||
widgets: {
|
widgets: {
|
||||||
...defaultRegistry.widgets,
|
|
||||||
// Override default widgets with shadcn/ui styled versions
|
// Override default widgets with shadcn/ui styled versions
|
||||||
TextWidget: TextWidget,
|
TextWidget: TextWidget,
|
||||||
PasswordWidget: PasswordWidget,
|
PasswordWidget: PasswordWidget,
|
||||||
@ -64,20 +61,15 @@ export const frigateTheme: FrigateTheme = {
|
|||||||
zoneNames: ZoneSwitchesWidget,
|
zoneNames: ZoneSwitchesWidget,
|
||||||
},
|
},
|
||||||
templates: {
|
templates: {
|
||||||
...defaultRegistry.templates,
|
|
||||||
FieldTemplate: FieldTemplate as React.ComponentType<FieldTemplateProps>,
|
FieldTemplate: FieldTemplate as React.ComponentType<FieldTemplateProps>,
|
||||||
ObjectFieldTemplate: ObjectFieldTemplate,
|
ObjectFieldTemplate: ObjectFieldTemplate,
|
||||||
ArrayFieldTemplate: ArrayFieldTemplate,
|
ArrayFieldTemplate: ArrayFieldTemplate,
|
||||||
|
ArrayFieldItemTemplate: ArrayFieldItemTemplate,
|
||||||
BaseInputTemplate: BaseInputTemplate as React.ComponentType<WidgetProps>,
|
BaseInputTemplate: BaseInputTemplate as React.ComponentType<WidgetProps>,
|
||||||
DescriptionFieldTemplate: DescriptionFieldTemplate,
|
DescriptionFieldTemplate: DescriptionFieldTemplate,
|
||||||
TitleFieldTemplate: TitleFieldTemplate,
|
TitleFieldTemplate: TitleFieldTemplate,
|
||||||
ErrorListTemplate: ErrorListTemplate,
|
ErrorListTemplate: ErrorListTemplate,
|
||||||
MultiSchemaFieldTemplate: MultiSchemaFieldTemplate,
|
MultiSchemaFieldTemplate: MultiSchemaFieldTemplate,
|
||||||
ButtonTemplates: {
|
|
||||||
...defaultRegistry.templates.ButtonTemplates,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fields: {
|
|
||||||
...defaultRegistry.fields,
|
|
||||||
},
|
},
|
||||||
|
fields: {},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,58 @@
|
|||||||
|
import type {
|
||||||
|
ArrayFieldItemTemplateProps,
|
||||||
|
FormContextType,
|
||||||
|
RJSFSchema,
|
||||||
|
StrictRJSFSchema,
|
||||||
|
} from "@rjsf/utils";
|
||||||
|
import { getTemplate, getUiOptions } from "@rjsf/utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom ArrayFieldItemTemplate to ensure array item content uses full width
|
||||||
|
* while keeping action buttons aligned to the right.
|
||||||
|
*/
|
||||||
|
export function ArrayFieldItemTemplate<
|
||||||
|
T = unknown,
|
||||||
|
S extends StrictRJSFSchema = RJSFSchema,
|
||||||
|
F extends FormContextType = FormContextType,
|
||||||
|
>(props: ArrayFieldItemTemplateProps<T, S, F>) {
|
||||||
|
const {
|
||||||
|
children,
|
||||||
|
buttonsProps,
|
||||||
|
displayLabel,
|
||||||
|
hasDescription,
|
||||||
|
hasToolbar,
|
||||||
|
uiSchema,
|
||||||
|
registry,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const uiOptions = getUiOptions<T, S, F>(uiSchema);
|
||||||
|
const ArrayFieldItemButtonsTemplate = getTemplate<
|
||||||
|
"ArrayFieldItemButtonsTemplate",
|
||||||
|
T,
|
||||||
|
S,
|
||||||
|
F
|
||||||
|
>("ArrayFieldItemButtonsTemplate", registry, uiOptions);
|
||||||
|
|
||||||
|
const margin = hasDescription ? -6 : 22;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="mb-2 flex w-full flex-row items-start gap-2">
|
||||||
|
<div className="min-w-0 flex-1">{children}</div>
|
||||||
|
{hasToolbar && (
|
||||||
|
<div
|
||||||
|
className="flex shrink-0 items-start justify-end gap-2"
|
||||||
|
style={{
|
||||||
|
marginLeft: "5px",
|
||||||
|
marginTop: displayLabel ? `${margin}px` : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrayFieldItemButtonsTemplate {...buttonsProps} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ArrayFieldItemTemplate;
|
||||||
@ -33,7 +33,7 @@ export function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
|
|||||||
<div
|
<div
|
||||||
key={element.key || index}
|
key={element.key || index}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-start gap-2",
|
"flex w-full items-start gap-2",
|
||||||
!isSimpleType && "flex-col",
|
!isSimpleType && "flex-col",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -43,11 +43,11 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
|||||||
|
|
||||||
// Get UI options
|
// Get UI options
|
||||||
const uiOptions = uiSchema?.["ui:options"] || {};
|
const uiOptions = uiSchema?.["ui:options"] || {};
|
||||||
|
|
||||||
|
// Determine field characteristics
|
||||||
const isAdvanced = uiOptions.advanced === true;
|
const isAdvanced = uiOptions.advanced === true;
|
||||||
|
|
||||||
// Boolean fields (switches) render label inline
|
|
||||||
const isBoolean = schema.type === "boolean";
|
const isBoolean = schema.type === "boolean";
|
||||||
|
const isObjectField = schema.type === "object";
|
||||||
const isNullableUnion = isNullableUnionSchema(schema as StrictRJSFSchema);
|
const isNullableUnion = isNullableUnionSchema(schema as StrictRJSFSchema);
|
||||||
const suppressMultiSchema =
|
const suppressMultiSchema =
|
||||||
(uiSchema?.["ui:options"] as Record<string, unknown> | undefined)
|
(uiSchema?.["ui:options"] as Record<string, unknown> | undefined)
|
||||||
@ -144,7 +144,7 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{finalDescription && !isMultiSchemaWrapper && (
|
{finalDescription && !isMultiSchemaWrapper && !isObjectField && (
|
||||||
<p className="text-xs text-muted-foreground">{finalDescription}</p>
|
<p className="text-xs text-muted-foreground">{finalDescription}</p>
|
||||||
)}
|
)}
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Object Field Template - renders nested object fields
|
// Object Field Template - renders nested object fields with i18n support
|
||||||
import type { ObjectFieldTemplateProps } from "@rjsf/utils";
|
import type { ObjectFieldTemplateProps } from "@rjsf/utils";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
@ -14,21 +14,15 @@ import { cn } from "@/lib/utils";
|
|||||||
|
|
||||||
export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
||||||
const { title, description, properties, uiSchema, registry, schema } = props;
|
const { title, description, properties, uiSchema, registry, schema } = props;
|
||||||
const { idSchema } = props as ObjectFieldTemplateProps & {
|
type FormContext = { i18nNamespace?: string };
|
||||||
idSchema?: { $id?: string };
|
const formContext = registry?.formContext as FormContext | undefined;
|
||||||
};
|
|
||||||
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 = idSchema?.$id === "root" || registry?.rootSchema === schema;
|
const isRoot = registry?.rootSchema === schema;
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
|
|
||||||
const { t } = useTranslation([
|
const { t } = useTranslation([formContext?.i18nNamespace || "common"]);
|
||||||
(formContext?.i18nNamespace as string | undefined) || "common",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const groupDefinitions =
|
const groupDefinitions =
|
||||||
(uiSchema?.["ui:groups"] as Record<string, string[]> | undefined) || {};
|
(uiSchema?.["ui:groups"] as Record<string, string[]> | undefined) || {};
|
||||||
@ -51,6 +45,52 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
|||||||
const toTitle = (value: string) =>
|
const toTitle = (value: string) =>
|
||||||
value.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
value.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
||||||
|
|
||||||
|
// Get the property name from the field path (e.g., "alerts" from path)
|
||||||
|
const fieldPathId = (
|
||||||
|
props as { fieldPathId?: { path?: (string | number)[] } }
|
||||||
|
).fieldPathId;
|
||||||
|
let propertyName: string | undefined;
|
||||||
|
const path = fieldPathId?.path;
|
||||||
|
if (path) {
|
||||||
|
for (let i = path.length - 1; i >= 0; i -= 1) {
|
||||||
|
const segment = path[i];
|
||||||
|
if (typeof segment === "string") {
|
||||||
|
propertyName = segment;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try i18n translation, fall back to schema or original values
|
||||||
|
const i18nNs = formContext?.i18nNamespace;
|
||||||
|
|
||||||
|
let inferredLabel: string | undefined;
|
||||||
|
if (i18nNs && propertyName) {
|
||||||
|
const translated = t(`${propertyName}.label`, {
|
||||||
|
ns: i18nNs,
|
||||||
|
defaultValue: "",
|
||||||
|
});
|
||||||
|
inferredLabel = translated || undefined;
|
||||||
|
}
|
||||||
|
const schemaTitle = schema?.title;
|
||||||
|
const fallbackLabel =
|
||||||
|
title || schemaTitle || (propertyName ? toTitle(propertyName) : undefined);
|
||||||
|
inferredLabel = inferredLabel ?? fallbackLabel;
|
||||||
|
|
||||||
|
let inferredDescription: string | undefined;
|
||||||
|
if (i18nNs && propertyName) {
|
||||||
|
const translated = t(`${propertyName}.description`, {
|
||||||
|
ns: i18nNs,
|
||||||
|
defaultValue: "",
|
||||||
|
});
|
||||||
|
inferredDescription = translated || undefined;
|
||||||
|
}
|
||||||
|
const schemaDescription = schema?.description;
|
||||||
|
const fallbackDescription =
|
||||||
|
(typeof description === "string" ? description : undefined) ||
|
||||||
|
schemaDescription;
|
||||||
|
inferredDescription = inferredDescription ?? fallbackDescription;
|
||||||
|
|
||||||
const renderGroupedFields = (items: (typeof properties)[number][]) => {
|
const renderGroupedFields = (items: (typeof properties)[number][]) => {
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
return null;
|
return null;
|
||||||
@ -142,16 +182,16 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
|||||||
|
|
||||||
// Nested objects render as collapsible cards
|
// Nested objects render as collapsible cards
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card className="w-full">
|
||||||
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<CardHeader className="cursor-pointer transition-colors hover:bg-muted/50">
|
<CardHeader className="cursor-pointer transition-colors hover:bg-muted/50">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-base">{title}</CardTitle>
|
<CardTitle className="text-sm">{inferredLabel}</CardTitle>
|
||||||
{description && (
|
{inferredDescription && (
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
{description}
|
{inferredDescription}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6,9 +6,13 @@ import { useCallback } from "react";
|
|||||||
export function ArrayAsTextWidget(props: WidgetProps) {
|
export function ArrayAsTextWidget(props: WidgetProps) {
|
||||||
const { value, onChange, disabled, readonly, placeholder } = props;
|
const { value, onChange, disabled, readonly, placeholder } = props;
|
||||||
|
|
||||||
// Convert array to space-separated string
|
// Convert array or string to text
|
||||||
const textValue =
|
let textValue = "";
|
||||||
Array.isArray(value) && value.length > 0 ? value.join(" ") : "";
|
if (typeof value === "string" && value.length > 0) {
|
||||||
|
textValue = value;
|
||||||
|
} else if (Array.isArray(value) && value.length > 0) {
|
||||||
|
textValue = value.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
|||||||
@ -325,7 +325,7 @@ const CameraConfigContent = memo(function CameraConfigContent({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-1 gap-6 overflow-hidden">
|
<div className="flex flex-1 gap-6 overflow-hidden">
|
||||||
{/* Section Navigation */}
|
{/* Section Navigation */}
|
||||||
<nav className="w-48 shrink-0">
|
<nav className="w-64 shrink-0">
|
||||||
<ul className="space-y-1">
|
<ul className="space-y-1">
|
||||||
{sections.map((section) => {
|
{sections.map((section) => {
|
||||||
const isOverridden = overriddenSections.includes(section.key);
|
const isOverridden = overriddenSections.includes(section.key);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user