mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-18 06:08:22 +03:00
add ability to render components before and after fields
This commit is contained in:
parent
e09928a7f0
commit
3c5298e304
@ -153,7 +153,8 @@
|
|||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"modified": "Modified",
|
"modified": "Modified",
|
||||||
"overridden": "Overridden",
|
"overridden": "Overridden",
|
||||||
"resetToGlobal": "Reset to Global"
|
"resetToGlobal": "Reset to Global",
|
||||||
|
"resetToDefault": "Reset to Default"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"system": "System",
|
"system": "System",
|
||||||
|
|||||||
@ -114,7 +114,7 @@
|
|||||||
"desc": "Semantic Search in Frigate allows you to find tracked objects within your review items using either the image itself, a user-defined text description, or an automatically generated one.",
|
"desc": "Semantic Search in Frigate allows you to find tracked objects within your review items using either the image itself, a user-defined text description, or an automatically generated one.",
|
||||||
"reindexNow": {
|
"reindexNow": {
|
||||||
"label": "Reindex Now",
|
"label": "Reindex Now",
|
||||||
"desc": "Reindexing will regenerate embeddings for all tracked object. This process runs in the background and may max out your CPU and take a fair amount of time depending on the number of tracked objects you have.",
|
"desc": "Reindexing will regenerate embeddings for all tracked objects. This process runs in the background and may max out your CPU and take a fair amount of time depending on the number of tracked objects you have.",
|
||||||
"confirmTitle": "Confirm Reindexing",
|
"confirmTitle": "Confirm Reindexing",
|
||||||
"confirmDesc": "Are you sure you want to reindex all tracked object embeddings? This process will run in the background but it may max out your CPU and take a fair amount of time. You can watch the progress on the Explore page.",
|
"confirmDesc": "Are you sure you want to reindex all tracked object embeddings? This process will run in the background but it may max out your CPU and take a fair amount of time. You can watch the progress on the Explore page.",
|
||||||
"confirmButton": "Reindex",
|
"confirmButton": "Reindex",
|
||||||
|
|||||||
@ -604,6 +604,11 @@ const sectionConfigs: Record<string, SectionConfigOverrides> = {
|
|||||||
hiddenFields: [],
|
hiddenFields: [],
|
||||||
advancedFields: [],
|
advancedFields: [],
|
||||||
overrideFields: [],
|
overrideFields: [],
|
||||||
|
uiSchema: {
|
||||||
|
enabled: {
|
||||||
|
"ui:after": { render: "SemanticSearchReindex" },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
global: {
|
global: {
|
||||||
fieldOrder: ["enabled", "reindex", "model", "model_size", "device"],
|
fieldOrder: ["enabled", "reindex", "model", "model_size", "device"],
|
||||||
|
|||||||
@ -0,0 +1,106 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
import { Button, buttonVariants } from "@/components/ui/button";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
|
||||||
|
export default function SemanticSearchReindex() {
|
||||||
|
const { t } = useTranslation("views/settings");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const onReindex = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await axios.put("/reindex");
|
||||||
|
if (res.status === 202) {
|
||||||
|
toast.success(t("enrichments.semanticSearch.reindexNow.success"), {
|
||||||
|
position: "top-center",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
t("enrichments.semanticSearch.reindexNow.error", {
|
||||||
|
errorMessage: res.statusText,
|
||||||
|
}),
|
||||||
|
{ position: "top-center" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (caught) {
|
||||||
|
const error = caught as {
|
||||||
|
response?: { data?: { message?: string; detail?: string } };
|
||||||
|
};
|
||||||
|
const errorMessage =
|
||||||
|
error.response?.data?.message || error.response?.data?.detail || "";
|
||||||
|
toast.error(
|
||||||
|
t("enrichments.semanticSearch.reindexNow.error", {
|
||||||
|
errorMessage: errorMessage || undefined,
|
||||||
|
}),
|
||||||
|
{ position: "top-center" },
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<div className="flex">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsDialogOpen(true)}
|
||||||
|
disabled={isLoading}
|
||||||
|
aria-label={t("enrichments.semanticSearch.reindexNow.label")}
|
||||||
|
>
|
||||||
|
{t("enrichments.semanticSearch.reindexNow.label")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-xs text-muted-foreground">
|
||||||
|
<Trans ns="views/settings">
|
||||||
|
enrichments.semanticSearch.reindexNow.desc
|
||||||
|
</Trans>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AlertDialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
{t("enrichments.semanticSearch.reindexNow.confirmTitle")}
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
<Trans ns="views/settings">
|
||||||
|
enrichments.semanticSearch.reindexNow.confirmDesc
|
||||||
|
</Trans>
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel onClick={() => setIsDialogOpen(false)}>
|
||||||
|
{t("button.cancel", { ns: "common" })}
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
className={buttonVariants({ variant: "select" })}
|
||||||
|
onClick={async () => {
|
||||||
|
await onReindex();
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("enrichments.semanticSearch.reindexNow.confirmButton")}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
web/src/components/config-form/sectionExtras/registry.ts
Normal file
25
web/src/components/config-form/sectionExtras/registry.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import type { ComponentType } from "react";
|
||||||
|
import SemanticSearchReindex from "./SemanticSearchReindex.tsx";
|
||||||
|
|
||||||
|
export type RendererComponent = ComponentType<
|
||||||
|
Record<string, unknown> | undefined
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type SectionRenderers = Record<
|
||||||
|
string,
|
||||||
|
Record<string, RendererComponent>
|
||||||
|
>;
|
||||||
|
|
||||||
|
// Section renderers registry
|
||||||
|
// Used to register custom renderer components for specific config sections.
|
||||||
|
// Maps a section key (e.g., `semantic_search`) to a mapping of renderer
|
||||||
|
// names to React components. These names are referenced from `uiSchema`
|
||||||
|
// descriptors (e.g., `{ "ui:after": { render: "SemanticSearchReindex" } }`) and
|
||||||
|
// are resolved by `FieldTemplate` through `formContext.renderers`.
|
||||||
|
export const sectionRenderers: SectionRenderers = {
|
||||||
|
semantic_search: {
|
||||||
|
SemanticSearchReindex,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sectionRenderers;
|
||||||
@ -6,6 +6,9 @@ import useSWR from "swr";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import sectionRenderers, {
|
||||||
|
RendererComponent,
|
||||||
|
} from "@/components/config-form/sectionExtras/registry";
|
||||||
import { ConfigForm } from "../ConfigForm";
|
import { ConfigForm } from "../ConfigForm";
|
||||||
import type { UiSchema } from "@rjsf/utils";
|
import type { UiSchema } from "@rjsf/utils";
|
||||||
import {
|
import {
|
||||||
@ -51,6 +54,8 @@ export interface SectionConfig {
|
|||||||
liveValidate?: boolean;
|
liveValidate?: boolean;
|
||||||
/** Additional uiSchema overrides */
|
/** Additional uiSchema overrides */
|
||||||
uiSchema?: UiSchema;
|
uiSchema?: UiSchema;
|
||||||
|
/** Optional per-section renderers usable by FieldTemplate `ui:before`/`ui:after` */
|
||||||
|
renderers?: Record<string, RendererComponent>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseSectionProps {
|
export interface BaseSectionProps {
|
||||||
@ -418,32 +423,45 @@ export function createConfigSection({
|
|||||||
updateTopic,
|
updateTopic,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Handle reset to global - removes camera-level override by deleting the section
|
// Handle reset to global/defaults - removes camera-level override or resets global to defaults
|
||||||
const handleResetToGlobal = useCallback(async () => {
|
const handleResetToGlobal = useCallback(async () => {
|
||||||
if (level !== "camera" || !cameraName) return;
|
if (level === "camera" && !cameraName) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const basePath = `cameras.${cameraName}.${sectionPath}`;
|
const basePath =
|
||||||
|
level === "camera" && cameraName
|
||||||
|
? `cameras.${cameraName}.${sectionPath}`
|
||||||
|
: sectionPath;
|
||||||
|
|
||||||
|
// const configData = level === "global" ? schemaDefaults : "";
|
||||||
|
|
||||||
// Send empty string to delete the key from config (see update_yaml in backend)
|
|
||||||
// await axios.put("config/set", {
|
// 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: {
|
||||||
// [basePath]: "",
|
// [basePath]: configData,
|
||||||
// },
|
// },
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// log reset to console for debugging
|
// log reset to console for debugging
|
||||||
console.log("Reset to global config for path:", basePath, {
|
console.log(
|
||||||
update_topic: updateTopic,
|
level === "global"
|
||||||
requires_restart: requiresRestart ? 0 : 1,
|
? "Reset to defaults for path:"
|
||||||
});
|
: "Reset to global config for path:",
|
||||||
|
basePath,
|
||||||
|
{
|
||||||
|
update_topic: updateTopic,
|
||||||
|
requires_restart: requiresRestart ? 0 : 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
toast.success(
|
toast.success(
|
||||||
t("toast.resetSuccess", {
|
t("toast.resetSuccess", {
|
||||||
ns: "views/settings",
|
ns: "views/settings",
|
||||||
defaultValue: "Reset to global defaults",
|
defaultValue:
|
||||||
|
level === "global"
|
||||||
|
? "Reset to defaults"
|
||||||
|
: "Reset to global defaults",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -519,6 +537,8 @@ export function createConfigSection({
|
|||||||
// 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:
|
||||||
|
sectionConfig?.renderers ?? sectionRenderers?.[sectionPath],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -593,7 +613,8 @@ export function createConfigSection({
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{level === "camera" && isOverridden && (
|
{((level === "camera" && isOverridden) ||
|
||||||
|
level === "global") && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -604,10 +625,15 @@ export function createConfigSection({
|
|||||||
className="gap-2"
|
className="gap-2"
|
||||||
>
|
>
|
||||||
<LuRotateCcw className="h-4 w-4" />
|
<LuRotateCcw className="h-4 w-4" />
|
||||||
{t("button.resetToGlobal", {
|
{level === "global"
|
||||||
ns: "common",
|
? t("button.resetToDefault", {
|
||||||
defaultValue: "Reset to Global",
|
ns: "common",
|
||||||
})}
|
defaultValue: "Reset to Default",
|
||||||
|
})
|
||||||
|
: t("button.resetToGlobal", {
|
||||||
|
ns: "common",
|
||||||
|
defaultValue: "Reset to Global",
|
||||||
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -650,7 +676,7 @@ export function createConfigSection({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{level === "camera" && isOverridden && (
|
{((level === "camera" && isOverridden) || level === "global") && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -658,32 +684,43 @@ export function createConfigSection({
|
|||||||
className="gap-2"
|
className="gap-2"
|
||||||
>
|
>
|
||||||
<LuRotateCcw className="h-4 w-4" />
|
<LuRotateCcw className="h-4 w-4" />
|
||||||
{t("button.resetToGlobal", {
|
{level === "global"
|
||||||
ns: "common",
|
? t("button.resetToDefault", {
|
||||||
defaultValue: "Reset to Global",
|
ns: "common",
|
||||||
})}
|
defaultValue: "Reset to Default",
|
||||||
|
})
|
||||||
|
: t("button.resetToGlobal", {
|
||||||
|
ns: "common",
|
||||||
|
defaultValue: "Reset to Global",
|
||||||
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Reset button when title is hidden but we're at camera level with override */}
|
{/* Reset button when title is hidden but we're at camera level with override */}
|
||||||
{!shouldShowTitle && level === "camera" && isOverridden && (
|
{!shouldShowTitle &&
|
||||||
<div className="flex justify-end">
|
((level === "camera" && isOverridden) || level === "global") && (
|
||||||
<Button
|
<div className="flex justify-end">
|
||||||
variant="ghost"
|
<Button
|
||||||
size="sm"
|
variant="ghost"
|
||||||
onClick={handleResetToGlobal}
|
size="sm"
|
||||||
className="gap-2"
|
onClick={handleResetToGlobal}
|
||||||
>
|
className="gap-2"
|
||||||
<LuRotateCcw className="h-4 w-4" />
|
>
|
||||||
{t("button.resetToGlobal", {
|
<LuRotateCcw className="h-4 w-4" />
|
||||||
ns: "common",
|
{level === "global"
|
||||||
defaultValue: "Reset to Global",
|
? t("button.resetToDefault", {
|
||||||
})}
|
ns: "common",
|
||||||
</Button>
|
defaultValue: "Reset to Default",
|
||||||
</div>
|
})
|
||||||
)}
|
: t("button.resetToGlobal", {
|
||||||
|
ns: "common",
|
||||||
|
defaultValue: "Reset to Global",
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{sectionContent}
|
{sectionContent}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -30,8 +30,8 @@ export function DescriptionFieldTemplate(props: DescriptionFieldProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p id={id} className="text-sm text-muted-foreground">
|
<span id={id} className="text-sm text-muted-foreground">
|
||||||
{resolvedDescription}
|
{resolvedDescription}
|
||||||
</p>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import {
|
|||||||
getUiOptions,
|
getUiOptions,
|
||||||
ADDITIONAL_PROPERTY_FLAG,
|
ADDITIONAL_PROPERTY_FLAG,
|
||||||
} from "@rjsf/utils";
|
} from "@rjsf/utils";
|
||||||
|
import { ComponentType, ReactNode } from "react";
|
||||||
|
import { isValidElement } from "react";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@ -52,6 +54,14 @@ function humanizeKey(value: string): string {
|
|||||||
.join(" ");
|
.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FieldRenderSpec =
|
||||||
|
| ReactNode
|
||||||
|
| ComponentType<unknown>
|
||||||
|
| {
|
||||||
|
render: string;
|
||||||
|
props?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
export function FieldTemplate(props: FieldTemplateProps) {
|
export function FieldTemplate(props: FieldTemplateProps) {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
@ -264,6 +274,41 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uiOptions = getUiOptions(uiSchema);
|
const uiOptions = getUiOptions(uiSchema);
|
||||||
|
const beforeSpec = uiSchema?.["ui:before"] as FieldRenderSpec | undefined;
|
||||||
|
const afterSpec = uiSchema?.["ui:after"] as FieldRenderSpec | undefined;
|
||||||
|
|
||||||
|
const renderCustom = (spec: FieldRenderSpec | undefined) => {
|
||||||
|
if (spec === undefined || spec === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValidElement(spec) || typeof spec === "string") {
|
||||||
|
return spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof spec === "number") {
|
||||||
|
return <span>{spec}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof spec === "function") {
|
||||||
|
const SpecComponent = spec as ComponentType<unknown>;
|
||||||
|
return <SpecComponent />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof spec === "object" && "render" in spec) {
|
||||||
|
const renderKey = spec.render;
|
||||||
|
const renderers = formContext?.renderers;
|
||||||
|
const RenderComponent = renderers?.[renderKey];
|
||||||
|
if (RenderComponent) {
|
||||||
|
return <RenderComponent {...(spec.props ?? {})} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const beforeContent = renderCustom(beforeSpec);
|
||||||
|
const afterContent = renderCustom(afterSpec);
|
||||||
const WrapIfAdditionalTemplate = getTemplate(
|
const WrapIfAdditionalTemplate = getTemplate(
|
||||||
"WrapIfAdditionalTemplate",
|
"WrapIfAdditionalTemplate",
|
||||||
registry,
|
registry,
|
||||||
@ -290,64 +335,71 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
|||||||
rawErrors={rawErrors}
|
rawErrors={rawErrors}
|
||||||
hideError={false}
|
hideError={false}
|
||||||
>
|
>
|
||||||
<div
|
<div className="flex flex-col space-y-6">
|
||||||
className={cn(
|
{beforeContent}
|
||||||
"space-y-1",
|
<div
|
||||||
isAdvanced && "border-l-2 border-muted pl-4",
|
className={cn(
|
||||||
isBoolean && "flex items-center justify-between gap-4",
|
"space-y-1",
|
||||||
)}
|
isAdvanced && "border-l-2 border-muted pl-4",
|
||||||
data-field-id={translationPath}
|
isBoolean && "flex items-center justify-between gap-4",
|
||||||
>
|
|
||||||
{displayLabel &&
|
|
||||||
finalLabel &&
|
|
||||||
!isBoolean &&
|
|
||||||
!isMultiSchemaWrapper &&
|
|
||||||
!isObjectField &&
|
|
||||||
!isAdditionalProperty && (
|
|
||||||
<Label
|
|
||||||
htmlFor={id}
|
|
||||||
className={cn(
|
|
||||||
"text-sm font-medium",
|
|
||||||
errors &&
|
|
||||||
errors.props?.errors?.length > 0 &&
|
|
||||||
"text-destructive",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{finalLabel}
|
|
||||||
{required && <span className="ml-1 text-destructive">*</span>}
|
|
||||||
</Label>
|
|
||||||
)}
|
)}
|
||||||
|
data-field-id={translationPath}
|
||||||
|
>
|
||||||
|
{displayLabel &&
|
||||||
|
finalLabel &&
|
||||||
|
!isBoolean &&
|
||||||
|
!isMultiSchemaWrapper &&
|
||||||
|
!isObjectField &&
|
||||||
|
!isAdditionalProperty && (
|
||||||
|
<Label
|
||||||
|
htmlFor={id}
|
||||||
|
className={cn(
|
||||||
|
"text-sm font-medium",
|
||||||
|
errors &&
|
||||||
|
errors.props?.errors?.length > 0 &&
|
||||||
|
"text-destructive",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{finalLabel}
|
||||||
|
{required && <span className="ml-1 text-destructive">*</span>}
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
|
|
||||||
{isBoolean ? (
|
{isBoolean ? (
|
||||||
<div className="flex w-full items-center justify-between gap-4">
|
<div className="flex w-full items-center justify-between gap-4">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
{displayLabel && finalLabel && (
|
{displayLabel && finalLabel && (
|
||||||
<Label htmlFor={id} className="text-sm font-medium">
|
<Label htmlFor={id} className="text-sm font-medium">
|
||||||
{finalLabel}
|
{finalLabel}
|
||||||
{required && <span className="ml-1 text-destructive">*</span>}
|
{required && (
|
||||||
</Label>
|
<span className="ml-1 text-destructive">*</span>
|
||||||
)}
|
)}
|
||||||
{finalDescription && !isMultiSchemaWrapper && (
|
</Label>
|
||||||
|
)}
|
||||||
|
{finalDescription && !isMultiSchemaWrapper && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{finalDescription}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">{children}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{finalDescription && !isMultiSchemaWrapper && !isObjectField && (
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{finalDescription}
|
{finalDescription}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
{children}
|
)}
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{children}
|
|
||||||
{finalDescription && !isMultiSchemaWrapper && !isObjectField && (
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{finalDescription}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{errors}
|
{errors}
|
||||||
{help}
|
{help}
|
||||||
|
</div>
|
||||||
|
{afterContent}
|
||||||
</div>
|
</div>
|
||||||
</WrapIfAdditionalTemplate>
|
</WrapIfAdditionalTemplate>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -221,7 +221,7 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{groups.map((group) => (
|
{groups.map((group) => (
|
||||||
<div key={group.key} className="space-y-4">
|
<div key={group.key} className="space-y-4">
|
||||||
<div className="text-sm font-medium text-muted-foreground">
|
<div className="text-md font-medium text-primary">
|
||||||
{group.label}
|
{group.label}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import type { RendererComponent } from "@/components/config-form/sectionExtras/registry";
|
||||||
import { CameraConfig, FrigateConfig } from "@/types/frigateConfig";
|
import { CameraConfig, FrigateConfig } from "@/types/frigateConfig";
|
||||||
|
|
||||||
export type JsonPrimitive = string | number | boolean | null;
|
export type JsonPrimitive = string | number | boolean | null;
|
||||||
@ -22,4 +23,5 @@ export type ConfigFormContext = {
|
|||||||
i18nNamespace?: string;
|
i18nNamespace?: string;
|
||||||
sectionI18nPrefix?: string;
|
sectionI18nPrefix?: string;
|
||||||
t?: (key: string, options?: Record<string, unknown>) => string;
|
t?: (key: string, options?: Record<string, unknown>) => string;
|
||||||
|
renderers?: Record<string, RendererComponent>;
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user