Files
manage/frontend/src/widgets/GrafanaPanelWidget.tsx
T
Developer b877a32ad8 Add Jellyfin Now Playing + Grafana Panel embed widgets
Two new additive widget kinds:

Jellyfin 'now_playing': like the existing 'activity' widget but filters
to only sessions with active playback (NowPlayingItem present + not
paused). Shows who's actually watching right now. The 'activity' kind
is unchanged (shows all sessions including idle).

Grafana 'panel': embeds a single Grafana panel directly in the app via
an iframe, using Grafana's /d-solo/ endpoint (renders one panel without
dashboard chrome, kiosk=tv). Configurable dashboard_uid, panel_id, and
time range (from/to, defaults now-1h/now). Includes a fallback 'Open in
Grafana' link for when embedding is blocked by X-Frame-Options/CSP. The
'link' kind is unchanged (still builds a deep-link URL).

Backend: new widget configs + definitions on jellyfin/grafana; source
adapter logic (session filter for now_playing; d-solo embed URL for
panel); 6 new tests.

Frontend: JellyfinNowPlayingWidget + GrafanaPanelWidget components;
registry bindings; 6 new tests.

278 backend tests pass (+6); 127 frontend tests pass (+6); lint/build
green both sides.
2026-06-28 15:26:43 +00:00

54 lines
1.5 KiB
TypeScript

import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { ExternalLink } from "lucide-react";
import { SectionCard } from "../components/SectionCard";
import { useWidgetData } from "../hooks/useWidgets";
import type { WidgetInstance } from "../types";
interface Props {
widget: WidgetInstance;
refreshIntervalMs: number;
description?: string;
}
export function GrafanaPanelWidget({
widget,
refreshIntervalMs,
description,
}: Props) {
const { data, isLoading } = useWidgetData(widget.id, refreshIntervalMs);
const embedUrl = data?.data?.embed_url as string | undefined;
return (
<SectionCard title={widget.title} description={description}>
{isLoading && !data ? (
<Skeleton className="h-[300px] w-full" />
) : data?.error ? (
<Alert variant="destructive">
<AlertDescription>{data.error}</AlertDescription>
</Alert>
) : embedUrl ? (
<div className="flex flex-col gap-2">
<iframe
src={embedUrl}
title={widget.title}
className="h-[300px] w-full rounded-lg border border-border"
loading="lazy"
/>
<Button variant="outline" size="sm" asChild>
<a href={embedUrl} target="_blank" rel="noopener noreferrer">
Open in Grafana
<ExternalLink className="ml-2 h-3 w-3" />
</a>
</Button>
</div>
) : (
<Alert>
<AlertDescription>No Grafana panel configured.</AlertDescription>
</Alert>
)}
</SectionCard>
);
}