Rebase services-as-hub-ia onto mobile-responsive-parity
Combine both branches into a single coherent branch: - Full mobile responsive parity (useIsMobile, MobileCardRow, SheetForm, .mobile-touch-target, mobile cards, SheetForm forms, 44px targets, dirty-state confirm, TablePagination, refetchIntervalInBackground). - Full services-as-hub IA (data-driven nav, service-page tab skeleton, new service types, Authentik directory + messaging, named dashboards, legacy routes 404, Observability split, Jellyseerr absorbed). Enhancement: service tabs now use mobile-parity primitives: - MediaTab: MobileCardRow below md (title/size/HDR/library/year) + TablePagination; DataTable at md+ (desktop branch preserved). - FilesTab: MobileCardRow below md (name/type/size/modified) + handleRowClick; DataTable at md+. - ServicePage: SheetForm branch below md (open-on-mount, sticky header + save bar, cancel navigates back to /services, dirty-state guard). - Dashboard: single-column + section anchors below md (from mobile-parity) + empty-state CTA (from services-hub). - App.tsx: useIsMobile() replaces inline matchMedia (from mobile-parity) + data-driven useNavItems (from services-hub). - Backup tables (BackupAlerts/Jobs/Runs) already have MobileCardRow from mobile-parity; JobsTab inherits mobile behavior through its sub-components. Conflict resolutions: - Backend: entirely from services-hub (mobile didn't touch it). - Deleted pages (Media/FileBrowser/Actions/Users/UsersPage/Applications/ ObservabilityPage/BackupsPage + hooks/useUsers + tests): kept deleted (services-hub deleted them; content moved into service tabs). - New service-tabs/*: from services-hub, enhanced with mobile patterns. - App.tsx: services-hub's data-driven nav + mobile-parity's useIsMobile. - Dashboard.tsx: merged (services-hub CTA + mobile-parity sections/anchors). - ServicePage.tsx: services-hub's tab skeleton + mobile-parity's SheetForm. - Primitives (useIsMobile/mobile-card/sheet-form/etc.): from mobile-parity. 117 frontend tests pass (mobile-parity's 122 - 5 deleted page tests + services-hub's new tab/dashboard tests); 271 backend tests pass; lint/ build green both sides.
This commit is contained in:
+71
-59
@@ -5,7 +5,6 @@ import {
|
||||
NavLink,
|
||||
useLocation,
|
||||
Outlet,
|
||||
Navigate,
|
||||
} from "react-router-dom";
|
||||
import {
|
||||
QueryClient,
|
||||
@@ -13,22 +12,22 @@ import {
|
||||
useQuery,
|
||||
} from "@tanstack/react-query";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { AuthProvider, useAuth } from "react-oidc-context";
|
||||
import { Dashboard } from "./pages/Dashboard";
|
||||
import { Applications } from "./pages/Applications";
|
||||
import { NamedDashboardPage } from "./pages/NamedDashboardPage";
|
||||
import { Settings } from "./pages/Settings";
|
||||
import { UsersPage } from "./pages/Users";
|
||||
import { FileBrowser } from "./pages/FileBrowser";
|
||||
import { Actions } from "./pages/Actions";
|
||||
import BackupsPage from "./components/BackupsPage";
|
||||
import { ObservabilityPage } from "./components/ObservabilityPage";
|
||||
import { ServicePage } from "./pages/ServicePage";
|
||||
import { ServiceTypePage } from "./pages/ServiceTypePage";
|
||||
import { ServicesPage } from "./pages/ServicesPage";
|
||||
import { getOidcConfig, isOidcConfigured, setAccessToken } from "./auth";
|
||||
import { fetchAppVersion } from "./api/client";
|
||||
import { FRONTEND_VERSION_LABEL } from "./version";
|
||||
import { usePersistentState } from "./hooks/usePersistentState";
|
||||
import { useIsMobile } from "./hooks/useIsMobile";
|
||||
import { useServiceInstances } from "./hooks/useServices";
|
||||
import { useDashboards } from "./hooks/useDashboards";
|
||||
import { configuredNavEntries } from "./integrations/navEntries";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -45,12 +44,6 @@ import {
|
||||
} from "@/components/ui/sheet";
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Activity,
|
||||
DatabaseBackup,
|
||||
Monitor,
|
||||
Users,
|
||||
Zap,
|
||||
FolderOpen,
|
||||
Settings as SettingsIcon,
|
||||
Menu,
|
||||
Sun,
|
||||
@@ -59,6 +52,7 @@ import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Boxes,
|
||||
LayoutTemplate,
|
||||
} from "lucide-react";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
@@ -66,9 +60,6 @@ const queryClient = new QueryClient({
|
||||
queries: {
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
// Pause interval-based refetches (widgets ~30s, queue status 5s,
|
||||
// media build progress 1s) when the tab is hidden. Saves battery on
|
||||
// mobile (D8 follow-up). Build progress polls resume on return.
|
||||
refetchIntervalInBackground: false,
|
||||
},
|
||||
},
|
||||
@@ -92,18 +83,40 @@ function useDarkMode() {
|
||||
return [darkMode, () => setDarkMode((prev) => !prev)] as const;
|
||||
}
|
||||
|
||||
// Navigation items for sidebar
|
||||
const navItems = [
|
||||
{ path: "/", label: "Dashboard", icon: LayoutDashboard },
|
||||
{ path: "/observability", label: "Observability", icon: Activity },
|
||||
{ path: "/media", label: "Media", icon: Monitor },
|
||||
{ path: "/files", label: "Files", icon: FolderOpen },
|
||||
{ path: "/backups", label: "Backups", icon: DatabaseBackup },
|
||||
{ path: "/users", label: "Users", icon: Users },
|
||||
{ path: "/actions", label: "Actions", icon: Zap },
|
||||
{ path: "/services", label: "Services", icon: Boxes },
|
||||
{ path: "/settings", label: "Settings", icon: SettingsIcon },
|
||||
];
|
||||
// Navigation items are data-driven (spec R1). Built from configured services + dashboards.
|
||||
interface NavItem {
|
||||
path: string;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
}
|
||||
|
||||
function useNavItems() {
|
||||
const { data: services = [] } = useServiceInstances();
|
||||
const { data: dashboards = [] } = useDashboards();
|
||||
|
||||
return useMemo<NavItem[]>(() => {
|
||||
const configuredTypes = new Set(
|
||||
services.filter((s) => s.enabled).map((s) => s.service_type),
|
||||
);
|
||||
const serviceEntries = configuredNavEntries(configuredTypes).map((e) => ({
|
||||
path: e.path,
|
||||
label: e.label,
|
||||
icon: e.icon,
|
||||
}));
|
||||
const dashboardEntries = dashboards.map((d) => ({
|
||||
path: `/d/${d.slug}`,
|
||||
label: d.label,
|
||||
icon: LayoutTemplate,
|
||||
}));
|
||||
return [
|
||||
{ path: "/", label: "Dashboard", icon: LayoutDashboard },
|
||||
...dashboardEntries,
|
||||
...serviceEntries,
|
||||
{ path: "/services", label: "Services", icon: Boxes },
|
||||
{ path: "/settings", label: "Settings", icon: SettingsIcon },
|
||||
];
|
||||
}, [services, dashboards]);
|
||||
}
|
||||
|
||||
function Sidebar({
|
||||
collapsed,
|
||||
@@ -115,6 +128,7 @@ function Sidebar({
|
||||
isMobile: boolean;
|
||||
}) {
|
||||
const location = useLocation();
|
||||
const navItems = useNavItems();
|
||||
|
||||
if (isMobile) return null;
|
||||
|
||||
@@ -199,11 +213,12 @@ function Sidebar({
|
||||
function MobileDrawer() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const location = useLocation();
|
||||
const navItems = useNavItems();
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="mobile-touch-target md:hidden">
|
||||
<Button variant="ghost" size="icon" className="md:hidden">
|
||||
<Menu className="h-5 w-5" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
@@ -263,6 +278,7 @@ function TopBar({
|
||||
});
|
||||
const backendLabel = appVersion?.backend_label || "…";
|
||||
|
||||
const navItems = useNavItems();
|
||||
const pageTitle =
|
||||
navItems.find((item) => item.path === location.pathname)?.label ||
|
||||
"Dashboard";
|
||||
@@ -290,7 +306,7 @@ function TopBar({
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onToggleDarkMode}
|
||||
className="mobile-touch-target h-8 w-8"
|
||||
className="h-8 w-8"
|
||||
>
|
||||
{darkMode ? (
|
||||
<Sun className="h-4 w-4" />
|
||||
@@ -303,7 +319,7 @@ function TopBar({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onSignOut}
|
||||
className="mobile-touch-target gap-2"
|
||||
className="gap-2"
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Logout</span>
|
||||
@@ -428,6 +444,18 @@ function AuthenticatedApp() {
|
||||
);
|
||||
}
|
||||
|
||||
function NotFoundPage() {
|
||||
return (
|
||||
<div className="flex min-h-[50vh] flex-col items-center justify-center gap-4">
|
||||
<h2 className="text-xl font-semibold">Not found</h2>
|
||||
<p className="text-sm text-muted-foreground">This page doesn't exist.</p>
|
||||
<Button asChild>
|
||||
<NavLink to="/">Back to dashboard</NavLink>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AppInner() {
|
||||
const [darkMode, toggleDarkMode] = useDarkMode();
|
||||
|
||||
@@ -439,26 +467,18 @@ function AppInner() {
|
||||
<Routes>
|
||||
<Route element={<AuthenticatedApp />}>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route
|
||||
path="/monitoring"
|
||||
element={<Navigate to="/observability" replace />}
|
||||
/>
|
||||
<Route path="/media" element={<Applications />} />
|
||||
<Route
|
||||
path="/applications"
|
||||
element={<Navigate to="/media" replace />}
|
||||
/>
|
||||
<Route path="/users" element={<UsersPage />} />
|
||||
<Route path="/actions" element={<Actions />} />
|
||||
<Route path="/files" element={<FileBrowser />} />
|
||||
<Route path="/backups" element={<BackupsPage />} />
|
||||
<Route path="/observability" element={<ObservabilityPage />} />
|
||||
<Route path="/d/:slug" element={<NamedDashboardPage />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/services" element={<ServicesPage />} />
|
||||
<Route
|
||||
path="/services/:serviceType"
|
||||
element={<ServiceTypePage />}
|
||||
/>
|
||||
<Route
|
||||
path="/services/:serviceType/:serviceId"
|
||||
element={<ServicePage />}
|
||||
/>
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
@@ -475,26 +495,18 @@ function AppInner() {
|
||||
}
|
||||
>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route
|
||||
path="/monitoring"
|
||||
element={<Navigate to="/observability" replace />}
|
||||
/>
|
||||
<Route path="/media" element={<Applications />} />
|
||||
<Route
|
||||
path="/applications"
|
||||
element={<Navigate to="/media" replace />}
|
||||
/>
|
||||
<Route path="/users" element={<UsersPage />} />
|
||||
<Route path="/actions" element={<Actions />} />
|
||||
<Route path="/files" element={<FileBrowser />} />
|
||||
<Route path="/backups" element={<BackupsPage />} />
|
||||
<Route path="/observability" element={<ObservabilityPage />} />
|
||||
<Route path="/d/:slug" element={<NamedDashboardPage />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/services" element={<ServicesPage />} />
|
||||
<Route
|
||||
path="/services/:serviceType"
|
||||
element={<ServiceTypePage />}
|
||||
/>
|
||||
<Route
|
||||
path="/services/:serviceType/:serviceId"
|
||||
element={<ServicePage />}
|
||||
/>
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
||||
Reference in New Issue
Block a user