feat(jellyseer): sortable/filterable requests table on the Requests tab
Replace the static "recent requests" list with a proper table of all Jellyseerr requests, sorted by date added (newest first by default) with standard sorting and filtering. Backend: - JellyseerrClient.requests(max_count=500): paginated GET /api/v1/request (sort=added), mapped with type (movie/tv), status, media_status, and created_at labels. Returns up to 500 so the table can sort/filter client-side. - fetch_jellyseer_requests(service) reuses the per-service cached client (shared with the stats widgets). - new GET /api/jellyseerr/requests endpoint. Frontend: - JellyseerRequestsTable: TanStack Table (sorting via getSortedRowModel, pagination via getPaginationRowModel) reusing the Table primitives + TablePagination. Columns: Name / Type / Status / Media / Requested, all sortable; default sort Requested desc. A search box filters by name and a status dropdown defaults to "Open" (pending+approved+processing) with All/Pending/Approved/Declined options. (The shared DataTable is deliberately visibility-only, so this is a dedicated sortable table.) - RequestsTab renders the stats grid + the new table (the compact recent list stays on the Requests overview widget). - useJellyseerRequests hook + fetchJellyseerRequests API client. Tests: client requests() mapping + single-page stop; fetch helper not-configured; RequestsTab test mocks both hooks. 404/404 backend + 184/184 frontend pass; build (tsc -b && vite build) + ESLint clean.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
dir: frontend/src/api
|
||||
|
||||
## role
|
||||
Typed API client layer that centralizes all backend communication for the frontend application across multiple service domains.
|
||||
Frontend API client layer that centralizes typed HTTP requests to backend services and external integrations.
|
||||
## parent
|
||||
index: frontend/src/.pi-map.index.md
|
||||
map: frontend/src/.pi-map.md
|
||||
|
||||
@@ -4,18 +4,18 @@ dir: frontend/src/api
|
||||
index: frontend/src/api/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Typed API client layer that centralizes all backend communication for the frontend application across multiple service domains.
|
||||
Frontend API client layer that centralizes typed HTTP requests to backend services and external integrations.
|
||||
## files
|
||||
- authentik.ts | API client providing functions to fetch users, send messages, and check message status from the Authentik service. | exp: AuthentikUser, AuthentikUsersResponse, AuthentikMessageInput, AuthentikMessageResponse, func:fetchAuthentikUsers(serviceId: string, params: { search?: string; page?: number; page_size?: number }) → Promise<AuthentikUsersResponse>, call:get, call:String, func:sendAuthentikMessage(serviceId: string, input: AuthentikMessageInput) → Promise<AuthentikMessageResponse>, call:post, func:fetchAuthentikMessageStatus(serviceId: string) → Promise<Record<string, unknown>>, call:get | dep: ./shared
|
||||
- backups.ts | API client functions for fetching and managing backup jobs, runs, alerts, and dashboard summaries. | exp: func:fetchBackupJobs(serviceId: string) → Promise<BackupJob[]>, call:get, func:fetchBackupJob(jobId: string) → Promise<{ job: BackupJob; runs: BackupRun[] }>, call:get, func:fetchBackupRuns(jobId: string, status: string, serviceId: string) → Promise<BackupRun[]>, call:get, func:fetchBackupRun(runId: string) → Promise<BackupRun>, call:get, func:fetchBackupAlerts(jobId: string, acknowledged: boolean, severity: string, serviceId: string) → Promise<BackupAlert[]>, call:get, call:String, func:acknowledgeBackupAlert(alertId: string) → Promise<BackupAlert>, call:post, func:fetchBackupDashboard() → Promise<BackupDashboardSummary>, call:get | dep: ./shared, ../types/backups
|
||||
- client.ts | Typed API client providing functions for interacting with a FastAPI backend across dashboard, monitoring, media, files, jobs, and observability endpoints. | exp: fetchCounts, fetchLibraries, fetchActivity, fetchUsers, fetchNowPlaying, fetchMonitoringMachines, fetchAppVersion, fetchDashboardShortcuts, saveDashboardShortcut, deleteDashboardShortcut, fetchMonitoringSettings, fetchSSHKeys, generateSSHKey, saveSSHKey, deleteSSHKey, fetchSavedTasks, fetchSavedTaskRuns, saveTask, deleteTask, runTask, saveMonitoringMachine, testMonitoringMachineSSH, deleteMonitoringMachine, resetLocalDatabase, fetchMediaStatus, buildMediaIndex, stopMediaIndexBuild, forceStopMediaIndexBuild, queryMedia, fetchDirectoryListing, fetchFfprobe, fetchStat, resolvePath, fetchJobTemplates, runJob, fetchUserMessageQueueStatus, sendUserMessage, fetchAlertmanagerAlerts, fetchAlertmanagerStatus, fetchPrometheusStatus, fetchPrometheusTargets | dep: ../types, ./shared
|
||||
- dashboards.ts | API client providing CRUD operations for named dashboards via REST endpoints. | exp: NamedDashboard, NamedDashboardInput, func:fetchDashboards() → Promise<NamedDashboard[]>, call:get, func:fetchDashboardBySlug(slug: string) → Promise<NamedDashboard>, call:get, call:encodeURIComponent, func:createDashboard(input: NamedDashboardInput) → Promise<NamedDashboard>, call:post, func:updateDashboard(input: NamedDashboardInput) → Promise<NamedDashboard>, call:put, func:deleteDashboard(id: string) → Promise<{ status: string }>, call:del | dep: ./shared
|
||||
- jellyseerr.ts | Fetches Jellyseerr request statistics and recent requests for a Jellyfin service instance via an API endpoint. | exp: JellyseerStat, JellyseerRecentRequest, JellyseerStatsResponse, func:fetchJellyseerrStats(jellyfinServiceId: string) → Promise<JellyseerStatsResponse>, call:get | dep: ./shared
|
||||
- jellyseerr.ts | This file provides API client functions to fetch Jellyseerr request statistics and request lists for a Jellyfin service instance. | exp: JellyseerStat, JellyseerRecentRequest, JellyseerStatsResponse, JellyseerRequest, func:fetchJellyseerrStats(jellyfinServiceId: string) → Promise<JellyseerStatsResponse>, call:get, func:fetchJellyseerrRequests(jellyfinServiceId: string) → Promise<JellyseerRequest[]> | dep: ./shared
|
||||
- services.ts | API client functions for CRUD operations and testing of service instances. | exp: func:fetchServiceTypes() → Promise<ServiceTypeInfo[]>, call:get, func:fetchServiceInstances(serviceType: string) → Promise<ServiceInstance[]>, call:get, func:createServiceInstance(input: ServiceInstanceInput) → Promise<ServiceInstance>, call:post, func:updateServiceInstance(input: ServiceInstanceInput) → Promise<ServiceInstance>, call:put, raise:Error, func:deleteServiceInstance(serviceId: string) → Promise<{ status: string }>, call:del, func:testServiceInstance(input: ServiceInstanceInput) → Promise<ServiceTestResult>, call:post | dep: ./shared, ../types
|
||||
- shared.ts | Provides shared API helper functions (GET, POST, PUT, DELETE, etc.) that automatically attach OIDC auth tokens and handle URL building and error parsing for backend requests. | exp: API_BASE, func:buildUrl(path: string, params: Record<string, string>) → string, call:isAbsoluteUrl, call:Object.entries, call:url.searchParams.set, call:url.toString, func:readErrorDetail(response: Response) → Promise<string>, call:response.text, call:JSON.parse, call:detail.trim, func:buildHeaders(isJsonBody: boolean) → Headers, call:getAccessToken, call:headers.set, func:get(path: string, params: Record<string, string>) → Promise<T>, call:fetch, call:buildUrl, call:buildHeaders, call:response.json, raise:Error, func:post(path: string, body: unknown) → Promise<T>, call:fetch, call:buildUrl, call:buildHeaders, call:JSON.stringify, call:response.json, raise:Error, func:postForm(path: string, body: FormData) → Promise<T>, call:fetch, call:buildUrl, call:buildHeaders, call:response.json, raise:Error, func:put(path: string, body: unknown) → Promise<T>, call:fetch, call:buildUrl, call:buildHeaders, call:JSON.stringify, call:response.json, raise:Error, func:del(path: string) → Promise<T>, call:fetch, call:buildUrl, call:buildHeaders, call:response.json, raise:Error | dep: ../auth, getAccessToken (from ../auth), fetch API, Headers API, URL API, import.meta.env
|
||||
- widgets.ts | API client module providing CRUD operations for widget instances, widget references, builtin widget kinds, and widget data retrieval. | exp: WidgetReference, WidgetReferenceInput, func:fetchBuiltinWidgetKinds() → Promise< BuiltinWidgetKindInfo[] >, call:get, func:fetchWidgetInstances(serviceId: string, scope: "dashboard" | "service") → Promise<WidgetInstance[]>, call:get, func:createWidgetInstance(input: WidgetInstanceInput) → Promise<WidgetInstance>, call:post, func:updateWidgetInstance(input: WidgetInstanceInput) → Promise<WidgetInstance>, call:put, raise:Error, func:deleteWidgetInstance(widgetId: string) → Promise<{ status: string }>, call:del, func:fetchWidgetData(widgetId: string) → Promise<WidgetDataResponse>, call:get, func:fetchWidgetReferences(dashboardScope: string) → Promise<WidgetReference[]>, call:get, func:createWidgetReference(input: WidgetReferenceInput) → Promise<WidgetReference>, call:post, func:deleteWidgetReference(referenceId: string) → Promise<{ status: string }>, call:del, func:detachWidgetReference(referenceId: string) → Promise<WidgetInstance>, call:post, func:updateWidgetReference(referenceId: string, sortOrder: number) → Promise<WidgetReference>, call:put | dep: ./shared, ../types
|
||||
## arch
|
||||
Modular domain-based API clients built on a shared HTTP helper that handles OIDC authentication, URL building, and error parsing, with each module exposing typed functions for specific service endpoints.
|
||||
Modular API client pattern with a shared helper module for authentication, URL building, and error handling, alongside domain-specific client files organized by service area.
|
||||
## tags
|
||||
fetch, call:get, widget, dashboard, call:build, authentik, delete, call:post
|
||||
## symbols
|
||||
|
||||
@@ -30,3 +30,23 @@ export async function fetchJellyseerrStats(
|
||||
jellyfinServiceId ? { jellyfin_service_id: jellyfinServiceId } : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
export interface JellyseerRequest {
|
||||
id?: number | string;
|
||||
type?: string;
|
||||
name?: string;
|
||||
status?: string;
|
||||
media_status?: string;
|
||||
created_at?: number | string;
|
||||
}
|
||||
|
||||
/** Fetch Jellyseerr requests (all, mapped) for the Requests tab table. */
|
||||
export async function fetchJellyseerrRequests(
|
||||
jellyfinServiceId?: string,
|
||||
): Promise<JellyseerRequest[]> {
|
||||
const res = await get<{ requests: JellyseerRequest[]}>(
|
||||
"/api/jellyseerr/requests",
|
||||
jellyfinServiceId ? { jellyfin_service_id: jellyfinServiceId } : undefined,
|
||||
);
|
||||
return res.requests ?? [];
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: frontend/src/components
|
||||
|
||||
## role
|
||||
UI component library providing reusable React components for dashboard widgets, backup management tables, charts, dialogs, and media session displays.
|
||||
Shared UI component library providing reusable React components for tables, cards, charts, dialogs, and dashboard widgets across the frontend application.
|
||||
## parent
|
||||
index: frontend/src/.pi-map.index.md
|
||||
map: frontend/src/.pi-map.md
|
||||
@@ -21,6 +21,7 @@ map: frontend/src/.pi-map.md
|
||||
- ConfirmDialog.tsx
|
||||
- DialogFooter.tsx
|
||||
- HoverEditButton.tsx
|
||||
- JellyseerRequestsTable.tsx
|
||||
- LibraryOverview.tsx
|
||||
- LineSeriesChart.tsx
|
||||
- MetricCard.tsx
|
||||
|
||||
@@ -4,7 +4,7 @@ dir: frontend/src/components
|
||||
index: frontend/src/components/.pi-map.index.md
|
||||
|
||||
## role
|
||||
UI component library providing reusable React components for dashboard widgets, backup management tables, charts, dialogs, and media session displays.
|
||||
Shared UI component library providing reusable React components for tables, cards, charts, dialogs, and dashboard widgets across the frontend application.
|
||||
## files
|
||||
- BackupAlertsTable.tsx | Renders a responsive table of backup alerts with severity badges and acknowledge actions, switching between desktop table and mobile card layouts. | exp: func:BackupAlertsTable({ alerts, onAcknowledge }: Props), call:useIsMobile, call:onAcknowledge, call:alerts.map, call:severityVariant, call:formatTimestamp | dep: @/components/ui/badge, @/components/ui/button, @/components/ui/table, @/components/ui/mobile-card, ../hooks/useIsMobile, ../types/backups, useIsMobile hook, BackupAlert type
|
||||
- BackupDashboardWidget.tsx | Displays a dashboard widget summarizing backup job statistics including total jobs, 24-hour success rate, active alerts, and last failure timestamp. | exp: func:BackupDashboardWidget(), call:useBackupDashboard, call:new Date(data.last_failed_at * 1000).toLocaleString | dep: @/components/ui/badge, @/components/ui/card, ../hooks/useBackups
|
||||
@@ -13,8 +13,9 @@ UI component library providing reusable React components for dashboard widgets,
|
||||
- ConfirmDialog.tsx | Reusable confirmation dialog component that wraps shadcn/ui Dialog primitives with standardized cancel/confirm footer behavior. | exp: func:ConfirmDialog({ open, title, message, confirmLabel = "Delete", onCancel, onConfirm, busy, }: { open: boolean; title: string; message: string; confirmLabel?: string; onCancel: () => void; onConfirm: () => void; busy?: boolean; }), call:onCancel | dep: @/components/ui/dialog, ./DialogFooter
|
||||
- DialogFooter.tsx | Renders a dialog footer component with cancel, optional secondary action, and confirm buttons, mapping legacy MUI color/variant props to shadcn Button variants. | exp: func:DialogFooter({ onCancel, cancelLabel = "Cancel", onConfirm, confirmLabel, confirmBusyLabel, confirmDisabled, confirmColor = "primary", confirmVariant = "contained", confirmStartIcon, secondaryAction, }: DialogFooterProps), call:resolveConfirmVariant | dep: react, @/components/ui/button
|
||||
- HoverEditButton.tsx | Renders a hover-reveal edit button for desktop and always-visible edit button for mobile, preserving legacy CSS class hooks. | exp: func:HoverEditButton({ onClick, label = "Edit", mobile = "always", }: HoverEditButtonProps), call:e.stopPropagation, call:onClick | dep: lucide-react, @/components/ui/button
|
||||
- JellyseerRequestsTable.tsx | Renders a sortable, filterable, and paginated table of Jellyseerr media requests using TanStack Table with client-side search and status filtering. | exp: func:JellyseerRequestsTable({ serviceId }: { serviceId: string }), call:useJellyseerRequests, call:useState, call:useMemo, call:search.trim().toLowerCase, call:requests.filter, call:OPEN_STATUSES.has, call:String(r.name ?? "").toLowerCase().includes, call:useReactTable, call:getCoreRowModel, call:getSortedRowModel, call:getPaginationRowModel, call:setSearch, call:setStatusFilter, call:table.getHeaderGroups().map, call:hg.headers.map, call:header.column.getToggleSortingHandler, call:flexRender, call:header.getContext, call:header.column.getIsSorted, call:table.getRowModel().rows.map, call:row.getVisibleCells().map, call:cell.getContext, call:table.getState, call:table.getPageCount | dep: react, @tanstack/react-table, lucide-react, @/components/ui/alert, @/components/ui/badge, @/components/ui/input, @/components/ui/select, @/components/ui/skeleton, @/components/ui/table, @/components/ui/table-pagination, ../hooks/useJellyseer, ../api/jellyseerr
|
||||
- LibraryOverview.tsx | Renders a two-column responsive grid displaying movie and TV library counts using shadcn/ui Card components | exp: func:LibraryOverview({ libraries }: Props), call:libraries.filter, call:movieLibs.map, call:lib.total.toLocaleString, call:lib.movies.toLocaleString, call:tvLibs.map, call:lib.series.toLocaleString | dep: @/components/ui/card, ../types
|
||||
- LineSeriesChart.tsx | Renders a responsive multi-series line chart using recharts with automatic metric scaling and time-based X-axis formatting. | exp: SeriesPoint, ChartSeries, func:LineSeriesChart({ series, height = 300, unit = "none", scale = "auto", }: LineSeriesChartProps), call:series.reduce, call:Math.abs, call:metricScaleInfo, call:formatScaled, call:mergeSeries, call:formatTime, call:Number, call:fmt, call:series.map | dep: recharts, ../lib/metricFormat, metricFormat
|
||||
- LineSeriesChart.tsx | Renders multiple time-series as a responsive line chart with automatic metric scaling and formatting. | exp: SeriesPoint, ChartSeries, func:LineSeriesChart({ series, height = 300, unit = "none", scale = "auto", }: LineSeriesChartProps), call:series.reduce, call:Math.abs, call:metricScaleInfo, call:formatScaled, call:mergeSeries, call:formatTime, call:Number, call:fmt, call:series.map | dep: recharts, ../lib/metricFormat, metricFormat
|
||||
- MetricCard.tsx | Renders a compact metric display card with label, value, and optional subtext using Tailwind CSS styling. | exp: func:MetricCard({ label, value, subtext }: Props) | dep: @/components/ui/card
|
||||
- NowPlaying.tsx | Renders a now-playing panel by wrapping SessionActivityPanel with a specific empty message for user activity sessions. | exp: func:NowPlaying({ sessions, onSelectSession }: Props) | dep: ../types, ./SessionActivityPanel
|
||||
- PinnedServiceLink.tsx | Renders a navigable card-shaped button for pinned service shortcuts on dashboards and provides a helper to construct service target paths. | exp: PinnedServiceLinkProps, func:PinnedServiceLink({ label, target, icon: Icon = Boxes, className, }: PinnedServiceLinkProps), call:useNavigate, call:navigate, call:cn, func:serviceLinkTarget(serviceType: string, serviceId: string, tab: string) → string | dep: react-router-dom, lucide-react, @/lib/utils
|
||||
@@ -23,12 +24,12 @@ UI component library providing reusable React components for dashboard widgets,
|
||||
- ServiceTestPanel.tsx | Presentational component rendering a "Test credentials" panel with test button, result display, and a "Save anyway" checkbox. | exp: func:ServiceTestPanel({ result, isPending, saveAnyway, onTest, onSaveAnywayChange, disabled, }: Props), call:onSaveAnywayChange | dep: @/components/ui/alert, @/components/ui/button, ../types
|
||||
- SessionActivityPanel.tsx | Renders a scrollable table displaying live media session activity details with status badges and optional session selection callbacks. | exp: func:SessionActivityPanel({ sessions, emptyMessage = "No live sessions matched to this user.", selectedUserLabel, onSelectSession, }: Props), call:buildStatusSummary, call:sessions.map, call:formatStateLabel, call:onSelectSession, call:sessionStateVariant, call:event.stopPropagation | dep: @/components/ui/badge, @/components/ui/button, @/components/ui/table, ../types
|
||||
- TabbedCard.tsx | Renders a card with a line-style tab bar header and content area, acting as a controlled wrapper around shadcn/ui Tabs for backward-compatible API migration from MUI. | exp: func:TabbedCard({ value, onChange, tabs, children, }: TabbedCardProps), call:onChange, call:String | dep: react, @/components/ui/card, @/components/ui/tabs
|
||||
- WidgetConfigDialog.tsx | This file provides a React dialog component for creating, editing, deleting, and managing dashboard widgets and their specific configurations. | exp: func:WidgetConfigDialog({ open, onClose, serviceId, dashboardScope, editWidgetId, }: Props), call:useWidgetInstances, call:useMemo, call:useServiceInstances, call:useTasks, call:useSaveWidgetInstance, call:useDeleteWidgetInstance, call:useWidgetReferences, call:useCreateWidgetReference, call:useDeleteWidgetReference, call:useDetachWidgetReference, call:useUpdateWidgetReference, call:useState, call:Boolean, call:useEffect, call:instances.find, call:references.find, call:startEdit, call:setDraft, call:SERVICE_REGISTRY[ services.find((s) => s.id === serviceId)?.service_type ?? "" ]?.widgets.find, call:services.find, call:setDraftBaseline, call:onClose, call:saveWidget.mutateAsync, call:reset, call:updateRef.mutateAsync, call:deleteWidget.mutateAsync, call:[...instances].sort, call:references.map, call:[...owned, ...refs].sort, call:instances.map, call:existingSearch.toLowerCase().trim, call:allWidgets .filter((w) => !onDashboard.has(w.id)) .filter, call:onDashboard.has, call:w.title.toLowerCase().includes, call:w.widget_kind.toLowerCase().includes, call:createRef.mutateAsync, call:deleteRef.mutateAsync, call:detachRef.mutateAsync, call:SERVICE_REGISTRY[ services.find((s) => s.id === draft.serviceId)?.service_type ?? "" ]?.widgets.find, call:useIsMobile, call:String, call:Number, call:combinedWidgets.map, call:bindingLabel, call:moveInstance, call:toggleEnabled, call:handleDetach, call:handleRemoveReference, call:removeInstance, call:setShowExisting, call:setExistingSearch, call:availableWidgets.map, call:handleAddReference, call:Object.values(BUILTIN_WIDGETS).map, call:startAddBuiltIn, call:services .filter((s) => s.enabled) // When scoped to a service Overview, only show widgets for THAT // service instance's type (not all services' widgets). .filter((s) => !serviceId || s.id === serviceId) .flatMap, call:(SERVICE_REGISTRY[s.service_type]?.widgets ?? []).map, call:startAddService, call:handleClose, call:JSON.stringify | dep: react, @/components/ui/dialog, @/components/ui/button, @/components/ui/input, @/components/ui/textarea, @/components/ui/label, @/components/ui/switch, @/components/ui/select, @/components/ui/badge, @/components/ui/alert, lucide-react, ../hooks/useWidgets, ../hooks/useServices, ../hooks/useSettings, ../hooks/useIsMobile, @/components/ui/sheet-form, ../types, ../integrations/registry, @/components/ui/* (dialog, button, input, textarea, label, switch, select, badge, alert, sheet-form)
|
||||
- WidgetConfigDialog.tsx | This file provides a React dialog component for creating, editing, reordering, and deleting dashboard widgets, including managing references to existing widgets. | exp: func:WidgetConfigDialog({ open, onClose, serviceId, dashboardScope, editWidgetId, }: Props), call:useWidgetInstances, call:useMemo, call:useServiceInstances, call:useTasks, call:useSaveWidgetInstance, call:useDeleteWidgetInstance, call:useWidgetReferences, call:useCreateWidgetReference, call:useDeleteWidgetReference, call:useDetachWidgetReference, call:useUpdateWidgetReference, call:useState, call:Boolean, call:useEffect, call:instances.find, call:references.find, call:startEdit, call:setDraft, call:SERVICE_REGISTRY[ services.find((s) => s.id === serviceId)?.service_type ?? "" ]?.widgets.find, call:services.find, call:setDraftBaseline, call:onClose, call:saveWidget.mutateAsync, call:reset, call:updateRef.mutateAsync, call:deleteWidget.mutateAsync, call:[...instances].sort, call:references.map, call:[...owned, ...refs].sort, call:instances.map, call:existingSearch.toLowerCase().trim, call:allWidgets .filter((w) => !onDashboard.has(w.id)) .filter, call:onDashboard.has, call:w.title.toLowerCase().includes, call:w.widget_kind.toLowerCase().includes, call:createRef.mutateAsync, call:deleteRef.mutateAsync, call:detachRef.mutateAsync, call:SERVICE_REGISTRY[ services.find((s) => s.id === draft.serviceId)?.service_type ?? "" ]?.widgets.find, call:useIsMobile, call:String, call:Number, call:combinedWidgets.map, call:bindingLabel, call:moveInstance, call:toggleEnabled, call:handleDetach, call:handleRemoveReference, call:removeInstance, call:setShowExisting, call:setExistingSearch, call:availableWidgets.map, call:handleAddReference, call:Object.values(BUILTIN_WIDGETS).map, call:startAddBuiltIn, call:services .filter((s) => s.enabled) // When scoped to a service Overview, only show widgets for THAT // service instance's type (not all services' widgets). .filter((s) => !serviceId || s.id === serviceId) .flatMap, call:(SERVICE_REGISTRY[s.service_type]?.widgets ?? []).map, call:startAddService, call:handleClose, call:JSON.stringify | dep: react, @/components/ui/dialog, @/components/ui/button, @/components/ui/input, @/components/ui/textarea, @/components/ui/label, @/components/ui/switch, @/components/ui/select, @/components/ui/badge, @/components/ui/alert, lucide-react, ../hooks/useWidgets, ../hooks/useServices, ../hooks/useSettings, ../hooks/useIsMobile, @/components/ui/sheet-form, ../types, ../integrations/registry, @/components/ui/*
|
||||
- WidgetInstance.tsx | Renders a widget instance card that dynamically resolves and displays a widget component, with optional edit and copy actions. | exp: func:WidgetInstanceCard({ widget, onEdit, onCopy }: Props), call:useServiceInstances, call:resolveWidget, call:onCopy, call:onEdit | dep: @/components/ui/alert, @/components/ui/button, lucide-react, ../hooks/useServices, ../integrations/registry, ../types, ./SectionCard
|
||||
## arch
|
||||
Presentational React components built on shadcn/ui primitives with Tailwind CSS, using responsive design patterns (desktop table/mobile card) and wrapping legacy APIs for MUI-to-shadcn migration compatibility.
|
||||
Presentational React components built on shadcn/ui primitives with responsive design patterns, TanStack Table for data tables, and legacy API compatibility layers for MUI migration.
|
||||
## tags
|
||||
call:use, components, ui, card, widget, table, backup, call:on
|
||||
call:use, components, ui, card, table, widget, backup, call:on
|
||||
## symbols
|
||||
- BackupAlertsTable
|
||||
- BackupDashboardWidget
|
||||
@@ -37,7 +38,7 @@ call:use, components, ui, card, widget, table, backup, call:on
|
||||
- ConfirmDialog
|
||||
- DialogFooter
|
||||
- HoverEditButton
|
||||
- LibraryOverview
|
||||
- JellyseerRequestsTable
|
||||
## workflows
|
||||
- change components behavior
|
||||
read: BackupAlertsTable.tsx, BackupDashboardWidget.tsx, BackupJobsTable.tsx
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* JellyseerRequestsTable — sortable/filterable table of Jellyseerr requests.
|
||||
*
|
||||
* Uses TanStack Table directly (the shared DataTable is deliberately
|
||||
* visibility-only). Defaults: status filter = "open" (pending/approved/
|
||||
* processing), sorted by date added (newest first). Client-side sort + filter
|
||||
* + pagination over the backend's fetched set.
|
||||
*/
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
type ColumnDef,
|
||||
type SortingState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { ArrowDown, ArrowUp, ChevronsUpDown, Search } from "lucide-react";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { TablePagination } from "@/components/ui/table-pagination";
|
||||
import { useJellyseerRequests } from "../hooks/useJellyseer";
|
||||
import type { JellyseerRequest } from "../api/jellyseerr";
|
||||
|
||||
const OPEN_STATUSES = new Set(["pending", "approved", "processing"]);
|
||||
type StatusFilter = "open" | "all" | "pending" | "approved" | "declined";
|
||||
|
||||
function formatDate(v?: number | string): string {
|
||||
if (!v) return "—";
|
||||
const n = Number(v);
|
||||
const ms = n > 1e12 ? n : n * 1000; // seconds -> ms
|
||||
const d = new Date(ms);
|
||||
return Number.isNaN(d.getTime()) ? String(v) : d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
|
||||
}
|
||||
|
||||
const columns: ColumnDef<JellyseerRequest>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Name",
|
||||
cell: ({ row }) => (
|
||||
<span className="truncate font-medium">{row.original.name ?? "—"}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "type",
|
||||
header: "Type",
|
||||
cell: ({ row }) => (
|
||||
<span className="capitalize">{row.original.type ?? "—"}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: "Status",
|
||||
cell: ({ row }) => (
|
||||
<Badge variant="secondary">{row.original.status ?? "—"}</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "media_status",
|
||||
header: "Media",
|
||||
cell: ({ row }) =>
|
||||
row.original.media_status ? (
|
||||
<Badge variant="outline">{row.original.media_status}</Badge>
|
||||
) : (
|
||||
"—"
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "created_at",
|
||||
header: "Requested",
|
||||
cell: ({ row }) => formatDate(row.original.created_at),
|
||||
sortDescFirst: true,
|
||||
},
|
||||
];
|
||||
|
||||
export function JellyseerRequestsTable({ serviceId }: { serviceId: string }) {
|
||||
const { data: requests = [], isLoading, error } = useJellyseerRequests(serviceId);
|
||||
const [sorting, setSorting] = useState<SortingState>([
|
||||
{ id: "created_at", desc: true },
|
||||
]);
|
||||
const [search, setSearch] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>("open");
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = search.trim().toLowerCase();
|
||||
return requests.filter((r) => {
|
||||
const status = String(r.status ?? "");
|
||||
if (statusFilter === "open") {
|
||||
if (!OPEN_STATUSES.has(status)) return false;
|
||||
} else if (statusFilter !== "all" && status !== statusFilter) {
|
||||
return false;
|
||||
}
|
||||
if (q && !String(r.name ?? "").toLowerCase().includes(q)) return false;
|
||||
return true;
|
||||
});
|
||||
}, [requests, statusFilter, search]);
|
||||
|
||||
/* eslint-disable react-hooks/incompatible-library -- TanStack's useReactTable
|
||||
intentionally returns non-memoizable updater fns (controlled state). */
|
||||
const table = useReactTable({
|
||||
data: filtered,
|
||||
columns,
|
||||
state: { sorting },
|
||||
onSortingChange: setSorting,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
initialState: { pagination: { pageSize: 10 } },
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <Skeleton className="h-48 w-full" />;
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{error.message}</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="relative min-w-[180px] flex-1">
|
||||
<Search className="absolute left-2 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search requests…"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="pl-8"
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
value={statusFilter}
|
||||
onValueChange={(v) => setStatusFilter(v as StatusFilter)}
|
||||
>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="open">Open</SelectItem>
|
||||
<SelectItem value="all">All</SelectItem>
|
||||
<SelectItem value="pending">Pending</SelectItem>
|
||||
<SelectItem value="approved">Approved</SelectItem>
|
||||
<SelectItem value="declined">Declined</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="overflow-hidden rounded-lg border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((hg) => (
|
||||
<TableRow key={hg.id} className="hover:bg-transparent">
|
||||
{hg.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder ? null : (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1"
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
{header.column.getIsSorted() === "asc" ? (
|
||||
<ArrowUp className="size-3" />
|
||||
) : header.column.getIsSorted() === "desc" ? (
|
||||
<ArrowDown className="size-3" />
|
||||
) : (
|
||||
<ChevronsUpDown className="size-3 opacity-40" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={String(row.original.id ?? row.index)}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-16 text-center text-muted-foreground"
|
||||
>
|
||||
No requests.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<TablePagination
|
||||
pageIndex={table.getState().pagination.pageIndex}
|
||||
pageSize={table.getState().pagination.pageSize}
|
||||
pageSizeOptions={[10, 20, 50]}
|
||||
totalRows={table.getRowModel().rows.length}
|
||||
pageCount={table.getPageCount()}
|
||||
onPaginationChange={table.setPagination}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
fetchJellyseerrRequests,
|
||||
fetchJellyseerrStats,
|
||||
type JellyseerRequest,
|
||||
type JellyseerStatsResponse,
|
||||
} from "../api/jellyseerr";
|
||||
|
||||
@@ -15,3 +17,13 @@ export function useJellyseerrStats(jellyfinServiceId?: string) {
|
||||
retry: false,
|
||||
});
|
||||
}
|
||||
|
||||
/** Poll Jellyseerr requests for the Requests tab table. */
|
||||
export function useJellyseerRequests(jellyfinServiceId?: string) {
|
||||
return useQuery<JellyseerRequest[], Error, JellyseerRequest[]>({
|
||||
queryKey: ["jellyseerr", "requests", jellyfinServiceId ?? "default"],
|
||||
queryFn: () => fetchJellyseerrRequests(jellyfinServiceId),
|
||||
refetchInterval: 60_000,
|
||||
retry: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: frontend/src/pages/service-tabs
|
||||
|
||||
## role
|
||||
Provides service-specific tabbed UI content components rendered within service detail pages.
|
||||
Provides service-specific tabbed UI components for managing and monitoring individual service instances across various integrations (SSH, Alertmanager, Jellyfin, Authentik, Prometheus, etc.).
|
||||
## parent
|
||||
index: frontend/src/pages/.pi-map.index.md
|
||||
map: frontend/src/pages/.pi-map.md
|
||||
|
||||
@@ -4,7 +4,7 @@ dir: frontend/src/pages/service-tabs
|
||||
index: frontend/src/pages/service-tabs/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides service-specific tabbed UI content components rendered within service detail pages.
|
||||
Provides service-specific tabbed UI components for managing and monitoring individual service instances across various integrations (SSH, Alertmanager, Jellyfin, Authentik, Prometheus, etc.).
|
||||
## files
|
||||
- ActionsTab.tsx | Provides a UI tab for managing, editing, and running saved SSH tasks (shell or Python) within a service page. | exp: func:ActionsTab({ instance }: { instance: ServiceInstance }), call:useTasks, call:useSaveTask, call:useDeleteTask, call:useRunTask, call:useState, call:emptyTask, call:useMemo, call:tasks.find, call:useTaskRuns, call:setDraft, call:setDraftBaseline, call:setEditOpen, call:saveTask.mutateAsync, call:setTab, call:String, call:openEdit, call:tasks.map, call:initialFromTask, call:runTask.mutateAsync, call:selectedRuns.data.items.map, call:new Date(run.created_at * 1000).toLocaleString, call:deleteTask.mutate | dep: react, ../../types, ../../hooks/useSettings, ../../components/DialogFooter, ../../components/HoverEditButton, ../../components/SectionCard, ../../components/SelectionRailCard, @/components/ui/alert, @/components/ui/badge, @/components/ui/button, @/components/ui/card, @/components/ui/dialog, @/components/ui/input, @/components/ui/label, @/components/ui/select, @/components/ui/separator, @/components/ui/tabs, @/components/ui/textarea
|
||||
- AlertsTab.tsx | Renders an Alertmanager alerts tab showing alert summaries and an expandable list of active alerts scoped by instance ID. | exp: func:AlertsTab({ instance }: { instance: ServiceInstance }), call:useAlertmanagerAlerts, call:useAlertmanagerStatus, call:alertsSummary.alerts.map | dep: lucide-react, ../../hooks/useObservability, @/components/ui/card, @/components/ui/badge, @/components/ui/alert, @/components/ui/skeleton, @/components/ui/collapsible, ../../types, useObservability hooks, ui/card, ui/badge, ui/alert, ui/skeleton, ui/collapsible, types
|
||||
@@ -14,11 +14,11 @@ Provides service-specific tabbed UI content components rendered within service d
|
||||
- MessagingTab.tsx | Provides a UI for composing and sending HTML email messages to Authentik users via a mail queue system. | exp: func:MessagingTab({ instance }: { instance: ServiceInstance }), call:useState, call:useAuthentikUsers, call:useSendAuthentikMessage, call:(data?.items ?? []).filter, call:setSelectedEmails, call:next.has, call:next.delete, call:next.add, call:subject.trim, call:sendMessage.mutate, call:Array.from, call:sendMessage.data.request_id?.slice, call:setSearch, call:users.slice(0, 20).map, call:selectedEmails.has, call:toggleEmail, call:setSubject, call:setHtmlBody | dep: react, @/components/ui/alert, @/components/ui/button, @/components/ui/input, @/components/ui/label, @/components/ui/textarea, ../../hooks/useAuthentik, ../../types
|
||||
- MetricsTab.tsx | Renders a Prometheus metrics monitoring tab showing service health status and Node Exporter scrape targets for a given service instance. | exp: func:MetricsTab({ instance }: { instance: ServiceInstance }), call:usePrometheusStatus, call:usePrometheusTargets | dep: lucide-react, ../../hooks/useObservability, @/components/ui/card, @/components/ui/badge, @/components/ui/alert, @/components/ui/skeleton, ../../types, useObservability hooks, ui/card, ui/badge, ui/alert, ui/skeleton, types
|
||||
- OverviewTab.tsx | Renders a configurable per-service overview tab that displays and manages service-specific widgets in a responsive grid. | exp: func:OverviewTab({ instance }: { instance: ServiceInstance }), call:useWidgetInstances, call:useState, call:useMemo, call:widgets .filter((w) => w.enabled) .sort, call:setConfigOpen, call:visibleWidgets.map, call:setEditWidgetId | dep: react, @/components/ui/alert, @/components/ui/button, lucide-react, ../../hooks/useWidgets, ../../components/WidgetInstance, ../../components/WidgetConfigDialog, ../../types, ui/alert, ui/button, useWidgets hook, WidgetInstance component, WidgetConfigDialog component, types
|
||||
- RequestsTab.tsx | Displays Jellyseerr request statistics and recent requests for a Jellyfin service instance. | exp: func:RequestsTab({ instance }: { instance: ServiceInstance }), call:String( (instance.config as Record<string, unknown>).jellyseerr_url ?? "", ).trim, call:Boolean, call:useJellyseerrStats, call:(data?.stats ?? []).map, call:data.recent.slice(0, 12).map | dep: ../../types, ../../hooks/useJellyseer, @/components/ui/alert, @/components/ui/badge, @/components/ui/skeleton, ../../components/MetricCard, lucide-react, ServiceInstance, useJellyseerrStats, Alert, Badge, Skeleton, MetricCard
|
||||
- RequestsTab.tsx | Displays Jellyseerr request statistics and a requests table for a Jellyfin service instance, with configuration validation and loading/error states. | exp: func:RequestsTab({ instance }: { instance: ServiceInstance }), call:String( (instance.config as Record<string, unknown>).jellyseerr_url ?? "", ).trim, call:Boolean, call:useJellyseerrStats, call:(data?.stats ?? []).map | dep: ../../types, ../../hooks/useJellyseer, @/components/ui/alert, @/components/ui/skeleton, ../../components/MetricCard, ../../components/JellyseerRequestsTable, lucide-react, ServiceInstance type, useJellyseerrStats hook, Alert, Skeleton, MetricCard, JellyseerRequestsTable
|
||||
- UsersTab.tsx | Displays a searchable, paginated table of Authentik users for a given service instance. | exp: func:UsersTab({ instance }: { instance: ServiceInstance }), call:useState, call:useAuthentikUsers, call:Math.max, call:Math.ceil, call:setPage, call:setCommittedSearch, call:setSearch, call:handleSearch, call:users.map, call:Math.min | dep: react, @/components/ui/alert, @/components/ui/badge, @/components/ui/button, @/components/ui/input, @/components/ui/table, ../../types, ../../hooks/useAuthentik
|
||||
- index.ts | Maps service types to their corresponding content tab components for rendering a service page. | exp: ServiceTabComponent, ContentTab, OVERVIEW_TAB, func:serviceContentTabs(serviceType: string) → ContentTab[] | dep: react, ../../types, ./OverviewTab, ./AlertsTab, ./MetricsTab, ./MediaTab, ./RequestsTab, ./FilesTab, ./ActionsTab, ./JobsTab, ./UsersTab, ./MessagingTab, OverviewTab, AlertsTab, MetricsTab, MediaTab, RequestsTab, FilesTab, ActionsTab, JobsTab, UsersTab, MessagingTab
|
||||
## arch
|
||||
Component-per-tab pattern with a central registry (index.ts) mapping service types to their respective tab components.
|
||||
Tab-based component architecture with a central type-to-component mapping registry, each tab being a self-contained React component scoped by service instance ID with shared patterns for pagination, loading/error states, and data tables.
|
||||
## tags
|
||||
call:use, components, ui, tab, state, call:set, locale, string
|
||||
## symbols
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
* 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.
|
||||
* (secret). When configured, polls /api/jellyseerr/stats for the count grid and
|
||||
* /api/jellyseerr/requests for a sortable/filterable requests table. 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 { JellyseerRequestsTable } from "../../components/JellyseerRequestsTable";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
|
||||
export function RequestsTab({ instance }: { instance: ServiceInstance }) {
|
||||
@@ -75,36 +75,16 @@ export function RequestsTab({ instance }: { instance: ServiceInstance }) {
|
||||
))}
|
||||
</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}
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Requests
|
||||
</span>
|
||||
<JellyseerRequestsTable serviceId={instance.id} />
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Pin individual stats to a dashboard with the “Request stat”
|
||||
widget.
|
||||
Pin individual stats to a dashboard with the “Request
|
||||
stat” widget.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { RequestsTab } from "../RequestsTab";
|
||||
import { useJellyseerrStats } from "../../../hooks/useJellyseer";
|
||||
import {
|
||||
useJellyseerrStats,
|
||||
useJellyseerRequests,
|
||||
} from "../../../hooks/useJellyseer";
|
||||
import type { JellyseerStatsResponse } from "../../../api/jellyseerr";
|
||||
import type { ServiceInstance } from "../../../types";
|
||||
|
||||
// Mock the stats hook so the tab renders without a QueryClientProvider and we
|
||||
// can drive the rendered state directly.
|
||||
// Mock both hooks so the tab + its table render without a QueryClientProvider.
|
||||
vi.mock("../../../hooks/useJellyseer", () => ({
|
||||
useJellyseerrStats: vi.fn(),
|
||||
useJellyseerRequests: vi.fn(),
|
||||
}));
|
||||
const mockUseJellyseerrStats = vi.mocked(useJellyseerrStats);
|
||||
const mockUseJellyseerRequests = vi.mocked(useJellyseerRequests);
|
||||
type StatsResult = ReturnType<typeof useJellyseerrStats>;
|
||||
type RequestsResult = ReturnType<typeof useJellyseerRequests>;
|
||||
|
||||
function mockStats(result: {
|
||||
data: JellyseerStatsResponse | undefined;
|
||||
@@ -20,6 +25,11 @@ function mockStats(result: {
|
||||
}) {
|
||||
// UseQueryResult has many fields; cast the partial we care about.
|
||||
mockUseJellyseerrStats.mockReturnValue(result as unknown as StatsResult);
|
||||
mockUseJellyseerRequests.mockReturnValue({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
} as unknown as RequestsResult);
|
||||
}
|
||||
|
||||
function makeInstance(
|
||||
@@ -57,7 +67,9 @@ describe("RequestsTab", () => {
|
||||
mockStats({ data: undefined, isLoading: false, error: null });
|
||||
render(
|
||||
<RequestsTab
|
||||
instance={makeInstance({ jellyseerr_url: "https://requests.example.com" })}
|
||||
instance={makeInstance({
|
||||
jellyseerr_url: "https://requests.example.com",
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText(/not configured/i)).toBeInTheDocument();
|
||||
@@ -91,11 +103,12 @@ describe("RequestsTab", () => {
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("https://requests.example.com")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("https://requests.example.com"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText(/not configured/i)).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Pending")).toBeInTheDocument();
|
||||
expect(screen.getByText("3")).toBeInTheDocument();
|
||||
expect(screen.getByText("Inception")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("surfaces a fetch error", () => {
|
||||
|
||||
Reference in New Issue
Block a user