mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-13 03:47:34 +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;
|
||||
};
|
||||
};
|
||||
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 = {
|
||||
cameras: string[];
|
||||
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",
|
||||
]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -421,15 +457,19 @@ export function prepareSectionSavePayload(opts: {
|
||||
level = "global";
|
||||
}
|
||||
|
||||
// Resolve section config
|
||||
const sectionConfig = getSectionConfig(sectionPath, level);
|
||||
// Detect profile-encoded section paths (e.g., "profiles.armed.detect")
|
||||
const profileInfo = parseProfileFromSectionPath(sectionPath);
|
||||
const schemaSection = profileInfo.actualSection;
|
||||
|
||||
// Resolve section schema
|
||||
const sectionSchema = extractSectionSchema(fullSchema, sectionPath, level);
|
||||
// Resolve section config using the actual section name (not the profile path)
|
||||
const sectionConfig = getSectionConfig(schemaSection, level);
|
||||
|
||||
// Resolve section schema using the actual section name
|
||||
const sectionSchema = extractSectionSchema(fullSchema, schemaSection, level);
|
||||
if (!sectionSchema) return null;
|
||||
|
||||
const modifiedSchema = modifySchemaForSection(
|
||||
sectionPath,
|
||||
schemaSection,
|
||||
level,
|
||||
sectionSchema,
|
||||
);
|
||||
@ -457,7 +497,7 @@ export function prepareSectionSavePayload(opts: {
|
||||
? applySchemaDefaults(modifiedSchema, {})
|
||||
: {};
|
||||
const effectiveDefaults = getEffectiveDefaultsForSection(
|
||||
sectionPath,
|
||||
schemaSection,
|
||||
level,
|
||||
modifiedSchema ?? undefined,
|
||||
schemaDefaults,
|
||||
@ -466,7 +506,7 @@ export function prepareSectionSavePayload(opts: {
|
||||
// Build overrides
|
||||
const overrides = buildOverrides(pendingData, rawData, effectiveDefaults);
|
||||
const sanitizedOverrides = sanitizeOverridesForSection(
|
||||
sectionPath,
|
||||
schemaSection,
|
||||
level,
|
||||
overrides,
|
||||
);
|
||||
@ -485,9 +525,11 @@ export function prepareSectionSavePayload(opts: {
|
||||
? `cameras.${cameraName}.${sectionPath}`
|
||||
: sectionPath;
|
||||
|
||||
// Compute updateTopic
|
||||
// Compute updateTopic — profile definitions don't trigger hot-reload
|
||||
let updateTopic: string | undefined;
|
||||
if (level === "camera" && cameraName) {
|
||||
if (profileInfo.isProfile) {
|
||||
updateTopic = undefined;
|
||||
} else if (level === "camera" && cameraName) {
|
||||
const topic = cameraUpdateTopicMap[sectionPath];
|
||||
updateTopic = topic ? `config/cameras/${cameraName}/${topic}` : undefined;
|
||||
} else if (globalCameraDefaultSections.has(sectionPath)) {
|
||||
@ -497,12 +539,14 @@ export function prepareSectionSavePayload(opts: {
|
||||
updateTopic = `config/${sectionPath}`;
|
||||
}
|
||||
|
||||
// Restart detection
|
||||
const needsRestart = requiresRestartForOverrides(
|
||||
sanitizedOverrides,
|
||||
sectionConfig.restartRequired,
|
||||
true,
|
||||
);
|
||||
// Restart detection — profile definitions never need restart
|
||||
const needsRestart = profileInfo.isProfile
|
||||
? false
|
||||
: requiresRestartForOverrides(
|
||||
sanitizedOverrides,
|
||||
sectionConfig.restartRequired,
|
||||
true,
|
||||
);
|
||||
|
||||
return {
|
||||
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