Clickable container port links (0.11.1)
Published container ports on the stack Overview (local + remote) render as clickable chips that open the service at the host's address + port in a new tab. New ContainerPorts component: links to the bound host IP when concrete, else the host you're viewing from; remote stacks link to the agent host (derived from the agent URL). http by default, https for 443/8443. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1931500c24
commit
e69c1fa065
@@ -0,0 +1,69 @@
|
||||
import { ExternalLink } from "lucide-react";
|
||||
|
||||
export type ContainerPort = {
|
||||
container: string;
|
||||
host_ip?: string;
|
||||
host_port?: string | null;
|
||||
};
|
||||
|
||||
const WILDCARD_IPS = new Set(["", "0.0.0.0", "::"]);
|
||||
|
||||
/** Pick the host to link to: an explicit override (remote agent), the bound
|
||||
* host IP if it's a concrete address, otherwise the host we're viewing from. */
|
||||
function linkHost(hostIp: string | undefined, override?: string): string {
|
||||
if (override) return override;
|
||||
if (hostIp && !WILDCARD_IPS.has(hostIp)) return hostIp;
|
||||
return window.location.hostname;
|
||||
}
|
||||
|
||||
/** Best-effort scheme guess — most homelab services are http; common TLS
|
||||
* ports get https. */
|
||||
function scheme(hostPort: string, container: string): "http" | "https" {
|
||||
if (hostPort === "443" || hostPort === "8443") return "https";
|
||||
if (container.startsWith("443/")) return "https";
|
||||
return "http";
|
||||
}
|
||||
|
||||
/** Clickable chips for a container's published ports. `host` overrides the
|
||||
* link target (used for remote agent stacks). Renders nothing if unpublished. */
|
||||
export function ContainerPorts({
|
||||
ports,
|
||||
host,
|
||||
}: {
|
||||
ports: ContainerPort[];
|
||||
host?: string;
|
||||
}) {
|
||||
const seen = new Set<string>();
|
||||
const published = ports.filter((p) => {
|
||||
if (!p.host_port || seen.has(p.host_port)) return false;
|
||||
seen.add(p.host_port);
|
||||
return true;
|
||||
});
|
||||
if (published.length === 0) return null;
|
||||
|
||||
return (
|
||||
<span className="flex flex-wrap items-center gap-1">
|
||||
{published.map((p) => {
|
||||
const url = `${scheme(p.host_port!, p.container)}://${linkHost(
|
||||
p.host_ip,
|
||||
host
|
||||
)}:${p.host_port}`;
|
||||
return (
|
||||
<a
|
||||
key={p.host_port}
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
title={`Open ${url}`}
|
||||
className="inline-flex items-center gap-1 rounded-md bg-slate-100 px-2 py-0.5 font-mono text-xs text-accent hover:underline dark:bg-slate-700 dark:text-accent-dark"
|
||||
>
|
||||
{p.host_port}
|
||||
<span className="text-slate-400">→{p.container}</span>
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user