Files
manage/frontend/src/pages/service-tabs/RequestsTab.tsx
T
Developer 0b039529f6 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.
2026-07-12 13:59:08 +00:00

114 lines
3.7 KiB
TypeScript

/**
* RequestsTab — Jellyseerr request stats surface on the Jellyfin page.
*
* 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 jellyseerrApiKeySet = Boolean(
(instance.secrets_set as Record<string, boolean> | undefined)
?.jellyseerr_api_key,
);
const { data, isLoading, error } = useJellyseerrStats(instance.id);
if (!jellyseerrUrl || !jellyseerrApiKeySet) {
return (
<Alert>
<AlertDescription>
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>
config field and a
<code className="mx-1 rounded bg-muted px-1 py-0.5 text-xs">
jellyseerr_api_key
</code>
secret to the Jellyfin service to enable request management.
</AlertDescription>
</Alert>
);
}
return (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<h3 className="text-sm font-semibold">Jellyseerr</h3>
<a
href={jellyseerrUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-sm text-primary hover:underline"
>
{jellyseerrUrl}
<ExternalLink className="size-3.5" />
</a>
</div>
{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 &ldquo;Request stat&rdquo;
widget.
</p>
</>
)}
</div>
);
}