mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 07:41:27 +00:00
* 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>
104 lines
3.3 KiB
TypeScript
104 lines
3.3 KiB
TypeScript
import { Check, X, Loader2, Box, HelpCircle, type LucideIcon } from 'lucide-react';
|
|
import type { PillTone } from './sentry-utils';
|
|
|
|
type ServiceStatus = 'running' | 'building' | 'failed' | 'stopped' | 'unknown';
|
|
|
|
interface StatusBadgeProps {
|
|
status: ServiceStatus | string;
|
|
size?: 'sm' | 'md' | 'lg';
|
|
showLabel?: boolean;
|
|
pulse?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
const statusConfig: Record<
|
|
ServiceStatus,
|
|
{ tone: PillTone; Icon: LucideIcon; label: string; animate?: boolean }
|
|
> = {
|
|
running: { tone: 'ok', Icon: Check, label: 'Active' },
|
|
building: { tone: 'warn', Icon: Loader2, label: 'Building', animate: true },
|
|
failed: { tone: 'err', Icon: X, label: 'Failed' },
|
|
stopped: { tone: 'off', Icon: Box, label: 'Stopped' },
|
|
unknown: { tone: 'off', Icon: HelpCircle, label: 'Unknown' },
|
|
};
|
|
|
|
const TONE_CLS: Record<PillTone, string> = {
|
|
ok: 'text-[var(--success)] bg-[var(--success-soft)] border-[color-mix(in_srgb,var(--success)_16%,transparent)]',
|
|
warn: 'text-[var(--warning)] bg-[var(--warning-soft)] border-[color-mix(in_srgb,var(--warning)_16%,transparent)]',
|
|
err: 'text-[var(--error)] bg-[var(--error-soft)] border-[color-mix(in_srgb,var(--error)_16%,transparent)]',
|
|
info: 'text-[var(--info)] bg-[var(--info-soft)] border-[color-mix(in_srgb,var(--info)_16%,transparent)]',
|
|
off: 'text-[var(--text-tertiary)] bg-[var(--tint-06)] border-[var(--border-subtle)]',
|
|
};
|
|
|
|
const TONE_DOT: Record<PillTone, string> = {
|
|
ok: 'var(--success)',
|
|
warn: 'var(--warning)',
|
|
err: 'var(--error)',
|
|
info: 'var(--info)',
|
|
off: 'var(--text-tertiary)',
|
|
};
|
|
|
|
export function StatusBadge({
|
|
status,
|
|
size = 'md',
|
|
showLabel = true,
|
|
pulse = true,
|
|
className = '',
|
|
}: StatusBadgeProps) {
|
|
const config = statusConfig[status as ServiceStatus] ?? statusConfig.unknown;
|
|
const { Icon } = config;
|
|
|
|
const sizeClasses = {
|
|
sm: 'px-2 py-[2px] text-[10px] gap-1.5',
|
|
md: 'px-2 py-[3.5px] text-[10.5px] gap-1.5',
|
|
lg: 'px-2.5 py-1 text-[11.5px] gap-1.5',
|
|
};
|
|
|
|
const iconSizes = { sm: 10, md: 11, lg: 12 };
|
|
|
|
return (
|
|
<div
|
|
className={`inline-flex items-center rounded-[5px] border font-medium transition-colors ${sizeClasses[size]} ${TONE_CLS[config.tone]} ${className}`}
|
|
>
|
|
{status === 'running' && pulse && (
|
|
<span
|
|
className="w-1.5 h-1.5 rounded-full live-pulse"
|
|
style={{ background: TONE_DOT[config.tone] }}
|
|
/>
|
|
)}
|
|
<Icon
|
|
size={iconSizes[size]}
|
|
className={config.animate ? 'animate-spin' : ''}
|
|
/>
|
|
{showLabel && <span>{config.label}</span>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface LiveIndicatorProps {
|
|
isLive: boolean;
|
|
label?: string;
|
|
size?: 'sm' | 'md' | 'lg';
|
|
}
|
|
|
|
export function LiveIndicator({ isLive, label, size = 'md' }: LiveIndicatorProps) {
|
|
const sizeClasses = {
|
|
sm: 'px-2 py-[2px] text-[10px] gap-1.5',
|
|
md: 'px-2 py-[3.5px] text-[10.5px] gap-1.5',
|
|
lg: 'px-2.5 py-1 text-[11.5px] gap-1.5',
|
|
};
|
|
const tone: PillTone = isLive ? 'ok' : 'off';
|
|
|
|
return (
|
|
<div
|
|
className={`inline-flex items-center rounded-[5px] border font-medium transition-colors ${sizeClasses[size]} ${TONE_CLS[tone]}`}
|
|
>
|
|
<span
|
|
className={`w-1.5 h-1.5 rounded-full ${isLive ? 'live-pulse' : ''}`}
|
|
style={{ background: TONE_DOT[tone] }}
|
|
/>
|
|
{label || (isLive ? 'Active' : 'Stopped')}
|
|
</div>
|
|
);
|
|
}
|