mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-29 11:30:17 +03:00
add frontend profile types, color utility, and config save support
This commit is contained in:
parent
60930e50c2
commit
72b4a4ddad
@ -305,8 +305,23 @@ export interface CameraConfig {
|
|||||||
friendly_name?: string;
|
friendly_name?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
profiles?: Record<string, CameraProfileConfig>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CameraProfileConfig = {
|
||||||
|
enabled?: boolean;
|
||||||
|
audio?: Partial<CameraConfig["audio"]>;
|
||||||
|
birdseye?: Partial<CameraConfig["birdseye"]>;
|
||||||
|
detect?: Partial<CameraConfig["detect"]>;
|
||||||
|
motion?: Partial<CameraConfig["motion"]>;
|
||||||
|
notifications?: Partial<CameraConfig["notifications"]>;
|
||||||
|
objects?: Partial<CameraConfig["objects"]>;
|
||||||
|
record?: Partial<CameraConfig["record"]>;
|
||||||
|
review?: Partial<CameraConfig["review"]>;
|
||||||
|
snapshots?: Partial<CameraConfig["snapshots"]>;
|
||||||
|
zones?: Partial<CameraConfig["zones"]>;
|
||||||
|
};
|
||||||
|
|
||||||
export type CameraGroupConfig = {
|
export type CameraGroupConfig = {
|
||||||
cameras: string[];
|
cameras: string[];
|
||||||
icon: IconName;
|
icon: IconName;
|
||||||
|
|||||||
23
web/src/types/profile.ts
Normal file
23
web/src/types/profile.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
export type ProfileColor = {
|
||||||
|
bg: string;
|
||||||
|
text: string;
|
||||||
|
dot: string;
|
||||||
|
bgMuted: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProfileState = {
|
||||||
|
editingProfile: Record<string, string | null>;
|
||||||
|
newProfiles: string[];
|
||||||
|
allProfileNames: string[];
|
||||||
|
onSelectProfile: (
|
||||||
|
camera: string,
|
||||||
|
section: string,
|
||||||
|
profile: string | null,
|
||||||
|
) => void;
|
||||||
|
onAddProfile: (name: string) => void;
|
||||||
|
onDeleteProfileSection: (
|
||||||
|
camera: string,
|
||||||
|
section: string,
|
||||||
|
profile: string,
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
@ -68,6 +68,42 @@ export const globalCameraDefaultSections = new Set([
|
|||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Profile helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Sections that can appear inside a camera profile definition. */
|
||||||
|
export const PROFILE_ELIGIBLE_SECTIONS = new Set([
|
||||||
|
"audio",
|
||||||
|
"birdseye",
|
||||||
|
"detect",
|
||||||
|
"motion",
|
||||||
|
"notifications",
|
||||||
|
"objects",
|
||||||
|
"record",
|
||||||
|
"review",
|
||||||
|
"snapshots",
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a section path that may encode a profile reference.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* "detect" → { isProfile: false, actualSection: "detect" }
|
||||||
|
* "profiles.armed.detect" → { isProfile: true, profileName: "armed", actualSection: "detect" }
|
||||||
|
*/
|
||||||
|
export function parseProfileFromSectionPath(sectionPath: string): {
|
||||||
|
isProfile: boolean;
|
||||||
|
profileName?: string;
|
||||||
|
actualSection: string;
|
||||||
|
} {
|
||||||
|
const match = sectionPath.match(/^profiles\.([^.]+)\.(.+)$/);
|
||||||
|
if (match) {
|
||||||
|
return { isProfile: true, profileName: match[1], actualSection: match[2] };
|
||||||
|
}
|
||||||
|
return { isProfile: false, actualSection: sectionPath };
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// buildOverrides — pure recursive diff of current vs stored config & defaults
|
// buildOverrides — pure recursive diff of current vs stored config & defaults
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -421,15 +457,19 @@ export function prepareSectionSavePayload(opts: {
|
|||||||
level = "global";
|
level = "global";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve section config
|
// Detect profile-encoded section paths (e.g., "profiles.armed.detect")
|
||||||
const sectionConfig = getSectionConfig(sectionPath, level);
|
const profileInfo = parseProfileFromSectionPath(sectionPath);
|
||||||
|
const schemaSection = profileInfo.actualSection;
|
||||||
|
|
||||||
// Resolve section schema
|
// Resolve section config using the actual section name (not the profile path)
|
||||||
const sectionSchema = extractSectionSchema(fullSchema, sectionPath, level);
|
const sectionConfig = getSectionConfig(schemaSection, level);
|
||||||
|
|
||||||
|
// Resolve section schema using the actual section name
|
||||||
|
const sectionSchema = extractSectionSchema(fullSchema, schemaSection, level);
|
||||||
if (!sectionSchema) return null;
|
if (!sectionSchema) return null;
|
||||||
|
|
||||||
const modifiedSchema = modifySchemaForSection(
|
const modifiedSchema = modifySchemaForSection(
|
||||||
sectionPath,
|
schemaSection,
|
||||||
level,
|
level,
|
||||||
sectionSchema,
|
sectionSchema,
|
||||||
);
|
);
|
||||||
@ -457,7 +497,7 @@ export function prepareSectionSavePayload(opts: {
|
|||||||
? applySchemaDefaults(modifiedSchema, {})
|
? applySchemaDefaults(modifiedSchema, {})
|
||||||
: {};
|
: {};
|
||||||
const effectiveDefaults = getEffectiveDefaultsForSection(
|
const effectiveDefaults = getEffectiveDefaultsForSection(
|
||||||
sectionPath,
|
schemaSection,
|
||||||
level,
|
level,
|
||||||
modifiedSchema ?? undefined,
|
modifiedSchema ?? undefined,
|
||||||
schemaDefaults,
|
schemaDefaults,
|
||||||
@ -466,7 +506,7 @@ export function prepareSectionSavePayload(opts: {
|
|||||||
// Build overrides
|
// Build overrides
|
||||||
const overrides = buildOverrides(pendingData, rawData, effectiveDefaults);
|
const overrides = buildOverrides(pendingData, rawData, effectiveDefaults);
|
||||||
const sanitizedOverrides = sanitizeOverridesForSection(
|
const sanitizedOverrides = sanitizeOverridesForSection(
|
||||||
sectionPath,
|
schemaSection,
|
||||||
level,
|
level,
|
||||||
overrides,
|
overrides,
|
||||||
);
|
);
|
||||||
@ -485,9 +525,11 @@ export function prepareSectionSavePayload(opts: {
|
|||||||
? `cameras.${cameraName}.${sectionPath}`
|
? `cameras.${cameraName}.${sectionPath}`
|
||||||
: sectionPath;
|
: sectionPath;
|
||||||
|
|
||||||
// Compute updateTopic
|
// Compute updateTopic — profile definitions don't trigger hot-reload
|
||||||
let updateTopic: string | undefined;
|
let updateTopic: string | undefined;
|
||||||
if (level === "camera" && cameraName) {
|
if (profileInfo.isProfile) {
|
||||||
|
updateTopic = undefined;
|
||||||
|
} else if (level === "camera" && cameraName) {
|
||||||
const topic = cameraUpdateTopicMap[sectionPath];
|
const topic = cameraUpdateTopicMap[sectionPath];
|
||||||
updateTopic = topic ? `config/cameras/${cameraName}/${topic}` : undefined;
|
updateTopic = topic ? `config/cameras/${cameraName}/${topic}` : undefined;
|
||||||
} else if (globalCameraDefaultSections.has(sectionPath)) {
|
} else if (globalCameraDefaultSections.has(sectionPath)) {
|
||||||
@ -497,12 +539,14 @@ export function prepareSectionSavePayload(opts: {
|
|||||||
updateTopic = `config/${sectionPath}`;
|
updateTopic = `config/${sectionPath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart detection
|
// Restart detection — profile definitions never need restart
|
||||||
const needsRestart = requiresRestartForOverrides(
|
const needsRestart = profileInfo.isProfile
|
||||||
sanitizedOverrides,
|
? false
|
||||||
sectionConfig.restartRequired,
|
: requiresRestartForOverrides(
|
||||||
true,
|
sanitizedOverrides,
|
||||||
);
|
sectionConfig.restartRequired,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
basePath,
|
basePath,
|
||||||
|
|||||||
67
web/src/utils/profileColors.ts
Normal file
67
web/src/utils/profileColors.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import type { ProfileColor } from "@/types/profile";
|
||||||
|
|
||||||
|
const PROFILE_COLORS: ProfileColor[] = [
|
||||||
|
{
|
||||||
|
bg: "bg-blue-500",
|
||||||
|
text: "text-blue-500",
|
||||||
|
dot: "bg-blue-500",
|
||||||
|
bgMuted: "bg-blue-500/20",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bg: "bg-emerald-500",
|
||||||
|
text: "text-emerald-500",
|
||||||
|
dot: "bg-emerald-500",
|
||||||
|
bgMuted: "bg-emerald-500/20",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bg: "bg-amber-500",
|
||||||
|
text: "text-amber-500",
|
||||||
|
dot: "bg-amber-500",
|
||||||
|
bgMuted: "bg-amber-500/20",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bg: "bg-purple-500",
|
||||||
|
text: "text-purple-500",
|
||||||
|
dot: "bg-purple-500",
|
||||||
|
bgMuted: "bg-purple-500/20",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bg: "bg-rose-500",
|
||||||
|
text: "text-rose-500",
|
||||||
|
dot: "bg-rose-500",
|
||||||
|
bgMuted: "bg-rose-500/20",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bg: "bg-cyan-500",
|
||||||
|
text: "text-cyan-500",
|
||||||
|
dot: "bg-cyan-500",
|
||||||
|
bgMuted: "bg-cyan-500/20",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bg: "bg-orange-500",
|
||||||
|
text: "text-orange-500",
|
||||||
|
dot: "bg-orange-500",
|
||||||
|
bgMuted: "bg-orange-500/20",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bg: "bg-teal-500",
|
||||||
|
text: "text-teal-500",
|
||||||
|
dot: "bg-teal-500",
|
||||||
|
bgMuted: "bg-teal-500/20",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a deterministic color for a profile name.
|
||||||
|
*
|
||||||
|
* Colors are assigned based on sorted position among all profile names,
|
||||||
|
* so the same profile always gets the same color regardless of context.
|
||||||
|
*/
|
||||||
|
export function getProfileColor(
|
||||||
|
profileName: string,
|
||||||
|
allProfileNames: string[],
|
||||||
|
): ProfileColor {
|
||||||
|
const sorted = [...allProfileNames].sort();
|
||||||
|
const index = sorted.indexOf(profileName);
|
||||||
|
return PROFILE_COLORS[(index >= 0 ? index : 0) % PROFILE_COLORS.length];
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user