fix: resolve 7 critical UI issues - charts, layouts, and mobile responsiveness
All checks were successful
Build and Push Docker Image to Gitea / build-and-push (push) Successful in 1m8s

This commit fixes all reported UI issues across the dashboard:

## Issue 1: Chart Colors and Tooltips 
- Create chartColors utility with static hex colors for Recharts compatibility
- Replace CSS variable colors (hsl(var(--))) with hex colors in all charts
- Add custom tooltip styling with dark background and white text for readability
- Fixes: ProcessStateChart, ProcessUptimeChart, GroupStatistics

## Issue 2: Process Card Heights 
- Add h-full and flex flex-col to ProcessCard component
- Add auto-rows-fr to process grid layout
- Ensures all cards have consistent heights regardless of content

## Issue 3: Batch Actions Button Labels 
- Simplify button labels from "Start Selected" to "Start"
- Remove "Stop Selected" to "Stop", "Restart Selected" to "Restart"
- Labels now always visible on all screen sizes

## Issue 4: Mobile Menu Background 
- Change mobile menu from semi-transparent (bg-background/95) to solid (bg-background)
- Removes backdrop blur for better visibility

## Issue 5: Group Header Button Overflow 
- Add flex-wrap to button container in GroupCard
- Stack buttons vertically on mobile (flex-col md:flex-row)
- Buttons take full width on mobile, auto width on desktop

## Issue 6: Logs Search Input Overflow 
- Change LogSearch from max-w-md to w-full sm:flex-1 sm:max-w-md
- Search input now full width on mobile, constrained on desktop

## Issue 7: Logs Action Button Overflow 
- Add flex-wrap to LogControls button container
- Buttons wrap to new row when space is limited

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-23 21:52:35 +01:00
parent 791c99097c
commit dda335d501
11 changed files with 105 additions and 26 deletions

View File

