diff --git a/hub/AGENTS.md b/hub/AGENTS.md index 8dbc35d1..12bf4c91 100644 --- a/hub/AGENTS.md +++ b/hub/AGENTS.md @@ -16,6 +16,8 @@ The in-repo JupyterHub image (`mddash-hub`) powering MDDash's hub: stock `quay.i - **`window.appConfig` injection**: each entry HTML has an inline ` diff --git a/hub/ui/spawn_pending.html b/hub/ui/spawn_pending.html index 54363e6f..6618cb98 100644 --- a/hub/ui/spawn_pending.html +++ b/hub/ui/spawn_pending.html @@ -15,7 +15,7 @@ userName: {{ user.name | tojson }}, xsrf: {{ xsrf_token | default("") | tojson }}, logoutUrl: {{ logout_url | default("") | tojson }}, - adminAccess: {{ admin_access | default(false) | tojson }}, + adminAccess: {{ admin_access | default(user.admin) | default(false) | tojson }}, announcement: {{ announcement | default(none) | tojson }}, progressUrl: {{ progress_url | default("") | tojson }} }; diff --git a/hub/ui/src/components/Hero.tsx b/hub/ui/src/components/Hero.tsx new file mode 100644 index 00000000..304d7172 --- /dev/null +++ b/hub/ui/src/components/Hero.tsx @@ -0,0 +1,101 @@ +import type { ReactNode } from "react" + +import { Link, Muted, Small } from "@e-infra/design-system" +import { Clock, type LucideIcon } from "lucide-react" + +/** Shared hero chrome for the hub status pages (home, spawn, spawn_pending, stop_pending, not_running). */ + +/** Centered hero shell. */ +export function PageHero({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ) +} + +type StatusTone = "primary" | "success" | "error" + +const TONE_CLASSES: Record = { + primary: "bg-primary text-primary-foreground", + success: "bg-success text-success-foreground", + error: "bg-error text-error-foreground", +} + +/** Decorative status circle at the top of every hero. */ +export function StatusIcon({ tone, icon: Icon }: { tone: StatusTone; icon: LucideIcon }) { + return ( + + ) +} + +/** + * H1 + body copy block. Set `ariaLive` when the state can flip while the page + * is open (e.g. EventSource reports a spawn failure) so screen readers announce it. + */ +export function HeroHeading({ children, ariaLive = false }: { children: ReactNode; ariaLive?: boolean }) { + return ( +
+ {children} +
+ ) +} + +/** Small clock hint under the hero. */ +export function WaitHint({ children }: { children: ReactNode }) { + return ( +
+
+ ) +} + +/** Collapsible raw event/failure log; pass `open` for failed states. */ +export function DetailsLog({ + open = false, + summary, + children, +}: { + open?: boolean + summary: string + children: ReactNode +}) { + return ( +
+ {summary} +
{children}
+
+ ) +} + +/** One log line inside DetailsLog; pass pre-rendered safe `html` or plain children. */ +export function LogEntry({ html, children }: { html?: string; children?: ReactNode }) { + const className = "text-text-muted text-xs" + if (html != null) { + return + } + return {children} +} + +/** Support escalation link. */ +export const SUPPORT_ISSUES_URL = "https://github.com/CERIT-SC/mddash/issues" + +export function SupportNote() { + return ( + + Still not working?{" "} + + Contact support + + + ) +} + +/** Caption below the Start my server action. */ +export const START_HINT = "This starts your personal notebook server. It usually takes up to a minute." + +/** Body copy for every spawn-failure state. */ +export const FAILED_LEAD = "This usually happens when the system is busy or restarting — it is not your fault." diff --git a/hub/ui/src/components/HubHeader.tsx b/hub/ui/src/components/HubHeader.tsx index 51a50de8..aabbfaf0 100644 --- a/hub/ui/src/components/HubHeader.tsx +++ b/hub/ui/src/components/HubHeader.tsx @@ -1,6 +1,14 @@ -import type { ReactNode } from "react" - -import { Button, Header, HeaderContent, HeaderLeft, HeaderRight } from "@e-infra/design-system" +import { + Button, + Header, + HeaderContent, + HeaderLeft, + HeaderRight, + NavigationMenu, + NavigationMenuItem, + NavigationMenuLink, + NavigationMenuList, +} from "@e-infra/design-system" import { LogOut } from "lucide-react" import { Logo } from "./Logo" @@ -16,20 +24,6 @@ export interface HubHeaderProps { current?: "home" | "token" | "admin" } -function NavLink({ href, active, children }: { href: string; active?: boolean; children: ReactNode }) { - return ( - - {children} - - ) -} - export function HubHeader({ baseUrl, userName, adminAccess, logoutUrl, current }: HubHeaderProps) { return (
@@ -40,19 +34,27 @@ export function HubHeader({ baseUrl, userName, adminAccess, logoutUrl, current } MDDash {userName ? ( - + + + + + Home + + + + + Get Token + + + {adminAccess ? ( + + + Admin + + + ) : null} + + ) : null} diff --git a/hub/ui/src/components/IconCard.tsx b/hub/ui/src/components/IconCard.tsx new file mode 100644 index 00000000..e535fa85 --- /dev/null +++ b/hub/ui/src/components/IconCard.tsx @@ -0,0 +1,36 @@ +import type { ReactNode } from "react" + +import { CardDescription, CardHeader, CardTitle } from "@e-infra/design-system" +import type { LucideIcon } from "lucide-react" + +type IconTone = "primary" | "success" | "warning" | "error" + +const TONE_CLASSES: Record = { + primary: "text-primary", + success: "text-success", + warning: "text-warning", + error: "text-error", +} + +/** Card header with a colored icon before the title — used by every hub card page. */ +export function IconCardHeader({ + icon: Icon, + tone = "primary", + title, + description, +}: { + icon: LucideIcon + tone?: IconTone + title: ReactNode + description?: ReactNode +}) { + return ( + + + + {description ? {description} : null} + + ) +} diff --git a/hub/ui/src/components/Layouts.tsx b/hub/ui/src/components/Layouts.tsx index 634aa8c4..363ae7d9 100644 --- a/hub/ui/src/components/Layouts.tsx +++ b/hub/ui/src/components/Layouts.tsx @@ -1,12 +1,17 @@ import type { ReactNode } from "react" -import { Toaster } from "@e-infra/design-system" +import { Content, Toaster } from "@e-infra/design-system" import { Announcement } from "./Announcement" import { HubHeader } from "./HubHeader" import { Logo } from "./Logo" import { ThemeToggle } from "./ThemeToggle" +/** Wide content column for data pages (token, admin). */ +export function PageBody({ children }: { children: ReactNode }) { + return
{children}
+} + /** Standard authenticated page: header with nav, optional announcement, centered content column. */ export function AuthedLayout({ baseUrl, @@ -35,7 +40,9 @@ export function AuthedLayout({ current={current} /> -
{children}
+
+ {children} +
) diff --git a/hub/ui/src/pages/ErrorPage.tsx b/hub/ui/src/pages/ErrorPage.tsx index 43368e9a..afbdf774 100644 --- a/hub/ui/src/pages/ErrorPage.tsx +++ b/hub/ui/src/pages/ErrorPage.tsx @@ -1,8 +1,9 @@ import { useEffect } from "react" -import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, P } from "@e-infra/design-system" +import { Button, Card, CardContent, P } from "@e-infra/design-system" import { Home, TriangleAlert } from "lucide-react" +import { IconCardHeader } from "../components/IconCard" import { CenteredLayout } from "../components/Layouts" import { getAppConfig } from "../lib/config" @@ -59,15 +60,12 @@ export function ErrorPage({ notFound = false }: { notFound?: boolean }) { return ( - - - - {friendlyTitle} - - - {cfg.statusCode} {cfg.statusMessage} - - + {cfg.messageHtml ? (

diff --git a/hub/ui/src/pages/admin.tsx b/hub/ui/src/pages/admin.tsx index 97125a57..d13f93ba 100644 --- a/hub/ui/src/pages/admin.tsx +++ b/hub/ui/src/pages/admin.tsx @@ -14,11 +14,11 @@ import { Button, Card, CardContent, - CardDescription, - CardHeader, - CardTitle, Input, Label, + Link, + Skeleton, + Small, Switch, Table, TableBody, @@ -30,7 +30,8 @@ import { import { Play, Square, Trash2, UserPlus, Users } from "lucide-react" import { toast } from "sonner" -import { AuthedLayout } from "../components/Layouts" +import { IconCardHeader } from "../components/IconCard" +import { AuthedLayout, PageBody } from "../components/Layouts" import { HubApi, type HubUserModel } from "../lib/api" import { getAppConfig } from "../lib/config" import { formatTime } from "../lib/format" @@ -95,15 +96,13 @@ export function AdminPage() { current="admin" announcement={cfg.announcement} > -

+ - - - - Users - - Start or stop user servers, grant admin rights, add or remove users. - +
@@ -127,9 +126,13 @@ export function AdminPage() { {forbidden ? ( You do not have permission to administer this hub. ) : users === null ? ( -

Loading users…

+
+ {[0, 1, 2].map((i) => ( + + ))} +
) : users.length === 0 ? ( -

No users found.

+ No users found. ) : (
@@ -169,9 +172,7 @@ export function AdminPage() { Running - - open - + open ) : status === "starting" ? ( "Starting…" @@ -187,7 +188,7 @@ export function AdminPage() { {status === "running" || status === "starting" ? ( - - - ) : null} - - {status === "stopped" ? ( + + + + +

+ {status === "running" ? "Your server is running" : null} + {status === "stopped" ? "Your server is offline" : null} + {status === "starting" ? "Starting your server…" : null} + {status === "stopping" ? "Stopping your server…" : null} +

+ + {status === "running" ? "Your personal notebook environment is up." : null} + {status === "stopped" ? "Your personal notebook server is not running." : null} + {status === "starting" ? "You will be redirected automatically when it's ready for you." : null} + {status === "stopping" ? "You can start it again once it has finished stopping." : null} + +
+ + {status === "running" ? ( + <> + + + + ) : null} + + {status === "stopped" ? ( + <> - ) : null} - - + {START_HINT} + + ) : null} +
) } diff --git a/hub/ui/src/pages/login.tsx b/hub/ui/src/pages/login.tsx index f9d50b07..b32faf87 100644 --- a/hub/ui/src/pages/login.tsx +++ b/hub/ui/src/pages/login.tsx @@ -1,17 +1,7 @@ -import { - Alert, - AlertDescription, - AlertTitle, - Button, - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, - Small, -} from "@e-infra/design-system" +import { Alert, AlertDescription, AlertTitle, Button, Card, CardContent, Small } from "@e-infra/design-system" import { KeyRound, TriangleAlert } from "lucide-react" +import { IconCardHeader } from "../components/IconCard" import { CenteredLayout } from "../components/Layouts" import { DEV_FALLBACK_BASE_URL, getAppConfig } from "../lib/config" import { mount } from "../lib/mount" @@ -35,13 +25,7 @@ export function LoginPage() { return ( - - - - Sign in - - Access your MDDash workspace - + {!window.isSecureContext ? ( @@ -56,8 +40,6 @@ export function LoginPage() { ) : null} {cfg.loginError ? {cfg.loginError} : null} {cfg.customHtml ? ( - // Raw HTML is provided by the configured authenticator (same - // rendering contract as the stock login.html template).
) : ( - - + + + {!cfg.failed && cfg.implicitSpawnSeconds > 0 ? ( + It will be restarted automatically. If you are not redirected in a few seconds, click below. + ) : null} + + + + {cfg.failed ? : null} + + {cfg.failed ? : null} + + {cfg.failed ? ( + + {cfg.failedHtmlMessage ? ( + + ) : cfg.failedMessage ? ( + {cfg.failedMessage} + ) : ( + No failure details available. + )} + + ) : null} + + {!cfg.failed ? {START_HINT} : null} + ) } diff --git a/hub/ui/src/pages/oauth.tsx b/hub/ui/src/pages/oauth.tsx index d503a075..c216a2f8 100644 --- a/hub/ui/src/pages/oauth.tsx +++ b/hub/ui/src/pages/oauth.tsx @@ -2,7 +2,6 @@ import { Button, Card, CardContent, - CardDescription, CardFooter, CardHeader, CardTitle, @@ -12,6 +11,7 @@ import { } from "@e-infra/design-system" import { ShieldCheck } from "lucide-react" +import { IconCardHeader } from "../components/IconCard" import { CenteredLayout } from "../components/Layouts" import { getAppConfig } from "../lib/config" import { mount } from "../lib/mount" @@ -44,15 +44,11 @@ export function OAuthPage() { return ( - - - - Authorize access - - - An application is requesting authorization to access data associated with your JupyterHub account. - - +

@@ -74,8 +70,13 @@ export function OAuthPage() { {cfg.scopeDescriptions.map((scope, i) => (

Requesting your server…

- + + + + +

{failed ? "Failed to start your server" : "Starting your server"}

+ {failed ? ( + {FAILED_LEAD} ) : ( - <> - - - - Spawn failed - - {error} - - - + You will be redirected to the progress page in a moment. )} -
-
+ + + {failed ? ( + <> + + + + + {errorHtml ? ( + + ) : (errorText ?? error) ? ( + {errorText ?? error} + ) : ( + No failure details available. + )} + + + ) : ( +