From 9271d0e3ac18aa5f29a288bc1f9f837c59190a1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Dvo=C5=99=C3=A1k?= <150935816+Dvorinka@users.noreply.github.com> Date: Sat, 10 Oct 2026 17:20:44 +0200 Subject: [PATCH] feat(ui): full frontend redesign to the Sentry-style design system (#86) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(ui): full frontend redesign to the Sentry-style design system Replace the legacy UI with the dark, mono-accent design language from design/23-sentry.html across every non-canvas surface. - PlatformShell: fixed 252px sidebar with grouped nav (Platform / Operate / Workspace), brand mark, user card, and a topbar with search (Cmd+K palette), env badge, upgrade, notifications, and theme toggle; mobile collapses to a top bar with horizontal nav - New shared component kit in shared/components/sentry.tsx: SPageHead, SStat, SCard, STable, SSection, SPill, Ticks, Seg, GhostBtn/QuietBtn/IconBtn, status tone helpers - New DashboardPage at / — stats row, per-node CPU chart with 1h/24h/7d ranges, resource usage tick strips, anomaly box, node summary table, recent deployments and project cards; real API data with demo-mode fixtures - Landing page moved to /landing and rebuilt: live host telemetry window, install strip, feature grid, template catalog, docs - Every page (Projects, Templates, Builds, Databases, HA, Operations, Activity, Security, Usage, Docs, People, Settings, Audit Logs, Webhooks, Admin, NodeDetail, ServiceDetail, auth, setup, not-found) migrated to the new kit and tokens - Nav active state resolves by longest matching href - Canvas internals intentionally untouched — separate pass Verified: tsc --noEmit, eslint, vite build, Playwright walkthrough of every route at desktop and mobile widths (demo + live session). Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(ui): deep audit pass — palette navigation, crash fix, honest badges - CommandPalette: full route navigation, project deep-links fetched on open, Create group only where onAddService is wired, dead Connect items removed, decorative kbd hints dropped; workspace passes its own view targets - NodeDetailPage crash: container status arrives as a Docker state object, not a string — flattened in listAgentContainers and guarded at render for persisted-cache payloads - env badge reflects import.meta.env.MODE instead of hardcoded 'production' - Relative times capped: >1y or future timestamps render 'n/a'/'—' (zero-time heartbeats no longer show 739898d ago) - Post-auth and setup redirects land on / (dashboard is the index) Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(ui): use SPageHead actions prop, satisfy tsc -b literal types Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- app/frontend/index.html | 6 + app/frontend/public/containr.svg | 6 +- app/frontend/src/app/App.tsx | 8 +- app/frontend/src/app/layout/PlatformShell.tsx | 260 ++--- .../features/activity/pages/ActivityPage.tsx | 72 +- .../src/features/admin/pages/AdminPage.tsx | 261 ++--- .../features/audit/pages/AuditLogsPage.tsx | 180 ++-- app/frontend/src/features/auth/pages.tsx | 73 +- .../src/features/builds/pages/BuildsPage.tsx | 358 +++---- .../dashboard/pages/DashboardPage.tsx | 662 +++++++++++++ .../src/features/landing/LandingPage.tsx | 272 ++++-- .../src/features/nodes/NodeDetailPage.tsx | 160 +-- .../operations/pages/OperationsPage.tsx | 90 +- .../features/projects/pages/ProjectsPage.tsx | 422 ++++---- app/frontend/src/features/secondary/pages.tsx | 302 +++--- .../secondary/pages/ComponentShowcase.tsx | 75 +- .../service/pages/ServiceDetailPage.tsx | 689 ++++++------- .../service/pages/ServiceMetricsDashboard.tsx | 79 +- .../src/features/setup/SetupWizardPage.tsx | 93 +- .../templates/builder/TemplateBuilder.tsx | 30 +- .../templates/pages/TemplatesPage.tsx | 161 ++-- .../features/webhooks/pages/WebhooksPage.tsx | 164 ++-- .../workspace/canvas/ProjectCanvas.tsx | 57 +- .../features/workspace/canvas/inspector.tsx | 118 +-- .../src/features/workspace/canvas/nodes.tsx | 64 +- .../workspace/canvas/service-visuals.tsx | 10 +- .../workspace/components/MetricsDashboard.tsx | 123 ++- .../components/SharedVariablesSection.tsx | 24 +- .../workspace/pages/ProjectWorkspacePage.tsx | 242 ++--- app/frontend/src/index.css | 909 ++++++++++++++---- app/frontend/src/lib/api-client.ts | 12 +- app/frontend/src/lib/time.ts | 5 + app/frontend/src/shared/components/Card.tsx | 4 +- app/frontend/src/shared/components/Charts.tsx | 8 +- .../src/shared/components/CommandPalette.tsx | 233 ++--- .../src/shared/components/DemoGate.tsx | 10 +- .../src/shared/components/DonutChart.tsx | 4 +- .../shared/components/EnhancedMetricCard.tsx | 431 +++------ .../src/shared/components/ErrorBoundary.tsx | 18 +- .../src/shared/components/MetricCard.tsx | 169 +--- .../src/shared/components/StatusBadge.tsx | 105 +- app/frontend/src/shared/components/index.ts | 1 + .../src/shared/components/sentry-utils.ts | 18 + app/frontend/src/shared/components/sentry.tsx | 372 +++++++ 44 files changed, 4210 insertions(+), 3150 deletions(-) create mode 100644 app/frontend/src/features/dashboard/pages/DashboardPage.tsx create mode 100644 app/frontend/src/shared/components/sentry-utils.ts create mode 100644 app/frontend/src/shared/components/sentry.tsx diff --git a/app/frontend/index.html b/app/frontend/index.html index 5f26d19..c80b50e 100644 --- a/app/frontend/index.html +++ b/app/frontend/index.html @@ -5,6 +5,12 @@ containr +
diff --git a/app/frontend/public/containr.svg b/app/frontend/public/containr.svg index 256a12a..f1ef974 100644 --- a/app/frontend/public/containr.svg +++ b/app/frontend/public/containr.svg @@ -1,12 +1,12 @@ - - + + - + diff --git a/app/frontend/src/app/App.tsx b/app/frontend/src/app/App.tsx index 7930e2e..0812169 100644 --- a/app/frontend/src/app/App.tsx +++ b/app/frontend/src/app/App.tsx @@ -14,6 +14,7 @@ import { OperationsPage } from '@/features/operations/pages/OperationsPage'; import { ActivityPage } from '@/features/activity/pages/ActivityPage'; import { AdminPage } from '@/features/admin/pages/AdminPage'; import { LandingPage } from '@/features/landing/LandingPage'; +import { DashboardPage } from '@/features/dashboard/pages/DashboardPage'; import { useBranding } from '@/lib/use-branding'; import { DocsPage, @@ -92,7 +93,7 @@ function AdminRequired() {
-

Admin access required

+

Admin access required

This area is restricted to the platform administrator.

@@ -115,7 +116,7 @@ function NotFoundPage() {
-

Page not found

+

Page not found

The page you are looking for does not exist or was moved.

@@ -135,7 +136,7 @@ export default function App() { return ( - } /> + } /> } /> } /> } /> @@ -147,6 +148,7 @@ export default function App() { } /> }> + } /> } /> } /> } /> diff --git a/app/frontend/src/app/layout/PlatformShell.tsx b/app/frontend/src/app/layout/PlatformShell.tsx index 35e0a62..4a47aab 100644 --- a/app/frontend/src/app/layout/PlatformShell.tsx +++ b/app/frontend/src/app/layout/PlatformShell.tsx @@ -9,11 +9,13 @@ import { ChartBar, CheckCircle2, ChevronsLeft, + ChevronsUpDown, Clock, Container, Database, FileText, FolderKanban, + LayoutDashboard, LayoutTemplate, LogIn, LogOut, @@ -41,22 +43,26 @@ import { markAllNotificationsRead, markNotificationRead, pullUpgradeImage, + listProjects, } from '@/lib/api-client'; import { signOutAuthSession } from '@/lib/auth-client'; import { isDemoSearch } from '@/lib/demo-mode'; +import { demoProjects } from '@/lib/demo-data'; import { useAuthSession } from '@/lib/use-auth-session'; -import { BannerBar, BrandWordmark, useToast } from '@/shared/components'; +import { BannerBar, BrandWordmark, CommandPalette, useToast } from '@/shared/components'; type NavItem = { label: string; href: string; icon: typeof FolderKanban; hint: string }; type NavSection = { title: string; items: NavItem[] }; const navSections: NavSection[] = [ { - title: 'Build', + title: 'Platform', items: [ + { label: 'Dashboard', href: '/', icon: LayoutDashboard, hint: 'Fleet overview' }, { label: 'Projects', href: '/projects', icon: FolderKanban, hint: 'Services & deployments' }, { label: 'Templates', href: '/templates', icon: LayoutTemplate, hint: 'Deployable presets' }, { label: 'Builds', href: '/builds', icon: FileText, hint: 'Build history' }, + { label: 'Databases', href: '/databases', icon: Database, hint: 'Managed data services' }, ], }, { @@ -64,7 +70,6 @@ const navSections: NavSection[] = [ items: [ { label: 'Operations', href: '/operations', icon: Activity, hint: 'Live jobs & failures' }, { label: 'Activity', href: '/activity', icon: Clock, hint: 'Platform event feed' }, - { label: 'Databases', href: '/databases', icon: Database, hint: 'Managed data services' }, { label: 'High Availability', href: '/ha', icon: ShieldCheck, hint: 'Failover & health' }, { label: 'Security', href: '/security', icon: Shield, hint: 'Scans & compliance' }, { label: 'Usage', href: '/usage', icon: ChartBar, hint: 'Resource consumption' }, @@ -164,6 +169,18 @@ export function PlatformShell() { const [theme, setTheme] = useState(() => getInitialTheme()); const [sidebarExpanded, setSidebarExpanded] = useState(() => getInitialSidebar()); const [notificationsOpen, setNotificationsOpen] = useState(false); + const [paletteOpen, setPaletteOpen] = useState(false); + + useEffect(() => { + const down = (e: KeyboardEvent) => { + if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + setPaletteOpen((open) => !open); + } + }; + document.addEventListener('keydown', down); + return () => document.removeEventListener('keydown', down); + }, []); const sessionQuery = useAuthSession({ enabled: !isDemoMode }); const signedIn = isDemoMode || Boolean(sessionQuery.data); @@ -234,8 +251,19 @@ export function PlatformShell() { } }; - const isActiveRoute = (itemHref: string) => - location.pathname === itemHref || location.pathname.startsWith(`${itemHref}/`); + // Longest matching href wins so /settings/audit-logs lights only + // "Audit Logs", not "Settings" as well. + const activeHref = location.pathname === '/' + ? '/' + : visibleNavSections + .flatMap((section) => section.items.map((item) => item.href)) + .filter( + (itemHref) => + itemHref !== '/' && + (location.pathname === itemHref || location.pathname.startsWith(`${itemHref}/`)), + ) + .sort((a, b) => b.length - a.length)[0] ?? null; + const isActiveRoute = (itemHref: string) => itemHref === activeHref; const userName = isDemoMode ? 'Demo workspace' : profileQuery.data?.name ?? sessionQuery.data?.user.name ?? 'Account'; const userEmail = isDemoMode ? 'Sample data · read-only' : profileQuery.data?.email ?? sessionQuery.data?.user.email ?? 'Local session'; @@ -246,14 +274,20 @@ export function PlatformShell() { .slice(0, 2) .toUpperCase() || 'C'; - const avatar = ( -
- {initials} -
- ); + const paletteNavigate = (target: string) => { + navigate(href(target)); + }; + + // Project deep-links for the palette — only fetched once it opens. + const paletteProjectsQuery = useQuery({ + queryKey: ['palette-projects'], + queryFn: () => listProjects(), + enabled: paletteOpen && !isDemoMode, + staleTime: 60_000, + }); + const paletteProjects = isDemoMode + ? demoProjects + : paletteProjectsQuery.data ?? []; if (!isDemoMode && signedIn && setupQuery.data?.needs_setup) { return ; @@ -261,40 +295,46 @@ export function PlatformShell() { return (
-
-
+ {/* ── Sidebar — fixed rail per design/23-sentry ── */}