Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ui/public/config.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"apiUrl": "http://localhost:9090/api/v1"
"apiUrl": "http://localhost:9090/api/v1",
"benchmarkoorUrl": "https://benchmarkoor.core.ethpandaops.io"
}
361 changes: 236 additions & 125 deletions ui/src/components/jobs/JobCard.tsx

Large diffs are not rendered by default.

33 changes: 25 additions & 8 deletions ui/src/components/jobs/JobDetailDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Job, JobTemplate } from '../../types';
import { api } from '../../api/client';
import { useAuthStore } from '../../stores/authStore';
import { LabelsDisplay } from '../common/LabelBadge';
import { buildBenchmarkoorRunUrl } from '../../config';

interface JobDetailDialogProps {
job: Job;
Expand Down Expand Up @@ -221,20 +222,20 @@ export function JobDetailDialog({ job, template, isOpen, onClose }: JobDetailDia
{/* Dialog */}
<div className="relative w-full max-w-2xl max-h-[85vh] mx-4 flex flex-col rounded-sm border border-zinc-800 bg-zinc-900 shadow-xl">
{/* Header */}
<div className="flex items-center justify-between border-b border-zinc-800 px-4 py-3 shrink-0">
<div className="flex items-center gap-3">
<span className={`inline-flex items-center gap-1.5 rounded-sm px-2 py-0.5 text-xs font-medium ${job.paused ? 'bg-zinc-500/10 text-zinc-400' : colors.bg + ' ' + colors.text}`}>
<div className="flex items-start justify-between gap-2 border-b border-zinc-800 px-4 py-3 shrink-0">
<div className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1">
<span className={`inline-flex shrink-0 items-center gap-1.5 rounded-sm px-2 py-0.5 text-xs font-medium ${job.paused ? 'bg-zinc-500/10 text-zinc-400' : colors.bg + ' ' + colors.text}`}>
<span className={`size-1.5 rounded-full ${job.paused ? 'bg-zinc-400' : colors.dot}`} />
{job.paused ? 'paused' : job.status}
</span>
<h2 className="text-lg font-semibold text-zinc-100">
<h2 className="min-w-0 break-words text-lg font-semibold text-zinc-100">
{job.name ?? template?.name ?? job.template_id}
</h2>
<span className="text-sm text-zinc-500">#{job.position}</span>
<span className="shrink-0 text-sm text-zinc-500">#{job.position}</span>
</div>
<button
onClick={onClose}
className="rounded-sm p-1 text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100"
className="mt-0.5 shrink-0 rounded-sm p-1 text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100"
>
<svg className="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
Expand All @@ -259,7 +260,7 @@ export function JobDetailDialog({ job, template, isOpen, onClose }: JobDetailDia
<span className="text-xs text-zinc-500">Edit fields to override job</span>
)}
</div>
<div className="grid grid-cols-2 gap-3 text-sm">
<div className="grid grid-cols-1 gap-3 text-sm sm:grid-cols-2">
{/* Owner */}
<div>
<div className="flex items-center gap-2 mb-1">
Expand Down Expand Up @@ -414,12 +415,28 @@ export function JobDetailDialog({ job, template, isOpen, onClose }: JobDetailDia
</a>
</div>
)}
{job.run_id && buildBenchmarkoorRunUrl(job.run_id) && (
<div className="pt-2 border-t border-zinc-700/50">
<span className="text-zinc-500 text-xs">Benchmarkoor</span>
<a
href={buildBenchmarkoorRunUrl(job.run_id)!}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-sm text-zinc-200 hover:text-blue-400"
>
View benchmark run
<svg className="size-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
</div>
)}
</div>

{/* Timing Info */}
<div className="rounded-sm border border-zinc-800 bg-zinc-800/30 p-3 space-y-2">
<h3 className="text-xs font-medium text-zinc-400 uppercase tracking-wide">Timing</h3>
<div className="grid grid-cols-2 gap-3 text-sm">
<div className="grid grid-cols-1 gap-3 text-sm sm:grid-cols-2">
<div>
<span className="text-zinc-500">Created</span>
<p className="text-zinc-200">{formatDateTime(job.created_at)}</p>
Expand Down
36 changes: 24 additions & 12 deletions ui/src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useAuthStore } from '../../stores/authStore';
import { StatusIndicator } from './StatusIndicator';

