fix: surface database volume in docker view
This commit is contained in:
@@ -377,6 +377,57 @@ h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.08em; }
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.docker-volume-focus {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid rgba(113, 191, 120, 0.34);
|
||||
background:
|
||||
linear-gradient(90deg, rgba(113, 191, 120, 0.11), transparent 55%),
|
||||
var(--panel-3);
|
||||
}
|
||||
.docker-volume-focus.missing {
|
||||
border-color: rgba(215, 161, 58, 0.45);
|
||||
background:
|
||||
linear-gradient(90deg, rgba(215, 161, 58, 0.12), transparent 55%),
|
||||
var(--panel-3);
|
||||
}
|
||||
.volume-focus-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.volume-focus-body {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
.volume-focus-body strong {
|
||||
font-size: 15px;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.volume-focus-body > span {
|
||||
color: var(--muted);
|
||||
}
|
||||
.volume-route {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto minmax(0, 0.82fr);
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
color: var(--muted);
|
||||
}
|
||||
.volume-route code {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #c7ddd8;
|
||||
}
|
||||
.docker-meta.compact {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.docker-section-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -433,6 +484,22 @@ h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.08em; }
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.docker-side-row.database-volume {
|
||||
border-color: rgba(113, 191, 120, 0.54);
|
||||
background:
|
||||
linear-gradient(90deg, rgba(113, 191, 120, 0.14), transparent 70%),
|
||||
var(--panel-3);
|
||||
}
|
||||
.docker-side-row.database-volume strong {
|
||||
color: #d6f1df;
|
||||
}
|
||||
.docker-side-more {
|
||||
padding: 6px 7px;
|
||||
border: 1px dashed var(--line-soft);
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
background: rgba(255,255,255,0.02);
|
||||
}
|
||||
|
||||
.monitor-page {
|
||||
display: grid;
|
||||
@@ -791,7 +858,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
|
||||
.topbar { align-items: flex-start; flex-direction: column; }
|
||||
.status-strip { flex-wrap: wrap; white-space: normal; }
|
||||
.metric-grid, .policy-grid, .security-board, .dispatch-form, .docker-metrics, .monitor-chart-grid, .monitor-summary-grid { grid-template-columns: 1fr; }
|
||||
.compact-row, .heartbeat-row, .log-row, .endpoint-list article { grid-template-columns: 1fr; align-items: start; }
|
||||
.compact-row, .heartbeat-row, .log-row, .endpoint-list article, .volume-route { grid-template-columns: 1fr; align-items: start; }
|
||||
.docker-hero, .monitor-hero { flex-direction: column; }
|
||||
.tab { min-width: 104px; }
|
||||
}
|
||||
|
||||
@@ -555,6 +555,33 @@ function dockerStateTone(state: string): string {
|
||||
return "internal";
|
||||
}
|
||||
|
||||
function isHashVolumeName(name: string): boolean {
|
||||
return /^[a-f0-9]{48,64}$/i.test(name);
|
||||
}
|
||||
|
||||
function isDatabaseVolume(volume: AnyRecord): boolean {
|
||||
const name = String(volume?.name || "");
|
||||
const labels = String(volume?.labels || "");
|
||||
return name === "unidesk_pgdata_10gb" || labels.includes("com.docker.compose.volume=unidesk_pgdata_10gb") || name.toLowerCase().includes("pgdata");
|
||||
}
|
||||
|
||||
function volumeRank(volume: AnyRecord): number {
|
||||
const name = String(volume?.name || "");
|
||||
const labels = String(volume?.labels || "");
|
||||
if (isDatabaseVolume(volume)) return 0;
|
||||
if (labels.includes("com.docker.compose.project=unidesk")) return 1;
|
||||
if (!isHashVolumeName(name)) return 2;
|
||||
return 3;
|
||||
}
|
||||
|
||||
function sortVolumes(volumes: AnyRecord[]): AnyRecord[] {
|
||||
return [...volumes].sort((left, right) => {
|
||||
const rankDelta = volumeRank(left) - volumeRank(right);
|
||||
if (rankDelta !== 0) return rankDelta;
|
||||
return String(left.name || "").localeCompare(String(right.name || ""));
|
||||
});
|
||||
}
|
||||
|
||||
function DockerStatusPage({ nodes, dockerStatuses, onRaw }: AnyRecord) {
|
||||
const [selectedProvider, setSelectedProvider] = useState("");
|
||||
const merged = useMemo(() => nodes.map((node: any) => {
|
||||
@@ -575,7 +602,8 @@ function DockerStatusPage({ nodes, dockerStatuses, onRaw }: AnyRecord) {
|
||||
const daemon = status?.daemon || {};
|
||||
const containers = status?.containers || [];
|
||||
const images = status?.images || [];
|
||||
const volumes = status?.volumes || [];
|
||||
const volumes = sortVolumes(status?.volumes || []);
|
||||
const databaseVolume = volumes.find(isDatabaseVolume);
|
||||
const networks = status?.networks || [];
|
||||
const runningContainers = containers.filter((item: any) => item.state === "running");
|
||||
const stoppedContainers = containers.filter((item: any) => item.state !== "running");
|
||||
@@ -619,9 +647,10 @@ function DockerStatusPage({ nodes, dockerStatuses, onRaw }: AnyRecord) {
|
||||
h("div", { className: "docker-metrics" },
|
||||
h(MetricCard, { label: "Containers", value: counts.containers ?? containers.length, hint: `${counts.running ?? runningContainers.length} running / ${counts.stopped ?? stoppedContainers.length} stopped`, tone: "ok" }),
|
||||
h(MetricCard, { label: "Images", value: counts.images ?? images.length, hint: `${counts.daemonImages ?? counts.images ?? images.length} daemon images` }),
|
||||
h(MetricCard, { label: "Volumes", value: counts.volumes ?? volumes.length, hint: "local volumes" }),
|
||||
h(MetricCard, { label: "Volumes", value: counts.volumes ?? volumes.length, hint: databaseVolume ? "database volume visible" : "local volumes", tone: databaseVolume ? "ok" : "" }),
|
||||
h(MetricCard, { label: "Networks", value: counts.networks ?? networks.length, hint: daemon.driver ? `driver ${daemon.driver}` : "docker networks" }),
|
||||
),
|
||||
h(DatabaseVolumeCard, { volume: databaseVolume, volumeCount: volumes.length }),
|
||||
h("div", { className: "docker-section-head" },
|
||||
h("h3", null, "Containers"),
|
||||
h("span", null, `updated ${fmtDate(active.dockerUpdatedAt || status.collectedAt)}`),
|
||||
@@ -641,17 +670,52 @@ function DockerStatusPage({ nodes, dockerStatuses, onRaw }: AnyRecord) {
|
||||
),
|
||||
h("div", { className: "docker-side-stack" },
|
||||
h(DockerSidePanel, { title: "Images", items: images, render: (image: any) => h("article", { key: `${image.id}-${image.repository}`, className: "docker-side-row" }, h("strong", null, `${image.repository}:${image.tag}`), h("span", null, image.size || "--"), h("code", null, image.id || "--")) }),
|
||||
h(DockerSidePanel, { title: "Volumes", items: volumes, render: (volume: any) => h("article", { key: volume.name, className: "docker-side-row" }, h("strong", null, volume.name), h("span", null, volume.driver || "--"), h("code", null, volume.scope || "--")) }),
|
||||
h(DockerSidePanel, { title: "Volumes", items: volumes, limit: volumes.length, render: (volume: any) => h("article", { key: volume.name, className: `docker-side-row volume-row ${isDatabaseVolume(volume) ? "database-volume" : ""}`, "data-testid": isDatabaseVolume(volume) ? "database-volume-row" : undefined },
|
||||
h("strong", null, volume.name),
|
||||
h("span", null, isDatabaseVolume(volume) ? "PostgreSQL" : isHashVolumeName(String(volume.name || "")) ? "anonymous" : "named"),
|
||||
h("code", null, volume.mountpoint || volume.driver || volume.scope || "--"),
|
||||
) }),
|
||||
h(DockerSidePanel, { title: "Networks", items: networks, render: (network: any) => h("article", { key: network.id || network.name, className: "docker-side-row" }, h("strong", null, network.name), h("span", null, network.driver || "--"), h("code", null, network.id || "--")) }),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function DockerSidePanel({ title, items, render }: AnyRecord) {
|
||||
function DatabaseVolumeCard({ volume, volumeCount }: AnyRecord) {
|
||||
return h("section", { className: `docker-volume-focus ${volume ? "ready" : "missing"}`, "data-testid": "database-volume-card" },
|
||||
h("div", { className: "volume-focus-head" },
|
||||
h("span", { className: "panel-eyebrow" }, "Database Named Volume"),
|
||||
h(StatusBadge, { status: volume ? "online" : "warn" }, volume ? "FOUND" : "MISSING"),
|
||||
),
|
||||
volume ? h("div", { className: "volume-focus-body" },
|
||||
h("strong", null, volume.name),
|
||||
h("span", null, "PostgreSQL data volume for unidesk-database"),
|
||||
h("div", { className: "volume-route" },
|
||||
h("code", null, volume.mountpoint || "/var/lib/docker/volumes/unidesk_pgdata_10gb/_data"),
|
||||
h("span", null, "->"),
|
||||
h("code", null, "unidesk-database:/var/lib/postgresql/data"),
|
||||
),
|
||||
h("div", { className: "docker-meta compact" },
|
||||
h("span", null, `driver ${volume.driver || "--"}`),
|
||||
h("span", null, `scope ${volume.scope || "--"}`),
|
||||
h("span", null, `${volumeCount} volumes reported`),
|
||||
),
|
||||
) : h("div", { className: "volume-focus-body" },
|
||||
h("strong", null, "unidesk_pgdata_10gb"),
|
||||
h("span", null, "当前 Docker 快照没有发现数据库命名卷;请检查 provider-gateway 的 Docker volume 上报。"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function DockerSidePanel({ title, items, render, limit }: AnyRecord) {
|
||||
const visibleItems = items.slice(0, limit ?? 12);
|
||||
const hiddenCount = Math.max(0, items.length - visibleItems.length);
|
||||
return h(Panel, { title, eyebrow: `${items.length} items`, className: "docker-side-panel" },
|
||||
items.length === 0 ? h(EmptyState, { title: `暂无 ${title}`, text: "等待 Docker 状态采集" }) :
|
||||
h("div", { className: "docker-side-list" }, items.slice(0, 12).map(render)),
|
||||
h("div", { className: "docker-side-list" },
|
||||
visibleItems.map(render),
|
||||
hiddenCount > 0 ? h("div", { className: "docker-side-more" }, `+ ${hiddenCount} more`) : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user