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,
threshold: number,
actions: TriggerAction[],
friendly_name: string,
) => void;
onEdit: (trigger: Trigger) => void;
onCancel: () => void;
@ -102,6 +103,7 @@ export default function CreateTriggerDialog({
!existingTriggerNames.includes(value) || value === trigger?.name,
t("triggers.dialog.form.name.error.alreadyExists"),
),
friendly_name: z.string().optional(),
type: z.enum(["thumbnail", "description"]),
data: z.string().min(1, t("triggers.dialog.form.content.error.required")),
threshold: z
@ -117,6 +119,7 @@ export default function CreateTriggerDialog({
defaultValues: {
enabled: trigger?.enabled ?? true,
name: trigger?.name ?? "",
friendly_name: trigger?.friendly_name ?? "",
type: trigger?.type ?? "description",
data: trigger?.data ?? "",
threshold: trigger?.threshold ?? 0.5,
@ -135,6 +138,7 @@ export default function CreateTriggerDialog({
values.data,
values.threshold,
values.actions,
values.friendly_name ?? "",
);
}
};
@ -144,6 +148,7 @@ export default function CreateTriggerDialog({
form.reset({
enabled: true,
name: "",
friendly_name: "",
type: "description",
data: "",
threshold: 0.5,
@ -154,6 +159,7 @@ export default function CreateTriggerDialog({
{
enabled: trigger.enabled,
name: trigger.name,
friendly_name: trigger.friendly_name ?? "",
type: trigger.type,
data: trigger.data,
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
control={form.control}
name="enabled"

View File

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

View File

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

View File

@ -1,9 +1,10 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import { Toaster, toast } from "sonner";
import useSWR from "swr";
import axios from "axios";
import { Button } from "@/components/ui/button";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import Heading from "@/components/ui/heading";
import { Badge } from "@/components/ui/badge";
import {
@ -12,7 +13,13 @@ import {
TooltipProvider,
TooltipTrigger,
} 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 CreateTriggerDialog from "@/components/overlay/CreateTriggerDialog";
import DeleteTriggerDialog from "@/components/overlay/DeleteTriggerDialog";
@ -24,6 +31,8 @@ import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import { Link } from "react-router-dom";
import { useTriggers } from "@/api/ws";
import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name";
import { CiCircleAlert } from "react-icons/ci";
import { useDocDomain } from "@/hooks/use-doc-domain";
type ConfigSetBody = {
requires_restart: number;
@ -39,6 +48,7 @@ type ConfigSetBody = {
data: string;
threshold: number;
actions: string[];
friendly_name?: string;
}
| "";
};
@ -80,6 +90,10 @@ export default function TriggerView({
const [isLoading, setIsLoading] = useState(false);
const cameraName = useCameraFriendlyName(selectedCamera);
const isSemanticSearchEnabled = config?.semantic_search?.enabled ?? false;
const { getLocaleDocUrl } = useDocDomain();
const triggers = useMemo(() => {
if (
!config ||
@ -93,6 +107,7 @@ export default function TriggerView({
).map(([name, trigger]) => ({
enabled: trigger.enabled,
name,
friendly_name: trigger.friendly_name,
type: trigger.type,
data: trigger.data,
threshold: trigger.threshold,
@ -139,7 +154,8 @@ export default function TriggerView({
const saveToConfig = useCallback(
(trigger: Trigger, isEdit: boolean) => {
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 embeddingUrl = isEdit
? `/trigger/embedding/${selectedCamera}/${name}`
@ -162,6 +178,7 @@ export default function TriggerView({
data,
threshold,
actions,
friendly_name,
},
},
},
@ -220,9 +237,21 @@ export default function TriggerView({
data: string,
threshold: number,
actions: TriggerAction[],
friendly_name: string,
) => {
setUnsavedChanges(true);
saveToConfig({ enabled, name, type, data, threshold, actions }, false);
saveToConfig(
{
enabled,
name,
type,
data,
threshold,
actions,
friendly_name,
},
false,
);
},
[saveToConfig, setUnsavedChanges],
);
@ -359,7 +388,7 @@ export default function TriggerView({
// for adding a trigger with event id via explore context menu
useSearchEffect("event_id", (eventId: string) => {
if (!config || isLoading) {
if (!config || isLoading || !isSemanticSearchEnabled) {
return false;
}
setShowCreate(true);
@ -386,6 +415,41 @@ export default function TriggerView({
<div className="flex size-full flex-col md:flex-row">
<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">
{!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="flex flex-col items-start">
<Heading as="h3" className="my-2">
@ -443,7 +507,7 @@ export default function TriggerView({
!trigger.enabled && "opacity-60",
)}
>
{trigger.name}
{trigger.friendly_name || trigger.name}
</h3>
<div
className={cn(
@ -473,7 +537,8 @@ export default function TriggerView({
className={cn(
"text-sm",
!trigger_status?.triggers[trigger.name]
?.triggering_event_id && "pointer-events-none",
?.triggering_event_id &&
"pointer-events-none",
)}
>
<div className="flex flex-row items-center">
@ -569,6 +634,8 @@ export default function TriggerView({
</div>
</div>
</div>
</>
)}
</div>
<CreateTriggerDialog
show={showCreate}