mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-30 18:47:40 +03:00
UI updates and add last triggering event time and link
This commit is contained in:
parent
4cfc72dfb5
commit
f553c59517
@ -1755,3 +1755,45 @@ def delete_trigger_embedding(
|
|||||||
},
|
},
|
||||||
status_code=500,
|
status_code=500,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/triggers/status/{camera_name}",
|
||||||
|
response_model=dict,
|
||||||
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
|
)
|
||||||
|
def get_triggers_status(
|
||||||
|
camera_name: str,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
# Fetch all triggers for the specified camera
|
||||||
|
triggers = Trigger.select().where(Trigger.camera == camera_name)
|
||||||
|
|
||||||
|
# Prepare the response with trigger status
|
||||||
|
status = {
|
||||||
|
trigger.name: {
|
||||||
|
"last_triggered": trigger.last_triggered.timestamp()
|
||||||
|
if trigger.last_triggered
|
||||||
|
else None,
|
||||||
|
"triggering_event_id": trigger.triggering_event_id
|
||||||
|
if trigger.triggering_event_id
|
||||||
|
else None,
|
||||||
|
}
|
||||||
|
for trigger in triggers
|
||||||
|
}
|
||||||
|
|
||||||
|
if not status:
|
||||||
|
return JSONResponse(
|
||||||
|
content={
|
||||||
|
"success": False,
|
||||||
|
"message": f"No triggers found for camera {camera_name}",
|
||||||
|
},
|
||||||
|
status_code=404,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"success": True, "triggers": status}
|
||||||
|
except Exception as e:
|
||||||
|
return JSONResponse(
|
||||||
|
content={"success": False, "message": str(e)},
|
||||||
|
status_code=500,
|
||||||
|
)
|
||||||
|
|||||||
@ -163,6 +163,13 @@ class SemanticTriggerProcessor(PostProcessorApi):
|
|||||||
f"Trigger {trigger['name']} activated with similarity {similarity:.4f}"
|
f"Trigger {trigger['name']} activated with similarity {similarity:.4f}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Update the trigger's last_triggered and triggering_event_id
|
||||||
|
Trigger.update(
|
||||||
|
last_triggered=datetime.datetime.now(), triggering_event_id=event_id
|
||||||
|
).where(
|
||||||
|
Trigger.camera == camera, Trigger.name == trigger["name"]
|
||||||
|
).execute()
|
||||||
|
|
||||||
# Always publish MQTT message
|
# Always publish MQTT message
|
||||||
self.requestor.send_data(
|
self.requestor.send_data(
|
||||||
"triggers",
|
"triggers",
|
||||||
|
|||||||
@ -649,7 +649,7 @@
|
|||||||
"documentTitle": "Triggers",
|
"documentTitle": "Triggers",
|
||||||
"management": {
|
"management": {
|
||||||
"title": "Trigger Management",
|
"title": "Trigger Management",
|
||||||
"desc": "Manage triggers for camera '{{camera}}' to detect specific events."
|
"desc": "Manage triggers for {{camera}}. Use the thumbnail type to trigger on similar thumbnails to your selected tracked object, and the description type to trigger on similar descriptions to text you specify."
|
||||||
},
|
},
|
||||||
"addTrigger": "Add Trigger",
|
"addTrigger": "Add Trigger",
|
||||||
"table": {
|
"table": {
|
||||||
|
|||||||
@ -5,14 +5,6 @@ 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 Heading from "@/components/ui/heading";
|
import Heading from "@/components/ui/heading";
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@ -20,13 +12,16 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { LuPlus, LuTrash, LuPencil } from "react-icons/lu";
|
import { LuPlus, LuTrash, LuPencil, LuSearch } 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";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { Trigger, TriggerAction, TriggerType } from "@/types/trigger";
|
import { Trigger, TriggerAction, TriggerType } from "@/types/trigger";
|
||||||
import { useSearchEffect } from "@/hooks/use-overlay-state";
|
import { useSearchEffect } from "@/hooks/use-overlay-state";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
type ConfigSetBody = {
|
type ConfigSetBody = {
|
||||||
requires_restart: number;
|
requires_restart: number;
|
||||||
@ -70,6 +65,7 @@ export default function TriggerView({
|
|||||||
const { t } = useTranslation("views/settings");
|
const { t } = useTranslation("views/settings");
|
||||||
const { data: config, mutate: updateConfig } =
|
const { data: config, mutate: updateConfig } =
|
||||||
useSWR<FrigateConfig>("config");
|
useSWR<FrigateConfig>("config");
|
||||||
|
const { data: trigger_status } = useSWR(`/triggers/status/${selectedCamera}`);
|
||||||
const [showCreate, setShowCreate] = useState(false);
|
const [showCreate, setShowCreate] = useState(false);
|
||||||
const [showDelete, setShowDelete] = useState(false);
|
const [showDelete, setShowDelete] = useState(false);
|
||||||
const [selectedTrigger, setSelectedTrigger] = useState<Trigger | null>(null);
|
const [selectedTrigger, setSelectedTrigger] = useState<Trigger | null>(null);
|
||||||
@ -374,118 +370,146 @@ export default function TriggerView({
|
|||||||
</div>
|
</div>
|
||||||
<div className="mb-6 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
<div className="mb-6 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="scrollbar-container flex-1 overflow-hidden rounded-lg border border-border bg-background_alt">
|
<div className="scrollbar-container flex-1 overflow-hidden rounded-lg border border-border bg-background_alt">
|
||||||
<div className="h-full overflow-auto">
|
<div className="h-full overflow-auto p-0">
|
||||||
<Table>
|
{triggers.length === 0 ? (
|
||||||
<TableHeader className="sticky top-0 bg-muted/50">
|
<div className="flex h-24 items-center justify-center">
|
||||||
<TableRow>
|
<p className="text-center text-muted-foreground">
|
||||||
<TableHead className="w-[200px]">
|
{t("triggers.table.noTriggers")}
|
||||||
{t("triggers.table.name")}
|
</p>
|
||||||
</TableHead>
|
</div>
|
||||||
<TableHead>{t("triggers.table.type")}</TableHead>
|
) : (
|
||||||
<TableHead>{t("triggers.table.content")}</TableHead>
|
<div className="space-y-2">
|
||||||
<TableHead>{t("triggers.table.threshold")}</TableHead>
|
{triggers.map((trigger) => (
|
||||||
<TableHead>{t("triggers.table.actions")}</TableHead>
|
<div
|
||||||
</TableRow>
|
key={trigger.name}
|
||||||
</TableHeader>
|
className="flex items-center justify-between rounded-lg border border-border bg-background p-4 transition-opacity"
|
||||||
<TableBody>
|
>
|
||||||
{triggers.length === 0 ? (
|
<div className="min-w-0 flex-1">
|
||||||
<TableRow>
|
<h3
|
||||||
<TableCell colSpan={4} className="h-24 text-center">
|
className={cn(
|
||||||
{t("triggers.table.noTriggers")}
|
"truncate text-lg font-medium",
|
||||||
</TableCell>
|
!trigger.enabled && "opacity-60",
|
||||||
</TableRow>
|
)}
|
||||||
) : (
|
>
|
||||||
triggers.map((trigger) => (
|
|
||||||
<TableRow key={trigger.name} className="group">
|
|
||||||
<TableCell className="font-medium">
|
|
||||||
{trigger.name}
|
{trigger.name}
|
||||||
</TableCell>
|
</h3>
|
||||||
<TableCell>
|
<div
|
||||||
<Badge
|
className={cn(
|
||||||
variant={
|
"mt-1 flex flex-col gap-0.5 text-sm text-muted-foreground md:flex-row md:items-center md:gap-3",
|
||||||
trigger.type === "thumbnail"
|
!trigger.enabled && "opacity-60",
|
||||||
? "default"
|
)}
|
||||||
: "outline"
|
>
|
||||||
}
|
<div>
|
||||||
className={
|
<Badge
|
||||||
trigger.type === "thumbnail"
|
variant={
|
||||||
? "bg-primary/20 text-primary hover:bg-primary/30"
|
trigger.type === "thumbnail"
|
||||||
: ""
|
? "default"
|
||||||
}
|
: "outline"
|
||||||
|
}
|
||||||
|
className={
|
||||||
|
trigger.type === "thumbnail"
|
||||||
|
? "bg-primary/20 text-primary hover:bg-primary/30"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t(`triggers.type.${trigger.type}`)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span>{trigger.threshold.toFixed(2)} threshold</span>
|
||||||
|
|
||||||
|
<span>{trigger.actions.length} actions</span>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to={`/explore?event_id=${trigger_status?.triggers[trigger.name]?.triggering_event_id || ""}`}
|
||||||
|
className={cn(
|
||||||
|
"text-sm",
|
||||||
|
!trigger_status?.triggers[trigger.name]
|
||||||
|
?.triggering_event_id &&
|
||||||
|
"pointer-events-none opacity-60",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{t(`triggers.type.${trigger.type}`)}
|
<div className="flex flex-row items-center justify-center">
|
||||||
</Badge>
|
Last:{" "}
|
||||||
</TableCell>
|
{trigger_status &&
|
||||||
<TableCell>
|
trigger_status.triggers[trigger.name]
|
||||||
{trigger.type === "thumbnail"
|
?.last_triggered
|
||||||
? trigger.data
|
? formatUnixTimestampToDateTime(
|
||||||
: trigger.data.length > 30
|
trigger_status.triggers[trigger.name]
|
||||||
? `${trigger.data.substring(0, 30)}...`
|
?.last_triggered,
|
||||||
: trigger.data}
|
{
|
||||||
</TableCell>
|
timezone: config.ui.timezone,
|
||||||
<TableCell className="font-medium">
|
date_format:
|
||||||
{trigger.threshold.toFixed(2)}
|
config.ui.time_format == "24hour"
|
||||||
</TableCell>
|
? t(
|
||||||
<TableCell>
|
"time.formattedTimestamp2.24hour",
|
||||||
{trigger.actions
|
{
|
||||||
.map((action) => t(`triggers.actions.${action}`))
|
ns: "common",
|
||||||
.join(", ")}
|
},
|
||||||
</TableCell>
|
)
|
||||||
<TableCell className="text-right">
|
: t(
|
||||||
<TooltipProvider>
|
"time.formattedTimestamp2.12hour",
|
||||||
<div className="flex items-center justify-end gap-2">
|
{
|
||||||
<Tooltip>
|
ns: "common",
|
||||||
<TooltipTrigger asChild>
|
},
|
||||||
<Button
|
),
|
||||||
size="sm"
|
time_style: "medium",
|
||||||
variant="outline"
|
date_style: "medium",
|
||||||
className="h-8 px-2"
|
},
|
||||||
onClick={() => {
|
)
|
||||||
setSelectedTrigger(trigger);
|
: "Never"}
|
||||||
setShowCreate(true);
|
<LuSearch className="ml-2 size-3.5" />
|
||||||
}}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
<LuPencil className="size-3.5" />
|
|
||||||
<span className="ml-1.5 hidden sm:inline-block">
|
|
||||||
{t("triggers.table.edit")}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{t("triggers.table.edit")}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="destructive"
|
|
||||||
className="h-8 px-2 text-white"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedTrigger(trigger);
|
|
||||||
setShowDelete(true);
|
|
||||||
}}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
<LuTrash className="size-3.5" />
|
|
||||||
<span className="ml-1.5 hidden sm:inline-block">
|
|
||||||
{t("button.delete", { ns: "common" })}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{t("triggers.table.deleteTrigger")}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</Link>
|
||||||
</TableCell>
|
</div>
|
||||||
</TableRow>
|
</div>
|
||||||
))
|
|
||||||
)}
|
<div className="flex items-center gap-2">
|
||||||
</TableBody>
|
<TooltipProvider>
|
||||||
</Table>
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedTrigger(trigger);
|
||||||
|
setShowCreate(true);
|
||||||
|
}}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<LuPencil className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{t("triggers.table.edit")}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
className="h-8 w-8 p-0 text-white"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedTrigger(trigger);
|
||||||
|
setShowDelete(true);
|
||||||
|
}}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<LuTrash className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{t("triggers.table.deleteTrigger")}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user