feat(jellyseer): stat widgets + rich Requests tab (slice 3/3)
Frontend for Jellyseerr request stats, reusing the generic stat abstraction.
- api/jellyseerr.ts + hooks/useJellyseer.ts: fetchJellyseerrStats +
useJellyseerrStats (polls /api/jellyseerr/stats, no-retry; shares the backend
cache with the widgets).
- Two reusable widgets backed by the stat/stats_overview kinds:
- RequestStatWidget: a single selected stat (big value + label).
- RequestsOverviewWidget: a MetricCard grid of all stats + a recent-requests
list with status/media-status badges.
- registry.ts: Jellyfin gains `stat` (a dropdown over
total/pending/approved/declined/processing/available — the "extract one stat
into a widget" affordance, rendered as a Select via the existing enum UI) and
`stats_overview` widget kinds, wired to the new components.
- RequestsTab rewritten: live stats grid (6 counts) + recent-requests list +
a hint to pin individual stats via the Request stat widget. Reads
jellyseerr_url from config and the now-secret jellyseerr_api_key from
secrets_set.
Tests: RequestStatWidget + RequestsOverviewWidget rendering/error; RequestsTab
not-configured CTA, configured stats grid, and error states. 184/184 frontend
tests pass; tsc + ESLint clean.
This commit is contained in:
@@ -1,38 +1,42 @@
|
||||
/**
|
||||
* RequestsTab — Jellyseerr request-management surface on the Jellyfin page.
|
||||
* RequestsTab — Jellyseerr request stats surface on the Jellyfin page.
|
||||
*
|
||||
* Jellyseerr was absorbed into Jellyfin config (jellyseerr_url +
|
||||
* jellyseerr_api_key) in Slice 1. This tab reads those config fields. When
|
||||
* configured, it shows the URL and a placeholder (no requests backend endpoint
|
||||
* exists yet — building one is out of scope for this slice). When not
|
||||
* configured, it shows an empty-state CTA directing the user to add the fields
|
||||
* to the Jellyfin config.
|
||||
* Reads the Jellyfin service's jellyseerr_url (config) + jellyseerr_api_key
|
||||
* (secret). When configured, polls /api/jellyseerr/stats and renders the
|
||||
* request-count grid + a recent-requests list. Individual stats can be pinned
|
||||
* to dashboards via the "Request stat" widget.
|
||||
*/
|
||||
import type { ServiceInstance } from "../../types";
|
||||
import { useJellyseerrStats } from "../../hooks/useJellyseer";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { MetricCard } from "../../components/MetricCard";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
|
||||
export function RequestsTab({ instance }: { instance: ServiceInstance }) {
|
||||
const jellyseerrUrl = String(
|
||||
(instance.config as Record<string, unknown>).jellyseerr_url ?? "",
|
||||
).trim();
|
||||
const jellyseerrApiKey = String(
|
||||
(instance.config as Record<string, unknown>).jellyseerr_api_key ?? "",
|
||||
).trim();
|
||||
const jellyseerrApiKeySet = Boolean(
|
||||
(instance.secrets_set as Record<string, boolean> | undefined)
|
||||
?.jellyseerr_api_key,
|
||||
);
|
||||
const { data, isLoading, error } = useJellyseerrStats(instance.id);
|
||||
|
||||
if (!jellyseerrUrl || !jellyseerrApiKey) {
|
||||
if (!jellyseerrUrl || !jellyseerrApiKeySet) {
|
||||
return (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
Jellyseerr is not configured for this Jellyfin instance. Add
|
||||
Jellyseerr is not configured for this Jellyfin instance. Add a
|
||||
<code className="mx-1 rounded bg-muted px-1 py-0.5 text-xs">
|
||||
jellyseerr_url
|
||||
</code>
|
||||
and
|
||||
config field and a
|
||||
<code className="mx-1 rounded bg-muted px-1 py-0.5 text-xs">
|
||||
jellyseerr_api_key
|
||||
</code>
|
||||
to the Jellyfin config (Config tab) to enable request management.
|
||||
secret to the Jellyfin service to enable request management.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
@@ -52,13 +56,58 @@ export function RequestsTab({ instance }: { instance: ServiceInstance }) {
|
||||
<ExternalLink className="size-3.5" />
|
||||
</a>
|
||||
</div>
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
Jellyseerr is configured. The requests view will show pending and
|
||||
recently fulfilled media requests. (This surface is under
|
||||
development.)
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{error ? (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{error.message}</AlertDescription>
|
||||
</Alert>
|
||||
) : isLoading ? (
|
||||
<Skeleton className="h-32 w-full" />
|
||||
) : data?.detail && !(data.stats && data.stats.length > 0) ? (
|
||||
<Alert>
|
||||
<AlertDescription>{data.detail}</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-6">
|
||||
{(data?.stats ?? []).map((s) => (
|
||||
<MetricCard key={s.key} label={s.label} value={String(s.value)} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{data?.recent && data.recent.length > 0 ? (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Recent requests
|
||||
</span>
|
||||
{data.recent.slice(0, 12).map((r, i) => (
|
||||
<div
|
||||
key={String(r.id ?? i)}
|
||||
className="flex items-center justify-between gap-2 rounded border p-2 text-sm"
|
||||
>
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="truncate font-medium">{r.name ?? "—"}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{r.type ? String(r.type) : ""}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
{r.media_status ? (
|
||||
<Badge variant="outline">{r.media_status}</Badge>
|
||||
) : null}
|
||||
{r.status ? <Badge variant="secondary">{r.status}</Badge> : null}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Pin individual stats to a dashboard with the “Request stat”
|
||||
widget.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user