@@ -280,7 +280,7 @@ export default function ProcessesPage() {
) : viewMode === 'grouped' ? ( ) : viewMode === 'grouped' ? (
<GroupView processes={displayedProcesses} /> <GroupView processes={displayedProcesses} />
) : ( ) : (
<div className="grid gap-4 md:gap-6 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 md:gap-6 md:grid-cols-2 lg:grid-cols-3 auto-rows-fr">
{displayedProcesses.map((process, index) => { {displayedProcesses.map((process, index) => {
const fullName = `${process.group}:${process.name}`; const fullName = `${process.group}:${process.name}`;
const isFocused = index === focusedIndex; const isFocused = index === focusedIndex;

View File

@@ -3,6 +3,7 @@
import { ProcessInfo, ProcessState } from '@/lib/supervisor/types'; import { ProcessInfo, ProcessState } from '@/lib/supervisor/types';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import { chartColors } from '@/lib/utils/chartColors';
interface GroupStatisticsProps { interface GroupStatisticsProps {
processes: ProcessInfo[]; processes: ProcessInfo[];
@@ -50,11 +51,19 @@ export function GroupStatistics({ processes }: GroupStatisticsProps) {
<CartesianGrid strokeDasharray="3 3" /> <CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" /> <XAxis dataKey="name" />
<YAxis /> <YAxis />
<Tooltip /> <Tooltip
contentStyle={{
backgroundColor: 'rgba(0, 0, 0, 0.9)',
border: 'none',
borderRadius: '8px',
color: '#ffffff',
}}
itemStyle={{ color: '#ffffff' }}
/>
<Legend /> <Legend />
<Bar dataKey="running" stackId="a" fill="hsl(var(--success))" name="Running" /> <Bar dataKey="running" stackId="a" fill={chartColors.running} name="Running" />
<Bar dataKey="stopped" stackId="a" fill="hsl(var(--muted-foreground))" name="Stopped" /> <Bar dataKey="stopped" stackId="a" fill={chartColors.stopped} name="Stopped" />
<Bar dataKey="fatal" stackId="a" fill="hsl(var(--destructive))" name="Fatal" /> <Bar dataKey="fatal" stackId="a" fill={chartColors.fatal} name="Fatal" />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</CardContent> </CardContent>

View File

@@ -3,6 +3,7 @@
import { ProcessInfo, ProcessState } from '@/lib/supervisor/types'; import { ProcessInfo, ProcessState } from '@/lib/supervisor/types';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts'; import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts';
import { chartColors } from '@/lib/utils/chartColors';
interface ProcessStateChartProps { interface ProcessStateChartProps {
processes: ProcessInfo[]; processes: ProcessInfo[];
@@ -23,12 +24,12 @@ export function ProcessStateChart({ processes }: ProcessStateChartProps) {
); );
const data = [ const data = [
{ name: 'Running', value: stateCounts.running, color: 'hsl(var(--success))' }, { name: 'Running', value: stateCounts.running, color: chartColors.running },
{ name: 'Stopped', value: stateCounts.stopped, color: 'hsl(var(--muted-foreground))' }, { name: 'Stopped', value: stateCounts.stopped, color: chartColors.stopped },
{ name: 'Fatal', value: stateCounts.fatal, color: 'hsl(var(--destructive))' }, { name: 'Fatal', value: stateCounts.fatal, color: chartColors.fatal },
{ name: 'Starting', value: stateCounts.starting, color: 'hsl(var(--warning))' }, { name: 'Starting', value: stateCounts.starting, color: chartColors.starting },
{ name: 'Stopping', value: stateCounts.stopping, color: 'hsl(var(--accent))' }, { name: 'Stopping', value: stateCounts.stopping, color: chartColors.stopping },
{ name: 'Other', value: stateCounts.other, color: 'hsl(var(--muted))' }, { name: 'Other', value: stateCounts.other, color: chartColors.muted },
].filter((item) => item.value > 0); ].filter((item) => item.value > 0);
return ( return (
@@ -53,7 +54,15 @@ export function ProcessStateChart({ processes }: ProcessStateChartProps) {
<Cell key={`cell-${index}`} fill={entry.color} /> <Cell key={`cell-${index}`} fill={entry.color} />
))} ))}
</Pie> </Pie>
<Tooltip /> <Tooltip
contentStyle={{
backgroundColor: 'rgba(0, 0, 0, 0.9)',
border: 'none',
borderRadius: '8px',
color: '#ffffff',
}}
itemStyle={{ color: '#ffffff' }}
/>
<Legend /> <Legend />
</PieChart> </PieChart>
</ResponsiveContainer> </ResponsiveContainer>

View File

@@ -3,6 +3,7 @@
import { ProcessInfo, ProcessState } from '@/lib/supervisor/types'; import { ProcessInfo, ProcessState } from '@/lib/supervisor/types';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import { chartColors } from '@/lib/utils/chartColors';
interface ProcessUptimeChartProps { interface ProcessUptimeChartProps {
processes: ProcessInfo[]; processes: ProcessInfo[];
@@ -56,9 +57,16 @@ export function ProcessUptimeChart({ processes }: ProcessUptimeChartProps) {
const minutes = Math.floor((value - hours) * 60); const minutes = Math.floor((value - hours) * 60);
return `${hours}h ${minutes}m`; return `${hours}h ${minutes}m`;
}} }}
contentStyle={{
backgroundColor: 'rgba(0, 0, 0, 0.9)',
border: 'none',
borderRadius: '8px',
color: '#ffffff',
}}
itemStyle={{ color: '#ffffff' }}
/> />
<Legend /> <Legend />
<Bar dataKey="uptime" fill="hsl(var(--success))" name="Uptime (hours)" /> <Bar dataKey="uptime" fill={chartColors.success} name="Uptime (hours)" />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</CardContent> </CardContent>

View File

@@ -49,7 +49,7 @@ export function GroupCard({ groupName, processes }: GroupCardProps) {
return ( return (
<Card className="overflow-hidden"> <Card className="overflow-hidden">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-center justify-between"> <div className="flex flex-col md:flex-row items-start md:items-center gap-3 md:justify-between">
<div className="flex items-center gap-3 flex-1"> <div className="flex items-center gap-3 flex-1">
<Button <Button
variant="ghost" variant="ghost"
@@ -80,13 +80,13 @@ export function GroupCard({ groupName, processes }: GroupCardProps) {
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex flex-wrap gap-2 w-full md:w-auto">
<Button <Button
variant="success" variant="success"
size="sm" size="sm"
onClick={handleStart} onClick={handleStart}
disabled={isLoading || stats.stopped === 0} disabled={isLoading || stats.stopped === 0}
className="gap-2" className="gap-2 flex-1 sm:flex-initial"
> >
<Play className="h-4 w-4" /> <Play className="h-4 w-4" />
Start All Start All
@@ -96,7 +96,7 @@ export function GroupCard({ groupName, processes }: GroupCardProps) {
size="sm" size="sm"
onClick={handleStop} onClick={handleStop}
disabled={isLoading || stats.running === 0} disabled={isLoading || stats.running === 0}
className="gap-2" className="gap-2 flex-1 sm:flex-initial"
> >
<Square className="h-4 w-4" /> <Square className="h-4 w-4" />
Stop All Stop All
@@ -106,7 +106,7 @@ export function GroupCard({ groupName, processes }: GroupCardProps) {
size="sm" size="sm"
onClick={handleRestart} onClick={handleRestart}
disabled={isLoading} disabled={isLoading}
className="gap-2" className="gap-2 flex-1 sm:flex-initial"
> >
<RotateCw className={cn('h-4 w-4', isLoading && 'animate-spin')} /> <RotateCw className={cn('h-4 w-4', isLoading && 'animate-spin')} />
Restart All Restart All

View File

@@ -116,7 +116,7 @@ export function Navbar() {
{/* Mobile Menu Drawer */} {/* Mobile Menu Drawer */}
{mobileMenuOpen && ( {mobileMenuOpen && (
<div className="md:hidden fixed inset-0 top-16 z-50 bg-background/95 backdrop-blur-sm"> <div className="md:hidden fixed inset-0 top-16 z-50 bg-background">
<div className="container px-4 py-6"> <div className="container px-4 py-6">
<nav className="flex flex-col gap-2"> <nav className="flex flex-col gap-2">
{navItems.map((item) => ( {navItems.map((item) => (

View File

@@ -26,7 +26,7 @@ export function LogControls({
isClearing = false, isClearing = false,
}: LogControlsProps) { }: LogControlsProps) {
return ( return (
<div className="flex items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<Button <Button
variant={isPlaying ? 'destructive' : 'success'} variant={isPlaying ? 'destructive' : 'success'}
size="sm" size="sm"

View File

@@ -12,7 +12,7 @@ interface LogSearchProps {
export function LogSearch({ value, onChange, placeholder = 'Search logs...' }: LogSearchProps) { export function LogSearch({ value, onChange, placeholder = 'Search logs...' }: LogSearchProps) {
return ( return (
<div className="relative flex-1 max-w-md"> <div className="relative w-full sm:flex-1 sm:max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input <Input
type="text" type="text"

View File

@@ -82,7 +82,7 @@ export function BatchActions({ selectedProcesses, processes, onClearSelection }:
className="gap-2 flex-1 sm:flex-initial" className="gap-2 flex-1 sm:flex-initial"
> >
<Play className="h-4 w-4" /> <Play className="h-4 w-4" />
<span className="hidden sm:inline">Start Selected</span> <span>Start</span>
</Button> </Button>
<Button <Button
variant="destructive" variant="destructive"
@@ -92,7 +92,7 @@ export function BatchActions({ selectedProcesses, processes, onClearSelection }:
className="gap-2 flex-1 sm:flex-initial" className="gap-2 flex-1 sm:flex-initial"
> >
<Square className="h-4 w-4" /> <Square className="h-4 w-4" />
<span className="hidden sm:inline">Stop Selected</span> <span>Stop</span>
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
@@ -102,7 +102,7 @@ export function BatchActions({ selectedProcesses, processes, onClearSelection }:
className="gap-2 flex-1 sm:flex-initial" className="gap-2 flex-1 sm:flex-initial"
> >
<RotateCw className={cn('h-4 w-4', isLoading && 'animate-spin')} /> <RotateCw className={cn('h-4 w-4', isLoading && 'animate-spin')} />
<span className="hidden sm:inline">Restart Selected</span> <span>Restart</span>
</Button> </Button>
</div> </div>

View File

@@ -42,7 +42,7 @@ export function ProcessCard({ process, isSelected = false, isFocused = false, on
return ( return (
<Card <Card
className={cn( className={cn(
'transition-all hover:shadow-lg animate-fade-in', 'transition-all hover:shadow-lg animate-fade-in h-full flex flex-col',
onSelectionChange && 'cursor-pointer', onSelectionChange && 'cursor-pointer',
isSelected && 'ring-2 ring-primary ring-offset-2', isSelected && 'ring-2 ring-primary ring-offset-2',
isFocused && 'ring-2 ring-accent ring-offset-2 shadow-xl' isFocused && 'ring-2 ring-accent ring-offset-2 shadow-xl'
@@ -82,7 +82,7 @@ export function ProcessCard({ process, isSelected = false, isFocused = false, on
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4 flex-1 flex flex-col justify-between">
{/* Metrics */} {/* Metrics */}
<div className="grid grid-cols-2 gap-3 text-sm"> <div className="grid grid-cols-2 gap-3 text-sm">
<div> <div>

53
lib/utils/chartColors.ts Normal file
View File

@@ -0,0 +1,53 @@
/**
* Chart color utilities for Recharts compatibility
*
* Recharts doesn't properly parse CSS custom properties or OKLCH colors,
* so we provide static hex colors that work reliably across themes.
*/
export const chartColors = {
primary: '#3b82f6', // Blue
success: '#22c55e', // Green
warning: '#eab308', // Yellow
destructive: '#ef4444', // Red
accent: '#06b6d4', // Cyan
muted: '#9ca3af', // Gray
running: '#22c55e', // Same as success
stopped: '#6b7280', // Darker gray
fatal: '#ef4444', // Same as destructive
starting: '#eab308', // Same as warning
backoff: '#f97316', // Orange
stopping: '#fb923c', // Light orange
exited: '#9ca3af', // Same as muted
unknown: '#64748b', // Slate
};
/**
* Get color for process state
*/
export function getStateColor(state: string): string {
const stateMap: Record<string, string> = {
running: chartColors.running,
stopped: chartColors.stopped,
fatal: chartColors.fatal,
starting: chartColors.starting,
backoff: chartColors.backoff,
stopping: chartColors.stopping,
exited: chartColors.exited,
unknown: chartColors.unknown,
};
return stateMap[state.toLowerCase()] || chartColors.muted;
}
/**
* Color palette for multiple data series
*/
export const colorPalette = [
chartColors.primary,
chartColors.success,
chartColors.warning,
chartColors.accent,
chartColors.destructive,
chartColors.muted,
];