Show running services as named chips with restart/stop on Overview
Replace anonymous green dots with named green chips, each with restart and stop icon buttons. Now all services have visible controls on the Overview regardless of state. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { usePortal } from '@/lib/PortalContext';
|
||||
import { clientConfig } from '@/lib/config';
|
||||
import { Icon } from './Icons';
|
||||
import { SystemTrends } from './SystemTrends';
|
||||
import { formatUptime } from '@/lib/stats';
|
||||
@@ -14,15 +15,6 @@ function isDiscoveredService(s: Service): s is DiscoveredService {
|
||||
return 'uuid' in s && 'resourceType' in s;
|
||||
}
|
||||
|
||||
const quickLinks = [
|
||||
{ name: 'Coolify', url: 'http://192.168.1.3:8000', icon: 'coolify', desc: 'Service manager' },
|
||||
{ name: 'Dozzle', url: 'http://192.168.1.3:9999', icon: 'scroll-text', desc: 'Container logs' },
|
||||
{ name: 'Uptime Kuma', url: 'http://192.168.1.3:3001', icon: 'activity', desc: 'Monitoring' },
|
||||
{ name: 'Ntfy', url: 'http://192.168.1.3:8333', icon: 'bell', desc: 'Notifications' },
|
||||
{ name: 'Gitea', url: 'http://192.168.1.3:3030', icon: 'git-branch', desc: 'Git hosting' },
|
||||
{ name: 'Adminer', url: 'http://192.168.1.3:8088', icon: 'database', desc: 'DB admin' },
|
||||
];
|
||||
|
||||
interface ProjectDef {
|
||||
name: string;
|
||||
icon: string;
|
||||
@@ -135,7 +127,6 @@ export function OverviewTab() {
|
||||
body: JSON.stringify({ uuid: s.uuid, resourceType: s.resourceType, action }),
|
||||
});
|
||||
if (res.ok) {
|
||||
// Wait briefly for Coolify to process, then refresh
|
||||
setTimeout(() => refreshDiscover(), 3000);
|
||||
}
|
||||
} catch { /* ignore */ } finally {
|
||||
@@ -143,6 +134,26 @@ export function OverviewTab() {
|
||||
}
|
||||
}, [refreshDiscover]);
|
||||
|
||||
// Build quick links dynamically from discovered services
|
||||
const quickLinkDefs = [
|
||||
{ key: 'coolify', icon: 'coolify', desc: 'Service manager' },
|
||||
{ key: 'dozzle', icon: 'scroll-text', desc: 'Container logs' },
|
||||
{ key: 'uptime kuma', icon: 'activity', desc: 'Monitoring' },
|
||||
{ key: 'ntfy', icon: 'bell', desc: 'Notifications' },
|
||||
{ key: 'gitea', icon: 'git-branch', desc: 'Git hosting' },
|
||||
{ key: 'adminer', icon: 'database', desc: 'DB admin' },
|
||||
];
|
||||
|
||||
const quickLinks = quickLinkDefs.map(def => {
|
||||
const svc = services.find(s => s.name.toLowerCase().includes(def.key));
|
||||
return {
|
||||
name: svc?.name || def.key.charAt(0).toUpperCase() + def.key.slice(1),
|
||||
url: svc?.url || `http://${clientConfig.nucHost}`,
|
||||
icon: svc?.icon || def.icon,
|
||||
desc: def.desc,
|
||||
};
|
||||
});
|
||||
|
||||
const runningServices = services.filter(s => healthStatus[s.name] === 'running');
|
||||
const stoppedServices = services.filter(s => healthStatus[s.name] === 'stopped');
|
||||
const unknownServices = services.filter(s => {
|
||||
@@ -184,7 +195,6 @@ export function OverviewTab() {
|
||||
<span className="text-sm text-slate-500 dark:text-stone-500">of {totalCount} running</span>
|
||||
</div>
|
||||
|
||||
{/* Stopped services as named chips with start button */}
|
||||
{stoppedServices.length > 0 && (
|
||||
<div className="mb-3">
|
||||
<p className="text-[11px] font-medium text-red-500/70 dark:text-red-400/70 uppercase tracking-wider mb-1.5">Stopped</p>
|
||||
@@ -220,7 +230,6 @@ export function OverviewTab() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Unknown services as named chips with start button */}
|
||||
{unknownServices.length > 0 && (
|
||||
<div className="mb-3">
|
||||
<p className="text-[11px] font-medium text-slate-400/70 dark:text-stone-500/70 uppercase tracking-wider mb-1.5">Unknown</p>
|
||||
@@ -256,16 +265,48 @@ export function OverviewTab() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Running services as green dots */}
|
||||
{runningServices.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{runningServices.map(s => (
|
||||
<div
|
||||
key={s.name}
|
||||
title={s.name}
|
||||
className="w-2.5 h-2.5 rounded-sm bg-emerald-500"
|
||||
/>
|
||||
))}
|
||||
<div>
|
||||
<p className="text-[11px] font-medium text-emerald-500/70 dark:text-emerald-400/70 uppercase tracking-wider mb-1.5">Running</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{runningServices.map(s => {
|
||||
const uuid = isDiscoveredService(s) ? s.uuid : null;
|
||||
const loading = uuid ? controlling[uuid] : false;
|
||||
return (
|
||||
<span
|
||||
key={s.name}
|
||||
className="inline-flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-medium bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-400 border border-emerald-100 dark:border-emerald-800/30"
|
||||
>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 flex-shrink-0" />
|
||||
{s.name}
|
||||
{uuid && (
|
||||
<span className="flex items-center gap-0.5 ml-0.5">
|
||||
<button
|
||||
onClick={() => controlService(s, 'restart')}
|
||||
disabled={loading}
|
||||
title={`Restart ${s.name}`}
|
||||
className="p-0.5 rounded hover:bg-emerald-100 dark:hover:bg-emerald-800/30 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{loading ? (
|
||||
<Icon name="loader" size={11} className="animate-spin" />
|
||||
) : (
|
||||
<Icon name="refresh-cw" size={11} />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => controlService(s, 'stop')}
|
||||
disabled={loading}
|
||||
title={`Stop ${s.name}`}
|
||||
className="p-0.5 rounded hover:bg-red-100 dark:hover:bg-red-800/30 hover:text-red-500 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<Icon name="power" size={11} />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -317,7 +358,7 @@ export function OverviewTab() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 3: Projects (side by side, scales with more projects) */}
|
||||
{/* Row 3: Projects */}
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 mb-3 flex items-center gap-2">
|
||||
<Icon name="box" size={16} className="text-slate-400 dark:text-stone-500" />
|
||||
|
||||
Reference in New Issue
Block a user