mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-10 10:33:11 +03:00
fix restart fields
This commit is contained in:
parent
f90acd9b71
commit
6cd914951d
@ -314,7 +314,7 @@ class FrigateConfig(FrigateBaseModel):
|
||||
environment_vars: EnvVars = Field(
|
||||
default_factory=dict,
|
||||
title="Environment variables",
|
||||
description="Key/value pairs of environment variables to set for the Frigate process.",
|
||||
description="Key/value pairs of environment variables to set for the Frigate process in Home Assistant OS. Non-HAOS users must use Docker environment variable configuration instead.",
|
||||
)
|
||||
logger: LoggerConfig = Field(
|
||||
default_factory=LoggerConfig,
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
},
|
||||
"environment_vars": {
|
||||
"label": "Environment variables",
|
||||
"description": "Key/value pairs of environment variables to set for the Frigate process."
|
||||
"description": "Key/value pairs of environment variables to set for the Frigate process in Home Assistant OS. Non-HAOS users must use Docker environment variable configuration instead."
|
||||
},
|
||||
"logger": {
|
||||
"label": "Logging",
|
||||
|
||||
@ -1246,6 +1246,7 @@
|
||||
"selectPreset": "Select preset",
|
||||
"manualPlaceholder": "Enter FFmpeg arguments"
|
||||
},
|
||||
"restartRequiredField": "Restart required",
|
||||
"restartRequiredFooter": "Configuration changed - Restart required",
|
||||
"sections": {
|
||||
"detect": "Detection",
|
||||
|
||||
@ -7,7 +7,7 @@ export const DESCRIPTION_CLASS_NAME = "text-sm text-muted-foreground";
|
||||
export const CONTROL_COLUMN_CLASS_NAME = "w-full md:max-w-2xl";
|
||||
|
||||
type SettingsGroupCardProps = {
|
||||
title: string;
|
||||
title: string | ReactNode;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
|
||||
@ -24,6 +24,19 @@ const audio: SectionConfigOverrides = {
|
||||
},
|
||||
},
|
||||
},
|
||||
global: {
|
||||
restartRequired: [
|
||||
"enabled",
|
||||
"listen",
|
||||
"filters",
|
||||
"min_volume",
|
||||
"max_not_heard",
|
||||
"num_threads",
|
||||
],
|
||||
},
|
||||
camera: {
|
||||
restartRequired: ["num_threads"],
|
||||
},
|
||||
};
|
||||
|
||||
export default audio;
|
||||
|
||||
@ -16,14 +16,7 @@ const detect: SectionConfigOverrides = {
|
||||
"threshold",
|
||||
"max_frames",
|
||||
],
|
||||
restartRequired: [
|
||||
"width",
|
||||
"height",
|
||||
"fps",
|
||||
"min_initialized",
|
||||
"max_disappeared",
|
||||
"stationary",
|
||||
],
|
||||
restartRequired: [],
|
||||
fieldGroups: {
|
||||
resolution: ["enabled", "width", "height", "fps"],
|
||||
tracking: ["min_initialized", "max_disappeared"],
|
||||
@ -36,6 +29,21 @@ const detect: SectionConfigOverrides = {
|
||||
"stationary",
|
||||
],
|
||||
},
|
||||
global: {
|
||||
restartRequired: [
|
||||
"enabled",
|
||||
"width",
|
||||
"height",
|
||||
"fps",
|
||||
"min_initialized",
|
||||
"max_disappeared",
|
||||
"annotation_offset",
|
||||
"stationary",
|
||||
],
|
||||
},
|
||||
camera: {
|
||||
restartRequired: ["width", "height", "min_initialized", "max_disappeared"],
|
||||
},
|
||||
};
|
||||
|
||||
export default detect;
|
||||
|
||||
@ -10,7 +10,6 @@ const detectorHiddenFields = [
|
||||
const detectors: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/object_detectors",
|
||||
restartRequired: ["*.type", "*.model", "*.model_path"],
|
||||
fieldOrder: [],
|
||||
advancedFields: [],
|
||||
hiddenFields: detectorHiddenFields,
|
||||
|
||||
@ -3,7 +3,6 @@ import type { SectionConfigOverrides } from "./types";
|
||||
const environmentVars: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/advanced#environment_vars",
|
||||
restartRequired: [],
|
||||
fieldOrder: [],
|
||||
advancedFields: [],
|
||||
uiSchema: {
|
||||
|
||||
@ -96,6 +96,16 @@ const ffmpeg: SectionConfigOverrides = {
|
||||
},
|
||||
},
|
||||
global: {
|
||||
restartRequired: [
|
||||
"path",
|
||||
"global_args",
|
||||
"hwaccel_args",
|
||||
"input_args",
|
||||
"output_args",
|
||||
"retry_interval",
|
||||
"apple_compatibility",
|
||||
"gpu",
|
||||
],
|
||||
fieldOrder: [
|
||||
"path",
|
||||
"global_args",
|
||||
@ -127,6 +137,19 @@ const ffmpeg: SectionConfigOverrides = {
|
||||
},
|
||||
},
|
||||
},
|
||||
camera: {
|
||||
restartRequired: [
|
||||
"inputs",
|
||||
"path",
|
||||
"global_args",
|
||||
"hwaccel_args",
|
||||
"input_args",
|
||||
"output_args",
|
||||
"retry_interval",
|
||||
"apple_compatibility",
|
||||
"gpu",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default ffmpeg;
|
||||
|
||||
@ -10,8 +10,12 @@ const live: SectionConfigOverrides = {
|
||||
advancedFields: ["height", "quality"],
|
||||
},
|
||||
global: {
|
||||
restartRequired: ["stream_name", "height", "quality"],
|
||||
hiddenFields: ["streams"],
|
||||
},
|
||||
camera: {
|
||||
restartRequired: ["height", "quality"],
|
||||
},
|
||||
};
|
||||
|
||||
export default live;
|
||||
|
||||
@ -28,6 +28,22 @@ const motion: SectionConfigOverrides = {
|
||||
"mqtt_off_delay",
|
||||
],
|
||||
},
|
||||
global: {
|
||||
restartRequired: [
|
||||
"enabled",
|
||||
"threshold",
|
||||
"lightning_threshold",
|
||||
"improve_contrast",
|
||||
"contour_area",
|
||||
"delta_alpha",
|
||||
"frame_alpha",
|
||||
"frame_height",
|
||||
"mqtt_off_delay",
|
||||
],
|
||||
},
|
||||
camera: {
|
||||
restartRequired: ["frame_height"],
|
||||
},
|
||||
};
|
||||
|
||||
export default motion;
|
||||
|
||||
@ -83,6 +83,7 @@ const objects: SectionConfigOverrides = {
|
||||
},
|
||||
},
|
||||
global: {
|
||||
restartRequired: ["track", "alert", "detect", "filters", "genai"],
|
||||
hiddenFields: [
|
||||
"enabled_in_config",
|
||||
"mask",
|
||||
@ -95,6 +96,9 @@ const objects: SectionConfigOverrides = {
|
||||
"genai.required_zones",
|
||||
],
|
||||
},
|
||||
camera: {
|
||||
restartRequired: [],
|
||||
},
|
||||
};
|
||||
|
||||
export default objects;
|
||||
|
||||
@ -3,7 +3,15 @@ import type { SectionConfigOverrides } from "./types";
|
||||
const onvif: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/cameras#setting-up-camera-ptz-controls",
|
||||
restartRequired: [],
|
||||
restartRequired: [
|
||||
"host",
|
||||
"port",
|
||||
"user",
|
||||
"password",
|
||||
"tls_insecure",
|
||||
"ignore_time_mismatch",
|
||||
"autotracking.calibrate_on_startup",
|
||||
],
|
||||
fieldOrder: [
|
||||
"host",
|
||||
"port",
|
||||
|
||||
@ -28,6 +28,21 @@ const record: SectionConfigOverrides = {
|
||||
},
|
||||
},
|
||||
},
|
||||
global: {
|
||||
restartRequired: [
|
||||
"enabled",
|
||||
"expire_interval",
|
||||
"continuous",
|
||||
"motion",
|
||||
"alerts",
|
||||
"detections",
|
||||
"preview",
|
||||
"export",
|
||||
],
|
||||
},
|
||||
camera: {
|
||||
restartRequired: [],
|
||||
},
|
||||
};
|
||||
|
||||
export default record;
|
||||
|
||||
@ -3,6 +3,7 @@ import type { SectionConfigOverrides } from "./types";
|
||||
const review: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/review",
|
||||
restartRequired: [],
|
||||
fieldOrder: ["alerts", "detections", "genai"],
|
||||
fieldGroups: {},
|
||||
hiddenFields: [
|
||||
@ -42,6 +43,12 @@ const review: SectionConfigOverrides = {
|
||||
},
|
||||
},
|
||||
},
|
||||
global: {
|
||||
restartRequired: ["alerts", "detections", "genai"],
|
||||
},
|
||||
camera: {
|
||||
restartRequired: [],
|
||||
},
|
||||
};
|
||||
|
||||
export default review;
|
||||
|
||||
@ -27,8 +27,19 @@ const snapshots: SectionConfigOverrides = {
|
||||
},
|
||||
},
|
||||
global: {
|
||||
restartRequired: [
|
||||
"enabled",
|
||||
"bounding_box",
|
||||
"crop",
|
||||
"quality",
|
||||
"timestamp",
|
||||
"retain",
|
||||
],
|
||||
hiddenFields: ["enabled_in_config", "required_zones"],
|
||||
},
|
||||
camera: {
|
||||
restartRequired: [],
|
||||
},
|
||||
};
|
||||
|
||||
export default snapshots;
|
||||
|
||||
@ -16,6 +16,12 @@ const timestampStyle: SectionConfigOverrides = {
|
||||
},
|
||||
},
|
||||
},
|
||||
global: {
|
||||
restartRequired: ["position", "format", "color", "thickness", "effect"],
|
||||
},
|
||||
camera: {
|
||||
restartRequired: [],
|
||||
},
|
||||
};
|
||||
|
||||
export default timestampStyle;
|
||||
|
||||
@ -514,13 +514,6 @@ export function ConfigSection({
|
||||
update_topic: updateTopic,
|
||||
config_data: configData,
|
||||
});
|
||||
// log save to console for debugging
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Saved config data:", {
|
||||
[basePath]: sanitizedOverrides,
|
||||
update_topic: updateTopic,
|
||||
requires_restart: needsRestart ? 1 : 0,
|
||||
});
|
||||
|
||||
if (needsRestart) {
|
||||
statusBar?.addMessage(
|
||||
@ -628,23 +621,11 @@ export function ConfigSection({
|
||||
const configData = buildConfigDataForPath(basePath, "");
|
||||
|
||||
await axios.put("config/set", {
|
||||
requires_restart: requiresRestart ? 0 : 1,
|
||||
requires_restart: requiresRestart ? 1 : 0,
|
||||
update_topic: updateTopic,
|
||||
config_data: configData,
|
||||
});
|
||||
|
||||
// log reset to console for debugging
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
level === "global"
|
||||
? "Reset to defaults for path:"
|
||||
: "Reset to global config for path:",
|
||||
basePath,
|
||||
{
|
||||
update_topic: updateTopic,
|
||||
requires_restart: requiresRestart ? 0 : 1,
|
||||
},
|
||||
);
|
||||
toast.success(
|
||||
t("toast.resetSuccess", {
|
||||
ns: "views/settings",
|
||||
@ -815,6 +796,8 @@ export function ConfigSection({
|
||||
sectionDocs: sectionConfig.sectionDocs,
|
||||
fieldDocs: sectionConfig.fieldDocs,
|
||||
hiddenFields: sectionConfig.hiddenFields,
|
||||
restartRequired: sectionConfig.restartRequired,
|
||||
requiresRestart,
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@ -18,6 +18,8 @@ import {
|
||||
import { applySchemaDefaults } from "@/lib/config-schema";
|
||||
import { cn, isJsonObject, mergeUiSchema } from "@/lib/utils";
|
||||
import { ConfigFormContext, JsonObject } from "@/types/configForm";
|
||||
import { requiresRestartForFieldPath } from "@/utils/configUtil";
|
||||
import RestartRequiredIndicator from "@/components/indicators/RestartRequiredIndicator";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Collapsible,
|
||||
@ -158,6 +160,8 @@ export function DetectorHardwareField(props: FieldProps) {
|
||||
const { t: fallbackT } = useTranslation(["common", configNamespace]);
|
||||
const t = formContext?.t ?? fallbackT;
|
||||
const sectionPrefix = formContext?.sectionI18nPrefix ?? "detectors";
|
||||
const restartRequired = formContext?.restartRequired;
|
||||
const defaultRequiresRestart = formContext?.requiresRestart ?? true;
|
||||
|
||||
const options =
|
||||
(uiSchema?.["ui:options"] as DetectorHardwareFieldOptions | undefined) ??
|
||||
@ -322,6 +326,24 @@ export function DetectorHardwareField(props: FieldProps) {
|
||||
[t, sectionPrefix, configNamespace],
|
||||
);
|
||||
|
||||
const shouldShowRestartForPath = useCallback(
|
||||
(path: Array<string | number>) =>
|
||||
requiresRestartForFieldPath(
|
||||
path,
|
||||
restartRequired,
|
||||
defaultRequiresRestart,
|
||||
),
|
||||
[defaultRequiresRestart, restartRequired],
|
||||
);
|
||||
|
||||
const renderRestartIcon = (isRequired: boolean) => {
|
||||
if (!isRequired) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <RestartRequiredIndicator className="ml-2" />;
|
||||
};
|
||||
|
||||
const isSingleInstanceType = useCallback(
|
||||
(type: string) => !multiInstanceSet.has(type),
|
||||
[multiInstanceSet],
|
||||
@ -646,6 +668,10 @@ export function DetectorHardwareField(props: FieldProps) {
|
||||
const typeDescription = type ? getTypeDescription(type) : "";
|
||||
const isOpen = openKeys.has(key);
|
||||
const renameDraft = renameDrafts[key] ?? key;
|
||||
const detectorPath = [...fieldPathId.path, key];
|
||||
const detectorTypePath = [...detectorPath, "type"];
|
||||
const detectorTypeRequiresRestart =
|
||||
shouldShowRestartForPath(detectorTypePath);
|
||||
|
||||
return (
|
||||
<div key={key} className="rounded-lg border bg-card">
|
||||
@ -680,8 +706,9 @@ export function DetectorHardwareField(props: FieldProps) {
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<div>
|
||||
<div className="text-sm font-medium">
|
||||
<div className="flex items-center text-sm font-medium">
|
||||
{typeLabel}
|
||||
{renderRestartIcon(detectorTypeRequiresRestart)}
|
||||
<span className="ml-2 text-xs text-muted-foreground">
|
||||
{key}
|
||||
</span>
|
||||
@ -707,7 +734,7 @@ export function DetectorHardwareField(props: FieldProps) {
|
||||
<div className="space-y-4 border-t p-4">
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<div className="space-y-2">
|
||||
<Label>
|
||||
<Label className="flex items-center">
|
||||
{t("label.ID", {
|
||||
ns: "common",
|
||||
defaultValue: "ID",
|
||||
@ -746,7 +773,7 @@ export function DetectorHardwareField(props: FieldProps) {
|
||||
)}
|
||||
</div>
|
||||
<div className="col-span-3 space-y-2">
|
||||
<Label>
|
||||
<Label className="flex items-center">
|
||||
{t("detectors.type.label", {
|
||||
ns: configNamespace,
|
||||
defaultValue: "Type",
|
||||
|
||||
@ -16,6 +16,8 @@ import { ConfigFormContext } from "@/types/configForm";
|
||||
import { Link } from "react-router-dom";
|
||||
import { LuExternalLink } from "react-icons/lu";
|
||||
import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||
import { requiresRestartForFieldPath } from "@/utils/configUtil";
|
||||
import RestartRequiredIndicator from "@/components/indicators/RestartRequiredIndicator";
|
||||
import {
|
||||
buildTranslationPath,
|
||||
getFilterObjectLabel,
|
||||
@ -196,6 +198,13 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
const fieldDocsUrl = fieldDocsPath
|
||||
? getLocaleDocUrl(fieldDocsPath)
|
||||
: undefined;
|
||||
const restartRequired = formContext?.restartRequired;
|
||||
const defaultRequiresRestart = formContext?.requiresRestart ?? true;
|
||||
const fieldRequiresRestart = requiresRestartForFieldPath(
|
||||
normalizedFieldPath,
|
||||
restartRequired,
|
||||
defaultRequiresRestart,
|
||||
);
|
||||
|
||||
// Use schema title/description as primary source (from JSON Schema)
|
||||
const schemaTitle = schema.title;
|
||||
@ -449,6 +458,7 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
>
|
||||
{finalLabel}
|
||||
{required && <span className="ml-1 text-destructive">*</span>}
|
||||
{fieldRequiresRestart && <RestartRequiredIndicator className="ml-2" />}
|
||||
</Label>
|
||||
);
|
||||
};
|
||||
@ -465,6 +475,7 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
>
|
||||
{finalLabel}
|
||||
{required && <span className="ml-1 text-destructive">*</span>}
|
||||
{fieldRequiresRestart && <RestartRequiredIndicator className="ml-2" />}
|
||||
</Label>
|
||||
);
|
||||
};
|
||||
@ -485,6 +496,7 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
>
|
||||
{finalLabel}
|
||||
{required && <span className="ml-1 text-destructive">*</span>}
|
||||
{fieldRequiresRestart && <RestartRequiredIndicator className="ml-2" />}
|
||||
</Label>
|
||||
);
|
||||
};
|
||||
|
||||
@ -8,10 +8,12 @@ import {
|
||||
} from "@/components/ui/collapsible";
|
||||
import { Children, useState, useEffect, useRef } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import RestartRequiredIndicator from "@/components/indicators/RestartRequiredIndicator";
|
||||
import { LuChevronDown, LuChevronRight } from "react-icons/lu";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getTranslatedLabel } from "@/utils/i18n";
|
||||
import { requiresRestartForFieldPath } from "@/utils/configUtil";
|
||||
import { ConfigFormContext } from "@/types/configForm";
|
||||
import {
|
||||
buildTranslationPath,
|
||||
@ -44,6 +46,8 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
||||
const baselineFormData = formContext?.baselineFormData;
|
||||
const hiddenFields = formContext?.hiddenFields;
|
||||
const fieldPath = props.fieldPathId.path;
|
||||
const restartRequired = formContext?.restartRequired;
|
||||
const defaultRequiresRestart = formContext?.requiresRestart ?? true;
|
||||
|
||||
// Strip fields from an object that should be excluded from modification
|
||||
// detection: fields listed in hiddenFields (stripped from baseline by
|
||||
@ -164,6 +168,11 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
||||
"views/settings",
|
||||
"common",
|
||||
]);
|
||||
const objectRequiresRestart = requiresRestartForFieldPath(
|
||||
fieldPath,
|
||||
restartRequired,
|
||||
defaultRequiresRestart,
|
||||
);
|
||||
|
||||
const domain = getDomainFromNamespace(formContext?.i18nNamespace);
|
||||
|
||||
@ -438,11 +447,14 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
||||
<div>
|
||||
<CardTitle
|
||||
className={cn(
|
||||
"text-sm",
|
||||
"flex items-center text-sm",
|
||||
hasModifiedDescendants && "text-danger",
|
||||
)}
|
||||
>
|
||||
{inferredLabel}
|
||||
{objectRequiresRestart && (
|
||||
<RestartRequiredIndicator className="ml-2" />
|
||||
)}
|
||||
</CardTitle>
|
||||
{inferredDescription && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
|
||||
32
web/src/components/indicators/RestartRequiredIndicator.tsx
Normal file
32
web/src/components/indicators/RestartRequiredIndicator.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LuRefreshCcw } from "react-icons/lu";
|
||||
|
||||
type RestartRequiredIndicatorProps = {
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
};
|
||||
|
||||
export default function RestartRequiredIndicator({
|
||||
className,
|
||||
iconClassName,
|
||||
}: RestartRequiredIndicatorProps) {
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
const restartRequiredLabel = t("configForm.restartRequiredField", {
|
||||
ns: "views/settings",
|
||||
defaultValue: "Restart required",
|
||||
});
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
title={restartRequiredLabel}
|
||||
aria-label={restartRequiredLabel}
|
||||
>
|
||||
<LuRefreshCcw className={cn("size-3", iconClassName)} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@ -38,6 +38,8 @@ export type ConfigFormContext = {
|
||||
sectionI18nPrefix?: string;
|
||||
sectionDocs?: string;
|
||||
fieldDocs?: Record<string, string>;
|
||||
restartRequired?: string[];
|
||||
requiresRestart?: boolean;
|
||||
t?: (key: string, options?: Record<string, unknown>) => string;
|
||||
renderers?: Record<string, RendererComponent>;
|
||||
};
|
||||
|
||||
@ -197,6 +197,44 @@ export function buildConfigDataForPath(
|
||||
// used; an empty array means "never restart"; otherwise the function checks
|
||||
// if any of the listed field paths are present in the overrides object.
|
||||
|
||||
function hasMatchAtPath(value: unknown, pathSegments: string[]): boolean {
|
||||
if (pathSegments.length === 0) {
|
||||
return value !== undefined;
|
||||
}
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [segment, ...rest] = pathSegments;
|
||||
|
||||
if (segment === "*") {
|
||||
if (Array.isArray(value)) {
|
||||
return value.some((item) => hasMatchAtPath(item, rest));
|
||||
}
|
||||
|
||||
if (isJsonObject(value)) {
|
||||
return Object.values(value).some((item) => hasMatchAtPath(item, rest));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const index = Number(segment);
|
||||
if (!Number.isInteger(index)) {
|
||||
return false;
|
||||
}
|
||||
return hasMatchAtPath(value[index], rest);
|
||||
}
|
||||
|
||||
if (isJsonObject(value)) {
|
||||
return hasMatchAtPath(value[segment], rest);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function requiresRestartForOverrides(
|
||||
overrides: unknown,
|
||||
restartRequired: string[] | undefined,
|
||||
@ -211,8 +249,47 @@ export function requiresRestartForOverrides(
|
||||
if (!overrides || typeof overrides !== "object") {
|
||||
return false;
|
||||
}
|
||||
return restartRequired.some(
|
||||
(path) => get(overrides as JsonObject, path) !== undefined,
|
||||
return restartRequired.some((path) => {
|
||||
if (!path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!path.includes("*")) {
|
||||
return get(overrides as JsonObject, path) !== undefined;
|
||||
}
|
||||
|
||||
return hasMatchAtPath(overrides, path.split("."));
|
||||
});
|
||||
}
|
||||
|
||||
export function requiresRestartForFieldPath(
|
||||
fieldPath: Array<string | number>,
|
||||
restartRequired: string[] | undefined,
|
||||
defaultRequiresRestart: boolean = true,
|
||||
): boolean {
|
||||
if (restartRequired === undefined) {
|
||||
return defaultRequiresRestart;
|
||||
}
|
||||
|
||||
if (restartRequired.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fieldPath.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const probe: Record<string, unknown> = {};
|
||||
set(
|
||||
probe,
|
||||
fieldPath.map((segment) => String(segment)),
|
||||
true,
|
||||
);
|
||||
|
||||
return requiresRestartForOverrides(
|
||||
probe,
|
||||
restartRequired,
|
||||
defaultRequiresRestart,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import Heading from "@/components/ui/heading";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { SettingsGroupCard } from "@/components/card/SettingsGroupCard";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import useSWR from "swr";
|
||||
@ -13,7 +14,6 @@ import { isDesktop } from "react-device-detect";
|
||||
import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Trans } from "react-i18next";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useEnabledState } from "@/api/ws";
|
||||
|
||||
type CameraManagementViewProps = {
|
||||
@ -64,42 +64,43 @@ export default function CameraManagementView({
|
||||
position="top-center"
|
||||
closeButton
|
||||
/>
|
||||
<div className="flex size-full flex-col md:flex-row">
|
||||
<div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none">
|
||||
<div className="flex size-full space-y-6">
|
||||
<div className="scrollbar-container flex-1 overflow-y-auto pb-2">
|
||||
{viewMode === "settings" ? (
|
||||
<>
|
||||
<Heading as="h4" className="mb-2">
|
||||
<Heading as="h4" className="mb-6">
|
||||
{t("cameraManagement.title")}
|
||||
</Heading>
|
||||
<div className="my-4 flex flex-col gap-4">
|
||||
|
||||
<div className="w-full max-w-5xl space-y-6">
|
||||
<Button
|
||||
variant="select"
|
||||
onClick={() => setShowWizard(true)}
|
||||
className="flex max-w-48 items-center gap-2"
|
||||
className="mb-2 flex max-w-48 items-center gap-2"
|
||||
>
|
||||
<LuPlus className="h-4 w-4" />
|
||||
{t("cameraManagement.addCamera")}
|
||||
</Button>
|
||||
|
||||
{cameras.length > 0 && (
|
||||
<>
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
<div className="max-w-7xl space-y-4">
|
||||
<Heading as="h4" className="my-2">
|
||||
<Trans ns="views/settings">
|
||||
cameraManagement.streams.title
|
||||
</Trans>
|
||||
</Heading>
|
||||
<div className="mt-3 text-sm text-muted-foreground">
|
||||
<SettingsGroupCard
|
||||
title={
|
||||
<Trans ns="views/settings">
|
||||
cameraManagement.streams.title
|
||||
</Trans>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="max-w-md text-sm text-muted-foreground">
|
||||
<Trans ns="views/settings">
|
||||
cameraManagement.streams.desc
|
||||
</Trans>
|
||||
</div>
|
||||
|
||||
<div className="max-w-md space-y-2 rounded-lg bg-secondary p-4">
|
||||
{cameras.map((camera) => (
|
||||
<div
|
||||
key={camera}
|
||||
className="flex items-center justify-between smart-capitalize"
|
||||
className="flex flex-row items-center justify-between"
|
||||
>
|
||||
<CameraNameLabel camera={camera} />
|
||||
<CameraEnableSwitch cameraName={camera} />
|
||||
@ -107,8 +108,7 @@ export default function CameraManagementView({
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="mb-2 mt-4 flex bg-secondary" />
|
||||
</>
|
||||
</SettingsGroupCard>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user