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.
79 lines
2.2 KiB
TypeScript
79 lines
2.2 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
|
import { render, screen } from "@testing-library/react";
|
|
import { GrafanaPanelWidget } from "../GrafanaPanelWidget";
|
|
import type { WidgetInstance } from "../../types";
|
|
|
|
vi.mock("../../hooks/useWidgets", () => ({
|
|
useWidgetData: vi.fn(),
|
|
}));
|
|
|
|
const widget: WidgetInstance = {
|
|
id: "w2",
|
|
service_id: "s2",
|
|
widget_kind: "panel",
|
|
title: "CPU Usage",
|
|
config: {},
|
|
enabled: true,
|
|
sort_order: 0,
|
|
created_at: 0,
|
|
updated_at: 0,
|
|
};
|
|
|
|
describe("GrafanaPanelWidget", () => {
|
|
it("renders an iframe with the embed URL", async () => {
|
|
const { useWidgetData } = await import("../../hooks/useWidgets");
|
|
vi.mocked(useWidgetData).mockReturnValue({
|
|
data: {
|
|
widget_id: "w2",
|
|
data: {
|
|
embed_url:
|
|
"http://grafana:3000/d-solo/ov/manage?panelId=4&from=now-1h&to=now&kiosk=tv",
|
|
},
|
|
fetched_at: 0,
|
|
},
|
|
isLoading: false,
|
|
} as never);
|
|
|
|
const { container } = render(
|
|
<GrafanaPanelWidget widget={widget} refreshIntervalMs={0} />,
|
|
);
|
|
const iframe = container.querySelector("iframe");
|
|
expect(iframe).not.toBeNull();
|
|
expect(iframe?.getAttribute("src")).toContain("d-solo/ov/manage");
|
|
expect(iframe?.getAttribute("src")).toContain("panelId=4");
|
|
});
|
|
|
|
it("renders Open in Grafana fallback link", async () => {
|
|
const { useWidgetData } = await import("../../hooks/useWidgets");
|
|
vi.mocked(useWidgetData).mockReturnValue({
|
|
data: {
|
|
widget_id: "w2",
|
|
data: {
|
|
embed_url:
|
|
"http://grafana:3000/d-solo/ov/manage?panelId=4&from=now-1h&to=now&kiosk=tv",
|
|
},
|
|
fetched_at: 0,
|
|
},
|
|
isLoading: false,
|
|
} as never);
|
|
|
|
render(<GrafanaPanelWidget widget={widget} refreshIntervalMs={0} />);
|
|
expect(screen.getByText("Open in Grafana")).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders error state", async () => {
|
|
const { useWidgetData } = await import("../../hooks/useWidgets");
|
|
vi.mocked(useWidgetData).mockReturnValue({
|
|
data: {
|
|
widget_id: "w2",
|
|
error: "dashboard_uid is required",
|
|
fetched_at: 0,
|
|
},
|
|
isLoading: false,
|
|
} as never);
|
|
|
|
render(<GrafanaPanelWidget widget={widget} refreshIntervalMs={0} />);
|
|
expect(screen.getByText("dashboard_uid is required")).toBeInTheDocument();
|
|
});
|
|
});
|