b877a32ad8
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.
54 lines
1.5 KiB
TypeScript
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>
|
|
);
|
|
}
|