export function Header() {
const { sidebarCollapsed, toggleSidebar } = useUIStore();
const { sidebarCollapsed, toggleSidebar, toggleMobileNav } = useUIStore();
const { user, logout } = useAuthStore();
const [showUserMenu, setShowUserMenu] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
Expand All @@ -26,10 +26,22 @@ export function Header() {
};

return (
<header className="sticky top-0 z-40 flex h-14 items-center gap-4 border-b border-zinc-800 bg-zinc-950 px-4">
<header className="sticky top-0 z-40 flex h-14 items-center gap-2 border-b border-zinc-800 bg-zinc-950 px-3 sm:gap-4 sm:px-4">
{/* Mobile: open the overlay nav drawer. */}
<button
onClick={toggleMobileNav}
className="inline-flex size-9 items-center justify-center rounded-sm text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100 lg:hidden"
aria-label="Open navigation"
>
<svg className="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>

{/* Desktop: collapse/expand the pinned sidebar. */}
<button
onClick={toggleSidebar}
className="inline-flex size-9 items-center justify-center rounded-sm text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100"
className="hidden size-9 items-center justify-center rounded-sm text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100 lg:inline-flex"
aria-label={sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
>
<svg className="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
Expand All @@ -42,14 +54,14 @@ export function Header() {
</svg>
</button>

<div className="flex flex-1 items-center">
<Link to="/" className="flex items-center gap-2 text-lg font-semibold text-zinc-100 hover:text-white">
<img src="/images/dispatchoor_logo_white.png" alt="Dispatchoor" className="h-8" />
Dispatchoor
<div className="flex min-w-0 flex-1 items-center">
<Link to="/" className="flex min-w-0 items-center gap-2 text-lg font-semibold text-zinc-100 hover:text-white">
<img src="/images/dispatchoor_logo_white.png" alt="Dispatchoor" className="h-8 shrink-0" />
<span className="truncate">Dispatchoor</span>
</Link>
</div>

<div className="flex items-center gap-3">
<div className="flex items-center gap-1 sm:gap-3">
{/* System status indicator */}
<StatusIndicator />

Expand Down Expand Up @@ -93,16 +105,16 @@ export function Header() {
onClick={() => setShowUserMenu(!showUserMenu)}
className="flex items-center gap-2 rounded-sm px-2 py-1.5 text-zinc-300 hover:bg-zinc-800"
>
<div className="flex size-7 items-center justify-center rounded-full bg-zinc-700 text-sm font-medium uppercase">
<div className="flex size-7 shrink-0 items-center justify-center rounded-full bg-zinc-700 text-sm font-medium uppercase">
{user.username.charAt(0)}
</div>
<span className="text-sm">{user.username}</span>
<span className="hidden text-sm sm:inline">{user.username}</span>
{user.role === 'admin' && (
<span className="rounded-xs bg-amber-500/20 px-1.5 py-0.5 text-xs text-amber-400">
<span className="hidden rounded-xs bg-amber-500/20 px-1.5 py-0.5 text-xs text-amber-400 sm:inline">
admin
</span>
)}
<svg className="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg className="size-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function Layout() {
<Sidebar />
<main
className={`transition-all duration-200 ${
sidebarCollapsed ? 'ml-0' : 'ml-64'
sidebarCollapsed ? 'lg:ml-0' : 'lg:ml-64'
}`}
>
{isFullBleed ? (
Expand Down
173 changes: 88 additions & 85 deletions ui/src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,102 +3,105 @@ import { useGroups } from '../../hooks/useGroups';
import { useUIStore } from '../../stores/uiStore';

export function Sidebar() {
const { sidebarCollapsed } = useUIStore();
const { sidebarCollapsed, mobileNavOpen, setMobileNavOpen } = useUIStore();
const { data: groups, isLoading } = useGroups();

if (sidebarCollapsed) {
return null;
}
// Close the mobile drawer after a navigation. Harmless on desktop (already closed).
const closeMobileNav = () => setMobileNavOpen(false);

return (
<aside className="fixed inset-y-0 left-0 z-30 w-64 border-r border-zinc-800 bg-zinc-900 pt-14">
<nav className="flex h-full flex-col gap-2 p-4">
<NavLink
to="/"
className={({ isActive }) =>
`flex items-center gap-3 rounded-sm px-3 py-2 text-sm font-medium transition-colors ${
isActive
? 'bg-blue-500/20 text-blue-400'
: 'text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100'
}`
}
end
>
<svg className="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
/>
</svg>
Dashboard
</NavLink>
const linkClass = (isActive: boolean) =>
`flex items-center gap-3 rounded-sm px-3 py-2 text-sm font-medium transition-colors ${
isActive
? 'bg-blue-500/20 text-blue-400'
: 'text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100'
}`;

<div className="mt-4">
<h3 className="px-3 text-xs font-semibold uppercase tracking-wider text-zinc-500">
Groups
</h3>
<div className="mt-2 space-y-1">
{isLoading ? (
<div className="px-3 py-2 text-sm text-zinc-500">Loading...</div>
) : groups && groups.length > 0 ? (
groups.map((group) => (
<NavLink
key={group.id}
to={`/groups/${group.id}`}
className={({ isActive }) =>
`flex items-center justify-between rounded-sm px-3 py-2 text-sm transition-colors ${
isActive
? 'bg-blue-500/20 text-blue-400'
: 'text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100'
}`
}
>
<span className="truncate">{group.name}</span>
<div className="flex items-center gap-1.5">
{group.running_jobs > 0 && (
<span className="inline-flex items-center justify-center rounded-full bg-green-500/20 px-2 py-0.5 text-xs font-medium text-green-400">
{group.running_jobs}
</span>
)}
{group.queued_jobs > 0 && (
<span className="inline-flex items-center justify-center rounded-full bg-amber-500/20 px-2 py-0.5 text-xs font-medium text-amber-400">
{group.queued_jobs}
</span>
)}
</div>
</NavLink>
))
) : (
<div className="px-3 py-2 text-sm text-zinc-500">No groups configured</div>
)}
</div>
</div>
return (
<>
{/* Backdrop: only on mobile while the drawer is open. */}
{mobileNavOpen && (
<div
className="fixed inset-0 z-20 bg-black/50 lg:hidden"
onClick={closeMobileNav}
aria-hidden="true"
/>
)}

<div className="mt-4">
<NavLink
to="/runners"
className={({ isActive }) =>
`flex items-center gap-3 rounded-sm px-3 py-2 text-sm font-medium transition-colors ${
isActive
? 'bg-blue-500/20 text-blue-400'
: 'text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100'
}`
}
>
<svg className="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<aside
className={`fixed inset-y-0 left-0 z-30 w-64 border-r border-zinc-800 bg-zinc-900 pt-14 transition-transform duration-200 ease-in-out ${
mobileNavOpen ? 'translate-x-0' : '-translate-x-full'
} ${sidebarCollapsed ? 'lg:-translate-x-full' : 'lg:translate-x-0'}`}
>
<nav className="flex h-full flex-col gap-2 overflow-y-auto p-4">
<NavLink to="/" onClick={closeMobileNav} className={({ isActive }) => linkClass(isActive)} end>
<svg className="size-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
/>
</svg>
All Runners
Dashboard
</NavLink>
</div>
</nav>
</aside>

<div className="mt-4">
<h3 className="px-3 text-xs font-semibold uppercase tracking-wider text-zinc-500">
Groups
</h3>
<div className="mt-2 space-y-1">
{isLoading ? (
<div className="px-3 py-2 text-sm text-zinc-500">Loading...</div>
) : groups && groups.length > 0 ? (
groups.map((group) => (
<NavLink
key={group.id}
to={`/groups/${group.id}`}
onClick={closeMobileNav}
className={({ isActive }) =>
`flex items-center justify-between rounded-sm px-3 py-2 text-sm transition-colors ${
isActive
? 'bg-blue-500/20 text-blue-400'
: 'text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100'
}`
}
>
<span className="truncate">{group.name}</span>
<div className="flex items-center gap-1.5">
{group.running_jobs > 0 && (
<span className="inline-flex items-center justify-center rounded-full bg-green-500/20 px-2 py-0.5 text-xs font-medium text-green-400">
{group.running_jobs}
</span>
)}
{group.queued_jobs > 0 && (
<span className="inline-flex items-center justify-center rounded-full bg-amber-500/20 px-2 py-0.5 text-xs font-medium text-amber-400">
{group.queued_jobs}
</span>
)}
</div>
</NavLink>
))
) : (
<div className="px-3 py-2 text-sm text-zinc-500">No groups configured</div>
)}
</div>
</div>

<div className="mt-4">
<NavLink to="/runners" onClick={closeMobileNav} className={({ isActive }) => linkClass(isActive)}>
<svg className="size-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"
/>
</svg>
All Runners
</NavLink>
</div>
</nav>
</aside>
</>
);
}
6 changes: 3 additions & 3 deletions ui/src/components/layout/StatusIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ export function StatusIndicator() {
onClick={() => setShowDropdown(!showDropdown)}
className="flex items-center gap-1.5 rounded-sm px-2 py-1 text-zinc-400 hover:bg-zinc-800 hover:text-zinc-300"
>
<div className={`size-2 rounded-full ${getStatusColor(overallStatus)}`} />
<span className="text-xs">{statusLabel}</span>
<svg className="size-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div className={`size-2 shrink-0 rounded-full ${getStatusColor(overallStatus)}`} />
<span className="hidden text-xs sm:inline">{statusLabel}</span>
<svg className="size-3 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
Expand Down
10 changes: 10 additions & 0 deletions ui/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
export interface AppConfig {
apiUrl: string;
// Base URL for Benchmarkoor (e.g. https://benchmarkoor.core.ethpandaops.io).
// When set, jobs with a GitHub run_id display a link to the corresponding
// benchmark run, filtered via the github.run_id metadata label.
benchmarkoorUrl?: string;
}

const defaultConfig: AppConfig = {
apiUrl: '/api/v1',
};

export function buildBenchmarkoorRunUrl(runId: number | string): string | undefined {
const base = config.benchmarkoorUrl?.replace(/\/+$/, '');
if (!base) return undefined;
return `${base}/runs?labels=${encodeURIComponent('github.run_id')}:${encodeURIComponent(String(runId))}`;
}

let config: AppConfig = defaultConfig;
let configLoaded = false;
let configPromise: Promise<AppConfig> | null = null;
Expand Down
Loading