This commit is contained in:
Josh Hawkins 2025-10-03 06:11:44 -05:00
parent 8f2cfb0544
commit 20091ea8b9
4 changed files with 285 additions and 185 deletions

View File

@ -60,6 +60,7 @@ type CreateTriggerDialogProps = {
data: string, data: string,
threshold: number, threshold: number,
actions: TriggerAction[], actions: TriggerAction[],
friendly_name: string,
) => void; ) => void;
onEdit: (trigger: Trigger) => void; onEdit: (trigger: Trigger) => void;
onCancel: () => void; onCancel: () => void;
@ -102,6 +103,7 @@ export default function CreateTriggerDialog({
!existingTriggerNames.includes(value) || value === trigger?.name, !existingTriggerNames.includes(value) || value === trigger?.name,
t("triggers.dialog.form.name.error.alreadyExists"), t("triggers.dialog.form.name.error.alreadyExists"),
), ),
friendly_name: z.string().optional(),
type: z.enum(["thumbnail", "description"]), type: z.enum(["thumbnail", "description"]),
data: z.string().min(1, t("triggers.dialog.form.content.error.required")), data: z.string().min(1, t("triggers.dialog.form.content.error.required")),
threshold: z threshold: z
@ -117,6 +119,7 @@ export default function CreateTriggerDialog({
defaultValues: { defaultValues: {
enabled: trigger?.enabled ?? true, enabled: trigger?.enabled ?? true,
name: trigger?.name ?? "", name: trigger?.name ?? "",
friendly_name: trigger?.friendly_name ?? "",
type: trigger?.type ?? "description", type: trigger?.type ?? "description",
data: trigger?.data ?? "", data: trigger?.data ?? "",
threshold: trigger?.threshold ?? 0.5, threshold: trigger?.threshold ?? 0.5,
@ -135,6 +138,7 @@ export default function CreateTriggerDialog({
values.data, values.data,
values.threshold, values.threshold,
values.actions, values.actions,
values.friendly_name ?? "",
); );
} }
}; };
@ -144,6 +148,7 @@ export default function CreateTriggerDialog({
form.reset({ form.reset({
enabled: true, enabled: true,
name: "", name: "",
friendly_name: "",
type: "description", type: "description",
data: "", data: "",
threshold: 0.5, threshold: 0.5,
@ -154,6 +159,7 @@ export default function CreateTriggerDialog({
{ {
enabled: trigger.enabled, enabled: trigger.enabled,
name: trigger.name, name: trigger.name,
friendly_name: trigger.friendly_name ?? "",
type: trigger.type, type: trigger.type,
data: trigger.data, data: trigger.data,
threshold: trigger.threshold, threshold: trigger.threshold,
@ -231,6 +237,31 @@ export default function CreateTriggerDialog({
)} )}
/> />
<FormField
control={form.control}
name="friendly_name"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("triggers.dialog.form.friendly_name.title")}
</FormLabel>
<FormControl>
<Input
placeholder={t(
"triggers.dialog.form.friendly_name.placeholder",
)}
className="h-10"
{...field}
/>
</FormControl>
<FormDescription>
{t("triggers.dialog.form.friendly_name.description")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="enabled" name="enabled"

View File

@ -237,6 +237,7 @@ export interface CameraConfig {
data: string; data: string;
threshold: number; threshold: number;
actions: TriggerAction[]; actions: TriggerAction[];
friendly_name: string;
}; };
}; };
}; };

View File

@ -8,4 +8,5 @@ export type Trigger = {
data: string; data: string;
threshold: number; threshold: number;
actions: TriggerAction[]; actions: TriggerAction[];
friendly_name?: string;
}; };

View File

@ -1,9 +1,10 @@
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { Toaster, toast } from "sonner"; import { Toaster, toast } from "sonner";
import useSWR from "swr"; import useSWR from "swr";
import axios from "axios"; import axios from "axios";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import Heading from "@/components/ui/heading"; import Heading from "@/components/ui/heading";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { import {
@ -12,7 +13,13 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { LuPlus, LuTrash, LuPencil, LuSearch } from "react-icons/lu"; import {
LuPlus,
LuTrash,
LuPencil,
LuSearch,
LuExternalLink,
} from "react-icons/lu";
import ActivityIndicator from "@/components/indicators/activity-indicator"; import ActivityIndicator from "@/components/indicators/activity-indicator";
import CreateTriggerDialog from "@/components/overlay/CreateTriggerDialog"; import CreateTriggerDialog from "@/components/overlay/CreateTriggerDialog";
import DeleteTriggerDialog from "@/components/overlay/DeleteTriggerDialog"; import DeleteTriggerDialog from "@/components/overlay/DeleteTriggerDialog";
@ -24,6 +31,8 @@ import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useTriggers } from "@/api/ws"; import { useTriggers } from "@/api/ws";
import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name"; import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name";
import { CiCircleAlert } from "react-icons/ci";
import { useDocDomain } from "@/hooks/use-doc-domain";
type ConfigSetBody = { type ConfigSetBody = {
requires_restart: number; requires_restart: number;
@ -39,6 +48,7 @@ type ConfigSetBody = {
data: string; data: string;
threshold: number; threshold: number;
actions: string[]; actions: string[];
friendly_name?: string;
} }
| ""; | "";
}; };
@ -80,6 +90,10 @@ export default function TriggerView({
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const cameraName = useCameraFriendlyName(selectedCamera); const cameraName = useCameraFriendlyName(selectedCamera);
const isSemanticSearchEnabled = config?.semantic_search?.enabled ?? false;
const { getLocaleDocUrl } = useDocDomain();
const triggers = useMemo(() => { const triggers = useMemo(() => {
if ( if (
!config || !config ||
@ -93,6 +107,7 @@ export default function TriggerView({
).map(([name, trigger]) => ({ ).map(([name, trigger]) => ({
enabled: trigger.enabled, enabled: trigger.enabled,
name, name,
friendly_name: trigger.friendly_name,
type: trigger.type, type: trigger.type,
data: trigger.data, data: trigger.data,
threshold: trigger.threshold, threshold: trigger.threshold,
@ -139,7 +154,8 @@ export default function TriggerView({
const saveToConfig = useCallback( const saveToConfig = useCallback(
(trigger: Trigger, isEdit: boolean) => { (trigger: Trigger, isEdit: boolean) => {
setIsLoading(true); setIsLoading(true);
const { enabled, name, type, data, threshold, actions } = trigger; const { enabled, name, type, data, threshold, actions, friendly_name } =
trigger;
const embeddingBody: TriggerEmbeddingBody = { type, data, threshold }; const embeddingBody: TriggerEmbeddingBody = { type, data, threshold };
const embeddingUrl = isEdit const embeddingUrl = isEdit
? `/trigger/embedding/${selectedCamera}/${name}` ? `/trigger/embedding/${selectedCamera}/${name}`
@ -162,6 +178,7 @@ export default function TriggerView({
data, data,
threshold, threshold,
actions, actions,
friendly_name,
}, },
}, },
}, },
@ -220,9 +237,21 @@ export default function TriggerView({
data: string, data: string,
threshold: number, threshold: number,
actions: TriggerAction[], actions: TriggerAction[],
friendly_name: string,
) => { ) => {
setUnsavedChanges(true); setUnsavedChanges(true);
saveToConfig({ enabled, name, type, data, threshold, actions }, false); saveToConfig(
{
enabled,
name,
type,
data,
threshold,
actions,
friendly_name,
},
false,
);
}, },
[saveToConfig, setUnsavedChanges], [saveToConfig, setUnsavedChanges],
); );
@ -359,7 +388,7 @@ export default function TriggerView({
// for adding a trigger with event id via explore context menu // for adding a trigger with event id via explore context menu
useSearchEffect("event_id", (eventId: string) => { useSearchEffect("event_id", (eventId: string) => {
if (!config || isLoading) { if (!config || isLoading || !isSemanticSearchEnabled) {
return false; return false;
} }
setShowCreate(true); setShowCreate(true);
@ -386,6 +415,41 @@ export default function TriggerView({
<div className="flex size-full flex-col md:flex-row"> <div className="flex size-full flex-col md:flex-row">
<Toaster position="top-center" closeButton={true} /> <Toaster position="top-center" closeButton={true} />
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0"> <div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0">
{!isSemanticSearchEnabled ? (
<div className="mb-5 flex flex-row items-center justify-between gap-2">
<div className="flex flex-col items-start">
<Heading as="h3" className="my-2">
{t("triggers.management.title")}
</Heading>
<p className="mb-5 text-sm text-muted-foreground">
{t("triggers.management.desc", {
camera: cameraName,
})}
</p>
<Alert variant="destructive">
<CiCircleAlert className="size-5" />
<AlertTitle>{t("triggers.semanticSearch.title")}</AlertTitle>
<AlertDescription>
<Trans ns="views/settings">
triggers.semanticSearch.desc
</Trans>
<div className="mt-3 flex items-center">
<Link
to={getLocaleDocUrl("configuration/semantic_search")}
target="_blank"
rel="noopener noreferrer"
className="inline"
>
{t("readTheDocumentation", { ns: "common" })}{" "}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
</AlertDescription>
</Alert>
</div>
</div>
) : (
<>
<div className="mb-5 flex flex-row items-center justify-between gap-2"> <div className="mb-5 flex flex-row items-center justify-between gap-2">
<div className="flex flex-col items-start"> <div className="flex flex-col items-start">
<Heading as="h3" className="my-2"> <Heading as="h3" className="my-2">
@ -443,7 +507,7 @@ export default function TriggerView({
!trigger.enabled && "opacity-60", !trigger.enabled && "opacity-60",
)} )}
> >
{trigger.name} {trigger.friendly_name || trigger.name}
</h3> </h3>
<div <div
className={cn( className={cn(
@ -473,7 +537,8 @@ export default function TriggerView({
className={cn( className={cn(
"text-sm", "text-sm",
!trigger_status?.triggers[trigger.name] !trigger_status?.triggers[trigger.name]
?.triggering_event_id && "pointer-events-none", ?.triggering_event_id &&
"pointer-events-none",
)} )}
> >
<div className="flex flex-row items-center"> <div className="flex flex-row items-center">
@ -569,6 +634,8 @@ export default function TriggerView({
</div> </div>
</div> </div>
</div> </div>
</>
)}
</div> </div>
<CreateTriggerDialog <CreateTriggerDialog
show={showCreate} show={showCreate}