mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-01 08:32:18 +03:00
Miscellaneous Fixes (#20973)
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
CI / AMD64 Build (push) Waiting to run
This commit is contained in:
@@ -39,6 +39,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import BlurredIconButton from "@/components/button/BlurredIconButton";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
const allModelTypes = ["objects", "states"] as const;
|
||||
type ModelType = (typeof allModelTypes)[number];
|
||||
@@ -332,9 +333,7 @@ function ModelCard({ config, onClick, onUpdate, onDelete }: ModelCardProps) {
|
||||
<ImageShadowOverlay lowerClassName="h-[30%] z-0" />
|
||||
</>
|
||||
) : (
|
||||
<div className="flex size-full items-center justify-center bg-background_alt">
|
||||
<MdModelTraining className="size-16 text-muted-foreground" />
|
||||
</div>
|
||||
<Skeleton className="flex size-full items-center justify-center" />
|
||||
)}
|
||||
<div className="absolute bottom-2 left-3 text-lg text-white smart-capitalize">
|
||||
{config.name}
|
||||
|
||||
@@ -20,7 +20,14 @@ import {
|
||||
FrigateConfig,
|
||||
} from "@/types/frigateConfig";
|
||||
import { ReviewSegment } from "@/types/review";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
isDesktop,
|
||||
isMobile,
|
||||
@@ -46,6 +53,8 @@ import { useStreamingSettings } from "@/context/streaming-settings-provider";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { EmptyCard } from "@/components/card/EmptyCard";
|
||||
import { BsFillCameraVideoOffFill } from "react-icons/bs";
|
||||
import { AuthContext } from "@/context/auth-context";
|
||||
import { useIsCustomRole } from "@/hooks/use-is-custom-role";
|
||||
|
||||
type LiveDashboardViewProps = {
|
||||
cameras: CameraConfig[];
|
||||
@@ -374,10 +383,6 @@ export default function LiveDashboardView({
|
||||
onSaveMuting(true);
|
||||
};
|
||||
|
||||
if (cameras.length == 0 && !includeBirdseye) {
|
||||
return <NoCameraView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="scrollbar-container size-full select-none overflow-y-auto px-1 pt-2 md:p-2"
|
||||
@@ -439,198 +444,215 @@ export default function LiveDashboardView({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!fullscreen && events && events.length > 0 && (
|
||||
<ScrollArea>
|
||||
<TooltipProvider>
|
||||
<div className="flex items-center gap-2 px-1">
|
||||
{events.map((event) => {
|
||||
return (
|
||||
<AnimatedEventCard
|
||||
key={event.id}
|
||||
event={event}
|
||||
selectedGroup={cameraGroup}
|
||||
updateEvents={updateEvents}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
)}
|
||||
|
||||
{!cameraGroup || cameraGroup == "default" || isMobileOnly ? (
|
||||
{cameras.length == 0 && !includeBirdseye ? (
|
||||
<NoCameraView />
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"mt-2 grid grid-cols-1 gap-2 px-2 md:gap-4",
|
||||
mobileLayout == "grid" &&
|
||||
"grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4",
|
||||
isMobile && "px-0",
|
||||
)}
|
||||
>
|
||||
{includeBirdseye && birdseyeConfig?.enabled && (
|
||||
{!fullscreen && events && events.length > 0 && (
|
||||
<ScrollArea>
|
||||
<TooltipProvider>
|
||||
<div className="flex items-center gap-2 px-1">
|
||||
{events.map((event) => {
|
||||
return (
|
||||
<AnimatedEventCard
|
||||
key={event.id}
|
||||
event={event}
|
||||
selectedGroup={cameraGroup}
|
||||
updateEvents={updateEvents}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
)}
|
||||
|
||||
{!cameraGroup || cameraGroup == "default" || isMobileOnly ? (
|
||||
<>
|
||||
<div
|
||||
className={(() => {
|
||||
const aspectRatio =
|
||||
birdseyeConfig.width / birdseyeConfig.height;
|
||||
if (aspectRatio > 2) {
|
||||
return `${mobileLayout == "grid" && "col-span-2"} aspect-wide`;
|
||||
} else if (aspectRatio < 1) {
|
||||
return `${mobileLayout == "grid" && "row-span-2 h-full"} aspect-tall`;
|
||||
} else {
|
||||
return "aspect-video";
|
||||
}
|
||||
})()}
|
||||
ref={birdseyeContainerRef}
|
||||
className={cn(
|
||||
"mt-2 grid grid-cols-1 gap-2 px-2 md:gap-4",
|
||||
mobileLayout == "grid" &&
|
||||
"grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4",
|
||||
isMobile && "px-0",
|
||||
)}
|
||||
>
|
||||
<BirdseyeLivePlayer
|
||||
birdseyeConfig={birdseyeConfig}
|
||||
liveMode={birdseyeConfig.restream ? "mse" : "jsmpeg"}
|
||||
onClick={() => onSelectCamera("birdseye")}
|
||||
containerRef={birdseyeContainerRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{cameras.map((camera) => {
|
||||
let grow;
|
||||
const aspectRatio = camera.detect.width / camera.detect.height;
|
||||
if (aspectRatio > 2) {
|
||||
grow = `${mobileLayout == "grid" && "col-span-2"} aspect-wide`;
|
||||
} else if (aspectRatio < 1) {
|
||||
grow = `${mobileLayout == "grid" && "row-span-2 h-full"} aspect-tall`;
|
||||
} else {
|
||||
grow = "aspect-video";
|
||||
}
|
||||
const availableStreams = camera.live.streams || {};
|
||||
const firstStreamEntry = Object.values(availableStreams)[0] || "";
|
||||
|
||||
const streamNameFromSettings =
|
||||
currentGroupStreamingSettings?.[camera.name]?.streamName || "";
|
||||
const streamExists =
|
||||
streamNameFromSettings &&
|
||||
Object.values(availableStreams).includes(
|
||||
streamNameFromSettings,
|
||||
);
|
||||
|
||||
const streamName = streamExists
|
||||
? streamNameFromSettings
|
||||
: firstStreamEntry;
|
||||
const streamType =
|
||||
currentGroupStreamingSettings?.[camera.name]?.streamType;
|
||||
const autoLive =
|
||||
streamType !== undefined
|
||||
? streamType !== "no-streaming"
|
||||
: undefined;
|
||||
const showStillWithoutActivity =
|
||||
currentGroupStreamingSettings?.[camera.name]?.streamType !==
|
||||
"continuous";
|
||||
const useWebGL =
|
||||
currentGroupStreamingSettings?.[camera.name]
|
||||
?.compatibilityMode || false;
|
||||
return (
|
||||
<LiveContextMenu
|
||||
className={grow}
|
||||
key={camera.name}
|
||||
camera={camera.name}
|
||||
cameraGroup={cameraGroup}
|
||||
streamName={streamName}
|
||||
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
|
||||
isRestreamed={isRestreamedStates[camera.name]}
|
||||
supportsAudio={
|
||||
supportsAudioOutputStates[streamName]?.supportsAudio ??
|
||||
false
|
||||
}
|
||||
audioState={audioStates[camera.name]}
|
||||
toggleAudio={() => toggleAudio(camera.name)}
|
||||
statsState={statsStates[camera.name]}
|
||||
toggleStats={() => toggleStats(camera.name)}
|
||||
volumeState={volumeStates[camera.name] ?? 1}
|
||||
setVolumeState={(value) =>
|
||||
setVolumeStates({
|
||||
[camera.name]: value,
|
||||
})
|
||||
}
|
||||
muteAll={muteAll}
|
||||
unmuteAll={unmuteAll}
|
||||
resetPreferredLiveMode={() =>
|
||||
resetPreferredLiveMode(camera.name)
|
||||
}
|
||||
config={config}
|
||||
>
|
||||
<LivePlayer
|
||||
cameraRef={cameraRef}
|
||||
key={camera.name}
|
||||
className={`${grow} rounded-lg bg-black md:rounded-2xl`}
|
||||
windowVisible={
|
||||
windowVisible && visibleCameras.includes(camera.name)
|
||||
}
|
||||
cameraConfig={camera}
|
||||
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
|
||||
autoLive={autoLive ?? globalAutoLive}
|
||||
showStillWithoutActivity={showStillWithoutActivity ?? true}
|
||||
alwaysShowCameraName={displayCameraNames}
|
||||
useWebGL={useWebGL}
|
||||
playInBackground={false}
|
||||
showStats={statsStates[camera.name]}
|
||||
streamName={streamName}
|
||||
onClick={() => onSelectCamera(camera.name)}
|
||||
onError={(e) => handleError(camera.name, e)}
|
||||
onResetLiveMode={() => resetPreferredLiveMode(camera.name)}
|
||||
playAudio={audioStates[camera.name] ?? false}
|
||||
volume={volumeStates[camera.name]}
|
||||
/>
|
||||
</LiveContextMenu>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{isDesktop && (
|
||||
<div
|
||||
className={cn(
|
||||
"fixed",
|
||||
isDesktop && "bottom-12 lg:bottom-9",
|
||||
isMobile && "bottom-12 lg:bottom-16",
|
||||
hasScrollbar && isDesktop ? "right-6" : "right-3",
|
||||
"z-50 flex flex-row gap-2",
|
||||
)}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
{includeBirdseye && birdseyeConfig?.enabled && (
|
||||
<div
|
||||
className="cursor-pointer rounded-lg bg-secondary text-secondary-foreground opacity-60 transition-all duration-300 hover:bg-muted hover:opacity-100"
|
||||
onClick={toggleFullscreen}
|
||||
className={(() => {
|
||||
const aspectRatio =
|
||||
birdseyeConfig.width / birdseyeConfig.height;
|
||||
if (aspectRatio > 2) {
|
||||
return `${mobileLayout == "grid" && "col-span-2"} aspect-wide`;
|
||||
} else if (aspectRatio < 1) {
|
||||
return `${mobileLayout == "grid" && "row-span-2 h-full"} aspect-tall`;
|
||||
} else {
|
||||
return "aspect-video";
|
||||
}
|
||||
})()}
|
||||
ref={birdseyeContainerRef}
|
||||
>
|
||||
{fullscreen ? (
|
||||
<FaCompress className="size-5 md:m-[6px]" />
|
||||
) : (
|
||||
<FaExpand className="size-5 md:m-[6px]" />
|
||||
)}
|
||||
<BirdseyeLivePlayer
|
||||
birdseyeConfig={birdseyeConfig}
|
||||
liveMode={birdseyeConfig.restream ? "mse" : "jsmpeg"}
|
||||
onClick={() => onSelectCamera("birdseye")}
|
||||
containerRef={birdseyeContainerRef}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{fullscreen
|
||||
? t("button.exitFullscreen", { ns: "common" })
|
||||
: t("button.fullscreen", { ns: "common" })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{cameras.map((camera) => {
|
||||
let grow;
|
||||
const aspectRatio =
|
||||
camera.detect.width / camera.detect.height;
|
||||
if (aspectRatio > 2) {
|
||||
grow = `${mobileLayout == "grid" && "col-span-2"} aspect-wide`;
|
||||
} else if (aspectRatio < 1) {
|
||||
grow = `${mobileLayout == "grid" && "row-span-2 h-full"} aspect-tall`;
|
||||
} else {
|
||||
grow = "aspect-video";
|
||||
}
|
||||
const availableStreams = camera.live.streams || {};
|
||||
const firstStreamEntry =
|
||||
Object.values(availableStreams)[0] || "";
|
||||
|
||||
const streamNameFromSettings =
|
||||
currentGroupStreamingSettings?.[camera.name]?.streamName ||
|
||||
"";
|
||||
const streamExists =
|
||||
streamNameFromSettings &&
|
||||
Object.values(availableStreams).includes(
|
||||
streamNameFromSettings,
|
||||
);
|
||||
|
||||
const streamName = streamExists
|
||||
? streamNameFromSettings
|
||||
: firstStreamEntry;
|
||||
const streamType =
|
||||
currentGroupStreamingSettings?.[camera.name]?.streamType;
|
||||
const autoLive =
|
||||
streamType !== undefined
|
||||
? streamType !== "no-streaming"
|
||||
: undefined;
|
||||
const showStillWithoutActivity =
|
||||
currentGroupStreamingSettings?.[camera.name]?.streamType !==
|
||||
"continuous";
|
||||
const useWebGL =
|
||||
currentGroupStreamingSettings?.[camera.name]
|
||||
?.compatibilityMode || false;
|
||||
return (
|
||||
<LiveContextMenu
|
||||
className={grow}
|
||||
key={camera.name}
|
||||
camera={camera.name}
|
||||
cameraGroup={cameraGroup}
|
||||
streamName={streamName}
|
||||
preferredLiveMode={
|
||||
preferredLiveModes[camera.name] ?? "mse"
|
||||
}
|
||||
isRestreamed={isRestreamedStates[camera.name]}
|
||||
supportsAudio={
|
||||
supportsAudioOutputStates[streamName]?.supportsAudio ??
|
||||
false
|
||||
}
|
||||
audioState={audioStates[camera.name]}
|
||||
toggleAudio={() => toggleAudio(camera.name)}
|
||||
statsState={statsStates[camera.name]}
|
||||
toggleStats={() => toggleStats(camera.name)}
|
||||
volumeState={volumeStates[camera.name] ?? 1}
|
||||
setVolumeState={(value) =>
|
||||
setVolumeStates({
|
||||
[camera.name]: value,
|
||||
})
|
||||
}
|
||||
muteAll={muteAll}
|
||||
unmuteAll={unmuteAll}
|
||||
resetPreferredLiveMode={() =>
|
||||
resetPreferredLiveMode(camera.name)
|
||||
}
|
||||
config={config}
|
||||
>
|
||||
<LivePlayer
|
||||
cameraRef={cameraRef}
|
||||
key={camera.name}
|
||||
className={`${grow} rounded-lg bg-black md:rounded-2xl`}
|
||||
windowVisible={
|
||||
windowVisible && visibleCameras.includes(camera.name)
|
||||
}
|
||||
cameraConfig={camera}
|
||||
preferredLiveMode={
|
||||
preferredLiveModes[camera.name] ?? "mse"
|
||||
}
|
||||
autoLive={autoLive ?? globalAutoLive}
|
||||
showStillWithoutActivity={
|
||||
showStillWithoutActivity ?? true
|
||||
}
|
||||
alwaysShowCameraName={displayCameraNames}
|
||||
useWebGL={useWebGL}
|
||||
playInBackground={false}
|
||||
showStats={statsStates[camera.name]}
|
||||
streamName={streamName}
|
||||
onClick={() => onSelectCamera(camera.name)}
|
||||
onError={(e) => handleError(camera.name, e)}
|
||||
onResetLiveMode={() =>
|
||||
resetPreferredLiveMode(camera.name)
|
||||
}
|
||||
playAudio={audioStates[camera.name] ?? false}
|
||||
volume={volumeStates[camera.name]}
|
||||
/>
|
||||
</LiveContextMenu>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{isDesktop && (
|
||||
<div
|
||||
className={cn(
|
||||
"fixed",
|
||||
isDesktop && "bottom-12 lg:bottom-9",
|
||||
isMobile && "bottom-12 lg:bottom-16",
|
||||
hasScrollbar && isDesktop ? "right-6" : "right-3",
|
||||
"z-50 flex flex-row gap-2",
|
||||
)}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className="cursor-pointer rounded-lg bg-secondary text-secondary-foreground opacity-60 transition-all duration-300 hover:bg-muted hover:opacity-100"
|
||||
onClick={toggleFullscreen}
|
||||
>
|
||||
{fullscreen ? (
|
||||
<FaCompress className="size-5 md:m-[6px]" />
|
||||
) : (
|
||||
<FaExpand className="size-5 md:m-[6px]" />
|
||||
)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{fullscreen
|
||||
? t("button.exitFullscreen", { ns: "common" })
|
||||
: t("button.fullscreen", { ns: "common" })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<DraggableGridLayout
|
||||
cameras={cameras}
|
||||
cameraGroup={cameraGroup}
|
||||
containerRef={containerRef}
|
||||
cameraRef={cameraRef}
|
||||
includeBirdseye={includeBirdseye}
|
||||
onSelectCamera={onSelectCamera}
|
||||
windowVisible={windowVisible}
|
||||
visibleCameras={visibleCameras}
|
||||
isEditMode={isEditMode}
|
||||
setIsEditMode={setIsEditMode}
|
||||
fullscreen={fullscreen}
|
||||
toggleFullscreen={toggleFullscreen}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<DraggableGridLayout
|
||||
cameras={cameras}
|
||||
cameraGroup={cameraGroup}
|
||||
containerRef={containerRef}
|
||||
cameraRef={cameraRef}
|
||||
includeBirdseye={includeBirdseye}
|
||||
onSelectCamera={onSelectCamera}
|
||||
windowVisible={windowVisible}
|
||||
visibleCameras={visibleCameras}
|
||||
isEditMode={isEditMode}
|
||||
setIsEditMode={setIsEditMode}
|
||||
fullscreen={fullscreen}
|
||||
toggleFullscreen={toggleFullscreen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -638,15 +660,26 @@ export default function LiveDashboardView({
|
||||
|
||||
function NoCameraView() {
|
||||
const { t } = useTranslation(["views/live"]);
|
||||
const { auth } = useContext(AuthContext);
|
||||
const isCustomRole = useIsCustomRole();
|
||||
|
||||
// Check if this is a restricted user with no cameras in this group
|
||||
const isRestricted = isCustomRole && auth.isAuthenticated;
|
||||
|
||||
return (
|
||||
<div className="flex size-full items-center justify-center">
|
||||
<EmptyCard
|
||||
icon={<BsFillCameraVideoOffFill className="size-8" />}
|
||||
title={t("noCameras.title")}
|
||||
description={t("noCameras.description")}
|
||||
buttonText={t("noCameras.buttonText")}
|
||||
link="/settings?page=cameraManagement"
|
||||
title={
|
||||
isRestricted ? t("noCameras.restricted.title") : t("noCameras.title")
|
||||
}
|
||||
description={
|
||||
isRestricted
|
||||
? t("noCameras.restricted.description")
|
||||
: t("noCameras.description")
|
||||
}
|
||||
buttonText={!isRestricted ? t("noCameras.buttonText") : undefined}
|
||||
link={!isRestricted ? "/settings?page=cameraManagement" : undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -198,9 +198,9 @@ export default function TriggerView({
|
||||
|
||||
return axios
|
||||
.put("config/set", configBody)
|
||||
.then((configResponse) => {
|
||||
.then(async (configResponse) => {
|
||||
if (configResponse.status === 200) {
|
||||
updateConfig();
|
||||
await updateConfig();
|
||||
const displayName =
|
||||
friendly_name && friendly_name !== ""
|
||||
? `${friendly_name} (${name})`
|
||||
@@ -353,9 +353,9 @@ export default function TriggerView({
|
||||
|
||||
return axios
|
||||
.put("config/set", configBody)
|
||||
.then((configResponse) => {
|
||||
.then(async (configResponse) => {
|
||||
if (configResponse.status === 200) {
|
||||
updateConfig();
|
||||
await updateConfig();
|
||||
const friendly =
|
||||
config?.cameras?.[selectedCamera]?.semantic_search
|
||||
?.triggers?.[name]?.friendly_name;
|
||||
|
||||
@@ -67,13 +67,14 @@ export default function EnrichmentMetrics({
|
||||
|
||||
// features stats
|
||||
|
||||
const embeddingInferenceTimeSeries = useMemo(() => {
|
||||
const groupedEnrichmentMetrics = useMemo(() => {
|
||||
if (!statsHistory) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const series: {
|
||||
[key: string]: {
|
||||
rawKey: string;
|
||||
name: string;
|
||||
metrics: Threshold;
|
||||
data: { x: number; y: number }[];
|
||||
@@ -90,6 +91,7 @@ export default function EnrichmentMetrics({
|
||||
|
||||
if (!(key in series)) {
|
||||
series[key] = {
|
||||
rawKey,
|
||||
name: t("enrichments.embeddings." + rawKey),
|
||||
metrics: getThreshold(rawKey),
|
||||
data: [],
|
||||
@@ -99,7 +101,57 @@ export default function EnrichmentMetrics({
|
||||
series[key].data.push({ x: statsIdx + 1, y: stat });
|
||||
});
|
||||
});
|
||||
return Object.values(series);
|
||||
|
||||
// Group series by category (extract base name from raw key)
|
||||
const grouped: {
|
||||
[category: string]: {
|
||||
categoryName: string;
|
||||
speedSeries?: {
|
||||
name: string;
|
||||
metrics: Threshold;
|
||||
data: { x: number; y: number }[];
|
||||
};
|
||||
eventsSeries?: {
|
||||
name: string;
|
||||
metrics: Threshold;
|
||||
data: { x: number; y: number }[];
|
||||
};
|
||||
};
|
||||
} = {};
|
||||
|
||||
Object.values(series).forEach((s) => {
|
||||
// Extract base category name from raw key
|
||||
// All metrics follow the pattern: {base}_speed and {base}_events_per_second
|
||||
let categoryKey = s.rawKey;
|
||||
let isSpeed = false;
|
||||
|
||||
if (s.rawKey.endsWith("_speed")) {
|
||||
categoryKey = s.rawKey.replace("_speed", "");
|
||||
isSpeed = true;
|
||||
} else if (s.rawKey.endsWith("_events_per_second")) {
|
||||
categoryKey = s.rawKey.replace("_events_per_second", "");
|
||||
isSpeed = false;
|
||||
}
|
||||
|
||||
// Get translated category name
|
||||
const categoryName = t("enrichments.embeddings." + categoryKey);
|
||||
|
||||
if (!(categoryKey in grouped)) {
|
||||
grouped[categoryKey] = {
|
||||
categoryName,
|
||||
speedSeries: undefined,
|
||||
eventsSeries: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (isSpeed) {
|
||||
grouped[categoryKey].speedSeries = s;
|
||||
} else {
|
||||
grouped[categoryKey].eventsSeries = s;
|
||||
}
|
||||
});
|
||||
|
||||
return Object.values(grouped);
|
||||
}, [statsHistory, t, getThreshold]);
|
||||
|
||||
return (
|
||||
@@ -110,35 +162,42 @@ export default function EnrichmentMetrics({
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"mt-4 grid w-full grid-cols-1 gap-2 sm:grid-cols-3",
|
||||
embeddingInferenceTimeSeries && "sm:grid-cols-4",
|
||||
"mt-4 grid w-full grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-4",
|
||||
)}
|
||||
>
|
||||
{statsHistory.length != 0 ? (
|
||||
<>
|
||||
{embeddingInferenceTimeSeries.map((series) => (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5 smart-capitalize">{series.name}</div>
|
||||
{series.name.endsWith("Speed") ? (
|
||||
<ThresholdBarGraph
|
||||
key={series.name}
|
||||
graphId={`${series.name}-inference`}
|
||||
name={series.name}
|
||||
unit="ms"
|
||||
threshold={series.metrics}
|
||||
updateTimes={updateTimes}
|
||||
data={[series]}
|
||||
/>
|
||||
) : (
|
||||
<EventsPerSecondsLineGraph
|
||||
key={series.name}
|
||||
graphId={`${series.name}-fps`}
|
||||
unit=""
|
||||
name={t("enrichments.infPerSecond")}
|
||||
updateTimes={updateTimes}
|
||||
data={[series]}
|
||||
/>
|
||||
)}
|
||||
{groupedEnrichmentMetrics.map((group) => (
|
||||
<div
|
||||
key={group.categoryName}
|
||||
className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl"
|
||||
>
|
||||
<div className="mb-5 smart-capitalize">
|
||||
{group.categoryName}
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{group.speedSeries && (
|
||||
<ThresholdBarGraph
|
||||
key={`${group.categoryName}-speed`}
|
||||
graphId={`${group.categoryName}-inference`}
|
||||
name={t("enrichments.averageInf")}
|
||||
unit="ms"
|
||||
threshold={group.speedSeries.metrics}
|
||||
updateTimes={updateTimes}
|
||||
data={[group.speedSeries]}
|
||||
/>
|
||||
)}
|
||||
{group.eventsSeries && (
|
||||
<EventsPerSecondsLineGraph
|
||||
key={`${group.categoryName}-events`}
|
||||
graphId={`${group.categoryName}-fps`}
|
||||
unit=""
|
||||
name={t("enrichments.infPerSecond")}
|
||||
updateTimes={updateTimes}
|
||||
data={[group.eventsSeries]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -729,33 +729,32 @@ export default function GeneralMetrics({
|
||||
) : (
|
||||
<Skeleton className="aspect-video w-full" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{statsHistory[0]?.npu_usages && (
|
||||
<div
|
||||
className={cn("mt-4 grid grid-cols-1 gap-2 sm:grid-cols-2")}
|
||||
>
|
||||
{statsHistory.length != 0 ? (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5">
|
||||
{t("general.hardwareInfo.npuUsage")}
|
||||
</div>
|
||||
{npuSeries.map((series) => (
|
||||
<ThresholdBarGraph
|
||||
key={series.name}
|
||||
graphId={`${series.name}-npu`}
|
||||
name={series.name}
|
||||
unit="%"
|
||||
threshold={GPUUsageThreshold}
|
||||
updateTimes={updateTimes}
|
||||
data={[series]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Skeleton className="aspect-video w-full" />
|
||||
|
||||
{statsHistory[0]?.npu_usages && (
|
||||
<>
|
||||
{statsHistory.length != 0 ? (
|
||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||
<div className="mb-5">
|
||||
{t("general.hardwareInfo.npuUsage")}
|
||||
</div>
|
||||
{npuSeries.map((series) => (
|
||||
<ThresholdBarGraph
|
||||
key={series.name}
|
||||
graphId={`${series.name}-npu`}
|
||||
name={series.name}
|
||||
unit="%"
|
||||
threshold={GPUUsageThreshold}
|
||||
updateTimes={updateTimes}
|
||||
data={[series]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Skeleton className="aspect-video w-full" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user