mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-29 00:01:22 +00:00
fix: polish service runtime UX and harden reconcile edge cases
Backend: - tolerate concurrent project-network creation (re-resolve on conflict) - always publish an ephemeral host port when a port is configured, even with a domain set — Traefik routes on top when the edge network exists - public_url now prefers the configured domain, falls back to the first published port - service logs resolve replicas by containr.service label and merge all replica streams — previously queried a nonexistent containr-<id> name Canvas/detail: - degraded status renders amber end-to-end (node card, detail badge, status dot) instead of falling through to "Stopped" - edges pick handle sides by relative node position — no more backward loops when the source sits right of the target - context menu gains Start for stopped services, Visit uses the published URL when no domain is set, menu clamps to canvas bounds - node cards show the image tag instead of a redundant hint - scaling tab falls back to live runtime replica counts when no autoscaler state exists; manual scale no longer requires it Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
04c3b99531
commit
b7e2e93261
9 files changed
+193
-69
No files matched your search
@@ -47,6 +47,8 @@ function statusLabel(status: string): string {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return 'Online';
|
||||
case 'degraded':
|
||||
return 'Degraded';
|
||||
case 'building':
|
||||
return 'Deploying';
|
||||
case 'failed':
|
||||
@@ -56,11 +58,25 @@ function statusLabel(status: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function statusColor(status: string): string {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return 'var(--success)';
|
||||
case 'degraded':
|
||||
case 'building':
|
||||
return 'var(--warning)';
|
||||
case 'failed':
|
||||
return 'var(--error)';
|
||||
default:
|
||||
return 'var(--text-tertiary)';
|
||||
}
|
||||
}
|
||||
|
||||
export function ServiceNode({ data }: NodeProps<ServiceNodeType>) {
|
||||
const { service, selected } = data;
|
||||
const typeColor = serviceTypeColor(service.type);
|
||||
const isRunning = service.status === 'running';
|
||||
const replicas = service.replicas ?? 0;
|
||||
const stateColor = statusColor(service.status);
|
||||
|
||||
const publicUrl = service.domain ? `https://${service.domain}` : service.publicUrl;
|
||||
const internalAddr = service.port ? `${service.name}:${service.port}` : service.name;
|
||||
@@ -79,12 +95,14 @@ export function ServiceNode({ data }: NodeProps<ServiceNodeType>) {
|
||||
style={{ background: `${typeColor}10` }}
|
||||
/>
|
||||
|
||||
<Handle type="target" position={Position.Left} className="!opacity-0 !w-2 !h-2 !border-0" />
|
||||
<Handle type="source" position={Position.Right} className="!opacity-0 !w-2 !h-2 !border-0" />
|
||||
<Handle id="t-l" type="target" position={Position.Left} className="!opacity-0 !w-2 !h-2 !border-0" />
|
||||
<Handle id="t-r" type="target" position={Position.Right} className="!opacity-0 !w-2 !h-2 !border-0" />
|
||||
<Handle id="s-l" type="source" position={Position.Left} className="!opacity-0 !w-2 !h-2 !border-0" />
|
||||
<Handle id="s-r" type="source" position={Position.Right} className="!opacity-0 !w-2 !h-2 !border-0" />
|
||||
|
||||
<div
|
||||
className={`absolute top-0 left-0 right-0 h-0.5 transition-all duration-300 ${isRunning ? 'opacity-100' : 'opacity-40'}`}
|
||||
style={{ background: isRunning ? 'var(--success)' : 'var(--text-tertiary)' }}
|
||||
className={`absolute top-0 left-0 right-0 h-0.5 transition-all duration-300 ${service.status === 'running' ? 'opacity-100' : 'opacity-40'}`}
|
||||
style={{ background: stateColor }}
|
||||
/>
|
||||
|
||||
<div className="relative p-4">
|
||||
@@ -148,14 +166,15 @@ export function ServiceNode({ data }: NodeProps<ServiceNodeType>) {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`status-dot ${serviceStatusClass(service.status)}`} />
|
||||
<span
|
||||
className="text-[11px] font-semibold tracking-wide uppercase"
|
||||
style={{ color: isRunning ? 'var(--success)' : 'var(--text-tertiary)' }}
|
||||
>
|
||||
<span className="text-[11px] font-semibold tracking-wide uppercase" style={{ color: stateColor }}>
|
||||
{statusLabel(service.status)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-[var(--text-tertiary)]">Right-click for actions</span>
|
||||
{service.image && (
|
||||
<span className="text-[10px] text-[var(--text-tertiary)] truncate mono max-w-[120px]" title={service.image}>
|
||||
{service.image}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user