From 9429747ca42b2976eaa9bdd0ff703455b6967279 Mon Sep 17 00:00:00 2001 From: Joes Date: Fri, 11 Sep 2026 17:17:31 +0800 Subject: [PATCH 1/3] =?UTF-8?q?chore(deps):=20=E6=8F=92=E4=BB=B6=20SDK=20?= =?UTF-8?q?=E6=8A=AC=E5=88=B0=202.0.4,StackExchange.Redis=20=E6=8A=AC?= =?UTF-8?q?=E5=88=B0=203.2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SDK 2.0.4 带的是标签页图标那一面(PluginIcon:ProtocolDescriptor.Icon / WorkspaceDescriptor.Icon / PanelOptions.Icon 三处共用一个类型),下一个提交要用它。 顺手把文件末尾缺的那个换行补回来(.editorconfig 的 insert_final_newline)。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01GXH7SxQFdYtgMWgfjY5TkS --- Directory.Packages.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 349aac8..d65234b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,9 +17,9 @@ - + - + - + + + + <_VelaAvaloniaEditPin>@(PackageVersion->WithMetadataValue('Identity','Avalonia.AvaloniaEdit')->'%(Version)') + + + diff --git a/Directory.Packages.props b/Directory.Packages.props index d65234b..8c8b425 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -31,6 +31,14 @@ + + + + diff --git a/README.md b/README.md index dcaead0..da216e6 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ > 当前版本 **2.0.0** · SDK **2.0.0** [VelaShell](https://github.com/joesdu/VelaShell) 官方维护的插件,一个解决方案管起来: -Redis、S3、Telnet、串口,外加示例插件 HelloWorld。 +Redis、S3、Telnet、串口、Docker 面板。 三个仓库各管一摊,别串: diff --git a/VelaShell.Plugins.slnx b/VelaShell.Plugins.slnx index d7a88f9..d451969 100644 --- a/VelaShell.Plugins.slnx +++ b/VelaShell.Plugins.slnx @@ -20,6 +20,7 @@ + @@ -29,6 +30,7 @@ + diff --git a/plugins/README.md b/plugins/README.md index cba19cc..48b15c9 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -9,7 +9,7 @@ SDK 契约与开发文档在工具链仓库 | 目录 | id | 随包分发 | 装载模式 | 说明 | | --- | --- | --- | --- | --- | -| [VelaShell.Plugin.HelloWorld](VelaShell.Plugin.HelloWorld/) | `velashell.hello-world` | 否 | 隔离进程 | 官方示例:SDK 各能力的最小用法 | +| [VelaShell.Plugin.DockerPanel](VelaShell.Plugin.DockerPanel/) | `velashell.dockerpanel` | 是 | 进程内 | 远端 Docker 管理面板:容器 / 镜像 / 卷 / 网络 / Compose,含实时统计、日志、容器内文件编辑与内置终端 | | [VelaShell.Plugin.Redis](VelaShell.Plugin.Redis/) | `velashell.redis` | 是 | 进程内 | Redis 客户端:键浏览、类型化查看与编辑、命令执行 | | [VelaShell.Plugin.S3](VelaShell.Plugin.S3/) | `velashell.s3` | 是 | 进程内 | S3 兼容对象存储:协议 + 桶管理器 + 对象检视器(协议能力域的首个使用者) | | [VelaShell.Plugin.Serial](VelaShell.Plugin.Serial/) | `velashell.serial` | 是 | 进程内 | RS-232 / USB 转串口终端:端口热插拔枚举、换行归一化、发送节流、Break 与 DTR/RTS | @@ -19,7 +19,20 @@ SDK 契约与开发文档在工具链仓库 隔离插件跑在独立的 `VelaShell.PluginHost` 进程里(实现在主仓库),崩溃不波及宿主; S3 与 Redis 因为**协议能力只在进程内可用**必须进程内装载 —— 协议是宿主反向调用插件的 高频通道,隔离进程的 RPC 只承载插件→宿主方向(清单校验会直接拒绝 protocols + isolated 的组合); -Telnet 与串口同理。 +Telnet 与串口同理。Docker 面板的理由是另一条:它要把一个原生 Avalonia 控件挂进主窗口标签区 +(控件无法跨进程嵌入),而且 `IRemoteTunnelApi` 交给它的是一条**活的 `Stream`**, +隔离进程里明确不可用。 + +> **Docker 面板要 SDK ≥ 2.0.0**(`plugin.json` 里钉了 `minSdkVersion`)。它说的是 +> Docker Engine 的 HTTP API,而这条 API 的载体是远端的一个 unix socket —— +> 要的是到那个 socket 的**裸字节双工流**(`IRemoteTunnelApi`)。SDK 1.1 的远程执行只有 +> "整段 UTF-8 解码"与"按 `\n` 切行"两种**文本**形态,承载不了分块传输、tar 归档流与 +> `exec` 的多路复用帧:UTF-8 解码把非法字节换成 U+FFFD(不可逆),按行切分在 `0x0A` +> 处把一帧劈成两半 —— 那不是慢一点,是**数据静默损坏**。`apiLevel` 表达不了这一档 +> (它只在**破坏性**变更时才动),所以另钉 `minSdkVersion`。 +> +> 它也是本仓库**唯一直接引 Avalonia.\* 包**的插件(`Avalonia.AvaloniaEdit`,用于 +> compose.yaml / .env 的语法高亮)。这一条有个反直觉的后果,见下一节最后那段。 > **串口插件要 SDK ≥ 1.5.0**。它是连接表单三件新面的驱动者与首个使用者: > `ProtocolFeatures.NoEndpoint`(收起端口栏)、`ProtocolSettingKind.DynamicChoice` + @@ -56,16 +69,36 @@ Telnet 与串口同理。 `VelaExcludeSharedRuntimeAssets` 已经按装载器的判定口径(`VelaShell.PluginSdk` 与 `Avalonia*` 前缀)把共享程序集的运行时资产排掉了。 +### 例外:确实需要某个 `Avalonia.*` 包时,`ExcludeAssets="runtime"` 必须自己写 + +上一段那条"不要写 `ExcludeAssets`"的前提是**插件不直接引 Avalonia 包** —— +前四个插件都不引,SDK 包处理它自己那条引用就够了。 + +DockerPanel 要 `Avalonia.AvaloniaEdit` 做语法高亮,于是撞上了另一面: +`VelaExcludeSharedRuntimeAssets` 排得掉 `Avalonia.AvaloniaEdit` 自己的运行时资产, +**排不掉它带进来的传递依赖**。去掉那条 `ExcludeAssets`, +`AvaloniaEdit → Avalonia → MicroCom.Runtime` 里的 `MicroCom.Runtime.dll` +就会落进插件目录(2026-09-11 并库时实测,构建 0 错 0 警告,包照常打出来)。 + +它不以 `Avalonia` 打头,因此**两道防线同时漏掉它**:装载器的共享前缀判定不认它 +(插件会加载自己那一份,与宿主的 Avalonia COM 互操作分属两个类型标识), +CI 那条泄漏体检原本也只认 `Avalonia*` 与 `VelaShell.PluginSdk.dll`。 +体检已经把 `MicroCom.Runtime.dll` 补进去了,但**规矩仍然是显式写**: + +```xml + +``` + 本目录的 `Directory.Build.props/targets` 只额外做三件仓库自己的事: `VelaPluginShip`(是否随应用分发)、构建后镜像到 `artifacts/plugins/<目录名>/` 与本机宿主、以及发布期的 `GetVelaPluginPayload`。 ## 分发 -"随包分发"由 csproj 的 `` 控制(默认 `true`)。示例插件设 `false`: +"随包分发"由 csproj 的 `` 控制(默认 `true`)。设成 `false` 的插件 本机构建仍会镜像到 `artifacts/plugins/`(以及 `VELASHELL_DEV_APP_DIR` 指定的应用目录), -装载起来验证插件系统没问题,但它不会被收进分发布局 —— -它是给开发者读的范例,不是给用户装的功能。 +装载起来验证插件系统没问题,但它不会被收进分发布局 —— 给开发者读的范例用这一档。 +(当前五个插件都是 `true`;示例插件 HelloWorld 已于 2026-08 移除。) [`build/PluginBundle.proj`](../build/PluginBundle.proj) 的 `Bundle` 目标把 `VelaPluginShip=true` 的插件收成安装包 `plugins/` 那一层的布局:它不再作为 Release 资产上传,只在 CI 与发布流水线里 @@ -86,17 +119,10 @@ Telnet 与串口同理。 与 MSBuild 的 `VelaPluginsVersion` 毫无关系。两处必须一起写,只写一处就会出现 "发了 1.4.0,包却叫 velashell.redis-0.1.0.vpx"。 -## 规划中(尚未创建) - -- **串口插件**(`velashell.serial`):与 Telnet 同为终端协议能力的使用者; - 依赖 `System.IO.Ports`,要处理三平台端口枚举与 `Close()` 死锁。 - 连接对话框里的「串口」页签在它落地前保持禁用占位。 -- **容器管理插件**:基于远程执行能力封装 docker/podman 常用操作 - (已有独立仓库原型 `joesdu/VelaShell.Plugin.DockerPanel`,尚未并入本仓库)。 - ## 新建插件 -1. 复制 `VelaShell.Plugin.HelloWorld/` 为新目录,改 csproj 中的 `` 与 `plugin.json`; +1. 挑一个形态最接近的现有插件复制成新目录(终端类看 Telnet,面板类看 DockerPanel), + 改 csproj 中的 `` 与 `plugin.json`; 2. 新依赖的版本加进根 `Directory.Packages.props`(中央包管理,csproj 里不写 `Version=`); 3. 把项目与它的测试工程加入 `VelaShell.Plugins.slnx` 的 `/plugins/`、`/tests/` 文件夹; 4. `dotnet build plugins/VelaShell.Plugin.<名字>` —— 输出自动镜像到 diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/BatchRunner.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/BatchRunner.cs new file mode 100644 index 0000000..0949a17 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/BatchRunner.cs @@ -0,0 +1,94 @@ +namespace VelaShell.Plugin.DockerPanel.Docker; + +/// 批量操作里单个目标的结果。 +/// 目标的显示名(容器名 / 卷名)。 +/// 是否成功。 +/// 失败原因(daemon 的原话);成功时为 。 +public readonly record struct BatchOutcome(string Target, bool Succeeded, string? Failure); + +/// 一次批量操作的汇总。 +/// 逐个目标的结果,顺序与发起时一致。 +public sealed record BatchResult(IReadOnlyList Outcomes) +{ + /// 成功数。 + public int SucceededCount => Outcomes.Count(o => o.Succeeded); + + /// 失败数。 + public int FailedCount => Outcomes.Count(o => !o.Succeeded); + + /// 全部成功。 + public bool AllSucceeded => FailedCount == 0; + + /// 失败的那些。 + public IEnumerable Failures => Outcomes.Where(o => !o.Succeeded); +} + +/// +/// 批量执行器。 +/// +/// 存在的唯一理由是**逐个目标判定**:选中 10 个容器点停止,其中 2 个因为被依赖而失败时, +/// 界面要能说"成功 8、失败 2 —— postgres-main 被 api-gateway 依赖", +/// 而不是把整批说成"操作失败"。所以这里绝不 short-circuit:一个目标的异常 +/// 只记在它自己名下,后面的照跑。 +/// +/// +public static class BatchRunner +{ + /// + /// 对每个目标跑一遍 ,逐个记录成败。 + /// + /// 目标与显示名。 + /// 对单个目标的操作。 + /// 每完成一个回调一次(已完成数, 总数, 当前目标名)。 + /// + /// 取消令牌。触发后**不再启动**新目标,已经发出去的那条等它自己结束 —— + /// 半路掐掉一条 docker stop 只会留下一个状态不明的容器。 + /// + public static async Task RunAsync( + IReadOnlyList<(T Target, string Name)> targets, + Func action, + Action? onProgress = null, + CancellationToken cancellationToken = default) + { + List outcomes = [with(targets.Count)]; + for (var i = 0; i < targets.Count; i++) + { + (var target, var name) = targets[i]; + if (cancellationToken.IsCancellationRequested) + { + outcomes.Add(new(name, false, "已取消,这个目标没有执行。")); + continue; + } + onProgress?.Invoke(i, targets.Count, name); + try + { + await action(target, cancellationToken).ConfigureAwait(false); + outcomes.Add(new(name, true, null)); + } + catch (DockerApiException ex) + { + outcomes.Add(new(name, false, ex.Message)); + } + catch (DockerUnreachableException ex) + { + // 连接没了就没有必要继续戳后面的目标 —— 它们只会拿到同一条错误。 + outcomes.Add(new(name, false, ex.Message)); + for (var j = i + 1; j < targets.Count; j++) + { + outcomes.Add(new(targets[j].Name, false, "连接已断开,这个目标没有执行。")); + } + break; + } + catch (OperationCanceledException) + { + outcomes.Add(new(name, false, "已取消。")); + } + catch (Exception ex) + { + outcomes.Add(new(name, false, ex.Message)); + } + } + onProgress?.Invoke(targets.Count, targets.Count, ""); + return new(outcomes); + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/ComposeCli.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/ComposeCli.cs new file mode 100644 index 0000000..5c53036 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/ComposeCli.cs @@ -0,0 +1,303 @@ +using System.Text.Json.Serialization; +using VelaShell.PluginSdk.RemoteExec; + +namespace VelaShell.Plugin.DockerPanel.Docker; + +/// POSIX shell 引用。 +public static class Sh +{ + /// 单引号引用一个参数(内部单引号转成 '\'')。 + public static string Quote(string value) => "'" + value.Replace("'", "'\\''") + "'"; +} + +/// 一个 compose 项目。 +/// 项目名。 +/// daemon 报的状态串,如 running(3)。 +/// compose 文件路径(逗号分隔的原文)。 +public sealed record ComposeProject(string Name, string Status, string ConfigFiles) +{ + /// 第一个 compose 文件的路径。 + public string PrimaryFile => ConfigFiles.Split(',', StringSplitOptions.RemoveEmptyEntries) is { Length: > 0 } files + ? files[0].Trim() + : ""; + + /// + /// 项目目录(compose 文件所在目录)。 + /// + /// 两种分隔符都得认:本机端点上 compose ls 报回来的是 Windows 路径, + /// 只认 / 的话这里会算出空串 —— 于是 .env 找不到、 + /// --project-directory 也不会下发。 + /// + /// + public string ProjectDirectory => PrimaryFile is { Length: > 0 } file && file != ComposePath.LastSegment(file) + ? ComposePath.DirectoryOf(file) + : ""; + + /// 运行中的服务数(从状态串里解出来;解不出给 0)。 + public int RunningCount => ParseCount("running"); + + /// 已退出的服务数。 + public int ExitedCount => ParseCount("exited"); + + private int ParseCount(string keyword) + { + var at = Status.IndexOf(keyword, StringComparison.OrdinalIgnoreCase); + if (at < 0) + { + return 0; + } + var open = Status.IndexOf('(', at); + var close = open > 0 ? Status.IndexOf(')', open) : -1; + return close > open && int.TryParse(Status.AsSpan(open + 1, close - open - 1), out var n) ? n : 0; + } +} + +/// compose 项目里的一个服务。 +public sealed record ComposeService +{ + /// 服务名。 + [JsonPropertyName("Service")] + public string Service { get; init; } = ""; + + /// 容器名。 + [JsonPropertyName("Name")] + public string Name { get; init; } = ""; + + /// 镜像。 + [JsonPropertyName("Image")] + public string? Image { get; init; } + + /// 状态,如 running + [JsonPropertyName("State")] + public string? State { get; init; } + + /// 人话状态。 + [JsonPropertyName("Status")] + public string? Status { get; init; } + + /// 端口摘要。 + [JsonPropertyName("Publishers")] + public ComposePublisher[]? Publishers { get; init; } + + /// 端口摘要文本。 + public string PortsText + { + get + { + if (Publishers is null or { Length: 0 }) + { + return "—"; + } + var parts = Publishers + .Where(p => p.PublishedPort > 0) + .Select(p => $"{p.PublishedPort}→{p.TargetPort}") + .Distinct(); + var text = string.Join(", ", parts); + return text.Length == 0 ? "—" : text; + } + } +} + +/// compose ps 里的一条端口发布。 +public sealed record ComposePublisher +{ + /// 容器内端口。 + public int TargetPort { get; init; } + + /// 宿主端口。 + public int PublishedPort { get; init; } + + /// 协议。 + public string? Protocol { get; init; } +} + +/// +/// Compose 走 CLI,不走 HTTP API。 +/// +/// 这不是偷懒。 Docker Engine API 里**没有 compose 这一块** —— compose 是一个 +/// CLI 插件,它读 yml、算出依赖顺序,再调用一串普通的 Engine API。想在面板里"用 API 做 +/// compose",等于把 compose 自己重写一遍,而且注定与远端装的那个版本行为不一致。 +/// 所以项目管理照旧走 docker compose,由 把命令送到该去的地方。 +/// +/// +/// 代价是:这台机器上得真有 docker compose。远端由 SSH 会话执行,本机则起一个本地 +/// docker 进程 —— 装了 Docker Desktop 的本机是有的,所以本机端点不再被一刀切成 +/// "不支持"。 会实地探一次,探不到才把话说给用户听。 +/// +/// +public sealed class ComposeCli(IComposeHost host) +{ + /// 这条 compose 通道是怎么跑的(诊断与文案用)。 + public string HostDescription => host.Description; + + /// 命令跑在本机(而不是经 SSH 下发到远端)。 + public bool IsLocal => host is LocalComposeHost; + + /// + /// 列出 compose 项目。 + /// + /// compose ls 只认得**起过至少一次**的项目 —— 它读的是容器上的标签,而不是 + /// 磁盘上的 yml。所以列表为空不代表机器上没有 compose 文件;界面另给一个 + /// 「按路径打开」的入口。 + /// + /// + public async Task ListProjectsAsync(CancellationToken cancellationToken = default) + { + var result = await host + .RunAsync(["compose", "ls", "--all", "--format", "json"], TimeSpan.FromSeconds(20), cancellationToken) + .ConfigureAwait(false); + if (!result.IsSuccess) + { + // compose v1(独立的 docker-compose)根本没有 ls 子命令。这时给空列表 + + // 上层的提示,而不是把一条看不懂的错误摔在用户脸上。 + return []; + } + var entries = DockerJson.TryDeserialize(result.Output.Trim()); + return entries is null + ? [] + : [.. entries.Select(e => new ComposeProject(e.Name ?? "", e.Status ?? "", e.ConfigFiles ?? ""))]; + } + + /// 列出项目里的服务。 + public async Task ListServicesAsync(ComposeProject project, CancellationToken cancellationToken = default) + { + var result = await host + .RunAsync([.. Prefix(project), "ps", "-a", "--format", "json"], TimeSpan.FromSeconds(20), cancellationToken) + .ConfigureAwait(false); + if (!result.IsSuccess) + { + return []; + } + var text = result.Output.Trim(); + if (text.Length == 0) + { + return []; + } + // compose 的 --format json 在不同版本里一会儿给 JSON 数组、一会儿给 NDJSON。 + // 两种都收,免得面板在某个次版本上突然空白。 + if (text.StartsWith('[')) + { + return DockerJson.TryDeserialize(text) ?? []; + } + List services = []; + foreach (var line in text.Split('\n', StringSplitOptions.RemoveEmptyEntries)) + { + if (DockerJson.TryDeserialize(line.Trim()) is { } service) + { + services.Add(service); + } + } + return [.. services]; + } + + /// 展开后的配置(顺带就是一次语法校验)。 + public async Task ConfigAsync(ComposeProject project, CancellationToken cancellationToken = default) => + await host.RunAsync([.. Prefix(project), "config"], TimeSpan.FromSeconds(30), cancellationToken) + .ConfigureAwait(false); + + /// + /// 跑一条 compose 子命令,输出**边跑边回调**(up -d 要几十秒, + /// 用户得看见它在动)。 + /// + /// 目标项目。 + /// 子命令与参数,如 up -d。 + /// 逐行输出。 + /// 取消令牌。 + /// 退出码。 + public Task RunAsync(ComposeProject project, IReadOnlyList arguments, + IProgress onOutput, CancellationToken cancellationToken = default) => + host.StreamAsync([.. Prefix(project), .. arguments], onOutput, cancellationToken); + + /// + /// 跟着一个项目的**合并日志**(compose logs -f)。 + /// + /// 与容器页那个合并流不是一回事:那一个是面板自己把 N 条 docker logs 并起来, + /// 这一个交给 compose 自己去并 —— 它认得项目里有哪些服务,包括面板列表还没刷到的新容器。 + /// + /// + public Task FollowLogsAsync(ComposeProject project, string tail, IProgress onOutput, + CancellationToken cancellationToken = default) => + RunAsync(project, ["logs", "-f", "--no-color", "--tail", tail], onOutput, cancellationToken); + + /// 对单个服务跑一条子命令(逐服务的日志 / 重启走这里)。 + public Task RunForServiceAsync(ComposeProject project, IReadOnlyList arguments, string service, + IProgress onOutput, CancellationToken cancellationToken = default) => + RunAsync(project, [.. arguments, service], onOutput, cancellationToken); + + /// + /// 项目的 .env 路径。 + /// + /// compose 只认项目目录下那一个 .env(--env-file 另说), + /// 所以这里不去猜别的位置 —— 猜错了改的是一个没人读的文件。 + /// + /// + public static string EnvPath(ComposeProject project) => + project.ProjectDirectory is { Length: > 0 } dir ? ComposePath.Combine(dir, ".env") : ""; + + /// 读 compose 文件(远端经 SFTP,本机直接读盘;都不经 shell)。 + public Task ReadFileAsync(string path, CancellationToken cancellationToken = default) => + host.ReadFileAsync(path, cancellationToken); + + /// + /// 写回 compose 文件(远端经 SFTP,本机直接落盘)。 + /// 覆盖写,界面上必须走"手打确认串"那一档闸门。 + /// + public Task WriteFileAsync(string path, string content, CancellationToken cancellationToken = default) => + host.WriteFileAsync(path, content, cancellationToken); + + /// 探测远端有没有 docker compose(v2)。 + public async Task IsAvailableAsync(CancellationToken cancellationToken = default) + { + try + { + var result = await host + .RunAsync(["compose", "version", "--short"], TimeSpan.FromSeconds(15), cancellationToken) + .ConfigureAwait(false); + return result.IsSuccess; + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + // 本机上 docker 根本不在 PATH 里时,起进程这一步就会抛 —— 那也是一种"没有", + // 不是一个要摔给用户的异常。 + return false; + } + } + + /// + /// compose 命令的固定前缀。 + /// + /// -p-f--project-directory 三个一起钉住,少一个都出错: + /// 光给项目名,compose 找不到 yml(它不记得项目从哪来);光给文件,项目名会按目录名 + /// 重新推导,down 掉的可能是另一个项目;不给 project-directory,yml 里的 + /// ./data 会以**登录目录**为基准解析 —— 一个安静地挂错盘的 bug。 + /// + /// + private static List Prefix(ComposeProject project) + { + List argv = ["compose", "-p", project.Name]; + foreach (var file in project.ConfigFiles.Split(',', StringSplitOptions.RemoveEmptyEntries)) + { + argv.Add("-f"); + argv.Add(file.Trim()); + } + if (project.ProjectDirectory is { Length: > 0 } directory) + { + argv.Add("--project-directory"); + argv.Add(directory); + } + return argv; + } +} + +/// docker compose ls --format json 的一项。 +internal sealed record ComposeListEntry +{ + [JsonPropertyName("Name")] + public string? Name { get; init; } + + [JsonPropertyName("Status")] + public string? Status { get; init; } + + [JsonPropertyName("ConfigFiles")] + public string? ConfigFiles { get; init; } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/ComposeHost.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/ComposeHost.cs new file mode 100644 index 0000000..92e14e4 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/ComposeHost.cs @@ -0,0 +1,222 @@ +using System.Diagnostics; +using System.Text; +using VelaShell.PluginSdk.RemoteExec; +using VelaShell.PluginSdk.RemoteFs; + +namespace VelaShell.Plugin.DockerPanel.Docker; + +/// +/// docker compose 跑在哪儿。 +/// +/// compose 与面板其余部分不同:容器、镜像、卷、网络全都是 Engine 的 HTTP API, +/// 而 compose 只有 CLI(daemon 上没有 compose 端点,它是客户端侧把 yml 展开成 +/// 一串 create/start 调用的)。所以这一块必须有一条"执行命令"的通道。 +/// +/// +/// 远端那条通道是 SSH(宿主的 );本机那条是**本地进程**。 +/// 早先本机端点直接被判成"不支持 compose",那句话其实是在描述面板自己缺一条通道, +/// 却被写成了"compose 是远端的东西" —— 本机装着 Docker Desktop 的用户当然不认。 +/// +/// +/// 参数一律按 argv 传,不拼成一整行:本机走 , +/// 根本不经过 shell,Windows 上也就没有引用规则可踩;远端才按 POSIX 规则引用一次。 +/// +/// +public interface IComposeHost +{ + /// 这条通道的说明(诊断与文案用)。 + string Description { get; } + + /// 跑一条命令并等它结束。退出码非 0 不抛异常 —— 那是一种正常结果。 + Task RunAsync(IReadOnlyList arguments, TimeSpan timeout, + CancellationToken cancellationToken = default); + + /// 跑一条命令,输出边跑边回调。返回退出码。 + Task StreamAsync(IReadOnlyList arguments, IProgress output, + CancellationToken cancellationToken = default); + + /// 读一个文件(compose.yaml / .env)。 + Task ReadFileAsync(string path, CancellationToken cancellationToken = default); + + /// 覆盖写一个文件。 + Task WriteFileAsync(string path, string content, CancellationToken cancellationToken = default); +} + +/// 远端:命令走 SSH 的 exec 通道,文件走 SFTP。 +public sealed class RemoteComposeHost(IRemoteExecApi exec, IRemoteFsApi remoteFs, string sessionId) : IComposeHost +{ + /// + public string Description => "经 SSH 会话执行"; + + /// + public Task RunAsync(IReadOnlyList arguments, TimeSpan timeout, + CancellationToken cancellationToken = default) => + exec.RunAsync(sessionId, Join(arguments), new ExecOptions { Timeout = timeout }, cancellationToken); + + /// + public async Task StreamAsync(IReadOnlyList arguments, IProgress output, + CancellationToken cancellationToken = default) + { + var result = await exec + .StreamAsync(sessionId, Join(arguments), new ExecStreamOptions { IncludeStandardError = true }, + output, cancellationToken) + .ConfigureAwait(false); + return result.ExitCode; + } + + /// + public async Task ReadFileAsync(string path, CancellationToken cancellationToken = default) + { + var bytes = await remoteFs.ReadAllBytesAsync(sessionId, path, 4 << 20, cancellationToken) + .ConfigureAwait(false); + return Encoding.UTF8.GetString(bytes); + } + + /// + public Task WriteFileAsync(string path, string content, CancellationToken cancellationToken = default) => + remoteFs.WriteAllBytesAsync(sessionId, path, Encoding.UTF8.GetBytes(content), cancellationToken); + + /// + /// 拼成远端 shell 的一行。 + /// + /// argv 里不含 docker —— 本机那头由 承担, + /// 远端这头就在这里补上。引用也只在这一处发生:远端那头是一个 shell,本机那头不是。 + /// + /// + private static string Join(IReadOnlyList arguments) => + "docker " + string.Join(' ', arguments.Select(a => a.Length > 0 && a.All(IsBare) ? a : Sh.Quote(a))); + + private static bool IsBare(char c) => char.IsAsciiLetterOrDigit(c) || c is '-' or '_' or '.' or '/' or '=' or ':'; +} + +/// +/// 本机:命令是一个本地进程,文件就是本地文件。 +/// +/// 本机端点本来就已经绕开 SDK 直接开命名管道 / unix 套接字连 daemon +/// (见 )—— 本地起一个 docker 进程与那件事同一性质, +/// 不是新开的口子。 +/// +/// +public sealed class LocalComposeHost : IComposeHost +{ + /// + public string Description => "在本机执行 docker compose"; + + /// + public async Task RunAsync(IReadOnlyList arguments, TimeSpan timeout, + CancellationToken cancellationToken = default) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(timeout); + var stdout = new StringBuilder(); + var stderr = new StringBuilder(); + int exit; + try + { + exit = await PumpAsync(arguments, + line => stdout.AppendLine(line), + line => stderr.AppendLine(line), + cts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + throw new TimeoutException($"本机命令超时({timeout.TotalSeconds:0} 秒):docker {string.Join(' ', arguments)}"); + } + return new(stdout.ToString()) + { + Error = stderr.ToString(), + ExitCode = exit + }; + } + + /// + public Task StreamAsync(IReadOnlyList arguments, IProgress output, + CancellationToken cancellationToken = default) => + PumpAsync(arguments, + line => output.Report(new(ExecStream.StandardOutput, line)), + line => output.Report(new(ExecStream.StandardError, line)), + cancellationToken); + + /// + public async Task ReadFileAsync(string path, CancellationToken cancellationToken = default) => + await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false); + + /// + public Task WriteFileAsync(string path, string content, CancellationToken cancellationToken = default) => + File.WriteAllTextAsync(path, content, cancellationToken); + + /// + /// 起一个 docker 进程,把两条输出逐行喂出去。 + /// + /// 不经 shell(UseShellExecute = false + ArgumentList):参数按数组交给操作系统, + /// 带空格的路径、带引号的项目名都不需要额外转义,Windows 与 POSIX 的行为也就一致了。 + /// + /// + /// 取消时先 Kill(entireProcessTree):compose up 会拉起子进程, + /// 只杀父进程会留下一串没人管的孤儿。 + /// + /// + private static async Task PumpAsync(IReadOnlyList arguments, Action onOut, + Action onError, CancellationToken cancellationToken) + { + var info = new ProcessStartInfo("docker") + { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + StandardOutputEncoding = Encoding.UTF8, + StandardErrorEncoding = Encoding.UTF8 + }; + foreach (var argument in arguments) + { + info.ArgumentList.Add(argument); + } + using var process = new Process { StartInfo = info, EnableRaisingEvents = true }; + process.OutputDataReceived += (_, e) => + { + if (e.Data is { } line) + { + onOut(line); + } + }; + process.ErrorDataReceived += (_, e) => + { + if (e.Data is { } line) + { + onError(line); + } + }; + if (!process.Start()) + { + throw new InvalidOperationException("起不了 docker 进程"); + } + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + try + { + await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + TryKill(process); + throw; + } + return process.ExitCode; + } + + private static void TryKill(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + } + catch (Exception) + { + // 进程可能刚好自己退了 —— 那正是我们想要的结果,不必再说什么。 + } + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/ComposePath.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/ComposePath.cs new file mode 100644 index 0000000..d45d9c9 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/ComposePath.cs @@ -0,0 +1,49 @@ +namespace VelaShell.Plugin.DockerPanel.Docker; + +/// +/// compose 项目路径的一点点常识。 +/// +/// 面板同时对着两种文件系统:远端是 POSIX(/srv/stacks/web/compose.yaml), +/// 本机可能是 Windows(盘符打头、反斜杠分隔)。原来这些地方一律按 / 处理, +/// 于是本机端点上目录永远算成空串,"要用绝对路径"还会拦下一个本来就绝对的路径。 +/// +/// +/// 这里:那套 API 按**跑面板的这台机器**判断分隔符, +/// 而路径说的是**Docker 所在的那台机器** —— 一个 Windows 客户端连远端 Linux 时,两者不是一回事。 +/// 分隔符只能从路径本身看出来。 +/// +/// +internal static class ComposePath +{ + private static readonly char[] Separators = ['/', '\\']; + + /// 这条路径用的是哪种分隔符(看不出来时按 POSIX)。 + internal static char SeparatorOf(string path) => + path.Contains('\\') && !path.Contains('/') ? '\\' : '/'; + + /// 绝对路径?POSIX 的 /…、Windows 的盘符式与 UNC 式都算。 + internal static bool IsAbsolute(string path) => + path.StartsWith('/') + || path.StartsWith(@"\\") + || (path.Length >= 3 && char.IsAsciiLetter(path[0]) && path[1] == ':' && Separators.Contains(path[2])); + + /// 去掉最后一段(取所在目录);没有分隔符就原样返回。 + internal static string DirectoryOf(string path) + { + var trimmed = path.TrimEnd(Separators); + var slash = trimmed.LastIndexOfAny(Separators); + return slash > 0 ? trimmed[..slash] : trimmed; + } + + /// 取最后一段(目录名 / 文件名)。 + internal static string LastSegment(string path) + { + var trimmed = path.TrimEnd(Separators); + var slash = trimmed.LastIndexOfAny(Separators); + return slash >= 0 ? trimmed[(slash + 1)..] : trimmed; + } + + /// 在目录后面接一段,沿用这条路径自己的分隔符。 + internal static string Combine(string directory, string segment) => + $"{directory.TrimEnd(Separators)}{SeparatorOf(directory)}{segment}"; +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DiskMath.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DiskMath.cs new file mode 100644 index 0000000..fd4e8a4 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DiskMath.cs @@ -0,0 +1,52 @@ +namespace VelaShell.Plugin.DockerPanel.Docker; + +/// 一份 system df 里"能删掉多少"的拆分。 +/// 未被任何容器引用的镜像。 +/// 引用计数为 0 的本地卷。 +/// 未在使用的构建缓存。 +/// 上述镜像的个数。 +/// 上述卷的个数。 +public readonly record struct ReclaimBreakdown( + long Images, + long Volumes, + long BuildCache, + int UnusedImages, + int UnusedVolumes) +{ + /// 合计可回收字节。 + public long Total => Images + Volumes + BuildCache; + + /// 一句话说清这些空间躺在哪儿。 + public string Describe() => Total <= 0 + ? "没有可回收的空间" + : $"{UnusedImages} 个未使用镜像 · {UnusedVolumes} 个游离卷"; +} + +/// +/// system df 的口径换算。 +/// +/// 单独拎出来是因为总览页与系统页都要给出"可回收多少",而这两处一旦各算各的, +/// 用户就会在同一个面板的两张卡上看到两个数 —— 那比不显示更糟。 +/// +/// +public static class DiskMath +{ + /// docker system prune -a --volumes 的口径算可回收空间。 + public static ReclaimBreakdown Reclaimable(DiskUsage? usage) + { + if (usage is null) + { + return default; + } + // "未使用"跟着 Containers == 0 走,与 daemon 自己 prune 时的判断一致; + // 悬空镜像只是其中一个子集,拿它当分母会把可回收量算少。 + ImageSummary[] unusedImages = [.. (usage.Images ?? []).Where(i => i.Containers <= 0)]; + VolumeSummary[] unusedVolumes = [.. (usage.Volumes ?? []).Where(v => v.UsageData is { RefCount: <= 0 })]; + return new( + unusedImages.Sum(i => i.Size), + unusedVolumes.Sum(v => v.UsageData is { Size: > 0 } u ? u.Size : 0), + (usage.BuildCache ?? []).Where(c => !c.InUse).Sum(c => c.Size), + unusedImages.Length, + unusedVolumes.Length); + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerApiException.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerApiException.cs new file mode 100644 index 0000000..666c6fc --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerApiException.cs @@ -0,0 +1,61 @@ +using System.Net; + +namespace VelaShell.Plugin.DockerPanel.Docker; + +/// +/// daemon 如实返回的一条失败。 +/// +/// Docker 把人话写在响应体的 {"message":"…"} 里,而状态码只给出粗分类。 +/// 界面上要显示的是那句人话("port is already allocated"、"container is paused"), +/// 不是 "HTTP 409" —— 后者对用户没有任何可操作性。 +/// +/// +public sealed class DockerApiException(HttpStatusCode statusCode, string message, string? requestPath = null) + : Exception(message) +{ + /// daemon 返回的状态码。 + public HttpStatusCode StatusCode { get; } = statusCode; + + /// 发起的请求路径(诊断用,不进主文案)。 + public string? RequestPath { get; } = requestPath; + + /// 目标不存在(404)。批量操作里把它单独归类:多半是别处已经删掉了。 + public bool IsNotFound => StatusCode == HttpStatusCode.NotFound; + + /// 状态冲突(409):容器已在运行、卷仍被占用、网络还连着容器。 + public bool IsConflict => StatusCode == HttpStatusCode.Conflict; +} + +/// +/// 连不上 daemon(隧道没建起来、socket 不存在、权限不足)。 +/// +/// 与 分开是因为**出路完全不同**:后者是"这条操作不行", +/// 前者是"这台机器还没准备好" —— 界面要给的是"把账号加进 docker 组"这类指引, +/// 而不是重试按钮。 +/// +/// +public sealed class DockerUnreachableException(string message, DockerUnreachableReason reason, Exception? inner = null) + : Exception(message, inner) +{ + /// 连不上的分类,决定界面给哪一条出路。 + public DockerUnreachableReason Reason { get; } = reason; +} + +/// 连不上 daemon 的分类。 +public enum DockerUnreachableReason +{ + /// 说不清,原样把底层异常的话报上去。 + Unknown, + + /// 会话没了或没连上。 + SessionUnavailable, + + /// socket 文件不存在 —— 多半是远端压根没装 Docker,或 daemon 没在跑。 + SocketMissing, + + /// socket 在,但当前账号没有读写权限(不在 docker 组)。 + PermissionDenied, + + /// 宿主不支持隧道(隔离进程模式)。 + TunnelUnsupported +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Containers.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Containers.cs new file mode 100644 index 0000000..ae535b8 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Containers.cs @@ -0,0 +1,241 @@ +namespace VelaShell.Plugin.DockerPanel.Docker; + +public sealed partial class DockerClient +{ + /// 列容器。 + /// 是否含已停止的。 + /// 取消令牌。 + public Task ListContainersAsync(bool all = true, CancellationToken cancellationToken = default) => + GetJsonAsync("/containers/json" + Query(("all", all ? "1" : "0")), cancellationToken); + + /// 容器 inspect(结构化)。 + public Task InspectContainerAsync(string id, CancellationToken cancellationToken = default) => + GetJsonAsync($"/containers/{Uri.EscapeDataString(id)}/json", cancellationToken); + + /// 容器 inspect 的**原始 JSON**,直接呈现给界面。 + /// + /// 详情页要的是"全部",而把整个 schema 抄成 DTO 只会让每次 Docker 升级 + /// 都悄悄少显示几个字段。原文没有这个问题。 + /// + public async Task InspectContainerRawAsync(string id, CancellationToken cancellationToken = default) => + DockerJson.Prettify(await GetStringAsync($"/containers/{Uri.EscapeDataString(id)}/json", cancellationToken) + .ConfigureAwait(false)); + + /// 启动容器。 + public Task StartContainerAsync(string id, CancellationToken cancellationToken = default) => + PostAsync($"/containers/{Uri.EscapeDataString(id)}/start", null, cancellationToken); + + /// 停止容器(先 SIGTERM,超时后 SIGKILL)。 + /// 容器 id 或名字。 + /// 等待优雅退出的秒数。 + /// 取消令牌。 + public Task StopContainerAsync(string id, int timeoutSeconds = 10, CancellationToken cancellationToken = default) => + PostAsync($"/containers/{Uri.EscapeDataString(id)}/stop" + Query(("t", timeoutSeconds.ToString())), null, cancellationToken); + + /// 重启容器。 + public Task RestartContainerAsync(string id, int timeoutSeconds = 10, CancellationToken cancellationToken = default) => + PostAsync($"/containers/{Uri.EscapeDataString(id)}/restart" + Query(("t", timeoutSeconds.ToString())), null, cancellationToken); + + /// 暂停容器(SIGSTOP,进程还在)。 + public Task PauseContainerAsync(string id, CancellationToken cancellationToken = default) => + PostAsync($"/containers/{Uri.EscapeDataString(id)}/pause", null, cancellationToken); + + /// 恢复容器。 + public Task UnpauseContainerAsync(string id, CancellationToken cancellationToken = default) => + PostAsync($"/containers/{Uri.EscapeDataString(id)}/unpause", null, cancellationToken); + + /// + /// 强杀容器。默认 SIGKILL —— 不给进程刷缓冲、关连接的机会, + /// 所以界面上它单独走一道确认。 + /// + public Task KillContainerAsync(string id, string signal = "SIGKILL", CancellationToken cancellationToken = default) => + PostAsync($"/containers/{Uri.EscapeDataString(id)}/kill" + Query(("signal", signal)), null, cancellationToken); + + /// 删除容器。 + /// 容器 id 或名字。 + /// 运行中也删(等价于先 kill)。 + /// 连同匿名卷一起删 —— 这一项会丢数据。 + /// 取消令牌。 + public Task RemoveContainerAsync(string id, bool force = false, bool removeVolumes = false, + CancellationToken cancellationToken = default) => + DeleteAsync($"/containers/{Uri.EscapeDataString(id)}" + + Query(("force", force ? "1" : "0"), ("v", removeVolumes ? "1" : "0")), cancellationToken); + + /// 重命名容器(不重启)。 + public Task RenameContainerAsync(string id, string newName, CancellationToken cancellationToken = default) => + PostAsync($"/containers/{Uri.EscapeDataString(id)}/rename" + Query(("name", newName)), null, cancellationToken); + + /// 改重启策略(立即生效,不重启容器)。 + public Task UpdateRestartPolicyAsync(string id, string policy, int maximumRetryCount = 0, + CancellationToken cancellationToken = default) => + PostAsync($"/containers/{Uri.EscapeDataString(id)}/update", + new { RestartPolicy = new { Name = policy, MaximumRetryCount = maximumRetryCount } }, cancellationToken); + + /// 容器内进程表(docker top)。 + public Task TopAsync(string id, string psArgs = "-ef", CancellationToken cancellationToken = default) => + GetJsonAsync($"/containers/{Uri.EscapeDataString(id)}/top" + Query(("ps_args", psArgs)), cancellationToken); + + /// 可写层相对镜像的变更(docker diff)。 + public async Task ChangesAsync(string id, CancellationToken cancellationToken = default) + { + // 没有任何变更时 daemon 返回 JSON null,不是空数组。 + var body = await GetStringAsync($"/containers/{Uri.EscapeDataString(id)}/changes", cancellationToken).ConfigureAwait(false); + return DockerJson.TryDeserialize(body) ?? []; + } + + /// 创建容器。 + /// 容器名;留空由 daemon 生成。 + /// 创建配置(见 )。 + /// 取消令牌。 + public Task CreateContainerAsync(string? name, ContainerCreateRequest config, + CancellationToken cancellationToken = default) => + PostJsonAsync( + "/containers/create" + (string.IsNullOrWhiteSpace(name) ? "" : Query(("name", name))), + config, cancellationToken); + + /// + /// 把容器当前的可写层固化成一个镜像(docker commit)。 + /// + /// 容器 id。 + /// 仓库名;留空则产生一个只有 id 的悬空镜像。 + /// 标签。 + /// 提交说明,会进镜像的 Comment。 + /// 作者。 + /// + /// 提交期间是否暂停容器。默认暂停 —— 与 docker CLI 一致: + /// 一个正在写文件的进程被拍下快照,得到的可能是一个写到一半的数据库。 + /// + /// 取消令牌。 + public Task CommitContainerAsync(string id, string? repository, string? tag, + string? comment, string? author, bool pause = true, CancellationToken cancellationToken = default) => + PostJsonAsync("/commit" + Query( + ("container", id), + ("repo", string.IsNullOrWhiteSpace(repository) ? null : repository), + ("tag", string.IsNullOrWhiteSpace(tag) ? null : tag), + ("comment", string.IsNullOrWhiteSpace(comment) ? null : comment), + ("author", string.IsNullOrWhiteSpace(author) ? null : author), + ("pause", pause ? "1" : "0")), null, cancellationToken); + + /// + /// 等容器结束并拿退出码。用于"跑一次性容器"的场景。 + /// + public Task WaitContainerAsync(string id, CancellationToken cancellationToken = default) => + PostJsonAsync($"/containers/{Uri.EscapeDataString(id)}/wait", null, cancellationToken); +} + +/// 等待容器结束的响应。 +public sealed record WaitResponse +{ + /// 退出码。 + public int StatusCode { get; init; } +} + +/// +/// POST /containers/create 的请求体。 +/// +/// 字段名必须与 Docker 的 schema 一致(PascalCase),所以这里不套记录的惯例改名。 +/// +/// +public sealed record ContainerCreateRequest +{ + /// 镜像引用。 + public required string Image { get; init; } + + /// 主机名。 + public string? Hostname { get; init; } + + /// 覆盖入口命令。 + public string[]? Cmd { get; init; } + + /// 覆盖 entrypoint。 + public string[]? Entrypoint { get; init; } + + /// 环境变量,KEY=VALUE + public string[]? Env { get; init; } + + /// 工作目录。 + public string? WorkingDir { get; init; } + + /// 用户。 + public string? User { get; init; } + + /// 标签。 + public Dictionary? Labels { get; init; } + + /// 要暴露的容器端口,键形如 80/tcp,值是空对象。 + public Dictionary? ExposedPorts { get; init; } + + /// 是否分配 TTY。 + public bool Tty { get; init; } + + /// 是否保持 stdin 打开。 + public bool OpenStdin { get; init; } + + /// 宿主配置。 + public HostConfigRequest? HostConfig { get; init; } + + /// 网络配置。 + public NetworkingConfigRequest? NetworkingConfig { get; init; } +} + +/// 创建容器时的宿主配置。 +public sealed record HostConfigRequest +{ + /// 端口映射:键 80/tcp,值是一组宿主绑定。 + public Dictionary? PortBindings { get; init; } + + /// 挂载,源:目标[:ro] 形态。 + public string[]? Binds { get; init; } + + /// 重启策略。 + public RestartPolicyRequest? RestartPolicy { get; init; } + + /// 退出即删除。 + public bool AutoRemove { get; init; } + + /// 特权模式。 + public bool Privileged { get; init; } + + /// 网络模式(接入单个网络时用它)。 + public string? NetworkMode { get; init; } + + /// 额外的 Linux capability。 + public string[]? CapAdd { get; init; } + + /// 内存上限(字节)。 + public long? Memory { get; init; } +} + +/// 一条宿主端口绑定。 +public sealed record PortBindingRequest +{ + /// 宿主监听地址;留空表示所有地址。 + public string? HostIp { get; init; } + + /// 宿主端口(字符串,Docker 的 schema 就是字符串)。 + public string? HostPort { get; init; } +} + +/// 重启策略(请求侧)。 +public sealed record RestartPolicyRequest +{ + /// 策略名。 + public string? Name { get; init; } + + /// on-failure 的最大重试次数。 + public int MaximumRetryCount { get; init; } +} + +/// 创建容器时的网络配置。 +public sealed record NetworkingConfigRequest +{ + /// 按网络名索引的接入设置。 + public Dictionary? EndpointsConfig { get; init; } +} + +/// 接入某网络的设置。 +public sealed record EndpointConfigRequest +{ + /// 网络别名。 + public string[]? Aliases { get; init; } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Exec.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Exec.cs new file mode 100644 index 0000000..5c61ead --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Exec.cs @@ -0,0 +1,180 @@ +using System.Text; +using System.Text.Json; + +namespace VelaShell.Plugin.DockerPanel.Docker; + +/// 一次 exec 的捕获结果。 +/// 标准输出。 +/// 标准错误(**单独一条流**,不并进标准输出)。 +/// 退出码。 +public readonly record struct ExecCapture(string StandardOutput, string StandardError, int ExitCode) +{ + /// 退出码为 0。 + public bool IsSuccess => ExitCode == 0; + + /// 给人看的一行失败原因。 + public string FailureText + { + get + { + foreach (var candidate in new[] { StandardError, StandardOutput }) + { + foreach (var line in candidate.Split('\n')) + { + var trimmed = line.Trim(); + if (trimmed.Length > 0) + { + return trimmed; + } + } + } + return $"exit {ExitCode}"; + } + } +} + +/// 一条已经接上的交互式 exec 会话。 +public sealed class DockerExecSession(string execId, Stream stream, DockerClient client, bool tty) : IAsyncDisposable +{ + /// exec 实例 id(调整窗口大小、取退出码用)。 + public string ExecId { get; } = execId; + + /// 容器是否分配了 TTY。 + public bool Tty { get; } = tty; + + /// 把键盘输入写进容器的 stdin。 + public async Task WriteAsync(ReadOnlyMemory data, CancellationToken cancellationToken = default) + { + await stream.WriteAsync(data, cancellationToken).ConfigureAwait(false); + await stream.FlushAsync(cancellationToken).ConfigureAwait(false); + } + + /// 持续读出容器的输出,直到会话结束或取消。 + public Task ReadAsync(Action onLine, CancellationToken cancellationToken) + { + var decoder = new DockerFrameDecoder(Tty, timestamps: false); + return decoder.ReadAsync(stream, onLine, cancellationToken); + } + + /// 把原始字节交给调用方(终端控件要的是字节,不是行)。 + public ValueTask ReadRawAsync(Memory buffer, CancellationToken cancellationToken = default) => + stream.ReadAsync(buffer, cancellationToken); + + /// + /// 底层的双工流,交给终端视图去泵。 + /// + /// 只在 为真时能这么用。 分配了 TTY 的 exec,daemon 两个方向 + /// 走的都是裸字节;没有 TTY 时输出是 8 字节头的多路复用帧,直接喂给终端会把 + /// [type][0][0][0][len] 这五个字节当成正文画出来。后一种情形请走 + /// ,它有解帧器。 + /// + /// + public Stream Stream => stream; + + /// 调整伪终端的行列。 + public Task ResizeAsync(int rows, int columns, CancellationToken cancellationToken = default) => + client.ResizeExecAsync(ExecId, rows, columns, cancellationToken); + + /// 取退出码(会话结束后才有意义)。 + public Task InspectAsync(CancellationToken cancellationToken = default) => + client.InspectExecAsync(ExecId, cancellationToken); + + /// + public ValueTask DisposeAsync() => stream.DisposeAsync(); +} + +public sealed partial class DockerClient +{ + /// + /// 在容器里开一条**交互式** exec 会话。返回的会话握着一条独立的隧道, + /// 调用方负责释放它 —— 释放即结束远端进程。 + /// + /// 容器 id 或名字。 + /// 要执行的命令(argv 形态,不经 shell 解析)。 + /// 是否分配伪终端。 + /// 以哪个用户执行;留空用镜像默认。 + /// 工作目录;留空用镜像默认。 + /// 取消令牌。 + public async Task StartExecAsync(string containerId, string[] command, bool tty, + string? user = null, string? workingDir = null, CancellationToken cancellationToken = default) + { + var created = await CreateExecAsync(containerId, command, tty, attachStdin: true, user, workingDir, + cancellationToken).ConfigureAwait(false); + // 劫持端点要独占一条流:HttpClient 的连接池不能用,它没法在同一条连接上 + // 边写 stdin 边读 stdout。 + var stream = await Transport.ConnectAsync(cancellationToken).ConfigureAwait(false); + try + { + var body = JsonSerializer.Serialize(new { Detach = false, Tty = tty }, DockerJson.Options); + (var status, var reason, var hijacked) = await DockerRawHttp + .PostAsync(stream, $"/exec/{Uri.EscapeDataString(created.Id)}/start", body, upgrade: true, cancellationToken) + .ConfigureAwait(false); + return status is not (200 or 101) ? throw new DockerApiException((System.Net.HttpStatusCode)status, $"启动 exec 失败:HTTP {status} {reason}") : new(created.Id, hijacked, this, tty); + } + catch + { + await stream.DisposeAsync().ConfigureAwait(false); + throw; + } + } + + /// + /// 在容器里跑一条命令并把输出**完整收回来**。 + /// + /// 这是文件浏览(ls)、探测(which)这类"要结果不要交互"的基础件。 + /// 刻意用 Tty=false:只有这样 stdout 与 stderr 才是分开的两条流, + /// 而"命令失败了"和"命令没有输出"才区分得开。 + /// + /// + public async Task ExecCaptureAsync(string containerId, string[] command, + string? user = null, string? workingDir = null, CancellationToken cancellationToken = default) + { + var created = await CreateExecAsync(containerId, command, tty: false, attachStdin: false, + user, workingDir, cancellationToken).ConfigureAwait(false); + var stream = await Transport.ConnectAsync(cancellationToken).ConfigureAwait(false); + var stdout = new StringBuilder(); + var stderr = new StringBuilder(); + await using (stream.ConfigureAwait(false)) + { + var body = JsonSerializer.Serialize(new { Detach = false, Tty = false }, DockerJson.Options); + (var status, var reason, var hijacked) = await DockerRawHttp + .PostAsync(stream, $"/exec/{Uri.EscapeDataString(created.Id)}/start", body, upgrade: true, cancellationToken) + .ConfigureAwait(false); + if (status is not (200 or 101)) + { + throw new DockerApiException((System.Net.HttpStatusCode)status, $"启动 exec 失败:HTTP {status} {reason}"); + } + var decoder = new DockerFrameDecoder(tty: false, timestamps: false); + await decoder.ReadAsync(hijacked, line => + { + var target = line.Kind == DockerStreamKind.StdErr ? stderr : stdout; + target.Append(line.Text).Append('\n'); + }, cancellationToken).ConfigureAwait(false); + } + var inspect = await InspectExecAsync(created.Id, cancellationToken).ConfigureAwait(false); + return new(stdout.ToString(), stderr.ToString(), inspect.ExitCode); + } + + /// 创建一个 exec 实例(还没开始跑)。 + private Task CreateExecAsync(string containerId, string[] command, bool tty, bool attachStdin, + string? user, string? workingDir, CancellationToken cancellationToken) => + PostJsonAsync($"/containers/{Uri.EscapeDataString(containerId)}/exec", new + { + AttachStdin = attachStdin, + AttachStdout = true, + AttachStderr = true, + Tty = tty, + Cmd = command, + User = string.IsNullOrWhiteSpace(user) ? null : user, + WorkingDir = string.IsNullOrWhiteSpace(workingDir) ? null : workingDir + }, cancellationToken); + + /// 调整 exec 的伪终端行列。 + internal Task ResizeExecAsync(string execId, int rows, int columns, CancellationToken cancellationToken = default) => + PostAsync($"/exec/{Uri.EscapeDataString(execId)}/resize" + + Query(("h", rows.ToString()), ("w", columns.ToString())), null, cancellationToken); + + /// 查 exec 的状态与退出码。 + internal Task InspectExecAsync(string execId, CancellationToken cancellationToken = default) => + GetJsonAsync($"/exec/{Uri.EscapeDataString(execId)}/json", cancellationToken); +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Files.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Files.cs new file mode 100644 index 0000000..9289869 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Files.cs @@ -0,0 +1,251 @@ +using System.Net.Http.Headers; + +namespace VelaShell.Plugin.DockerPanel.Docker; + +/// 容器内的一个文件系统条目。 +/// 名字(不含路径)。 +/// 绝对路径。 +/// 是否目录。 +/// 是否符号链接。 +/// 字节数;目录为 0。 +/// 权限串,如 -rw-r--r--。 +/// 属主与属组,如 root root。 +/// 修改时间的原始文本。 +/// 符号链接的目标。 +public readonly record struct ContainerFileEntry( + string Name, + string FullPath, + bool IsDirectory, + bool IsSymlink, + long Size, + string Mode, + string Owner, + string Modified, + string? LinkTarget); + +public sealed partial class DockerClient +{ + /// 面板允许在线编辑的单文件上限。超过它的文件只给下载,不进编辑器。 + public const long MaxEditableFileBytes = 2 * 1024 * 1024; + + /// + /// 单文件上传上限。 + /// + /// 比编辑上限宽得多(配置文件之外还有证书、静态资源),但仍然有限: + /// tar 打包是在内存里做的,而这条隧道跟事件流、日志流共用同一批 SSH 通道 —— + /// 一个几百兆的上传会把它们一起饿死。真要搬大文件,scp 是对的工具。 + /// + /// + public const long MaxUploadFileBytes = 64 * 1024 * 1024; + + /// + /// 列出容器内某个目录。 + /// + /// Engine API **没有**列目录这个端点 —— /archive 只能整包取走, + /// 拿一个 /var/log 下来可能是几个 G。所以这里走 exec 跑一条 ls, + /// 只传目录条目本身,不传文件内容。 + /// + /// + public async Task ListDirectoryAsync(string containerId, string path, + CancellationToken cancellationToken = default) + { + var target = string.IsNullOrWhiteSpace(path) ? "/" : path; + // -A 排掉 . 与 ..,--time-style=long-iso 把时间列压成固定的两段, + // 省得为不同 locale 的 ls 输出各写一套解析。 + var result = await ExecCaptureAsync(containerId, + ["/bin/sh", "-c", $"ls -lAL --time-style=long-iso -- {ShellQuote(target)} 2>/dev/null || ls -lA -- {ShellQuote(target)}"], + cancellationToken: cancellationToken).ConfigureAwait(false); + if (!result.IsSuccess && result.StandardOutput.Length == 0) + { + throw new DockerApiException(System.Net.HttpStatusCode.NotFound, + result.StandardError.Length > 0 ? result.FailureText : $"列不出目录 {target}。"); + } + List entries = []; + foreach (var raw in result.StandardOutput.Split('\n')) + { + if (ParseLsLine(raw, target) is { } entry) + { + entries.Add(entry); + } + } + return [.. entries.OrderByDescending(e => e.IsDirectory).ThenBy(e => e.Name, StringComparer.OrdinalIgnoreCase)]; + } + + /// + /// 解一行 ls -lA。解不动就返回 null —— 总量行("total 12")与空行都走这条路。 + /// + internal static ContainerFileEntry? ParseLsLine(string line, string directory) + { + var trimmed = line.TrimEnd('\r'); + if (trimmed.Length < 10 || trimmed.StartsWith("total ", StringComparison.Ordinal)) + { + return null; + } + var mode = trimmed[..10]; + if (mode[0] is not ('-' or 'd' or 'l' or 'c' or 'b' or 'p' or 's')) + { + return null; + } + // 权限 链接数 属主 属组 大小 日期 时间 名字… + var parts = trimmed.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length < 8) + { + return null; + } + var owner = $"{parts[2]} {parts[3]}"; + _ = long.TryParse(parts[4], out var size); + var modified = $"{parts[5]} {parts[6]}"; + // 名字里可能有空格,所以按"前七段之后的全部"取,而不是取第八段。 + var nameStart = IndexOfNthToken(trimmed, 7); + var name = nameStart >= 0 ? trimmed[nameStart..] : parts[^1]; + string? linkTarget = null; + if (mode[0] == 'l') + { + var arrow = name.IndexOf(" -> ", StringComparison.Ordinal); + if (arrow > 0) + { + linkTarget = name[(arrow + 4)..]; + name = name[..arrow]; + } + } + if (name is "." or "..") + { + return null; + } + var full = directory.TrimEnd('/') + "/" + name; + return new(name, full, mode[0] == 'd', mode[0] == 'l', size, mode, owner, modified, linkTarget); + } + + /// 找出第 n 个空白分隔字段的起始下标(n 从 0 起)。 + private static int IndexOfNthToken(string text, int n) + { + int index = 0, token = 0; + while (index < text.Length) + { + while (index < text.Length && text[index] == ' ') + { + index++; + } + if (index >= text.Length) + { + return -1; + } + if (token == n) + { + return index; + } + while (index < text.Length && text[index] != ' ') + { + index++; + } + token++; + } + return -1; + } + + /// POSIX 单引号引用。列目录是这一层唯一一处把用户输入拼进 shell 的地方。 + internal static string ShellQuote(string value) => Sh.Quote(value); + + /// + /// 读容器里的一个文件。走 GET /archive 拿一段 tar 再取出唯一那个文件 —— + /// 不经过 shell,因此不受登录 shell、引用规则与 locale 的影响。 + /// + public async Task ReadFileAsync(string containerId, string path, long maxBytes = MaxEditableFileBytes, + CancellationToken cancellationToken = default) + { + var url = $"/containers/{Uri.EscapeDataString(containerId)}/archive" + Query(("path", path)); + using var response = await OpenStreamAsync(HttpMethod.Get, url, cancellationToken).ConfigureAwait(false); + await using var body = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + var entry = await TarUtil.ReadFirstFileAsync(body, maxBytes, cancellationToken) + .ConfigureAwait(false); + return entry?.Content + ?? throw new DockerApiException(System.Net.HttpStatusCode.NotFound, $"{path} 不是一个普通文件。"); + } + + /// + /// 把内容写回容器里的一个文件。 + /// + /// 整个文件以 tar 流 PUT 回容器的可写层。这是会丢数据的操作 —— + /// 原文件被整体覆盖,Docker 不做备份 —— 界面必须走"手打确认串"那一档闸门。 + /// + /// + public async Task WriteFileAsync(string containerId, string path, ReadOnlyMemory content, + CancellationToken cancellationToken = default) + { + var directory = GetDirectory(path); + var name = path[(path.LastIndexOf('/') + 1)..]; + var tar = TarUtil.CreateSingleFile(name, content.Span); + var url = $"/containers/{Uri.EscapeDataString(containerId)}/archive" + + Query(("path", directory), ("noOverwriteDirNonDir", "1")); + using var request = new HttpRequestMessage(HttpMethod.Put, url) + { + Content = new ByteArrayContent(tar) + }; + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-tar"); + using var response = await SendRawAsync(request, cancellationToken).ConfigureAwait(false); + await EnsureSuccessAsync(response, url, cancellationToken).ConfigureAwait(false); + } + + /// 把容器里的一个路径整包取下来(目录也行),交给调用方落盘。 + public async Task DownloadArchiveAsync(string containerId, string path, CancellationToken cancellationToken = default) + { + var url = $"/containers/{Uri.EscapeDataString(containerId)}/archive" + Query(("path", path)); + var response = await OpenStreamAsync(HttpMethod.Get, url, cancellationToken).ConfigureAwait(false); + return new ResponseOwningStream(response, await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false)); + } + + private static string GetDirectory(string path) + { + var slash = path.LastIndexOf('/'); + return slash <= 0 ? "/" : path[..slash]; + } + + private async Task SendRawAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + ObjectDisposedException.ThrowIf(_disposed, this); + try + { + return await _http.SendAsync(request, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + throw Unreachable(ex); + } + } + + /// 的生命周期钉在它的响应体流上。 + private sealed class ResponseOwningStream(HttpResponseMessage response, Stream inner) : Stream + { + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => throw new NotSupportedException(); + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override void Flush() => inner.Flush(); + public override int Read(byte[] buffer, int offset, int count) => inner.Read(buffer, offset, count); + public override int Read(Span buffer) => inner.Read(buffer); + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + => inner.ReadAsync(buffer, cancellationToken); + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + protected override void Dispose(bool disposing) + { + if (disposing) + { + inner.Dispose(); + response.Dispose(); + } + base.Dispose(disposing); + } + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Images.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Images.cs new file mode 100644 index 0000000..0548d4c --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Images.cs @@ -0,0 +1,190 @@ +using System.Text.Json; + +namespace VelaShell.Plugin.DockerPanel.Docker; + +public sealed partial class DockerClient +{ + /// 列镜像。 + /// 是否含中间层镜像。 + /// 取消令牌。 + public Task ListImagesAsync(bool all = false, CancellationToken cancellationToken = default) => + GetJsonAsync("/images/json" + Query(("all", all ? "1" : "0")), cancellationToken); + + /// 镜像 inspect 的原始 JSON。 + public async Task InspectImageRawAsync(string id, CancellationToken cancellationToken = default) => + DockerJson.Prettify(await GetStringAsync($"/images/{Uri.EscapeDataString(id)}/json", cancellationToken) + .ConfigureAwait(false)); + + /// 镜像详情(docker image inspect)。 + public Task InspectImageAsync(string id, CancellationToken cancellationToken = default) => + GetJsonAsync($"/images/{Uri.EscapeDataString(id)}/json", cancellationToken); + + /// 镜像的构建历史。 + public Task ImageHistoryAsync(string id, CancellationToken cancellationToken = default) => + GetJsonAsync($"/images/{Uri.EscapeDataString(id)}/history", cancellationToken); + + /// 给镜像打一个新标签(不复制镜像,只加一个引用)。 + public Task TagImageAsync(string id, string repository, string? tag, CancellationToken cancellationToken = default) => + PostAsync($"/images/{Uri.EscapeDataString(id)}/tag" + Query(("repo", repository), ("tag", tag)), null, cancellationToken); + + /// 删除镜像。 + /// 镜像 id 或 repo:tag。 + /// 有容器在用也删。 + /// 取消令牌。 + public Task RemoveImageAsync(string id, bool force = false, CancellationToken cancellationToken = default) => + DeleteJsonAsync($"/images/{Uri.EscapeDataString(id)}" + Query(("force", force ? "1" : "0")), cancellationToken); + + /// + /// 拉取镜像,逐帧回调进度。 + /// + /// daemon 用 NDJSON 边拉边推,每层一条("Downloading" 带字节数、"Already exists"、 + /// "Pull complete"),最后一条要么是摘要要么是 error。 + /// HTTP 200 不代表拉取成功 —— 失败也是 200,错误写在流末尾的那一帧里。 + /// + /// + /// 镜像引用,不含标签。 + /// 标签; 为真时忽略。 + /// 平台,如 linux/amd64;留空用 daemon 默认。 + /// 拉取全部标签。 + /// X-Registry-Auth 头的值(base64 JSON);公开仓库传 null。 + /// 进度接收器。 + /// 取消令牌。 + /// 流末尾报了错误。 + public async Task PullImageAsync(string fromImage, string? tag, string? platform, bool allTags, + string? registryAuth, IProgress progress, CancellationToken cancellationToken = default) + { + var path = "/images/create" + Query( + ("fromImage", fromImage), + ("tag", allTags ? null : string.IsNullOrWhiteSpace(tag) ? "latest" : tag), + ("platform", string.IsNullOrWhiteSpace(platform) ? null : platform)); + await StreamNdjsonAsync(HttpMethod.Post, path, registryAuth, progress, cancellationToken).ConfigureAwait(false); + } + + /// 推送镜像,逐帧回调进度。语义与拉取同构(200 也可能是失败)。 + public async Task PushImageAsync(string name, string? tag, string? registryAuth, + IProgress progress, CancellationToken cancellationToken = default) + { + var path = $"/images/{Uri.EscapeDataString(name)}/push" + Query(("tag", tag)); + await StreamNdjsonAsync(HttpMethod.Post, path, registryAuth, progress, cancellationToken).ConfigureAwait(false); + } + + /// 清理镜像。 + /// + /// 只清悬空镜像(dangling=true);为假时清理**全部未被容器使用的镜像**, + /// 那会连带删掉有标签但暂时没人用的镜像,重新拉要花时间与带宽。 + /// + /// 取消令牌。 + public Task PruneImagesAsync(bool danglingOnly, CancellationToken cancellationToken = default) => + PostJsonAsync("/images/prune" + Query(("filters", Filters(("dangling", danglingOnly ? "true" : "false")))), + null, cancellationToken); + + /// + /// 把一个或多个镜像导出成 tar 流(docker save)。 + /// + /// 交出来的是一条还开着的流,调用方负责边读边写盘并释放它 —— + /// 镜像动辄上 GB,先读进内存再落盘就是把宿主换成一台会 OOM 的机器。 + /// + /// + /// 镜像引用,可以是 repo:tag 也可以是 id。 + /// 取消令牌。 + public async Task SaveImagesAsync(IReadOnlyList names, CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_disposed, this); + var path = "/images/get" + Query([.. names.Select(n => ("names", (string?)n))]); + HttpResponseMessage response; + try + { + response = await _http.GetAsync(path, HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .ConfigureAwait(false); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + throw Unreachable(ex); + } + await EnsureSuccessAsync(response, path, cancellationToken).ConfigureAwait(false); + return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + } + + /// + /// 从一条 tar 流导入镜像(docker load),逐帧回调进度。 + /// + /// + /// 请求体直接挂在调用方给的流上,不缓冲 —— 与导出同一个理由。 + /// 与拉取同构:HTTP 200 不等于成功,错误在流末尾那一帧里。 + /// + public async Task LoadImagesAsync(Stream tar, IProgress progress, + CancellationToken cancellationToken = default) + { + using var content = new StreamContent(tar); + content.Headers.ContentType = new("application/x-tar"); + await StreamNdjsonAsync(HttpMethod.Post, "/images/load" + Query(("quiet", "0")), null, progress, + cancellationToken, content).ConfigureAwait(false); + } + + /// + /// 读一条 NDJSON 进度流,逐帧回调,并把流末尾的 error 翻成异常。 + /// + private async Task StreamNdjsonAsync(HttpMethod method, string path, string? registryAuth, + IProgress progress, CancellationToken cancellationToken, HttpContent? content = null) + { + ObjectDisposedException.ThrowIf(_disposed, this); + using var request = new HttpRequestMessage(method, path); + if (content is not null) + { + request.Content = content; + } + if (!string.IsNullOrEmpty(registryAuth)) + { + request.Headers.TryAddWithoutValidation("X-Registry-Auth", registryAuth); + } + HttpResponseMessage response; + try + { + response = await _http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .ConfigureAwait(false); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + throw Unreachable(ex); + } + using (response) + { + await EnsureSuccessAsync(response, path, cancellationToken).ConfigureAwait(false); + await using var body = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + using var reader = new StreamReader(body); + string? failure = null; + while (await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false) is { } line) + { + if (line.Length == 0) + { + continue; + } + PullProgressFrame? frame; + try + { + frame = JsonSerializer.Deserialize(line, DockerJson.Options); + } + catch (JsonException) + { + // 进度流里混进过非 JSON 的一行不该让整次拉取失败。 + continue; + } + if (frame is null) + { + continue; + } + if (!string.IsNullOrEmpty(frame.Error)) + { + failure = frame.Error; + } + progress.Report(frame); + } + if (failure is not null) + { + // 拉取失败时 HTTP 仍然是 200 —— 错误只在流末尾那一帧里。 + // 不在这里翻成异常的话,界面会显示"拉取完成"然后列表里什么都没多出来。 + throw new DockerApiException(System.Net.HttpStatusCode.OK, failure, path); + } + } + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Streams.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Streams.cs new file mode 100644 index 0000000..3214e91 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Streams.cs @@ -0,0 +1,116 @@ +using System.Text.Json; + +namespace VelaShell.Plugin.DockerPanel.Docker; + +public sealed partial class DockerClient +{ + /// + /// 接上 GET /events,逐条回调。 + /// + /// 面板的刷新是**事件驱动**的:别处起了个容器、CI 推了个镜像、某个容器 OOM 死掉, + /// 界面在一秒内自己就更新了。所以自动刷新默认是关的 —— 它只在这条流断掉时兜底。 + /// + /// + /// 从这个时刻起补发历史事件;为 null 只收新的。 + /// 逐条回调(在读流的线程上,应快速返回)。 + /// 取消令牌(**必须**持有并在不再需要时触发)。 + public async Task StreamEventsAsync(DateTimeOffset? since, Action onEvent, + CancellationToken cancellationToken) + { + var path = "/events" + Query(("since", since?.ToUnixTimeSeconds().ToString())); + using var response = await OpenStreamAsync(HttpMethod.Get, path, cancellationToken).ConfigureAwait(false); + await using var body = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + using var reader = new StreamReader(body); + while (await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false) is { } line) + { + if (line.Length == 0) + { + continue; + } + DockerEvent? evt; + try + { + evt = JsonSerializer.Deserialize(line, DockerJson.Options); + } + catch (JsonException) + { + continue; + } + if (evt is not null) + { + onEvent(evt); + } + } + } + + /// + /// 容器日志。 为真时是一条真正的长连接 + /// (等价于 docker logs -f),新行到达即回调。 + /// + /// 容器 id 或名字。 + /// 容器是否分配了 TTY —— 决定要不要解 8 字节帧头,取自 inspect。 + /// 是否跟随。 + /// 先补多少行历史;all 表示全部。 + /// 是否带时间戳。 + /// 只要这个时刻之后的日志。 + /// 逐行回调。 + /// 取消令牌。 + public async Task StreamLogsAsync(string id, bool tty, bool follow, string tail, bool timestamps, + DateTimeOffset? since, Action onLine, CancellationToken cancellationToken) + { + var path = $"/containers/{Uri.EscapeDataString(id)}/logs" + Query( + ("stdout", "1"), + ("stderr", "1"), + ("follow", follow ? "1" : "0"), + ("tail", string.IsNullOrWhiteSpace(tail) ? "500" : tail), + ("timestamps", timestamps ? "1" : "0"), + ("since", since?.ToUnixTimeSeconds().ToString())); + using var response = await OpenStreamAsync(HttpMethod.Get, path, cancellationToken).ConfigureAwait(false); + await using var body = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + var decoder = new DockerFrameDecoder(tty, timestamps); + await decoder.ReadAsync(body, onLine, cancellationToken).ConfigureAwait(false); + } + + /// + /// 容器实时统计(stream=1)。daemon 每秒推一帧, + /// 第一帧的 precpu 是空的,所以 CPU 百分比从第二帧起才有意义。 + /// + public async Task StreamStatsAsync(string id, Action onSample, CancellationToken cancellationToken) + { + var path = $"/containers/{Uri.EscapeDataString(id)}/stats" + Query(("stream", "1"), ("one-shot", "0")); + using var response = await OpenStreamAsync(HttpMethod.Get, path, cancellationToken).ConfigureAwait(false); + await using var body = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + using var reader = new StreamReader(body); + while (await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false) is { } line) + { + if (line.Length == 0) + { + continue; + } + ContainerStats? sample; + try + { + sample = JsonSerializer.Deserialize(line, DockerJson.Options); + } + catch (JsonException) + { + continue; + } + if (sample is not null) + { + onSample(sample); + } + } + } + + /// + /// 取一次性的统计快照(stream=0)。列表页用它给每一行补 CPU/内存, + /// 不为每个容器长期占一条连接。 + /// + public async Task StatsSnapshotAsync(string id, CancellationToken cancellationToken = default) + { + var path = $"/containers/{Uri.EscapeDataString(id)}/stats" + Query(("stream", "0"), ("one-shot", "0")); + var body = await GetStringAsync(path, cancellationToken).ConfigureAwait(false); + return DockerJson.TryDeserialize(body); + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.System.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.System.cs new file mode 100644 index 0000000..0dcfa66 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.System.cs @@ -0,0 +1,38 @@ +namespace VelaShell.Plugin.DockerPanel.Docker; + +public sealed partial class DockerClient +{ + /// daemon 版本。 + public Task VersionAsync(CancellationToken cancellationToken = default) => + GetJsonAsync("/version", cancellationToken); + + /// daemon 概况。 + public Task InfoAsync(CancellationToken cancellationToken = default) => + GetJsonAsync("/info", cancellationToken); + + /// /version 的原始 JSON。 + public async Task VersionRawAsync(CancellationToken cancellationToken = default) => + DockerJson.Prettify(await GetStringAsync("/version", cancellationToken).ConfigureAwait(false)); + + /// /info 的原始 JSON。 + public async Task InfoRawAsync(CancellationToken cancellationToken = default) => + DockerJson.Prettify(await GetStringAsync("/info", cancellationToken).ConfigureAwait(false)); + + /// + /// 磁盘用量(docker system df -v)。 + /// + /// 这条请求在镜像多的机器上要几秒 —— daemon 得把每个卷都 du 一遍。 + /// 界面因此不把它挂在自动刷新上,只在进"系统"页与手动重算时发。 + /// + /// + public Task DiskUsageAsync(CancellationToken cancellationToken = default) => + GetJsonAsync("/system/df", cancellationToken); + + /// 清理已停止的容器。 + public Task PruneContainersAsync(CancellationToken cancellationToken = default) => + PostJsonAsync("/containers/prune", null, cancellationToken); + + /// 清理构建缓存。 + public Task PruneBuildCacheAsync(bool all = false, CancellationToken cancellationToken = default) => + PostJsonAsync("/build/prune" + Query(("all", all ? "1" : "0")), null, cancellationToken); +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.VolumesNetworks.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.VolumesNetworks.cs new file mode 100644 index 0000000..11efb71 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.VolumesNetworks.cs @@ -0,0 +1,111 @@ +namespace VelaShell.Plugin.DockerPanel.Docker; + +public sealed partial class DockerClient +{ + // ─────────────────────────── 卷 ─────────────────────────── + + /// 列卷。 + public async Task ListVolumesAsync(CancellationToken cancellationToken = default) + { + var response = await GetJsonAsync("/volumes", cancellationToken).ConfigureAwait(false); + return response.Volumes ?? []; + } + + /// 卷 inspect 的原始 JSON。 + public async Task InspectVolumeRawAsync(string name, CancellationToken cancellationToken = default) => + DockerJson.Prettify(await GetStringAsync($"/volumes/{Uri.EscapeDataString(name)}", cancellationToken) + .ConfigureAwait(false)); + + /// 新建卷。 + /// 卷名(创建后不可改)。 + /// 驱动;留空用 local。 + /// 驱动选项(NFS 之类)。 + /// 标签。 + /// 取消令牌。 + public Task CreateVolumeAsync(string name, string? driver, + Dictionary? driverOpts, Dictionary? labels, + CancellationToken cancellationToken = default) => + PostJsonAsync("/volumes/create", new + { + Name = name, + Driver = string.IsNullOrWhiteSpace(driver) ? "local" : driver, + DriverOpts = driverOpts, + Labels = labels + }, cancellationToken); + + /// + /// 删除卷。会永久丢失卷里的数据,界面上必须走"手打确认串"那一档闸门。 + /// + public Task RemoveVolumeAsync(string name, bool force = false, CancellationToken cancellationToken = default) => + DeleteAsync($"/volumes/{Uri.EscapeDataString(name)}" + Query(("force", force ? "1" : "0")), cancellationToken); + + /// 清理未被任何容器使用的卷。同样会丢数据。 + public Task PruneVolumesAsync(CancellationToken cancellationToken = default) => + PostJsonAsync("/volumes/prune", null, cancellationToken); + + // ─────────────────────────── 网络 ─────────────────────────── + + /// 列网络。 + public Task ListNetworksAsync(CancellationToken cancellationToken = default) => + GetJsonAsync("/networks", cancellationToken); + + /// 网络 inspect(结构化,含已接入容器)。 + public Task InspectNetworkAsync(string id, CancellationToken cancellationToken = default) => + GetJsonAsync($"/networks/{Uri.EscapeDataString(id)}", cancellationToken); + + /// 网络 inspect 的原始 JSON。 + public async Task InspectNetworkRawAsync(string id, CancellationToken cancellationToken = default) => + DockerJson.Prettify(await GetStringAsync($"/networks/{Uri.EscapeDataString(id)}", cancellationToken) + .ConfigureAwait(false)); + + /// 新建网络。 + public Task CreateNetworkAsync(string name, string driver, string? subnet, string? gateway, + bool @internal, bool attachable, bool enableIPv6, CancellationToken cancellationToken = default) + { + object? ipam = string.IsNullOrWhiteSpace(subnet) + ? null + : new { Config = new[] { new { Subnet = subnet, Gateway = string.IsNullOrWhiteSpace(gateway) ? null : gateway } } }; + return PostJsonAsync("/networks/create", new + { + Name = name, + Driver = driver, + Internal = @internal, + Attachable = attachable, + EnableIPv6 = enableIPv6, + IPAM = ipam + }, cancellationToken); + } + + /// 删除网络。内置的 bridge/host/none 删不掉,界面应提前置灰。 + public Task RemoveNetworkAsync(string id, CancellationToken cancellationToken = default) => + DeleteAsync($"/networks/{Uri.EscapeDataString(id)}", cancellationToken); + + /// 把容器接入网络(立即生效,不重启容器)。 + public Task ConnectNetworkAsync(string networkId, string container, string[]? aliases = null, + CancellationToken cancellationToken = default) => + PostAsync($"/networks/{Uri.EscapeDataString(networkId)}/connect", new + { + Container = container, + EndpointConfig = aliases is { Length: > 0 } ? new { Aliases = aliases } : null + }, cancellationToken); + + /// 把容器从网络上摘掉。 + public Task DisconnectNetworkAsync(string networkId, string container, bool force = false, + CancellationToken cancellationToken = default) => + PostAsync($"/networks/{Uri.EscapeDataString(networkId)}/disconnect", + new { Container = container, Force = force }, cancellationToken); + + /// 清理未使用的网络。 + public Task PruneNetworksAsync(CancellationToken cancellationToken = default) => + PostJsonAsync("/networks/prune", null, cancellationToken); +} + +/// 新建网络的响应。 +public sealed record CreateNetworkResponse +{ + /// 新网络 id。 + public string Id { get; init; } = ""; + + /// daemon 的警告。 + public string? Warning { get; init; } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.cs new file mode 100644 index 0000000..7f5efca --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.cs @@ -0,0 +1,315 @@ +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; + +namespace VelaShell.Plugin.DockerPanel.Docker; + +/// +/// Docker Engine API 客户端。一个实例对应一个端点(一台机器上的一个 daemon)。 +/// +/// 传输是可换的: 给出一条双工流, +/// 把它接进 —— +/// 于是"经 SSH 隧道连远端 socket"与"连本机命名管道"在这一层之上完全一样。 +/// +/// +/// 不带 API 版本前缀。 Docker 对未带版本的路径按 daemon 当前版本处理, +/// 而钉一个版本号意味着每次 Docker 升级都要跟着改一次,还会在旧 daemon 上直接 400。 +/// 面板改为把 daemon 报的 ApiVersion 显示在状态栏,让用户知道自己在跟谁说话。 +/// +/// +public sealed partial class DockerClient : IAsyncDisposable +{ + /// 一次性请求的默认时限。流式请求不受它约束。 + private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30); + + private readonly SocketsHttpHandler _handler; + private readonly HttpClient _http; + private bool _disposed; + + /// 按端点建一个客户端。 + public DockerClient(DockerEndpoint endpoint, IDockerTransport transport) + { + Endpoint = endpoint; + Transport = transport; + _handler = new SocketsHttpHandler + { + ConnectCallback = async (_, ct) => await transport.ConnectAsync(ct).ConfigureAwait(false), + // 连接池要留出余量:隧道配额是每插件 16 条,而事件流、每条跟随中的日志与统计 + // 各自长期占一条。池子开太大,真正要紧的那几条流就开不出来了。 + MaxConnectionsPerServer = 6, + PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2), + // 隧道自己会随 SSH 会话一起断,不需要再叠一层生存期轮转。 + PooledConnectionLifetime = Timeout.InfiniteTimeSpan, + AllowAutoRedirect = false, + UseCookies = false, + UseProxy = false + }; + _http = new HttpClient(_handler, disposeHandler: false) + { + // 主机名是给 Host 头凑数的:对面是一个 socket,不看它。 + BaseAddress = new Uri("http://docker/"), + // 总时限交给每次调用的取消令牌 —— HttpClient.Timeout 会把流式请求一并掐掉。 + Timeout = Timeout.InfiniteTimeSpan + }; + _http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + } + + /// 这个客户端连的是哪个端点。 + public DockerEndpoint Endpoint { get; } + + /// 底层传输(exec 之类要绕开 的地方直接用它)。 + public IDockerTransport Transport { get; } + + /// + public ValueTask DisposeAsync() + { + if (_disposed) + { + return ValueTask.CompletedTask; + } + _disposed = true; + _http.Dispose(); + _handler.Dispose(); + return ValueTask.CompletedTask; + } + + // ─────────────────────────── 请求原语 ─────────────────────────── + + /// 拼一条查询串;值为 null 的项跳过。 + internal static string Query(params (string Key, string? Value)[] items) + { + var sb = new StringBuilder(); + foreach ((var key, var value) in items) + { + if (value is null) + { + continue; + } + sb.Append(sb.Length == 0 ? '?' : '&') + .Append(Uri.EscapeDataString(key)) + .Append('=') + .Append(Uri.EscapeDataString(value)); + } + return sb.ToString(); + } + + /// 把一组 filters 编码成 Docker 要的那种 JSON 查询参数。 + internal static string? Filters(params (string Key, string Value)[] items) + { + if (items.Length == 0) + { + return null; + } + var map = new Dictionary(); + foreach (var group in items.GroupBy(i => i.Key)) + { + map[group.Key] = [.. group.Select(g => g.Value)]; + } + return JsonSerializer.Serialize(map, DockerJson.Options); + } + + /// GET 一段 JSON 并反序列化。 + internal async Task GetJsonAsync(string path, CancellationToken cancellationToken) + { + using var response = await SendAsync(HttpMethod.Get, path, null, cancellationToken).ConfigureAwait(false); + return await ReadJsonAsync(response, path, cancellationToken).ConfigureAwait(false); + } + + /// GET 一段原始文本(inspect 的完整 JSON 直接给界面看)。 + internal async Task GetStringAsync(string path, CancellationToken cancellationToken) + { + using var response = await SendAsync(HttpMethod.Get, path, null, cancellationToken).ConfigureAwait(false); + return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + } + + /// POST,不关心响应体。 + internal async Task PostAsync(string path, object? body = null, CancellationToken cancellationToken = default) + { + using var response = await SendAsync(HttpMethod.Post, path, JsonBody(body), cancellationToken).ConfigureAwait(false); + await EnsureSuccessAsync(response, path, cancellationToken).ConfigureAwait(false); + } + + /// POST 并反序列化响应。 + internal async Task PostJsonAsync(string path, object? body = null, CancellationToken cancellationToken = default) + { + using var response = await SendAsync(HttpMethod.Post, path, JsonBody(body), cancellationToken).ConfigureAwait(false); + return await ReadJsonAsync(response, path, cancellationToken).ConfigureAwait(false); + } + + /// DELETE,不关心响应体。 + internal async Task DeleteAsync(string path, CancellationToken cancellationToken = default) + { + using var response = await SendAsync(HttpMethod.Delete, path, null, cancellationToken).ConfigureAwait(false); + await EnsureSuccessAsync(response, path, cancellationToken).ConfigureAwait(false); + } + + /// DELETE 并反序列化响应。 + internal async Task DeleteJsonAsync(string path, CancellationToken cancellationToken = default) + { + using var response = await SendAsync(HttpMethod.Delete, path, null, cancellationToken).ConfigureAwait(false); + return await ReadJsonAsync(response, path, cancellationToken).ConfigureAwait(false); + } + + /// + /// 发一条**流式**请求:响应头一到就返回,响应体留着慢慢读。 + /// 调用方负责释放返回的 。 + /// + internal async Task OpenStreamAsync(HttpMethod method, string path, + CancellationToken cancellationToken) + { + using var request = new HttpRequestMessage(method, path); + HttpResponseMessage response; + try + { + response = await _http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + throw Unreachable(ex); + } + if (!response.IsSuccessStatusCode) + { + try + { + await EnsureSuccessAsync(response, path, cancellationToken).ConfigureAwait(false); + } + finally + { + response.Dispose(); + } + } + return response; + } + + private static JsonContent? JsonBody(object? body) => body is null ? null : JsonContent.Create(body, options: DockerJson.Options); + + private async Task SendAsync(HttpMethod method, string path, HttpContent? content, + CancellationToken cancellationToken) + { + ObjectDisposedException.ThrowIf(_disposed, this); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(DefaultTimeout); + using var request = new HttpRequestMessage(method, path) { Content = content }; + try + { + return await _http.SendAsync(request, HttpCompletionOption.ResponseContentRead, cts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + throw new DockerUnreachableException( + $"请求 {path} 超过 {DefaultTimeout.TotalSeconds:0} 秒没有响应。", DockerUnreachableReason.Unknown); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + throw Unreachable(ex); + } + } + + private static async Task ReadJsonAsync(HttpResponseMessage response, string path, CancellationToken cancellationToken) + { + await EnsureSuccessAsync(response, path, cancellationToken).ConfigureAwait(false); + var value = await response.Content.ReadFromJsonAsync(DockerJson.Options, cancellationToken).ConfigureAwait(false); + return value ?? throw new DockerApiException(response.StatusCode, "daemon 返回了空响应体。", path); + } + + /// + /// 把非 2xx 翻成带 daemon 原话的异常。 + /// + /// 状态码只给粗分类,真正能指导用户下一步的是响应体里的 message —— + /// "port is already allocated" 有可操作性,"HTTP 409" 没有。 + /// + /// + private static async Task EnsureSuccessAsync(HttpResponseMessage response, string path, CancellationToken cancellationToken) + { + if (response.IsSuccessStatusCode) + { + return; + } + var body = ""; + try + { + body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + // 连错误响应体都读不回来时,至少还有状态码可报。 + } + var message = DockerJson.TryDeserialize(body)?.Message + ?? (body.Length > 0 ? body.Trim() : $"HTTP {(int)response.StatusCode} {response.ReasonPhrase}"); + throw new DockerApiException(response.StatusCode, message, path); + } + + /// + /// 把"连不上"翻成一句能指导下一步的话。 + /// + /// 这里刻意按**出路**分类而不是按异常类型:socket 不存在要去装 Docker, + /// 权限不足要去加 docker 组 —— 给一个统一的"连接失败"等于把用户扔在原地。 + /// + /// + private DockerUnreachableException Unreachable(Exception ex) + { + var text = Flatten(ex); + // 权限先判:它是明说的,而下面那组"通道开不起来"是笼统的 —— + // 一条同时带着两者的消息,按"没权限"处理才对得上用户要做的事。 + var reason = + text.Contains("permission denied", StringComparison.OrdinalIgnoreCase) || + text.Contains("access is denied", StringComparison.OrdinalIgnoreCase) || + text.Contains("EACCES", StringComparison.OrdinalIgnoreCase) + ? DockerUnreachableReason.PermissionDenied + // sshd 开不起到 socket 的通道时只回一句笼统的失败,不区分"不存在"与"没权限"。 + // 归到"找不到 socket"这一档:它给的出路(去终端 ls 一下、换条路径)对两种情形都成立。 + // 注意 SDK 报的是 ConnectFailed —— 中间没有空格,只匹配 "connect failed" 会漏掉。 + : text.Contains("no such file", StringComparison.OrdinalIgnoreCase) || + text.Contains("does not exist", StringComparison.OrdinalIgnoreCase) || + text.Contains("connect failed", StringComparison.OrdinalIgnoreCase) || + text.Contains("connectfailed", StringComparison.OrdinalIgnoreCase) || + text.Contains("open failed", StringComparison.OrdinalIgnoreCase) || + text.Contains("ENOENT", StringComparison.OrdinalIgnoreCase) + ? DockerUnreachableReason.SocketMissing + : ex is NotSupportedException + ? DockerUnreachableReason.TunnelUnsupported + : text.Contains("session", StringComparison.OrdinalIgnoreCase) && + text.Contains("not found", StringComparison.OrdinalIgnoreCase) + ? DockerUnreachableReason.SessionUnavailable + : DockerUnreachableReason.Unknown; + var message = reason switch + { + DockerUnreachableReason.SocketMissing => + $"连不上 {Transport.Description}:打不开到它的通道。远端可能没装 Docker、daemon 没在跑,或者这条路径不对。", + DockerUnreachableReason.PermissionDenied => + $"没有 {Transport.Description} 的读写权限。把账号加进 docker 组,或改用一个有权限的账号。", + DockerUnreachableReason.TunnelUnsupported => + "当前宿主不支持远程隧道(插件被装载为隔离进程)。Docker 面板需要 hostMode = inProcess。", + DockerUnreachableReason.SessionUnavailable => + "这条 SSH 会话已经断开了。重新连上之后再打开面板。", + _ => $"连不上 {Transport.Description}:{text}" + }; + return new(message, reason, ex); + } + + private static string Flatten(Exception ex) + { + var sb = new StringBuilder(); + for (var current = ex; current is not null; current = current.InnerException) + { + if (sb.Length > 0) + { + sb.Append(" → "); + } + sb.Append(current.Message); + } + return sb.ToString(); + } + + // ─────────────────────────── 探测 ─────────────────────────── + + /// + /// 探一下这个端点通不通。通了返回 daemon 的版本信息;不通抛 + /// (带分类与出路)。 + /// + public async Task PingAsync(CancellationToken cancellationToken = default) + { + var version = await GetJsonAsync("/version", cancellationToken).ConfigureAwait(false); + return version; + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerEndpoint.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerEndpoint.cs new file mode 100644 index 0000000..ee459f4 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerEndpoint.cs @@ -0,0 +1,62 @@ +namespace VelaShell.Plugin.DockerPanel.Docker; + +/// 面板这一次要管哪台机器上的 Docker。 +public enum DockerEndpointKind +{ + /// 宿主所在这台机器上的 Docker(unix socket 或 Windows 命名管道)。 + Local, + + /// 某条已连接 SSH 会话对面那台机器上的 Docker(经隧道直连它的 socket)。 + Remote +} + +/// +/// 一个 Docker 端点的完整描述。**不可变**,且是面板里一切请求的第一参数 —— +/// 同时开着生产与测试两台机器时,"这条请求发给谁"必须是一个显式的值, +/// 而不是某个"当前会话"的隐式全局状态。 +/// +/// 本机还是远端。 +/// +/// 远端时为 SSH 会话 id;本机时为 。 +/// +/// +/// daemon 的 socket 路径。远端默认 /var/run/docker.sock; +/// 本机在 Windows 上是命名管道 //./pipe/docker_engine,其余平台同远端。 +/// +/// 界面上显示的名字(主机名 / “本机”)。 +/// 显示名下面那行小字(user@host、管道路径)。 +public sealed record DockerEndpoint( + DockerEndpointKind Kind, + string SessionId, + string SocketPath, + string DisplayName, + string Detail) +{ + /// 本机端点用的伪会话 id。 + public const string LocalSessionId = "@local"; + + /// Linux / macOS 上 daemon 的默认 socket 路径。 + public const string DefaultUnixSocket = "/var/run/docker.sock"; + + /// Windows 上 daemon 的默认命名管道。 + public const string DefaultWindowsPipe = @"\\.\pipe\docker_engine"; + + /// 本机端点(按当前操作系统选默认 socket)。 + public static DockerEndpoint Local(string displayName) => + new(DockerEndpointKind.Local, + LocalSessionId, + OperatingSystem.IsWindows() ? DefaultWindowsPipe : DefaultUnixSocket, + displayName, + OperatingSystem.IsWindows() ? DefaultWindowsPipe : DefaultUnixSocket); + + /// 远端端点。 + public static DockerEndpoint Remote(string sessionId, string displayName, string detail, string? socketPath = null) => + new(DockerEndpointKind.Remote, sessionId, string.IsNullOrWhiteSpace(socketPath) ? DefaultUnixSocket : socketPath!, + displayName, detail); + + /// + /// 端点的稳定标识:用于按端点缓存连接池、记忆每主机设置。 + /// + public string Key => $"{Kind}|{SessionId}|{SocketPath}"; + +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerFrames.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerFrames.cs new file mode 100644 index 0000000..21e50b9 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerFrames.cs @@ -0,0 +1,179 @@ +using System.Buffers.Binary; +using System.Text; + +namespace VelaShell.Plugin.DockerPanel.Docker; + +/// 一段输出来自哪条流。 +public enum DockerStreamKind +{ + /// 标准输入回显(attach 时才可能出现)。 + StdIn = 0, + + /// 标准输出。 + StdOut = 1, + + /// 标准错误。 + StdErr = 2 +} + +/// 解出来的一行日志。 +/// 来自哪条流。 +/// 一行文本(不含行尾换行)。 +/// timestamps=1 时解出的时间戳,否则 。 +public readonly record struct DockerLogLine(DockerStreamKind Kind, string Text, DateTimeOffset? Timestamp); + +/// +/// Docker 输出流的解帧器。 +/// +/// 容器**没有** TTY 时,daemon 把 stdout/stderr 复用在一条连接上,每块前面加一个 +/// 8 字节头:[类型][0][0][0][长度(大端 4 字节)]。有 TTY 时没有头,就是裸字节。 +/// 这就是这条通道必须是二进制的原因之一 —— 长度字段里出现 0x0A 是常态, +/// 按行切分会把一帧劈成两半。 +/// +/// +/// 解出来的字节再按 UTF-8 解码并切行。跨帧的半个字符与半行都被缓存住, +/// 不会吐出替换字符或半行 —— 半行开头的日志比少几行更难读。 +/// +/// +public sealed class DockerFrameDecoder(bool tty, bool timestamps) +{ + private readonly byte[] _header = new byte[8]; + private readonly Decoder _decoder = new UTF8Encoding(false).GetDecoder(); + private readonly StringBuilder _stdout = new(); + private readonly StringBuilder _stderr = new(); + private char[] _chars = new char[4096]; + + /// + /// 从流里持续解出日志行,直到流结束或取消。 + /// + /// 已经定位到帧起点的响应体流。 + /// 逐行回调(在读流的线程上同步调用,应快速返回)。 + /// 取消令牌。 + public async Task ReadAsync(Stream stream, Action onLine, CancellationToken cancellationToken) + { + var payload = new byte[64 * 1024]; + while (!cancellationToken.IsCancellationRequested) + { + DockerStreamKind kind; + int length; + if (tty) + { + // 有 TTY:没有帧头,读到多少算多少,全部归 stdout + // ——(TTY 本来就把 stderr 并进了同一条流,这不是我们丢的)。 + kind = DockerStreamKind.StdOut; + length = await stream.ReadAsync(payload, cancellationToken).ConfigureAwait(false); + if (length <= 0) + { + break; + } + Emit(kind, payload.AsSpan(0, length), onLine); + continue; + } + if (!await ReadExactlyAsync(stream, _header, cancellationToken).ConfigureAwait(false)) + { + break; + } + kind = (DockerStreamKind)_header[0]; + length = BinaryPrimitives.ReadInt32BigEndian(_header.AsSpan(4, 4)); + if (length < 0) + { + break; + } + if (length > payload.Length) + { + payload = new byte[length]; + } + if (!await ReadExactlyAsync(stream, payload.AsMemory(0, length), cancellationToken).ConfigureAwait(false)) + { + break; + } + Emit(kind, payload.AsSpan(0, length), onLine); + } + // 收尾:流结束时缓冲里可能还剩最后一行(没有行尾换行的那一行)。 + FlushRemainder(DockerStreamKind.StdOut, onLine); + FlushRemainder(DockerStreamKind.StdErr, onLine); + } + + private void Emit(DockerStreamKind kind, ReadOnlySpan bytes, Action onLine) + { + var needed = _decoder.GetCharCount(bytes, flush: false); + if (needed > _chars.Length) + { + _chars = new char[needed]; + } + var produced = _decoder.GetChars(bytes, _chars, flush: false); + var buffer = kind == DockerStreamKind.StdErr ? _stderr : _stdout; + for (var i = 0; i < produced; i++) + { + var c = _chars[i]; + if (c == '\n') + { + PublishLine(kind, buffer, onLine); + } + else if (c != '\r') + { + buffer.Append(c); + } + } + } + + private void FlushRemainder(DockerStreamKind kind, Action onLine) + { + var buffer = kind == DockerStreamKind.StdErr ? _stderr : _stdout; + if (buffer.Length > 0) + { + PublishLine(kind, buffer, onLine); + } + } + + private void PublishLine(DockerStreamKind kind, StringBuilder buffer, Action onLine) + { + var text = buffer.ToString(); + buffer.Clear(); + DateTimeOffset? stamp = null; + if (timestamps && TrySplitTimestamp(text, out var parsed, out var rest)) + { + stamp = parsed; + text = rest; + } + onLine(new(kind, text, stamp)); + } + + /// + /// 带 timestamps=1 时,daemon 会在每行前面拼一个 RFC3339 时间戳加一个空格。 + /// 把它拆出来交给界面,而不是让时间戳混在正文里被搜索命中。 + /// + internal static bool TrySplitTimestamp(string line, out DateTimeOffset timestamp, out string rest) + { + timestamp = default; + rest = line; + var space = line.IndexOf(' '); + // RFC3339 至少是 "2026-08-21T09:41:22Z" 这么长。 + if (space is < 20 or > 40) + { + return false; + } + if (!DateTimeOffset.TryParse(line.AsSpan(0, space), null, + System.Globalization.DateTimeStyles.RoundtripKind, out timestamp)) + { + return false; + } + rest = line[(space + 1)..]; + return true; + } + + private static async ValueTask ReadExactlyAsync(Stream stream, Memory buffer, CancellationToken cancellationToken) + { + var read = 0; + while (read < buffer.Length) + { + var n = await stream.ReadAsync(buffer[read..], cancellationToken).ConfigureAwait(false); + if (n <= 0) + { + return false; + } + read += n; + } + return true; + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerModels.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerModels.cs new file mode 100644 index 0000000..8e01ee2 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerModels.cs @@ -0,0 +1,1080 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace VelaShell.Plugin.DockerPanel.Docker; + +/// +/// Docker Engine API 的 DTO。 +/// +/// 只声明面板真正读的字段。daemon 的响应比这里宽得多,而反序列化器对多出来的字段 +/// 一律忽略 —— 把整个 schema 抄下来只会让每次 Docker 升级都变成一次维护事件。 +/// 需要看"全部"的地方(详情抽屉的 inspect 页)直接呈现原始 JSON,不经过 DTO。 +/// +/// +public static class DockerJson +{ + /// 全局共用的序列化设置。 + /// + /// 大小写不敏感是必需的:Docker 的字段大多是 PascalCase(IdNames), + /// 但统计与事件那几条流混着 snake_case(cpu_statstimeNano)。 + /// + public static readonly JsonSerializerOptions Options = new() + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + NumberHandling = JsonNumberHandling.AllowReadingFromString + }; + + private static readonly JsonSerializerOptions HumanJson = new() { WriteIndented = true }; + + /// 把一段 JSON 反序列化成 ;失败返回 + /// + /// 先看一眼首字符再解析。这条路上最常见的输入根本不是 JSON —— 反代挡下来的 HTML、 + /// socket 权限不足时的一行纯文本、空响应体,都会走到这里。为这些"抛出再吞掉" + /// 一次异常,除了在调试器里刷一屏首次异常之外没有任何收益。 + /// + public static T? TryDeserialize(string json) where T : class + { + var body = json.AsSpan().Trim(); + if (body.IsEmpty || (body[0] != '{' && body[0] != '[')) + { + return null; + } + try + { + return JsonSerializer.Deserialize(json, Options); + } + catch (JsonException) + { + return null; + } + } + + /// 把 JSON 重新缩进成人能读的样子;不是合法 JSON 就原样返回。 + public static string Prettify(string json) + { + try + { + using var doc = JsonDocument.Parse(json); + return JsonSerializer.Serialize(doc.RootElement, HumanJson); + } + catch (JsonException) + { + return json; + } + } +} + +/// daemon 的错误响应体。 +public sealed record DockerErrorBody +{ + /// 人话形式的失败原因。 + [JsonPropertyName("message")] + public string? Message { get; init; } +} + +// ─────────────────────────── 容器 ─────────────────────────── + +/// 端口映射(列表接口给的形态)。 +public sealed record DockerPort +{ + /// 宿主侧监听地址;未发布时为空。 + public string? IP { get; init; } + + /// 容器内端口。 + public int PrivatePort { get; init; } + + /// 宿主侧端口;未发布时为 0。 + public int PublicPort { get; init; } + + /// tcp / udp / sctp。 + public string? Type { get; init; } +} + +/// 挂载点(列表与 inspect 共用)。 +public sealed record DockerMount +{ + /// bind / volume / tmpfs。 + public string? Type { get; init; } + + /// 命名卷的名字;bind 挂载为空。 + public string? Name { get; init; } + + /// 宿主侧路径(bind)或卷的挂载点(volume)。 + public string? Source { get; init; } + + /// 容器内路径。 + public string? Destination { get; init; } + + /// 驱动。 + public string? Driver { get; init; } + + /// 是否可写。 + public bool RW { get; init; } +} + +/// 容器列表项(GET /containers/json)。 +public sealed record ContainerSummary +{ + /// 完整 id。 + public string Id { get; init; } = ""; + + /// 名字,带前导斜杠(daemon 的形态)。 + public string[]? Names { get; init; } + + /// 镜像引用。 + public string? Image { get; init; } + + /// 镜像 id。 + public string? ImageID { get; init; } + + /// 入口命令。 + public string? Command { get; init; } + + /// 创建时间(unix 秒)。 + public long Created { get; init; } + + /// created / running / paused / restarting / removing / exited / dead。 + public string? State { get; init; } + + /// 人话状态,如 Up 3 days (healthy) + public string? Status { get; init; } + + /// 端口映射。 + public DockerPort[]? Ports { get; init; } + + /// 标签。 + public Dictionary? Labels { get; init; } + + /// 挂载。 + public DockerMount[]? Mounts { get; init; } + + /// + /// 可写层占用的字节数。 + /// + /// /system/df 永远给这个值,而 /containers/json 只在带 + /// size=1 时才算(那要对每个容器做一次 diff,贵得多)—— 所以列表页拿到的是 0, + /// 系统页拿到的是真值。判空要看是不是 0,不要当成"容器没占空间"。 + /// + /// + public long SizeRw { get; init; } + + /// 包含镜像层在内的总占用;同样只有 /system/dfsize=1 才给。 + public long SizeRootFs { get; init; } + + /// 去掉前导斜杠的第一个名字。 + public string Name => Names is { Length: > 0 } n ? n[0].TrimStart('/') : Id[..Math.Min(12, Id.Length)]; + + /// compose 项目名(来自标准标签)。 + public string? ComposeProject => Labels?.GetValueOrDefault("com.docker.compose.project"); + + /// compose 服务名。 + public string? ComposeService => Labels?.GetValueOrDefault("com.docker.compose.service"); +} + +/// 健康检查状态。 +public sealed record ContainerHealth +{ + /// starting / healthy / unhealthy / none。 + public string? Status { get; init; } + + /// 连续失败次数。 + public int FailingStreak { get; init; } +} + +/// 容器状态(inspect)。 +public sealed record ContainerState +{ + /// 状态字符串。 + public string? Status { get; init; } + + /// 是否在跑。 + public bool Running { get; init; } + + /// 是否暂停。 + public bool Paused { get; init; } + + /// 是否正在重启。 + public bool Restarting { get; init; } + + /// 是否被 OOM 杀掉。 + public bool OOMKilled { get; init; } + + /// 进程号。 + public int Pid { get; init; } + + /// 退出码。 + public int ExitCode { get; init; } + + /// daemon 记下的错误。 + public string? Error { get; init; } + + /// 启动时间。 + public string? StartedAt { get; init; } + + /// 结束时间。 + public string? FinishedAt { get; init; } + + /// 健康检查。 + public ContainerHealth? Health { get; init; } +} + +/// 重启策略。 +public sealed record RestartPolicy +{ + /// no / on-failure / always / unless-stopped。 + public string? Name { get; init; } + + /// on-failure 的最大重试次数。 + public int MaximumRetryCount { get; init; } +} + +/// 宿主配置(inspect,只取面板用到的)。 +public sealed record ContainerHostConfig +{ + /// 重启策略。 + public RestartPolicy? RestartPolicy { get; init; } + + /// 网络模式。 + public string? NetworkMode { get; init; } + + /// 是否特权。 + public bool Privileged { get; init; } + + /// 内存上限(字节),0 表示不限。 + public long Memory { get; init; } + + /// CPU 配额。 + public long NanoCpus { get; init; } + + /// 退出即删除(--rm)。 + public bool AutoRemove { get; init; } + + /// + /// 端口映射声明,键是 80/tcp,值是绑定列表。 + /// + /// 用它而不是 ContainerSummary.Ports 重建 docker run:后者是**运行态**, + /// 容器停了就是空的,而"这个停掉的容器当初是怎么起的"恰恰是最需要复制那条命令的时候。 + /// + /// + public Dictionary? PortBindings { get; init; } + + /// 绑定挂载,源:目标[:模式] 形态。 + public string[]? Binds { get; init; } +} + +/// 一条端口绑定。 +public sealed record PortBinding +{ + /// 宿主 IP;留空是全部接口。 + public string? HostIp { get; init; } + + /// 宿主端口。 + public string? HostPort { get; init; } +} + +/// 容器配置(inspect,只取面板用到的)。 +public sealed record ContainerConfig +{ + /// 主机名。 + public string? Hostname { get; init; } + + /// 镜像引用。 + public string? Image { get; init; } + + /// 环境变量,KEY=VALUE 形态。 + public string[]? Env { get; init; } + + /// 命令。 + public string[]? Cmd { get; init; } + + /// 入口点。 + public string[]? Entrypoint { get; init; } + + /// 工作目录。 + public string? WorkingDir { get; init; } + + /// 用户。 + public string? User { get; init; } + + /// 标签。 + public Dictionary? Labels { get; init; } + + /// + /// 声明暴露的端口,键是 80/tcp 形态,值恒为空对象 —— + /// Docker 用它当集合使,面板只读键。 + /// + public Dictionary? ExposedPorts { get; init; } +} + +/// POST /commit 的响应。 +public sealed record CommitResponse +{ + /// 新镜像的 id。 + public string Id { get; init; } = ""; +} + +/// 镜像的层构成。 +public sealed record ImageRootFs +{ + /// 类型,基本恒为 layers + public string? Type { get; init; } + + /// 各层的 diff id。 + public string[]? Layers { get; init; } +} + +/// 一个镜像的完整信息(docker image inspect)。 +public sealed record ImageInspect +{ + /// 镜像 id。 + public string Id { get; init; } = ""; + + /// 标签。 + public string[]? RepoTags { get; init; } + + /// 摘要引用。 + public string[]? RepoDigests { get; init; } + + /// 作者留下的注释。 + public string? Comment { get; init; } + + /// 创建时间,RFC3339 字符串(不是 unix 秒 —— 与列表接口不一样)。 + public string? Created { get; init; } + + /// 构建它的 Docker 版本。 + public string? DockerVersion { get; init; } + + /// 作者。 + public string? Author { get; init; } + + /// 架构(amd64 / arm64…)。 + public string? Architecture { get; init; } + + /// 架构变体(v7…)。 + public string? Variant { get; init; } + + /// 操作系统。 + public string? Os { get; init; } + + /// 大小。 + public long Size { get; init; } + + /// 运行时默认配置。 + public ContainerConfig? Config { get; init; } + + /// 层构成。 + public ImageRootFs? RootFS { get; init; } +} + +/// 容器接入的一个网络。 +public sealed record EndpointSettings +{ + /// 网络 id。 + public string? NetworkID { get; init; } + + /// 容器在该网络中的 IPv4。 + public string? IPAddress { get; init; } + + /// 网关。 + public string? Gateway { get; init; } + + /// MAC 地址。 + public string? MacAddress { get; init; } + + /// 网络别名。 + public string[]? Aliases { get; init; } +} + +/// 网络设置(inspect)。 +public sealed record ContainerNetworkSettings +{ + /// 按网络名索引的接入信息。 + public Dictionary? Networks { get; init; } +} + +/// 容器 inspect。 +public sealed record ContainerInspect +{ + /// 完整 id。 + public string Id { get; init; } = ""; + + /// 名字,带前导斜杠。 + public string? Name { get; init; } + + /// 创建时间(ISO8601)。 + public string? Created { get; init; } + + /// 镜像 id。 + public string? Image { get; init; } + + /// 平台,如 linux + public string? Platform { get; init; } + + /// 重启次数。 + public int RestartCount { get; init; } + + /// 状态。 + public ContainerState? State { get; init; } + + /// 宿主配置。 + public ContainerHostConfig? HostConfig { get; init; } + + /// 容器配置。 + public ContainerConfig? Config { get; init; } + + /// 挂载。 + public DockerMount[]? Mounts { get; init; } + + /// 网络设置。 + public ContainerNetworkSettings? NetworkSettings { get; init; } + + /// 去掉前导斜杠的名字。 + public string DisplayName => Name?.TrimStart('/') ?? Id[..Math.Min(12, Id.Length)]; +} + +/// docker top 的结果。 +public sealed record ContainerTopResult +{ + /// 列标题。 + public string[]? Titles { get; init; } + + /// 每行一个进程,列与 对应。 + public string[][]? Processes { get; init; } +} + +/// 容器可写层相对镜像的一处变更(docker diff)。 +public sealed record FilesystemChange +{ + /// 路径。 + public string Path { get; init; } = ""; + + /// 0=修改 1=新增 2=删除。 + public int Kind { get; init; } + + /// 单字母标记,与 docker diff 的输出一致。 + public string Marker => Kind switch { 1 => "A", 2 => "D", _ => "C" }; +} + +/// 创建容器的响应。 +public sealed record CreateContainerResponse +{ + /// 新容器 id。 + public string Id { get; init; } = ""; + + /// daemon 的警告(端口冲突提醒之类)。 + public string[]? Warnings { get; init; } +} + +// ─────────────────────────── 镜像 ─────────────────────────── + +/// 镜像列表项。 +public sealed record ImageSummary +{ + /// 镜像 id,带 sha256: 前缀。 + public string Id { get; init; } = ""; + + /// 标签,repo:tag 形态;悬空镜像为 <none>:<none> + public string[]? RepoTags { get; init; } + + /// 摘要引用。 + public string[]? RepoDigests { get; init; } + + /// 创建时间(unix 秒)。 + public long Created { get; init; } + + /// 大小(字节)。 + public long Size { get; init; } + + /// 与其它镜像共享的层大小。 + public long SharedSize { get; init; } + + /// 使用它的容器数;-1 表示 daemon 没算。 + public int Containers { get; init; } = -1; + + /// 标签。 + public Dictionary? Labels { get; init; } + + /// 短 id(去掉 sha256:,取前 12 位)。 + public string ShortId => Id.StartsWith("sha256:", StringComparison.Ordinal) + ? Id[7..Math.Min(19, Id.Length)] + : Id[..Math.Min(12, Id.Length)]; + + /// 没有任何标签 —— 即悬空镜像。 + public bool IsDangling => + RepoTags is null or { Length: 0 } || RepoTags.All(t => t == ":"); +} + +/// 镜像构建历史的一层。 +public sealed record ImageHistoryEntry +{ + /// 层 id。 + public string Id { get; init; } = ""; + + /// 创建时间(unix 秒)。 + public long Created { get; init; } + + /// 产生这一层的指令。 + public string? CreatedBy { get; init; } + + /// 大小。 + public long Size { get; init; } + + /// 注释。 + public string? Comment { get; init; } + + /// 标签。 + public string[]? Tags { get; init; } +} + +// ─────────────────────────── 卷 / 网络 ─────────────────────────── + +/// 卷的用量(仅 /system/df 会填)。 +public sealed record VolumeUsageData +{ + /// 占用字节;-1 表示未统计。 + public long Size { get; init; } = -1; + + /// 被多少容器引用。 + public int RefCount { get; init; } +} + +/// 卷。 +public sealed record VolumeSummary +{ + /// 卷名。 + public string Name { get; init; } = ""; + + /// 驱动。 + public string? Driver { get; init; } + + /// 宿主上的挂载点。 + public string? Mountpoint { get; init; } + + /// 创建时间(ISO8601)。 + public string? CreatedAt { get; init; } + + /// 作用域。 + public string? Scope { get; init; } + + /// 标签。 + public Dictionary? Labels { get; init; } + + /// 驱动选项。 + public Dictionary? Options { get; init; } + + /// 用量。 + public VolumeUsageData? UsageData { get; init; } +} + +/// 卷列表的响应外壳。 +public sealed record VolumeListResponse +{ + /// 卷。 + public VolumeSummary[]? Volumes { get; init; } + + /// daemon 的警告。 + public string[]? Warnings { get; init; } +} + +/// IPAM 的一段配置。 +public sealed record IpamConfig +{ + /// 子网。 + public string? Subnet { get; init; } + + /// 网关。 + public string? Gateway { get; init; } + + /// 可分配范围。 + public string? IPRange { get; init; } +} + +/// 网络的 IPAM。 +public sealed record IpamSettings +{ + /// 驱动。 + public string? Driver { get; init; } + + /// 配置段。 + public IpamConfig[]? Config { get; init; } +} + +/// 接在某网络上的一个容器。 +public sealed record NetworkContainer +{ + /// 容器名。 + public string? Name { get; init; } + + /// IPv4 地址(带掩码)。 + public string? IPv4Address { get; init; } + + /// MAC。 + public string? MacAddress { get; init; } +} + +/// 网络。 +public sealed record NetworkSummary +{ + /// 网络 id。 + public string Id { get; init; } = ""; + + /// 网络名。 + public string Name { get; init; } = ""; + + /// 创建时间。 + public string? Created { get; init; } + + /// 作用域。 + public string? Scope { get; init; } + + /// 驱动。 + public string? Driver { get; init; } + + /// 是否启用 IPv6。 + public bool EnableIPv6 { get; init; } + + /// 是否内部网络(不通外网)。 + public bool Internal { get; init; } + + /// 是否允许事后接入。 + public bool Attachable { get; init; } + + /// IPAM。 + public IpamSettings? IPAM { get; init; } + + /// 已接入的容器,按容器 id 索引。 + public Dictionary? Containers { get; init; } + + /// 标签。 + public Dictionary? Labels { get; init; } + + /// 选项。 + public Dictionary? Options { get; init; } + + /// + /// Docker 内置的三个网络删不掉,界面据此把删除按钮直接置灰 —— + /// 而不是让用户去撞一条 daemon 的错误。 + /// + public bool IsPredefined => Name is "bridge" or "host" or "none"; + + /// 第一段子网(界面上只显示一段)。 + public string? FirstSubnet => IPAM?.Config is { Length: > 0 } c ? c[0].Subnet : null; + + /// 第一段网关。 + public string? FirstGateway => IPAM?.Config is { Length: > 0 } c ? c[0].Gateway : null; +} + +// ─────────────────────────── 系统 ─────────────────────────── + +/// GET /version +public sealed record SystemVersion +{ + /// Engine 版本。 + public string? Version { get; init; } + + /// 当前 API 版本。 + public string? ApiVersion { get; init; } + + /// 支持的最低 API 版本。 + public string? MinAPIVersion { get; init; } + + /// 提交号。 + public string? GitCommit { get; init; } + + /// Go 版本。 + public string? GoVersion { get; init; } + + /// 操作系统。 + public string? Os { get; init; } + + /// 架构。 + public string? Arch { get; init; } + + /// 内核版本。 + public string? KernelVersion { get; init; } +} + +/// Swarm 状态(只取节点状态)。 +public sealed record SwarmInfo +{ + /// inactive / pending / active / error / locked。 + public string? LocalNodeState { get; init; } +} + +/// GET /info +public sealed record SystemInfo +{ + /// daemon 唯一 id。 + public string? ID { get; init; } + + /// daemon 主机名。 + public string? Name { get; init; } + + /// 容器总数。 + public int Containers { get; init; } + + /// 运行中容器数。 + public int ContainersRunning { get; init; } + + /// 暂停容器数。 + public int ContainersPaused { get; init; } + + /// 停止容器数。 + public int ContainersStopped { get; init; } + + /// 镜像数。 + public int Images { get; init; } + + /// 存储驱动。 + public string? Driver { get; init; } + + /// 日志驱动。 + public string? LoggingDriver { get; init; } + + /// cgroup 版本。 + public string? CgroupVersion { get; init; } + + /// cgroup 驱动。 + public string? CgroupDriver { get; init; } + + /// 内存总量(字节)。 + public long MemTotal { get; init; } + + /// CPU 核数。 + public int NCPU { get; init; } + + /// 操作系统。 + public string? OperatingSystem { get; init; } + + /// 内核版本。 + public string? KernelVersion { get; init; } + + /// Engine 版本。 + public string? ServerVersion { get; init; } + + /// 架构。 + public string? Architecture { get; init; } + + /// Docker 根目录。 + public string? DockerRootDir { get; init; } + + /// Swarm。 + public SwarmInfo? Swarm { get; init; } +} + +/// 构建缓存的一条记录。 +public sealed record BuildCacheRecord +{ + /// id。 + public string? ID { get; init; } + + /// 是否可回收。 + public bool InUse { get; init; } + + /// 占用字节。 + public long Size { get; init; } +} + +/// GET /system/df +public sealed record DiskUsage +{ + /// 镜像层总占用。 + public long LayersSize { get; init; } + + /// 镜像。 + public ImageSummary[]? Images { get; init; } + + /// 容器。 + public ContainerSummary[]? Containers { get; init; } + + /// 卷。 + public VolumeSummary[]? Volumes { get; init; } + + /// 构建缓存。 + public BuildCacheRecord[]? BuildCache { get; init; } +} + +/// 各种 prune 的统一响应形态(字段按类型取其一)。 +public sealed record PruneReport +{ + /// 被删掉的容器 id。 + public string[]? ContainersDeleted { get; init; } + + /// 被删掉的卷名。 + public string[]? VolumesDeleted { get; init; } + + /// 被删掉的网络名。 + public string[]? NetworksDeleted { get; init; } + + /// 被删掉的镜像。 + public DeletedImage[]? ImagesDeleted { get; init; } + + /// 被删掉的构建缓存。 + public BuildCacheRecord[]? CachesDeleted { get; init; } + + /// 回收的字节数。 + public long SpaceReclaimed { get; init; } + + /// 被删项目的条数(各类之和)。 + public int DeletedCount => + (ContainersDeleted?.Length ?? 0) + (VolumesDeleted?.Length ?? 0) + + (NetworksDeleted?.Length ?? 0) + (ImagesDeleted?.Length ?? 0) + (CachesDeleted?.Length ?? 0); +} + +/// 被删掉/取消标签的镜像。 +public sealed record DeletedImage +{ + /// 被删掉的镜像 id。 + public string? Deleted { get; init; } + + /// 被取消的标签。 + public string? Untagged { get; init; } +} + +// ─────────────────────────── 事件 / 统计 ─────────────────────────── + +/// 事件的主体。 +public sealed record EventActor +{ + /// 对象 id。 + public string? ID { get; init; } + + /// 属性(容器名、镜像、compose 标签都在这里)。 + public Dictionary? Attributes { get; init; } +} + +/// GET /events 推来的一条事件。 +public sealed record DockerEvent +{ + /// container / image / volume / network / daemon… + [JsonPropertyName("Type")] + public string? Type { get; init; } + + /// start / die / pull / health_status: healthy … + [JsonPropertyName("Action")] + public string? Action { get; init; } + + /// 主体。 + [JsonPropertyName("Actor")] + public EventActor? Actor { get; init; } + + /// 秒级时间戳。 + [JsonPropertyName("time")] + public long Time { get; init; } + + /// 纳秒级时间戳。 + [JsonPropertyName("timeNano")] + public long TimeNano { get; init; } + + /// 对象的显示名:容器名 / 镜像名 / 卷名。 + public string DisplayName => + Actor?.Attributes?.GetValueOrDefault("name") + ?? Actor?.ID?[..Math.Min(12, Actor.ID.Length)] + ?? ""; + + /// 事件发生时间(本地)。 + public DateTimeOffset At => TimeNano > 0 + ? DateTimeOffset.FromUnixTimeMilliseconds(TimeNano / 1_000_000).ToLocalTime() + : DateTimeOffset.FromUnixTimeSeconds(Time).ToLocalTime(); +} + +/// CPU 用量的一次采样。 +public sealed record CpuUsage +{ + /// 容器累计使用的纳秒。 + [JsonPropertyName("total_usage")] + public ulong TotalUsage { get; init; } + + /// 按核拆分的累计使用。 + [JsonPropertyName("percpu_usage")] + public ulong[]? PerCpuUsage { get; init; } +} + +/// CPU 统计。 +public sealed record CpuStats +{ + /// 容器 CPU 用量。 + [JsonPropertyName("cpu_usage")] + public CpuUsage? CpuUsage { get; init; } + + /// 整机累计 CPU 纳秒。 + [JsonPropertyName("system_cpu_usage")] + public ulong SystemCpuUsage { get; init; } + + /// 在线核数(cgroup v2 给这个)。 + [JsonPropertyName("online_cpus")] + public int OnlineCpus { get; init; } +} + +/// 内存统计。 +public sealed record MemoryStats +{ + /// 当前用量(含缓存)。 + [JsonPropertyName("usage")] + public ulong Usage { get; init; } + + /// 上限。 + [JsonPropertyName("limit")] + public ulong Limit { get; init; } + + /// 明细,其中 cache / inactive_file 要从用量里扣掉才是"真占用"。 + [JsonPropertyName("stats")] + public Dictionary? Stats { get; init; } +} + +/// 一张网卡的收发计数。 +public sealed record NetworkStats +{ + /// 收到的字节。 + [JsonPropertyName("rx_bytes")] + public ulong RxBytes { get; init; } + + /// 发出的字节。 + [JsonPropertyName("tx_bytes")] + public ulong TxBytes { get; init; } +} + +/// GET /containers/{id}/stats 推来的一帧。 +public sealed record ContainerStats +{ + /// 容器 id。 + [JsonPropertyName("id")] + public string? Id { get; init; } + + /// 采样时刻。 + [JsonPropertyName("read")] + public DateTimeOffset Read { get; init; } + + /// 本次 CPU 采样。 + [JsonPropertyName("cpu_stats")] + public CpuStats? CpuStats { get; init; } + + /// 上一次 CPU 采样(算差值用)。 + [JsonPropertyName("precpu_stats")] + public CpuStats? PreCpuStats { get; init; } + + /// 内存。 + [JsonPropertyName("memory_stats")] + public MemoryStats? MemoryStats { get; init; } + + /// 按网卡名索引的收发计数。 + [JsonPropertyName("networks")] + public Dictionary? Networks { get; init; } + + /// + /// CPU 占用百分比(与 docker stats 同一口径:两次采样的差值之比 × 核数)。 + /// 采样不全或分母为 0 时返回 0,而不是一个假的尖峰。 + /// + public double CpuPercent + { + get + { + if (CpuStats?.CpuUsage is null || PreCpuStats?.CpuUsage is null) + { + return 0; + } + var cpuDelta = (double)CpuStats.CpuUsage.TotalUsage - PreCpuStats.CpuUsage.TotalUsage; + var systemDelta = (double)CpuStats.SystemCpuUsage - PreCpuStats.SystemCpuUsage; + if (cpuDelta <= 0 || systemDelta <= 0) + { + return 0; + } + var cpus = CpuStats.OnlineCpus > 0 + ? CpuStats.OnlineCpus + : CpuStats.CpuUsage.PerCpuUsage?.Length ?? 1; + return cpuDelta / systemDelta * cpus * 100.0; + } + } + + /// + /// 真实内存占用:总用量减去页缓存。 + /// + /// 不扣缓存的话,一个只是读过大文件的容器会显示成"内存快满了" —— + /// docker stats 自己也是这么扣的。cgroup v2 的字段叫 inactive_file, + /// v1 叫 cache;两个都试,取到哪个算哪个。 + /// + /// + public ulong MemoryUsed + { + get + { + if (MemoryStats is null) + { + return 0; + } + ulong cache = 0; + if (MemoryStats.Stats is { } stats) + { + if (stats.TryGetValue("inactive_file", out var inactive)) + { + cache = inactive; + } + else if (stats.TryGetValue("cache", out var c)) + { + cache = c; + } + } + return MemoryStats.Usage > cache ? MemoryStats.Usage - cache : MemoryStats.Usage; + } + } + + /// 内存上限;0 表示不限。 + public ulong MemoryLimit => MemoryStats?.Limit ?? 0; +} + +// ─────────────────────────── exec ─────────────────────────── + +/// 创建 exec 的响应。 +public sealed record ExecCreateResponse +{ + /// exec 实例 id。 + public string Id { get; init; } = ""; +} + +/// exec 的检查结果(拿退出码)。 +public sealed record ExecInspectResponse +{ + /// 是否还在跑。 + public bool Running { get; init; } + + /// 退出码。 + public int ExitCode { get; init; } + + /// 进程号。 + public int Pid { get; init; } +} + +// ─────────────────────────── 拉取进度 ─────────────────────────── + +/// 拉取/推送进度的一帧(NDJSON)。 +public sealed record PullProgressFrame +{ + /// 层 id;总览行没有 id。 + [JsonPropertyName("id")] + public string? Id { get; init; } + + /// Downloading / Extracting / Pull complete / Already exists … + [JsonPropertyName("status")] + public string? Status { get; init; } + + /// 字节进度。 + [JsonPropertyName("progressDetail")] + public PullProgressDetail? ProgressDetail { get; init; } + + /// 错误(有它就意味着这次拉取失败了)。 + [JsonPropertyName("error")] + public string? Error { get; init; } +} + +/// 拉取进度的字节明细。 +public sealed record PullProgressDetail +{ + /// 已完成字节。 + [JsonPropertyName("current")] + public long Current { get; init; } + + /// 总字节;未知时为 0。 + [JsonPropertyName("total")] + public long Total { get; init; } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerRawHttp.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerRawHttp.cs new file mode 100644 index 0000000..1247f78 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerRawHttp.cs @@ -0,0 +1,81 @@ +using System.Text; + +namespace VelaShell.Plugin.DockerPanel.Docker; + +/// +/// 直接在一条隧道上说 HTTP/1.1,用于 Docker 那几个**劫持连接**的端点 +/// (/exec/{id}/start/containers/{id}/attach)。 +/// +/// 这些端点在响应头之后就把连接变成一条裸的双工管道,而 +/// 的模型是"请求发完才读响应" —— 它没有办法在同一条连接上边写 stdin 边读 stdout。 +/// 既然我们本来就握着这条流,不如自己写这十几行,而不是去跟一个不为此设计的抽象较劲。 +/// +/// +internal static class DockerRawHttp +{ + private static readonly byte[] HeaderTerminator = "\r\n\r\n"u8.ToArray(); + + /// + /// 在 上发一条带 JSON 体的 POST,读完响应头后把流原样交回。 + /// + /// 已连到 daemon 的双工流(调用后归本方法的调用方所有)。 + /// 请求路径。 + /// 请求体;为 null 表示没有体。 + /// 是否带 Upgrade: tcp(exec/attach 要)。 + /// 取消令牌。 + /// 状态码与"已经把头读完"的流(头之后多读到的字节已经补回流首)。 + public static async Task<(int StatusCode, string ReasonPhrase, Stream Body)> PostAsync( + Stream stream, string path, string? jsonBody, bool upgrade, CancellationToken cancellationToken) + { + var body = jsonBody is null ? [] : Encoding.UTF8.GetBytes(jsonBody); + var request = new StringBuilder() + .Append("POST ").Append(path).Append(" HTTP/1.1\r\n") + .Append("Host: docker\r\n") + .Append("User-Agent: VelaShell-DockerPanel\r\n") + .Append("Content-Type: application/json\r\n") + .Append("Content-Length: ").Append(body.Length).Append("\r\n"); + if (upgrade) + { + request.Append("Connection: Upgrade\r\nUpgrade: tcp\r\n"); + } + request.Append("\r\n"); + await stream.WriteAsync(Encoding.ASCII.GetBytes(request.ToString()), cancellationToken).ConfigureAwait(false); + if (body.Length > 0) + { + await stream.WriteAsync(body, cancellationToken).ConfigureAwait(false); + } + await stream.FlushAsync(cancellationToken).ConfigureAwait(false); + return await ReadResponseHeadAsync(stream, cancellationToken).ConfigureAwait(false); + } + + /// + /// 一个字节一个字节地读到 \r\n\r\n 为止。 + /// + /// + /// 刻意不做块读:响应头之后紧跟着的就是要交给调用方的裸数据,多读一个字节 + /// 就得自己补回去。头很短(几百字节),这点系统调用换来的是零缓冲复杂度。 + /// + private static async Task<(int, string, Stream)> ReadResponseHeadAsync(Stream stream, CancellationToken cancellationToken) + { + var head = new List(512); + var one = new byte[1]; + var matched = 0; + while (matched < HeaderTerminator.Length) + { + var read = await stream.ReadAsync(one.AsMemory(0, 1), cancellationToken).ConfigureAwait(false); + if (read <= 0) + { + throw new DockerApiException(System.Net.HttpStatusCode.BadGateway, + "daemon 在返回响应头之前就把连接关掉了。"); + } + head.Add(one[0]); + matched = one[0] == HeaderTerminator[matched] ? matched + 1 : one[0] == HeaderTerminator[0] ? 1 : 0; + } + var text = Encoding.ASCII.GetString([.. head]); + var statusLine = text[..text.IndexOf('\r')]; + var parts = statusLine.Split(' ', 3); + var status = parts.Length > 1 && int.TryParse(parts[1], out var code) ? code : 0; + var reason = parts.Length > 2 ? parts[2] : ""; + return (status, reason, stream); + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerTransport.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerTransport.cs new file mode 100644 index 0000000..8f1a703 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerTransport.cs @@ -0,0 +1,80 @@ +using System.IO.Pipes; +using System.Net.Sockets; +using VelaShell.PluginSdk.RemoteTunnel; + +namespace VelaShell.Plugin.DockerPanel.Docker; + +/// +/// 打开一条到 Docker daemon 的双工字节流。 +/// +/// 这一层刻意只有一个方法:面板上面的所有东西 —— HTTP 客户端、事件流、exec 的 +/// 多路复用帧 —— 都只要求"给我一条能读能写的流",至于它是 SSH 通道、本机 unix socket +/// 还是 Windows 命名管道,再往上就不该有人关心了。 +/// +/// +public interface IDockerTransport +{ + /// 端点描述(给日志与错误文案用)。 + string Description { get; } + + /// 打开一条新的双工流。调用方负责释放。 + Task ConnectAsync(CancellationToken cancellationToken = default); +} + +/// +/// 经 SDK 的远程隧道能力连到远端 daemon 的 socket +/// (SSH 的 direct-streamlocal@openssh.com 通道)。 +/// +/// 相比"在本机开一个转发端口再连过去",这条路不在本机留下任何别的进程能连上的入口 —— +/// 隧道对面是一个 root 等价的 socket,这个区别不是优化而是前提。 +/// +/// +public sealed class TunnelTransport(IRemoteTunnelApi tunnels, string sessionId, string socketPath) : IDockerTransport +{ + /// + public string Description { get; } = $"{socketPath} (SSH 隧道)"; + + /// + public Task ConnectAsync(CancellationToken cancellationToken = default) => + tunnels.OpenUnixSocketAsync(sessionId, socketPath, null, cancellationToken); +} + +/// +/// 连本机 daemon:Windows 走命名管道,其余平台走 unix 域套接字。 +/// +public sealed class LocalTransport(string socketPath) : IDockerTransport +{ + /// + public string Description { get; } = socketPath; + + /// + public async Task ConnectAsync(CancellationToken cancellationToken = default) + { + if (OperatingSystem.IsWindows() && Description.StartsWith(@"\\.\pipe\", StringComparison.Ordinal)) + { + var pipeName = Description[@"\\.\pipe\".Length..]; + var pipe = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); + try + { + await pipe.ConnectAsync(cancellationToken).ConfigureAwait(false); + return pipe; + } + catch + { + await pipe.DisposeAsync().ConfigureAwait(false); + throw; + } + } + var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); + try + { + await socket.ConnectAsync(new UnixDomainSocketEndPoint(Description), cancellationToken).ConfigureAwait(false); + return new NetworkStream(socket, ownsSocket: true); + } + catch + { + socket.Dispose(); + throw; + } + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/Humanize.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/Humanize.cs new file mode 100644 index 0000000..5066f0c --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/Humanize.cs @@ -0,0 +1,126 @@ +using System.Globalization; + +namespace VelaShell.Plugin.DockerPanel.Docker; + +/// 把机器的数字翻成人看的字。 +public static class Humanize +{ + private static readonly string[] SizeUnits = ["B", "KB", "MB", "GB", "TB", "PB"]; + + /// + /// 字节数。与 docker 自己一样用 1000 进制 —— 面板里显示的 "214 MB" + /// 要和用户在 docker images 里看到的对得上,不然他会以为哪里算错了。 + /// + public static string Bytes(long value) + { + if (value < 0) + { + return "—"; + } + if (value < 1000) + { + return $"{value} B"; + } + double size = value; + var unit = 0; + while (size >= 1000 && unit < SizeUnits.Length - 1) + { + size /= 1000; + unit++; + } + return size >= 100 + ? $"{size:0} {SizeUnits[unit]}" + : size >= 10 + ? $"{size:0.#} {SizeUnits[unit]}" + : $"{size:0.##} {SizeUnits[unit]}"; + } + + /// 字节数(无符号重载)。 + public static string Bytes(ulong value) => Bytes(value > long.MaxValue ? long.MaxValue : (long)value); + + /// 百分比,保留一位小数。 + public static string Percent(double value) => value <= 0 ? "0%" : $"{value.ToString("0.#", CultureInfo.InvariantCulture)}%"; + + /// + /// 一段时长,精确到两档("3d 4h"、"6h 12m"、"48s")。 + /// 运维要的是量级,不是秒数 —— 多一档只会把列挤宽。 + /// + public static string Duration(TimeSpan span) + { + if (span < TimeSpan.Zero) + { + span = TimeSpan.Zero; + } + return span.TotalDays >= 1 + ? $"{(int)span.TotalDays}d {span.Hours}h" + : span.TotalHours >= 1 + ? $"{(int)span.TotalHours}h {span.Minutes}m" + : span.TotalMinutes >= 1 + ? $"{(int)span.TotalMinutes}m {span.Seconds}s" + : $"{(int)span.TotalSeconds}s"; + } + + /// 把一个时刻说成"多久以前"。 + public static string Ago(DateTimeOffset moment) + { + var span = DateTimeOffset.UtcNow - moment.ToUniversalTime(); + return span < TimeSpan.FromSeconds(5) ? "刚刚" : $"{Duration(span)} 前"; + } + + /// 把 ISO8601 文本说成"多久以前";解析不了返回破折号。 + /// + /// 镜像的两个接口对创建时间用了两种表示:列表给 unix 秒,inspect 给 RFC3339 串。 + /// 所以这两个入口都得有。 + /// + public static string AgoFromIso(string? iso) => + DateTimeOffset.TryParse(iso, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var moment) + ? Ago(moment) + : "—"; + + /// 把 unix 秒说成"多久以前"。 + public static string AgoFromUnix(long unixSeconds) => + unixSeconds <= 0 ? "—" : Ago(DateTimeOffset.FromUnixTimeSeconds(unixSeconds)); + + /// 把 ISO8601 文本解析成本地时间文本;解析不了就原样返回。 + public static string LocalTime(string? iso) + { + return string.IsNullOrWhiteSpace(iso) ? "—" + : DateTimeOffset.TryParse(iso, null, DateTimeStyles.RoundtripKind, out var parsed) + ? parsed.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture) + : iso; + } + + /// + /// 容器的端口摘要:8080→80,多条用逗号连;没有发布端口就给一个破折号。 + /// + public static string Ports(DockerPort[]? ports) + { + if (ports is null or { Length: 0 }) + { + return "—"; + } + // 同一个容器端口可能同时绑在 0.0.0.0 与 :: 上,daemon 会给两条。 + // 界面上它们是同一件事,去重之后才不会显示成 "8080→80, 8080→80"。 + var published = ports + .Where(p => p.PublicPort > 0) + .Select(p => $"{p.PublicPort}→{p.PrivatePort}") + .Distinct(); + var exposed = ports + .Where(p => p.PublicPort == 0) + .Select(p => $"{p.PrivatePort}/{p.Type ?? "tcp"}") + .Distinct(); + var text = string.Join(", ", published.Concat(exposed)); + return text.Length == 0 ? "—" : text; + } + + /// 短 id(12 位),Docker 自己在命令行里就是这么显示的。 + public static string ShortId(string? id) + { + if (string.IsNullOrEmpty(id)) + { + return ""; + } + var text = id.StartsWith("sha256:", StringComparison.Ordinal) ? id[7..] : id; + return text.Length <= 12 ? text : text[..12]; + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/LineDiff.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/LineDiff.cs new file mode 100644 index 0000000..12d3c15 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/LineDiff.cs @@ -0,0 +1,134 @@ +namespace VelaShell.Plugin.DockerPanel.Docker; + +/// 一行在差异里的角色。 +public enum DiffMarker +{ + /// 没变。 + None, + + /// 新增。 + Added, + + /// 删除。 + Removed, + + /// 改过(同一位置上的替换)。 + Changed +} + +/// 差异里的一行。 +/// 角色。 +/// 在原文里的行号;新增行为 0。 +/// 在新文里的行号;删除行为 0。 +/// 行内容。 +public readonly record struct DiffLine(DiffMarker Marker, int OldNumber, int NewNumber, string Text); + +/// +/// 按行比对两段文本。 +/// +/// 用最长公共子序列,不是逐行对齐:在一个配置文件顶部插一行,逐行对齐会把 +/// **之后的每一行**都标成改过,那样的差异视图等于没有。 +/// +/// +/// 面板要比的是配置文件(几十到几千行),LCS 的 O(n·m) 在这个量级上无所谓; +/// 真碰上超大文件,上层的 2 MB 在线编辑上限先一步挡住了。 +/// +/// +public static class LineDiff +{ + /// 相邻的一删一增合并成"改过";超过这个规模就不合并了。 + private const int MaxPairedChange = 200; + + /// 比出差异。 + public static IReadOnlyList Compute(string oldText, string newText) + { + var a = Split(oldText); + var b = Split(newText); + var lcs = BuildLcs(a, b); + + List result = []; + var i = 0; + var j = 0; + while (i < a.Length && j < b.Length) + { + if (a[i] == b[j]) + { + result.Add(new(DiffMarker.None, i + 1, j + 1, a[i])); + i++; + j++; + } + else if (lcs[i + 1, j] >= lcs[i, j + 1]) + { + result.Add(new(DiffMarker.Removed, i + 1, 0, a[i])); + i++; + } + else + { + result.Add(new(DiffMarker.Added, 0, j + 1, b[j])); + j++; + } + } + while (i < a.Length) + { + result.Add(new(DiffMarker.Removed, i + 1, 0, a[i])); + i++; + } + while (j < b.Length) + { + result.Add(new(DiffMarker.Added, 0, j + 1, b[j])); + j++; + } + return Pair(result); + } + + /// 新文里有多少行与原文不同(底部那句"已修改 N 行")。 + public static int CountChanged(IReadOnlyList lines) => + lines.Count(l => l.Marker != DiffMarker.None); + + /// + /// 把"紧挨着的一删一增"合并成一条"改过"。 + /// + /// 改一行的值(listen 80;listen 8080;)在 LCS 里天然是一删一增, + /// 但人看到的是**一行改了**。不合并的话,一个改了三处的文件会显示成六行差异。 + /// + /// + private static List Pair(List lines) + { + List paired = [with(lines.Count)]; + for (var k = 0; k < lines.Count; k++) + { + var current = lines[k]; + if (current.Marker == DiffMarker.Removed + && k + 1 < lines.Count + && lines[k + 1].Marker == DiffMarker.Added + && lines.Count <= MaxPairedChange * 4) + { + var next = lines[k + 1]; + paired.Add(new(DiffMarker.Changed, current.OldNumber, next.NewNumber, next.Text)); + k++; + continue; + } + paired.Add(current); + } + return paired; + } + + private static string[] Split(string text) => + text.Length == 0 ? [] : text.Replace("\r\n", "\n", StringComparison.Ordinal).Split('\n'); + + private static int[,] BuildLcs(string[] a, string[] b) + { + // lcs[i, j] = a[i..] 与 b[j..] 的最长公共子序列长度。倒着填,回溯时正着走。 + var lcs = new int[a.Length + 1, b.Length + 1]; + for (var i = a.Length - 1; i >= 0; i--) + { + for (var j = b.Length - 1; j >= 0; j--) + { + lcs[i, j] = a[i] == b[j] + ? lcs[i + 1, j + 1] + 1 + : Math.Max(lcs[i + 1, j], lcs[i, j + 1]); + } + } + return lcs; + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/RegistryAuth.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/RegistryAuth.cs new file mode 100644 index 0000000..87e9d1c --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/RegistryAuth.cs @@ -0,0 +1,200 @@ +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using VelaShell.PluginSdk.RemoteFs; + +namespace VelaShell.Plugin.DockerPanel.Docker; + +/// 某个仓库的登录状态,用于在拉取表单上如实显示。 +/// 仓库地址。 +/// 状态。 +/// 补充说明(凭据来源、为什么用不了)。 +public readonly record struct RegistryAuthStatus(string Registry, RegistryAuthState State, string Detail); + +/// 仓库登录状态。 +public enum RegistryAuthState +{ + /// 公开仓库,不需要凭据。 + NotRequired, + + /// 在远端的 config.json 里找到了可用凭据。 + Available, + + /// config.json 里有这个仓库,但凭据交给了 credential helper —— 面板取不到。 + HelperOnly, + + /// 没有找到凭据。私有仓库会因此拉取失败。 + Missing +} + +/// +/// 仓库凭据。 +/// +/// 这里有一个 CLI 版没有的坑。 docker pull 的凭据是 **CLI** 从 +/// ~/.docker/config.json 读出来、放进 X-Registry-Auth 头再发给 daemon 的; +/// daemon **自己不读那个文件**。所以改走 HTTP API 之后,私有仓库的拉取必须由面板 +/// 自己去读那份 config —— 否则表现为"命令行能拉、面板拉不动",而且报的是 +/// 一句没头没脑的 401。 +/// +/// +/// 凭据只在**发起那一次请求时**读取,不落盘、不进内存缓存、不写日志。 +/// 用 credential helper(credsStore / credHelpers)保存的凭据取不到 —— +/// 那需要在远端跑一个 helper 可执行文件,面板不做这件事,而是在界面上说清楚。 +/// +/// +public sealed class RegistryAuthProvider(IRemoteFsApi remoteFs, DockerEndpoint endpoint) +{ + /// Docker Hub 在 config.json 里的键。 + public const string DockerHub = "https://index.docker.io/v1/"; + + /// 从镜像引用里解出仓库地址。 + /// + /// 规则与 Docker 自己一致:第一段里带 .: 或等于 localhost + /// 才算仓库主机,否则整串都是 Docker Hub 上的仓库名。 + /// + public static string ResolveRegistry(string imageReference) + { + var reference = imageReference.Trim(); + var slash = reference.IndexOf('/'); + if (slash <= 0) + { + return DockerHub; + } + var first = reference[..slash]; + var isHost = first.Contains('.', StringComparison.Ordinal) + || first.Contains(':', StringComparison.Ordinal) + || first == "localhost"; + return isHost ? first : DockerHub; + } + + /// 查这个镜像所属仓库的登录状态(给表单显示用,不返回凭据本身)。 + public async Task GetStatusAsync(string imageReference, CancellationToken cancellationToken = default) + { + var registry = ResolveRegistry(imageReference); + var config = await ReadConfigAsync(cancellationToken).ConfigureAwait(false); + if (config is null) + { + return new(registry, registry == DockerHub ? RegistryAuthState.NotRequired : RegistryAuthState.Missing, + "远端没有 ~/.docker/config.json —— 公开仓库不受影响。"); + } + if (config.CredHelpers?.ContainsKey(registry) == true || + (!string.IsNullOrEmpty(config.CredsStore) && config.Auths?.ContainsKey(registry) == true && + string.IsNullOrEmpty(config.Auths[registry].Auth))) + { + return new(registry, RegistryAuthState.HelperOnly, + "凭据由 credential helper 保管,面板读不到。私有镜像请在终端里 docker pull。"); + } + if (config.Auths?.TryGetValue(registry, out var entry) == true && !string.IsNullOrEmpty(entry.Auth)) + { + return new(registry, RegistryAuthState.Available, "凭据来自远端 ~/.docker/config.json"); + } + return new(registry, registry == DockerHub ? RegistryAuthState.NotRequired : RegistryAuthState.Missing, + registry == DockerHub ? "公开仓库,不需要凭据。" : "config.json 里没有这个仓库的凭据。"); + } + + /// + /// 造 X-Registry-Auth 头的值;取不到凭据返回 + /// (公开镜像照样能拉)。 + /// + public async Task GetAuthHeaderAsync(string imageReference, CancellationToken cancellationToken = default) + { + var registry = ResolveRegistry(imageReference); + var config = await ReadConfigAsync(cancellationToken).ConfigureAwait(false); + if (config?.Auths is null || !config.Auths.TryGetValue(registry, out var entry)) + { + return null; + } + var username = entry.Username; + var password = entry.Password; + if (!string.IsNullOrEmpty(entry.Auth)) + { + try + { + var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(entry.Auth)); + var colon = decoded.IndexOf(':'); + if (colon > 0) + { + username = decoded[..colon]; + password = decoded[(colon + 1)..]; + } + } + catch (FormatException) + { + return null; + } + } + if (string.IsNullOrEmpty(username) && string.IsNullOrEmpty(entry.IdentityToken)) + { + return null; + } + var json = JsonSerializer.Serialize(new + { + username, + password, + serveraddress = registry, + identitytoken = string.IsNullOrEmpty(entry.IdentityToken) ? null : entry.IdentityToken + }, DockerJson.Options); + // Docker 要的是 base64url(RFC 4648 §5)—— 用标准 base64 的话, + // 带 + 或 / 的凭据会在头里被截断,表现为一个莫名其妙的 401。 + return Convert.ToBase64String(Encoding.UTF8.GetBytes(json)) + .Replace('+', '-').Replace('/', '_').TrimEnd('='); + } + + private async Task ReadConfigAsync(CancellationToken cancellationToken) + { + try + { + if (endpoint.Kind == DockerEndpointKind.Local) + { + var path = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".docker", "config.json"); + return File.Exists(path) + ? DockerJson.TryDeserialize(await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false)) + : null; + } + var home = await remoteFs.GetWorkingDirectoryAsync(endpoint.SessionId, cancellationToken).ConfigureAwait(false); + var remotePath = $"{home.TrimEnd('/')}/.docker/config.json"; + if (!await remoteFs.ExistsAsync(endpoint.SessionId, remotePath, cancellationToken).ConfigureAwait(false)) + { + return null; + } + var bytes = await remoteFs.ReadAllBytesAsync(endpoint.SessionId, remotePath, 1 << 20, cancellationToken) + .ConfigureAwait(false); + return DockerJson.TryDeserialize(Encoding.UTF8.GetString(bytes)); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + // 读不到 config 不是错误 —— 公开镜像本来就不需要它。 + return null; + } + } +} + +/// ~/.docker/config.json 里我们关心的那几项。 +internal sealed record DockerConfigFile +{ + [JsonPropertyName("auths")] + public Dictionary? Auths { get; init; } + + [JsonPropertyName("credsStore")] + public string? CredsStore { get; init; } + + [JsonPropertyName("credHelpers")] + public Dictionary? CredHelpers { get; init; } +} + +/// config.json 里一个仓库的凭据项。 +internal sealed record DockerAuthEntry +{ + [JsonPropertyName("auth")] + public string? Auth { get; init; } + + [JsonPropertyName("username")] + public string? Username { get; init; } + + [JsonPropertyName("password")] + public string? Password { get; init; } + + [JsonPropertyName("identitytoken")] + public string? IdentityToken { get; init; } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/RunCommandBuilder.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/RunCommandBuilder.cs new file mode 100644 index 0000000..7eec0c5 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/RunCommandBuilder.cs @@ -0,0 +1,143 @@ +using System.Text; + +namespace VelaShell.Plugin.DockerPanel.Docker; + +/// +/// 从一个已存在的容器反推出等价的 docker run 命令。 +/// +/// 用途是"这个容器当初是怎么起的" —— 手起的容器没有 compose 文件, +/// 而三个月后要在另一台机器上重建它时,inspect 的那一大坨 JSON 没人愿意读。 +/// +/// +/// 它是近似的,而且必须说清楚是近似的。 Engine 不保存原始命令行,只保存生效后的配置, +/// 所以推回去必然有偏差:镜像自带的 ENV 与用户 -e 传的混在一起(这里靠减去镜像的 +/// 环境变量还原)、--network 之外的网络别名、能力增删、日志驱动等等都不在面板取的那几个字段里。 +/// 因此产物末尾会附一句提醒,而不是假装它可以照抄执行。 +/// +/// +public static class RunCommandBuilder +{ + /// 拼出命令。 + /// 容器 inspect。 + /// 镜像自带的环境变量(用来把用户加的那些筛出来);拿不到时传 null。 + public static string Build(ContainerInspect inspect, IReadOnlyCollection? imageEnv) + { + var sb = new StringBuilder("docker run"); + var config = inspect.Config; + var host = inspect.HostConfig; + + // -d 是面板起容器的默认形态,也是绝大多数长期容器的形态。 + sb.Append(" -d"); + if (host?.AutoRemove == true) + { + sb.Append(" --rm"); + } + if (inspect.Name is { Length: > 0 } name) + { + sb.Append(" --name ").Append(Sh.Quote(name.TrimStart('/'))); + } + + foreach ((var port, var bindings) in Ordered(host?.PortBindings)) + { + // tcp 是 -p 的默认协议,写出来只是噪音;udp / sctp 必须留着。 + var spec = port.EndsWith("/tcp", StringComparison.Ordinal) ? port[..^4] : port; + foreach (var binding in bindings ?? []) + { + var hostPart = string.IsNullOrEmpty(binding.HostIp) + ? binding.HostPort ?? "" + : $"{binding.HostIp}:{binding.HostPort}"; + sb.Append(" -p ").Append(Sh.Quote(hostPart.Length > 0 ? $"{hostPart}:{spec}" : spec)); + } + } + + foreach (var bind in host?.Binds ?? []) + { + sb.Append(" -v ").Append(Sh.Quote(bind)); + } + // Binds 只覆盖 -v 起的那些;命名卷经 Mounts 出现,别漏掉。 + foreach (var mount in inspect.Mounts ?? []) + { + if (mount.Type != "volume" || mount.Name is not { Length: > 0 } volume) + { + continue; + } + var spec = $"{volume}:{mount.Destination}{(mount.RW ? "" : ":ro")}"; + sb.Append(" -v ").Append(Sh.Quote(spec)); + } + + foreach (var entry in UserEnv(config?.Env, imageEnv)) + { + sb.Append(" -e ").Append(Sh.Quote(entry)); + } + + if (host?.NetworkMode is { Length: > 0 } network && network != "default" && !network.StartsWith("container:", StringComparison.Ordinal)) + { + sb.Append(" --network ").Append(Sh.Quote(network)); + } + if (host?.RestartPolicy?.Name is { Length: > 0 } policy && policy != "no") + { + sb.Append(" --restart ") + .Append(policy == "on-failure" && host.RestartPolicy.MaximumRetryCount > 0 + ? $"on-failure:{host.RestartPolicy.MaximumRetryCount}" + : policy); + } + if (config?.User is { Length: > 0 } user) + { + sb.Append(" -u ").Append(Sh.Quote(user)); + } + if (config?.WorkingDir is { Length: > 0 } workdir) + { + sb.Append(" -w ").Append(Sh.Quote(workdir)); + } + if (host?.Privileged == true) + { + sb.Append(" --privileged"); + } + if (host?.Memory > 0) + { + sb.Append(" -m ").Append(host.Memory); + } + + sb.Append(' ').Append(Sh.Quote(config?.Image ?? inspect.Image ?? "")); + + // Cmd 只有在覆盖了镜像默认值时才该出现,但面板拿不到镜像的默认 Cmd 来比对, + // 所以照原样附上 —— 多写一次等价的命令,好过漏掉一个真正被覆盖过的。 + foreach (var arg in config?.Cmd ?? []) + { + sb.Append(' ').Append(Sh.Quote(arg)); + } + return sb.ToString(); + } + + /// 随命令一起复制的那句提醒。 + public const string Caveat = + "# 由面板从 inspect 反推,是近似值:Engine 不保存原始命令行。\n" + + "# 网络别名、能力增删、日志驱动、健康检查等未包含在内,执行前请核对。"; + + /// + /// 把镜像自带的环境变量减掉,只留用户真正传过的。 + /// + /// 不减的话,一条 docker run 会拖着镜像里那十几条 PATHLANG、 + /// NGINX_VERSION —— 它们不是用户写的,抄进新命令里既噪音又可能过时。 + /// + /// + internal static IEnumerable UserEnv(string[]? containerEnv, IReadOnlyCollection? imageEnv) + { + if (containerEnv is null) + { + yield break; + } + HashSet fromImage = imageEnv is null ? [] : [.. imageEnv]; + foreach (var entry in containerEnv) + { + if (!fromImage.Contains(entry)) + { + yield return entry; + } + } + } + + private static IEnumerable> Ordered( + Dictionary? bindings) => + bindings is null ? [] : bindings.OrderBy(p => p.Key, StringComparer.Ordinal); +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/SocketProbe.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/SocketProbe.cs new file mode 100644 index 0000000..9ffd537 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/SocketProbe.cs @@ -0,0 +1,136 @@ +using System.Diagnostics; +using VelaShell.PluginSdk.RemoteExec; + +namespace VelaShell.Plugin.DockerPanel.Docker; + +/// 探出来的结论。 +public enum SocketProbeKind +{ + /// 没探成(命令跑不了、超时、看不懂的输出)。 + Unknown, + + /// socket 在,而且当前账号读写得动 —— 那么连不上是别的原因。 + Ready, + + /// 那个位置上没有 socket。 + Missing, + + /// socket 在,但当前账号不在它的属组里。 + PermissionDenied +} + +/// +/// 探测结果。名字都是**具体**的,界面要拿它们直接写进句子里。 +/// +/// 结论。 +/// 当前账号名(如 joes)。 +/// socket 的属组(如 docker)。 +/// 当前账号所在的组,空格分隔。 +public sealed record SocketProbeResult( + SocketProbeKind Kind, + string Account = "", + string Group = "", + string Groups = ""); + +/// +/// 连不上的时候,替用户去问一句"到底是哪种连不上"。 +/// +/// 为什么必须由面板来问:sshd 打不开到一个 unix socket 的通道时,只回一句笼统的失败, +/// 不区分"文件不存在"和"你没权限"。而这两件事要做的完全不是一回事 —— +/// 一个是去装 Docker / 改路径,一个是去加组。分不出来,界面就只能说一句 +/// "连不上,自己去终端看看",然后把 ls -lid 两串输出丢给用户自己交叉比对。 +/// +/// +/// 探测走的是 SSH 的执行通道(远端)或本地 shell(本机),只读、不改任何东西: +/// -S 看在不在,-r/-w 看碰不碰得到,再把属组与账号所在的组带回来 —— +/// 界面据此写出"账号 joes 不在 docker 组"这样一句人话,而不是让用户去读权限位。 +/// +/// +public static class SocketProbe +{ + private static readonly SocketProbeResult Unknown = new(SocketProbeKind.Unknown); + + /// 探一次。任何异常都归到 —— 这是诊断,不该自己再抛。 + public static async Task RunAsync(IRemoteExecApi exec, DockerEndpoint endpoint, + CancellationToken cancellationToken = default) + { + try + { + var script = Script(endpoint.SocketPath); + var output = endpoint.Kind == DockerEndpointKind.Remote + ? (await exec.RunAsync(endpoint.SessionId, script, + new ExecOptions { Timeout = TimeSpan.FromSeconds(10) }, cancellationToken) + .ConfigureAwait(false)).Output + : await LocalAsync(script, cancellationToken).ConfigureAwait(false); + return Parse(output); + } + catch (Exception) + { + // 探测失败不该盖过原本那条错误 —— 界面照旧显示笼统的那一屏。 + return Unknown; + } + } + + /// + /// 一行 POSIX sh。刻意不用 ls -l 之类需要人来读的输出: + /// -r/-w 问的正是"这个账号碰不碰得到",答案是确定的。 + /// + private static string Script(string path) => + $"s={Sh.Quote(path)}; if [ -S \"$s\" ]; then if [ -r \"$s\" ] && [ -w \"$s\" ]; " + + "then echo OK; else echo \"DENIED|$(stat -c %G \"$s\" 2>/dev/null)|$(id -un)|$(id -nG)\"; fi; " + + "else echo MISSING; fi"; + + /// + /// 本机端点上自己起一个 shell。 + /// + /// Windows 上没有 /bin/sh,而命名管道也没有"属组"这回事 —— 那里探不出东西来, + /// 直接放弃比硬凑一个答案好。 + /// + /// + private static async Task LocalAsync(string script, CancellationToken cancellationToken) + { + if (OperatingSystem.IsWindows()) + { + return ""; + } + using var process = new Process + { + StartInfo = new("/bin/sh") + { + ArgumentList = { "-c", script }, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + process.Start(); + var output = await process.StandardOutput.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); + return output; + } + + private static SocketProbeResult Parse(string output) + { + var line = output.Split('\n').Select(l => l.Trim()) + .LastOrDefault(l => l.Length > 0) ?? ""; + if (line == "OK") + { + return new(SocketProbeKind.Ready); + } + if (line == "MISSING") + { + return new(SocketProbeKind.Missing); + } + if (!line.StartsWith("DENIED|", StringComparison.Ordinal)) + { + return Unknown; + } + // DENIED|属组|账号|账号所在的组 + var parts = line.Split('|'); + return new(SocketProbeKind.PermissionDenied, + parts.Length > 2 ? parts[2].Trim() : "", + parts.Length > 1 ? parts[1].Trim() : "", + parts.Length > 3 ? parts[3].Trim() : ""); + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/TarUtil.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/TarUtil.cs new file mode 100644 index 0000000..cd8786d --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/TarUtil.cs @@ -0,0 +1,221 @@ +using System.Text; + +namespace VelaShell.Plugin.DockerPanel.Docker; + +/// +/// 只够用的 tar 读写。 +/// +/// /containers/{id}/archive 收发的是 tar 流,而 .NET 自带的 +/// System.Formats.Tar 完全能用 —— 这里仍然手写,是因为面板只做**单文件**的 +/// 读与写,手写这一小段换来的是零额外依赖,以及对 Docker 那几个怪癖 +/// (目录项以 / 结尾、长度补齐到 512、两个全零块收尾)的完全掌控。 +/// +/// +internal static class TarUtil +{ + private const int BlockSize = 512; + + /// 把一个文件打成一个最小的 tar(ustar 格式)。 + /// tar 内的文件名(不带路径分隔前缀)。 + /// 文件内容。 + /// 权限位,默认 0644。 + /// 修改时间;默认取当前时间。 + public static byte[] CreateSingleFile(string name, ReadOnlySpan content, int mode = 0b110_100_100, + DateTimeOffset? modified = null) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + var header = new byte[BlockSize]; + WriteString(header, 0, 100, name); + WriteOctal(header, 100, 8, (ulong)mode); + WriteOctal(header, 108, 8, 0); // uid + WriteOctal(header, 116, 8, 0); // gid + WriteOctal(header, 124, 12, (ulong)content.Length); // size + WriteOctal(header, 136, 12, (ulong)(modified ?? DateTimeOffset.UtcNow).ToUnixTimeSeconds()); + header[156] = (byte)'0'; // typeflag: 普通文件 + WriteString(header, 257, 6, "ustar"); + header[263] = (byte)'0'; + header[264] = (byte)'0'; + // 校验和的算法要求先把校验和字段当成 8 个空格再求和 —— 顺序反了 tar 会说文件损坏。 + for (var i = 148; i < 156; i++) + { + header[i] = (byte)' '; + } + uint checksum = 0; + foreach (var b in header) + { + checksum += b; + } + WriteOctal(header, 148, 7, checksum); + header[155] = (byte)' '; + + var padded = (content.Length + BlockSize - 1) / BlockSize * BlockSize; + var result = new byte[BlockSize + padded + BlockSize * 2]; + header.CopyTo(result, 0); + content.CopyTo(result.AsSpan(BlockSize)); + return result; + } + + /// + /// 从一条 tar 流里取出第一个普通文件的内容。 + /// 找不到普通文件返回 (比如路径指的是目录)。 + /// + public static async Task<(string Name, byte[] Content)?> ReadFirstFileAsync(Stream tar, long maxBytes, + CancellationToken cancellationToken) + { + var header = new byte[BlockSize]; + while (true) + { + if (!await ReadExactlyAsync(tar, header, cancellationToken).ConfigureAwait(false)) + { + return null; + } + // 两个全零块 = 归档结束。 + if (header.All(b => b == 0)) + { + return null; + } + var name = ReadString(header, 0, 100); + var size = (long)ReadOctal(header, 124, 12); + var type = (char)header[156]; + var padded = (int)((size + BlockSize - 1) / BlockSize * BlockSize); + if (type is '0' or '\0') + { + if (size > maxBytes) + { + throw new InvalidOperationException( + $"文件 {name} 有 {size:N0} 字节,超过了面板允许在线编辑的上限 {maxBytes:N0} 字节。"); + } + var content = new byte[size]; + if (!await ReadExactlyAsync(tar, content, cancellationToken).ConfigureAwait(false)) + { + return null; + } + await SkipAsync(tar, padded - size, cancellationToken).ConfigureAwait(false); + return (name, content); + } + await SkipAsync(tar, padded, cancellationToken).ConfigureAwait(false); + } + } + + /// + /// 把 tar 流里第一个普通文件边读边写到目标流,返回写出的字节数; + /// 没有普通文件返回 0。 + /// + /// + /// 与 的分工:那一个是给在线编辑用的,要整份内容 + /// 在手上,所以有 2 MB 上限;这一个是给"另存为"用的,只是把字节搬过去 —— + /// 给它同样的上限就等于宣布几百兆的日志不许下载,而那是这个功能最常见的用途。 + /// + public static async Task ExtractFirstFileAsync(Stream tar, Stream destination, + CancellationToken cancellationToken) + { + var header = new byte[BlockSize]; + var buffer = new byte[64 * 1024]; + while (true) + { + if (!await ReadExactlyAsync(tar, header, cancellationToken).ConfigureAwait(false) + || header.All(b => b == 0)) + { + return 0; + } + var size = (long)ReadOctal(header, 124, 12); + var type = (char)header[156]; + var padded = (size + BlockSize - 1) / BlockSize * BlockSize; + if (type is not ('0' or '\0')) + { + await SkipAsync(tar, padded, cancellationToken).ConfigureAwait(false); + continue; + } + var remaining = size; + while (remaining > 0) + { + var want = (int)Math.Min(buffer.Length, remaining); + var read = await tar.ReadAsync(buffer.AsMemory(0, want), cancellationToken).ConfigureAwait(false); + if (read <= 0) + { + // 流在中途断了。已经写出去的那部分是残缺的,必须说出来 —— + // 一个静默截断的文件比一个下载失败的提示危险得多。 + throw new EndOfStreamException( + $"tar 流在读到 {size - remaining:N0} / {size:N0} 字节时结束了,文件不完整。"); + } + await destination.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false); + remaining -= read; + } + await SkipAsync(tar, padded - size, cancellationToken).ConfigureAwait(false); + return size; + } + } + + private static void WriteString(byte[] buffer, int offset, int length, string value) + { + var bytes = Encoding.UTF8.GetBytes(value); + var n = Math.Min(bytes.Length, length - 1); + Array.Copy(bytes, 0, buffer, offset, n); + } + + private static void WriteOctal(byte[] buffer, int offset, int length, ulong value) + { + var text = Convert.ToString((long)value, 8).PadLeft(length - 1, '0'); + WriteString(buffer, offset, length, text); + } + + private static string ReadString(byte[] buffer, int offset, int length) + { + var end = offset; + while (end < offset + length && buffer[end] != 0) + { + end++; + } + return Encoding.UTF8.GetString(buffer, offset, end - offset); + } + + private static ulong ReadOctal(byte[] buffer, int offset, int length) + { + ulong value = 0; + for (var i = offset; i < offset + length; i++) + { + var b = buffer[i]; + if (b is 0 or (byte)' ') + { + continue; + } + if (b is < (byte)'0' or > (byte)'7') + { + break; + } + value = value * 8 + (ulong)(b - '0'); + } + return value; + } + + private static async Task SkipAsync(Stream stream, long count, CancellationToken cancellationToken) + { + var scratch = new byte[Math.Min(count, 64 * 1024) is var n && n > 0 ? n : 1]; + var left = count; + while (left > 0) + { + var read = await stream.ReadAsync(scratch.AsMemory(0, (int)Math.Min(left, scratch.Length)), cancellationToken) + .ConfigureAwait(false); + if (read <= 0) + { + return; + } + left -= read; + } + } + + private static async Task ReadExactlyAsync(Stream stream, Memory buffer, CancellationToken cancellationToken) + { + var read = 0; + while (read < buffer.Length) + { + var n = await stream.ReadAsync(buffer[read..], cancellationToken).ConfigureAwait(false); + if (n <= 0) + { + return false; + } + read += n; + } + return true; + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/PluginMain.cs b/plugins/VelaShell.Plugin.DockerPanel/PluginMain.cs new file mode 100644 index 0000000..070a92c --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/PluginMain.cs @@ -0,0 +1,97 @@ +using VelaShell.Plugin.DockerPanel.Ui; +using VelaShell.PluginSdk; +using VelaShell.PluginSdk.Ui; + +namespace VelaShell.Plugin.DockerPanel; + +/// +/// Docker 面板插件的入口。 +/// +/// 只做三件事:注册命令、按需开面板、停用时收干净。真正的东西都在 +/// 里。 +/// +/// +[VelaPlugin] +public sealed class DockerPanelPlugin : IVelaPlugin +{ + private IPluginContext? _context; + private IPluginPanel? _panel; + private DockerPanelViewModel? _viewModel; + private IDisposable? _command; + + /// + public async Task ActivateAsync(IPluginContext context, CancellationToken cancellationToken) + { + _context = context; + // 转换器取的那些画刷要跟着宿主换肤走。订阅的是宿主 Application 的资源变更, + // 是一条从宿主指向本程序集的引用 —— 停用时必须 Detach,否则 ALC 回收不掉。 + ThemeBrushes.Attach(); + _command = context.Commands.Register(new( + "velashell.dockerpanel.open", + "Docker: 打开 Docker 管理面板", + "Docker", + _ => OpenPanelAsync())); + // 激活即打开:用户是按了命令面板里那一条才走到这里的,再让他找一次入口没有道理。 + await OpenPanelAsync().ConfigureAwait(false); + } + + /// + public async Task DeactivateAsync(CancellationToken cancellationToken) + { + _command?.Dispose(); + _command = null; + if (_panel is { } panel) + { + await panel.CloseAsync().ConfigureAwait(false); + _panel = null; + } + if (_viewModel is { } viewModel) + { + // 事件流、日志流、统计流、隧道 —— 全都挂在这个对象上,它一走远端就干净了。 + await viewModel.DisposeAsync().ConfigureAwait(false); + _viewModel = null; + } + // 撤掉挂在宿主 Application 上的资源订阅(见 ActivateAsync)。 + ThemeBrushes.Detach(); + _context = null; + } + + private async Task OpenPanelAsync() + { + if (_context is not { } context) + { + return; + } + // 已经开着就把它带到眼前 —— 再开一个重复的不对,什么都不做又像是按钮坏了。 + if (_panel is { IsOpen: true } existing) + { + await existing.ActivateAsync().ConfigureAwait(false); + return; + } + var viewModel = new DockerPanelViewModel(context); + _viewModel = viewModel; + try + { + _panel = await context.Ui.ShowPanelAsync( + new() { Title = "Docker", DisplayMode = PanelDisplayMode.Document }, + () => new DockerPanelView(viewModel), + context.Shutdown).ConfigureAwait(false); + _panel.Closed += async () => + { + _panel = null; + await viewModel.DisposeAsync(); + if (ReferenceEquals(_viewModel, viewModel)) + { + _viewModel = null; + } + }; + await viewModel.InitializeAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + context.Log.Error($"failed to open the docker panel: {ex.Message}"); + await viewModel.DisposeAsync().ConfigureAwait(false); + _viewModel = null; + } + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/README.md b/plugins/VelaShell.Plugin.DockerPanel/README.md new file mode 100644 index 0000000..cd96c8a --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/README.md @@ -0,0 +1,168 @@ +# Docker 面板 + +在 VelaShell **已经连上的 SSH 会话**上,开一个完整的 Docker 管理面板:总览、容器、镜像、 +卷、网络、Compose、系统七页,外加实时统计、日志、容器内文件编辑与内置控制台。 + +--- + +## 一、它现在怎么连(v0.2 的核心变化) + +**走 Docker Engine 的 HTTP API,而不是 `docker` CLI。** 但**服务器上什么都不用改** —— +面板经 SSH 会话开一条到远端 `/var/run/docker.sock` 的直连通道 +(SSH 的 `direct-streamlocal@openssh.com`),不需要把 daemon 暴露在 2375/2376 上。 + +| | 服务端要做什么 | 面板能拿到什么 | +| --- | --- | --- | +| `docker` CLI over SSH(v0.1) | 什么都不用 | 每个动作一次 exec;输出是**文本** | +| daemon 端口 2375 / 2376 | 暴露一个 root 等价的端口 | 完整 API,但那是个新的攻击面 | +| **SSH 隧道 + unix socket(现在)** | **什么都不用** | **完整 API,且不给主机开任何新入口** | + +这条通道也**不是**本地端口转发:转发会在本机开一个监听端口,同机的任何进程都能连上去, +而对面是一个 root 等价的 socket。隧道把流直接交给面板,本机不留入口。 + +面板同时支持**管本机 Docker**(Windows 命名管道 / unix socket),主机切换器里多一项。 + +### 这需要 SDK 1.2 + +SDK 1.1 的远程执行只有两种**文本**形态:整段 UTF-8 解码,或按 `\n` 切行回调。而 Engine API +的分块传输、`/archive` 的 tar 流、`/exec` 的 8 字节多路复用帧全是二进制 —— 用文本模型承载 +不是"慢一点",是**数据静默损坏**:UTF-8 解码把非法字节换成 U+FFFD(不可逆),按行切分在 +`0x0A` 处把一帧劈成两半。 + +所以 SDK 加了 `IRemoteTunnelApi`:到远端 unix socket / TCP 端点的裸字节双工流。 +`plugin.json` 钉了 `minSdkVersion: "2.0.0"`,装在老宿主上会在**发现期**干净地标为不兼容, +而不是运行到一半炸出 `MissingMethodException`。 + +--- + +## 二、改走 API 之后多出来的东西 + +这三样是 CLI 版做不到的,也是这次重构的理由: + +1. **行内实时统计** —— `/containers/{id}/stats` 是真流。列表每行画一条 CPU sparkline + (5 秒一轮快照,并发压在 3);详情抽屉里那**一个**容器走真正的流(每秒一帧)。 + 一条流占一个 SSH 通道,所以不能每行都开一条。 +2. **面板内控制台** —— `/exec` 的劫持连接。逐条命令、标准输出与标准错误分开收, + 输出干净且"失败"与"没有输出"区分得开。 +3. **容器内文件浏览与在线编辑** —— `/archive` 的 tar 流读写单个文件,不经 shell, + 因此不受登录 shell、引用规则与 locale 的影响。文件树上带 `docker diff` 的 A/C/D 标记。 + +--- + +## 三、面板里有什么 + +- **总览** —— 运行中容器 / CPU / 内存 / 可回收空间四张卡、CPU 趋势、占用 Top 5、 + 实时事件时间线、需要关注(不健康 / 异常退出 / 暂停 / 闲置卷)、快捷动作。 +- **容器** —— 行内 sparkline、左侧状态色条、compose 项目归属、批量操作(启动 / 停止 / + 重启 / 暂停 / 强杀 / 删除),右侧详情抽屉:概览 · 日志 · 文件 · 终端 · 统计。 +- **镜像** —— 拉取(三态对话框)、推送、打标签、删除、清理悬空 / 全部未使用,以及 + **运行容器**的完整表单(端口 / 卷 / 环境变量 / 网络 / 重启策略 / `-d` / `--rm` / + 特权 / 覆盖命令 / 额外参数)。 +- **卷** —— 新建(驱动选项 + 标签)、删除、清理未使用;详情里列出**谁在用它**。 + 「浏览卷内文件」借一个已经挂着它的运行中容器去看 —— 不为了看一眼去拉一个 alpine。 +- **网络** —— 新建(驱动 / 子网 / 网关 / internal / attachable / IPv6)、接入 / 摘除容器、 + 删除、清理未使用。内置的 `bridge` / `host` / `none` 删除按钮直接置灰。 +- **Compose** —— 三栏:项目列表 / `compose.yaml` 编辑器 + 服务表 / 执行记录。 +- **系统** —— `system df -v` 的堆叠占用条与明细表、六张回收卡片(按安全 / 注意 / 危险 / + 会丢数据分级)、引擎信息。 + +### 事件驱动,不是定时轮询 + +面板接着一条 `docker events` 流。别处起了个容器、CI 推了个镜像、某个容器 OOM 死掉 —— +界面在一秒内自己就更新了。所以**自动刷新默认是关的**;顶栏那枚绿色的「实时」徽章亮着, +才说明事件流真的接上了。 + +事件密集时会合并(350ms 窗口:`compose up` 一次能推来几十条,逐条刷新等于把远端敲成筛子); +流断掉时退化为 30 秒轮询,并且状态栏与徽章都会说清楚 —— 一块静止的、看起来正常的面板, +比明说"没连上"更糟。 + +### 日志 + +跟随是真的 `docker logs -f`。攒批更新(每 120ms 交一次),缓冲 512 KB 并**按行**截断 —— +半行开头的日志比少几行更难读。带正则搜索(命中整行着色)、时间戳单列、只看 stderr。 + +### 进度 + +三个层级:对话框内的详细进度 → 顶栏任务中心的并行任务 → 状态栏与行内的就地反馈。 + +- 拉取镜像:同一个对话框**原地换态**(填表 → 拉取中 → 完成),复用的层折成一行, + 「转入后台」把进度移交任务中心、任务继续跑。 +- 批量操作:选中条**原地变进度条**,不弹窗、不遮列表;完成后**逐个目标**判定, + 失败的单独列原因,而不是把整批说成"失败"。 +- 确定型(字节数、目标个数)画进度条,不确定型(等健康检查、等 SIGTERM)画不确定条 —— + 给后者编一个假百分比,只会让用户在 90% 处等两分钟。 + +--- + +## 四、护栏 + +破坏性操作一律走**面板内**的确认闸门(不是飘在屏幕中央的弹窗 —— 同时开着生产与测试两台 +机器时,一个不写清主机的"确定删除 3 个卷吗"是这个插件能犯的最贵的错误)。 +闸门第二行永远是**目标主机**,正文里摆的是**那几条真请求**加等价命令,不是一句概括。 + +两档: + +- **一般破坏性**(删容器、删镜像、删网络、`compose down`、多数 prune):后果说明 + 危险色按钮; +- **会丢数据**(删卷、`compose down -v`、`system prune --volumes`、覆盖容器内文件、 + 覆盖远端 compose 文件):红框 + **手打确认串**,打完之前按钮是灰的。 + +「强杀」单独一档:SIGKILL 不给进程刷缓冲、关连接的机会,文案里明说"除非它已经卡死, +否则该用停止"。 + +--- + +## 五、已知边界(有意为之) + +| 边界 | 为什么 | +| --- | --- | +| 只能管**已经连上**的 SSH 主机 | SDK v1:插件不能自行发起连接。这是契约 | +| 需要 SDK ≥ 1.2 且 `hostMode = inProcess` | 隧道交出去的是一条活的 `Stream`,跨进程代理不了;面板也要以停靠标签的身份进主窗口 | +| Compose 仍走远端的 `docker compose` CLI | **Engine API 里没有 compose 这一块** —— 它是个 CLI 插件,自己读 yml、算依赖顺序,再调一串普通 API。在面板里重写一遍注定与远端那个版本行为不一致 | +| **本机端点没有 Compose 页** | 那需要在宿主机上跑进程,不在插件的职责里 | +| 私有仓库凭据由面板**自己**读远端 `~/.docker/config.json` | `docker pull` 的凭据是 **CLI** 读出来放进 `X-Registry-Auth` 的,daemon 自己不读那个文件。凭据只在发起请求那一次读取,不落盘、不缓存、不写日志 | +| credential helper 保存的凭据取不到 | 那需要在远端跑一个 helper 可执行文件。界面上会说清楚,私有镜像请先在终端里 `docker pull` | +| 控制台是**逐条命令**,不是终端仿真器 | `vim` / `top` 这类要伪终端的程序跑不了 —— 那是终端标签的活儿,面板不假装自己能干 | +| 在线编辑只认文本,单文件上限 2 MB | 把一个 ELF 塞进文本编辑器,点一次保存就毁了,而"毁了"要等容器下次启动失败才被发现 | +| 界面只有中文 | v0.1 的双语文案表没有迁过来 | +| 没有 `docker login` | 口令走命令行会留在进程表与 shell 历史里。私有仓库请先在终端标签里登录 | +| Windows 容器未验证 | 命令与解析按 Linux 写(`ls` 的列格式尤其) | + +--- + +## 六、验证清单(界面行为靠手测) + +单测覆盖协议解析(帧、tar、`ls`)、批量判定、进度聚合、行合并、确认闸门与表单校验; +**界面行为**没法在没有宿主的进程里可信地验证,请在真机上过一遍: + +1. 没有已连接会话时打开面板 → "选一条已连接的 SSH 会话开始",并给出「管理本机 Docker」; +2. 连一台**没装 docker** 的机器 → "找不到 docker.sock",而不是一片空列表; +3. 连一台当前账号不在 `docker` 组的机器 → 明确给出两条出路; +4. 选中多个容器 → 停止 → 选中条变进度条 → 结果逐个目标判定,失败的列出原因; +5. 打开日志 + 跟随 → 新日志即时出现,没有重复行、没有半行;关掉面板 → 远端不再有本插件的通道; +6. 拉一个大镜像 → 逐层进度 → 点「转入后台」→ 对话框关掉但顶栏任务中心继续走; +7. 删卷 → 必须手打 `delete` 才能按下确认;确认框第二行写着目标主机; +8. 编辑容器内文件 → 保存 → 必须手打 `save`;保存后文件树上出现 `C` 标记; +9. 编辑 compose 文件 → 保存 → 必须手打 `save`;`config` 能解析; +10. 切明暗主题 → 面板配色立即跟随(全部走 `{DynamicResource Vela*}`)。 + +--- + +## 七、代码地图 + +| 文件 | 内容 | +| --- | --- | +| `PluginMain.cs` | 入口:注册命令、开 / 激活面板、停用时收掉视图模型 | +| `Docker/DockerTransport.cs` | 两种传输:SSH 隧道 / 本机 socket-管道 | +| `Docker/DockerClient*.cs` | Engine API 客户端,按域拆 partial | +| `Docker/DockerFrames.cs` | 8 字节多路复用帧解码 + 跨帧 UTF-8 与半行处理 | +| `Docker/DockerRawHttp.cs` | 劫持端点(exec / attach)的手写 HTTP/1.1 | +| `Docker/TarUtil.cs` | 单文件 tar 读写(`/archive`) | +| `Docker/RegistryAuth.cs` | 读远端 `~/.docker/config.json` 造 `X-Registry-Auth` | +| `Docker/ComposeCli.cs` | Compose 经远程执行下发 | +| `Docker/BatchRunner.cs` | 批量操作的逐目标判定 | +| `Ui/DockerPanelViewModel*.cs` | 外壳:端点、导航、事件驱动刷新、对话框宿主 | +| `Ui/ConfirmGate.cs` | 两档确认闸门 | +| `Ui/TaskCenter.cs` | 任务中心与拉取进度聚合 | +| `Ui/PanelForm.cs` · `Forms.cs` · `RunContainerForm.cs` | 声明式表单与各具体表单 | +| `Ui/Pages/*` | 七个页面 + 详情抽屉 + 日志 / 文件 / 控制台 | +| `Ui/DockerTheme.axaml` · `DockerStyles.axaml` | 私有图标与控件主题、通用版式 | diff --git a/plugins/VelaShell.Plugin.DockerPanel/Syntax/DotEnv.xshd b/plugins/VelaShell.Plugin.DockerPanel/Syntax/DotEnv.xshd new file mode 100644 index 0000000..2e2c304 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Syntax/DotEnv.xshd @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + ^\s*\[\[?[^\]]*\]\]? + + + + + ^\s*[A-Za-z_][A-Za-z0-9_.\-]*(?=\s*[=:]) + + + + " + " + + + + + + + ' + ' + + + + true + false + yes + no + on + off + none + null + enabled + disabled + + + + \b[0-9]+([.:][0-9]+)*\b + + + + [=:] + + + + diff --git a/plugins/VelaShell.Plugin.DockerPanel/Syntax/Yaml.xshd b/plugins/VelaShell.Plugin.DockerPanel/Syntax/Yaml.xshd new file mode 100644 index 0000000..d80a10d --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Syntax/Yaml.xshd @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + ^(---|\.\.\.)\s*$ + + + + + ^\s*(-\s+)?[^\s#][^:#]*(?=:(\s|$)) + + + + ' + ' + + + + " + " + + + + + + + + [&*][A-Za-z0-9_-]+|!!?[A-Za-z0-9_/:.-]+ + + + + true + false + True + False + TRUE + FALSE + yes + no + on + off + null + Null + NULL + ~ + + + + \b[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?\b + + + + + [|>][-+]?\s*$|[\[\]{},] + + + + diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/AutoScroll.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/AutoScroll.cs new file mode 100644 index 0000000..cae36ef --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/AutoScroll.cs @@ -0,0 +1,138 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Threading; +using Avalonia.VisualTree; +using System.Collections.Specialized; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// +/// 让一份滚动内容跟着集合的新条目往下走。 +/// +/// 规矩只有一条:用户自己滚上去看东西的时候不许抢。所以"跟不跟"不是一个设置, +/// 而是从用户最近一次滚动推出来的 —— 停在底部就继续跟,往上翻了就停住, +/// 再滚回底部又接着跟。日志窗口都是这个约定,只是通常没人把它写下来。 +/// +/// +/// 判断"是不是用户滚的"看 :内容变长会带来 +/// ExtentDelta,而用户拖滚动条只有 OffsetDelta。把两者分开, +/// 才不会把"新日志把视口顶上去"误当成"用户翻上去了"。 +/// +/// +/// 挂在哪:可以是 自己,也可以是**里面**有一个滚动条的控件 +/// (虚拟化的列表就是这种 —— 滚动条在它自己的模板里,外面套不得)。 +/// +/// +public static class AutoScroll +{ + private static readonly Dictionary Watchers = []; + + /// 要跟的集合。绑上去就开始跟。 + public static readonly AttachedProperty SourceProperty = + AvaloniaProperty.RegisterAttached("Source", typeof(AutoScroll)); + + static AutoScroll() => SourceProperty.Changed.AddClassHandler(OnSourceChanged); + + /// + public static INotifyCollectionChanged? GetSource(Control control) => control.GetValue(SourceProperty); + + /// + public static void SetSource(Control control, INotifyCollectionChanged? value) => + control.SetValue(SourceProperty, value); + + private static void OnSourceChanged(Control control, AvaloniaPropertyChangedEventArgs e) + { + if (Watchers.Remove(control, out var previous)) + { + previous.Detach(); + } + if (e.NewValue is INotifyCollectionChanged source) + { + Watchers[control] = new(control, source); + } + } + + /// 一个控件上的一次"跟随"。状态是每个控件各自的,所以做成实例而不是静态表。 + private sealed class Watcher + { + private readonly Control _control; + private readonly INotifyCollectionChanged _source; + private ScrollViewer? _scroll; + // 一接上就是"在底部":此时还没有内容,用户也还没表达过任何意图。 + private bool _sticky = true; + + internal Watcher(Control control, INotifyCollectionChanged source) + { + _control = control; + _source = source; + source.CollectionChanged += OnCollectionChanged; + control.AttachedToVisualTree += OnAttached; + Bind(); + } + + internal void Detach() + { + _source.CollectionChanged -= OnCollectionChanged; + _control.AttachedToVisualTree -= OnAttached; + if (_scroll is { } scroll) + { + scroll.ScrollChanged -= OnScrollChanged; + _scroll = null; + } + } + + private void OnAttached(object? sender, VisualTreeAttachmentEventArgs e) => Bind(); + + /// + /// 找到那个真正在滚的 ScrollViewer。 + /// + /// 虚拟化列表的滚动条长在它自己的模板里,模板套上之前找不着 —— 所以这里 + /// 每次要用的时候再找,而不是找不到就排一个任务下次再找: + /// 那个控件可能压根不会显示(收起来的页签),自我重排的任务就成了一个永不结束的循环。 + /// + /// + private ScrollViewer? Bind() + { + if (_scroll is not null) + { + return _scroll; + } + _scroll = _control as ScrollViewer ?? _control.FindDescendantOfType(); + if (_scroll is { } found) + { + found.ScrollChanged += OnScrollChanged; + } + return _scroll; + } + + private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + // 清屏(Reset)也要回到底,否则视口会停在一个已经不存在的位置上。 + if (e.Action is NotifyCollectionChangedAction.Add or NotifyCollectionChangedAction.Reset && _sticky) + { + ScrollLater(); + } + } + + private void OnScrollChanged(object? sender, ScrollChangedEventArgs e) + { + if (_scroll is not { } scroll) + { + return; + } + // 内容变长带来的滚动不算用户意图 —— 只有 Offset 单独动了才是他自己滚的。 + if (e.ExtentDelta.Y != 0 || e.OffsetDelta.Y == 0) + { + return; + } + // 留一行的余量:滚动条很少停在恰好 0 的位置上。 + _sticky = scroll.Extent.Height - scroll.Viewport.Height - scroll.Offset.Y <= 24; + } + + /// + /// 排到布局之后再滚:集合刚变,新的那几行还没测量,这一刻的 Extent 还是旧的。 + /// + private void ScrollLater() => + Dispatcher.UIThread.Post(() => Bind()?.ScrollToEnd(), DispatcherPriority.Background); + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/CodeEditor.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/CodeEditor.cs new file mode 100644 index 0000000..aab1eb5 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/CodeEditor.cs @@ -0,0 +1,336 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using AvaloniaEdit; +using AvaloniaEdit.Highlighting; +using AvaloniaEdit.Highlighting.Xshd; +using System.Xml; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// 这块文本按哪种语法着色。 +public enum CodeLanguage +{ + /// 不着色(纯文本)。 + None, + + /// YAML —— compose.yaml 与 config 展开。 + Yaml, + + /// .env + DotEnv +} + +/// +/// 带语法高亮的文本框。 +/// +/// 里面是 AvaloniaEdit 的 ,但外面只露出四个属性 —— +/// compose 页有三处要它(compose.yaml、.env、config 展开),各自的差别只有 +/// "什么语言、能不能改、要不要折行"。把 TextEditor 直接写进 AXAML 的话, +/// 这三处就得各抄一遍主题、字体、行号与配色的接线。 +/// +/// +/// 为什么不是 TextBox:TextBox 的整块文本共用一个 Foreground, +/// 按 token 着色它给不了。而 compose.yaml 恰恰是"缩进错一格就全错"的格式, +/// 高亮在这里不是装饰,是校对工具。 +/// +/// +public sealed class CodeEditor : UserControl +{ + private static readonly Lock RegistrationGate = new(); + private static bool _registered; + + private readonly TextEditor _editor; + + /// 应用级资源变更(= 宿主换了配色)的处理器;进出可视树时挂/摘。 + private readonly EventHandler _onAppResourcesChanged; + private bool _syncing; + + /// 文本。双向 —— 编辑器里改了要回到视图模型。 + public static readonly StyledProperty TextProperty = + AvaloniaProperty.Register(nameof(Text), + defaultBindingMode: Avalonia.Data.BindingMode.TwoWay); + + /// 按哪种语法着色。 + public static readonly StyledProperty LanguageProperty = + AvaloniaProperty.Register(nameof(Language)); + + /// 只读(config 展开是 compose 算出来的,改它没有意义)。 + public static readonly StyledProperty IsReadOnlyProperty = + AvaloniaProperty.Register(nameof(IsReadOnly)); + + /// 自动折行。 + public static readonly StyledProperty WordWrapProperty = + AvaloniaProperty.Register(nameof(WordWrap)); + + /// 显示行号。 + public static readonly StyledProperty ShowLineNumbersProperty = + AvaloniaProperty.Register(nameof(ShowLineNumbers), true); + + /// 建编辑器。 + public CodeEditor() + { + _editor = new() + { + Background = Brushes.Transparent, + BorderThickness = new(0), + Padding = new(10, 8), + ShowLineNumbers = true, + HorizontalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Auto, + VerticalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Auto + }; + _editor.Options.IndentationSize = 2; + _editor.Options.ConvertTabsToSpaces = true; + _editor.Options.HighlightCurrentLine = true; + // 关掉链接识别。AvaloniaEdit 默认会把 http:// 与邮箱地址画成"超链接": + // 一个写死的蓝色加下划线,既不认主题也不认语法配色 —— 深色底上那个蓝几乎读不出来, + // 而在一份 compose.yaml 里,ZO_ROOT_USER_EMAIL 的值本来就只是一个字符串, + // 把它做成可点的链接既没用又抢眼。 + _editor.Options.EnableHyperlinks = false; + _editor.Options.EnableEmailHyperlinks = false; + _editor.TextChanged += (_, _) => + { + if (_syncing) + { + return; + } + _syncing = true; + SetCurrentValue(TextProperty, _editor.Text); + _syncing = false; + }; + Content = _editor; + Focusable = false; + // 编辑器的配色是**一次性取值**的(xshd 高亮定义要的是 Color 不是 Brush, + // 挂不了 DynamicResource),所以换肤时必须有人来叫醒它。 + // + // 只挂 ActualThemeVariantChanged 是不够的 —— 那只在明↔暗之间翻转时才响。 + // 宿主有十二套主题,VelaDark → Tokyo Night 是**同一个变体**内部的换肤: + // 变体没变,颜色全变了,这个事件一声不吭。 + // 应用级资源字典的变更覆盖全部三种情形(换具名主题、跟随系统翻转、改强调色), + // 是这里唯一靠得住的信号。 + ActualThemeVariantChanged += (_, _) => ApplyLanguage(); + _onAppResourcesChanged = (_, _) => ApplyLanguage(); + } + + /// + public string? Text + { + get => GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + /// + public CodeLanguage Language + { + get => GetValue(LanguageProperty); + set => SetValue(LanguageProperty, value); + } + + /// + public bool IsReadOnly + { + get => GetValue(IsReadOnlyProperty); + set => SetValue(IsReadOnlyProperty, value); + } + + /// + public bool WordWrap + { + get => GetValue(WordWrapProperty); + set => SetValue(WordWrapProperty, value); + } + + /// + public bool ShowLineNumbers + { + get => GetValue(ShowLineNumbersProperty); + set => SetValue(ShowLineNumbersProperty, value); + } + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == TextProperty && !_syncing) + { + _syncing = true; + var incoming = Text ?? ""; + // 只在真不一样时才写:同一份文本写回去会把光标顶到开头, + // 用户打一个字就跳一次。 + if (_editor.Text != incoming) + { + _editor.Text = incoming; + } + _syncing = false; + } + else if (change.Property == LanguageProperty || change.Property == ForegroundProperty || + change.Property == FontFamilyProperty || change.Property == FontSizeProperty) + { + ApplyLanguage(); + } + else if (change.Property == IsReadOnlyProperty) + { + _editor.IsReadOnly = IsReadOnly; + } + else if (change.Property == WordWrapProperty) + { + _editor.WordWrap = WordWrap; + } + else if (change.Property == ShowLineNumbersProperty) + { + _editor.ShowLineNumbers = ShowLineNumbers; + } + } + + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + if (Application.Current is { } app) + { + app.ResourcesChanged += _onAppResourcesChanged; + } + ApplyLanguage(); + } + + /// + /// + /// 订阅挂在 Application.Current 上(活得比本控件久),不摘就是一条把整个面板 + /// 钉在内存里的引用 —— 插件停用后 ALC 也就回收不掉了。 + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + if (Application.Current is { } app) + { + app.ResourcesChanged -= _onAppResourcesChanged; + } + base.OnDetachedFromVisualTree(e); + } + + /// 把语言、字体与当前主题的配色一起落到编辑器上。 + private void ApplyLanguage() + { + _editor.FontFamily = FontFamily; + _editor.FontSize = FontSize; + if (Foreground is { } foreground) + { + _editor.Foreground = foreground; + } + if (Resource("VelaAccent") is IBrush caret) + { + _editor.TextArea.Caret.CaretBrush = caret; + } + if (Resource("VelaAccentDim") is ISolidColorBrush selection) + { + _editor.TextArea.SelectionBrush = selection; + _editor.TextArea.SelectionBorder = null; + } + if (Resource("VelaTextTertiary") is IBrush lineNumbers) + { + _editor.LineNumbersForeground = lineNumbers; + } + _editor.SyntaxHighlighting = Definition(Language) is { } definition ? Recolor(definition) : null; + } + + /// + /// 按当前主题给定义重着色。 + /// + /// xshd 里写死的是 Dracula 的暗色值;亮色主题下那几个浅黄浅绿压在奶白底上等于隐形。 + /// 所以颜色的**唯一事实来源**是主题字典里的 DockerCode* 令牌,xshd 只负责"哪一段算什么角色"。 + /// + /// + /// 定义是 里的全局单例,重着色改的是同一个对象 —— + /// 这正是想要的:主题一切,下次渲染就跟着变。 + /// + /// + private IHighlightingDefinition Recolor(IHighlightingDefinition definition) + { + foreach (var color in definition.NamedHighlightingColors) + { + if (Role(color.Name) is { } token && Resource(token) is ISolidColorBrush brush) + { + color.Foreground = new SimpleHighlightingBrush(brush.Color); + } + } + return definition; + } + + /// + /// xshd 里的角色名 → 宿主令牌名。 + /// + /// 一律指宿主的**终端十六色**那一族(VelaShell*):那正是各主题为"代码/日志里的 + /// 语法着色"准备的一套语义色,逐主题派生。面板自己曾经按明暗写死过两套 Dracula / + /// Alucard 的值,结果是十二套主题只有两套语法色 —— 底色换了,字没换。 + /// + /// + private static string? Role(string name) => name switch + { + "Comment" => "VelaShellSubtle", + "String" => "VelaShellYellow", + "Number" or "Constant" => "VelaShellBlue", + "Key" => "VelaShellCyan", + "Section" => "VelaShellGreen", + "Punctuation" => "VelaShellMagenta", + "Variable" => "VelaWarning", + _ => null + }; + + private object? Resource(string key) => + this.TryFindResource(key, ActualThemeVariant, out var value) ? value : null; + + /// + /// 取一份语法定义。 + /// + /// 定义随插件自带(Syntax/*.xshd 的嵌入资源),注册名带 Docker. 前缀: + /// 是**整个进程共享**的,宿主自己也往里注册 + /// "YAML"、"Ini" —— 不加前缀就会互相顶掉,而且谁先跑起来是不确定的。 + /// + /// + private static IHighlightingDefinition? Definition(CodeLanguage language) + { + if (language == CodeLanguage.None) + { + return null; + } + EnsureRegistered(); + return HighlightingManager.Instance.GetDefinition(language == CodeLanguage.Yaml + ? "Docker.Yaml" + : "Docker.DotEnv"); + } + + private static void EnsureRegistered() + { + lock (RegistrationGate) + { + if (_registered) + { + return; + } + _registered = true; + var assembly = typeof(CodeEditor).Assembly; + foreach ((var resource, var extensions) in + new[] + { + ("VelaShell.Plugin.DockerPanel.Syntax.Yaml.xshd", new[] { ".yaml", ".yml" }), + ("VelaShell.Plugin.DockerPanel.Syntax.DotEnv.xshd", [".env"]) + }) + { + try + { + using var stream = assembly.GetManifestResourceStream(resource); + if (stream is null) + { + continue; + } + using var reader = XmlReader.Create(stream); + var definition = HighlightingLoader.Load(reader, HighlightingManager.Instance); + HighlightingManager.Instance.RegisterHighlighting(definition.Name, extensions, definition); + } + catch (Exception) + { + // 一份定义坏掉不该让编辑器打不开 —— 那一种退化成纯文本,其余照常。 + } + } + } + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/ColumnResizer.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/ColumnResizer.cs new file mode 100644 index 0000000..d813a6a --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/ColumnResizer.cs @@ -0,0 +1,150 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; +using System.Globalization; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// +/// 列头上的列宽拖拽。四张列表(容器 / 镜像 / 卷 / 网络)共用这一份。 +/// +/// 做法与宿主的文件浏览器一致:每列右边一条 宽的轨道, +/// 轨道的 Tag 说明它改的是哪一列;按下时**把当前全部列宽快照下来**, +/// 位移一律按"当前指针 − 按下时指针"算,而不是一帧一帧累加 —— +/// 累加会在列宽被夹住(碰到上下限)之后跑偏,指针和界线就再也对不上了。 +/// +/// +/// 指针捕获在**视图**上而不是那条 6px 的窄轨道上:拖出轨道之外仍然收得到移动。 +/// 移动与抬起因此挂在视图根上(隧道式),而不是挂在轨道自己身上。 +/// +/// +public sealed class ColumnResizer +{ + private readonly Control _host; + private readonly Grid _header; + private readonly double _chrome; + private readonly Dictionary _startWidths = []; + private string? _active; + private double _startX; + + /// 接上一张列表的列头。 + /// 视图根(指针捕获在它身上)。 + /// 列头那张 Grid 的名字。 + /// 列宽之外那些**不可拖**的固定占位之和(勾选框、状态色条、行尾动作…)。 + public ColumnResizer(Control host, string headerName, double chrome) + { + _host = host; + _header = host.GetControl(headerName); + _chrome = chrome; + // 轨道在 XAML 里只写 Classes 与 Tag,事件在这里挂 —— 四个页面就不必各抄一遍转发方法。 + foreach (var grip in _header.Children.OfType().Where(b => b.Tag is string)) + { + grip.PointerPressed += OnPressed; + grip.PointerReleased += OnReleased; + } + host.AddHandler(InputElement.PointerMovedEvent, OnMoved, RoutingStrategies.Tunnel); + host.AddHandler(InputElement.PointerReleasedEvent, OnReleased, RoutingStrategies.Tunnel); + } + + private ListColumns? Columns => (_host.DataContext as PageViewModel)?.ColumnLayout; + + private void OnPressed(object? sender, PointerPressedEventArgs e) + { + if (sender is not Border { Tag: string key } grip || Columns is not { } columns) + { + return; + } + // 双击 = 按内容自适应,与宿主文件浏览器一致。 + if (e.ClickCount >= 2) + { + AutoFit(columns, key, grip); + e.Handled = true; + return; + } + _active = key; + _startX = e.GetPosition(_host).X; + _startWidths.Clear(); + foreach (var other in columns.Keys) + { + _startWidths[other] = columns.Get(other); + } + e.Pointer.Capture(_host); + e.Handled = true; + } + + private void OnMoved(object? sender, PointerEventArgs e) + { + if (_active is not { } key || Columns is not { } columns || + !_startWidths.TryGetValue(key, out var start)) + { + return; + } + var delta = e.GetPosition(_host).X - _startX; + columns.Set(key, Math.Clamp(start + delta, columns.Min(key), Max(columns, key))); + e.Handled = true; + } + + private void OnReleased(object? sender, PointerReleasedEventArgs e) + { + if (_active is null) + { + return; + } + _active = null; + e.Pointer.Capture(null); + e.Handled = true; + } + + /// + /// 一列最宽能到哪:剩下那些列、每条轨道与行首行尾的固定占位都得留出来, + /// 否则一拖就把行尾那几颗动作按钮挤出可视区。 + /// + private double Max(ListColumns columns, string key) + { + var others = columns.Keys.Where(k => k != key).Sum(columns.Get); + var available = _header.Bounds.Width > 0 ? _header.Bounds.Width : _host.Bounds.Width; + return Math.Max(columns.Min(key), + available - others - (columns.Keys.Count * ListColumns.TrackWidth) - _chrome); + } + + /// + /// 双击轨道:把这一列收放到"正好装得下当前这些行"。 + /// + /// 量的是当前**筛出来的**那些行,不是全部 —— 用户看得见的就是这些, + /// 为了一行被筛掉的超长文本把列撑开,反而看不成。 + /// + /// + private void AutoFit(ListColumns columns, string key, Border grip) + { + if (_host.DataContext is not PageViewModel page) + { + return; + } + var size = Resource(key is "name" or "repo" ? "VelaFontSize12" : "VelaFontSize11") as double? ?? 11; + Typeface mono = new(Resource("VelaUiMonoFont") as FontFamily ?? FontFamily.Default); + Typeface ui = new(FontFamily.Default); + // 表头那几个字也要装得下。文字直接从列头里读,免得把同一串标题在代码里再写一遍。 + var widest = Measure(HeaderText(grip), ui, size); + foreach (var text in page.ColumnTexts(key)) + { + widest = Math.Max(widest, Measure(text, key is "name" or "repo" ? ui : mono, size)); + } + columns.Set(key, Math.Clamp(widest + columns.Padding(key), + columns.Min(key), Math.Min(columns.MaxAutoFit(key), Max(columns, key)))); + } + + private string HeaderText(Border grip) => + _header.Children.OfType() + .FirstOrDefault(t => Grid.GetColumn(t) == Grid.GetColumn(grip) - 1)?.Text ?? ""; + + private static double Measure(string text, Typeface typeface, double size) => + text.Length == 0 + ? 0 + : new FormattedText(text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, + typeface, size, Brushes.Black).Width; + + private object? Resource(string key) => + _host.TryFindResource(key, (_host as StyledElement)?.ActualThemeVariant, out var value) ? value : null; +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/CommandPalette.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/CommandPalette.cs new file mode 100644 index 0000000..eb9ec7d --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/CommandPalette.cs @@ -0,0 +1,234 @@ +using System.Collections.ObjectModel; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// 命令面板里的一条。 +/// 分组标题(动作 / 容器 / 镜像·卷 / 面板命令)。 +/// 主文字。 +/// 右侧小字。 +/// 图标资源键。 +/// 语气(破坏性的那几条用危险色)。 +/// 破坏性:标题带省略号,选中后仍走确认闸门。 +/// 执行。 +public sealed record PaletteEntry( + string Group, + string Title, + string Detail, + string Icon, + RowTone Tone, + bool Destructive, + Func Run); + +/// 命令面板里的一行(含分组头的显示状态)。 +public sealed class PaletteItem(PaletteEntry entry) : ObservableObject +{ + + /// 底层条目。 + public PaletteEntry Entry { get; } = entry; + + /// 分组标题。 + public string Group => Entry.Group; + + /// 主文字。 + public string Title => Entry.Title; + + /// 右侧小字。 + public string Detail => Entry.Detail; + + /// 图标。 + public string Icon => Entry.Icon; + + /// 语气。 + public RowTone Tone => Entry.Tone; + + /// 这一条上面要不要画分组头。 + public bool ShowGroup { get; set; } + + /// 分组里有几条(分组头右侧那个数字)。 + public int GroupCount { get; set; } + + /// 键盘选中的那一条。 + public bool Active + { + get; + set => SetField(ref field, value); + } +} + +/// +/// 面板内的命令面板(Ctrl+K)。 +/// +/// 它在面板里,不劫持宿主的命令面板。 宿主自己有一个 Ctrl+K, +/// 那一个管的是会话、设置、插件;这一个管的是当前这台 Docker 主机上的容器、镜像、卷。 +/// 两者搜的是不同的东西,合并成一个只会让两边都变难用。 +/// +/// +/// 破坏性的条目标题带省略号,并且**选中后仍然走确认闸门** —— 命令面板是个加速器, +/// 不是绕过闸门的后门。 +/// +/// +public sealed class CommandPalette : ObservableObject +{ + private readonly Func> _collect; + private List _all = []; + private int _selectedIndex; + + /// 建面板。 + /// 打开时去收集当前可用的条目。 + public CommandPalette(Func> collect) + { + _collect = collect; + CloseCommand = new RelayCommand(_ => + { + IsOpen = false; + return Task.CompletedTask; + }); + RunSelectedCommand = new RelayCommand(_ => RunSelectedAsync()); + RunCommand = new RelayCommand(p => p is PaletteItem item ? RunAsync(item) : Task.CompletedTask); + } + + /// 开着没。 + public bool IsOpen + { + get; + private set => SetField(ref field, value); + } + + /// 搜索词。 + public string Query + { + get; + set + { + if (SetField(ref field, value)) + { + ApplyFilter(); + } + } + } = ""; + + /// 过滤后的条目。 + public ObservableCollection Items { get; } = []; + + /// 一条都没匹配上。 + public bool IsEmpty => Items.Count == 0; + + /// 当前主机名(面板顶部显示 —— 命令作用在哪台机器上必须写清)。 + public string HostName { get; private set; } = ""; + + /// 关闭。 + public RelayCommand CloseCommand { get; } + + /// 执行当前选中的那条。 + public RelayCommand RunSelectedCommand { get; } + + /// 执行指定的那条(鼠标点击)。 + public RelayCommand RunCommand { get; } + + /// 打开面板并收集条目。 + public void Open(string hostName) + { + HostName = hostName; + _all = [.. _collect()]; + Query = ""; + ApplyFilter(); + OnPropertyChanged(nameof(HostName)); + IsOpen = true; + } + + /// 上下移动选中项。 + public void Move(int delta) + { + if (Items.Count == 0) + { + return; + } + // 环绕:到底了按下一次回到第一条,比停在那里不动更符合预期。 + var next = (_selectedIndex + delta + Items.Count) % Items.Count; + Select(next); + } + + /// + /// Tab 补全:把选中那条的标题填进搜索框。 + /// + /// 补的是标题里去掉省略号的部分 —— 省略号代表"还会再问你一次", + /// 把它补进搜索框只会让下一次匹配失败。 + /// + /// + public void Complete() + { + if (Items.Count == 0) + { + return; + } + Query = Items[_selectedIndex].Title.TrimEnd('…'); + } + + private void Select(int index) + { + for (var i = 0; i < Items.Count; i++) + { + Items[i].Active = i == index; + } + _selectedIndex = index; + } + + private async Task RunSelectedAsync() + { + if (Items.Count == 0) + { + return; + } + await RunAsync(Items[_selectedIndex]).ConfigureAwait(true); + } + + private async Task RunAsync(PaletteItem item) + { + // 先关面板再执行:被触发的动作多半会自己弹确认闸门或表单, + // 让命令面板压在它上面既难看也点不到。 + IsOpen = false; + await item.Entry.Run().ConfigureAwait(true); + } + + private void ApplyFilter() + { + Items.Clear(); + var needle = Query.Trim(); + List matched = needle.Length == 0 + ? [.. _all] + : [.. _all.Where(e => Matches(e, needle)).OrderByDescending(e => Score(e, needle))]; + + string? group = null; + foreach (var entry in matched) + { + var item = new PaletteItem(entry); + if (entry.Group != group) + { + item.ShowGroup = true; + item.GroupCount = matched.Count(e => e.Group == entry.Group); + group = entry.Group; + } + Items.Add(item); + } + OnPropertyChanged(nameof(IsEmpty)); + if (Items.Count > 0) + { + Select(0); + } + } + + private static bool Matches(PaletteEntry entry, string needle) => + entry.Title.Contains(needle, StringComparison.OrdinalIgnoreCase) + || entry.Detail.Contains(needle, StringComparison.OrdinalIgnoreCase); + + /// + /// 排序权重。标题前缀命中 > 标题包含 > 只在小字里命中。 + /// + /// 输入 res 时,"重启 nginx-proxy" 该排在"名字里恰好带 res 的某个卷"前面。 + /// + /// + private static int Score(PaletteEntry entry, string needle) => + entry.Title.StartsWith(needle, StringComparison.OrdinalIgnoreCase) ? 3 + : entry.Title.Contains(needle, StringComparison.OrdinalIgnoreCase) ? 2 + : 1; +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/CommandPaletteView.axaml b/plugins/VelaShell.Plugin.DockerPanel/Ui/CommandPaletteView.axaml new file mode 100644 index 0000000..bc1847b --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/CommandPaletteView.axaml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/CommandPaletteView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/CommandPaletteView.axaml.cs new file mode 100644 index 0000000..5e56b4c --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/CommandPaletteView.axaml.cs @@ -0,0 +1,62 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Markup.Xaml; +using Avalonia.Threading; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// +/// 命令面板的视图。 +/// +/// 代码后置只做键盘:上下移动、回车执行、Tab 补全、Esc 关闭。 +/// 这四个键必须在**输入框**上处理 —— 用户打开面板之后手就没离开过键盘, +/// 让他为了选一条命令去摸鼠标,这个功能就白做了。 +/// +/// +public sealed partial class CommandPaletteView : UserControl +{ + /// 建视图。 + public CommandPaletteView() + { + AvaloniaXamlLoader.Load(this); + // 打开即聚焦到输入框:面板是被 Ctrl+K 唤出来的,那一刻用户已经在打字了。 + AttachedToVisualTree += (_, _) => + Dispatcher.UIThread.Post(() => this.FindControl("QueryBox")?.Focus()); + } + + /// + protected override void OnKeyDown(KeyEventArgs e) + { + if (DataContext is not CommandPalette palette) + { + base.OnKeyDown(e); + return; + } + switch (e.Key) + { + case Key.Down: + palette.Move(1); + e.Handled = true; + break; + case Key.Up: + palette.Move(-1); + e.Handled = true; + break; + case Key.Enter: + palette.RunSelectedCommand.Execute(null); + e.Handled = true; + break; + case Key.Tab: + palette.Complete(); + e.Handled = true; + break; + case Key.Escape: + palette.CloseCommand.Execute(null); + e.Handled = true; + break; + default: + base.OnKeyDown(e); + break; + } + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/ConfirmGate.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/ConfirmGate.cs new file mode 100644 index 0000000..a2a1962 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/ConfirmGate.cs @@ -0,0 +1,313 @@ +using System.Collections.ObjectModel; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// 确认闸门的档位。 +public enum ConfirmTier +{ + /// + /// 一般破坏性:删容器、删镜像、删网络、compose down、多数 prune。 + /// 后果说明 + 危险色按钮,点一下就执行。 + /// + Destructive, + + /// + /// 会丢数据:删卷、compose down -vsystem prune --volumes、 + /// 覆盖容器内文件或远端 compose 文件。额外要求**手打确认串**,与删仓库同款 —— + /// 因为后果同款。 + /// + DataLoss +} + +/// 后果说明的一条。 +/// 0=中性 1=好消息 2=注意 3=危险,决定图标与颜色。 +/// 一句话。 +public readonly record struct ConfirmConsequence(int Severity, string Text); + +/// 确认框里列出的一个目标。 +/// 名字。 +/// 中间那段小字(镜像、驱动)。 +/// 右侧状态。 +/// 是否在运行(决定左侧圆点的颜色)。 +public readonly record struct ConfirmTarget(string Name, string Meta, string Status, bool Running); + +/// 一次确认请求。 +public sealed record ConfirmRequest +{ + /// 标题,如“删除 2 个容器?”。 + public required string Title { get; init; } + + /// 标题左侧的图标资源键。 + public string Icon { get; init; } = "Icon.trash-2"; + + /// 档位。 + public ConfirmTier Tier { get; init; } = ConfirmTier.Destructive; + + /// + /// 目标主机名。一行都不能省 —— 同时开着生产与测试两台机器时, + /// 一个不写清主机的“确定删除 3 个卷吗”是这个面板能犯的最贵的错误。 + /// + public required string HostName { get; init; } + + /// 主机名后面那行小字(地址、传输方式)。 + public string HostDetail { get; init; } = ""; + + /// 主机小字是否用警示色(生产环境提醒)。 + public bool HostWarning { get; init; } + + /// 将要执行的**那几条真请求**,不是一句概括。 + public IReadOnlyList Commands { get; init; } = []; + + /// 请求下面的等价命令行(给用户核对与复制)。 + public string? CommandNote { get; init; } + + /// 目标列表。 + public IReadOnlyList Targets { get; init; } = []; + + /// 后果说明。 + public IReadOnlyList Consequences { get; init; } = []; + + /// 确认按钮文字。 + public required string ConfirmLabel { get; init; } + + /// 确认按钮的图标。 + public string ConfirmIcon { get; init; } = "Icon.trash-2"; + + /// + /// 档要求手打的确认串,如 delete / save。 + /// + public string ConfirmWord { get; init; } = "delete"; + + /// 会丢数据那一档要额外醒目显示的一句话。 + public string? DataLossHeadline { get; init; } + + /// 会丢数据那一档的补充说明(逐条)。 + public IReadOnlyList DataLossPoints { get; init; } = []; + + /// + /// 闸门上那个可选的"先做一件事再执行"勾选(删卷之前先备份为 tar)。 + /// 留空表示这次不提供这个选项。 + /// + public string? PrecautionLabel { get; init; } + + /// 勾选默认打开没有。删数据这一档默认**打开** —— 默认值该偏向不丢东西那一边。 + public bool PrecautionDefault { get; init; } = true; +} + +/// +/// 面板内的确认闸门。 +/// +/// 它**贴在这个面板上**,不是一个飘在屏幕中央的系统弹窗:同时开着两个 Docker 面板时, +/// 一个居中的对话框根本说不清自己属于哪一台机器。 +/// +/// +public sealed class ConfirmGate : ObservableObject +{ + private TaskCompletionSource? _pending; + + /// 建一个闸门。 + public ConfirmGate() + { + ConfirmCommand = new RelayCommand(_ => Complete(true), _ => CanConfirm); + CancelCommand = new RelayCommand(_ => Complete(false)); + } + + /// + /// 那个可选的"先做一件事再执行"勾选是否打开。发起方在 AskAsync 返回之后读它。 + /// + public bool Precaution + { + get; + set => SetField(ref field, value); + } = true; + + /// 这次闸门有没有提供那个勾选。 + public bool HasPrecaution => Request?.PrecautionLabel is { Length: > 0 }; + + /// 那个勾选的文字。 + public string PrecautionLabel => Request?.PrecautionLabel ?? ""; + + /// 当前请求;没有待确认的事情时为 + public ConfirmRequest? Request + { + get; + private set + { + if (SetField(ref field, value)) + { + OnPropertiesChanged(nameof(IsOpen), nameof(IsDataLoss), nameof(HasTargets), + nameof(HasConsequences), nameof(HasCommandNote), nameof(CanConfirm), nameof(RemainingHint), + nameof(HasPrecaution), nameof(PrecautionLabel), nameof(Icon), nameof(Title), + nameof(HostName), nameof(HostDetail), nameof(CommandNote), nameof(DataLossHeadline), + nameof(ConfirmWord), nameof(ConfirmIcon), nameof(ConfirmLabel)); + ConfirmCommand.RaiseCanExecuteChanged(); + } + } + } + + /// 闸门是否打开着。 + public bool IsOpen => Request is not null; + + // 下面这些都是 Request 的空安全转发。界面不直接绑 Request.Xxx —— + // 闸门关着时 Request 是 null,而 IsVisible 只是不画:子树照样建、绑定照样求值, + // 于是每关一次就刷一屏 “binding … Value is null”。转发一层,关着时给的是空串。 + + /// 标题左侧的图标资源键。 + public string Icon => Request?.Icon ?? "Icon.trash-2"; + + /// 标题。 + public string Title => Request?.Title ?? ""; + + /// 目标主机名。 + public string HostName => Request?.HostName ?? ""; + + /// 主机名后面那行小字。 + public string HostDetail => Request?.HostDetail ?? ""; + + /// + /// 目标不是本机时,主机那行小字要用警示色。 + /// + /// 请求里早就带着这一位(),界面却一直没读 —— + /// 于是"在生产机上删 3 个卷"和"在本机删 3 个卷"长得一模一样。 + /// + /// + public bool HostWarning => Request?.HostWarning == true; + + /// 请求下面那行等价的命令行。 + public string CommandNote => Request?.CommandNote ?? ""; + + /// 会丢数据那一档要额外醒目显示的一句话。 + public string DataLossHeadline => Request?.DataLossHeadline ?? ""; + + /// 会丢数据那一档要手打的确认串。 + public string ConfirmWord => Request?.ConfirmWord ?? ""; + + /// 确认按钮的图标。 + public string ConfirmIcon => Request?.ConfirmIcon ?? "Icon.trash-2"; + + /// 确认按钮文字。 + public string ConfirmLabel => Request?.ConfirmLabel ?? ""; + + /// 是不是"会丢数据"那一档。 + public bool IsDataLoss => Request?.Tier == ConfirmTier.DataLoss; + + /// 有没有目标列表。 + public bool HasTargets => Request?.Targets.Count > 0; + + /// 有没有后果说明。 + public bool HasConsequences => Request?.Consequences.Count > 0; + + /// 有没有等价命令。 + public bool HasCommandNote => !string.IsNullOrEmpty(Request?.CommandNote); + + /// 目标列表(绑定用)。 + public ObservableCollection Targets { get; } = []; + + /// 后果说明(绑定用)。 + public ObservableCollection Consequences { get; } = []; + + /// 将要执行的请求(绑定用)。 + public ObservableCollection Commands { get; } = []; + + /// 会丢数据那一档的补充说明(绑定用)。 + public ObservableCollection DataLossPoints { get; } = []; + + /// 用户手打的确认串。 + public string TypedWord + { + get; + set + { + if (SetField(ref field, value)) + { + OnPropertiesChanged(nameof(CanConfirm), nameof(RemainingHint)); + ConfirmCommand.RaiseCanExecuteChanged(); + } + } + } = ""; + + /// 确认按钮能不能按。 + public bool CanConfirm => + Request is not null && + (Request.Tier != ConfirmTier.DataLoss || + string.Equals(TypedWord.Trim(), Request.ConfirmWord, StringComparison.Ordinal)); + + /// 还差几个字符的提示。 + public string RemainingHint + { + get + { + if (Request is not { Tier: ConfirmTier.DataLoss } request) + { + return ""; + } + var typed = TypedWord.Trim(); + return typed == request.ConfirmWord + ? "可以确认了" + : request.ConfirmWord.StartsWith(typed, StringComparison.Ordinal) + ? $"还差 {request.ConfirmWord.Length - typed.Length} 个字符" + : $"请输入 {request.ConfirmWord}"; + } + } + + /// 确认。 + public RelayCommand ConfirmCommand { get; } + + /// 取消。 + public RelayCommand CancelCommand { get; } + + /// + /// 打开闸门并等用户决定。已经有一个在等的时候,新请求直接被拒绝 + /// (返回 false)—— 两层确认框叠在一起,用户不可能说清自己在确认哪一个。 + /// + public Task AskAsync(ConfirmRequest request) + { + if (_pending is not null) + { + return Task.FromResult(false); + } + _pending = new(TaskCreationOptions.RunContinuationsAsynchronously); + Ui.Post(() => + { + TypedWord = ""; + Precaution = request.PrecautionDefault; + Commands.Clear(); + foreach (var command in request.Commands) + { + Commands.Add(command); + } + Targets.Clear(); + foreach (var target in request.Targets) + { + Targets.Add(target); + } + Consequences.Clear(); + foreach (var consequence in request.Consequences) + { + Consequences.Add(consequence); + } + DataLossPoints.Clear(); + foreach (var point in request.DataLossPoints) + { + DataLossPoints.Add(point); + } + Request = request; + }); + return _pending.Task; + } + + /// 面板关闭时把等待中的请求当作取消。 + public void CancelPending() => Complete(false); + + private void Complete(bool confirmed) + { + var pending = _pending; + _pending = null; + Ui.Post(() => + { + Request = null; + TypedWord = ""; + }); + pending?.TrySetResult(confirmed); + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/ConfirmGateView.axaml b/plugins/VelaShell.Plugin.DockerPanel/Ui/ConfirmGateView.axaml new file mode 100644 index 0000000..8fcc090 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/ConfirmGateView.axaml @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/ConfirmGateView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/ConfirmGateView.axaml.cs new file mode 100644 index 0000000..e16a87e --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/ConfirmGateView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// 确认闸门的视图。 +public sealed partial class ConfirmGateView : UserControl +{ + /// 建视图。 + public ConfirmGateView() => AvaloniaXamlLoader.Load(this); +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Converters.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Converters.cs new file mode 100644 index 0000000..b8e99c3 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Converters.cs @@ -0,0 +1,568 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Data.Converters; +using Avalonia.Media; +using System.Globalization; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// +/// 面板用到的值转换器。 +/// +/// 全部走宿主的 Vela* 令牌:转换器只负责"这一行是什么状态", +/// 具体是哪个颜色由主题字典决定。 +/// +/// +/// 取色一律经 —— 转换器只在**绑定源**变化时才跑,而主题不是绑定源, +/// 所以换肤时这里的代码根本不会被再调一次。 的办法是让画刷对象 +/// 保持不变、只改它的颜色,于是绑定一次都不用重算。别在这个文件里直接去 TryFindResource +/// 拿画刷:那样拿到的是宿主当前那一格字典里的实例,宿主换主题是整格替换,它会就此停在旧色上。 +/// +/// +public static class Converters +{ + /// 状态色 → 画刷。参数可选 dim,取半透明底色。 + public static readonly IValueConverter ToneBrush = new ToneBrushConverter(); + + /// 状态色 → 图标资源键。 + public static readonly IValueConverter ToneIcon = new FuncValueConverter(tone => tone switch + { + RowTone.Ok => "Icon.circle-check", + RowTone.Warn => "Icon.triangle-alert", + RowTone.Danger => "Docker.circle-x", + RowTone.Busy => "Icon.refresh-cw", + _ => "Icon.circle-help" + }); + + /// 后果说明的严重度 → 画刷。 + public static readonly IValueConverter SeverityBrush = new SeverityBrushConverter(); + + /// 后果说明的严重度 → 图标。 + public static readonly IValueConverter SeverityIcon = new FuncValueConverter(severity => severity switch + { + 1 => "Icon.shield", + 2 => "Icon.triangle-alert", + 3 => "Docker.circle-x", + _ => "Icon.info" + }); + + /// 反馈语气 → 画刷。 + public static readonly IValueConverter FeedbackBrush = new FeedbackBrushConverter(); + + /// 0–1 的比例 → 星形宽度(进度条的已填充段)。 + public static readonly IValueConverter RatioStar = + new FuncValueConverter(v => new(Math.Clamp(v, 0, 1), GridUnitType.Star)); + + /// 0–1 的比例 → 剩余段的星形宽度。 + public static readonly IValueConverter RatioRestStar = + new FuncValueConverter(v => new(1 - Math.Clamp(v, 0, 1), GridUnitType.Star)); + + /// 把 0–100 的采样值换算成 sparkline 里的柱高(最高 16)。 + public static readonly IValueConverter SampleHeight = + new FuncValueConverter(v => Math.Max(2, Math.Clamp(v, 0, 100) / 100 * 16)); + + /// 把 0–100 的采样值换算成抽屉里那张卡片的柱高(最高 30)。 + public static readonly IValueConverter CardSampleHeight = + new FuncValueConverter(v => Math.Max(2, Math.Clamp(v, 0, 100) / 100 * 30)); + + /// 把 0–100 的采样值换算成趋势图里的柱高(最高 96)。 + public static readonly IValueConverter TrendHeight = + new FuncValueConverter(v => Math.Max(2, Math.Clamp(v, 0, 100) / 100 * 96)); + + /// 0–1 的比例 → 剩下那一段的权重(配合 )。 + public static readonly IValueConverter RemainingWeight = + new FuncValueConverter(v => Math.Max(0.0001, 1 - Math.Clamp(v, 0, 1))); + + /// + /// 值为 0(含极小的浮点残留)为 true。进度条据此在"不知道分母"时退化成不确定型。 + /// + public static readonly IValueConverter IsZero = + new FuncValueConverter(v => v <= 0.0001); + + /// + /// 来源序号 → 一个稳定的颜色。 + /// + /// 用宿主已有的语义色轮着来,不自造调色板 —— 它们在明暗两套主题下都已经调过对比度, + /// 而临时挑的六个 hex 在亮色模式下多半有一半看不清。 + /// + /// + public static readonly IValueConverter SourceBrush = new SourceBrushConverter(); + + /// 差异行的底色:增绿、删红、改黄,没变透明。 + public static readonly IValueConverter DiffBackground = new DiffBackgroundConverter(); + + /// 键盘选中项的底色。 + public static readonly IValueConverter ActiveBackground = new ActiveBackgroundConverter(); + + /// 非空字符串为 true。 + public static readonly IValueConverter NotEmpty = + new FuncValueConverter(v => !string.IsNullOrEmpty(v)); + + /// 取反。 + public static readonly IValueConverter Not = new FuncValueConverter(v => !v); + + /// 文件树里的图标色:目录用强调色,文件退到弱化色。 + public static readonly IValueConverter FileIconBrush = + new FuncValueConverter(isDirectory => isDirectory + ? ThemeBrush("VelaAccent", Brushes.MediumPurple) + : ThemeBrush("VelaTextTertiary", Brushes.Gray)); + + /// 「这一行是当前打开的那个」→ 选中底色,否则透明。 + public static readonly IValueConverter CurrentRowBackground = + new FuncValueConverter(current => current + ? ThemeBrush("VelaAccentDim", Brushes.Transparent) + : Brushes.Transparent); + + /// 「换行」开关 → 文本换行模式。关掉时长行横向截断,不折成好几屏。 + public static readonly IValueConverter WrapMode = + new FuncValueConverter(v => v ? TextWrapping.Wrap : TextWrapping.NoWrap); + + /// + /// 「换行」开关 → 横向滚动条。 + /// + /// 两者必须互斥:开着横向滚动,正文就是在无限宽里量的,TextWrapping.Wrap 永远折不了行; + /// 关掉横向滚动而又不折行,超出去的那半行则彻底没办法看到。 + /// + /// + public static readonly IValueConverter WrapScroll = + new FuncValueConverter(wrap => + wrap ? ScrollBarVisibility.Disabled : ScrollBarVisibility.Auto); + + /// + /// 提示文字:空的就**不给提示**。 + /// + /// ToolTip.Tip 只看是不是 :空字符串照样会弹一个空框出来。 + /// 而绑上去的那些文字有一半天生可能是空的(没有端口的容器、没有禁用理由的选项、 + /// 还没打开文件的编辑器),于是鼠标一悬停就冒出一个空白小方块。 + /// + /// + public static readonly IValueConverter TipText = + new FuncValueConverter(text => string.IsNullOrWhiteSpace(text) ? null : text); + + /// 最大化 / 还原是同一颗键,图标要跟着当前状态走 —— 否则铺开之后没人看得出怎么回去。 + public static readonly IValueConverter MaximizeIcon = + new FuncValueConverter(max => max ? "Docker.minimize-2" : "Docker.maximize-2"); + + /// + /// 控件宽度 ≥ 参数(像素)才为真。 + /// + /// 抽屉是用户拖出来的,同一条工具条要在 440 和 1400 两种宽度下都成立 —— + /// 靠它把次要的那几颗按钮按宽度逐级收起,而不是让它们溢出到屏幕外。 + /// + /// + public static readonly IValueConverter WiderThan = new WiderThanConverter(); + + /// + /// CPU 高负载 → 警示色,否则强调色。 + /// + /// 行内 sparkline 与它旁边那个百分比共用这一条:两者必须同时变色, + /// 否则会出现"柱子是黄的、数字是紫的"这种自相矛盾的一行。 + /// + /// + public static readonly IValueConverter HotAccent = + new FuncValueConverter(hot => hot + ? ThemeBrush("VelaWarning", Brushes.Orange) + : ThemeBrush("VelaAccent", Brushes.MediumPurple)); + + /// CPU 高负载 → 警示色,否则常规数值色。给 sparkline 旁边那个百分比用。 + public static readonly IValueConverter HotText = + new FuncValueConverter(hot => hot + ? ThemeBrush("VelaWarning", Brushes.Orange) + : ThemeBrush("VelaTextSecondary", Brushes.Gray)); + + /// + /// 按令牌取画刷。**全部**转换器的取色都必须走这一条。 + /// + /// 拿到的是一支会跟着宿主换肤就地改色的画刷(见 )。 + /// 直接把宿主资源里的画刷返回出去是不行的:转换器只在绑定源变化时才跑,主题不是绑定源, + /// 而宿主换主题是整格替换字典 —— 返回出去的那个对象会永远停在旧主题的颜色上。 + /// + /// + internal static IBrush ThemeBrush(string key, IBrush fallback) => ThemeBrushes.Get(key, fallback); + + /// + /// 按当前主题变体查一个**非颜色**资源(图标几何)。 + /// + /// 颜色不要走这里 —— 走 。几何不随主题变,取到就一直有效。 + /// + /// + /// 必须带上 ActualThemeVariant:宿主的资源分两族,不带主题的那个重载按 + /// ThemeVariant.Default 查,写在 ThemeDictionaries 分支下的那一族一个都查不到。 + /// + /// + internal static object? Lookup(string key) + { + return Application.Current is not { } app ? null : app.TryFindResource(key, app.ActualThemeVariant, out var value) ? value : null; + } + + private sealed class ToneBrushConverter : IValueConverter + { + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + var dim = (parameter as string) == "dim"; + var key = value switch + { + RowTone.Ok => dim ? "VelaShellGreenDim" : "VelaStatusConnected", + RowTone.Warn => dim ? "VelaShellYellowDim" : "VelaWarning", + RowTone.Danger => dim ? "VelaShellRedDim" : "VelaError", + RowTone.Busy => dim ? "VelaAccentDim" : "VelaAccent", + _ => dim ? "VelaShellSubtleDim" : "VelaTextTertiary" + }; + return ThemeBrush(key, Brushes.Gray); + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); + } + + private sealed class WiderThanConverter : IValueConverter + { + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => + value is double width && !double.IsNaN(width) && + double.TryParse(parameter as string, NumberStyles.Float, CultureInfo.InvariantCulture, out var least) && + width >= least; + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); + } + + private sealed class DiffBackgroundConverter : IValueConverter + { + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => + value switch + { + RowTone.Ok => ThemeBrush("VelaShellGreenDim", Brushes.Transparent), + RowTone.Danger => ThemeBrush("VelaShellRedDim", Brushes.Transparent), + RowTone.Warn => ThemeBrush("VelaShellYellowDim", Brushes.Transparent), + _ => Brushes.Transparent + }; + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); + } + + private sealed class ActiveBackgroundConverter : IValueConverter + { + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => + value is true ? ThemeBrush("VelaAccentDim", Brushes.Transparent) : Brushes.Transparent; + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); + } + + private sealed class SourceBrushConverter : IValueConverter + { + /// 轮换用的令牌。六个之后回头 —— 同屏合并超过六条流本来就读不动了。 + private static readonly string[] Palette = + [ + "VelaAccent", "VelaStatusConnected", "VelaInfo", + "VelaWarning", "VelaShellMagenta", "VelaShellCyan" + ]; + + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + var index = value is int i && i >= 0 ? i : 0; + return ThemeBrush(Palette[index % Palette.Length], Brushes.Gray); + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); + } + + private sealed class SeverityBrushConverter : IValueConverter + { + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => + ThemeBrush(value switch + { + 1 => "VelaStatusConnected", + 2 => "VelaWarning", + 3 => "VelaError", + _ => "VelaTextTertiary" + }, Brushes.Gray); + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); + } + + private sealed class FeedbackBrushConverter : IValueConverter + { + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + var dim = (parameter as string) == "dim"; + var key = value switch + { + FeedbackKind.Success => dim ? "VelaShellGreenDim" : "VelaStatusConnected", + FeedbackKind.Warning => dim ? "VelaShellYellowDim" : "VelaWarning", + FeedbackKind.Error => dim ? "VelaShellRedDim" : "VelaError", + _ => dim ? "VelaAccentDim" : "VelaAccent" + }; + return ThemeBrush(key, Brushes.Gray); + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); + } +} + +/// +/// 按资源键取图标几何。 +/// +/// 图标名是数据(视图模型给的字符串),而 StreamGeometry 在资源字典里 —— +/// 这个转换器把两者接上,免得每处都写一个 DataTrigger 大表。 +/// +/// +public sealed class IconLookupConverter : IValueConverter +{ + /// 单例。 + public static readonly IconLookupConverter Instance = new(); + + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value is not string key || key.Length == 0 ? null : Converters.Lookup(key); + } + + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); +} + +/// +/// 会丢数据那一档的闸门用红边框,一般破坏性用常规描边。 +/// +/// 边框颜色是两档之间**唯一**不靠文字的区分 —— 用户在读标题之前就该感觉到分量不同。 +/// +/// +public sealed class DataLossBorderConverter : IValueConverter +{ + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + var key = value is true ? "VelaError" : "VelaBorderSecondary"; + return Converters.ThemeBrush(key, Brushes.Gray); + } + + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); +} + +/// 只读字段的底色比可编辑的暗一档 —— 不用读提示就知道这一格改不了。 +public sealed class ReadOnlyBackgroundConverter : IValueConverter +{ + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + var key = value is true ? "VelaBgSurface" : "VelaBgInput"; + return Converters.ThemeBrush(key, Brushes.Transparent); + } + + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); +} + +/// 危险开关的说明文字用警示色。 +public sealed class DangerTextConverter : IValueConverter +{ + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + var key = value is true ? "VelaError" : "VelaTextTertiary"; + return Converters.ThemeBrush(key, Brushes.Gray); + } + + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); +} + +/// 跟随中的按钮用绿底,停了就是透明。 +public sealed class FollowBackgroundConverter : IValueConverter +{ + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value is not true ? Brushes.Transparent : (object)(Converters.ThemeBrush("VelaShellGreenDim", Brushes.Transparent)); + } + + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); +} + +/// 跟随中的文字与图标用绿色。 +public sealed class FollowForegroundConverter : IValueConverter +{ + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + var key = value is true ? "VelaStatusConnected" : "VelaTextSecondary"; + return Converters.ThemeBrush(key, Brushes.Gray); + } + + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); +} + +/// +/// 搜索命中的行整行加一层淡黄底。 +/// +/// 不做行内高亮是有意的:日志行是纯文本(SelectableTextBlock 才能复制), +/// 把它拆成几段富文本会同时毁掉选中与复制 —— 而那两件事在日志里比高亮更常用。 +/// +/// +public sealed class MatchBackgroundConverter : IValueConverter +{ + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value is not true ? Brushes.Transparent : (object)(Converters.ThemeBrush("VelaShellYellowDim", Brushes.Transparent)); + } + + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); +} + +/// 标准错误的行用红色,标准输出用终端前景色。 +public sealed class LogLineForegroundConverter : IValueConverter +{ + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + var key = value is true ? "VelaShellRed" : "VelaShellWhite"; + return Converters.ThemeBrush(key, Brushes.Gray); + } + + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); +} + +/// +/// 合并日志正文的颜色:按级别分色。 +/// +/// 刻意把 INFO 染成绿色。日志里绝大多数行都是 INFO,整屏绿字既刺眼又等于没分色 —— +/// 分色的意义是让少数几行跳出来。所以只有 WARN 与 ERROR 换色,DEBUG 压暗, +/// 其余保持正文色。stderr 一律按错误算:它走的是哪条流,这件事本身就是信息。 +/// +/// +public sealed class LogBodyBrushConverter : IMultiValueConverter +{ + /// + public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) + { + var isError = values.Count > 0 && values[0] is true; + var level = values.Count > 1 && values[1] is LogLevel l ? l : LogLevel.None; + var key = isError || level == LogLevel.Error ? "VelaShellRed" + : level == LogLevel.Warn ? "VelaWarning" + : level == LogLevel.Debug ? "VelaTextTertiary" + : "VelaShellWhite"; + return Converters.ThemeBrush(key, Brushes.Gray); + } +} + +/// 仓库凭据状态的图标:拿到了凭据用锁,没拿到用警示 —— 它决定拉取会不会 401。 +public sealed class AuthIconConverter : IValueConverter +{ + /// + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => + value is true ? "Docker.lock" : "Icon.triangle-alert"; + + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); +} + +/// 仓库凭据状态的颜色。 +public sealed class AuthBrushConverter : IValueConverter +{ + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + var key = value is true ? "VelaStatusConnected" : "VelaWarning"; + return Converters.ThemeBrush(key, Brushes.Gray); + } + + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); +} + +/// 悬空镜像用虚线圈,有标签的用层叠图标。 +public sealed class ImageIconConverter : IValueConverter +{ + /// + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => + value is true ? "Docker.circle-dashed" : "Icon.layers"; + + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); +} + +/// 执行记录的行色:命令行绿、标准错误红、其余是终端前景色。 +public sealed class OutputLineForegroundConverter : IMultiValueConverter +{ + /// + public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) + { + var isError = values.Count > 0 && values[0] is true; + var isCommand = values.Count > 1 && values[1] is true; + var level = values.Count > 2 && values[2] is LogLevel l ? l : LogLevel.None; + // 命令本身 > 走 stderr > 正文里认出来的级别。最后这一档平时不会触发 + // (compose 自己的输出没有级别),但 up 的时候服务把 ERROR 打到 stdout 是常有的事。 + var key = isCommand ? "VelaStatusConnected" + : isError || level == LogLevel.Error ? "VelaShellRed" + : level == LogLevel.Warn ? "VelaWarning" + : "VelaShellWhite"; + return Converters.ThemeBrush(key, Brushes.Gray); + } +} + +/// 按资源键取画刷(堆叠占用条的每一段各用一个令牌)。 +public sealed class ResourceBrushConverter : IValueConverter +{ + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value is not string key ? Brushes.Transparent : (object)(Converters.ThemeBrush(key, Brushes.Transparent)); + } + + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); +} + +/// 危险档的回收卡片用红边,其余用常规边。 +public sealed class PruneBorderConverter : IValueConverter +{ + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + var key = value is RowTone.Danger ? "VelaShellRedDim" : "VelaBorderPrimary"; + return Converters.ThemeBrush(key, Brushes.Gray); + } + + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); +} + +/// 高负载的条与数字转警示色。 +public sealed class HotBrushConverter : IValueConverter +{ + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + var key = value is true ? "VelaGaugeWarn" : "VelaGaugeCpu"; + return Converters.ThemeBrush(key, Brushes.Gray); + } + + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelView.axaml b/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelView.axaml new file mode 100644 index 0000000..46409fb --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelView.axaml @@ -0,0 +1,721 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelView.axaml.cs new file mode 100644 index 0000000..82a4cbe --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelView.axaml.cs @@ -0,0 +1,195 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.VisualTree; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// +/// 面板视图。 +/// +/// 代码后置只做一件事:把几个"参数是数据对象、目标是普通委托"的点击接回视图模型 —— +/// 这些回调(恢复动作、toast 动作、取消某个任务)天生就是 Action, +/// 为它们各造一个 ICommand 只是为了讨好 XAML,不值得。 +/// +/// +public sealed partial class DockerPanelView : UserControl +{ + /// 建视图并挂上视图模型。 + public DockerPanelView(DockerPanelViewModel viewModel) + { + InitializeComponent(); + DataContext = viewModel; + // 文件选择器要一个顶层窗口,而只有视图知道自己被画在哪个窗口里。 + FilePicker.Attach(() => TopLevel.GetTopLevel(this)); + // 弹层一开就把焦点收进面板:键盘事件是顺着焦点那条路由走的, + // 焦点若还留在面板外面(或者压根没有),Esc 根本到不了这里。 + viewModel.PropertyChanged += (_, e) => + { + if (e.PropertyName is nameof(DockerPanelViewModel.HasForm) or nameof(DockerPanelViewModel.HasDialog)) + { + TakeFocusForOverlay(); + } + }; + viewModel.Confirm.PropertyChanged += (_, e) => + { + if (e.PropertyName is nameof(ConfirmGate.IsOpen)) + { + TakeFocusForOverlay(); + } + }; + viewModel.Palette.PropertyChanged += (_, e) => + { + if (e.PropertyName is nameof(CommandPalette.IsOpen)) + { + TakeFocusForOverlay(); + } + }; + } + + /// 焦点已经在面板里就别动它(命令面板自己会去聚焦输入框);不在就收回来。 + private void TakeFocusForOverlay() + { + if (TopLevel.GetTopLevel(this)?.FocusManager?.GetFocusedElement() is Visual focused && + focused.FindAncestorOfType(true) == this) + { + return; + } + Focus(); + } + + /// 无参构造只为设计器与 XAML 装载器;运行时走带上下文的那个。 + public DockerPanelView() => InitializeComponent(); + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + Focusable = true; + } + + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + // Esc 挂在**顶层**上,不挂在面板上。 + // 键盘事件是顺着焦点那条路由走的:焦点若不在面板里(用户刚点过遮罩、 + // 或者面板压根还没拿到过焦点),挂在面板上的处理器根本不在路由上 —— + // 这正是"所有二级窗口按 Esc 都没反应"的原因。顶层在任何一条路由的最上游。 + // Tunnel 优先:冒泡的话,焦点所在的 TextBox 会先把 Esc 吃掉(清输入、收候选); + // 两种策略都注册是为了兼容,处理过的那一次会置 Handled,不会关掉两层。 + if (TopLevel.GetTopLevel(this) is { } top) + { + top.AddHandler(KeyDownEvent, OnEscape, RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + } + } + + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + if (TopLevel.GetTopLevel(this) is { } top) + { + top.RemoveHandler(KeyDownEvent, OnEscape); + } + base.OnDetachedFromVisualTree(e); + } + + /// Esc:关掉最上面那一层弹层。 + private void OnEscape(object? sender, KeyEventArgs e) + { + if (!e.Handled && e.Key == Key.Escape && + DataContext is DockerPanelViewModel viewModel && viewModel.CloseTopOverlay()) + { + e.Handled = true; + } + } + + /// + /// 点遮罩 = 关掉这一层。 + /// + /// 只有命令面板这么做。表单、闸门、对话框里用户是**输了东西**的, + /// 手滑点到旁边就把内容丢掉,比多按一次 Esc 糟得多;它们留 Esc 与「取消」两条路。 + /// + /// + private void OnScrimPressed(object? sender, PointerPressedEventArgs e) + { + if (DataContext is DockerPanelViewModel { Palette.IsOpen: true } viewModel) + { + viewModel.Palette.CloseCommand.Execute(null); + e.Handled = true; + } + } + + /// + /// 面板级快捷键。 + /// + /// 只在**面板拿到焦点**时生效(OnKeyDown 走的是面板自己的可视树), + /// 不注册全局热键 —— 宿主自己也有 Ctrl+K,抢它的键会让用户在别处按不出宿主的命令面板。 + /// + /// + protected override void OnKeyDown(KeyEventArgs e) + { + if (DataContext is not DockerPanelViewModel viewModel || !viewModel.IsReady) + { + base.OnKeyDown(e); + return; + } + var ctrl = e.KeyModifiers.HasFlag(KeyModifiers.Control); + if (ctrl && e.Key == Key.K) + { + viewModel.OpenPaletteCommand.Execute(null); + e.Handled = true; + return; + } + // 行级快捷键落在当前选中的那一行上;没有选中就什么都不做, + // 而不是猜一个 —— 对着错的容器执行"停止"比没反应糟得多。 + if (ctrl && viewModel.Containers.Detail is { } detail) + { + switch (e.Key) + { + case Key.R: + detail.RestartCommand.Execute(null); + e.Handled = true; + return; + case Key.OemPeriod: + detail.StopCommand.Execute(null); + e.Handled = true; + return; + } + } + base.OnKeyDown(e); + } + + private void OnRecoveryClick(object? sender, RoutedEventArgs e) + { + if (sender is Control { Tag: RecoveryAction action }) + { + action.Invoke(); + } + } + + private void OnToastAction(object? sender, RoutedEventArgs e) + { + if (sender is Control { Tag: ToastAction action }) + { + action.Invoke(); + } + } + + private void OnDismissToast(object? sender, RoutedEventArgs e) + { + if (sender is Control { Tag: Toast toast } && DataContext is DockerPanelViewModel viewModel) + { + viewModel.Feedback.Dismiss(toast); + } + } + + private void OnCancelTask(object? sender, RoutedEventArgs e) + { + if (sender is Control { Tag: PanelTask task }) + { + task.Cancel(); + } + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelViewModel.Dialogs.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelViewModel.Dialogs.cs new file mode 100644 index 0000000..40d0302 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelViewModel.Dialogs.cs @@ -0,0 +1,206 @@ +using VelaShell.Plugin.DockerPanel.Docker; +using VelaShell.Plugin.DockerPanel.Ui.Pages; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +public sealed partial class DockerPanelViewModel +{ + private TaskCompletionSource? _formPending; + + /// 当前打开的表单;没有时为 + public PanelForm? ActiveForm + { + get; + private set + { + if (SetField(ref field, value)) + { + OnPropertyChanged(nameof(HasForm)); + } + } + } + + /// 有表单开着。 + public bool HasForm => ActiveForm is not null; + + /// 当前打开的自定义对话框(拉取镜像那种有自己状态机的)。 + public object? ActiveDialog + { + get; + set + { + if (SetField(ref field, value)) + { + OnPropertyChanged(nameof(HasDialog)); + } + } + } + + /// 有自定义对话框开着。 + public bool HasDialog => ActiveDialog is not null; + + /// 确认表单。 + public RelayCommand FormConfirmCommand => field ??= new(_ => + { + if (ActiveForm is not { } form) + { + return; + } + form.FormError = null; + foreach (var each in form.Fields) + { + each.Error = null; + } + // 校验不过就停在原地,并且把原因写在**那一格**下面 —— + // 一句"输入有误"等于让用户自己去猜是哪一格。 + if (!form.Validate()) + { + return; + } + CompleteForm(true); + }); + + /// 取消表单。 + public RelayCommand FormCancelCommand => field ??= new(_ => CompleteForm(false)); + + /// + /// 关掉最上面那一层弹层(Esc 与点击遮罩都走这里)。 + /// + /// 次序与它们在视觉上的层叠次序一致:对话框 → 表单 → 闸门 → 命令面板。 + /// 一次只关一层 —— 命令面板唤出的动作往往还要再弹一次闸门, + /// 一按 Esc 就把两层一起收掉,用户会以为自己刚才那下点丢了。 + /// + /// + /// 确实关掉了一层。 + public bool CloseTopOverlay() + { + if (HasDialog) + { + CloseDialogCommand.Execute(null); + return true; + } + if (HasForm) + { + FormCancelCommand.Execute(null); + return true; + } + if (Confirm.IsOpen) + { + Confirm.CancelCommand.Execute(null); + return true; + } + if (Palette.IsOpen) + { + Palette.CloseCommand.Execute(null); + return true; + } + return false; + } + + /// 关掉自定义对话框。 + public RelayCommand CloseDialogCommand => field ??= new(_ => + { + if (ActiveDialog is IAsyncDisposable disposable) + { + _ = disposable.DisposeAsync(); + } + ActiveDialog = null; + }); + + /// + /// 打开一个表单并等用户确认。 + /// + /// 与确认闸门一样,同一时刻只允许一个 —— 两层表单叠在一起, + /// 用户不可能说清自己在填哪一个。 + /// + /// + public Task ShowFormAsync(PanelForm form) + { + if (_formPending is not null) + { + return Task.FromResult(false); + } + _formPending = new(TaskCreationOptions.RunContinuationsAsynchronously); + Ui.Post(() => ActiveForm = form); + return _formPending.Task; + } + + private void CompleteForm(bool confirmed) + { + var pending = _formPending; + _formPending = null; + Ui.Post(() => ActiveForm = null); + pending?.TrySetResult(confirmed); + } + + /// 打开拉取镜像对话框。 + public Task ShowPullDialogAsync(string? initialReference) + { + ActiveDialog = new PullImageViewModel(this, initialReference); + return Task.CompletedTask; + } + + /// 打开「运行容器」表单,确认后创建并按需启动。 + public async Task ShowRunContainerAsync(string image) + { + if (Client is not { } client) + { + return; + } + string[] networks; + try + { + var all = await client.ListNetworksAsync(Lifetime).ConfigureAwait(true); + networks = [.. all.Select(n => n.Name).OrderBy(n => n, StringComparer.OrdinalIgnoreCase)]; + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + networks = ["bridge"]; + } + // 镜像的小字:大小与平台。表单里原来完全看不到自己要跑的是哪个镜像 + // (只有最底下那条等效命令里出现过),而这颗按钮是从好几个地方点进来的。 + var detail = ""; + try + { + var images = await client.ListImagesAsync(false, Lifetime).ConfigureAwait(true); + if (images.FirstOrDefault(i => i.RepoTags?.Contains(image) == true) is { } match) + { + detail = Humanize.Bytes(match.Size); + } + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + // 小字而已,取不到就不写 —— 不值得为它把整张表单挡下来。 + Context.Log.Debug($"read image size failed: {ex.Message}"); + } + var form = new RunContainerForm(image, detail, networks); + if (!await ShowFormAsync(form).ConfigureAwait(true)) + { + return; + } + var task = Tasks.Start("Icon.play", $"运行 {image}", indeterminate: true); + try + { + var created = await client + .CreateContainerAsync(form.ContainerName, form.ToRequest(), task.Token).ConfigureAwait(true); + foreach (var warning in created.Warnings ?? []) + { + Feedback.Notify(FeedbackKind.Warning, "daemon 有话说", warning); + } + if (form.Detach) + { + await client.StartContainerAsync(created.Id, task.Token).ConfigureAwait(true); + } + task.Finish(PanelTaskState.Succeeded, "完成", Humanize.ShortId(created.Id)); + Feedback.Notify(FeedbackKind.Success, + form.Detach ? "容器已启动" : "容器已创建", + $"{(form.ContainerName.Length > 0 ? form.ContainerName : Humanize.ShortId(created.Id))} · {image}"); + await GoToAsync(PanelPage.Containers).ConfigureAwait(true); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + task.Finish(PanelTaskState.Failed, "失败", ex.Message); + Feedback.ReportError("运行容器", ex); + } + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelViewModel.Events.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelViewModel.Events.cs new file mode 100644 index 0000000..2c4b373 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelViewModel.Events.cs @@ -0,0 +1,209 @@ +using Avalonia.Threading; +using VelaShell.Plugin.DockerPanel.Docker; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +public sealed partial class DockerPanelViewModel +{ + /// 事件密集时的合并窗口。compose up 一次能推来几十条事件,逐条刷新等于把远端敲成筛子。 + private static readonly TimeSpan CoalesceWindow = TimeSpan.FromMilliseconds(350); + + /// 事件流断掉时的兜底轮询间隔。 + private static readonly TimeSpan FallbackInterval = TimeSpan.FromSeconds(30); + + private CancellationTokenSource? _eventCts; + private Task? _eventTask; + private DispatcherTimer? _coalesceTimer; + private DispatcherTimer? _fallbackTimer; + private DateTimeOffset _eventsSince; + + /// + /// 事件流接上了没有。顶栏那枚「实时」徽章绑的就是它 —— + /// 徽章亮着,才代表事件流真的接上了;灭了就说明现在看到的东西最多滞后 30 秒。 + /// + public bool EventsConnected + { + get; + private set + { + if (SetField(ref field, value)) + { + OnPropertiesChanged(nameof(EventsText), nameof(EventsDegraded), nameof(EventsDegradedText)); + } + } + } + + /// 状态栏右侧那句关于事件流的话。 + public string EventsText => EventsConnected + ? "事件流 已连接" + : Settings.AutoRefreshFallback ? "事件流 断开 · 已退化为 30s 轮询" : "事件流 断开"; + + /// + /// 已经连上 daemon,但事件流断了。 + /// + /// 这一档要在**页面里**说,不能只让顶栏那枚徽章灭掉:此刻列表看起来一切正常, + /// 而它显示的可能是 30 秒前的世界 —— 在这种状态下按「删除」是要出事的。 + /// + /// + public bool EventsDegraded => IsReady && !EventsConnected; + + /// 退化横幅上的那句话。 + public string EventsDegradedText => Settings.AutoRefreshFallback + ? "事件流已断开,已退化为 30 秒定时刷新 —— 看到的状态最多可能滞后 30 秒。" + : "事件流已断开,且自动刷新是关的 —— 现在看到的状态不会自己更新。"; + + /// 起事件流。 + private void StartEventStream() + { + _eventCts = CancellationTokenSource.CreateLinkedTokenSource(Lifetime); + _eventsSince = DateTimeOffset.UtcNow; + var token = _eventCts.Token; + _eventTask = Task.Run(() => EventLoopAsync(token), token); + } + + /// 停事件流并等它收尾。 + private async Task StopEventStreamAsync() + { + StopTimers(); + EventsConnected = false; + if (_eventCts is null) + { + return; + } + await _eventCts.CancelAsync().ConfigureAwait(false); + if (_eventTask is { } task) + { + try + { + await task.ConfigureAwait(false); + } + catch (Exception) + { + // 收尾时的异常一律吞掉:这条流本来就是被我们掐断的。 + } + } + _eventCts.Dispose(); + _eventCts = null; + _eventTask = null; + } + + private async Task EventLoopAsync(CancellationToken token) + { + // 退避从 1 秒起、封顶 15 秒:daemon 重启时不该被我们每 100ms 敲一次, + // 但用户也不该为了看到界面复活而等上一分钟。 + var backoff = TimeSpan.FromSeconds(1); + while (!token.IsCancellationRequested) + { + var client = Client; + if (client is null) + { + return; + } + try + { + Ui.Post(() => + { + EventsConnected = true; + StopFallbackTimer(); + }); + await client.StreamEventsAsync(_eventsSince, OnDockerEvent, token).ConfigureAwait(false); + // 流正常结束(daemon 关了连接)也走重连,而不是安静地不再更新。 + backoff = TimeSpan.FromSeconds(1); + } + catch (OperationCanceledException) + { + return; + } + catch (Exception ex) + { + Context.Log.Debug($"docker events stream ended: {ex.Message}"); + backoff = backoff < TimeSpan.FromSeconds(15) ? backoff * 2 : TimeSpan.FromSeconds(15); + } + Ui.Post(() => + { + EventsConnected = false; + StartFallbackTimer(); + }); + try + { + await Task.Delay(backoff, token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + return; + } + // 重连时从上次收到的那一刻续上,把断线期间发生的事补回来 —— + // 否则界面会漏掉整段"你没看见但确实发生了"的变化。 + _eventsSince = DateTimeOffset.UtcNow.AddSeconds(-5); + } + } + + /// + /// 一条事件到了。在**读流的线程**上被调用,所以这里只做判断与投递,不碰界面。 + /// + private void OnDockerEvent(DockerEvent dockerEvent) + { + Ui.Post(() => + { + Overview.AcceptEvent(dockerEvent); + var wanted = ActivePage?.WantsRefresh(dockerEvent) ?? false; + // 总览页永远关心计数,即使它不在前台 —— 用户切回去时不该看到一份旧数字。 + if (!wanted && !Overview.WantsRefresh(dockerEvent)) + { + return; + } + RequestCoalescedRefresh(); + }); + } + + /// 把这一刻起 350ms 内的所有刷新请求并成一次。 + private void RequestCoalescedRefresh() + { + _coalesceTimer ??= CreateTimer(CoalesceWindow, () => + { + StopCoalesceTimer(); + _ = RefreshActiveAsync(); + }); + // 重新计时:事件还在涌进来的时候不急着刷,等它停下来那一刻刷一次就够了。 + _coalesceTimer.Stop(); + _coalesceTimer.Start(); + } + + private void StartFallbackTimer() + { + if (!Settings.AutoRefreshFallback || _fallbackTimer is not null) + { + return; + } + _fallbackTimer = CreateTimer(FallbackInterval, () => _ = RefreshActiveAsync()); + _fallbackTimer.Start(); + } + + private void StopFallbackTimer() + { + _fallbackTimer?.Stop(); + _fallbackTimer = null; + } + + private void StopCoalesceTimer() + { + _coalesceTimer?.Stop(); + _coalesceTimer = null; + } + + private void StopTimers() + { + Ui.Post(() => + { + StopCoalesceTimer(); + StopFallbackTimer(); + }); + } + + private static DispatcherTimer CreateTimer(TimeSpan interval, Action tick) + { + var timer = new DispatcherTimer { Interval = interval }; + timer.Tick += (_, _) => tick(); + return timer; + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelViewModel.cs new file mode 100644 index 0000000..e5a5f29 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelViewModel.cs @@ -0,0 +1,1028 @@ +using System.Collections.ObjectModel; +using VelaShell.Plugin.DockerPanel.Docker; +using VelaShell.Plugin.DockerPanel.Ui.Pages; +using VelaShell.PluginSdk; +using VelaShell.PluginSdk.Sessions; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// 面板与 daemon 的连接状态。 +public enum PanelConnectionState +{ + /// 还没选端点。 + NoEndpoint, + + /// 正在建立通道。 + Connecting, + + /// 连上了。 + Ready, + + /// 连不上。 + Failed +} + +/// 主机切换器里的一项。 +public sealed class EndpointItem(DockerEndpoint endpoint, bool available, string stateText, FeedbackKind stateKind) + : ObservableObject +{ + /// 端点。 + public DockerEndpoint Endpoint { get; } = endpoint; + + /// 显示名。 + public string DisplayName => Endpoint.DisplayName; + + /// 小字(user@host / 管道路径)。 + public string Detail => Endpoint.Detail; + + /// 是不是本机。 + public bool IsLocal => Endpoint.Kind == DockerEndpointKind.Local; + + /// 能不能选(会话断了、找不到 socket 的项置灰)。 + public bool Available { get; private set; } = available; + + /// 右侧的状态短语。 + public string StateText { get; private set; } = stateText; + + /// 状态短语的语气。 + public FeedbackKind StateKind { get; private set; } = stateKind; + + /// 图标资源键。 + public string Icon => IsLocal ? "Docker.monitor" : "Docker.server"; + + /// 更新状态。 + public void Update(bool available, string stateText, FeedbackKind stateKind) + { + Available = available; + StateText = stateText; + StateKind = stateKind; + OnPropertiesChanged(nameof(Available), nameof(StateText), nameof(StateKind)); + } +} + +/// 连不上时给用户的一条出路。 +/// 按钮文字。 +/// 图标资源键。 +/// 是不是主要按钮。 +/// 回调。 +public sealed record RecoveryAction(string Label, string Icon, bool Primary, Action Invoke); + +/// 设置里列出的一个常见 socket 位置。 +/// 路径。 +/// 哪一种安装方式会摆在这儿。 +public sealed record SocketHint(string Path, string Source); + +/// +/// 面板外壳的视图模型:端点、导航、连接生命周期与事件驱动刷新。 +/// +/// 页面自己不拉数据也不挂定时器 —— 刷新的时机全部由这里按 docker events 决定, +/// 一条事件流喂饱所有页面。 +/// +/// +public sealed partial class DockerPanelViewModel : ObservableObject, IAsyncDisposable +{ + private int _countContainers; + private int _countImages; + private int _countVolumes; + private readonly CancellationTokenSource _lifetime = new(); + + // 落地页是总览:刚连上时,用户第一件想知道的是"这台机器现在怎么样", + // 而不是"这里有哪些容器"—— 后者是他确认前者之后才要往下走的一步。 + private PageViewModel? _activePage; + + /// 建外壳。 + public DockerPanelViewModel(IPluginContext context) + { + Context = context; + Settings = new(context.Storage); + Confirm = new(); + Tasks = new(); + Feedback = new(); + Overview = new OverviewPageViewModel(this); + Containers = new ContainersPageViewModel(this); + Images = new ImagesPageViewModel(this); + Volumes = new VolumesPageViewModel(this); + Networks = new NetworksPageViewModel(this); + ComposePage = new ComposePageViewModel(this); + SystemPage = new SystemPageViewModel(this); + AllPages = [Overview, Containers, Images, Volumes, Networks, ComposePage, SystemPage]; + _activePage = Overview; + + SelectPageCommand = new RelayCommand(p => + { + if (p is PanelPage page) + { + _ = GoToAsync(page); + } + }); + RefreshCommand = new RelayCommand(_ => RefreshActiveAsync(), _ => IsReady); + ReconnectCommand = new RelayCommand(_ => ConnectAsync(SelectedEndpoint)); + SelectEndpointCommand = new RelayCommand(p => + { + EndpointMenuOpen = false; + return p is EndpointItem item && item.Available ? ConnectAsync(item) : Task.CompletedTask; + }); + ToggleEndpointMenuCommand = new RelayCommand(_ => EndpointMenuOpen = !EndpointMenuOpen); + ToggleTaskCenterCommand = new RelayCommand(_ => TaskCenterOpen = !TaskCenterOpen); + ToggleSettingsCommand = new RelayCommand(_ => SettingsOpen = !SettingsOpen); + Palette = new(CollectPaletteEntries); + OpenPaletteCommand = new RelayCommand(_ => + { + Palette.Open(SelectedEndpoint?.DisplayName ?? "(未选择)"); + return Task.CompletedTask; + }); + SetLogTailCommand = new RelayCommand(p => + { + if (p is string tail) + { + Settings.LogTail = tail; + Settings.NotifyLogTailSegments(); + } + }); + ApplySocketPathCommand = new RelayCommand(_ => ApplySocketPathAsync()); + // 一条命令把四个常见位置和 docker 自己的说法一次问清,省得用户挨个试。 + // 用 -S 而不是 -e:那个位置上摆着一个同名的普通文件,比不存在更难查。 + // + // 光回答"有没有"是不够的。最常见的一种失败是:socket 在、daemon 也在跑, + // 但当前账号不在它的属组里 —— 那要用户对着 `ls -l` 和 `id` 两串输出自己交叉比对。 + // 所以这里直接用 -r/-w 替他试一次,并把属组和他自己所在的组打在同一行上。 + DiscoverSocketCommand = new RelayCommand(_ => SendToHostTerminalAsync( + "for s in /var/run/docker.sock \"$XDG_RUNTIME_DIR/docker.sock\" " + + "\"$HOME/.docker/run/docker.sock\" \"$HOME/.colima/default/docker.sock\"; do " + + "[ -S \"$s\" ] || continue; " + + "if [ -r \"$s\" ] && [ -w \"$s\" ]; then echo \"可用 $s\"; " + + "else echo \"有 $s,但当前账号读写不了:它属组 $(stat -c %G \"$s\" 2>/dev/null)," + + "而你在 $(id -nG)\"; fi; done; " + + "docker context inspect --format '{{.Endpoints.docker.Host}}' 2>/dev/null; " + + "echo \"DOCKER_HOST=$DOCKER_HOST\"")); + ResetSocketPathCommand = new RelayCommand(_ => + { + SocketPathInput = SocketPathDefault; + return ApplySocketPathAsync(); + }); + ClearFinishedTasksCommand = new RelayCommand(_ => Tasks.ClearFinished()); + + RelayCommand.UnhandledCommandError += ex => Feedback.ReportError("操作", ex); + Settings.Changed += () => + { + _ = Settings.SaveAsync(_lifetime.Token); + // 关掉"实时统计"要立刻停下采样,而不是等下次切页 —— + // 那个开关的整个卖点就是省远端开销。 + if (IsReady) + { + Containers.StartSampling(); + } + }; + } + + /// 宿主上下文。 + public IPluginContext Context { get; } + + /// 面板设置。 + public PanelSettings Settings { get; } + + /// 确认闸门。 + public ConfirmGate Confirm { get; } + + /// 任务中心。 + public TaskCenter Tasks { get; } + + /// 结果反馈(状态栏 + toast)。 + public Feedback Feedback { get; } + + /// 当前端点的客户端;没连上时为 + public DockerClient? Client { get; private set; } + + /// Compose 通道;没连上时为 + public ComposeCli? Compose { get; private set; } + + /// 仓库凭据。 + public RegistryAuthProvider? RegistryAuth { get; private set; } + + /// 面板生命周期令牌。 + public CancellationToken Lifetime => _lifetime.Token; + + // ── 端点 ────────────────────────────────────────────────────── + + /// 可选的端点。 + public ObservableCollection Endpoints { get; } = []; + + /// 当前端点。 + public EndpointItem? SelectedEndpoint + { + get; private set + { + if (SetField(ref field, value)) + { + SocketPathInput = value?.Endpoint.SocketPath ?? ""; + OnPropertiesChanged(nameof(EndpointName), nameof(EndpointDetail), nameof(HasEndpoint), + nameof(ComposeAvailable), nameof(SocketPathDefault), nameof(SocketPathChanged)); + } + } + } + + /// 顶栏显示的主机名。 + public string EndpointName => SelectedEndpoint?.DisplayName ?? "选择目标"; + + /// 顶栏主机名后面那行小字。 + public string EndpointDetail => SelectedEndpoint is { } item + ? $"{item.Endpoint.SocketPath} · {(item.IsLocal ? "本机" : "SSH 隧道")}" + : ""; + + /// 选了端点没有。 + public bool HasEndpoint => SelectedEndpoint is not null; + + /// + /// 设置抽屉里那个 socket 路径输入框。 + /// + /// 存在的理由:"这台机器上找不到 docker.sock"的补救动作就是换一条路径 + /// (rootless Docker 在 $XDG_RUNTIME_DIR/docker.sock,Colima、OrbStack 各有各的位置)。 + /// 补救按钮把设置抽屉打开却没有这个框,等于把用户领到一堵墙前面。 + /// + /// + public string SocketPathInput + { + get; set + { + if (SetField(ref field, value)) + { + OnPropertyChanged(nameof(SocketPathChanged)); + } + } + } = ""; + + /// 当前端点默认的 socket 路径(用来判断"是不是改过了")。 + public string SocketPathDefault => + SelectedEndpoint?.Endpoint.Kind == DockerEndpointKind.Local && OperatingSystem.IsWindows() + ? DockerEndpoint.DefaultWindowsPipe + : DockerEndpoint.DefaultUnixSocket; + + /// 改过 socket 路径没有(决定"恢复默认"要不要亮)。 + public bool SocketPathChanged => + SocketPathInput.Trim().Length > 0 && SocketPathInput.Trim() != SocketPathDefault; + + /// 主机切换器是否展开。 + public bool EndpointMenuOpen { get; set => SetField(ref field, value); } + + /// + /// Compose 页可不可用。 + /// + /// 只取决于**有没有连上** —— 远端与本机都有各自的执行通道 + /// (见 )。至于那台机器上到底装没装 docker compose, + /// 是 Compose 页自己 compose version 探出来的,不该在这里猜。 + /// + /// + public bool ComposeAvailable => Compose is not null; + + // ── 连接状态 ────────────────────────────────────────────────── + + /// 连接状态。 + public PanelConnectionState State + { + get; private set + { + if (SetField(ref field, value)) + { + OnPropertiesChanged(nameof(IsConnecting), nameof(IsReady), nameof(IsFailed), nameof(NeedsEndpoint), + nameof(EventsDegraded)); + RefreshCommand.RaiseCanExecuteChanged(); + } + } + } = PanelConnectionState.NoEndpoint; + + /// 还没选端点。 + public bool NeedsEndpoint => State == PanelConnectionState.NoEndpoint; + + /// 正在建立通道。 + public bool IsConnecting => State == PanelConnectionState.Connecting; + + /// 连上了。 + public bool IsReady => State == PanelConnectionState.Ready; + + /// 连不上。 + public bool IsFailed => State == PanelConnectionState.Failed; + + /// 连不上时的标题。 + public string ErrorTitle { get; private set => SetField(ref field, value); } = ""; + + /// 连不上时的正文。 + public string ErrorDetail { get; private set => SetField(ref field, value); } = ""; + + /// 连不上时下面那行等宽小字(daemon 的原话)。 + public string ErrorHint { get; private set => SetField(ref field, value); } = ""; + + /// 连不上时的图标。 + public string ErrorIcon { get; private set => SetField(ref field, value); } = "Icon.circle-alert"; + + /// 连不上时给的出路。 + public ObservableCollection RecoveryActions { get; } = []; + + // ── 导航 ────────────────────────────────────────────────────── + + /// 总览页。 + public OverviewPageViewModel Overview { get; } + + /// 容器页。 + public ContainersPageViewModel Containers { get; } + + /// 镜像页。 + public ImagesPageViewModel Images { get; } + + /// 卷页。 + public VolumesPageViewModel Volumes { get; } + + /// 网络页。 + public NetworksPageViewModel Networks { get; } + + /// Compose 页。 + public ComposePageViewModel ComposePage { get; } + + /// 系统页。 + public SystemPageViewModel SystemPage { get; } + + /// 全部页面。 + public IReadOnlyList AllPages { get; } + + /// 当前页标识(左导航栏据此选中)。 + public PanelPage CurrentPage + { + get; private set + { + if (SetField(ref field, value)) + { + OnPropertiesChanged(nameof(IsOverview), nameof(IsContainers), nameof(IsImages), + nameof(IsVolumes), nameof(IsNetworks), nameof(IsCompose), nameof(IsSystem)); + } + } + } = PanelPage.Overview; + + /// 当前页的视图模型。 + public PageViewModel? ActivePage + { + get => _activePage; + private set => SetField(ref _activePage, value); + } + + /// 当前是总览页。 + public bool IsOverview => CurrentPage == PanelPage.Overview; + + /// 当前是容器页。 + public bool IsContainers => CurrentPage == PanelPage.Containers; + + /// 当前是镜像页。 + public bool IsImages => CurrentPage == PanelPage.Images; + + /// 当前是卷页。 + public bool IsVolumes => CurrentPage == PanelPage.Volumes; + + /// 当前是网络页。 + public bool IsNetworks => CurrentPage == PanelPage.Networks; + + /// 当前是 Compose 页。 + public bool IsCompose => CurrentPage == PanelPage.Compose; + + /// 当前是系统页。 + public bool IsSystem => CurrentPage == PanelPage.System; + + // ── 状态栏 ──────────────────────────────────────────────────── + + /// daemon 版本。 + public string EngineVersion { get; private set => SetField(ref field, value); } = ""; + + /// API 版本。 + public string ApiVersion { get; private set => SetField(ref field, value); } = ""; + + /// “18 容器 · 34 镜像 · 9 卷”。 + public string CountsText { get; private set => SetField(ref field, value); } = ""; + + /// 任务中心弹层是否展开。 + public bool TaskCenterOpen { get; set => SetField(ref field, value); } + + /// 设置抽屉是否展开。 + public bool SettingsOpen { get; set => SetField(ref field, value); } + + // ── 命令 ────────────────────────────────────────────────────── + + /// 切页。 + public RelayCommand SelectPageCommand { get; } + + /// 手动刷新当前页。 + public RelayCommand RefreshCommand { get; } + + /// 重连。 + public RelayCommand ReconnectCommand { get; } + + /// 选一个端点。 + public RelayCommand SelectEndpointCommand { get; } + + /// 展开/收起主机切换器。 + public RelayCommand ToggleEndpointMenuCommand { get; } + + /// 展开/收起任务中心。 + public RelayCommand ToggleTaskCenterCommand { get; } + + /// 展开/收起设置。 + public RelayCommand ToggleSettingsCommand { get; } + + /// 面板内的命令面板(Ctrl+K)。 + public CommandPalette Palette { get; } + + /// 打开命令面板。 + public RelayCommand OpenPaletteCommand { get; } + + /// + /// 收集当前能做的事。 + /// + /// 每次打开都重新收集,不缓存 —— 容器列表随时在变,一份缓存下来的命令列表 + /// 会让用户对着一个已经不存在的容器按回车。 + /// + /// + private List CollectPaletteEntries() + { + List entries = []; + if (!IsReady) + { + return entries; + } + + // ── 动作:对具体容器的高频操作。放最前面,因为它们是"要做一件事"而不是"要看一眼"。 + foreach (var row in Containers.View.Where(r => r.IsRunning).Take(20)) + { + var target = row; + entries.Add(new("动作", $"重启 {target.Name}", DescribeContainer(target), "Docker.rotate-cw", + RowTone.Ok, false, () => { Containers.RestartCommand.Execute(target); return Task.CompletedTask; })); + entries.Add(new("动作", $"停止 {target.Name}", DescribeContainer(target), "Icon.square", + RowTone.Idle, false, () => { Containers.StopCommand.Execute(target); return Task.CompletedTask; })); + } + + // ── 容器 / 镜像 / 卷:导航到某个对象。 + foreach (var row in Containers.View.Take(40)) + { + var target = row; + entries.Add(new("容器", target.Name, DescribeContainer(target), "Docker.box", + target.Tone, false, () => Containers.OpenDetailCommand is { } open + ? Task.Run(() => Ui.Post(() => open.Execute(target))) + : Task.CompletedTask)); + } + foreach (var row in Images.View.Take(40)) + { + var target = row; + entries.Add(new("镜像 / 卷", $"{target.Repository}:{target.Tag}", $"镜像 · {target.SizeText}", + "Icon.layers", RowTone.Idle, false, + () => { Images.OpenDetailCommand.Execute(target); return Task.CompletedTask; })); + } + foreach (var row in Volumes.View.Take(40)) + { + var target = row; + entries.Add(new("镜像 / 卷", target.Name, $"卷 · {target.SizeText}", "Icon.hard-drive", + RowTone.Idle, false, () => { Volumes.SelectCommand.Execute(target); return Task.CompletedTask; })); + } + + // ── 面板命令:导航与全局动作。破坏性的带省略号,选中后仍走闸门。 + entries.Add(new("面板命令", "拉取镜像…", "从仓库拉一个镜像", "Docker.arrow-down-to-line", + RowTone.Idle, false, () => ShowPullDialogAsync(null))); + entries.Add(new("面板命令", "清理未使用的镜像…", "释放磁盘,重新拉要花时间与带宽", "Docker.broom", + RowTone.Warn, true, () => { Images.PruneAllCommand.Execute(null); return Task.CompletedTask; })); + entries.Add(new("面板命令", "清理悬空镜像…", "只删没有标签也没人用的中间层", "Docker.broom", + RowTone.Idle, true, () => { Images.PruneDanglingCommand.Execute(null); return Task.CompletedTask; })); + entries.Add(new("面板命令", "打开设置", "连接、显示、行为", "Icon.settings", + RowTone.Idle, false, () => { SettingsOpen = true; return Task.CompletedTask; })); + foreach ((var page, var title, var icon) in ((PanelPage, string, string)[]) + [ + (PanelPage.Overview, "总览", "Docker.layout-dashboard"), + (PanelPage.Containers, "容器", "Docker.box"), + (PanelPage.Images, "镜像", "Icon.layers"), + (PanelPage.Volumes, "卷", "Icon.hard-drive"), + (PanelPage.Networks, "网络", "Icon.network"), + (PanelPage.System, "系统", "Icon.gauge") + ]) + { + var target = page; + entries.Add(new("面板命令", $"转到{title}", "切换页面", icon, RowTone.Idle, false, + () => GoToAsync(target))); + } + return entries; + } + + private static string DescribeContainer(ContainerRow row) => + row.HasProject ? $"{(row.IsRunning ? "运行中" : row.Uptime)} · {row.Project}" + : $"{(row.IsRunning ? "运行中" : row.Uptime)} · {row.Image}"; + + /// 设置默认补多少行历史。 + public RelayCommand SetLogTailCommand { get; } + + /// 用输入框里的路径重连。 + public RelayCommand ApplySocketPathCommand { get; } + + /// 恢复默认 socket 路径并重连。 + public RelayCommand ResetSocketPathCommand { get; } + + /// + /// 常见的 socket 位置。摆在设置里而不是写进文档: + /// 用户是在"连不上"的当口来找它的,那一刻他不会去翻文档。 + /// + public IReadOnlyList SocketHints { get; } = + [ + new("/var/run/docker.sock", "标准安装"), + new("$XDG_RUNTIME_DIR/docker.sock", "rootless"), + new("~/.docker/run/docker.sock", "Docker Desktop"), + new("~/.colima/default/docker.sock", "Colima") + ]; + + /// + /// 把"这台机器的 socket 到底在哪"这条命令送到宿主终端里去问。 + /// + /// 面板不替用户猜:rootless、Colima、OrbStack、Docker Desktop 各摆各的位置, + /// 而**远端自己**一句话就能答上来。本机端点没有终端可送,那就把命令原样告诉他。 + /// + /// + public RelayCommand DiscoverSocketCommand { get; } + + /// 清掉已完成的任务。 + public RelayCommand ClearFinishedTasksCommand { get; } + + // ── 生命周期 ────────────────────────────────────────────────── + + /// 面板打开时调用:读设置、列端点、自动连上唯一那个。 + public async Task InitializeAsync() + { + await Settings.LoadAsync(_lifetime.Token).ConfigureAwait(true); + Context.Events.SessionConnected += OnSessionChanged; + Context.Events.SessionDisconnected += OnSessionChanged; + await ReloadEndpointsAsync().ConfigureAwait(true); + // 只有一个可用端点时直接连上 —— 让用户为一个没得选的选择再点一次没有意义。 + EndpointItem[] usable = [.. Endpoints.Where(e => e.Available)]; + if (usable.Length == 1) + { + await ConnectAsync(usable[0]).ConfigureAwait(true); + } + } + + /// 重新列一遍端点(会话连上/断开时也会走这里)。 + public async Task ReloadEndpointsAsync() + { + IReadOnlyList sessions; + try + { + sessions = await Context.Sessions.ListAsync(_lifetime.Token).ConfigureAwait(true); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + sessions = []; + } + List items = + [ + new(DockerEndpoint.Local("本机 Docker"), true, "可用", FeedbackKind.Info) + ]; + foreach (var session in sessions.OrderBy(s => s.Host, StringComparer.OrdinalIgnoreCase)) + { + var connected = session.State == SessionState.Connected; + items.Add(new( + DockerEndpoint.Remote(session.SessionId, session.Host, $"{session.Username}@{session.Host}:{session.Port}"), + connected, + connected ? "已连接" : "未连接", + connected ? FeedbackKind.Success : FeedbackKind.Info)); + } + Ui.Post(() => + { + Endpoints.Clear(); + foreach (var item in items) + { + Endpoints.Add(item); + } + if (State == PanelConnectionState.NoEndpoint) + { + SetNoEndpoint(sessions.Count(s => s.State == SessionState.Connected)); + } + }); + } + + private void OnSessionChanged(SessionInfo session) => _ = ReloadEndpointsAsync(); + + /// + /// 连到一个端点:建客户端 → 探一次 /version → 起事件流 → 刷新当前页。 + /// + public async Task ConnectAsync(EndpointItem? item) + { + if (item is null) + { + return; + } + await StopEventStreamAsync().ConfigureAwait(true); + if (Client is not null) + { + await Client.DisposeAsync().ConfigureAwait(true); + Client = null; + } + // 通道跟着连接一起作废 —— 留着旧的,导航栏会显示 Compose 可点, + // 点进去却是上一台机器的项目。 + Compose = null; + OnPropertyChanged(nameof(ComposeAvailable)); + foreach (var page in AllPages) + { + page.Reset(); + } + SelectedEndpoint = item; + State = PanelConnectionState.Connecting; + RecoveryActions.Clear(); + + var endpoint = item.Endpoint; + var remembered = await Settings.GetSocketPathAsync(endpoint.DisplayName, _lifetime.Token).ConfigureAwait(true); + if (!string.IsNullOrWhiteSpace(remembered) && remembered != endpoint.SocketPath) + { + endpoint = endpoint with { SocketPath = remembered }; + } + // 输入框显示的是**实际用的**那条路径,而不是端点的出厂默认值。 + SocketPathInput = endpoint.SocketPath; + IDockerTransport transport = endpoint.Kind == DockerEndpointKind.Local + ? new LocalTransport(endpoint.SocketPath) + : new TunnelTransport(Context.RemoteTunnel, endpoint.SessionId, endpoint.SocketPath); + var client = new DockerClient(endpoint, transport); + try + { + var version = await client.PingAsync(_lifetime.Token).ConfigureAwait(true); + Client = client; + RegistryAuth = new(Context.RemoteFs, endpoint); + // compose 只有 CLI,所以要一条"跑命令"的通道:远端是 SSH,本机是本地进程。 + Compose = new ComposeCli(endpoint.Kind == DockerEndpointKind.Remote + ? new RemoteComposeHost(Context.RemoteExec, Context.RemoteFs, endpoint.SessionId) + : new LocalComposeHost()); + OnPropertyChanged(nameof(ComposeAvailable)); + EngineVersion = version.Version is { Length: > 0 } v ? $"Engine {v}" : ""; + ApiVersion = version.ApiVersion is { Length: > 0 } a ? $"API v{a}" : ""; + State = PanelConnectionState.Ready; + item.Update(true, "通道已建立", FeedbackKind.Success); + Feedback.Status(FeedbackKind.Success, $"已连上 {endpoint.DisplayName} 的 Docker"); + StartEventStream(); + // 容器列表 + 统计采样在后台先跑起来:总览页那几张卡靠它喂, + // 而用户可能整段时间都不会点开容器页。 + _ = Containers.PrimeAsync(_lifetime.Token); + await RefreshActiveAsync().ConfigureAwait(true); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + await client.DisposeAsync().ConfigureAwait(true); + SetFailed(ex, item); + } + } + + /// + /// 记住这台主机的 socket 路径并重连。 + /// + /// 路径按主机存,不按会话 id —— 会话 id 每次重连都换, + /// 而"这台机器的 docker 在哪儿"是这台机器的属性,不该跟着会话一起过期。 + /// + /// + private async Task ApplySocketPathAsync() + { + if (SelectedEndpoint is not { } item) + { + return; + } + var path = SocketPathInput.Trim(); + if (path.Length == 0) + { + Feedback.Status(FeedbackKind.Warning, "socket 路径不能为空。"); + return; + } + await Settings.SetSocketPathAsync(item.DisplayName, path, _lifetime.Token).ConfigureAwait(true); + SettingsOpen = false; + OnPropertyChanged(nameof(SocketPathChanged)); + await ConnectAsync(item).ConfigureAwait(true); + } + + /// + /// 把一条排查命令送进宿主的终端(如同用户键入,需要授权)。 + /// + /// 送过去而不是面板自己跑:排查这类命令的输出常常要人读、要接着改着再跑一遍, + /// 那是终端的主场;而且它经宿主的授权闸,面板不越过那道门。 + /// + /// + /// + /// 把一条命令送到宿主终端里去(补救动作用:登录 registry、查 socket、看 daemon 日志)。 + /// 本机端点没有终端可送 —— 那就把命令原样告诉用户,让他自己敲。 + /// + public async Task SendToHostTerminalAsync(string command) + { + if (SelectedEndpoint?.Endpoint is not { Kind: DockerEndpointKind.Remote } endpoint) + { + Feedback.Notify(FeedbackKind.Info, "本机端点没有终端可送", $"自己执行:{command}"); + return; + } + try + { + await Context.Terminal.WriteAsync(endpoint.SessionId, command + "\n", _lifetime.Token) + .ConfigureAwait(true); + Feedback.Notify(FeedbackKind.Info, "已送到宿主终端", command); + } + catch (PluginPermissionDeniedException) + { + Feedback.Notify(FeedbackKind.Warning, "没有向终端回写的授权", $"可以自己执行:{command}"); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Feedback.ReportError("送往宿主终端", ex); + } + } + + private void SetNoEndpoint(int connectedSessions) + { + State = PanelConnectionState.NoEndpoint; + ErrorIcon = "Icon.plug"; + ErrorTitle = "选一条已连接的 SSH 会话开始"; + ErrorDetail = "面板不自己发起连接 —— 凭据永远不出宿主核心。选定后会在这条会话上打开一条到 docker.sock 的通道。"; + ErrorHint = connectedSessions switch + { + 0 => "当前没有已连接的会话 —— 也可以直接管本机 Docker。", + 1 => "当前宿主有 1 条已连接会话。", + _ => $"当前宿主有 {connectedSessions} 条已连接会话。" + }; + RecoveryActions.Clear(); + RecoveryActions.Add(new("选择会话…", "Icon.chevron-down", true, () => EndpointMenuOpen = true)); + RecoveryActions.Add(new("管理本机 Docker", "Docker.monitor", false, () => + _ = ConnectAsync(Endpoints.FirstOrDefault(e => e.IsLocal)))); + } + + private void SetFailed(Exception ex, EndpointItem item) + { + State = PanelConnectionState.Failed; + RecoveryActions.Clear(); + if (ex is DockerUnreachableException unreachable) + { + ErrorHint = unreachable.InnerException?.Message ?? ""; + switch (unreachable.Reason) + { + case DockerUnreachableReason.SocketMissing: + ErrorIcon = "Docker.circle-x"; + ErrorTitle = "打不开到 docker.sock 的通道"; + ErrorDetail = "路径不存在、daemon 没在跑,或者当前账号根本碰不到这个 socket —— 远端只回了一句笼统的失败,自己分不出是哪一种。下面这条命令一次看清。"; + // 三种可能一条命令全覆盖:socket 在不在、当前账号读不读得动它、daemon 跑没跑。 + // 关键是**替他判**,而不是把 `ls -l` 和 `id` 两串输出丢过去让他自己对 —— + // 权限这一档最常见,而 "srw-rw---- root docker" 与 "groups=Users" + // 要交叉比对才看得出来。 + RecoveryActions.Add(new("在终端里检查", "Icon.terminal", true, + () => _ = SendToHostTerminalAsync( + $"s={item.Endpoint.SocketPath}; " + + "if [ ! -S \"$s\" ]; then echo \"没有 $s —— 换条路径,或者远端压根没装 Docker\"; " + + "elif [ -r \"$s\" ] && [ -w \"$s\" ]; then echo \"$s 可读写 —— 问题不在权限\"; " + + "else echo \"$s 在,但当前账号读写不了:它属组 $(stat -c %G \"$s\" 2>/dev/null)," + + "而你在 $(id -nG)\"; fi; " + + "systemctl status docker --no-pager | head -3"))); + RecoveryActions.Add(new("换一个 socket 路径", "Icon.settings", false, () => SettingsOpen = true)); + item.Update(false, "打不开通道", FeedbackKind.Error); + break; + case DockerUnreachableReason.PermissionDenied: + ErrorIcon = "Docker.lock"; + ErrorTitle = "当前账号没有 docker.sock 的读写权限"; + ErrorDetail = "账号不在 socket 的属组里。把账号加进 docker 组,或者换一个有权限的账号 —— " + + "面板不会替你 sudo,那需要一个它拿不到也不该拿的口令。"; + // 这一句不是啰嗦:组是**登录时**读进去的,usermod 改完之后当前这条 SSH 会话 + // 仍然带着旧的组。不说清楚的话,用户会以为那条命令没生效,然后反复再跑一遍。 + ErrorHint = "改完要重开一条 SSH 会话再连 —— 组是登录时定的,现有会话不会自己更新。" + + "另外:docker 组等同于 root(能挂载宿主根目录),给之前先掂量一下。"; + RecoveryActions.Add(new("加进 docker 组", "Docker.users", true, + () => _ = SendToHostTerminalAsync("sudo usermod -aG docker $USER"))); + // sudo -n 只在**已经配了免密**时才成 —— 面板绝不弹口令框, + // 那需要一个它拿不到也不该拿的东西。配不了就如实报出来。 + RecoveryActions.Add(new("试试 sudo -n", "Docker.shield-alert", false, + () => _ = SendToHostTerminalAsync( + $"sudo -n test -r {item.Endpoint.SocketPath} && echo OK || echo '需要口令 —— 面板走不了这条路'"))); + item.Update(true, "没有权限", FeedbackKind.Warning); + break; + case DockerUnreachableReason.TunnelUnsupported: + ErrorIcon = "Icon.triangle-alert"; + ErrorTitle = "这个宿主不支持远程隧道"; + ErrorDetail = "Docker 面板需要 hostMode = inProcess:它交出去的是一条活的字节流,跨进程代理不了。"; + break; + case DockerUnreachableReason.SessionUnavailable: + ErrorIcon = "Icon.wifi-off"; + ErrorTitle = "这条 SSH 会话已经断开"; + ErrorDetail = "重新连上之后再选一次。"; + item.Update(false, "已断开", FeedbackKind.Error); + break; + default: + ErrorIcon = "Icon.circle-alert"; + ErrorTitle = "连不上 Docker"; + ErrorDetail = unreachable.Message; + break; + } + } + else + { + ErrorIcon = "Icon.circle-alert"; + ErrorTitle = "连不上 Docker"; + ErrorDetail = ex.Message; + ErrorHint = ""; + } + RecoveryActions.Add(new("重试", "Icon.refresh-cw", RecoveryActions.Count == 0, () => _ = ConnectAsync(item))); + Context.Log.Warn($"connect to docker failed: {ex.Message}"); + // 上面那一屏是"还不知道是哪种连不上"时的说法。真去问一句要跑一条远端命令, + // 不该让用户对着转圈等 —— 先显示笼统的版本,探出结果再替换成具体的那一屏。 + if (ex is DockerUnreachableException + { + Reason: DockerUnreachableReason.SocketMissing + or DockerUnreachableReason.PermissionDenied or DockerUnreachableReason.Unknown + }) + { + _ = RefineFailureAsync(item); + } + } + + /// + /// 连不上之后,替用户把"到底是哪种连不上"问清楚,再用一句人话重写这一屏。 + /// + /// 为什么值得多跑一次:sshd 打不开通道时只回一句笼统的失败,面板分不出"没这个文件" + /// 与"你没权限"。而这两件事要做的完全不一样。分不出来,界面就只能说 + /// "自己去终端看看",再把 ls -lid 两串输出丢给用户交叉比对 —— + /// 那是把诊断工作外包给了最不该做诊断的人。 + /// + /// + /// 探测是只读的,而且**不阻塞**上面那一屏:先把笼统的版本显示出来,探出结果再替换。 + /// 探不出来就什么都不动。 + /// + /// + private async Task RefineFailureAsync(EndpointItem item) + { + var probe = await SocketProbe + .RunAsync(Context.RemoteExec, item.Endpoint, _lifetime.Token).ConfigureAwait(true); + // 探测期间用户可能已经换了端点、或者又连上了 —— 那就别再动这一屏。 + if (State != PanelConnectionState.Failed || !ReferenceEquals(SelectedEndpoint, item)) + { + return; + } + switch (probe.Kind) + { + case SocketProbeKind.PermissionDenied: + ShowPermissionDenied(item, probe); + break; + case SocketProbeKind.Missing: + ShowSocketMissing(item); + break; + case SocketProbeKind.Ready: + // 文件在、也读得动,却还是连不上 —— 那就不是这两件事。别再引导用户去加组。 + ErrorHint = $"{item.Endpoint.SocketPath} 存在而且当前账号读写得动,所以问题不在路径也不在权限。" + + "多半是 Docker 服务本身没起来,或者这条 SSH 会话中途断了。"; + break; + } + } + + /// + /// "没权限"这一屏。 + /// + /// 句子里的账号名与组名都是**探出来的真名**,不是"当前账号""某个组"。 + /// 用户不需要知道什么叫属组,他只需要看懂"joes 不在 docker 里面,把它加进去"。 + /// + /// + private void ShowPermissionDenied(EndpointItem item, SocketProbeResult probe) + { + var account = probe.Account is { Length: > 0 } a ? a : "当前账号"; + var group = probe.Group is { Length: > 0 } g ? g : "docker"; + ErrorIcon = "Docker.lock"; + ErrorTitle = $"账号「{account}」还不被允许使用这台机器上的 Docker"; + ErrorDetail = $"Docker 装着、也在跑,只是它只对 {group} 组开放,而 {account} 不在这个组里" + + (probe.Groups is { Length: > 0 } groups ? $"(它现在属于:{groups})" : "") + + $"。把 {account} 加进 {group} 组就能用了 —— 这一步要管理员口令,面板不会替你做。"; + // 这一句不是啰嗦:账号属于哪些组是**登录那一刻**定下来的。改完不重连, + // 用户会以为那条命令没生效,然后反复再跑一遍。 + ErrorHint = "加完之后要重新连一次这条 SSH 会话才算数 —— 账号属于哪些组是登录时定下来的," + + "现在这条连接还带着旧的。 · 提醒:进了这个组就等于拿到这台机器的完全控制权,加之前先想清楚。"; + RecoveryActions.Clear(); + RecoveryActions.Add(new($"把 {account} 加进 {group} 组", "Docker.users", true, + () => _ = SendToHostTerminalAsync($"sudo usermod -aG {group} {account}"))); + RecoveryActions.Add(new("改好了,重新连", "Icon.refresh-cw", false, () => _ = ConnectAsync(item))); + item.Update(true, "没有权限", FeedbackKind.Warning); + } + + /// "那个位置上没有 socket"这一屏。三种原因各给一条出路。 + private void ShowSocketMissing(EndpointItem item) + { + ErrorIcon = "Docker.circle-x"; + ErrorTitle = "这台机器上找不到 Docker 的接口"; + ErrorDetail = $"面板要找的是 {item.Endpoint.SocketPath} 这个文件,但它不在那儿。" + + "常见的就三种:这台机器没装 Docker、Docker 服务没启动,或者它把接口放在了别的位置。"; + ErrorHint = ""; + RecoveryActions.Clear(); + RecoveryActions.Add(new("看看 Docker 服务在不在跑", "Icon.terminal", true, + () => _ = SendToHostTerminalAsync("systemctl status docker --no-pager | head -5"))); + RecoveryActions.Add(new("换一个位置", "Icon.settings", false, () => SettingsOpen = true)); + RecoveryActions.Add(new("重新连", "Icon.refresh-cw", false, () => _ = ConnectAsync(item))); + item.Update(false, "找不到接口", FeedbackKind.Error); + } + + /// 切页。 + public async Task GoToAsync(PanelPage page) + { + if (page == PanelPage.Compose && !ComposeAvailable) + { + Feedback.Status(FeedbackKind.Info, "还没连上 Docker —— Compose 页要等通道建立之后才能用。"); + return; + } + CurrentPage = page; + PageViewModel target = page switch + { + PanelPage.Overview => Overview, + PanelPage.Images => Images, + PanelPage.Volumes => Volumes, + PanelPage.Networks => Networks, + PanelPage.Compose => ComposePage, + PanelPage.System => SystemPage, + _ => Containers + }; + ActivePage = target; + if (IsReady) + { + try + { + await target.ActivateAsync(_lifetime.Token).ConfigureAwait(true); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Feedback.ReportError(target.Title, ex); + } + } + } + + /// 刷新当前页。 + public async Task RefreshActiveAsync() + { + if (!IsReady || ActivePage is not { } page) + { + return; + } + try + { + await page.RefreshAsync(_lifetime.Token).ConfigureAwait(true); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Feedback.ReportError(page.Title, ex); + } + } + + /// 状态栏那串计数:容器数。 + public void SetContainerCount(int count) => SetCount(ref _countContainers, count); + + /// 状态栏那串计数:镜像数。 + public void SetImageCount(int count) => SetCount(ref _countImages, count); + + /// 状态栏那串计数:卷数。 + public void SetVolumeCount(int count) => SetCount(ref _countVolumes, count); + + /// + /// 三个数字分开记。 + /// + /// 早先是一个 SetCounts(a, b, c),每个调用方只知道自己那一个数, + /// 另外两个只能去读别的页 —— 而那些页在被打开之前都是 0, + /// 于是状态栏在用户逛到那一页之前一直显示"0 镜像"。 + /// + /// + private void SetCount(ref int slot, int count) + { + if (slot == count) + { + return; + } + slot = count; + CountsText = $"{_countContainers} 容器 · {_countImages} 镜像 · {_countVolumes} 卷"; + } + + /// + /// 给一条确认请求补上主机信息。 + /// + /// 调用方不必自己填 —— 漏填一次的代价是一个不写清主机的"确定删除 3 个卷吗", + /// 而那正是这个面板能犯的最贵的错误。所以这条路是**唯一**打开闸门的路。 + /// + /// + public ConfirmRequest BuildConfirm(ConfirmRequest request) => request with + { + HostName = SelectedEndpoint?.DisplayName ?? "(未选择)", + HostDetail = SelectedEndpoint is { } item + ? $"· {item.Detail} · {item.Endpoint.SocketPath}" + : "", + HostWarning = SelectedEndpoint?.IsLocal == false + }; + + /// + public async ValueTask DisposeAsync() + { + Context.Events.SessionConnected -= OnSessionChanged; + Context.Events.SessionDisconnected -= OnSessionChanged; + Confirm.CancelPending(); + Tasks.CancelAll(); + await _lifetime.CancelAsync().ConfigureAwait(false); + await StopEventStreamAsync().ConfigureAwait(false); + foreach (var page in AllPages) + { + if (page is IAsyncDisposable disposable) + { + await disposable.DisposeAsync().ConfigureAwait(false); + } + } + if (Client is not null) + { + await Client.DisposeAsync().ConfigureAwait(false); + Client = null; + } + _lifetime.Dispose(); + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerStyles.axaml b/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerStyles.axaml new file mode 100644 index 0000000..b1ccd67 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerStyles.axaml @@ -0,0 +1,435 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerTheme.axaml b/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerTheme.axaml new file mode 100644 index 0000000..9a42348 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerTheme.axaml @@ -0,0 +1,377 @@ + + + + + + + + + + + + + + + + + M4 12.5h16.5c0 3.6-2.4 6.5-6.5 6.5H8.5C5.5 19 4 16.5 4 13.5z M6 12V9.5h2.5V12 M9 12V9.5h2.5V12 M12 12V9.5h2.5V12 M9 9V6.5h2.5V9 M15 12V9.5h2.5V12 M19 10c1-1 2.5-.8 3 0-.4 1.2-1.7 1.6-3 1.4 + M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z M3.3 7 12 12l8.7-5 M12 22V12 + M7 6.5 12 4l5 2.5V11l-5 2.5L7 11z M2.5 14.5 7 12.5l4.5 2v4.5L7 21l-4.5-2.5z M12.5 14.5 17 12.5l4.5 2v4.5L17 21l-4.5-2.5z + M3 4a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z M14 3h6a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z M14 12h6a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1z M4 15h5a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1z + M21 12a9 9 0 1 1-2.64-6.36L21 8 M21 3v5h-5 + M15 12h-5 M15 8h-5 M19 17V5a2 2 0 0 0-2-2H4 M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3 + M5 2h14a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3z M5 14h14a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3v-2a3 3 0 0 1 3-3z M6.5 6h.01 M6.5 18h.01 + M8 21h8 M12 17v4 M2 5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2z + M20 6 9 17l-5-5 + M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z M15 9l-6 6 M9 9l6 6 + M10.1 2.2a10 10 0 0 1 3.8 0 M13.9 21.8a10 10 0 0 1-3.8 0 M17.6 3.7a10 10 0 0 1 2.7 2.7 M2.2 13.9a10 10 0 0 1 0-3.8 M20.3 17.6a10 10 0 0 1-2.7 2.7 M21.8 10.1a10 10 0 0 1 0 3.8 M3.7 6.4a10 10 0 0 1 2.7-2.7 M6.4 20.3a10 10 0 0 1-2.7-2.7 + M3 5a9 3 0 1 0 18 0 9 3 0 1 0-18 0 M3 5v14a9 3 0 0 0 18 0V5 M3 12a9 3 0 0 0 18 0 + M12.6 2.6A2 2 0 0 0 11.2 2H4a2 2 0 0 0-2 2v7.2a2 2 0 0 0 .6 1.4l8.7 8.7a2.4 2.4 0 0 0 3.4 0l6.6-6.6a2.4 2.4 0 0 0 0-3.4z M7 7h.01 + M5 12h14 M12 5l7 7-7 7 + M12 17V3 M6 11l6 6 6-6 M19 21H5 + M16 22l-1-4 M19 14a1 1 0 0 0 1-1v-1a2 2 0 0 0-2-2h-3a1 1 0 0 1-1-1V4a2 2 0 0 0-4 0v5a1 1 0 0 1-1 1H6a2 2 0 0 0-2 2v1a1 1 0 0 0 1 1 M5 14h14l2 6.8a1 1 0 0 1-1 1.2H4a1 1 0 0 1-1-1.2z M8 22l1-4 + M7 11l2-2-2-2 M11 13h4 M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z + M3 12a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z M7 10V7a5 5 0 0 1 10 0v3 + M18 21a8 8 0 0 0-16 0 M10 13a5 5 0 1 0 0-10 5 5 0 0 0 0 10z M22 20c0-3.4-2-6.5-4-8a5 5 0 0 0-.5-8.3 + M12 17v5 M9 10.8a2 2 0 0 1-1.1 1.8l-1.8.9A2 2 0 0 0 5 15.2V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.8a2 2 0 0 0-1.1-1.8l-1.8-.9A2 2 0 0 1 15 10.8V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z + M21 21H8a2 2 0 0 1-1.4-.6l-4-4a2 2 0 0 1 0-2.8l10-10a2 2 0 0 1 2.8 0l6 6a2 2 0 0 1 0 2.8L12.8 21 M5 11l4 4 + M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5c0 1.1.9 2 2 2h1 M16 21h1a2 2 0 0 0 2-2v-5c0-1.1.9-2 2-2a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2h-1 + M21 12h-8 M21 6H8 M21 18h-8 M3 6v4c0 1.1.9 2 2 2h3 M3 10v6c0 1.1.9 2 2 2h3 + M15 12l-8.4 8.4a1.5 1.5 0 1 1-2.1-2.1L12 9.9 M17.5 15L22 10.5 M21.5 11.5l-1.9-1.9A2 2 0 0 1 19 8.2V7l-2.3-2.3a6 6 0 0 0-4.2-1.7L9 3l.9.8A6.2 6.2 0 0 1 12 8.4V10l2 2h1.2a2 2 0 0 1 1.4.6l1.9 1.9 + M12 2v10 M18.4 6.6a9 9 0 1 1-12.8 0 + M10 12.5 8 15l2 2.5 M14 12.5l2 2.5-2 2.5 M14 2v4a2 2 0 0 0 2 2h4 M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z + M4 14h6v6 M20 10h-6V4 M14 10l7-7 M3 21l7-7 + M15 3h6v6 M9 21H3v-6 M21 3l-7 7 M3 21l7-7 + M5 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M19 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M5 15V9a4 4 0 0 1 4-4h3 M19 9v6a4 4 0 0 1-4 4h-3 M10 3 8 5l2 2 M14 21l2-2-2-2 + M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M6 15V9 M18 9a9 9 0 0 1-9 9 + M10 8h.01 M12 12h.01 M14 8h.01 M16 12h.01 M18 8h.01 M6 8h.01 M7 16h10 M8 12h.01 M2 6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2z + M9 14 4 9l5-5 M4 9h10.5a5.5 5.5 0 0 1 0 11H11 + M3 6h18 M7 12h10 M10 18h4 + M3 6h18 M3 12h15a3 3 0 1 1 0 6h-4 M16 16l-2 2 2 2 M3 18h4 + M14 2v4a2 2 0 0 0 2 2h4 M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z M10 6h1 M10 9h1 M10 12h1 M10 15h1 M9.5 18h2v2h-2z + M20 13c0 5-3.5 7.5-7.7 9a1 1 0 0 1-.6 0C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.2-2.7a1 1 0 0 1 1.6 0C14.5 3.8 17 5 19 5a1 1 0 0 1 1 1z M12 8v4 M12 16h.01 + M21.8 10a10 10 0 1 1-5.9-7.8 M9 11l3 3L22 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/DrawerLayout.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/DrawerLayout.cs new file mode 100644 index 0000000..02f3aeb --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/DrawerLayout.cs @@ -0,0 +1,116 @@ +using Avalonia.Controls; +using System.ComponentModel; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// +/// 把 (开着没有、最大化没有、多宽)翻译成外层那三列的宽度。 +/// 四个带抽屉的页面共用这一份。 +/// +/// 列定义由代码管而不是绑上去:分割条改的就是列定义本身,再往上套一层绑定只会两边打架 —— +/// 宿主的侧栏与文件面板也是这么写的。 +/// +/// +public sealed class DrawerLayout +{ + private static readonly GridLength Track = new(5); + private static readonly GridLength Collapsed = new(0); + private static readonly GridLength Fill = new(1, GridUnitType.Star); + + private readonly Control _host; + private readonly Grid _root; + private readonly double _listReserve; + private DrawerState? _drawer; + private double _hostWidth; + + /// 接上一个页面的外层布局。 + /// 页面视图。 + /// 外层那张三列 Grid 的名字(列表 / 5px 分割条 / 抽屉)。 + /// 分割条的名字。 + /// + /// 抽屉再宽也要给列表留下的宽度。抽屉比面板还宽的话,它的头(还原 / 关闭)会被顶出可视区, + /// 而那是回到列表的唯一入口;抽屉与列表同屏对照,本来也是这个布局存在的理由。 + /// + public DrawerLayout(Control host, string rootName, string splitterName, double listReserve = 360) + { + _host = host; + _root = host.GetControl(rootName); + _listReserve = listReserve; + host.GetControl(splitterName).DragCompleted += (_, _) => Capture(); + host.DataContextChanged += (_, _) => Rebind(); + host.SizeChanged += (_, e) => + { + _hostWidth = e.NewSize.Width; + Apply(); + }; + Rebind(); + } + + private void Rebind() + { + if (_drawer is { } previous) + { + previous.PropertyChanged -= OnDrawerChanged; + } + _drawer = (_host.DataContext as PageViewModel)?.Drawer; + if (_drawer is { } drawer) + { + drawer.PropertyChanged += OnDrawerChanged; + } + Apply(); + } + + private void OnDrawerChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is nameof(DrawerState.IsOpen) or nameof(DrawerState.Maximized) + or nameof(DrawerState.Width)) + { + Apply(); + } + } + + private void Apply() + { + if (_drawer is not { } drawer || _root.ColumnDefinitions.Count < 3) + { + return; + } + var list = _root.ColumnDefinitions[0]; + var splitter = _root.ColumnDefinitions[1]; + var panel = _root.ColumnDefinitions[2]; + + if (!drawer.IsOpen) + { + list.Width = Fill; + splitter.Width = Collapsed; + panel.Width = Collapsed; + panel.MinWidth = 0; + return; + } + if (drawer.Maximized) + { + list.Width = Collapsed; + splitter.Width = Collapsed; + panel.MinWidth = 0; + panel.MaxWidth = double.PositiveInfinity; + panel.Width = Fill; + return; + } + var max = Math.Max(DrawerState.MinWidth, _hostWidth - _listReserve); + list.Width = Fill; + splitter.Width = Track; + panel.MinWidth = DrawerState.MinWidth; + panel.MaxWidth = max; + panel.Width = new(Math.Clamp(drawer.Width, DrawerState.MinWidth, max)); + } + + /// 拖完把拖出来的实际宽度写回去 —— 与宿主侧栏那两条分割条一样。 + private void Capture() + { + if (_drawer is { } drawer && _root.ColumnDefinitions.Count >= 3 && + _root.ColumnDefinitions[2].ActualWidth > 0) + { + drawer.Width = _root.ColumnDefinitions[2].ActualWidth; + } + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/DrawerState.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/DrawerState.cs new file mode 100644 index 0000000..3042f14 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/DrawerState.cs @@ -0,0 +1,79 @@ +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// +/// 右侧详情抽屉的尺寸与形态。四个页面(容器 / 镜像 / 卷 / 网络)共用这一份逻辑。 +/// +/// 宽度存在**页面**上而不是抽屉自己的视图模型上:换一行就是换一个抽屉视图模型, +/// 宽度跟着它走的话,用户每点一行都得重拖一次。 +/// +/// +/// 与视图是双向的:开 / 还原时由视图把这个宽度写进列定义,用户拖完(DragCompleted) +/// 再由视图把拖出来的实际宽度写回来 —— 与宿主侧栏那两条分割条一样。 +/// 上界由列定义的 MaxWidth 兜着,因为"面板有多宽"只有视图知道。 +/// +/// +public sealed class DrawerState : ObservableObject +{ + /// 再窄就摆不下抽屉头里那一行了。 + public const double MinWidth = 360; + + /// 抽屉宽度(用户拖出来的)。设计稿四个页面都是 440。 + public double Width + { + get; + set => SetField(ref field, Math.Max(MinWidth, value)); + } = 440; + + /// 最大化:占满整个页签。 + public bool Maximized + { + get; + set + { + if (SetField(ref field, value)) + { + OnPropertiesChanged(nameof(CanResize), nameof(ListVisible)); + } + } + } + + /// 抽屉开着没有。由页面在选中项变化时写。 + public bool IsOpen + { + get; + set + { + if (SetField(ref field, value)) + { + // 抽屉没了,最大化也就该没了 —— 不然下一次开抽屉会莫名其妙地直接铺满整页。 + if (!value) + { + Maximized = false; + } + OnPropertyChanged(nameof(CanResize)); + } + } + } + + /// 分割条要不要露出来。 + public bool CanResize => IsOpen && !Maximized; + + /// 列表那一块要不要露出来(抽屉铺满时盖住它)。 + public bool ListVisible => !Maximized; + + /// + /// 把抽屉至少撑到这么宽。 + /// + /// 容器的文件页那种三栏在 440px 里根本摆不开;但"撑满整个页签"又太狠 —— + /// 撑到够用就停,列表还在旁边,分割条也还在,用户随时能往回拖。 + /// + /// + public void EnsureAtLeast(double width) => Width = Math.Max(Width, width); + + /// 切换最大化。抽屉头上那颗键绑它。 + public RelayCommand ToggleMaximizeCommand => field ??= new(_ => + { + Maximized = !Maximized; + return Task.CompletedTask; + }); +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Feedback.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Feedback.cs new file mode 100644 index 0000000..26df2b8 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Feedback.cs @@ -0,0 +1,220 @@ +using Avalonia.Threading; +using System.Collections.ObjectModel; +using System.Net; +using VelaShell.Plugin.DockerPanel.Docker; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// 反馈的语气。 +public enum FeedbackKind +{ + /// 中性。 + Info, + + /// 成功。 + Success, + + /// 部分成功 / 需要注意。 + Warning, + + /// 失败。 + Error +} + +/// 面板右下角的一条 toast。 +/// 建一条 toast。 +public sealed class Toast(FeedbackKind kind, string title, string detail) : ObservableObject +{ + + /// 语气。 + public FeedbackKind Kind { get; } = kind; + + /// 标题。 + public string Title { get; } = title; + + /// 正文。 + public string Detail { get; } = detail; + + /// 可点的动作(文字 → 回调)。 + public ObservableCollection Actions { get; } = []; + + /// 图标资源键。 + public string Icon => Kind switch + { + FeedbackKind.Success => "Docker.circle-check-big", + FeedbackKind.Warning => "Icon.triangle-alert", + FeedbackKind.Error => "Docker.circle-x", + _ => "Icon.info" + }; + + /// 是否自动消失。成功的 4 秒后自己走,失败的**不自动消失**。 + public bool AutoDismiss => Kind is FeedbackKind.Success or FeedbackKind.Info; +} + +/// toast 上的一个动作。 +/// 文字。 +/// 点击回调。 +public sealed record ToastAction(string Label, Action Invoke); + +/// +/// 结果反馈。 +/// +/// 规则只有一条:动作发起点还在视野里,就只更新状态栏和那一行;用户已经切走了 +/// (换了页签、关了对话框),才弹 toast。 在用户正盯着的列表上方再弹一个 +/// "已停止 nginx-proxy",只是把他刚看到的事情又说了一遍。 +/// +/// +public sealed class Feedback : ObservableObject +{ + private const int MaxToasts = 3; + + /// 当前的 toast(最多三条,新的在下面)。 + public ObservableCollection Toasts { get; } = []; + + /// 状态栏左侧那句话 —— 永远说最后一件事的结果。 + public string StatusText + { + get; + private set => SetField(ref field, value); + } = ""; + + /// 状态栏那句话的语气。 + public FeedbackKind StatusKind + { + get; + private set + { + if (SetField(ref field, value)) + { + OnPropertyChanged(nameof(StatusIcon)); + } + } + } = FeedbackKind.Info; + + /// 状态栏图标。 + public string StatusIcon => StatusKind switch + { + FeedbackKind.Success => "Docker.circle-check-big", + FeedbackKind.Warning => "Icon.triangle-alert", + FeedbackKind.Error => "Docker.circle-x", + _ => "Icon.info" + }; + + /// 只更新状态栏。 + public void Status(FeedbackKind kind, string text) => Ui.Post(() => + { + StatusKind = kind; + StatusText = text; + }); + + /// 弹一条 toast,同时更新状态栏。 + public Toast Notify(FeedbackKind kind, string title, string detail, params ToastAction[] actions) + { + var toast = new Toast(kind, title, detail); + foreach (var action in actions) + { + toast.Actions.Add(action); + } + Ui.Post(() => + { + StatusKind = kind; + StatusText = detail.Length > 0 ? $"{title} · {detail.Split('\n')[0]}" : title; + Toasts.Add(toast); + while (Toasts.Count > MaxToasts) + { + Toasts.RemoveAt(0); + } + if (toast.AutoDismiss) + { + DispatcherTimer.RunOnce(() => Toasts.Remove(toast), TimeSpan.FromSeconds(4)); + } + }); + return toast; + } + + /// 关掉一条 toast。 + public void Dismiss(Toast toast) => Ui.Post(() => Toasts.Remove(toast)); + + /// + /// 把一次批量操作的结果如实报出来。成功全过就一句话;有失败的话**逐个目标**列原因, + /// 而不是把整批说成"操作失败"。 + /// + /// 动作名,如“停止”。 + /// 批量结果。 + /// 发起点是否还在用户视野里(决定要不要弹 toast)。 + /// 用户点"查看详情"时的回调。 + public void ReportBatch(string verb, BatchResult result, bool inView, Action? onShowDetail = null) + { + if (result.AllSucceeded) + { + var text = $"已{verb} {result.SucceededCount} 个"; + if (inView) + { + Status(FeedbackKind.Success, text); + } + else + { + Notify(FeedbackKind.Success, text, ""); + } + return; + } + var summary = $"已{verb} {result.SucceededCount} 个,{result.FailedCount} 个失败"; + var detail = string.Join('\n', result.Failures.Take(3).Select(f => $"{f.Target}:{f.Failure}")); + if (result.FailedCount > 3) + { + detail += $"\n…还有 {result.FailedCount - 3} 个"; + } + Status(FeedbackKind.Warning, $"{summary} · {result.Failures.First().Target}:{result.Failures.First().Failure}"); + List actions = []; + if (onShowDetail is not null) + { + actions.Add(new("查看详情", onShowDetail)); + } + Notify(FeedbackKind.Warning, summary, detail, [.. actions]); + } + + /// + /// 把一个异常报出来。连不上与操作失败的语气不一样。 + /// + /// daemon 的原文一律留着 —— 它是唯一的事实。但只给原文不够: + /// 409 volume is in use 这种话,用户读完还是不知道下一步该做什么。 + /// 所以在原文**前面**加一句人话,并且允许调用方挂一颗补救按钮 + /// (「看看是谁在占」这类);两者都没有的时候,行为与从前一致。 + /// + /// + /// 在做什么(“删除卷”)。 + /// 异常。 + /// 补救动作,调用方按场景给。 + public void ReportError(string what, Exception ex, params ToastAction[] actions) + { + if (ex is OperationCanceledException) + { + Status(FeedbackKind.Info, $"{what} 已取消"); + return; + } + var detail = ex switch + { + DockerApiException api => Explain(api) is { Length: > 0 } why ? $"{why}\n{api.Message}" : api.Message, + DockerUnreachableException unreachable => unreachable.Message, + _ => ex.Message + }; + Notify(FeedbackKind.Error, $"{what} 失败", detail, actions); + } + + /// + /// 给 daemon 的状态码配一句人话。 + /// + /// 只翻译**状态码本身**的含义,不猜具体是哪个资源出的事 —— + /// 后者要看上下文,由调用方用补救动作去说。 + /// + /// + private static string Explain(DockerApiException api) => api.StatusCode switch + { + HttpStatusCode.Conflict => "冲突:目标正被别的东西占着,或者当前状态不允许这个动作。", + HttpStatusCode.NotFound => "目标已经不在了 —— 多半是别处(或另一个面板)已经删掉了。", + HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden => "认证不通过:凭据过期、没登录,或者这个仓库不让你做这件事。", + HttpStatusCode.BadRequest => "daemon 认为这个请求本身有问题 —— 多半是参数或名字不合法。", + HttpStatusCode.InternalServerError => "daemon 内部出错。它的日志(journalctl -u docker)里会有更完整的一段。", + _ => "" + }; +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/FilePicker.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/FilePicker.cs new file mode 100644 index 0000000..e61bc5c --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/FilePicker.cs @@ -0,0 +1,59 @@ +using Avalonia.Controls; +using Avalonia.Platform.Storage; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// +/// 本地文件选择。 +/// +/// SDK 没有这个能力,因为它对隔离进程的插件没法给 —— 但 Docker 面板是 +/// hostMode: inProcess(隧道要一条活的字节流,跨不了进程),所以它就在宿主的 +/// Avalonia 里,能直接问 。 +/// +/// +/// 挂在这里而不是让视图模型自己去爬可视树:视图模型不该知道自己被画在哪儿, +/// 而"当前顶层窗口"是只有视图答得上来的问题。 +/// +/// +public static class FilePicker +{ + private static Func? _accessor; + + /// 视图挂上来的时候把自己的顶层窗口交出来。 + public static void Attach(Func accessor) => _accessor = accessor; + + /// 能不能弹选择器(视图还没挂上、或者宿主没给顶层窗口时不能)。 + public static bool IsAvailable => _accessor?.Invoke()?.StorageProvider is { CanOpen: true }; + + /// 选一个本地文件读。取消返回 + public static async Task PickOpenAsync(string title) + { + if (_accessor?.Invoke()?.StorageProvider is not { } storage) + { + return null; + } + var picked = await storage.OpenFilePickerAsync(new() + { + Title = title, + AllowMultiple = false + }).ConfigureAwait(true); + return picked.Count > 0 ? picked[0] : null; + } + + /// 选一个本地路径写。取消返回 + /// 对话框标题。 + /// 建议文件名。 + /// 默认扩展名,不带点。 + public static async Task PickSaveAsync(string title, string suggestedName, string? extension = null) + { + return _accessor?.Invoke()?.StorageProvider is not { } storage + ? null + : await storage.SaveFilePickerAsync(new() + { + Title = title, + SuggestedFileName = suggestedName, + DefaultExtension = extension, + ShowOverwritePrompt = true + }).ConfigureAwait(true); + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Forms.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Forms.cs new file mode 100644 index 0000000..bfd790e --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Forms.cs @@ -0,0 +1,687 @@ +using System.Text; +using System.Text.RegularExpressions; +using VelaShell.Plugin.DockerPanel.Docker; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// 重命名容器。 +public sealed partial class RenameContainerForm : PanelForm +{ + private readonly TextField _current; + private readonly TextField _next; + + /// 建表单。 + public RenameContainerForm(string currentName, string composeProject) + { + ComposeWarning = composeProject; + _current = new("当前名称") { Value = currentName, ReadOnly = true }; + _next = new("新名称") { Value = currentName, Placeholder = "只能是字母、数字与 _ . -" }; + Fields.Add(_current); + Watch(_next); + UpdatePreview(); + } + + /// + public override string Title => "重命名容器"; + + /// + public override string Icon => "Icon.pencil"; + + /// + public override string ConfirmLabel => "重命名"; + + /// + public override string FooterHint => "改名不重启容器"; + + /// 新名字。 + public string NewName => _next.Value; + + /// + /// compose 管着的容器改名后,compose 会认为那个服务不存在 —— 这句话必须说在前面。 + /// + public string ComposeWarning => field.Length > 0 + ? $"这个容器由 compose 项目 {field} 管理。改名后 compose 会认为服务不存在,下次 up -d 会再建一个。要长期生效,请改 compose.yaml 里的 container_name。" + : ""; + + /// + // 这句话原来只算不显示:表单壳上压根没有放它的地方, + // 于是给一个 compose 管着的容器改名,界面上一句提醒都没有。 + public override string Notice => ComposeWarning; + + /// + public override bool Validate() + { + var value = _next.Value.Trim(); + if (value.Length == 0) + { + _next.Error = "名字不能为空。"; + return false; + } + if (!NamePattern().IsMatch(value)) + { + _next.Error = "只能包含字母、数字与 _ . -,且必须以字母或数字开头。"; + return false; + } + if (value == _current.Value) + { + _next.Error = "和当前名字一样。"; + return false; + } + return true; + } + + /// + protected override void UpdatePreview() + { + CommandPreview = $"POST /containers/{_current.Value}/rename?name={_next.Value.Trim()}"; + CommandNote = $"等价于 docker rename {_current.Value} {_next.Value.Trim()}"; + } + + [GeneratedRegex("^[a-zA-Z0-9][a-zA-Z0-9_.-]*$")] + private static partial Regex NamePattern(); +} + +/// 修改重启策略。 +public sealed class RestartPolicyForm : PanelForm +{ + private readonly RadioListField _policy; + private readonly TextField _retries; + + /// 建表单。 + public RestartPolicyForm(string current, int maxRetries) + { + _policy = new("策略") { Value = current }; + _policy.Options.Add(new("no", "no", "容器退出后就停在那里。")); + _policy.Options.Add(new("on-failure", "on-failure", "退出码非 0 才重启,可限次数。")); + _policy.Options.Add(new("always", "always", "daemon 重启后也拉起 —— 手动停过的也会被拉起。")); + _policy.Options.Add(new("unless-stopped", "unless-stopped", "同 always,但你手动停过的不会被拉起。")); + _retries = new("最大重试次数") { Value = maxRetries.ToString(), Hint = "仅 on-failure 有意义" }; + Fields.Add(_policy); + Watch(_retries); + UpdatePreview(); + } + + /// + public override string Title => "修改重启策略"; + + /// + public override string Icon => "Icon.refresh-cw"; + + /// + public override string ConfirmLabel => "保存"; + + /// + public override string FooterHint => "立即生效,不重启容器"; + + /// 选中的策略。 + public string Policy => _policy.Value; + + /// 最大重试次数。 + public int MaxRetries => int.TryParse(_retries.Value, out var n) ? Math.Max(0, n) : 0; + + /// + public override bool Validate() + { + if (_policy.Value == "on-failure" && !int.TryParse(_retries.Value.Trim(), out _)) + { + _retries.Error = "要一个整数。"; + return false; + } + return true; + } + + /// + protected override void UpdatePreview() + { + CommandPreview = $"POST /containers/{{id}}/update RestartPolicy.Name = {_policy.Value}"; + CommandNote = $"等价于 docker update --restart {_policy.Value}{(_policy.Value == "on-failure" ? $":{MaxRetries}" : "")} <容器>"; + } +} + +/// 给镜像打标签。 +public sealed class TagImageForm : PanelForm +{ + private readonly string _sourceId; + private readonly TextField _repository; + private readonly TextField _tag; + + /// 建表单。 + public TagImageForm(string sourceId, string sourceReference) + { + _sourceId = sourceId; + Fields.Add(new TextField("源镜像") { Value = sourceReference, ReadOnly = true }); + _repository = new("新仓库") { Value = "", Placeholder = "registry.internal/acme/api" }; + _tag = new("新标签") { Value = "latest" }; + Watch(_repository); + Watch(_tag); + UpdatePreview(); + } + + /// + public override string Title => "打标签"; + + /// + public override string Icon => "Docker.tag"; + + /// + public override string ConfirmLabel => "打标签"; + + /// + public override string ConfirmIcon => "Docker.tag"; + + /// + public override string FooterHint => "标签只是指针 —— 磁盘不会多占一份"; + + /// 目标仓库。 + public string Repository => _repository.Value.Trim(); + + /// 目标标签。 + public string Tag => _tag.Value.Trim(); + + /// + public override bool Validate() + { + if (Repository.Length == 0) + { + _repository.Error = "仓库名不能为空。"; + return false; + } + if (Repository.Contains(' ', StringComparison.Ordinal)) + { + _repository.Error = "仓库名里不能有空格。"; + return false; + } + return true; + } + + /// + protected override void UpdatePreview() + { + CommandPreview = $"POST /images/{Humanize.ShortId(_sourceId)}/tag?repo={Repository}&tag={Tag}"; + CommandNote = $"等价于 docker tag {Humanize.ShortId(_sourceId)} {Repository}:{Tag}"; + } +} + +/// 新建卷。 +public sealed class CreateVolumeForm : PanelForm +{ + private readonly TextField _name; + private readonly ChoiceField _driver; + + /// 建表单。 + public CreateVolumeForm() + { + _name = new("名称") { Placeholder = "pg-backup", Hint = "创建后不可改" }; + _driver = new("驱动") { Value = "local", AsSegments = true }; + _driver.Options.Add(new("local", "local")); + _driver.Options.Add(new("nfs", "nfs(经 local 驱动选项)")); + Watch(_name); + Watch(_driver); + Options = new("驱动选项") + { + Hint = "留空即用默认本地目录 /var/lib/docker/volumes", + KeyPlaceholder = "type", + ValuePlaceholder = "nfs", + AddLabel = "+ 添加选项" + }; + Labels = new("标签") { KeyPlaceholder = "backup.retention", ValuePlaceholder = "30d" }; + Watch(Options); + Watch(Labels); + UpdatePreview(); + } + + /// + public override string Title => "新建卷"; + + /// + public override string Icon => "Docker.database"; + + /// + public override string ConfirmLabel => "新建卷"; + + /// + public override string ConfirmIcon => "Icon.plus"; + + /// 驱动选项。 + public PairListField Options { get; } + + /// 标签。 + public PairListField Labels { get; } + + /// 卷名。 + public string Name => _name.Value.Trim(); + + /// 驱动(nfs 也是 local 驱动 + 选项)。 + public string Driver => "local"; + + /// 驱动选项字典。 + public Dictionary? DriverOptions => + Options.Filled.ToDictionary(r => r.Key.Trim(), r => r.Value.Trim()) is { Count: > 0 } map ? map : null; + + /// 标签字典。 + public Dictionary? LabelMap => + Labels.Filled.ToDictionary(r => r.Key.Trim(), r => r.Value.Trim()) is { Count: > 0 } map ? map : null; + + /// + public override bool Validate() + { + if (Name.Length == 0) + { + _name.Error = "卷名不能为空。"; + return false; + } + return true; + } + + /// + protected override void UpdatePreview() + { + CommandPreview = "POST /volumes/create"; + CommandNote = $"等价于 docker volume create {Name}"; + } +} + +/// 新建网络。 +public sealed class CreateNetworkForm : PanelForm +{ + private readonly TextField _name; + private readonly ChoiceField _driver; + private readonly TextField _subnet; + private readonly TextField _gateway; + private readonly ToggleField _internal; + private readonly ToggleField _attachable; + private readonly ToggleField _ipv6; + + /// 建表单。 + /// 远端 swarm 是否 active —— 决定 overlay 能不能选。 + public CreateNetworkForm(bool swarmActive) + { + _name = new("名称") { Placeholder = "edge-dmz" }; + _driver = new("驱动") { Value = "bridge", AsSegments = true }; + _driver.Options.Add(new("bridge", "bridge")); + _driver.Options.Add(new("macvlan", "macvlan")); + _driver.Options.Add(new("ipvlan", "ipvlan")); + // overlay 直接置灰而不是让用户去撞一条 daemon 的错误。 + _driver.Options.Add(new("overlay", "overlay", "", swarmActive, + swarmActive ? "" : "需要 swarm 模式,当前主机 Swarm: inactive")); + _subnet = new("子网") { Placeholder = "172.28.0.0/16", Hint = "留空由 Docker 自动分配" }; + _gateway = new("网关") { Placeholder = "172.28.0.1" }; + _internal = new("internal") { Description = "不给这个网络出外网的能力。" }; + _attachable = new("attachable") { Value = true, Description = "允许 docker run --network 事后接入。" }; + _ipv6 = new("启用 IPv6") { Description = "需要 daemon 开了 ipv6。" }; + Watch(_name); + Watch(_driver); + Watch(_subnet); + Watch(_gateway); + Watch(_internal); + Watch(_attachable); + Watch(_ipv6); + UpdatePreview(); + } + + /// + public override string Title => "新建网络"; + + /// + public override string Icon => "Icon.network"; + + /// + public override string ConfirmLabel => "新建网络"; + + /// + public override string ConfirmIcon => "Icon.plus"; + + /// 网络名。 + public string Name => _name.Value.Trim(); + + /// 驱动。 + public string Driver => _driver.Value; + + /// 子网。 + public string Subnet => _subnet.Value.Trim(); + + /// 网关。 + public string Gateway => _gateway.Value.Trim(); + + /// 是不是内部网络。 + public bool Internal => _internal.Value; + + /// 能不能事后接入。 + public bool Attachable => _attachable.Value; + + /// 启不启用 IPv6。 + public bool EnableIPv6 => _ipv6.Value; + + /// + public override bool Validate() + { + if (Name.Length == 0) + { + _name.Error = "网络名不能为空。"; + return false; + } + if (Subnet.Length > 0 && !Subnet.Contains('/', StringComparison.Ordinal)) + { + _subnet.Error = "子网要带掩码位数,如 172.28.0.0/16。"; + return false; + } + if (Gateway.Length > 0 && Subnet.Length == 0) + { + _gateway.Error = "给了网关就要一并给子网,否则 Docker 不知道它属于哪一段。"; + return false; + } + return true; + } + + /// + protected override void UpdatePreview() + { + var sb = new StringBuilder("docker network create -d ").Append(Driver); + if (Subnet.Length > 0) + { + sb.Append(" --subnet ").Append(Subnet); + } + if (Gateway.Length > 0) + { + sb.Append(" --gateway ").Append(Gateway); + } + if (Internal) + { + sb.Append(" --internal"); + } + if (Attachable) + { + sb.Append(" --attachable"); + } + if (EnableIPv6) + { + sb.Append(" --ipv6"); + } + sb.Append(' ').Append(Name.Length > 0 ? Name : "<名称>"); + CommandPreview = "POST /networks/create"; + CommandNote = $"等价于 {sb}"; + } +} + +/// 把容器接入网络。 +public sealed class ConnectNetworkForm : PanelForm +{ + private readonly TextField _aliases; + + /// 建表单。 + /// 目标网络的描述。 + /// 候选容器(id, 名字, 状态, 能不能选, 不能选的原因)。 + public ConnectNetworkForm(string network, + IEnumerable<(string Id, string Name, string Meta, bool Enabled, string Reason)> candidates) + { + Fields.Add(new TextField("目标网络") { Value = network, ReadOnly = true }); + Containers = new("选择容器") { Placeholder = "过滤未接入的容器…" }; + foreach ((var id, var name, var meta, var enabled, var reason) in candidates) + { + Containers.Items.Add(new(id, name, meta, enabled, reason)); + } + Containers.ApplyFilter(); + Fields.Add(Containers); + _aliases = new("网络别名") { Hint = "逗号分隔", Placeholder = "db, primary" }; + Watch(_aliases); + UpdatePreview(); + } + + /// + public override string Title => "接入容器到网络"; + + /// + public override string Icon => "Icon.plug"; + + /// + public override string ConfirmLabel => "接入"; + + /// + public override string ConfirmIcon => "Icon.plug"; + + /// + public override string FooterHint => "接入后立即生效,不重启容器"; + + /// 候选容器。 + public SelectListField Containers { get; } + + /// 选中的容器 id。 + public string[] SelectedIds => [.. Containers.SelectedItems.Select(i => i.Id)]; + + /// 别名。 + public string[] Aliases => + [.. _aliases.Value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)]; + + /// + public override bool Validate() + { + if (SelectedIds.Length == 0) + { + Containers.Error = "至少选一个容器。"; + return false; + } + if (SelectedIds.Length > 1 && Aliases.Length > 0) + { + // 同一个别名指向多个容器,DNS 解析结果就成了随机的 —— 那不是用户想要的。 + _aliases.Error = "别名只能给单个容器 —— 多个容器共用一个别名会让 DNS 解析变成随机的。"; + return false; + } + return true; + } + + /// + protected override void UpdatePreview() + { + CommandPreview = "POST /networks/{id}/connect"; + CommandNote = "等价于 docker network connect [--alias …] <网络> <容器>"; + } +} + +/// +/// 把容器提交成镜像。 +/// +/// 这条路专治"手改到能跑了,别再让我复现一遍" —— 但它做出来的镜像没有 Dockerfile, +/// 半年后没人说得清里面有什么。所以表单要把这句话说在脸上,而不是藏在文档里。 +/// +/// +public sealed class CommitContainerForm : PanelForm +{ + private readonly string _containerName; + private readonly TextField _repository; + private readonly TextField _tag; + private readonly TextField _comment; + private readonly ToggleField _pause; + + /// 建表单。 + public CommitContainerForm(string containerName, string sourceImage) + { + _containerName = containerName; + Fields.Add(new TextField("源容器") { Value = containerName, ReadOnly = true }); + Fields.Add(new TextField("当前镜像") { Value = sourceImage, ReadOnly = true }); + _repository = new("新仓库") { Value = "", Placeholder = "registry.internal/acme/api" }; + _tag = new("新标签") { Value = "latest" }; + _comment = new("提交说明") { Value = "", Placeholder = "改了什么 —— 这是以后唯一的线索" }; + _pause = new("提交期间暂停容器") + { + Value = true, + Description = "默认开着,与 docker commit 一致。关掉的话,一个正在写文件的进程会被拍成写到一半的样子。" + }; + Fields.Add(_repository); + Fields.Add(_tag); + Fields.Add(_comment); + Fields.Add(_pause); + Watch(_repository); + Watch(_tag); + Watch(_comment); + Watch(_pause); + UpdatePreview(); + } + + /// + public override string Title => "提交为镜像"; + + /// + public override string Icon => "Docker.box"; + + /// + public override string ConfirmLabel => "提交"; + + /// + public override string ConfirmIcon => "Docker.box"; + + /// + public override string FooterHint => "提交出来的镜像没有 Dockerfile —— 一次性救急可以,别当成构建流程"; + + /// 目标仓库。 + public string Repository => _repository.Value.Trim(); + + /// 目标标签。 + public string Tag => _tag.Value.Trim(); + + /// 提交说明。 + public string Comment => _comment.Value.Trim(); + + /// 提交期间是否暂停。 + public bool Pause => _pause.Value; + + /// + public override bool Validate() + { + if (Repository.Length == 0) + { + _repository.Error = "仓库名不能为空 —— 留空会得到一个没有标签的悬空镜像。"; + return false; + } + if (Repository.Contains(' ', StringComparison.Ordinal)) + { + _repository.Error = "仓库名里不能有空格。"; + return false; + } + return true; + } + + /// + protected override void UpdatePreview() + { + CommandPreview = $"POST /commit?container={_containerName}&repo={Repository}&tag={Tag}&pause={(Pause ? 1 : 0)}"; + CommandNote = "等价于 docker commit " + (Pause ? "" : "-p=false ") + + (Comment.Length > 0 ? $"-m {Sh.Quote(Comment)} " : "") + + $"{_containerName} {Repository}:{Tag}"; + } +} + +/// +/// exec 会话的用户与 shell。 +/// +/// 这两样在 exec 建立时就定死了,改不了活着的那一个会话 —— +/// 所以这张表单确认之后必然伴随一次重连,而不是"下次生效"。 +/// +/// +public sealed class ExecUserForm : PanelForm +{ + private readonly TextField _user; + private readonly ChoiceField _shell; + + /// 建表单。 + public ExecUserForm(string currentUser, string currentShell) + { + _user = new("用户") + { + Value = currentUser, + Placeholder = "留空 = 镜像里 USER 指定的那个", + Hint = "可以是名字或 uid,也可以写 uid:gid" + }; + _shell = new("Shell") { Value = currentShell, AsSegments = true }; + _shell.Options.Add(new("/bin/bash", "bash", "多数发行版镜像都有。")); + _shell.Options.Add(new("/bin/sh", "sh", "精简镜像(alpine / distroless)只有它。")); + _shell.Options.Add(new("/bin/ash", "ash", "busybox 系。")); + Watch(_user); + Watch(_shell); + UpdatePreview(); + } + + /// + public override string Title => "切换用户"; + + /// + public override string Icon => "Docker.users"; + + /// + public override string ConfirmLabel => "重新连接"; + + /// + public override string ConfirmIcon => "Icon.refresh-cw"; + + /// + public override string FooterHint => "会结束当前 exec 会话并重开一个"; + + /// 用户。 + public string User => _user.Value.Trim(); + + /// shell 的绝对路径。 + public string Shell => _shell.Value; + + /// + protected override void UpdatePreview() + { + var user = User.Length > 0 ? $"&User={User}" : ""; + CommandPreview = $"POST /containers/{{id}}/exec Cmd=[{Shell}]{user}"; + CommandNote = "等价于 docker exec -it " + (User.Length > 0 ? $"-u {User} " : "") + $"<容器> {Shell}"; + } +} + +/// exec 会话的工作目录。 +public sealed class ExecWorkingDirForm : PanelForm +{ + private readonly TextField _dir; + + /// 建表单。 + public ExecWorkingDirForm(string current) + { + _dir = new("工作目录") + { + Value = current, + Placeholder = "留空 = 镜像里 WORKDIR 指定的那个", + Hint = "绝对路径;目录不存在时 exec 会直接失败" + }; + Watch(_dir); + UpdatePreview(); + } + + /// + public override string Title => "工作目录"; + + /// + public override string Icon => "Icon.folder"; + + /// + public override string ConfirmLabel => "重新连接"; + + /// + public override string ConfirmIcon => "Icon.refresh-cw"; + + /// + public override string FooterHint => "会结束当前 exec 会话并重开一个"; + + /// 工作目录。 + public string WorkingDir => _dir.Value.Trim(); + + /// + public override bool Validate() + { + if (WorkingDir.Length > 0 && !WorkingDir.StartsWith('/')) + { + _dir.Error = "要绝对路径 —— exec 没有「当前目录」可以相对。"; + return false; + } + return true; + } + + /// + protected override void UpdatePreview() + { + var dir = WorkingDir.Length > 0 ? $"&WorkingDir={WorkingDir}" : ""; + CommandPreview = $"POST /containers/{{id}}/exec Cmd=[shell]{dir}"; + CommandNote = "等价于 docker exec -it " + (WorkingDir.Length > 0 ? $"-w {WorkingDir} " : "") + "<容器> "; + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Glyph.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Glyph.cs new file mode 100644 index 0000000..142e13e --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Glyph.cs @@ -0,0 +1,122 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// +/// 一枚描边图标。 +/// +/// lucide 的路径是**描边**的(没有填充),用 PathIcon 那种填充渲染会糊成一团黑。 +/// 这个控件按 24×24 的视框等比缩放,描边宽度固定在 2(与 lucide 原始设计一致), +/// 于是 13px 与 20px 的图标看起来是同一套线宽。 +/// +/// +/// 图标既可以直接给几何(),也可以给资源键()—— +/// 后者让视图模型能用字符串决定画哪一个,而不必在视图里堆一张 DataTrigger 大表。 +/// +/// +public sealed class Glyph : Control +{ + /// lucide 的设计视框边长。 + private const double ViewBox = 24; + + /// 图标几何。 + public static readonly StyledProperty DataProperty = + AvaloniaProperty.Register(nameof(Data)); + + /// 图标的资源键(如 Icon.play / Docker.box)。 + public static readonly StyledProperty KeyProperty = + AvaloniaProperty.Register(nameof(Key)); + + /// 描边画刷。 + public static readonly StyledProperty BrushProperty = + AvaloniaProperty.Register(nameof(Brush)); + + /// 边长(逻辑像素),宽高相同。 + public static readonly StyledProperty SizeProperty = + AvaloniaProperty.Register(nameof(Size), 14d); + + static Glyph() + { + AffectsRender(DataProperty, BrushProperty, SizeProperty); + AffectsMeasure(SizeProperty); + KeyProperty.Changed.AddClassHandler((glyph, _) => glyph.ResolveKey()); + // 图标是定尺寸的,默认的 Stretch 对它没有意义:排版会把它拉成整行高, + // 而 Render 从原点开画 —— 于是图标贴在行顶,旁边居中的文字与它错开一截。 + // 这是"图标和文字不在一个水平中轴上"的根因,逐处补 VerticalAlignment 补不干净。 + // 枚举与 Layoutable.VerticalAlignment 属性同名,这里必须写全限定。 + VerticalAlignmentProperty.OverrideDefaultValue(Avalonia.Layout.VerticalAlignment.Center); + } + + /// 图标几何。 + public Geometry? Data + { + get => GetValue(DataProperty); + set => SetValue(DataProperty, value); + } + + /// 图标的资源键。 + public string? Key + { + get => GetValue(KeyProperty); + set => SetValue(KeyProperty, value); + } + + /// 描边画刷。 + public IBrush? Brush + { + get => GetValue(BrushProperty); + set => SetValue(BrushProperty, value); + } + + /// 边长。 + public double Size + { + get => GetValue(SizeProperty); + set => SetValue(SizeProperty, value); + } + + /// + protected override Size MeasureOverride(Size availableSize) => new(Size, Size); + + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + // 资源要等控件进了可视树才查得到 —— 在那之前 FindResource 一定落空。 + ResolveKey(); + } + + private void ResolveKey() + { + if (Key is not { Length: > 0 } key) + { + return; + } + if (this.TryFindResource(key, out var resource) && resource is Geometry geometry) + { + Data = geometry; + } + } + + /// + public override void Render(DrawingContext context) + { + if (Data is not { } geometry || Brush is not { } brush) + { + return; + } + var scale = Size / ViewBox; + // 在实际分到的框里居中再画。默认对齐下这一步是零位移(框正好是 Size×Size); + // 但调用方显式写了 Stretch、或父容器给了更大的框时,图标仍然待在正中, + // 而不是缩在左上角。 + var transform = Matrix.CreateScale(scale, scale) * + Matrix.CreateTranslation((Bounds.Width - Size) / 2, (Bounds.Height - Size) / 2); + using (context.PushTransform(transform)) + { + // 线宽写在 24 的坐标系里,缩放之后自然与 lucide 在各尺寸下的观感一致。 + context.DrawGeometry(null, new Pen(brush, 2, lineCap: PenLineCap.Round, lineJoin: PenLineJoin.Round), geometry); + } + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/LineBuffer.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/LineBuffer.cs new file mode 100644 index 0000000..83335fd --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/LineBuffer.cs @@ -0,0 +1,92 @@ +using Avalonia.Threading; +using System.Collections.ObjectModel; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// +/// 把一条一条涌进来的行,攒成一小批再交给界面。 +/// +/// 直接 Ui.Post 每一行有个不明显的代价:每一次投递都是一个独立的调度任务, +/// 每一次 Add 都是一次 CollectionChanged,于是一次布局。 +/// compose logs --tail 500 在七个服务的项目上开头就是三千多行 —— +/// 三千多次调度、三千多次排版,界面自然要僵一下,而这段时间里它什么也没多做。 +/// +/// +/// 攒 120ms 再一起交,人眼看不出差别(一帧 16ms,日志本来也不是逐行读的), +/// 调度与布局却从"每行一次"变成"每批一次"。容器日志那边早就是这么做的, +/// 这里把它挪成两处共用。 +/// +/// +/// 行的类型。 +/// 最终落到的集合(只在 UI 线程上动)。 +/// 保留多少行;超了从最旧的开始扔。 +public sealed class LineBuffer(ObservableCollection target, int max) +{ + private readonly Lock _gate = new(); + private readonly List _pending = []; + private DispatcherTimer? _timer; + + /// 加一行。可以从任何线程调用。 + public void Add(T item) + { + lock (_gate) + { + _pending.Add(item); + } + Ui.Post(Start); + } + + /// 清空(界面上的和还没交出去的一起)。 + public void Clear() + { + lock (_gate) + { + _pending.Clear(); + } + Ui.Post(target.Clear); + } + + /// 把还压着的那一批立刻交出去 —— 一段命令跑完时用,免得最后几行等满 120ms。 + public void Flush() => Ui.Post(() => OnTick(null, EventArgs.Empty)); + + private void Start() + { + _timer ??= new() { Interval = TimeSpan.FromMilliseconds(120) }; + if (_timer.IsEnabled) + { + return; + } + _timer.Tick += OnTick; + _timer.Start(); + } + + private void OnTick(object? sender, EventArgs e) + { + List batch; + lock (_gate) + { + if (_pending.Count == 0) + { + // 空转两轮就把定时器停了 —— 一个面板上同时开着好几处日志时, + // 常驻的定时器加起来也是一笔开销。 + if (_timer is { } idle) + { + idle.Tick -= OnTick; + idle.Stop(); + } + return; + } + batch = [.. _pending]; + _pending.Clear(); + } + foreach (var item in batch) + { + target.Add(item); + } + // 按行截断:从最旧的开始扔,直到回到上限之内。 + while (target.Count > max) + { + target.RemoveAt(0); + } + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/ListColumns.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/ListColumns.cs new file mode 100644 index 0000000..16f9792 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/ListColumns.cs @@ -0,0 +1,46 @@ +using Avalonia.Controls; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// +/// 一张列表的列宽。 +/// +/// 列宽是一组 ,**列头和每一行的 ColumnDefinitions 绑同一份实例** —— +/// Avalonia 没有 SharedSizeGroup,这是让列头与几百行单元格始终对齐的唯一办法; +/// 与宿主文件浏览器的列拖拽同一路数。 +/// +/// +/// 各页的列不一样,所以宽度写成具名属性(界面绑得到、编译期查得出); +/// 而拖拽代码只按 key 认列,这几个方法就是它与具体页面之间的全部接口。 +/// +/// +public abstract class ListColumns : ObservableObject +{ + /// 列与列之间那条拖拽轨道的宽度。与 XAML 里的 6 是同一个数。 + public const double TrackWidth = 6; + + /// 可拖的列,次序与界面一致。 + public abstract IReadOnlyList Keys { get; } + + /// 按名字读一列的宽度。 + public abstract double Get(string key); + + /// 按名字写一列的宽度。 + public abstract void Set(string key, double width); + + /// 某一列的下限。再窄就只剩省略号了。 + public abstract double Min(string key); + + /// 某一列双击自适应时的上限 —— 一个超长的镜像名不该把整张表挤没。 + public virtual double MaxAutoFit(string key) => 640; + + /// 单元格里除文字之外还占着的宽度:图标、徽标、sparkline、右侧留白。 + public virtual double Padding(string key) => 18; + + /// 用户拖出来的列宽只可能是像素值 —— 星形和 Auto 在这里没有意义。 + protected GridLength Clamp(GridLength value, string key) + { + var min = Min(key); + return new(Math.Max(min, value.IsAbsolute ? value.Value : min)); + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/LogLevel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/LogLevel.cs new file mode 100644 index 0000000..96ed4ca --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/LogLevel.cs @@ -0,0 +1,147 @@ +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// 一行日志的级别。 +public enum LogLevel +{ + /// 认不出来。 + None, + + /// 调试。 + Debug, + + /// 普通信息。 + Info, + + /// 警告。 + Warn, + + /// 错误。 + Error +} + +/// +/// 从一行日志里认出级别。 +/// +/// 日志没有统一格式:nginx 写 [warn],Go 服务写 "level":"error", +/// Java 写 ERROR 顶格,systemd 风格写 <3>。这里只认前三种最常见的形态, +/// 并且只在行首那一小段里认 —— 全行扫描会把正文里提到 "error" 的成功日志 +/// 也染成红色,那比不着色更糟。 +/// +/// +public static class LogLevels +{ + /// 只在这么多字符里找级别标记。 + private const int HeadWindow = 64; + + /// 认一行的级别。 + public static LogLevel Detect(string text) + { + if (text.Length == 0) + { + return LogLevel.None; + } + // 结构化日志(以 { 开头)**只**认显式的 level 字段。 + // 对它做裸词扫描会把 {"msg":"error budget remaining"} 染成红色 —— + // 那是一条报告余量的正常日志,而 JSON 日志把级别放在字段里本来就是约定。 + if (text.AsSpan().TrimStart().StartsWith("{", StringComparison.Ordinal)) + { + return TryJsonLevel(text) ?? LogLevel.None; + } + if (TryJsonLevel(text) is { } fromJson) + { + return fromJson; + } + var head = text.AsSpan(0, Math.Min(text.Length, HeadWindow)); + return HasWord(head, "ERROR") || HasWord(head, "FATAL") || HasWord(head, "PANIC") ? LogLevel.Error + : HasWord(head, "WARN") || HasWord(head, "WARNING") ? LogLevel.Warn + : HasWord(head, "DEBUG") || HasWord(head, "TRACE") ? LogLevel.Debug + : HasWord(head, "INFO") || HasWord(head, "NOTICE") ? LogLevel.Info + : LogLevel.None; + } + + /// + /// 整词匹配。 + /// + /// 不能用裸的 Contains:一条 200 的访问日志里带着 /api/v1/errors?page=2, + /// 子串匹配会把它染成红色,然后有人去查一个不存在的故障。要求前后都不是字母数字, + /// errors 就不再命中 ERROR。 + /// + /// + private static bool HasWord(ReadOnlySpan haystack, ReadOnlySpan word) + { + var from = 0; + while (from < haystack.Length) + { + var at = haystack[from..].IndexOf(word, StringComparison.OrdinalIgnoreCase); + if (at < 0) + { + return false; + } + at += from; + var after = at + word.Length; + var leftOk = at == 0 || !char.IsLetterOrDigit(haystack[at - 1]); + var rightOk = after >= haystack.Length || !char.IsLetterOrDigit(haystack[after]); + if (leftOk && rightOk) + { + return true; + } + from = at + 1; + } + return false; + } + + private static LogLevel? TryJsonLevel(string text) + { + foreach (var key in (string[])["\"level\"", "\"lvl\"", "\"severity\""]) + { + var at = text.IndexOf(key, StringComparison.OrdinalIgnoreCase); + if (at < 0) + { + continue; + } + // 取键之后一小段,足够覆盖 `: "error"` 这种带空格的写法。 + var from = at + key.Length; + var value = text.AsSpan(from, Math.Min(24, text.Length - from)); + if (Contains(value, "error") || Contains(value, "fatal") || Contains(value, "panic")) + { + return LogLevel.Error; + } + if (Contains(value, "warn")) + { + return LogLevel.Warn; + } + if (Contains(value, "debug") || Contains(value, "trace")) + { + return LogLevel.Debug; + } + if (Contains(value, "info")) + { + return LogLevel.Info; + } + } + return null; + } + + private static bool Contains(ReadOnlySpan haystack, ReadOnlySpan needle) => + haystack.Contains(needle, StringComparison.OrdinalIgnoreCase); + + /// 级别的显示文字;认不出来时为空。 + public static string Label(LogLevel level) => level switch + { + LogLevel.Debug => "DEBUG", + LogLevel.Info => "INFO", + LogLevel.Warn => "WARN", + LogLevel.Error => "ERROR", + _ => "" + }; + + /// 级别对应的语气(界面按它取颜色)。 + public static RowTone Tone(LogLevel level) => level switch + { + LogLevel.Error => RowTone.Danger, + LogLevel.Warn => RowTone.Warn, + LogLevel.Debug => RowTone.Idle, + LogLevel.Info => RowTone.Ok, + _ => RowTone.Idle + }; +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Mvvm.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Mvvm.cs new file mode 100644 index 0000000..3c1d7e1 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Mvvm.cs @@ -0,0 +1,232 @@ +using Avalonia.Threading; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Windows.Input; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// +/// 最小的可观察对象。 +/// +/// 插件刻意不引 ReactiveUI / CommunityToolkit:插件包会被原样塞进 .vpx, +/// 而一个 Docker 面板需要的全部"MVVM"就是这两个类。少一个依赖, +/// 就少一次"宿主与插件加载了同一个库的两个版本"的排查。 +/// +/// +public abstract class ObservableObject : INotifyPropertyChanged +{ + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// 赋值并在值真的变了时通知。 + protected bool SetField(ref T field, T value, [CallerMemberName] string? propertyName = null) + { + if (EqualityComparer.Default.Equals(field, value)) + { + return false; + } + field = value; + OnPropertyChanged(propertyName); + return true; + } + + /// + /// 手动触发一次通知(计算属性跟着源属性变时用)。 + /// + /// 通知一律在 UI 线程上发出去。订阅者不是普通的观察者:XAML 绑定与视图侧那几个 + /// 监听器(抽屉布局、列宽)收到通知之后会直接去改控件,而在别的线程上碰控件是一个硬性异常 + /// (The calling thread cannot access this object)。 + /// + /// + /// 为什么放在这里而不是让每个调用方自己 :后台线程改属性的地方太多了 + /// —— 事件流、统计采样、日志、以及**释放路径**(面板关闭时那串 + /// ConfigureAwait(false) 之后就已经不在 UI 线程上了)。漏掉任何一处, + /// 用户看到的都是一个崩溃,而不是一次退化。这一处兜住,就没有"哪一处漏了"这回事。 + /// + /// + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + // 先取出来:发出去之前最后一个订阅者退订了的话,字段就成 null 了。 + if (PropertyChanged is not { } handler) + { + return; + } + if (Dispatcher.UIThread.CheckAccess()) + { + handler(this, new(propertyName)); + return; + } + try + { + Dispatcher.UIThread.Post(() => handler(this, new(propertyName))); + } + catch (Exception) + { + // 应用正在退出时 dispatcher 可能已经关了。那时没人再看这条通知了, + // 而为一条通知把关闭流程炸掉是更糟的结果。 + } + } + + /// 触发一组通知。 + protected void OnPropertiesChanged(params string[] propertyNames) + { + foreach (var name in propertyNames) + { + OnPropertyChanged(name); + } + } +} + +/// +/// 一个命令。异步版本自带"跑着的时候不能再点"—— 双击一个"删除"按钮 +/// 不该发出两条删除请求。 +/// +public sealed class RelayCommand : ICommand +{ + private readonly Func _execute; + private readonly Func? _canExecute; + private bool _running; + + /// 同步命令。 + public RelayCommand(Action execute, Func? canExecute = null) + { + _execute = p => + { + execute(p); + return Task.CompletedTask; + }; + _canExecute = canExecute; + } + + /// 异步命令。 + public RelayCommand(Func execute, Func? canExecute = null) + { + _execute = execute; + _canExecute = canExecute; + } + + /// + public event EventHandler? CanExecuteChanged; + + /// + public bool CanExecute(object? parameter) => !_running && (_canExecute?.Invoke(parameter) ?? true); + + /// + public async void Execute(object? parameter) + { + if (!CanExecute(parameter)) + { + return; + } + _running = true; + RaiseCanExecuteChanged(); + try + { + await _execute(parameter).ConfigureAwait(true); + } + catch (Exception ex) + { + // 命令的实现自己负责把失败呈现出来;真漏到这里的话, + // 至少不能让一个未观察的异常把宿主拖走。 + UnhandledCommandError?.Invoke(ex); + } + finally + { + _running = false; + RaiseCanExecuteChanged(); + } + } + + /// 命令实现漏出来的异常(面板挂一个全局处理,写进状态栏)。 + public static event Action? UnhandledCommandError; + + /// 重新求一次 + public void RaiseCanExecuteChanged() + { + if (Dispatcher.UIThread.CheckAccess()) + { + CanExecuteChanged?.Invoke(this, EventArgs.Empty); + } + else + { + Dispatcher.UIThread.Post(() => CanExecuteChanged?.Invoke(this, EventArgs.Empty)); + } + } +} + +/// UI 线程封送的小工具。 +public static class Ui +{ + /// 在 UI 线程上跑一段同步代码(已经在 UI 线程就直接跑)。 + public static void Post(Action action) + { + if (Dispatcher.UIThread.CheckAccess()) + { + action(); + } + else + { + Dispatcher.UIThread.Post(action); + } + } + + /// 在 UI 线程上跑一段代码并等它完成。 + public static Task InvokeAsync(Action action) => + Dispatcher.UIThread.CheckAccess() + ? RunInline(action) + : Dispatcher.UIThread.InvokeAsync(action).GetTask(); + + private static Task RunInline(Action action) + { + action(); + return Task.CompletedTask; + } +} + +/// 带"整体替换但保留身份"的可观察集合。 +public sealed class KeyedCollection(Func keySelector) : ObservableCollection +{ + /// + /// 用新快照就地合并:同 key 的项**保留原实例**(只更新内容),新增的插进去, + /// 消失的删掉。 + /// + /// 不能简单地 Clear + AddRange:那会把选中态、滚动位置与展开状态全部清掉 —— + /// 而这个面板每秒都可能因为一条事件而刷新。用户选中三行准备批量停止, + /// 刷新一次就全没了,是这个面板能犯的最烦人的错误。 + /// + /// + /// 新快照(顺序即目标顺序)。 + /// 把新数据合并进旧实例。 + public void Merge(IReadOnlyList snapshot, Action update) + { + Dictionary existing = []; + foreach (var item in this) + { + existing[keySelector(item)] = item; + } + for (var i = 0; i < snapshot.Count; i++) + { + var incoming = snapshot[i]; + var key = keySelector(incoming); + if (existing.TryGetValue(key, out var current)) + { + update(current, incoming); + var at = IndexOf(current); + if (at != i && at >= 0) + { + Move(at, i); + } + existing.Remove(key); + } + else + { + Insert(i, incoming); + } + } + foreach (var stale in existing.Values) + { + Remove(stale); + } + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/PageViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/PageViewModel.cs new file mode 100644 index 0000000..9880796 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/PageViewModel.cs @@ -0,0 +1,104 @@ +using VelaShell.Plugin.DockerPanel.Docker; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// 左导航栏的一个去处。 +public enum PanelPage +{ + /// 总览。 + Overview, + + /// 容器。 + Containers, + + /// 镜像。 + Images, + + /// 卷。 + Volumes, + + /// 网络。 + Networks, + + /// Compose。 + Compose, + + /// 系统。 + System +} + +/// +/// 一个页面的视图模型。 +/// +/// 页面**不自己拉数据**:什么时候刷新由外壳按事件流决定。这样一条 docker events +/// 就能同时喂饱所有页面,而不是每页各挂一个定时器把远端敲成筛子。 +/// +/// +public abstract class PageViewModel(DockerPanelViewModel shell) : ObservableObject +{ + + /// 外壳(拿客户端、闸门、任务中心、反馈)。 + protected DockerPanelViewModel Shell { get; } = shell; + + /// 当前端点的客户端;还没连上时为 + protected DockerClient? Client => Shell.Client; + + /// 页面标识。 + public abstract PanelPage Page { get; } + + /// 标题(工具条左上角)。 + public abstract string Title { get; } + + /// + /// 右侧详情抽屉的尺寸与形态。没有抽屉的页面(总览 / 系统 / Compose)不用它。 + /// + /// 放在基类上,是为了让视图那两个通用件(、 + /// )对四个页面一视同仁 —— 否则同一套拖拽逻辑要抄四遍。 + /// + /// + public DrawerState Drawer { get; } = new(); + + /// 这一页列表的列宽;没有可拖列表的页面为 + public virtual ListColumns? ColumnLayout => null; + + /// 某一列当前可见行里的文字(双击自适应时用来量宽度)。 + public virtual IEnumerable ColumnTexts(string key) => []; + + /// 正在加载。 + public bool Busy + { + get; + protected set => SetField(ref field, value); + } + + /// 至少成功加载过一次 —— 决定骨架屏什么时候让位给真数据。 + public bool LoadedOnce + { + get; + protected set => SetField(ref field, value); + } + + /// 页面被选中时调用。 + public virtual Task ActivateAsync(CancellationToken cancellationToken) => RefreshAsync(cancellationToken); + + /// 重新拉一遍这一页的数据。 + public abstract Task RefreshAsync(CancellationToken cancellationToken); + + /// + /// 换了端点:把这一页清空。 + /// + /// 不清的话,切到另一台机器的瞬间会先看到上一台的容器列表 —— + /// 一个足以让人对着错误的机器按下"删除"的过渡态。 + /// + /// + public abstract void Reset(); + + /// + /// 一条 daemon 事件到了。返回 表示这一页需要刷新。 + /// + /// 由页面自己判断,而不是一律刷新:compose up 一次能推来几十条事件, + /// 逐条刷新等于把远端敲成筛子。 + /// + /// + public abstract bool WantsRefresh(DockerEvent dockerEvent); +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ComposePageView.axaml b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ComposePageView.axaml new file mode 100644 index 0000000..7f5caac --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ComposePageView.axaml @@ -0,0 +1,576 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ComposePageView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ComposePageView.axaml.cs new file mode 100644 index 0000000..f0f663a --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ComposePageView.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace VelaShell.Plugin.DockerPanel.Ui.Pages; + +/// ComposePageView 的视图。 +public sealed partial class ComposePageView : UserControl +{ + // 服务表的列拖拽走的是与另外四张表同一份通用件,这里只交代这一页的一个数: + // 列宽之外那些不可拖的固定占位。左右各 14 的留白**不算** —— 那是列头 Border 的 + // Padding,量到的 Grid 宽度里本来就没有它;再减一次会让"还能拖多宽"凭空少 28。 + private readonly ColumnResizer _columns; + + /// 建视图。 + public ComposePageView() + { + AvaloniaXamlLoader.Load(this); + _columns = new(this, "ServicesHeader", chrome: 84); + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ComposePageViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ComposePageViewModel.cs new file mode 100644 index 0000000..3e5815b --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ComposePageViewModel.cs @@ -0,0 +1,1306 @@ +using Avalonia.Controls; +using System.Collections.ObjectModel; +using VelaShell.Plugin.DockerPanel.Docker; +using VelaShell.PluginSdk.RemoteExec; + +namespace VelaShell.Plugin.DockerPanel.Ui.Pages; + +/// +/// 执行记录 / 合并日志里的一行。 +/// +/// 合并日志多两样东西:这一行来自哪个服务(compose logs 把服务名写在 +/// 名字 | 正文 的前缀里),以及正文认出来的级别。两者都只是用来取颜色 —— +/// 七个服务的日志混在一起时,靠颜色分辨来源比逐行读名字快得多,而级别决定正文的语气。 +/// +/// +public sealed class OutputLine(string time, string text, bool isError, bool isCommand, + string source = "", int sourceIndex = 0) +{ + /// 时刻。 + public string Time { get; } = time; + + /// 来自哪个服务;执行记录里为空。 + public string Source { get; } = source; + + /// 服务在项目里的序号 —— 界面按它取一个稳定的颜色。 + public int SourceIndex { get; } = sourceIndex; + + /// 有没有服务名这一列。 + public bool HasSource => Source.Length > 0; + + /// 文本。 + public string Text { get; } = text; + + /// 来自标准错误。 + public bool IsError { get; } = isError; + + /// 是不是那条命令本身。 + public bool IsCommand { get; } = isCommand; + + /// 认出来的级别。 + public LogLevel Level { get; } = LogLevels.Detect(text); + + /// 级别文字;认不出来时为空。 + public string LevelLabel => LogLevels.Label(Level); + + /// 有没有级别标记。 + public bool HasLevel => Level != LogLevel.None; + + /// + /// 这一行的语气。标准错误一律按错误算 —— 哪怕正文里没有 ERROR 字样, + /// 它走的是 stderr 这件事本身就是信息。 + /// + public RowTone Tone => IsError ? RowTone.Danger : LogLevels.Tone(Level); +} + +/// +/// 拆 docker compose logs 的一行。 +/// +/// 它的格式是 服务名 | 正文(名字右侧补空格对齐)。看着好拆,但正文里完全可能 +/// 也有竖线 —— 2026-08-23 | INFO | ok 这种自带竖线的日志格式很常见, +/// 只按"第一根竖线"切会把时间戳当成服务名,日志本身就被改了样子。 +/// +/// +/// 所以前缀必须是一个**认得的**名字:调用方给一个判定,拿项目里真实的服务名 / 容器名去比。 +/// 认不出来就整行当正文 —— 最坏也只是少一列颜色。Attaching to … 这类 compose +/// 自己说的话本来就没有前缀,走的也是这一条。 +/// +/// +public static class MergedLog +{ + /// 服务名最长认到这里。再长就不必拿去比了。 + private const int MaxNameLength = 64; + + /// 拆出服务名与正文。 + /// compose 吐出来的一行。 + /// 这个名字是不是本项目的服务 / 容器。 + public static (string Source, string Body) Split(string line, Func known) + { + var bar = line.IndexOf('|'); + if (bar is <= 0 or > MaxNameLength) + { + return ("", line); + } + var name = line[..bar].TrimEnd(); + if (name.Length == 0 || name.AsSpan().ContainsAny(' ', '\t') || !known(name)) + { + return ("", line); + } + // 竖线后面 compose 固定跟一个空格;没有也无所谓,别把正文的第一个字符吃掉。 + var body = line[(bar + 1)..]; + return (name, body.StartsWith(' ') ? body[1..] : body); + } +} + +/// Compose 页的页签。 +public enum ComposeTab +{ + /// compose.yaml。 + Yaml, + + /// .env。 + Env, + + /// 服务列表。 + Services, + + /// 展开后的配置。 + Config, + + /// 项目的合并日志。 + Logs +} + +/// Compose 页。 +public sealed class ComposePageViewModel : PageViewModel +{ + private string _originalYaml = ""; + private string _originalEnv = ""; + private CancellationTokenSource? _logsCts; + // 日志是一行一行涌进来的,但界面不该一行一行地排版 —— 见 LineBuffer。 + private readonly LineBuffer _output; + private readonly LineBuffer _logs; + // 服务列表里没有的日志来源(已删掉的容器、一次性任务),各自也要一个稳定的颜色序号。 + private readonly Dictionary _extraSources = []; + private int _lastExitCode; + + /// 当前页签。 + public ComposeTab Tab + { + get; + private set + { + if (SetField(ref field, value)) + { + OnPropertiesChanged(nameof(IsYamlTab), nameof(IsEnvTab), nameof(IsServicesTab), + nameof(IsConfigTab), nameof(IsLogsTab)); + } + } + } = ComposeTab.Yaml; + + /// 在 compose.yaml 页签。 + public bool IsYamlTab => Tab == ComposeTab.Yaml; + + /// 在 .env 页签。 + public bool IsEnvTab => Tab == ComposeTab.Env; + + /// 在服务页签。 + public bool IsServicesTab => Tab == ComposeTab.Services; + + /// 在 config 展开页签。 + public bool IsConfigTab => Tab == ComposeTab.Config; + + /// 在合并日志页签。 + public bool IsLogsTab => Tab == ComposeTab.Logs; + + /// 项目的 .env 内容。 + public string Env + { + get; + set + { + if (SetField(ref field, value)) + { + OnPropertiesChanged(nameof(EnvModified), nameof(EnvModifiedText)); + } + } + } = ""; + + /// .env 改过了。 + public bool EnvModified => Env != _originalEnv; + + /// .env 的未保存提示。 + public string EnvModifiedText => EnvModified ? "未保存 · 保存需手打 save" : ""; + + /// 这个项目有没有 .env + public bool HasEnv + { + get; + private set => SetField(ref field, value); + } + + /// compose config 展开后的结果。 + public string Config + { + get; + private set => SetField(ref field, value); + } = ""; + + /// 合并日志的行。 + public ObservableCollection Logs { get; } = []; + + /// 日志正在跟随。 + public bool LogsFollowing + { + get; + private set => SetField(ref field, value); + } + + /// 最近一次子命令的退出码文本(执行记录头上那个 chip)。 + public string ExitCodeText => _lastExitCode == 0 ? "退出码 0" : $"退出码 {_lastExitCode}"; + + /// 最近一次子命令的语气。 + public RowTone ExitTone => _lastExitCode == 0 ? RowTone.Ok : RowTone.Danger; + + /// 建 Compose 页。 + public ComposePageViewModel(DockerPanelViewModel shell) : base(shell) + { + _output = new(Output, 2000); + _logs = new(Logs, 5000); + SelectCommand = new RelayCommand(p => p is ComposeProject project ? SelectAsync(project) : Task.CompletedTask); + UpCommand = new RelayCommand(_ => RunAsync("up -d", "启动项目")); + StopCommand = new RelayCommand(_ => RunAsync("stop", "停止项目")); + RestartCommand = new RelayCommand(_ => RunAsync("restart", "重启项目")); + PullCommand = new RelayCommand(_ => RunAsync("pull", "拉取镜像")); + BuildCommand = new RelayCommand(_ => RunAsync("build", "构建镜像")); + DownCommand = new RelayCommand(_ => DownAsync(withVolumes: false)); + DownVolumesCommand = new RelayCommand(_ => DownAsync(withVolumes: true)); + ConfigCommand = new RelayCommand(_ => ShowConfigAsync()); + SaveCommand = new RelayCommand(_ => SaveYamlAsync()); + RevertCommand = new RelayCommand(_ => Yaml = _originalYaml); + ClearOutputCommand = new RelayCommand(_ => _output.Clear()); + OpenByPathCommand = new RelayCommand(_ => OpenByPathAsync()); + RefreshCommand = new RelayCommand(_ => RefreshAsync(Shell.Lifetime)); + SetTabCommand = new RelayCommand(p => p is ComposeTab tab ? SetTabAsync(tab) : Task.CompletedTask); + SaveEnvCommand = new RelayCommand(_ => SaveEnvAsync()); + RevertEnvCommand = new RelayCommand(_ => + { + Env = _originalEnv; + return Task.CompletedTask; + }); + ServiceLogsCommand = new RelayCommand(p => p is ComposeService s + ? RunForServiceAsync("logs --tail 200 --no-color", s, $"{s.Service} 的日志") + : Task.CompletedTask); + ServiceRestartCommand = new RelayCommand(p => p is ComposeService s + ? RunForServiceAsync("restart", s, $"重启 {s.Service}") + : Task.CompletedTask); + ServiceTerminalCommand = new RelayCommand(p => p is ComposeService s + ? OpenServiceTerminalAsync(s) + : Task.CompletedTask); + ToggleLogsCommand = new RelayCommand(_ => ToggleLogsAsync()); + ClearLogsCommand = new RelayCommand(_ => + { + _logs.Clear(); + return Task.CompletedTask; + }); + CopyOutputCommand = new RelayCommand(_ => Shell.Context.Clipboard.SetTextAsync( + string.Join('\n', Output.Select(o => $"{o.Time} {o.Text}")), Shell.Lifetime)); + NewProjectCommand = new RelayCommand(_ => NewProjectAsync()); + } + + /// 切页签。 + public RelayCommand SetTabCommand { get; } + + /// 保存 .env + public RelayCommand SaveEnvCommand { get; } + + /// 撤销 .env 的改动。 + public RelayCommand RevertEnvCommand { get; } + + /// 看某个服务的日志。 + public RelayCommand ServiceLogsCommand { get; } + + /// 重启某个服务。 + public RelayCommand ServiceRestartCommand { get; } + + /// 进某个服务的终端。 + public RelayCommand ServiceTerminalCommand { get; } + + /// 开始 / 停止跟随合并日志。 + public RelayCommand ToggleLogsCommand { get; } + + /// 清空合并日志。 + public RelayCommand ClearLogsCommand { get; } + + /// 复制执行记录。 + public RelayCommand CopyOutputCommand { get; } + + /// 新建一个 compose 项目。 + public RelayCommand NewProjectCommand { get; } + + /// + public override PanelPage Page => PanelPage.Compose; + + /// + public override string Title => "Compose"; + + /// 项目。 + public ObservableCollection Projects { get; } = []; + + /// 选中项目的服务。 + public ObservableCollection Services { get; } = []; + + /// 服务表的列宽。 + public ComposeServiceColumns Columns { get; } = new(); + + /// + public override ListColumns ColumnLayout => Columns; + + /// + public override IEnumerable ColumnTexts(string key) => key switch + { + "service" => Services.Select(s => s.Service), + "name" => Services.Select(s => s.Name), + "status" => Services.Select(s => s.Status ?? ""), + "ports" => Services.Select(s => s.PortsText), + _ => [] + }; + + /// 执行记录。 + public ObservableCollection Output { get; } = []; + + /// 当前项目。 + public ComposeProject? Selected + { + get; + private set + { + if (SetField(ref field, value)) + { + OnPropertiesChanged(nameof(HasSelection), nameof(ProjectName), nameof(ProjectStatus), + nameof(ProjectPrefix), nameof(ProjectFile)); + } + } + } + + /// 选了项目没有。 + public bool HasSelection => Selected is not null; + + /// 项目名。 + public string ProjectName => Selected?.Name ?? ""; + + /// 项目状态串。 + public string ProjectStatus => Selected?.Status ?? ""; + + /// compose 命令的固定前缀(显示给用户看清楚)。 + public string ProjectPrefix => Selected is { } project + ? $"-p {project.Name} -f {project.PrimaryFile} --project-directory {project.ProjectDirectory}" + : ""; + + /// compose 文件路径。 + public string ProjectFile => Selected?.PrimaryFile ?? ""; + + /// compose 文件内容。 + public string Yaml + { + get; + set + { + if (SetField(ref field, value)) + { + OnPropertiesChanged(nameof(IsModified), nameof(ModifiedText)); + } + } + } = ""; + + /// 改过了没有。 + public bool IsModified => Yaml != _originalYaml && _originalYaml.Length > 0; + + /// 改动摘要。 + public string ModifiedText => IsModified ? "未保存 · 保存需手打 save" : ""; + + /// 出错信息。 + public string Error + { + get; + private set + { + if (SetField(ref field, value)) + { + OnPropertyChanged(nameof(HasError)); + } + } + } = ""; + + /// 有没有出错。 + public bool HasError => Error.Length > 0; + + /// 有 compose 命令在跑。 + public bool Running + { + get; + private set => SetField(ref field, value); + } + + /// 这台机器上有没有 compose v2。 + public bool ComposeAvailable + { + get; + private set + { + if (SetField(ref field, value)) + { + OnPropertyChanged(nameof(UnavailableHint)); + } + } + } = true; + + /// compose 用不了时的说明。 + public string UnavailableHint => Shell.Compose is { IsLocal: true } + ? "本机的 PATH 上找不到 docker compose(v2)。Docker Desktop 自带它,若刚装好请重开一次终端 / 宿主程序让 PATH 生效;compose v1 那个独立的 docker-compose 没有 ls 子命令,面板列不出项目。" + : "远端没有 docker compose(v2)。compose v1 那个独立的 docker-compose 没有 ls 子命令,面板列不出项目。"; + + /// 列表空了。 + public bool IsEmpty => LoadedOnce && Projects.Count == 0; + + /// 选一个项目。 + public RelayCommand SelectCommand { get; } + + /// up -d。 + public RelayCommand UpCommand { get; } + + /// stop。 + public RelayCommand StopCommand { get; } + + /// restart。 + public RelayCommand RestartCommand { get; } + + /// pull。 + public RelayCommand PullCommand { get; } + + /// build。 + public RelayCommand BuildCommand { get; } + + /// down。 + public RelayCommand DownCommand { get; } + + /// down -v。 + public RelayCommand DownVolumesCommand { get; } + + /// 展开配置(顺带校验语法)。 + public RelayCommand ConfigCommand { get; } + + /// 保存 compose 文件。 + public RelayCommand SaveCommand { get; } + + /// 撤销未保存的修改。 + public RelayCommand RevertCommand { get; } + + /// 清空执行记录。 + public RelayCommand ClearOutputCommand { get; } + + /// 按路径打开一个项目。 + public RelayCommand OpenByPathCommand { get; } + + /// 刷新。 + public RelayCommand RefreshCommand { get; } + + /// + public override async Task RefreshAsync(CancellationToken cancellationToken) + { + if (Shell.Compose is not { } compose) + { + return; + } + Busy = true; + try + { + ComposeAvailable = await compose.IsAvailableAsync(cancellationToken).ConfigureAwait(true); + var projects = ComposeAvailable + ? await compose.ListProjectsAsync(cancellationToken).ConfigureAwait(true) + : []; + var keep = Selected?.Name; + Projects.Clear(); + foreach (var project in projects.OrderBy(p => p.Name, StringComparer.OrdinalIgnoreCase)) + { + Projects.Add(project); + } + LoadedOnce = true; + OnPropertyChanged(nameof(IsEmpty)); + if (keep is not null && Projects.FirstOrDefault(p => p.Name == keep) is { } same) + { + Selected = same; + await LoadServicesAsync(same, cancellationToken).ConfigureAwait(true); + } + else if (Selected is null && Projects.Count > 0) + { + await SelectAsync(Projects[0]).ConfigureAwait(true); + } + } + finally + { + Busy = false; + } + } + + /// + public override void Reset() + { + Projects.Clear(); + Services.Clear(); + _output.Clear(); + _logs.Clear(); + _extraSources.Clear(); + Selected = null; + Yaml = ""; + _originalYaml = ""; + Error = ""; + LoadedOnce = false; + OnPropertyChanged(nameof(IsEmpty)); + } + + /// + public override bool WantsRefresh(DockerEvent dockerEvent) => dockerEvent.Type == "container"; + + /// + /// 把用户送到 Compose 页并选中这个项目 —— 执行记录就在那一页的右侧那一栏。 + /// 已经在这一页、而且选的就是它,就什么都不做:重选一次会把刚出的那份记录冲掉。 + /// + private async Task ShowOutputAsync(ComposeProject project) + { + await Shell.GoToAsync(PanelPage.Compose).ConfigureAwait(true); + if (Selected?.Name != project.Name) + { + await SelectAsync(project).ConfigureAwait(true); + } + } + + private async Task SelectAsync(ComposeProject project) + { + await StopLogsAsync().ConfigureAwait(true); + Selected = project; + Error = ""; + Config = ""; + _logs.Clear(); + // 换项目就换一组服务,旧的颜色序号留着只会让新项目的颜色对不上名字。 + _extraSources.Clear(); + Tab = ComposeTab.Yaml; + await LoadServicesAsync(project, Shell.Lifetime).ConfigureAwait(true); + await LoadYamlAsync(project).ConfigureAwait(true); + await LoadEnvAsync(project).ConfigureAwait(true); + } + + private async Task SetTabAsync(ComposeTab tab) + { + Tab = tab; + switch (tab) + { + // config 展开要跑一次 compose 命令,只在进这一页时跑,而且只跑一次。 + case ComposeTab.Config when Config.Length == 0: + await ShowConfigAsync().ConfigureAwait(true); + break; + case ComposeTab.Logs when !LogsFollowing && Logs.Count == 0: + await ToggleLogsAsync().ConfigureAwait(true); + break; + } + } + + /// + /// 读项目的 .env。 + /// + /// 读不到就当没有 —— 大多数项目根本没有这个文件,把"文件不存在"报成错误 + /// 会让一个正常的项目看起来出了问题。 + /// + /// + private async Task LoadEnvAsync(ComposeProject project) + { + _originalEnv = ""; + Env = ""; + HasEnv = false; + if (Shell.Compose is not { } compose || ComposeCli.EnvPath(project) is not { Length: > 0 } path) + { + return; + } + try + { + _originalEnv = await compose.ReadFileAsync(path, Shell.Lifetime).ConfigureAwait(true); + Env = _originalEnv; + HasEnv = true; + } + catch (Exception) + { + HasEnv = false; + } + } + + private async Task SaveEnvAsync() + { + if (Shell.Compose is not { } compose || Selected is not { } project || !EnvModified) + { + return; + } + var path = ComposeCli.EnvPath(project); + var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + { + Title = $"覆盖 {path}?", + Icon = "Docker.shield-alert", + Tier = ConfirmTier.DataLoss, + ConfirmWord = "save", + ConfirmLabel = compose.IsLocal ? "写回本机" : "写回远端", + ConfirmIcon = "Icon.save", + HostName = "", + Commands = [compose.IsLocal ? $"WRITE {path}" : $"SFTP PUT {path}"], + CommandNote = compose.IsLocal + ? ".env 直接落盘覆盖,不经 shell。" + : ".env 经 SFTP 覆盖写,不经 shell。", + DataLossHeadline = "原文件会被整体覆盖,无法撤销", + DataLossPoints = + [ + "这里常放的是口令与密钥 —— 覆盖之前确认你手上这份是完整的。", + "已经在跑的容器不会因此重新读取环境变量,要 up -d 重建才生效。" + ] + })).ConfigureAwait(true); + if (!confirmed) + { + return; + } + try + { + await compose.WriteFileAsync(path, Env, Shell.Lifetime).ConfigureAwait(true); + _originalEnv = Env; + OnPropertiesChanged(nameof(EnvModified), nameof(EnvModifiedText)); + HasEnv = true; + Log($"✔ 已写回 {path}"); + Shell.Feedback.Notify(FeedbackKind.Success, "已写回 .env", + "已在跑的容器要 up -d 重建才会读到新值。"); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Shell.Feedback.ReportError("写回 .env", ex); + } + } + + /// + /// 把界面上那串固定写法("up -d")切成 argv。 + /// + /// 这些串全是代码里的字面量、不含用户输入,所以按空格切是安全的; + /// 项目名、文件路径这些真会带空格的东西从来不走这里,它们由 ComposeCli + /// 自己按 argv 拼进前缀。 + /// + /// + private static string[] Argv(string arguments) => + arguments.Split(' ', StringSplitOptions.RemoveEmptyEntries); + + private async Task RunForServiceAsync(string arguments, ComposeService service, string title) + { + if (Shell.Compose is not { } compose || Selected is not { } project || Running) + { + return; + } + Running = true; + Log($"$ docker compose {ProjectPrefix} {arguments} {service.Service}", isCommand: true); + try + { + var exit = await compose.RunForServiceAsync(project, Argv(arguments), service.Service, + new DirectProgress(output => + Log(output.Line, output.Stream == ExecStream.StandardError)), + Shell.Lifetime).ConfigureAwait(true); + _lastExitCode = exit; + OnPropertiesChanged(nameof(ExitCodeText), nameof(ExitTone)); + Log(exit == 0 ? $"✔ {title} 完成" : $"✘ {title} 失败 · 退出码 {exit}", isError: exit != 0); + await LoadServicesAsync(project, Shell.Lifetime).ConfigureAwait(true); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Log(ex.Message, isError: true); + Shell.Feedback.ReportError(title, ex); + } + finally + { + // 最后那几行不必再等一个 120ms 的节拍。 + _output.Flush(); + Running = false; + } + } + + /// 进这个服务对应容器的终端(借容器页的详情抽屉)。 + private async Task OpenServiceTerminalAsync(ComposeService service) + { + await Shell.GoToAsync(PanelPage.Containers).ConfigureAwait(true); + var row = Shell.Containers.View.FirstOrDefault(r => r.Name == service.Name); + if (row is null) + { + Shell.Feedback.Status(FeedbackKind.Warning, $"容器列表里没有 {service.Name} —— 它可能还没起来。"); + return; + } + Shell.Containers.RowTerminalCommand.Execute(row); + } + + /// + /// 跟随项目的合并日志。 + /// + /// 交给 compose logs -f 去并,而不是面板自己并 N 条 docker logs —— + /// compose 认得项目里有哪些服务,包括面板列表还没刷到的那些。 + /// + /// + private async Task ToggleLogsAsync() + { + if (LogsFollowing) + { + await StopLogsAsync().ConfigureAwait(true); + return; + } + if (Shell.Compose is not { } compose || Selected is not { } project) + { + return; + } + _logsCts = CancellationTokenSource.CreateLinkedTokenSource(Shell.Lifetime); + var token = _logsCts.Token; + LogsFollowing = true; + var tail = Shell.Settings.LogTail; + _ = Task.Run(async () => + { + try + { + await compose.FollowLogsAsync(project, tail == "all" ? "all" : tail, + new DirectProgress(output => _logs.Add(MergedLine(output))), token) + .ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + catch (Exception ex) + { + _logs.Add(new(DateTimeOffset.Now.ToString("HH:mm:ss"), + $"日志流断开:{ex.Message}", true, false)); + } + Ui.Post(() => LogsFollowing = false); + }, token); + } + + /// + /// 把 compose logs 的一行变成界面上的一行:拆出服务名,给它一个稳定的颜色序号。 + /// + /// 序号取自 里的次序,而不是"第几个说话的" —— 后者会让 + /// 同一个服务在两次打开日志时拿到不同的颜色,颜色就失去了指认的作用。 + /// + /// + private OutputLine MergedLine(ExecOutput output) + { + (var source, var body) = MergedLog.Split(output.Line, IsKnownSource); + return new(DateTimeOffset.Now.ToString("HH:mm:ss"), body, + output.Stream == ExecStream.StandardError, false, source, SourceIndex(source)); + } + + /// + /// 这个前缀是不是本项目里的一个服务。 + /// + /// compose logs 的前缀有时是服务名、有时是容器名(取决于 compose 的版本与 + /// container_name 有没有写),两个都认。 + /// + /// + private bool IsKnownSource(string name) + { + foreach (var service in Services) + { + if (service.Service == name || service.Name == name) + { + return true; + } + } + return _extraSources.ContainsKey(name); + } + + private int SourceIndex(string source) + { + if (source.Length == 0) + { + return 0; + } + for (var i = 0; i < Services.Count; i++) + { + if (Services[i].Service == source || Services[i].Name == source) + { + return i; + } + } + // 服务列表里没有的来源(还没刷新到的、一次性任务),各自也要一个稳定的序号。 + if (!_extraSources.TryGetValue(source, out var extra)) + { + extra = Services.Count + _extraSources.Count; + _extraSources[source] = extra; + } + return extra; + } + + private async Task StopLogsAsync() + { + if (_logsCts is not { } cts) + { + return; + } + await cts.CancelAsync().ConfigureAwait(true); + cts.Dispose(); + _logsCts = null; + LogsFollowing = false; + } + + /// + /// 新建一个 compose 项目:写一个骨架 compose.yaml,然后按路径打开它。 + /// + /// 顺手 up -d —— 骨架里的服务是占位的,起起来只会得到一个失败的容器。 + /// + /// + private async Task NewProjectAsync() + { + if (Shell.Compose is not { } compose) + { + return; + } + var form = new NewComposeProjectForm(compose.IsLocal); + if (!await Shell.ShowFormAsync(form).ConfigureAwait(true)) + { + return; + } + var path = ComposePath.Combine(form.Directory, "compose.yaml"); + try + { + await compose.WriteFileAsync(path, NewComposeProjectForm.Skeleton(form.ProjectName), Shell.Lifetime) + .ConfigureAwait(true); + Log($"✔ 已创建 {path}"); + Shell.Feedback.Notify(FeedbackKind.Success, "项目已创建", + $"{path} —— 改完 compose.yaml 之后按 up -d 起它。"); + await OpenPathAsync(path).ConfigureAwait(true); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Shell.Feedback.ReportError("新建 Compose 项目", ex); + } + } + + private async Task LoadServicesAsync(ComposeProject project, CancellationToken cancellationToken) + { + if (Shell.Compose is not { } compose) + { + return; + } + var services = await compose.ListServicesAsync(project, cancellationToken).ConfigureAwait(true); + Services.Clear(); + foreach (var service in services) + { + Services.Add(service); + } + } + + private async Task LoadYamlAsync(ComposeProject project) + { + if (Shell.Compose is not { } compose || project.PrimaryFile.Length == 0) + { + Yaml = ""; + _originalYaml = ""; + return; + } + try + { + // 直接读文件,不经 shell —— 免得被登录 shell 的输出、locale 与引用规则搅进来。 + _originalYaml = await compose.ReadFileAsync(project.PrimaryFile, Shell.Lifetime).ConfigureAwait(true); + Yaml = _originalYaml; + Error = ""; + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + _originalYaml = ""; + Yaml = ""; + Error = $"读不到 {project.PrimaryFile}:{ex.Message}"; + } + } + + private void Log(string text, bool isError = false, bool isCommand = false) => + _output.Add(new(DateTimeOffset.Now.ToString("HH:mm:ss"), text, isError, isCommand)); + + private async Task RunAsync(string arguments, string title) + { + if (Shell.Compose is not { } compose || Selected is not { } project || Running) + { + return; + } + Running = true; + var task = Shell.Tasks.Start("Docker.boxes", $"{title} · {project.Name}", indeterminate: true); + Log($"$ docker compose {ProjectPrefix} {arguments}", isCommand: true); + try + { + var exit = await compose.RunAsync(project, Argv(arguments), + new DirectProgress(output => + Log(output.Line, output.Stream == ExecStream.StandardError)), + task.Token).ConfigureAwait(true); + _lastExitCode = exit; + OnPropertiesChanged(nameof(ExitCodeText), nameof(ExitTone)); + if (exit == 0) + { + task.Finish(PanelTaskState.Succeeded, "完成"); + Log($"✔ {title} 完成 · 退出码 0"); + Shell.Feedback.Status(FeedbackKind.Success, $"{title} 完成 · {project.Name}"); + } + else + { + task.Finish(PanelTaskState.Failed, $"退出码 {exit}"); + Log($"✘ {title} 失败 · 退出码 {exit}", isError: true); + // 执行记录就在 Compose 页右侧那一栏 —— 把用户送过去并选中出事的那个项目。 + // 原来这颗按钮挂的是一个空 lambda:点了什么都不会发生。 + Shell.Feedback.Notify(FeedbackKind.Error, $"{title} 失败", $"{project.Name} · 退出码 {exit}", + new ToastAction("查看执行记录", () => _ = ShowOutputAsync(project))); + } + await RefreshAsync(Shell.Lifetime).ConfigureAwait(true); + } + catch (OperationCanceledException) + { + task.Finish(PanelTaskState.Cancelled, "已取消"); + Log("! 已取消", isError: true); + } + catch (Exception ex) + { + task.Finish(PanelTaskState.Failed, "失败", ex.Message); + Log(ex.Message, isError: true); + Shell.Feedback.ReportError(title, ex); + } + finally + { + // 最后那几行不必再等一个 120ms 的节拍。 + _output.Flush(); + Running = false; + } + } + + private async Task DownAsync(bool withVolumes) + { + if (Selected is not { } project) + { + return; + } + var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + { + Title = withVolumes ? $"down -v 项目 {project.Name}?" : $"down 项目 {project.Name}?", + Icon = withVolumes ? "Docker.shield-alert" : "Icon.trash-2", + Tier = withVolumes ? ConfirmTier.DataLoss : ConfirmTier.Destructive, + ConfirmWord = "delete", + ConfirmLabel = withVolumes ? "停止并删除卷" : "停止并删除容器", + HostName = "", + Commands = [$"docker compose {ProjectPrefix} down{(withVolumes ? " -v" : "")}"], + CommandNote = "compose 会按依赖顺序停止并删除这个项目的容器与网络。", + Consequences = withVolumes + ? [] + : + [ + new(2, "项目的容器与默认网络会被删除。"), + new(1, "命名卷不受影响 —— 数据还在,下次 up 会挂回去。"), + new(0, "compose 文件不动,随时可以再 up 起来。") + ], + DataLossHeadline = withVolumes ? "-v 会把这个项目的命名卷一起删掉,数据永久丢失" : null, + DataLossPoints = withVolumes + ? + [ + "数据库、上传目录、缓存 —— 只要是这个项目声明的命名卷,全部清空。", + "Docker 不做回收站,也没有快照。", + "只想停服务的话用不带 -v 的 down,或者直接 stop。" + ] + : [] + })).ConfigureAwait(true); + if (confirmed) + { + await RunAsync(withVolumes ? "down -v" : "down", withVolumes ? "down -v" : "down").ConfigureAwait(true); + } + } + + private async Task ShowConfigAsync() + { + if (Shell.Compose is not { } compose || Selected is not { } project) + { + return; + } + Log($"$ docker compose {ProjectPrefix} config", isCommand: true); + Config = "正在展开…"; + try + { + var result = await compose.ConfigAsync(project, Shell.Lifetime).ConfigureAwait(true); + // 展开的结果进它自己那一页,不再倒进执行记录 —— 几百行 YAML 会把记录冲干净。 + Config = result.IsSuccess ? result.Output : result.Error; + Log(result.IsSuccess ? "✔ 配置可以解析 —— 语法没问题" : "✘ 配置有问题(见 config 页签)", + isError: !result.IsSuccess); + Shell.Feedback.Status(result.IsSuccess ? FeedbackKind.Success : FeedbackKind.Error, + result.IsSuccess ? "配置可以解析 —— 语法没问题" : $"配置有问题:{result.FailureText}"); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Shell.Feedback.ReportError("展开配置", ex); + } + } + + private async Task SaveYamlAsync() + { + if (Shell.Compose is not { } compose || Selected is not { } project || !IsModified) + { + return; + } + var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + { + Title = $"覆盖 {project.PrimaryFile}?", + Icon = "Docker.shield-alert", + Tier = ConfirmTier.DataLoss, + ConfirmWord = "save", + ConfirmLabel = compose.IsLocal ? "写回本机" : "写回远端", + ConfirmIcon = "Icon.save", + HostName = "", + Commands = [compose.IsLocal ? $"WRITE {project.PrimaryFile}" : $"SFTP PUT {project.PrimaryFile}"], + CommandNote = compose.IsLocal ? "直接写这个文件,不经过 shell。" : "经 SFTP 直接写,不经过 shell。", + DataLossHeadline = compose.IsLocal ? "这份 compose 文件会被整体覆盖" : "远端那份 compose 文件会被整体覆盖", + DataLossPoints = + [ + "面板不做备份,那个位置也没有版本历史 —— 除非那个目录在 git 里。", + "保存不会自动 up:改动要等下一次 up -d 才生效。", + "保存前建议先跑一次 config 确认语法能解析。" + ] + })).ConfigureAwait(true); + if (!confirmed) + { + return; + } + try + { + await compose.WriteFileAsync(project.PrimaryFile, Yaml, Shell.Lifetime).ConfigureAwait(true); + _originalYaml = Yaml; + OnPropertiesChanged(nameof(IsModified), nameof(ModifiedText)); + Shell.Feedback.Notify(FeedbackKind.Success, "已写回", project.PrimaryFile); + Log($"✔ 已写回 {project.PrimaryFile}"); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Shell.Feedback.ReportError("写回 compose 文件", ex); + } + } + + private async Task OpenByPathAsync() + { + if (Shell.Compose is not { } compose) + { + return; + } + var form = new OpenComposeForm(compose.IsLocal); + if (!await Shell.ShowFormAsync(form).ConfigureAwait(true)) + { + return; + } + await OpenPathAsync(form.FilePath, form.ProjectName).ConfigureAwait(true); + } + + /// 把一个 compose 文件加进项目列表并选中它。 + private async Task OpenPathAsync(string filePath, string? projectName = null) + { + // compose 的项目名默认取项目目录名 —— 与 compose 自己的规则一致, + // 不一致的话面板起的容器会带上一个和命令行不同的前缀。 + var directory = filePath[..Math.Max(0, filePath.LastIndexOf('/'))]; + var name = projectName is { Length: > 0 } given + ? given + : directory[(directory.LastIndexOf('/') + 1)..]; + var project = new ComposeProject(name, "(未起过)", filePath); + if (Projects.All(p => p.PrimaryFile != filePath)) + { + Projects.Add(project); + } + await SelectAsync(project).ConfigureAwait(true); + OnPropertyChanged(nameof(IsEmpty)); + } +} + +/// 按路径打开一个 compose 项目。 +public sealed class OpenComposeForm : PanelForm +{ + private readonly TextField _path; + private readonly TextField _name; + + private readonly bool _isLocal; + + /// 建表单。 决定占位示例写成哪种路径。 + public OpenComposeForm(bool isLocal) + { + _isLocal = isLocal; + _path = new("compose 文件路径") + { + Placeholder = isLocal ? @"D:\stacks\web-stack\compose.yaml" : "/srv/stacks/web-stack/compose.yaml" + }; + _name = new("项目名") { Hint = "留空按目录名推导", Placeholder = "web-stack" }; + Watch(_path); + Watch(_name); + UpdatePreview(); + } + + /// + public override string Title => "按路径打开项目"; + + /// + public override string Icon => "Icon.folder-open"; + + /// + public override string ConfirmLabel => "打开"; + + /// + public override string FooterHint => "compose ls 只认得起过至少一次的项目"; + + /// 文件路径。 + public string FilePath => _path.Value.Trim(); + + /// 项目名。 + public string ProjectName + { + get + { + if (_name.Value.Trim() is { Length: > 0 } explicitName) + { + return explicitName; + } + // 项目名默认取**文件所在目录**的名字 —— compose 自己也是这么推的。 + return ComposePath.LastSegment(ComposePath.DirectoryOf(FilePath)); + } + } + + /// + public override bool Validate() + { + if (FilePath.Length == 0) + { + _path.Error = "路径不能为空。"; + return false; + } + if (!ComposePath.IsAbsolute(FilePath)) + { + _path.Error = _isLocal + ? "要用绝对路径(带盘符,或 / 打头)—— 相对路径会以面板的工作目录为基准,那多半不是你想要的。" + : "要用绝对路径 —— 相对路径会以登录目录为基准,那多半不是你想要的。"; + return false; + } + return true; + } + + /// + protected override void UpdatePreview() + { + CommandPreview = $"docker compose -p {ProjectName} -f {FilePath} --project-directory <文件所在目录>"; + CommandNote = "三个参数一起钉住:少 -f 找不到 yml,少 -p 项目名会被重新推导,少 --project-directory 里面的相对路径会挂错盘。"; + } +} + +/// +/// 新建一个 compose 项目。 +/// +/// 只写一个骨架 compose.yaml,**不**顺手 up -d —— +/// 骨架里的服务是占位的,起起来只会得到一个失败的容器和一条看不懂的报错。 +/// +/// +public sealed class NewComposeProjectForm : PanelForm +{ + private readonly TextField _directory; + private readonly TextField _name; + + private readonly bool _isLocal; + + /// 建表单。 决定占位示例写成哪种路径。 + public NewComposeProjectForm(bool isLocal) + { + _isLocal = isLocal; + _directory = new("项目目录") { Placeholder = isLocal ? @"D:\stacks\new-stack" : "/srv/stacks/new-stack" }; + _name = new("项目名") { Hint = "留空按目录名推导", Placeholder = "new-stack" }; + Watch(_directory); + Watch(_name); + UpdatePreview(); + } + + /// + public override string Title => "新建 Compose 项目"; + + /// + public override string Icon => "Docker.file-code"; + + /// + public override string ConfirmLabel => "创建"; + + /// + public override string ConfirmIcon => "Icon.plus"; + + /// + public override string FooterHint => "只写文件,不起容器 —— 改完 compose.yaml 再按 up -d"; + + /// 项目目录。 + public string Directory => _directory.Value.Trim(); + + /// 项目名。 + public string ProjectName + { + get + { + return _name.Value.Trim() is { Length: > 0 } explicitName ? explicitName : ComposePath.LastSegment(Directory); + } + } + + /// 骨架内容。注释里写清下一步该干什么,而不是丢一个空文件给用户。 + public static string Skeleton(string projectName) => + $""" + # {projectName} —— 由 VelaShell Docker 面板创建 + # 把下面这个占位服务换成你自己的,然后在面板里按 up -d。 + + services: + app: + image: nginx:alpine + restart: unless-stopped + ports: + - "8080:80" + + """; + + /// + public override bool Validate() + { + if (Directory.Length == 0) + { + _directory.Error = "目录不能为空。"; + return false; + } + if (!ComposePath.IsAbsolute(Directory)) + { + _directory.Error = _isLocal + ? "要用绝对路径(带盘符,或 / 打头)—— 相对路径会以面板的工作目录为基准,那多半不是你想要的。" + : "要用绝对路径 —— 相对路径会以登录目录为基准,那多半不是你想要的。"; + return false; + } + return true; + } + + /// + protected override void UpdatePreview() + { + CommandPreview = $"{(_isLocal ? "WRITE" : "SFTP PUT")} {ComposePath.Combine(Directory, "compose.yaml")}"; + CommandNote = $"随后用 -p {ProjectName} 打开它。目录必须已经存在 —— 面板不替你 mkdir。"; + } +} + +/// +/// 服务表的列宽。 +/// +/// 缺省值合计 600,加上行首行尾的固定占位与四条轨道之后仍给中间那根 * 留了余量 —— +/// 四列一上来就把宽度占满的话,每一列的"还能拖多宽"当场就是 0,拖谁都只会缩到下限。 +/// 端口那一列最容易长(一个服务映射四五个端口是常事),所以它的自适应上限放得比别的宽。 +/// +/// +public sealed class ComposeServiceColumns : ListColumns +{ + + /// + public override IReadOnlyList Keys { get; } = ["service", "name", "status", "ports"]; + + /// 服务列。 + public GridLength Service + { + get; + set => SetField(ref field, Clamp(value, "service")); + } = new(130); + + /// 容器列。 + public GridLength Name + { + get; + set => SetField(ref field, Clamp(value, "name")); + } = new(190); + + /// 状态列。 + public GridLength Status + { + get; + set => SetField(ref field, Clamp(value, "status")); + } = new(130); + + /// 端口列。 + public GridLength Ports + { + get; + set => SetField(ref field, Clamp(value, "ports")); + } = new(150); + + /// + public override double Get(string key) => key switch + { + "service" => Service.Value, + "name" => Name.Value, + "status" => Status.Value, + _ => Ports.Value + }; + + /// + public override void Set(string key, double width) + { + GridLength value = new(width); + switch (key) + { + case "service": + Service = value; + break; + case "name": + Name = value; + break; + case "status": + Status = value; + break; + default: + Ports = value; + break; + } + } + + /// + public override double Min(string key) => key switch + { + "service" => 80, + "name" => 110, + _ => 90 + }; + + /// + public override double MaxAutoFit(string key) => key == "ports" ? 520 : 420; +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerDetailView.axaml b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerDetailView.axaml new file mode 100644 index 0000000..a8c7a90 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerDetailView.axaml @@ -0,0 +1,475 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerDetailView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerDetailView.axaml.cs new file mode 100644 index 0000000..e2e0971 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerDetailView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace VelaShell.Plugin.DockerPanel.Ui.Pages; + +/// ContainerDetailView 的视图。 +public sealed partial class ContainerDetailView : UserControl +{ + /// 建视图。 + public ContainerDetailView() => AvaloniaXamlLoader.Load(this); +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerDetailViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerDetailViewModel.cs new file mode 100644 index 0000000..fa76f22 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerDetailViewModel.cs @@ -0,0 +1,701 @@ +using System.Collections.ObjectModel; +using VelaShell.Plugin.DockerPanel.Docker; + +namespace VelaShell.Plugin.DockerPanel.Ui.Pages; + +/// 详情抽屉的页签。 +public enum DetailTab +{ + /// 概览。 + Overview, + + /// 日志。 + Logs, + + /// 文件。 + Files, + + /// 终端。 + Terminal, + + /// 统计。 + Stats +} + +/// 概览页里的一条键值。 +/// 键。 +/// 值。 +/// 语气(决定值的颜色)。 +public readonly record struct DetailField(string Key, string Value, RowTone Tone = RowTone.Idle); + +/// 概览里的一条挂载。 +/// 图标资源键。 +/// “源 → 目标”。 +/// 只读 / 读写。 +/// 是不是只读。 +public readonly record struct MountLine(string Icon, string Text, string Mode, bool ReadOnly); + +/// +/// 容器详情抽屉。 +/// +/// 抽屉在右侧而不是底部:详情与列表同屏对照,选中哪一行就看哪一行 —— +/// 底部抽屉会把列表挤成三行,而那正是用户要在里面找东西的地方。 +/// +/// +public sealed class ContainerDetailViewModel : ObservableObject, IAsyncDisposable +{ + private readonly DockerPanelViewModel _shell; + private CancellationTokenSource? _statsCts; + private ContainerRow _row; + private ContainerInspect? _inspect; + + /// 建抽屉。 + public ContainerDetailViewModel(DockerPanelViewModel shell, ContainersPageViewModel page, ContainerRow row) + { + _shell = shell; + Owner = page; + _row = row; + Logs = new(shell, row.Id, () => _inspect?.Config is not null && IsTty); + Files = new(shell, row.Id, row.Name); + Terminal = new(shell, row.Id, row.Name); + + SetTabCommand = new RelayCommand(p => p is DetailTab tab ? SetTabAsync(tab) : Task.CompletedTask); + RefreshProcessesCommand = new RelayCommand(_ => RefreshProcessesAsync(_shell.Lifetime)); + CloseCommand = new RelayCommand(_ => page.CloseDetailCommand.Execute(null)); + CommitCommand = new RelayCommand(_ => CommitAsync()); + ToggleRawCommand = new RelayCommand(_ => ShowRaw = !ShowRaw); + CopyIdCommand = new RelayCommand(_ => shell.Context.Clipboard.SetTextAsync(row.Id, shell.Lifetime)); + StartCommand = new RelayCommand(_ => page.StartCommand.Execute(_row)); + StopCommand = new RelayCommand(_ => page.StopCommand.Execute(_row)); + RestartCommand = new RelayCommand(_ => page.RestartCommand.Execute(_row)); + PauseCommand = new RelayCommand(_ => (_row.IsPaused ? page.UnpauseCommand : page.PauseCommand).Execute(_row)); + RemoveCommand = new RelayCommand(_ => page.RemoveCommand.Execute(_row)); + RenameCommand = new RelayCommand(_ => RenameAsync()); + RestartPolicyCommand = new RelayCommand(_ => ChangeRestartPolicyAsync()); + } + + /// 容器 id。 + public string ContainerId => _row.Id; + + /// 容器名。 + public string Name => _row.Name; + + /// 短 id。 + public string ShortId => _row.ShortId; + + /// 状态串。 + public string Status => _row.Status; + + /// 状态色。 + public RowTone Tone => _row.Tone; + + /// compose 归属(“项目 / 服务”)。 + public string ComposePath => _row.Summary is { ComposeProject: { Length: > 0 } project } summary + ? $"{project} / {summary.ComposeService ?? "—"}" + : ""; + + /// 属不属于某个 compose 项目。 + public bool HasCompose => ComposePath.Length > 0; + + /// 是否在跑。 + public bool IsRunning => _row.IsRunning; + + /// 是否暂停。 + public bool IsPaused => _row.IsPaused; + + /// 暂停按钮的文字(暂停 / 恢复)。 + public string PauseLabel => IsPaused ? "恢复" : "暂停"; + + /// 容器有没有分配 TTY —— 决定日志与 exec 的解帧方式。 + public bool IsTty { get => _inspect?.Config is not null && _row.Summary.State == "running" && field; private set; } + + /// 当前页签。 + public DetailTab Tab + { + get; + private set + { + if (SetField(ref field, value)) + { + OnPropertiesChanged(nameof(IsOverview), nameof(IsLogs), nameof(IsFiles), nameof(IsTerminal), nameof(IsStats)); + } + } + } = DetailTab.Overview; + + /// 当前是概览页。 + public bool IsOverview => Tab == DetailTab.Overview; + + /// 当前是日志页。 + public bool IsLogs => Tab == DetailTab.Logs; + + /// 当前是文件页。 + public bool IsFiles => Tab == DetailTab.Files; + + /// 当前是终端页。 + public bool IsTerminal => Tab == DetailTab.Terminal; + + /// 当前是统计页。 + public bool IsStats => Tab == DetailTab.Stats; + + /// 基本信息。 + public ObservableCollection Basics { get; } = []; + + /// 端口映射。 + public ObservableCollection PortLines { get; } = []; + + /// 挂载。 + public ObservableCollection Mounts { get; } = []; + + /// 网络。 + public ObservableCollection Networks { get; } = []; + + /// 环境变量。 + public ObservableCollection Environment { get; } = []; + + /// 健康检查状态。 + public string HealthText { get; private set; } = ""; + + /// 有没有健康检查。 + public bool HasHealth => HealthText.Length > 0; + + /// inspect 的原文。 + public string RawInspect + { + get; + private set => SetField(ref field, value); + } = ""; + + /// 概览里是不是在看原文。 + public bool ShowRaw + { + get; + set => SetField(ref field, value); + } + + /// 实时 CPU 百分比 0–100。 + public double CpuPercent + { + get; + private set => SetField(ref field, value); + } + + /// CPU 文本。 + public string CpuText + { + get; + private set => SetField(ref field, value); + } = "—"; + + /// 内存文本。 + public string MemText + { + get; + private set => SetField(ref field, value); + } = "—"; + + /// 内存上限文本。 + public string MemLimitText + { + get; + private set => SetField(ref field, value); + } = ""; + + /// 内存占比 0–1。 + public double MemRatio + { + get; + private set => SetField(ref field, value); + } + + /// + /// 卡片右上角那行小字。CPU 给的是这段采样里的峰值 —— + /// 当前值是 2%、峰值是 90% 的容器,和一直 2% 的容器是两回事, + /// 而只看当下那一个数字,这两者长得一模一样。 + /// + public string CpuPeakText + { + get; + private set => SetField(ref field, value); + } = ""; + + /// 内存卡片右上角:占上限的百分比。没有上限时为空。 + public string MemRatioText + { + get; + private set => SetField(ref field, value); + } = ""; + + /// CPU 采样(抽屉里那条图)。 + public ObservableCollection CpuHistory { get; } = []; + + /// 内存采样。 + public ObservableCollection MemHistory { get; } = []; + + /// 容器里正在跑的进程(docker top)。 + public ObservableCollection Processes { get; } = []; + + /// 进程表第三列的表头 —— 远端的 ps 给的是 TIME 还是 %CPU 由它决定。 + public string ProcessCpuTitle + { + get; + private set => SetField(ref field, value); + } = "CPU"; + + /// 进程表的一句话状态(空 = 有数据)。 + public string ProcessNote + { + get; + private set + { + if (SetField(ref field, value)) + { + OnPropertyChanged(nameof(HasProcessNote)); + } + } + } = ""; + + /// 有没有要说的(没在跑 / 读失败 / 还没读)。 + public bool HasProcessNote => ProcessNote.Length > 0; + + /// 刷新进程表。 + public RelayCommand RefreshProcessesCommand { get; } + + /// 关掉抽屉。 + public RelayCommand CloseCommand { get; } + + /// + /// 钉住抽屉:列表刷新、切页、点别的行都不关它。 + /// + /// 用得着的场合很具体 —— 一边看着这个容器的日志,一边在列表里找别的东西。 + /// + /// + public bool Pinned + { + get; + private set => SetField(ref field, value); + } + + /// + /// 抽屉最大化(占满整个页签)。 + /// + /// 状态存在**页面**上:抽屉的宽度与最大化是外层那张 Grid 的布局, + /// 而换一个容器就是换一个抽屉视图模型 —— 存在这里会一点行就丢。 + /// + /// + public bool Maximized + { + get => Owner.Drawer.Maximized; + private set => Owner.Drawer.Maximized = value; + } + + /// + /// 抽屉所在的页面。 + /// + /// 界面要绑页面上的布局状态(最大化与否)。绕道 不行 —— + /// 通知是页面发的,而这个视图模型不会替它转发一遍。 + /// + /// + public ContainersPageViewModel Owner { get; } + + /// 切换钉住。 + public RelayCommand TogglePinCommand => field ??= new(_ => + { + Pinned = !Pinned; + return Task.CompletedTask; + }); + + /// 切换最大化。 + public RelayCommand ToggleMaximizeCommand => field ??= new(_ => + { + Maximized = !Maximized; + return Task.CompletedTask; + }); + + /// 复制等价的 docker run 命令(与右键菜单同一条路)。 + public RelayCommand CopyRunCommand => field ??= new(_ => + { + Owner.RowCopyRunCommand.Execute(_row); + return Task.CompletedTask; + }); + + /// 强杀。 + public RelayCommand KillCommand => field ??= new(_ => + { + Owner.KillCommand.Execute(_row); + return Task.CompletedTask; + }); + + /// 把当前可写层提交成一个镜像。 + public RelayCommand CommitCommand { get; } + + /// + /// 提交为镜像。 + /// + /// 不走确认闸门:它不删除也不覆盖任何东西,只是多出一个镜像。 + /// 真正要提醒的是"这个镜像没有 Dockerfile",而那句话在表单的脚注里说更合适 —— + /// 把闸门用在不会丢东西的操作上,只会让用户学会无视闸门。 + /// + /// + private async Task CommitAsync() + { + if (_shell.Client is not { } client) + { + return; + } + var form = new CommitContainerForm(Name, _row.Image); + if (!await _shell.ShowFormAsync(form).ConfigureAwait(true)) + { + return; + } + try + { + var result = await client.CommitContainerAsync(ContainerId, form.Repository, form.Tag, + form.Comment, author: null, form.Pause, _shell.Lifetime).ConfigureAwait(true); + _shell.Feedback.Notify(FeedbackKind.Success, "已提交为镜像", + $"{form.Repository}:{form.Tag} · {Humanize.ShortId(result.Id)}"); + await _shell.Images.RefreshAsync(_shell.Lifetime).ConfigureAwait(true); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + _shell.Feedback.ReportError("提交为镜像", ex); + } + } + + /// 日志页。 + public LogsViewModel Logs { get; } + + /// 文件页。 + public ContainerFilesViewModel Files { get; } + + /// 终端页。 + public ContainerTerminalViewModel Terminal { get; } + + /// 切页签。 + public RelayCommand SetTabCommand { get; } + + /// 切换原文视图。 + public RelayCommand ToggleRawCommand { get; } + + /// 复制容器 id。 + public RelayCommand CopyIdCommand { get; } + + /// 启动。 + public RelayCommand StartCommand { get; } + + /// 停止。 + public RelayCommand StopCommand { get; } + + /// 重启。 + public RelayCommand RestartCommand { get; } + + /// 暂停 / 恢复。 + public RelayCommand PauseCommand { get; } + + /// 删除。 + public RelayCommand RemoveCommand { get; } + + /// 重命名。 + public RelayCommand RenameCommand { get; } + + /// 改重启策略。 + public RelayCommand RestartPolicyCommand { get; } + + /// 加载详情。 + public async Task LoadAsync(CancellationToken cancellationToken) + { + if (_shell.Client is not { } client) + { + return; + } + _inspect = await client.InspectContainerAsync(ContainerId, cancellationToken).ConfigureAwait(true); + RawInspect = await client.InspectContainerRawAsync(ContainerId, cancellationToken).ConfigureAwait(true); + IsTty = RawInspect.Contains("\"Tty\": true", StringComparison.Ordinal); + BuildOverview(); + if (IsRunning) + { + StartStatsStream(); + } + } + + /// 切到某个页签(右键菜单与命令面板从外面调)。 + public Task GoToTabAsync(DetailTab tab) => SetTabAsync(tab); + + /// 打开文件页并直接落到某个目录。 + public async Task OpenFilesAtAsync(string path) + { + Tab = DetailTab.Files; + await Files.GoToAsync(path).ConfigureAwait(true); + } + + /// 列表刷新后同步这一行的新状态。 + public void ApplySummary(ContainerRow row) + { + _row = row; + OnPropertiesChanged(nameof(Name), nameof(Status), nameof(Tone), nameof(IsRunning), nameof(IsPaused), + nameof(PauseLabel), nameof(ComposePath), nameof(HasCompose)); + if (!IsRunning) + { + StopStatsStream(); + CpuText = "—"; + MemText = "—"; + CpuPercent = 0; + MemRatio = 0; + } + else if (_statsCts is null) + { + StartStatsStream(); + } + } + + private void BuildOverview() + { + Basics.Clear(); + PortLines.Clear(); + Mounts.Clear(); + Networks.Clear(); + Environment.Clear(); + if (_inspect is not { } inspect) + { + return; + } + Basics.Add(new("镜像", inspect.Config?.Image ?? _row.Image)); + Basics.Add(new("镜像摘要", Humanize.ShortId(inspect.Image))); + Basics.Add(new("容器 ID", Humanize.ShortId(inspect.Id))); + Basics.Add(new("创建于", Humanize.LocalTime(inspect.Created))); + Basics.Add(new("启动于", Humanize.LocalTime(inspect.State?.StartedAt))); + if (inspect.State is { Running: false, FinishedAt: { } finished }) + { + Basics.Add(new("结束于", Humanize.LocalTime(finished), + inspect.State.ExitCode == 0 ? RowTone.Idle : RowTone.Danger)); + Basics.Add(new("退出码", inspect.State.ExitCode.ToString(), + inspect.State.ExitCode == 0 ? RowTone.Idle : RowTone.Danger)); + } + Basics.Add(new("重启策略", inspect.HostConfig?.RestartPolicy?.Name is { Length: > 0 } policy ? policy : "no")); + Basics.Add(new("重启次数", inspect.RestartCount.ToString())); + Basics.Add(new("平台", inspect.Platform ?? "—")); + if (inspect.Config?.Cmd is { Length: > 0 } cmd) + { + Basics.Add(new("命令", string.Join(' ', cmd))); + } + if (inspect.HostConfig?.Privileged == true) + { + Basics.Add(new("特权模式", "是 —— 容器可以做几乎任何宿主能做的事", RowTone.Danger)); + } + + foreach (var port in _row.Summary.Ports ?? []) + { + PortLines.Add(port.PublicPort > 0 + ? new($"{port.PrivatePort}/{port.Type ?? "tcp"}", + $"{(string.IsNullOrEmpty(port.IP) ? "0.0.0.0" : port.IP)}:{port.PublicPort} → {port.PrivatePort}", + RowTone.Ok) + : new($"{port.PrivatePort}/{port.Type ?? "tcp"}", "仅容器内可见(未发布)")); + } + + foreach (var mount in inspect.Mounts ?? []) + { + var source = mount.Type == "volume" ? mount.Name ?? mount.Source ?? "" : mount.Source ?? ""; + Mounts.Add(new( + mount.Type == "volume" ? "Docker.database" : "Icon.folder", + $"{source} → {mount.Destination}", + mount.RW ? "读写" : "只读", + !mount.RW)); + } + + foreach ((var name, var endpoint) in inspect.NetworkSettings?.Networks ?? []) + { + Networks.Add(new(name, endpoint.IPAddress is { Length: > 0 } ip ? ip : "(未分配)", RowTone.Ok)); + } + + foreach (var entry in inspect.Config?.Env ?? []) + { + var equals = entry.IndexOf('='); + Environment.Add(equals > 0 + ? new(entry[..equals], entry[(equals + 1)..]) + : new(entry, "")); + } + + HealthText = inspect.State?.Health is { Status: { Length: > 0 } status } health + ? health.FailingStreak > 0 ? $"{status} · 连续失败 {health.FailingStreak} 次" : status + : ""; + OnPropertiesChanged(nameof(HealthText), nameof(HasHealth), nameof(IsTty)); + } + + private async Task SetTabAsync(DetailTab tab) + { + Tab = tab; + // 文件页是设计稿里的整屏三栏(文件树 / 编辑器 / 属性),440px 的抽屉装不下。 + // 但"直接铺满整个页签"太狠:列表整个没了,回去的路只剩头上那颗还原键。 + // 撑到够摆开三栏就停,列表还在旁边,手柄也还在,用户随时能往回拖。 + if (tab is DetailTab.Files) + { + Owner.Drawer.EnsureAtLeast(820); + } + switch (tab) + { + case DetailTab.Logs: + await Logs.EnsureStartedAsync().ConfigureAwait(true); + break; + case DetailTab.Files: + await Files.EnsureLoadedAsync().ConfigureAwait(true); + break; + case DetailTab.Terminal: + await Terminal.EnsureStartedAsync().ConfigureAwait(true); + break; + case DetailTab.Stats: + await RefreshProcessesAsync(_shell.Lifetime).ConfigureAwait(true); + break; + } + } + + /// + /// 读一次容器里的进程表。 + /// + /// 不做轮询:这张表的价值是"这一刻里面到底跑着什么",而不是看它跳动 —— + /// 后者是 top 的活,该在终端页里干。 + /// + /// + private async Task RefreshProcessesAsync(CancellationToken cancellationToken) + { + if (!IsRunning) + { + Processes.Clear(); + ProcessNote = "容器没在跑,没有进程可看。"; + return; + } + if (_shell.Client is not { } client) + { + return; + } + try + { + var top = await client.TopAsync(ContainerId, cancellationToken: cancellationToken) + .ConfigureAwait(true); + Processes.Clear(); + foreach (var row in ProcessTable.Normalize(top)) + { + Processes.Add(row); + } + ProcessCpuTitle = ProcessTable.CpuColumnTitle(top); + ProcessNote = Processes.Count == 0 ? "这一刻没有读到进程。" : ""; + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Processes.Clear(); + // 用的是精简镜像(distroless、scratch)时容器里没有 ps,daemon 会直接报错 —— + // 这不是面板的故障,得说清楚。 + ProcessNote = $"读不到进程表:{ex.Message}"; + } + } + + /// + /// 抽屉里那一个容器才配拥有一条真正的 stats 流 —— + /// 列表走的是低频快照(见 )。 + /// + private void StartStatsStream() + { + if (_shell.Client is not { } client) + { + return; + } + StopStatsStream(); + _statsCts = CancellationTokenSource.CreateLinkedTokenSource(_shell.Lifetime); + var token = _statsCts.Token; + var id = ContainerId; + _ = Task.Run(async () => + { + try + { + await client.StreamStatsAsync(id, sample => Ui.Post(() => ApplyStats(sample)), token).ConfigureAwait(false); + } + catch (Exception) + { + // 容器停了、面板关了都会走到这里,都不是要报给用户的错误。 + } + }, token); + } + + private void StopStatsStream() + { + _statsCts?.Cancel(); + _statsCts?.Dispose(); + _statsCts = null; + } + + private void ApplyStats(ContainerStats stats) + { + CpuPercent = stats.CpuPercent; + CpuText = Humanize.Percent(stats.CpuPercent); + MemText = Humanize.Bytes(stats.MemoryUsed); + MemLimitText = stats.MemoryLimit > 0 ? $"/ {Humanize.Bytes(stats.MemoryLimit)}" : ""; + MemRatio = stats.MemoryLimit > 0 ? Math.Clamp((double)stats.MemoryUsed / stats.MemoryLimit, 0, 1) : 0; + MemRatioText = stats.MemoryLimit > 0 ? Humanize.Percent(MemRatio * 100) : ""; + Append(CpuHistory, stats.CpuPercent); + Append(MemHistory, MemRatio * 100); + CpuPeakText = CpuHistory.Count > 0 ? $"峰值 {Humanize.Percent(CpuHistory.Max())}" : ""; + } + + private static void Append(ObservableCollection series, double value) + { + series.Add(value); + while (series.Count > 40) + { + series.RemoveAt(0); + } + } + + private async Task RenameAsync() + { + if (_shell.Client is not { } client) + { + return; + } + var form = new RenameContainerForm(Name, _row.Project); + if (!await _shell.ShowFormAsync(form).ConfigureAwait(true)) + { + return; + } + try + { + await client.RenameContainerAsync(ContainerId, form.NewName.Trim(), _shell.Lifetime).ConfigureAwait(true); + _shell.Feedback.Status(FeedbackKind.Success, $"已重命名为 {form.NewName.Trim()}"); + await Owner.RefreshAsync(_shell.Lifetime).ConfigureAwait(true); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + _shell.Feedback.ReportError("重命名", ex); + } + } + + private async Task ChangeRestartPolicyAsync() + { + if (_shell.Client is not { } client) + { + return; + } + var form = new RestartPolicyForm(_inspect?.HostConfig?.RestartPolicy?.Name ?? "no", + _inspect?.HostConfig?.RestartPolicy?.MaximumRetryCount ?? 5); + if (!await _shell.ShowFormAsync(form).ConfigureAwait(true)) + { + return; + } + try + { + await client.UpdateRestartPolicyAsync(ContainerId, form.Policy, form.MaxRetries, _shell.Lifetime) + .ConfigureAwait(true); + _shell.Feedback.Status(FeedbackKind.Success, $"重启策略已改为 {form.Policy}"); + await LoadAsync(_shell.Lifetime).ConfigureAwait(true); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + _shell.Feedback.ReportError("修改重启策略", ex); + } + } + + /// + public async ValueTask DisposeAsync() + { + StopStatsStream(); + await Logs.DisposeAsync().ConfigureAwait(false); + await Terminal.DisposeAsync().ConfigureAwait(false); + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerFilesView.axaml b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerFilesView.axaml new file mode 100644 index 0000000..683194d --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerFilesView.axaml @@ -0,0 +1,382 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerFilesView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerFilesView.axaml.cs new file mode 100644 index 0000000..a5dbc17 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerFilesView.axaml.cs @@ -0,0 +1,83 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.Platform.Storage; + +namespace VelaShell.Plugin.DockerPanel.Ui.Pages; + +/// +/// 容器文件页的视图。 +/// +/// 代码后置做两件视图模型做不了的事:把编辑器的光标位置报给状态条, +/// 以及接住拖进来的文件 —— 拖放的事件与数据格式只有控件层看得到。 +/// +/// +public sealed partial class ContainerFilesView : UserControl +{ + /// 建视图。 + public ContainerFilesView() + { + AvaloniaXamlLoader.Load(this); + AddHandler(DragDrop.DragOverEvent, OnDragOver); + AddHandler(DragDrop.DropEvent, OnDrop); + } + + /// + protected override void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + if (this.FindControl("Editor") is { } editor) + { + // 光标位置要在**每次选区变化**时报,不只是敲字时 —— 点一下也会移动光标。 + editor.PropertyChanged += (_, args) => + { + if (args.Property == TextBox.CaretIndexProperty + && DataContext is ContainerFilesViewModel viewModel) + { + ReportCaret(viewModel, editor); + } + }; + } + } + + private static void ReportCaret(ContainerFilesViewModel viewModel, TextBox editor) + { + var text = editor.Text ?? ""; + var caret = Math.Clamp(editor.CaretIndex, 0, text.Length); + var line = 1; + var lineStart = 0; + for (var i = 0; i < caret; i++) + { + if (text[i] == '\n') + { + line++; + lineStart = i + 1; + } + } + viewModel.ReportCaret(line, caret - lineStart + 1); + } + + private void OnDragOver(object? sender, DragEventArgs e) + { + var acceptable = DataContext is ContainerFilesViewModel { CanPickFiles: true } + && e.DataTransfer.TryGetFiles()?.Length > 0; + e.DragEffects = acceptable ? DragDropEffects.Copy : DragDropEffects.None; + e.Handled = true; + } + + private void OnDrop(object? sender, DragEventArgs e) + { + if (DataContext is not ContainerFilesViewModel viewModel) + { + return; + } + // 只接第一个:一次拖十个文件进容器,失败到一半留下的半套文件比什么都不做更糟。 + var file = e.DataTransfer.TryGetFiles()?.OfType().FirstOrDefault(); + if (file is not null) + { + _ = viewModel.UploadDroppedAsync(file); + } + e.Handled = true; + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerFilesViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerFilesViewModel.cs new file mode 100644 index 0000000..b65cd1b --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerFilesViewModel.cs @@ -0,0 +1,1013 @@ +using Avalonia.Platform.Storage; +using System.Collections.ObjectModel; +using System.Text; +using VelaShell.Plugin.DockerPanel.Docker; + +namespace VelaShell.Plugin.DockerPanel.Ui.Pages; + +/// 编辑器 / 差异视图里的一行。 +/// 显示的行号。 +/// 差异标记(+ / - / ~;没变为空)。 +/// 行内容。 +/// 语气(界面按它给标记与底色上色)。 +public readonly record struct EditorLine(string Number, string Marker, string Text, RowTone Tone); + +/// 本面板往这个容器里写过的一次。 +/// 什么时候。 +/// 写了哪个文件。 +/// 改动摘要,如 +2 −1新建。 +public readonly record struct FileWriteRecord(DateTimeOffset At, string Path, string Summary) +{ + /// 时间的显示文本。 + public string TimeText => At.ToLocalTime().ToString( + At.ToLocalTime().Date == DateTime.Today ? "HH:mm:ss" : "MM-dd HH:mm"); + + /// 文件名(路径太长,历史列表里只放名字)。 + public string Name => Path[(Path.LastIndexOf('/') + 1)..]; +} + +/// 文件树里的一项。 +public sealed class FileEntryItem(ContainerFileEntry entry, string changeMarker) : ObservableObject +{ + /// 底层条目。 + public ContainerFileEntry Entry { get; } = entry; + + /// 名字。 + public string Name => Entry.Name; + + /// 绝对路径。 + public string FullPath => Entry.FullPath; + + /// 是否目录。 + public bool IsDirectory => Entry.IsDirectory; + + /// 图标资源键。 + public string Icon => Entry.IsDirectory ? "Icon.folder" : Entry.IsSymlink ? "Icon.link-2" : "Icon.file-text"; + + /// 大小文本。 + public string SizeText => Entry.IsDirectory ? "" : Humanize.Bytes(Entry.Size); + + /// 权限。 + public string Mode => Entry.Mode; + + /// 修改时间。 + public string Modified => Entry.Modified; + + /// 符号链接的目标。 + public string LinkText => Entry.LinkTarget is { Length: > 0 } target ? $"→ {target}" : ""; + + /// 是不是软链接(决定要不要画出目标)。 + public bool IsSymlink => Entry.IsSymlink; + + /// + /// 悬停时那行详情:权限、大小、修改时间。 + /// + /// 这三样 ls 早就解析好了,却一直没有任何地方显示 —— + /// 树里一行只有 24px,塞不下三列;挂在提示上是它们唯一放得下的位置。 + /// + /// + public string Details => string.Join(" ", new[] { Mode, SizeText, Modified }.Where(s => s.Length > 0)); + + /// 相对镜像的变更标记(A/C/D);没变过为空。 + public string ChangeMarker { get; } = changeMarker; + + /// 有没有变更标记。 + public bool HasChange => ChangeMarker.Length > 0; + + /// 「只看变更」过滤后是否显示。 + public bool Visible + { + get; + set => SetField(ref field, value); + } = true; + + /// 变更标记的语气。 + public RowTone ChangeTone => ChangeMarker switch + { + "A" => RowTone.Ok, + "D" => RowTone.Danger, + "C" => RowTone.Warn, + _ => RowTone.Idle + }; + + // ── 树 ──────────────────────────────────────────────────────── + + /// 在树里的层级。根("/")是 0。 + public int Depth { get; init; } + + /// 缩进像素。每层 13px —— 与折叠箭头同宽,竖着看是一条对齐的线。 + public double Indent => Depth * 13; + + /// 只有目录才有折叠箭头。文件那一格留空,名字才不会左右跳。 + public bool HasCaret => IsDirectory; + + /// 展开了没有。 + public bool Expanded + { + get; + set + { + if (SetField(ref field, value)) + { + OnPropertyChanged(nameof(CaretIcon)); + } + } + } + + /// 折叠箭头的朝向。 + public string CaretIcon => Expanded ? "Icon.chevron-down" : "Icon.chevron-right"; + + /// 子节点。目录第一次展开时才去列 —— 一上来递归整棵树会把隧道占满。 + public List Children { get; } = []; + + /// 子节点列过没有。空目录也算列过,否则每次点都会再发一次 exec。 + public bool ChildrenLoaded { get; set; } + + /// 这一行是不是编辑器里正打开的那个文件。 + public bool Current + { + get; + set => SetField(ref field, value); + } +} + +/// +/// 容器内文件浏览与在线编辑。 +/// +/// 列目录走 exec 跑 ls(Engine API 没有列目录这个端点,而 /archive +/// 只能整包取走);读写单个文件走 /archive 的 tar 流 —— 不经 shell, +/// 因此不受登录 shell、引用规则与 locale 的影响。 +/// +/// +public sealed class ContainerFilesViewModel(DockerPanelViewModel shell, string containerId, string containerName) + : ObservableObject +{ + private readonly Dictionary _changes = []; + private readonly List _history = []; + private bool _loaded; + private string _originalText = ""; + private ContainerFileEntry? _openEntry; + + /// 只看相对镜像有变更的那些条目。 + public bool ChangedOnly + { + get; + set + { + if (SetField(ref field, value)) + { + ApplyEntryView(); + } + } + } + + /// + /// 差异视图(编辑 / 差异两态切换)。 + /// + /// 比的是**这次编辑前后**,不是相对镜像 —— 后者由左边那个 A/C/D 标记表达。 + /// 用户按下保存前想确认的是"我刚改了什么",而不是"这个文件相对镜像改过什么"。 + /// + /// + public bool DiffMode + { + get; + set + { + if (SetField(ref field, value)) + { + OnPropertiesChanged(nameof(EditMode)); + RebuildEditorLines(); + } + } + } + + /// 在编辑态(与 互斥)。 + public bool EditMode => !DiffMode; + + /// 保存后顺带在容器里跑一次重载命令。 + public bool ReloadAfterSave + { + get; + set => SetField(ref field, value); + } + + /// 差异视图里的行。 + public ObservableCollection DiffLines { get; } = []; + + /// 本面板往这个容器里写过的记录(最近在前)。 + public ObservableCollection History { get; } = []; + + /// 有写入历史。 + public bool HasHistory => History.Count > 0; + + /// 打开的那个文件的属性(大小 / 权限 / 属主 / 修改时间 / 相对镜像)。 + public ObservableCollection FileProperties { get; } = []; + + /// 光标位置文本。 + public string CaretText + { + get; + private set => SetField(ref field, value); + } = "行 1, 列 1"; + + /// 已改了几行。 + public string ModifiedLinesText + { + get + { + if (!IsModified) + { + return "未修改"; + } + var changed = LineDiff.CountChanged(LineDiff.Compute(_originalText, EditorText)); + return $"已修改 {changed} 行"; + } + } + + /// 换行符(照原文件的,保存时不改它)。 + public string LineEnding => _originalText.Contains("\r\n", StringComparison.Ordinal) ? "CRLF" : "LF"; + + /// 按扩展名猜的语言(只用来在状态条上显示)。 + public string Language => GuessLanguage(OpenFileName); + + /// + /// 这个文件多半需要的重载命令;猜不出来时为空。 + /// + /// 只覆盖最常改的那几类配置。猜不到就不显示那个勾选框 —— + /// 给一个会失败的默认命令,比不给更糟。 + /// + /// + public string ReloadCommand => OpenFilePath is not { } path ? "" : path switch + { + var p when p.Contains("/nginx/", StringComparison.Ordinal) => "nginx -s reload", + var p when p.Contains("/postgresql/", StringComparison.Ordinal) => "pg_ctl reload", + var p when p.Contains("/redis", StringComparison.Ordinal) => "redis-cli CONFIG SET appendonly yes", + _ => "" + }; + + /// 有没有可用的重载命令。 + public bool HasReloadCommand => ReloadCommand.Length > 0; + + private static string GuessLanguage(string name) + { + var dot = name.LastIndexOf('.'); + var ext = dot > 0 ? name[(dot + 1)..].ToLowerInvariant() : ""; + return ext switch + { + "conf" or "cnf" or "ini" => name.Contains("nginx", StringComparison.OrdinalIgnoreCase) ? "nginx" : "ini", + "yml" or "yaml" => "yaml", + "json" => "json", + "sh" or "bash" => "shell", + "toml" => "toml", + "xml" => "xml", + "env" => "dotenv", + "" => name.StartsWith('.') ? "dotfile" : "text", + _ => ext + }; + } + + /// + /// 当前目录 —— 树上最后一个被展开或被打开文件所在的那一个。 + /// 上传落在这里,所以它必须跟着树走,而不是自己另记一份。 + /// + public string Path + { + get; + private set => SetField(ref field, value); + } = "/"; + + /// 正在读。 + public bool Busy + { + get; + private set => SetField(ref field, value); + } + + /// 出错信息。 + public string Error + { + get; + private set + { + if (SetField(ref field, value)) + { + OnPropertyChanged(nameof(HasError)); + } + } + } = ""; + + /// 有没有出错。 + public bool HasError => Error.Length > 0; + + /// 相对镜像变更了几项。 + public int ChangeCount => _changes.Count; + + /// 变更计数文本。 + // diff 读失败时不能说"与镜像一致"—— 那是在**断言一件没验证过的事**, + // 而这一页的读者正要据此决定改哪个文件。读不到就说读不到。 + public string ChangeText => _diffFailed + ? "读不到相对镜像的变更(A/C/D 标记这一次不可用)" + : _changes.Count == 0 + ? "与镜像一致" + : $"相对镜像已变更 {_changes.Count} 项"; + + private bool _diffFailed; + + /// 正在编辑的文件路径;没打开时为 + public string? OpenFilePath + { + get; + private set + { + if (SetField(ref field, value)) + { + OnPropertiesChanged(nameof(HasOpenFile), nameof(OpenFileName), nameof(OpenFileDirectory)); + MarkCurrentFile(value); + } + } + } + + /// 有文件开着。 + public bool HasOpenFile => OpenFilePath is not null; + + /// 打开的文件名。 + public string OpenFileName => OpenFilePath is { } p ? p[(p.LastIndexOf('/') + 1)..] : ""; + + /// 打开的文件所在目录。 + public string OpenFileDirectory => OpenFilePath is { } p && p.LastIndexOf('/') > 0 ? p[..(p.LastIndexOf('/') + 1)] : "/"; + + /// 编辑器内容。 + public string EditorText + { + get; + set + { + if (SetField(ref field, value)) + { + OnPropertiesChanged(nameof(IsModified), nameof(ModifiedText), nameof(ModifiedLinesText)); + RebuildEditorLines(); + } + } + } = ""; + + /// 改过了没有。 + public bool IsModified => EditorText != _originalText; + + /// 改动摘要。 + public string ModifiedText + { + get + { + if (!IsModified) + { + return "未修改"; + } + var before = _originalText.Split('\n').Length; + var after = EditorText.Split('\n').Length; + var delta = after - before; + return delta switch + { + > 0 => $"未保存 · +{delta} 行", + < 0 => $"未保存 · {delta} 行", + _ => "未保存" + }; + } + } + + /// 打开一个目录或文件。 + public RelayCommand OpenCommand => field ??= new(p => p is FileEntryItem item + ? item.IsDirectory ? ToggleAsync(item) : OpenFileAsync(item.FullPath) + : Task.CompletedTask); + + /// 重新列一次(整棵树丢掉重建,并重新取一次相对镜像的变更)。 + public RelayCommand RefreshCommand => field ??= new(_ => ReloadTreeAsync()); + + /// 关掉编辑器。 + public RelayCommand CloseFileCommand => field ??= new(_ => + { + OpenFilePath = null; + EditorText = ""; + _originalText = ""; + }); + + /// 撤销未保存的修改。 + public RelayCommand RevertCommand => field ??= new(_ => EditorText = _originalText); + + /// 保存回容器。 + public RelayCommand SaveCommand => field ??= new(_ => SaveAsync()); + + /// 把一个文件或目录取到本地。 + public RelayCommand DownloadCommand => field ??= new(p => p is FileEntryItem item + ? DownloadAsync(item) + : Task.CompletedTask); + + /// 把本地一个文件传进当前目录。 + public RelayCommand UploadCommand => field ??= new(_ => UploadAsync()); + + /// 能不能弹本地文件对话框(宿主没给顶层窗口时不能)。 + public bool CanPickFiles => FilePicker.IsAvailable; + + /// + /// 取一份到本地。 + /// + /// 目录只能是 tar —— /archive 交出来的就是一个 tar 流,面板不替用户解包: + /// 一个几万文件的目录在这里展开,失败到一半留下的半个目录树比什么都不做更糟。 + /// 单个文件则从 tar 里取出来还原成原样,因为用户要的是那个文件,不是一个装着它的盒子。 + /// + /// + private async Task DownloadAsync(FileEntryItem item) + { + if (shell.Client is not { } client) + { + return; + } + var suggested = item.IsDirectory ? $"{item.Name}.tar" : item.Name; + var target = await FilePicker + .PickSaveAsync(item.IsDirectory ? $"把 {item.Name}/ 存成 tar" : $"保存 {item.Name}", + suggested, item.IsDirectory ? "tar" : null) + .ConfigureAwait(true); + if (target is null) + { + return; + } + Busy = true; + Error = ""; + try + { + await using var archive = await client.DownloadArchiveAsync(containerId, item.FullPath, shell.Lifetime) + .ConfigureAwait(true); + await using var output = await target.OpenWriteAsync().ConfigureAwait(true); + long written; + if (item.IsDirectory) + { + await archive.CopyToAsync(output, shell.Lifetime).ConfigureAwait(true); + written = output.CanSeek ? output.Length : 0; + } + else + { + // 单文件走流式解 tar:整份读进内存的话,一个 2 GB 的日志会把宿主拖垮。 + written = await TarUtil.ExtractFirstFileAsync(archive, output, shell.Lifetime).ConfigureAwait(true); + } + shell.Feedback.Notify(FeedbackKind.Success, "已取到本地", + $"{target.Name}{(written > 0 ? $" · {Humanize.Bytes(written)}" : "")}"); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Error = ex is DockerApiException api ? api.Message : ex.Message; + shell.Feedback.ReportError("下载文件", ex); + } + finally + { + Busy = false; + } + } + + /// 切到编辑态。 + public RelayCommand ShowEditCommand => field ??= new(_ => + { + DiffMode = false; + return Task.CompletedTask; + }); + + /// 切到差异态。 + public RelayCommand ShowDiffCommand => field ??= new(_ => + { + DiffMode = true; + return Task.CompletedTask; + }); + + /// 编辑器报一次光标位置(视图在选区变化时调)。 + public void ReportCaret(int line, int column) => CaretText = $"行 {line}, 列 {column}"; + + /// 按当前文本重算差异行。 + private void RebuildEditorLines() + { + DiffLines.Clear(); + if (!DiffMode) + { + return; + } + foreach (var line in LineDiff.Compute(_originalText, EditorText)) + { + (var marker, var tone) = line.Marker switch + { + DiffMarker.Added => ("+", RowTone.Ok), + DiffMarker.Removed => ("−", RowTone.Danger), + DiffMarker.Changed => ("~", RowTone.Warn), + _ => (" ", RowTone.Idle) + }; + // 删除的行在新文里没有行号,显示原文的那个 —— 空着会让人对不上原文件。 + var number = line.Marker == DiffMarker.Removed ? line.OldNumber : line.NewNumber; + DiffLines.Add(new(number > 0 ? number.ToString() : "", marker, line.Text, tone)); + } + } + + private void BuildFileProperties() + { + FileProperties.Clear(); + if (_openEntry is not { } entry) + { + return; + } + FileProperties.Add(new("大小", Humanize.Bytes(entry.Size))); + FileProperties.Add(new("权限", entry.Mode)); + FileProperties.Add(new("属主", entry.Owner)); + FileProperties.Add(new("修改时间", entry.Modified)); + var marker = _changes.GetValueOrDefault(entry.FullPath, ""); + FileProperties.Add(marker switch + { + "A" => new("相对镜像", "新增 (A)", RowTone.Ok), + "C" => new("相对镜像", "已修改 (C)", RowTone.Warn), + "D" => new("相对镜像", "已删除 (D)", RowTone.Danger), + _ => new("相对镜像", "与镜像一致") + }); + } + + private void Record(string path, string summary) + { + // 只留在内存里:写入历史是"这次会话里我动过什么"的备忘, + // 落盘会把它变成一份需要清理、需要考虑隐私的东西,而它不值那个代价。 + _history.Insert(0, new(DateTimeOffset.UtcNow, path, summary)); + while (_history.Count > 20) + { + _history.RemoveAt(_history.Count - 1); + } + History.Clear(); + foreach (var record in _history) + { + History.Add(record); + } + OnPropertyChanged(nameof(HasHistory)); + } + + /// 这次改动的摘要,进写入历史(+2 −1 这种)。 + private string DescribeEdit() + { + var diff = LineDiff.Compute(_originalText, EditorText); + var added = diff.Count(l => l.Marker is DiffMarker.Added or DiffMarker.Changed); + var removed = diff.Count(l => l.Marker is DiffMarker.Removed or DiffMarker.Changed); + return _originalText.Length == 0 ? "新建" + : added == 0 && removed == 0 ? "无改动" + : $"+{added} −{removed}"; + } + + /// + /// 保存后跑一次重载命令。 + /// + /// 失败**不**回滚文件 —— 文件已经写进去了,那是既成事实; + /// 假装"保存失败"会让用户以为可以重来一次,而实际上容器里的内容已经变了。 + /// + /// + private async Task RunReloadAsync(DockerClient client) + { + var command = ReloadCommand; + try + { + var result = await client + .ExecCaptureAsync(containerId, ["/bin/sh", "-c", command], cancellationToken: shell.Lifetime) + .ConfigureAwait(true); + if (result.ExitCode == 0) + { + shell.Feedback.Notify(FeedbackKind.Success, "已重载配置", $"{containerName}:{command}"); + return; + } + shell.Feedback.Notify(FeedbackKind.Warning, "文件已写回,但重载失败", + $"{command} 退出码 {result.ExitCode}\n{result.StandardError.Trim()}"); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + shell.Feedback.Notify(FeedbackKind.Warning, "文件已写回,但重载没跑成", $"{command}:{ex.Message}"); + } + } + + private void ApplyEntryView() => RebuildTree(); + + // ── 文件树 ──────────────────────────────────────────────────── + // + // 设计稿 04 号板的左栏是一棵**常驻**的树,不是"打开文件就消失的目录列表": + // 改配置这件事本来就是在几个相邻文件之间来回跳, + // 每跳一次都要先关掉编辑器再重新走一遍目录,是这一页最费手的地方。 + + /// 左栏那棵树:已展开的节点摊平成一列(界面绑这个)。 + public ObservableCollection Tree { get; } = []; + + /// 左栏标题:哪个容器的文件系统。 + public string TreeTitle => $"{containerName} 文件系统"; + + private FileEntryItem? _root; + + /// 展开 / 收起一个目录,并把它记成"当前目录"(上传落到这里)。 + private async Task ToggleAsync(FileEntryItem item) + { + if (!item.IsDirectory) + { + return; + } + if (item.Expanded) + { + item.Expanded = false; + } + else + { + await ExpandAsync(item).ConfigureAwait(true); + } + Path = item.FullPath; + RebuildTree(); + } + + /// 展开一个目录。第一次展开才去列 —— 一上来递归整棵树会把隧道占满。 + private async Task ExpandAsync(FileEntryItem item) + { + if (item.ChildrenLoaded) + { + item.Expanded = true; + return; + } + if (shell.Client is not { } client) + { + return; + } + Busy = true; + Error = ""; + try + { + var entries = await client + .ListDirectoryAsync(containerId, item.FullPath, shell.Lifetime).ConfigureAwait(true); + item.Children.Clear(); + // 目录在前、再按名字排:树里最常做的动作是"往下钻", + // 目录混在文件中间会让每一层都要重新找一遍。 + foreach (var entry in entries + .OrderByDescending(e => e.IsDirectory) + .ThenBy(e => e.Name, StringComparer.Ordinal)) + { + item.Children.Add(new(entry, _changes.GetValueOrDefault(entry.FullPath, "")) + { + Depth = item.Depth + 1 + }); + } + item.ChildrenLoaded = true; + item.Expanded = true; + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Error = ex is DockerApiException api ? api.Message : ex.Message; + } + finally + { + Busy = false; + } + } + + /// 把展开着的部分摊平成一列。 + private void RebuildTree() + { + Tree.Clear(); + if (_root is null) + { + return; + } + Append(_root); + return; + + void Append(FileEntryItem node) + { + // 目录永远留着 —— 藏掉目录会让"只看变更"变成一个走不进任何子目录的死胡同。 + if (!ChangedOnly || node.IsDirectory || node.HasChange) + { + Tree.Add(node); + } + if (!node.Expanded) + { + return; + } + foreach (var child in node.Children) + { + Append(child); + } + } + } + + /// 建根节点并展开一层。 + private async Task BuildTreeAsync() + { + _root = new(new("/", "/", true, false, 0, "", "", "", null), ""); + await ExpandAsync(_root).ConfigureAwait(true); + RebuildTree(); + } + + /// 把树里指向某个路径的那一行标成"正在编辑"。 + private void MarkCurrentFile(string? path) + { + foreach (var node in Tree) + { + node.Current = node.FullPath == path; + } + } + + /// 在**已展开过**的那部分树里按路径找一个节点。 + private FileEntryItem? FindNode(string path) + { + if (_root is null) + { + return null; + } + var node = _root; + foreach (var segment in path.Split('/', StringSplitOptions.RemoveEmptyEntries)) + { + if (node.Children.FirstOrDefault(c => c.Name == segment) is not { } child) + { + return null; + } + node = child; + } + return node; + } + + /// + /// 一路展开到某个路径。从抽屉的「挂载」那一节点进来时用 —— + /// 用户点的是一个挂载点,期待的是"树已经停在那里",而不是"从 / 自己找过去"。 + /// + private async Task ExpandToAsync(string path) + { + if (_root is null) + { + return; + } + var node = _root; + await ExpandAsync(node).ConfigureAwait(true); + foreach (var segment in path.Split('/', StringSplitOptions.RemoveEmptyEntries)) + { + if (node.Children.FirstOrDefault(c => c.Name == segment) is not { } child) + { + break; + } + node = child; + if (node.IsDirectory) + { + await ExpandAsync(node).ConfigureAwait(true); + } + } + if (node.IsDirectory) + { + Path = node.FullPath; + } + RebuildTree(); + } + + /// 把整棵树丢掉重来。变更标记也重新取一次 —— 它决定了 A/C/D 和「只看变更」。 + private async Task ReloadTreeAsync() + { + await LoadChangesAsync().ConfigureAwait(true); + var previous = Path; + _root = null; + await BuildTreeAsync().ConfigureAwait(true); + if (previous != "/") + { + await ExpandToAsync(previous).ConfigureAwait(true); + } + } + + /// 把本地一个文件送进容器当前目录。 + /// 接住拖进来的那个文件,当成上传到当前目录。 + public Task UploadDroppedAsync(IStorageFile file) => UploadFileAsync(file); + + private async Task UploadAsync() + { + var source = await FilePicker.PickOpenAsync($"上传到 {Path}").ConfigureAwait(true); + if (source is not null) + { + await UploadFileAsync(source).ConfigureAwait(true); + } + } + + private async Task UploadFileAsync(IStorageFile source) + { + if (shell.Client is not { } client) + { + return; + } + byte[] content; + await using (var input = await source.OpenReadAsync().ConfigureAwait(true)) + { + if (input.CanSeek && input.Length > DockerClient.MaxUploadFileBytes) + { + Error = $"{source.Name} 有 {Humanize.Bytes(input.Length)},超过单文件上传上限 " + + $"{Humanize.Bytes(DockerClient.MaxUploadFileBytes)}。大文件请用 scp 之类的通道。"; + return; + } + using var buffer = new MemoryStream(); + await input.CopyToAsync(buffer, shell.Lifetime).ConfigureAwait(true); + content = buffer.ToArray(); + } + var target = Path.TrimEnd('/') + "/" + source.Name; + var exists = FindNode(target) is not null; + var confirmed = await shell.Confirm.AskAsync(shell.BuildConfirm(new() + { + Title = exists ? $"覆盖容器内的 {source.Name}?" : $"上传 {source.Name} 到 {containerName}?", + Icon = exists ? "Docker.shield-alert" : "Icon.upload", + // 覆盖已有文件是不可撤销的,要手打确认串;新建一个文件不是,点一下就够。 + Tier = exists ? ConfirmTier.DataLoss : ConfirmTier.Destructive, + ConfirmWord = "overwrite", + ConfirmLabel = exists ? "覆盖" : "上传", + ConfirmIcon = "Icon.upload", + HostName = "", + Commands = [$"docker cp {source.Name} {containerName}:{target}"], + CommandNote = $"{Humanize.Bytes(content.Length)},以 tar 流写入容器可写层,不经过 shell。", + DataLossHeadline = exists ? "容器里同名的那个文件会被整体覆盖" : "", + DataLossPoints = exists + ? + [ + "Docker 不做备份,也没有回收站。", + "改动只在容器的可写层里:容器一旦被删除或重建,它就没了。" + ] + : [] + })).ConfigureAwait(true); + if (!confirmed) + { + return; + } + Busy = true; + Error = ""; + try + { + await client.WriteFileAsync(containerId, target, content, shell.Lifetime).ConfigureAwait(true); + shell.Feedback.Notify(FeedbackKind.Success, "已上传", $"{target} · {Humanize.Bytes(content.Length)}"); + await ReloadTreeAsync().ConfigureAwait(true); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Error = ex is DockerApiException api ? api.Message : ex.Message; + shell.Feedback.ReportError("上传文件", ex); + } + finally + { + Busy = false; + } + } + + /// + /// 直接跳到某个目录(卷页"浏览卷内文件"用:它已经知道卷挂在容器的哪儿了, + /// 让用户从 / 一级一级点下去是白费一遍功夫)。 + /// + public async Task GoToAsync(string path) + { + await EnsureLoadedAsync().ConfigureAwait(true); + await ExpandToAsync(path).ConfigureAwait(true); + } + + /// 第一次进这一页时才加载。 + public async Task EnsureLoadedAsync() + { + if (_loaded) + { + return; + } + _loaded = true; + await LoadChangesAsync().ConfigureAwait(true); + await BuildTreeAsync().ConfigureAwait(true); + } + + private async Task LoadChangesAsync() + { + if (shell.Client is not { } client) + { + return; + } + try + { + var changes = await client.ChangesAsync(containerId, shell.Lifetime).ConfigureAwait(true); + _changes.Clear(); + foreach (var change in changes) + { + _changes[change.Path] = change.Marker; + } + _diffFailed = false; + OnPropertiesChanged(nameof(ChangeCount), nameof(ChangeText)); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + // 拿不到 diff 不该挡住文件浏览 —— 标记只是锦上添花。 + // 但那一行小字得改口:不能继续说"与镜像一致"。 + _diffFailed = true; + OnPropertyChanged(nameof(ChangeText)); + shell.Context.Log.Debug($"docker diff failed: {ex.Message}"); + } + } + + private async Task OpenFileAsync(string path) + { + if (shell.Client is not { } client) + { + return; + } + Busy = true; + Error = ""; + try + { + var bytes = await client.ReadFileAsync(containerId, path, DockerClient.MaxEditableFileBytes, shell.Lifetime) + .ConfigureAwait(true); + if (LooksBinary(bytes)) + { + Error = $"{path} 看起来是二进制文件 —— 面板只在线编辑文本。"; + return; + } + _originalText = Encoding.UTF8.GetString(bytes); + EditorText = _originalText; + OpenFilePath = path; + // 从树里取元数据,而不是从"当前目录的列表"里 —— + // 树上点开的文件所在目录,未必是 Path 指着的那一个。 + _openEntry = FindNode(path)?.Entry; + if (path.LastIndexOf('/') > 0) + { + Path = path[..path.LastIndexOf('/')]; + } + DiffMode = false; + BuildFileProperties(); + OnPropertiesChanged(nameof(ModifiedLinesText), nameof(LineEnding), nameof(Language), + nameof(ReloadCommand), nameof(HasReloadCommand)); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Error = ex is DockerApiException api ? api.Message : ex.Message; + } + finally + { + Busy = false; + } + } + + /// + /// 二进制探测:前 8 KB 里出现 NUL 就当二进制。 + /// + /// 把一个 ELF 塞进文本编辑器,用户点一次保存就把它毁了 —— + /// 而"毁了"这件事要等到容器下次启动失败才会被发现。 + /// + /// + private static bool LooksBinary(ReadOnlySpan bytes) + { + var limit = Math.Min(bytes.Length, 8192); + for (var i = 0; i < limit; i++) + { + if (bytes[i] == 0) + { + return true; + } + } + return false; + } + + private async Task SaveAsync() + { + if (shell.Client is not { } client || OpenFilePath is not { } path || !IsModified) + { + return; + } + var confirmed = await shell.Confirm.AskAsync(shell.BuildConfirm(new() + { + Title = $"覆盖容器内的 {OpenFileName}?", + Icon = "Docker.shield-alert", + Tier = ConfirmTier.DataLoss, + ConfirmWord = "save", + ConfirmLabel = "写回容器", + ConfirmIcon = "Icon.save", + HostName = "", + Commands = [$"PUT /containers/{containerName}/archive?path={OpenFileDirectory}"], + CommandNote = "整个文件以 tar 流写回容器可写层,不经过 shell。", + DataLossHeadline = "原文件会被整体覆盖,无法撤销", + DataLossPoints = + [ + "Docker 不做备份,也没有回收站 —— 写回去就是写回去了。", + "改动只在容器的可写层里:容器一旦被删除或重建,它就没了。", + "多数服务不会自动重载配置,保存后多半还要 reload 或重启容器。" + ] + })).ConfigureAwait(true); + if (!confirmed) + { + return; + } + Busy = true; + try + { + await client.WriteFileAsync(containerId, path, Encoding.UTF8.GetBytes(EditorText), shell.Lifetime) + .ConfigureAwait(true); + Record(path, DescribeEdit()); + _originalText = EditorText; + OnPropertiesChanged(nameof(IsModified), nameof(ModifiedText), nameof(ModifiedLinesText)); + RebuildEditorLines(); + shell.Feedback.Notify(FeedbackKind.Success, "已写回容器", $"{path} · {Humanize.Bytes(Encoding.UTF8.GetByteCount(EditorText))}"); + await LoadChangesAsync().ConfigureAwait(true); + if (ReloadAfterSave && HasReloadCommand) + { + await RunReloadAsync(client).ConfigureAwait(true); + } + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + shell.Feedback.ReportError("写回文件", ex); + } + finally + { + Busy = false; + } + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerTerminalView.axaml b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerTerminalView.axaml new file mode 100644 index 0000000..8c434cd --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerTerminalView.axaml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerTerminalView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerTerminalView.axaml.cs new file mode 100644 index 0000000..f257faf --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerTerminalView.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace VelaShell.Plugin.DockerPanel.Ui.Pages; + +/// +/// 容器终端的视图。 +/// +/// 代码后置是空的:键盘、选区、IME、鼠标上报全归宿主那个终端控件管 —— +/// 早先这里有一段"回车执行、上下键翻历史"的逻辑,那是行式控制台的需要; +/// 真终端里这三个键属于远端的 shell,面板拦下来反而是错的。 +/// +/// +public sealed partial class ContainerTerminalView : UserControl +{ + /// 建视图。 + public ContainerTerminalView() => AvaloniaXamlLoader.Load(this); +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerTerminalViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerTerminalViewModel.cs new file mode 100644 index 0000000..27f0bc4 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerTerminalViewModel.cs @@ -0,0 +1,419 @@ +using System.Text; +using VelaShell.Plugin.DockerPanel.Docker; +using VelaShell.PluginSdk; +using VelaShell.PluginSdk.TerminalView; + +namespace VelaShell.Plugin.DockerPanel.Ui.Pages; + +/// +/// 容器内的**真终端**(exec + TTY)。 +/// +/// 它借的是宿主的终端仿真器(SDK 1.3 的 ):VT 解析、 +/// 屏幕缓冲、回滚、选区、IME、鼠标上报、键盘编码全都是宿主那一套 —— 与用户在 SSH 标签里 +/// 用的是同一个控件、同一份字体与配色。面板这一层只做三件事:开 exec、把流接上去、 +/// 把尺寸变化告诉远端。 +/// +/// +/// 早先这里是一个"一条命令一份输出"的行式控制台。它对 cat 与改配置够用, +/// 但 topvimless 要的是一个会重绘的屏幕 —— 而重写一个 ANSI +/// 解析器是几周的活,宿主里那一个已经跑了很久了。 +/// +/// +public sealed class ContainerTerminalViewModel(DockerPanelViewModel shell, string containerId, string containerName) + : ObservableObject, IAsyncDisposable +{ + /// 依次尝试的 shell:多数镜像有 bash,精简镜像只有 sh。 + private static readonly string[] ShellCandidates = ["/bin/bash", "/bin/sh", "/bin/ash"]; + + private IPluginTerminalView? _view; + private DockerExecSession? _session; + private CancellationTokenSource? _sessionCts; + private bool _starting; + + /// 终端控件(交给视图去承载;宿主给的是 Avalonia Control)。 + public object? TerminalControl => _view?.Control; + + /// 用哪个 shell。 + public string Shell + { + get; + set + { + if (SetField(ref field, value)) + { + OnPropertiesChanged(nameof(HeaderText), nameof(ShellName)); + } + } + } = "/bin/bash"; + + /// 工作目录;留空用镜像默认。 + public string WorkingDir + { + get; + set + { + if (SetField(ref field, value)) + { + OnPropertyChanged(nameof(HeaderText)); + } + } + } = ""; + + /// 以哪个用户执行;留空用镜像默认。 + public string User + { + get; + set + { + if (SetField(ref field, value)) + { + OnPropertyChanged(nameof(HeaderText)); + } + } + } = ""; + + /// 会话是否连着。 + public bool Connected + { + get; + private set + { + if (SetField(ref field, value)) + { + OnPropertiesChanged(nameof(Disconnected), nameof(ConnectionTone)); + } + } + } + + /// 会话断了。 + public bool Disconnected => !Connected; + + /// 连接状态的语气(界面据此给状态点上色)。 + public RowTone ConnectionTone => Connected ? RowTone.Ok : RowTone.Idle; + + /// 状态短语。 + public string Status + { + get; + private set => SetField(ref field, value); + } = "未连接"; + + /// 头部那行小字。 + public string HeaderText + { + get + { + var sb = new StringBuilder(containerName); + sb.Append(" · ").Append(Shell); + if (User.Length > 0) + { + sb.Append(" · ").Append(User); + } + if (WorkingDir.Length > 0) + { + sb.Append(" · ").Append(WorkingDir); + } + return sb.ToString(); + } + } + + /// 终端尺寸文字。 + public string SizeText => _view is { } view ? $"{view.Columns} × {view.Rows}" : "—"; + + /// 状态条上显示的 shell 名(去掉路径,只留 bash / sh / ash)。 + public string ShellName => Shell[(Shell.LastIndexOf('/') + 1)..]; + + /// exec 实例 id 的短形态(排查时对得上 daemon 的日志)。 + public string ExecIdText => _session is { } session ? Humanize.ShortId(session.ExecId) : "—"; + + /// 清屏。 + public RelayCommand ClearCommand => field ??= new(_ => + { + _view?.Clear(); + return Task.CompletedTask; + }); + + /// 把屏幕上的文本复制走。 + public RelayCommand CopyCommand => field ??= new(_ => + _view is { } view + ? shell.Context.Clipboard.SetTextAsync(view.GetText(2000), shell.Lifetime) + : Task.CompletedTask); + + /// 结束这个 exec 会话。 + public RelayCommand DisconnectCommand => field ??= new(_ => StopSessionAsync("会话已结束")); + + /// 重开一个会话(换 shell / 换用户之后用)。 + public RelayCommand ReconnectCommand => field ??= new(_ => RestartAsync()); + + /// + /// 换用户 / 换 shell,然后重开会话。 + /// + /// exec 的用户与 shell 是**开会话时**定死的,改不了活着的那一个 —— + /// 所以这里改完就直接重连,而不是让用户自己再想起来按一次刷新。 + /// + /// + public RelayCommand SwitchUserCommand => field ??= new(_ => SwitchUserAsync()); + + /// 换工作目录,然后重开会话。 + public RelayCommand SwitchWorkingDirCommand => field ??= new(_ => SwitchWorkingDirAsync()); + + private async Task SwitchUserAsync() + { + var form = new ExecUserForm(User, Shell); + if (!await shell.ShowFormAsync(form).ConfigureAwait(true)) + { + return; + } + User = form.User; + Shell = form.Shell; + await RestartAsync().ConfigureAwait(true); + } + + private async Task SwitchWorkingDirAsync() + { + var form = new ExecWorkingDirForm(WorkingDir); + if (!await shell.ShowFormAsync(form).ConfigureAwait(true)) + { + return; + } + WorkingDir = form.WorkingDir; + await RestartAsync().ConfigureAwait(true); + } + + /// + /// 在宿主那个 SSH 终端里开一个会话。 + /// + /// 面板里这一个已经是真终端了,但它活在面板的生命周期里 —— + /// 想让会话跟着 SSH 标签一起留着,还是宿主的终端更合适。 + /// + /// + public RelayCommand OpenInHostTerminalCommand => field ??= new(_ => OpenInHostTerminalAsync()); + + /// 能不能跳到宿主终端(本机端点没有 SSH 会话可跳)。 + public bool CanOpenInHostTerminal => shell.SelectedEndpoint?.IsLocal == false; + + /// 第一次进这一页时才建终端并连上。 + public async Task EnsureStartedAsync() + { + if (_view is not null || _starting) + { + return; + } + _starting = true; + try + { + if (!shell.Context.TerminalView.IsAvailable) + { + // 清单钉了 hostMode: inProcess,理论上到不了这里;真到了要说人话, + // 而不是让用户对着一块空白发呆。 + Status = "这个宿主不提供终端视图(需要 hostMode: inProcess)"; + return; + } + _view = shell.Context.TerminalView.Create(new() + { + ScrollbackLines = 5000, + FollowHostAppearance = true + }); + _view.Resized += OnResized; + OnPropertiesChanged(nameof(TerminalControl), nameof(SizeText)); + await StartSessionAsync().ConfigureAwait(true); + } + finally + { + _starting = false; + } + } + + private async Task StartSessionAsync() + { + if (_view is not { } view || shell.Client is not { } client) + { + return; + } + Status = "正在建立 exec 会话…"; + try + { + var resolved = await ResolveShellAsync(client).ConfigureAwait(true); + Shell = resolved; + var session = await client.StartExecAsync(containerId, [resolved], tty: true, + string.IsNullOrWhiteSpace(User) ? null : User, + string.IsNullOrWhiteSpace(WorkingDir) ? null : WorkingDir, + shell.Lifetime).ConfigureAwait(true); + _session = session; + _sessionCts = CancellationTokenSource.CreateLinkedTokenSource(shell.Lifetime); + Connected = true; + Status = "已连接"; + OnPropertiesChanged(nameof(ExecIdText), nameof(HeaderText)); + // 先把当前尺寸报过去:远端默认 80×24,而控件多半不是这个大小。 + await ResizeRemoteAsync(view.Columns, view.Rows).ConfigureAwait(true); + _ = PumpAsync(view, session, _sessionCts.Token); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Connected = false; + Status = $"建立失败:{ex.Message}"; + view.Write($"\u001b[31m无法在 {containerName} 里启动 {Shell}:{ex.Message}\u001b[0m\r\n"); + // daemon 的原文(多半是一句 "no such file or directory")指不出下一步该做什么。 + // 建不起来的原因就那么三种,直说 —— 对着一句原文没人猜得到该换什么。 + var warn = (char)0x1b + "[33m"; + var reset = (char)0x1b + "[0m"; + view.Write($"{warn}常见原因:容器没在跑;镜像里根本没有 shell(distroless 一类);" + + $"或者指定的用户在容器里不存在。{reset}\r\n"); + view.Write($"{warn}工具条上的「切换用户」可以换 shell 与身份,换完会自动重连。{reset}\r\n"); + } + } + + /// + /// 挑一个容器里真的存在的 shell。 + /// + /// 直接开 /bin/bash 在 alpine 上会失败,而失败信息是 daemon 的 + /// "no such file or directory" —— 对着它没人猜得到该换 /bin/sh。 + /// 所以先探一次,探不动就退回 /bin/sh。 + /// + /// + private async Task ResolveShellAsync(DockerClient client) + { + // 用户手动指定过就不猜了。 + if (Shell.Length > 0 && !ShellCandidates.Contains(Shell)) + { + return Shell; + } + foreach (var candidate in ShellCandidates) + { + try + { + var probe = await client + .ExecCaptureAsync(containerId, ["/bin/sh", "-c", $"command -v {candidate} >/dev/null"], + cancellationToken: shell.Lifetime) + .ConfigureAwait(true); + if (probe.ExitCode == 0) + { + return candidate; + } + } + catch (Exception) + { + // 探测本身失败(容器里连 /bin/sh 都没有,或者刚好停了)—— + // 不在这里报错,让真正的 StartExec 去报,那条消息更准。 + break; + } + } + return "/bin/sh"; + } + + private async Task PumpAsync(IPluginTerminalView view, DockerExecSession session, CancellationToken token) + { + try + { + // TTY 模式下 daemon 两个方向都走裸字节,可以整条流直接交给终端。 + await view.AttachAsync(session.Stream, token).ConfigureAwait(true); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Ui.Post(() => Status = $"会话中断:{ex.Message}"); + } + finally + { + Ui.Post(() => + { + if (Connected) + { + Connected = false; + Status = "会话已结束"; + view.Write("\u001b[90m\r\n[会话已结束 —— 按「重新连接」再开一个]\u001b[0m\r\n"); + } + }); + } + } + + private void OnResized(int columns, int rows) + { + OnPropertyChanged(nameof(SizeText)); + _ = ResizeRemoteAsync(columns, rows); + } + + /// + /// 把终端尺寸告诉远端。不报的话 vim 会照 80×24 画,画出来是错位的。 + /// + private async Task ResizeRemoteAsync(int columns, int rows) + { + if (_session is not { } session || !Connected || columns <= 0 || rows <= 0) + { + return; + } + try + { + await session.ResizeAsync(rows, columns, shell.Lifetime).ConfigureAwait(true); + } + catch (Exception) + { + // 会话正在结束时 resize 必然失败,而那不是一件要告诉用户的事。 + } + } + + private async Task RestartAsync() + { + await StopSessionAsync(null).ConfigureAwait(true); + _view?.Clear(); + await StartSessionAsync().ConfigureAwait(true); + } + + private async Task StopSessionAsync(string? status) + { + if (_sessionCts is { } cts) + { + await cts.CancelAsync().ConfigureAwait(true); + cts.Dispose(); + _sessionCts = null; + } + if (_session is { } session) + { + await session.DisposeAsync().ConfigureAwait(true); + _session = null; + } + Connected = false; + if (status is not null) + { + Status = status; + } + OnPropertyChanged(nameof(ExecIdText)); + } + + private async Task OpenInHostTerminalAsync() + { + if (shell.SelectedEndpoint?.Endpoint is not { Kind: DockerEndpointKind.Remote } endpoint) + { + return; + } + var command = $"docker exec -it {Sh.Quote(containerName)} {Sh.Quote(Shell)}"; + try + { + // 回写走宿主的输入队列("如同用户键入"),并且需要用户授权 —— + // 面板不直写 SSH 流,这条边界不为方便让路。 + await shell.Context.Terminal.WriteAsync(endpoint.SessionId, command + "\n", shell.Lifetime) + .ConfigureAwait(true); + shell.Feedback.Notify(FeedbackKind.Info, "已送到宿主终端", command); + } + catch (PluginPermissionDeniedException) + { + shell.Feedback.Notify(FeedbackKind.Warning, "没有向终端回写的授权", + $"可以自己在那条会话里执行:{command}"); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + shell.Feedback.ReportError("送往宿主终端", ex); + } + } + + /// + public async ValueTask DisposeAsync() + { + await StopSessionAsync(null).ConfigureAwait(false); + if (_view is { } view) + { + view.Resized -= OnResized; + view.Dispose(); + _view = null; + } + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainersPageView.axaml b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainersPageView.axaml new file mode 100644 index 0000000..ecdd8be --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainersPageView.axaml @@ -0,0 +1,709 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainersPageView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainersPageView.axaml.cs new file mode 100644 index 0000000..5367b05 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainersPageView.axaml.cs @@ -0,0 +1,47 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Markup.Xaml; +using Avalonia.VisualTree; + +namespace VelaShell.Plugin.DockerPanel.Ui.Pages; + +/// ContainersPageView 的视图。 +public sealed partial class ContainersPageView : UserControl +{ + // 列宽拖拽与抽屉布局都是四个页面共用的通用件,这里只交代这一页的两个数: + // 列宽之外的固定占位(状态色条 3 + 勾选框 26 + 行尾动作 92),以及抽屉要给列表留多宽。 + private readonly ColumnResizer _columns; + private readonly DrawerLayout _drawer; + + /// 建视图。 + public ContainersPageView() + { + AvaloniaXamlLoader.Load(this); + _columns = new(this, "HeaderGrid", chrome: 3 + 26 + 92 + 8); + _drawer = new(this, "Root", "DrawerSplitter"); + } + + /// + /// 点整行 = 打开详情抽屉。 + /// + /// 设计稿的行尾只有三颗动作按钮,没有单独的"详情"按钮 —— 行本身就是那颗按钮。 + /// 但行里还坐着勾选框和三颗动作按钮,它们的 Tapped 会一路冒泡到这里; + /// 所以先看事件是不是从某个按钮里出来的,是就让开。 + /// + /// + private void OnRowTapped(object? sender, TappedEventArgs e) + { + if (e.Source is Visual source && + (source.FindAncestorOfType + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ImageDetailView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ImageDetailView.axaml.cs new file mode 100644 index 0000000..e23ac80 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ImageDetailView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace VelaShell.Plugin.DockerPanel.Ui.Pages; + +/// ImageDetailView 的视图。 +public sealed partial class ImageDetailView : UserControl +{ + /// 建视图。 + public ImageDetailView() => AvaloniaXamlLoader.Load(this); +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ImageDetailViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ImageDetailViewModel.cs new file mode 100644 index 0000000..f9f1539 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ImageDetailViewModel.cs @@ -0,0 +1,360 @@ +using System.Collections.ObjectModel; +using VelaShell.Plugin.DockerPanel.Docker; + +namespace VelaShell.Plugin.DockerPanel.Ui.Pages; + +/// 镜像详情抽屉的页签。 +public enum ImageDetailTab +{ + /// 概览。 + Overview, + + /// 层历史。 + History, + + /// 原文。 + Raw +} + +/// 构建历史里的一层。 +/// 产生这一层的指令(已剥掉 /bin/sh -c #(nop) 这类噪声)。 +/// 这一层带来的体积。 +/// 距今多久。 +/// 空层(只改元数据,不占体积)。 +/// 相对最大层的宽度比 0–1,用来画条。 +/// 条右边剩下的那一段(1 - Weight)—— 要两段才分得了宽度。 +/// 层已经不在本地(基于其它镜像共享,daemon 只留了元数据)。 +public readonly record struct LayerRow( + string Instruction, + string SizeText, + string AgeText, + bool IsEmpty, + double Weight, + double Rest, + bool Missing); + +/// +/// 镜像详情抽屉。 +/// +/// 它回答的是运维在删镜像之前会问的三个问题:这东西是什么时候、怎么来的; +/// 它跑起来默认执行什么;以及那 1.2 GB 到底胖在哪一层。 +/// 前两个靠 inspect,第三个只有 history 能答。 +/// +/// +public sealed class ImageDetailViewModel : ObservableObject +{ + private readonly DockerPanelViewModel _shell; + private ImageRow _row; + private bool _historyLoaded; + + /// 建镜像详情。 + public ImageDetailViewModel(DockerPanelViewModel shell, ImagesPageViewModel page, ImageRow row) + { + _shell = shell; + Owner = page; + _row = row; + SetTabCommand = new RelayCommand(p => p is ImageDetailTab tab ? SetTabAsync(tab) : Task.CompletedTask); + CopyIdCommand = new RelayCommand(_ => shell.Context.Clipboard.SetTextAsync(row.Id, shell.Lifetime)); + RunCommand = new RelayCommand(_ => _shell.ShowRunContainerAsync(PrimaryReference)); + TagCommand = new RelayCommand(_ => Owner.TagCommand.Execute(_row)); + PushCommand = new RelayCommand(_ => Owner.PushCommand.Execute(_row)); + RemoveCommand = new RelayCommand(_ => Owner.RemoveCommand.Execute(_row)); + CloseCommand = new RelayCommand(_ => Owner.CloseDetailCommand.Execute(null)); + } + + /// 镜像 id。 + public string ImageId => _row.Id; + + /// 短 id。 + public string ShortId => _row.ShortId; + + /// 标题(第一个标签,悬空镜像用短 id)。 + public string Title => _row.IsDangling ? $" · {ShortId}" : $"{_row.Repository}:{_row.Tag}"; + + /// + /// 抽屉所在的页面。 + /// 抽屉的宽度与铺没铺满是**页面**的布局状态,界面要绑得到它。 + /// + public ImagesPageViewModel Owner { get; } + + /// 拿去 run 的引用:有标签用标签,悬空镜像只能用 id。 + public string PrimaryReference => + _row.Summary.RepoTags?.FirstOrDefault(t => t != ":") ?? _row.Id; + + /// 大小。 + public string SizeText => _row.SizeText; + + /// 被多少容器用着。 + public string UsageText => _row.UsageText; + + /// 当前页签。 + public ImageDetailTab Tab + { + get; private set + { + if (SetField(ref field, value)) + { + OnPropertiesChanged(nameof(IsOverview), nameof(IsHistory), nameof(IsRaw)); + } + } + } = ImageDetailTab.Overview; + + /// 在概览页。 + public bool IsOverview => Tab == ImageDetailTab.Overview; + + /// 在层历史页。 + public bool IsHistory => Tab == ImageDetailTab.History; + + /// 在原文页。 + public bool IsRaw => Tab == ImageDetailTab.Raw; + + /// 基本信息。 + public ObservableCollection Basics { get; } = []; + + /// 全部标签。 + public ObservableCollection Tags { get; } = []; + + /// 摘要引用。 + public ObservableCollection Digests { get; } = []; + + /// 默认执行什么。 + public ObservableCollection Runtime { get; } = []; + + /// 环境变量。 + public ObservableCollection Environment { get; } = []; + + /// 镜像标签(LABEL)。 + public ObservableCollection Labels { get; } = []; + + /// 构建历史。 + public ObservableCollection Layers { get; } = []; + + /// 层历史读不到时的说明。 + public string HistoryNote + { + get; private set + { + if (SetField(ref field, value)) + { + OnPropertyChanged(nameof(HasHistoryNote)); + } + } + } = ""; + + /// 有没有要说的。 + public bool HasHistoryNote => HistoryNote.Length > 0; + + /// inspect 原文。 + public string RawInspect { get; private set => SetField(ref field, value); } = ""; + + /// 切页签。 + public RelayCommand SetTabCommand { get; } + + /// 复制 id。 + public RelayCommand CopyIdCommand { get; } + + /// 用它跑一个容器。 + public RelayCommand RunCommand { get; } + + /// 打标签。 + public RelayCommand TagCommand { get; } + + /// 推送。 + public RelayCommand PushCommand { get; } + + /// 删除。 + public RelayCommand RemoveCommand { get; } + + /// 关掉抽屉。 + public RelayCommand CloseCommand { get; } + + /// 加载详情。 + public async Task LoadAsync(CancellationToken cancellationToken) + { + if (_shell.Client is not { } client) + { + return; + } + var inspect = await client.InspectImageAsync(ImageId, cancellationToken).ConfigureAwait(true); + BuildOverview(inspect); + } + + /// 列表刷新后同步这一行。 + public void ApplyRow(ImageRow row) + { + _row = row; + OnPropertiesChanged(nameof(Title), nameof(SizeText), nameof(UsageText), nameof(PrimaryReference)); + } + + private void BuildOverview(ImageInspect inspect) + { + Basics.Clear(); + Tags.Clear(); + Digests.Clear(); + Runtime.Clear(); + Environment.Clear(); + Labels.Clear(); + + Basics.Add(new("镜像 id", ShortId)); + Basics.Add(new("大小", Humanize.Bytes(inspect.Size > 0 ? inspect.Size : _row.Summary.Size))); + if (_row.Summary.SharedSize > 0) + { + Basics.Add(new("与其它镜像共享", Humanize.Bytes(_row.Summary.SharedSize))); + } + Basics.Add(new("创建", Humanize.AgoFromIso(inspect.Created))); + var platform = string.Join("/", new[] { inspect.Os, inspect.Architecture, inspect.Variant } + .Where(s => !string.IsNullOrWhiteSpace(s))); + if (platform.Length > 0) + { + // 平台不匹配是"镜像拉下来了但跑不起来"最常见的原因,值得单独一行。 + Basics.Add(new("平台", platform)); + } + if (inspect.DockerVersion is { Length: > 0 } dockerVersion) + { + Basics.Add(new("构建于 Docker", dockerVersion)); + } + if (inspect.RootFS?.Layers is { Length: > 0 } layers) + { + Basics.Add(new("层数", layers.Length.ToString())); + } + Basics.Add(new("使用情况", UsageText, _row.Summary.Containers > 0 ? RowTone.Ok : RowTone.Idle)); + + foreach (var tag in inspect.RepoTags ?? _row.Summary.RepoTags ?? []) + { + Tags.Add(tag); + } + foreach (var digest in inspect.RepoDigests ?? _row.Summary.RepoDigests ?? []) + { + Digests.Add(digest); + } + + var config = inspect.Config; + if (config?.Entrypoint is { Length: > 0 } entrypoint) + { + Runtime.Add(new("Entrypoint", string.Join(" ", entrypoint))); + } + if (config?.Cmd is { Length: > 0 } cmd) + { + Runtime.Add(new("Cmd", string.Join(" ", cmd))); + } + if (config?.WorkingDir is { Length: > 0 } workingDir) + { + Runtime.Add(new("工作目录", workingDir)); + } + // 以 root 跑是个安全事实,不该只躺在原文里等人翻。 + Runtime.Add(config?.User is { Length: > 0 } user + ? new("用户", user) + : new("用户", "root(镜像未指定 USER)", RowTone.Warn)); + if (config?.ExposedPorts is { Count: > 0 } exposed) + { + Runtime.Add(new("暴露端口", string.Join(", ", exposed.Keys.OrderBy(k => k, StringComparer.Ordinal)))); + } + + foreach (var entry in config?.Env ?? []) + { + var split = entry.IndexOf('=', StringComparison.Ordinal); + Environment.Add(split > 0 + ? new(entry[..split], entry[(split + 1)..]) + : new(entry, "")); + } + foreach ((var key, var value) in config?.Labels ?? []) + { + Labels.Add(new(key, value)); + } + OnPropertiesChanged(nameof(Title)); + } + + private async Task SetTabAsync(ImageDetailTab tab) + { + Tab = tab; + switch (tab) + { + case ImageDetailTab.History when !_historyLoaded: + await LoadHistoryAsync(_shell.Lifetime).ConfigureAwait(true); + break; + case ImageDetailTab.Raw when RawInspect.Length == 0: + await LoadRawAsync(_shell.Lifetime).ConfigureAwait(true); + break; + } + } + + private async Task LoadHistoryAsync(CancellationToken cancellationToken) + { + if (_shell.Client is not { } client) + { + return; + } + try + { + var history = await client.ImageHistoryAsync(ImageId, cancellationToken) + .ConfigureAwait(true); + Layers.Clear(); + var largest = history.Length > 0 ? history.Max(h => h.Size) : 0; + // daemon 返回的是从新到旧;Dockerfile 是从旧到新读的,按后者排更容易对上。 + foreach (var entry in history.Reverse()) + { + var weight = largest > 0 ? Math.Clamp((double)entry.Size / largest, 0, 1) : 0; + Layers.Add(new( + CleanInstruction(entry.CreatedBy), + entry.Size > 0 ? Humanize.Bytes(entry.Size) : "—", + Humanize.AgoFromUnix(entry.Created), + entry.Size == 0, + weight, + 1 - weight, + entry.Id is "" or "")); + } + _historyLoaded = true; + HistoryNote = Layers.Count == 0 ? "这个镜像没有历史记录。" : ""; + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Layers.Clear(); + HistoryNote = $"读不到构建历史:{ex.Message}"; + } + } + + private async Task LoadRawAsync(CancellationToken cancellationToken) + { + if (_shell.Client is not { } client) + { + return; + } + try + { + RawInspect = await client.InspectImageRawAsync(ImageId, cancellationToken).ConfigureAwait(true); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + RawInspect = ex.Message; + } + } + + /// + /// 把 history 里那句指令收拾干净。 + /// + /// daemon 存的是构建器当时执行的完整命令,前面挂着 /bin/sh -c #(nop) + /// 这样的脚手架 —— 对着一屏这种前缀,真正的 COPY / ENV 反而看不见了。 + /// + /// + public static string CleanInstruction(string? createdBy) + { + var text = (createdBy ?? "").Trim(); + if (text.Length == 0) + { + return "(无记录)"; + } + const string ShellNop = "/bin/sh -c #(nop) "; + const string Shell = "/bin/sh -c "; + if (text.StartsWith(ShellNop, StringComparison.Ordinal)) + { + text = text[ShellNop.Length..]; + } + else if (text.StartsWith(Shell, StringComparison.Ordinal)) + { + // 真正的 RUN 层:补回 RUN,让它和上下那些 COPY/ENV 读起来是同一套语言。 + text = "RUN " + text[Shell.Length..]; + } + // BuildKit 走的是另一套记法,前缀已经是 "RUN /bin/sh -c" 之类,不再动它。 + return text.Trim(); + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ImagesPageView.axaml b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ImagesPageView.axaml new file mode 100644 index 0000000..e19d150 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ImagesPageView.axaml @@ -0,0 +1,325 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ImagesPageView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ImagesPageView.axaml.cs new file mode 100644 index 0000000..0b1a700 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ImagesPageView.axaml.cs @@ -0,0 +1,42 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Markup.Xaml; +using Avalonia.VisualTree; + +namespace VelaShell.Plugin.DockerPanel.Ui.Pages; + +/// ImagesPageView 的视图。 +public sealed partial class ImagesPageView : UserControl +{ + // 列宽拖拽与抽屉布局都是四个页面共用的通用件,这里只交代这一页的一个数: + // 列宽之外的固定占位(勾选框 26 + 行尾动作 120)。 + private readonly ColumnResizer _columns; + private readonly DrawerLayout _drawer; + + /// 建视图。 + public ImagesPageView() + { + AvaloniaXamlLoader.Load(this); + _columns = new(this, "HeaderGrid", chrome: 26 + 120 + 8); + _drawer = new(this, "Root", "DrawerSplitter"); + } + + /// + /// 点整行 = 打开详情抽屉。行尾那几颗动作按钮的 Tapped 会一路冒泡到这里, + /// 所以先看事件是不是从某个按钮里出来的,是就让开。 + /// + private void OnRowTapped(object? sender, TappedEventArgs e) + { + if (e.Source is Visual source && + (source.FindAncestorOfType + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/LogsView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/LogsView.axaml.cs new file mode 100644 index 0000000..e26d201 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/LogsView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace VelaShell.Plugin.DockerPanel.Ui.Pages; + +/// LogsView 的视图。 +public sealed partial class LogsView : UserControl +{ + /// 建视图。 + public LogsView() => AvaloniaXamlLoader.Load(this); +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/LogsViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/LogsViewModel.cs new file mode 100644 index 0000000..ef0908f --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/LogsViewModel.cs @@ -0,0 +1,594 @@ +using Avalonia.Threading; +using System.Collections.ObjectModel; +using System.Text; +using System.Text.RegularExpressions; +using VelaShell.Plugin.DockerPanel.Docker; + +namespace VelaShell.Plugin.DockerPanel.Ui.Pages; + +/// 合并流里的一个来源。 +/// 容器 id。 +/// 容器名(显示在来源列上)。 +/// 容器是否分配了 TTY(决定日志流要不要解多路复用帧)。 +public readonly record struct LogSource(string ContainerId, string Name, bool Tty); + +/// 日志里的一行。 +public sealed class LogLineItem(string timestamp, string source, int sourceIndex, bool isError, string text) +{ + /// 时间戳文本;没开时间戳时为空。 + public string Timestamp { get; } = timestamp; + + /// 来源容器名;单容器时为空。 + public string Source { get; } = source; + + /// + /// 来源在当前来源列表里的序号。界面按它取一个稳定的颜色 —— + /// 合并五条流时,靠颜色分辨来源比逐行读容器名快得多。 + /// + public int SourceIndex { get; } = sourceIndex; + + /// 有没有来源列。 + public bool HasSource => Source.Length > 0; + + /// 来自标准错误。 + public bool IsError { get; } = isError; + + /// 正文。 + public string Text { get; } = text; + + /// 认出来的级别。 + public LogLevel Level { get; } = LogLevels.Detect(text); + + /// 级别文字;认不出来时为空。 + public string LevelLabel => LogLevels.Label(Level); + + /// 有没有级别标记。 + public bool HasLevel => Level != LogLevel.None; + + /// + /// 这一行的语气。标准错误一律按错误算 —— 哪怕正文里没有 ERROR 字样, + /// 它走的是 stderr 这件事本身就是信息。 + /// + public RowTone Tone => IsError ? RowTone.Danger : LogLevels.Tone(Level); + + /// 是不是当前搜索的命中行。 + public bool Matched { get; set; } +} + +/// 来源选择器里的一项。 +public sealed class LogSourceItem(LogSource source, string status, RowTone tone, int index) : ObservableObject +{ + + /// 底层来源。 + public LogSource Source { get; } = source; + + /// 容器名。 + public string Name => Source.Name; + + /// 状态短语(运行中 / 已停止 / 退出 1…)。 + public string Status { get; } = status; + + /// 状态语气。 + public RowTone Tone { get; } = tone; + + /// 颜色序号(与 对齐)。 + public int Index { get; set; } = index; + + /// 过滤后是否显示。 + public bool Visible + { + get; + set => SetField(ref field, value); + } = true; + + /// 选中没有。 + public bool Selected + { + get; + set + { + if (SetField(ref field, value)) + { + SelectionChanged?.Invoke(); + } + } + } + + /// 勾选变了。 + public event Action? SelectionChanged; +} + +/// +/// 日志视图。 +/// +/// 「跟随」是一条真正的 docker logs -f,不是按间隔补拉:新行到达即出现。 +/// 刷屏的容器一秒能吐几千行,所以界面**攒批**更新(每 120ms 交一次), +/// 并把缓冲钉在 512 KB 上 —— 按行截断,半行开头的日志比少几行更难读。 +/// +/// +public sealed partial class LogsViewModel : ObservableObject, IAsyncDisposable +{ + /// 缓冲上限。 + private const int MaxBufferedChars = 512 * 1024; + + /// 界面上最多留多少行。 + private const int MaxLines = 5000; + + private readonly DockerPanelViewModel shell; + private readonly Func ttyAccessor; + private readonly List _pending = []; + private readonly Lock _pendingGate = new(); + private readonly List _sources = []; + private DateTimeOffset _startedAt; + private CancellationTokenSource? _cts; + private DispatcherTimer? _flushTimer; + private Regex? _filter; + private long _bufferedChars; + private bool _started; + + /// 单容器(详情抽屉的日志页签)。 + public LogsViewModel(DockerPanelViewModel shell, string containerId, Func ttyAccessor) + : this(shell, ttyAccessor) + { + _sources.Add(new(containerId, "", ttyAccessor())); + } + + /// 多容器合并流(容器页的日志模式)。 + public LogsViewModel(DockerPanelViewModel shell, IEnumerable sources) + : this(shell, () => false) + { + _sources.AddRange(sources); + } + + private LogsViewModel(DockerPanelViewModel shell, Func ttyAccessor) + { + this.shell = shell; + this.ttyAccessor = ttyAccessor; + } + + /// 界面上的行。 + public ObservableCollection Lines { get; } = []; + + /// 当前接着几条流。 + public int SourceCount => _sources.Count; + + /// 合并了多于一条流(界面据此显示来源列)。 + public bool IsMerged => _sources.Count > 1; + + /// 当前来源的名字(顶部 chips 用)。 + public IReadOnlyList Sources => _sources; + + /// + /// 顶部 chip 上那个 × 该做什么。由拥有来源清单的那一方(容器页)装上。 + /// + /// 不在这里直接改 :左边面板的勾选才是唯一的真相, + /// 两处各改各的,取消一个来源之后左边还亮着,就成了两套互相矛盾的状态。 + /// + /// + public Func? SourceRemover { get; set; } + + /// 能不能从 chip 上摘掉来源(单容器的日志页签不能)。 + public bool CanRemoveSources => SourceRemover is not null; + + /// 从合并流里摘掉一条来源。 + public RelayCommand RemoveSourceCommand => field ??= new(p => + p is LogSource source && SourceRemover is { } remover ? remover(source) : Task.CompletedTask); + + /// 底部那句"几条流、跑了多久"。 + public string StreamSummary + { + get + { + if (_sources.Count == 0) + { + return "没有选中任何来源"; + } + var elapsed = _startedAt == default ? "" : $" · 已运行 {Humanize.Duration(DateTimeOffset.UtcNow - _startedAt)}"; + return $"docker logs{(Follow ? " -f" : "")} · {_sources.Count} 条流{elapsed}"; + } + } + + /// + /// 换一组来源并重开流。 + /// + /// 换来源会清屏:合并流的行序是**到达顺序**,把旧来源的行留着而新来源从头补 tail, + /// 拼出来的时间线是假的。 + /// + /// + public async Task SetSourcesAsync(IEnumerable sources) + { + _sources.Clear(); + _sources.AddRange(sources); + OnPropertiesChanged(nameof(SourceCount), nameof(IsMerged), nameof(Sources), nameof(StreamSummary)); + if (_started) + { + await RestartAsync().ConfigureAwait(true); + } + } + + /// 是否跟随(-f)。 + public bool Follow + { + get; + set + { + if (SetField(ref field, value)) + { + OnPropertyChanged(nameof(FollowLabel)); + _ = RestartAsync(); + } + } + } = true; + + /// 跟随按钮上的字。开着时说"跟随中" —— 这颗按钮本身就是状态灯。 + public string FollowLabel => Follow ? "跟随中" : "跟随"; + + /// 只看标准错误。 + public bool ErrorsOnly + { + get; + set + { + if (SetField(ref field, value)) + { + ApplyFilter(); + } + } + } + + /// 搜索词。 + public string Search + { + get; + set + { + if (SetField(ref field, value)) + { + CompileFilter(); + ApplyFilter(); + } + } + } = ""; + + /// 搜索按正则解释。 + public bool UseRegex + { + get; + set + { + if (SetField(ref field, value)) + { + CompileFilter(); + ApplyFilter(); + } + } + } + + /// 命中数。 + public int MatchCount + { + get; + private set + { + if (SetField(ref field, value)) + { + OnPropertyChanged(nameof(MatchText)); + } + } + } + + /// 命中数文本。 + public string MatchText => Search.Length == 0 ? "" : $"{MatchCount} 处"; + + /// 底部状态条那句话。 + public string StatusText + { + get; + private set => SetField(ref field, value); + } = ""; + + /// 缓冲用量文本。 + public string BufferText => $"缓冲 {Humanize.Bytes(_bufferedChars)} / {Humanize.Bytes(MaxBufferedChars)}(按行截断)"; + + /// 行数文本。 + public string LineCountText => $"{Lines.Count:N0} 行"; + + /// 补多少行历史。 + public string Tail + { + get => shell.Settings.LogTail; + set + { + shell.Settings.LogTail = value; + OnPropertyChanged(); + _ = RestartAsync(); + } + } + + /// 带时间戳。 + public bool Timestamps + { + get => shell.Settings.LogTimestamps; + set + { + shell.Settings.LogTimestamps = value; + OnPropertyChanged(); + _ = RestartAsync(); + } + } + + /// 自动换行。 + public bool Wrap + { + get => shell.Settings.LogWrap; + set + { + shell.Settings.LogWrap = value; + OnPropertyChanged(); + } + } + + /// 切换跟随。 + public RelayCommand ToggleFollowCommand => field ??= new(_ => Follow = !Follow); + + /// 清屏。 + public RelayCommand ClearCommand => field ??= new(_ => + { + Lines.Clear(); + _bufferedChars = 0; + OnPropertiesChanged(nameof(BufferText), nameof(LineCountText)); + }); + + /// 复制全部。 + public RelayCommand CopyCommand => field ??= new(_ => + { + var sb = new StringBuilder(); + foreach (var line in Lines) + { + if (line.Timestamp.Length > 0) + { + sb.Append(line.Timestamp).Append(' '); + } + sb.AppendLine(line.Text); + } + return shell.Context.Clipboard.SetTextAsync(sb.ToString(), shell.Lifetime); + }); + + /// 把当前缓冲里的行存成本地文件。 + public RelayCommand DownloadCommand => field ??= new(_ => DownloadAsync()); + + /// 能不能弹本地文件对话框。 + public bool CanPickFiles => FilePicker.IsAvailable; + + /// + /// 导出当前缓冲。 + /// + /// 导的是**界面上这些行**,不是重新去 daemon 拉一遍 —— 用户按下按钮时看到的那一屏 + /// (含筛选与来源合并的结果)才是他要的东西;重拉会得到一份不一样的、没有来源标记的日志。 + /// + /// + private async Task DownloadAsync() + { + var suggested = _sources.Count == 1 && _sources[0].Name.Length > 0 + ? $"{_sources[0].Name}.log" + : $"docker-logs-{_sources.Count}.log"; + var target = await FilePicker + .PickSaveAsync("保存日志", suggested, "log").ConfigureAwait(true); + if (target is null) + { + return; + } + try + { + await using var output = await target.OpenWriteAsync().ConfigureAwait(true); + await using var writer = new StreamWriter(output, Encoding.UTF8); + foreach (var line in Lines) + { + if (line.Timestamp.Length > 0) + { + await writer.WriteAsync(line.Timestamp + " ").ConfigureAwait(true); + } + if (line.HasSource) + { + await writer.WriteAsync($"[{line.Source}] ").ConfigureAwait(true); + } + await writer.WriteLineAsync(line.Text).ConfigureAwait(true); + } + shell.Feedback.Notify(FeedbackKind.Success, "日志已保存", $"{target.Name} · {Lines.Count:N0} 行"); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + shell.Feedback.ReportError("保存日志", ex); + } + } + + /// 设置补历史的行数。 + public RelayCommand SetTailCommand => field ??= new(p => + { + if (p is string tail) + { + Tail = tail; + } + }); + + /// 第一次进这一页时才起流。 + public async Task EnsureStartedAsync() + { + if (_started) + { + return; + } + _started = true; + await RestartAsync().ConfigureAwait(true); + } + + /// 按当前选项重开一条日志流。 + public async Task RestartAsync() + { + if (!_started || shell.Client is not { } client) + { + return; + } + await StopAsync().ConfigureAwait(true); + Lines.Clear(); + _bufferedChars = 0; + if (_sources.Count == 0) + { + StatusText = "没有选中任何来源"; + OnPropertiesChanged(nameof(BufferText), nameof(LineCountText), nameof(StreamSummary)); + return; + } + _cts = CancellationTokenSource.CreateLinkedTokenSource(shell.Lifetime); + var token = _cts.Token; + _startedAt = DateTimeOffset.UtcNow; + StartFlushTimer(); + StatusText = Follow ? "跟随中 · 新行即时到达" : "已加载历史"; + OnPropertyChanged(nameof(StreamSummary)); + var timestamps = Timestamps; + var tail = Tail; + var live = _sources.Count; + + // 每个来源一条独立的流,全部写进同一个待刷队列 —— + // 合并的顺序就是**到达顺序**,不按时间戳重排:重排要缓冲、要等, + // 而"跟随"这个功能的全部意义就是不等。 + for (var i = 0; i < _sources.Count; i++) + { + var source = _sources[i]; + var index = i; + var tty = _sources.Count == 1 ? ttyAccessor() : source.Tty; + _ = Task.Run(async () => + { + try + { + await client.StreamLogsAsync(source.ContainerId, tty, Follow, tail, timestamps, null, + line => Enqueue(line, timestamps, source, index), token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + return; + } + catch (Exception ex) + { + // 一条流断了不该把整块面板说成失败 —— 另外几条还在跑。 + Ui.Post(() => StatusText = _sources.Count > 1 + ? $"{source.Name} 的日志流断开:{ex.Message}" + : $"日志流断开:{ex.Message}"); + return; + } + Ui.Post(() => + { + if (--live <= 0) + { + StatusText = Follow ? "全部流已结束(容器可能停了)" : "已加载历史"; + } + }); + }, token); + } + } + + private void Enqueue(DockerLogLine line, bool timestamps, LogSource source, int index) + { + var item = new LogLineItem( + timestamps && line.Timestamp is { } stamp ? stamp.ToLocalTime().ToString("HH:mm:ss.fff") : "", + // 只有合并流才显示来源列:单容器时那一列每行都一样,纯属占地方。 + _sources.Count > 1 ? source.Name : "", + index, + line.Kind == DockerStreamKind.StdErr, + line.Text); + lock (_pendingGate) + { + _pending.Add(item); + } + } + + private void StartFlushTimer() + { + _flushTimer ??= new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(120) }; + if (_flushTimer.IsEnabled) + { + return; + } + _flushTimer.Tick += OnFlush; + _flushTimer.Start(); + } + + private void OnFlush(object? sender, EventArgs e) + { + List batch; + lock (_pendingGate) + { + if (_pending.Count == 0) + { + return; + } + batch = [.. _pending]; + _pending.Clear(); + } + foreach (var item in batch) + { + if (ErrorsOnly && !item.IsError) + { + continue; + } + item.Matched = _filter?.IsMatch(item.Text) ?? false; + Lines.Add(item); + _bufferedChars += item.Text.Length + 1; + } + // 按行截断:从最旧的开始扔,直到缓冲回到上限之内。 + while ((_bufferedChars > MaxBufferedChars || Lines.Count > MaxLines) && Lines.Count > 0) + { + _bufferedChars -= Lines[0].Text.Length + 1; + Lines.RemoveAt(0); + } + MatchCount = _filter is null ? 0 : Lines.Count(l => l.Matched); + OnPropertiesChanged(nameof(BufferText), nameof(LineCountText), nameof(StreamSummary)); + } + + private void CompileFilter() + { + if (Search.Length == 0) + { + _filter = null; + return; + } + try + { + _filter = new(UseRegex ? Search : Regex.Escape(Search), + RegexOptions.IgnoreCase | RegexOptions.Compiled, TimeSpan.FromMilliseconds(100)); + } + catch (ArgumentException) + { + // 正则还没写完就开始匹配是常态,写坏了不该把日志视图弄崩。 + _filter = null; + } + } + + private void ApplyFilter() + { + foreach (var line in Lines) + { + line.Matched = _filter?.IsMatch(line.Text) ?? false; + } + MatchCount = _filter is null ? 0 : Lines.Count(l => l.Matched); + } + + /// 停掉流。 + public async Task StopAsync() + { + if (_flushTimer is { IsEnabled: true } timer) + { + timer.Stop(); + timer.Tick -= OnFlush; + } + if (_cts is null) + { + return; + } + await _cts.CancelAsync().ConfigureAwait(true); + _cts.Dispose(); + _cts = null; + } + + /// + public async ValueTask DisposeAsync() => await StopAsync().ConfigureAwait(false); +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/NetworksPageView.axaml b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/NetworksPageView.axaml new file mode 100644 index 0000000..bf6d504 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/NetworksPageView.axaml @@ -0,0 +1,359 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/NetworksPageView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/NetworksPageView.axaml.cs new file mode 100644 index 0000000..c7d4b40 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/NetworksPageView.axaml.cs @@ -0,0 +1,21 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace VelaShell.Plugin.DockerPanel.Ui.Pages; + +/// NetworksPageView 的视图。 +public sealed partial class NetworksPageView : UserControl +{ + // 列宽拖拽与抽屉布局都是四个页面共用的通用件,这里只交代这一页的一个数: + // 列宽之外的固定占位(名称列左边 12 的留白 + 行尾动作 104)。 + private readonly ColumnResizer _columns; + private readonly DrawerLayout _drawer; + + /// 建视图。 + public NetworksPageView() + { + AvaloniaXamlLoader.Load(this); + _columns = new(this, "HeaderGrid", chrome: 12 + 104 + 8); + _drawer = new(this, "Root", "DrawerSplitter"); + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/NetworksPageViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/NetworksPageViewModel.cs new file mode 100644 index 0000000..6385344 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/NetworksPageViewModel.cs @@ -0,0 +1,737 @@ +using Avalonia.Controls; +using System.Collections.ObjectModel; +using VelaShell.Plugin.DockerPanel.Docker; + +namespace VelaShell.Plugin.DockerPanel.Ui.Pages; + +/// 网络详情里已接入的一个容器。 +/// 容器 id。 +/// 容器名。 +/// IPv4(带掩码)。 +public readonly record struct AttachedContainer(string Id, string Name, string Address); + +/// 网络页。 +public sealed class NetworksPageViewModel : PageViewModel +{ + private readonly List _all = []; + private bool _swarmActive; + + /// 建网络页。 + public NetworksPageViewModel(DockerPanelViewModel shell) : base(shell) + { + SelectCommand = new RelayCommand(p => SelectAsync(p as NetworkRow)); + CreateCommand = new RelayCommand(_ => CreateAsync()); + RemoveCommand = new RelayCommand(p => p is NetworkRow row ? RemoveAsync(row) : Task.CompletedTask); + PruneCommand = new RelayCommand(_ => PruneAsync()); + ConnectCommand = new RelayCommand(p => p is NetworkRow row ? ConnectAsync(row) : Task.CompletedTask); + DisconnectCommand = new RelayCommand(p => p is AttachedContainer attached + ? DisconnectAsync(attached) + : Task.CompletedTask); + RefreshCommand = new RelayCommand(_ => RefreshAsync(Shell.Lifetime)); + } + + /// + public override PanelPage Page => PanelPage.Networks; + + /// + public override string Title => "网络"; + + /// 过滤后的行。 + public KeyedCollection View { get; } = new(r => r.Id); + + /// 搜索词。 + public string Search + { + get; + set + { + if (SetField(ref field, value)) + { + ApplyView(); + } + } + } = ""; + + /// 总数。 + public int TotalCount => _all.Count; + + /// 自定义网络数。 + public int CustomCount => _all.Count(r => !r.IsPredefined); + + /// 内置网络数。 + public int PredefinedCount => _all.Count(r => r.IsPredefined); + + /// + /// 只看自定义网络。 + /// + /// bridge / host / none 这三个内置的删不掉、改不了,却永远占着列表最前面几行; + /// 用户来这一页多半是找自己建的那几个。计数早就算好了(), + /// 一直缺的只是这个开关。 + /// + /// + public bool CustomOnly + { + get; + set + { + if (SetField(ref field, value)) + { + ApplyView(); + } + } + } + + /// 未接入任何容器的自定义网络数。 + public int UnusedCount => _all.Count(r => !r.IsPredefined && r.AttachedCount == 0); + + /// 列表空了。 + public bool IsEmpty => LoadedOnce && _all.Count == 0; + + /// 筛完之后没有匹配项 —— 与"这台机器上没有网络"是两回事,得分开说。 + public bool NoMatch => LoadedOnce && _all.Count > 0 && View.Count == 0; + + /// 当前选中的网络。 + public NetworkRow? Selected + { + get; + private set + { + if (SetField(ref field, value)) + { + Drawer.IsOpen = value is not null; + OnPropertiesChanged(nameof(HasSelection), nameof(CanRemove), nameof(RemoveHint)); + } + } + } + + /// 有选中。 + public bool HasSelection => Selected is not null; + + /// 列表的列宽。列头与数据行共用这一份 —— 拖列头的轨道改的就是它。 + public NetworkColumns Columns { get; } = new(); + + /// + public override ListColumns ColumnLayout => Columns; + + /// + public override IEnumerable ColumnTexts(string key) => key switch + { + "name" => View.Select(r => r.Name), + "driver" => View.Select(r => r.Driver), + "scope" => View.Select(r => r.Scope), + "subnet" => View.Select(r => r.Subnet), + "attached" => View.Select(r => r.AttachedText), + _ => [] + }; + + /// 选中网络的接入容器。 + public ObservableCollection Attached { get; } = []; + + /// 选中网络的 IPAM 明细。 + public ObservableCollection Ipam { get; } = []; + + /// inspect 原文;还没读过时为空。 + public string RawInspect + { + get; + private set + { + if (SetField(ref field, value)) + { + OnPropertyChanged(nameof(HasRaw)); + } + } + } = ""; + + /// 原文读到了没有。 + public bool HasRaw => RawInspect.Length > 0; + + /// 显示 inspect 原文(设计稿 08 号板的「原始 JSON」)。 + public RelayCommand ShowRawCommand => field ??= new(_ => LoadRawAsync()); + + /// 把 inspect 原文存成本地 JSON。 + public RelayCommand ExportCommand => field ??= new(_ => ExportAsync()); + + /// 把所有容器从这个网络上摘掉。 + public RelayCommand DisconnectAllCommand => field ??= new(_ => DisconnectAllAsync()); + + /// 能不能弹本地文件对话框。 + public bool CanPickFiles => FilePicker.IsAvailable; + + private async Task LoadRawAsync() + { + if (Client is not { } client || Selected is not { } row) + { + return; + } + try + { + RawInspect = await client.InspectNetworkRawAsync(row.Id, Shell.Lifetime).ConfigureAwait(true); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + RawInspect = ex.Message; + } + } + + /// + /// 导出网络配置。 + /// + /// 导的是 inspect 原文,不是一份"能直接 create 回去"的清单 —— + /// inspect 里带着运行态(已接入的容器、分配出去的地址),那些东西重建时既不该也无法照搬。 + /// 它的用途是留档与比对,文件头一行会写清这一点。 + /// + /// + private async Task ExportAsync() + { + if (Selected is not { } row) + { + return; + } + if (RawInspect.Length == 0) + { + await LoadRawAsync().ConfigureAwait(true); + } + var target = await FilePicker + .PickSaveAsync($"导出 {row.Name} 的配置", $"{row.Name}.json", "json").ConfigureAwait(true); + if (target is null) + { + return; + } + try + { + await using var output = await target.OpenWriteAsync().ConfigureAwait(true); + await using var writer = new StreamWriter(output); + await writer.WriteLineAsync( + "// docker network inspect 的原文 —— 含运行态(已接入容器与已分配地址),不是可直接重建的清单。") + .ConfigureAwait(true); + await writer.WriteAsync(RawInspect).ConfigureAwait(true); + Shell.Feedback.Notify(FeedbackKind.Success, "网络配置已导出", target.Name); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Shell.Feedback.ReportError("导出网络配置", ex); + } + } + + private async Task DisconnectAllAsync() + { + if (Client is not { } client || Selected is not { } row || Attached.Count == 0) + { + return; + } + AttachedContainer[] targets = [.. Attached]; + var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + { + Title = $"把 {targets.Length} 个容器从 {row.Name} 上摘掉?", + Icon = "Icon.unplug", + HostName = "", + ConfirmLabel = "全部摘除", + ConfirmIcon = "Icon.unplug", + Commands = [.. targets.Select(t => $"POST /networks/{row.ShortId}/disconnect ({t.Name})")], + CommandNote = $"等价于逐个执行 docker network disconnect {row.Name} <容器>", + Targets = [.. targets.Select(t => new ConfirmTarget(t.Name, t.Address, "", false))], + Consequences = + [ + new(3, "容器之间将无法再通过这个网络互相解析与访问 —— 依赖服务名通信的会立刻失败。"), + new(1, "容器本身不受影响,不会停止;重新接入即可恢复。") + ] + })).ConfigureAwait(true); + if (!confirmed) + { + return; + } + foreach (var target in targets) + { + try + { + await client.DisconnectNetworkAsync(row.Id, target.Id, false, Shell.Lifetime).ConfigureAwait(true); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + // 逐个判定:一个摘不掉不该让其余的也不摘。 + Shell.Feedback.Notify(FeedbackKind.Warning, $"{target.Name} 摘不掉", ex.Message); + } + } + Shell.Feedback.Status(FeedbackKind.Success, $"已从 {row.Name} 摘除 {targets.Length} 个容器"); + await RefreshAsync(Shell.Lifetime).ConfigureAwait(true); + } + + /// 选中网络的基本信息。 + public ObservableCollection Basics { get; } = []; + + /// 能不能删。 + public bool CanRemove => Selected is { IsPredefined: false, AttachedCount: 0 }; + + /// 不能删时的原因。 + public string RemoveHint => Selected switch + { + null => "", + { IsPredefined: true } => "Docker 内置的 bridge / host / none 删不掉。", + { AttachedCount: > 0 } row => $"仍有 {row.AttachedCount} 个容器接入,必须先摘除或停止它们。", + _ => "删除网络本身不会丢数据。" + }; + + /// 选中一行。 + public RelayCommand SelectCommand { get; } + + /// 关掉右侧详情。 + public RelayCommand ClearSelectionCommand => field ??= new(_ => SelectAsync(null)); + + /// 新建网络。 + public RelayCommand CreateCommand { get; } + + /// 删除网络。 + public RelayCommand RemoveCommand { get; } + + /// 清理未使用网络。 + public RelayCommand PruneCommand { get; } + + /// 接入容器。 + public RelayCommand ConnectCommand { get; } + + /// 摘除容器。 + public RelayCommand DisconnectCommand { get; } + + /// 刷新。 + public RelayCommand RefreshCommand { get; } + + /// + public override async Task RefreshAsync(CancellationToken cancellationToken) + { + if (Client is not { } client) + { + return; + } + Busy = true; + try + { + var networks = await client.ListNetworksAsync(cancellationToken).ConfigureAwait(true); + List incoming = + [ + .. networks + .OrderBy(n => n.IsPredefined) + .ThenBy(n => n.Name, StringComparer.OrdinalIgnoreCase) + .Select(n => new NetworkRow(n)) + ]; + var previous = _all.ToDictionary(r => r.Id); + _all.Clear(); + foreach (var row in incoming) + { + if (previous.TryGetValue(row.Id, out var existing)) + { + existing.Update(row); + _all.Add(existing); + } + else + { + // 行要回指页面:列宽绑在页面上,行模板得找得到它。 + row.Owner = this; + _all.Add(row); + } + } + LoadedOnce = true; + ApplyView(); + if (Selected is { } selected) + { + var still = _all.FirstOrDefault(r => r.Id == selected.Id); + Selected = still; + if (still is not null) + { + await LoadDetailAsync(still, cancellationToken).ConfigureAwait(true); + } + } + OnPropertiesChanged(nameof(TotalCount), nameof(CustomCount), nameof(PredefinedCount), nameof(UnusedCount)); + } + finally + { + Busy = false; + } + } + + /// + public override void Reset() + { + _all.Clear(); + View.Clear(); + Attached.Clear(); + Ipam.Clear(); + Basics.Clear(); + Selected = null; + LoadedOnce = false; + OnPropertiesChanged(nameof(TotalCount), nameof(CustomCount), nameof(PredefinedCount), nameof(IsEmpty)); + } + + /// + public override bool WantsRefresh(DockerEvent dockerEvent) => dockerEvent.Type == "network"; + + private void ApplyView() + { + var needle = Search.Trim(); + IEnumerable filtered = _all; + if (CustomOnly) + { + filtered = filtered.Where(r => !r.IsPredefined); + } + if (needle.Length > 0) + { + filtered = filtered.Where(r => + r.Name.Contains(needle, StringComparison.OrdinalIgnoreCase) || + r.Subnet.Contains(needle, StringComparison.OrdinalIgnoreCase) || + r.Driver.Contains(needle, StringComparison.OrdinalIgnoreCase)); + } + View.Merge([.. filtered], (_, _) => { }); + OnPropertiesChanged(nameof(IsEmpty), nameof(NoMatch), nameof(CustomCount), nameof(PredefinedCount)); + } + + private async Task SelectAsync(NetworkRow? row) + { + Selected = row; + if (row is not null) + { + await LoadDetailAsync(row, Shell.Lifetime).ConfigureAwait(true); + } + } + + /// + /// 一个 CIDR 里能给容器用多少个地址;解不出来返回 。 + /// + /// 减 2 是网络地址与广播地址,再减 1 是网关 —— docker 的 bridge 网络会占掉 .1。 + /// + /// + internal static long? SubnetCapacity(string? cidr) + { + if (cidr is not { Length: > 0 } text || text.IndexOf('/') is not (> 0 and int slash)) + { + return null; + } + if (!int.TryParse(text[(slash + 1)..], out var prefix)) + { + return null; + } + // IPv6 的地址空间大到显示出来毫无意义,不给。 + if (text.Contains(':', StringComparison.Ordinal) || prefix is < 0 or > 32) + { + return null; + } + var total = 1L << (32 - prefix); + return total > 3 ? total - 3 : 0; + } + + private async Task LoadDetailAsync(NetworkRow row, CancellationToken cancellationToken) + { + if (Client is not { } client) + { + return; + } + try + { + var detail = await client.InspectNetworkAsync(row.Id, cancellationToken).ConfigureAwait(true); + Attached.Clear(); + foreach ((var id, var container) in detail.Containers ?? []) + { + Attached.Add(new(id, container.Name ?? Humanize.ShortId(id), container.IPv4Address ?? "—")); + } + Ipam.Clear(); + Ipam.Add(new("子网", detail.FirstSubnet ?? "(自动)", RowTone.Ok)); + Ipam.Add(new("网关", detail.FirstGateway ?? "(自动)", RowTone.Ok)); + Ipam.Add(new("IPAM 驱动", detail.IPAM?.Driver ?? "default")); + // 「已分配 3 / 65533」:子网快用满是一个到了才发现就太晚的问题。 + if (SubnetCapacity(detail.FirstSubnet) is { } capacity) + { + var used = Attached.Count; + Ipam.Add(new("已分配", $"{used} / {capacity:N0}", + capacity > 0 && used > capacity * 0.8 ? RowTone.Warn : RowTone.Idle)); + } + Basics.Clear(); + Basics.Add(new("网络 ID", Humanize.ShortId(detail.Id))); + Basics.Add(new("驱动", detail.Driver ?? "—")); + Basics.Add(new("作用域", detail.Scope ?? "—")); + Basics.Add(new("创建于", Humanize.LocalTime(detail.Created))); + Basics.Add(new("internal", detail.Internal ? "是 · 不通外网" : "否 · 可访问外网")); + Basics.Add(new("attachable", detail.Attachable ? "是" : "否")); + Basics.Add(new("IPv6", detail.EnableIPv6 ? "已启用" : "关闭")); + OnPropertiesChanged(nameof(CanRemove), nameof(RemoveHint)); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Shell.Feedback.ReportError("读取网络详情", ex); + } + } + + private async Task CreateAsync() + { + if (Client is not { } client) + { + return; + } + try + { + var info = await client.InfoAsync(Shell.Lifetime).ConfigureAwait(true); + _swarmActive = info.Swarm?.LocalNodeState == "active"; + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + _swarmActive = false; + } + var form = new CreateNetworkForm(_swarmActive); + if (!await Shell.ShowFormAsync(form).ConfigureAwait(true)) + { + return; + } + try + { + var created = await client.CreateNetworkAsync(form.Name, form.Driver, form.Subnet, + form.Gateway, form.Internal, form.Attachable, form.EnableIPv6, Shell.Lifetime).ConfigureAwait(true); + if (created.Warning is { Length: > 0 } warning) + { + Shell.Feedback.Notify(FeedbackKind.Warning, "daemon 有话说", warning); + } + else + { + Shell.Feedback.Status(FeedbackKind.Success, $"已新建网络 {form.Name}"); + } + await RefreshAsync(Shell.Lifetime).ConfigureAwait(true); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Shell.Feedback.ReportError("新建网络", ex); + } + } + + private async Task RemoveAsync(NetworkRow row) + { + if (Client is not { } client || row.IsPredefined) + { + return; + } + var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + { + Title = $"删除网络 {row.Name}?", + Icon = "Icon.trash-2", + HostName = "", + ConfirmLabel = "删除网络", + Commands = [$"DELETE /networks/{Humanize.ShortId(row.Id)}"], + CommandNote = $"等价于 docker network rm {row.Name}", + Consequences = + [ + new(1, "删除网络本身不会丢数据。"), + new(row.AttachedCount > 0 ? 3 : 0, + row.AttachedCount > 0 + ? $"仍有 {row.AttachedCount} 个容器接入 —— daemon 会拒绝,先摘除或停止它们。" + : "当前没有容器接入。"), + new(0, "compose 项目的默认网络删掉后,下次 up 会自动重建。") + ] + })).ConfigureAwait(true); + if (!confirmed) + { + return; + } + try + { + await client.RemoveNetworkAsync(row.Id, Shell.Lifetime).ConfigureAwait(true); + Shell.Feedback.Status(FeedbackKind.Success, $"已删除网络 {row.Name}"); + Selected = null; + await RefreshAsync(Shell.Lifetime).ConfigureAwait(true); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + // 409 = 还连着容器。RemoveHint 里那句话本来就是为这件事写的, + // 却只在按钮置灰时才说过 —— 真撞上了反而只剩 daemon 的原文。 + ToastAction[] actions = ex is DockerApiException { IsConflict: true } + ? [new("看看还连着谁", () => Selected = row)] + : []; + Shell.Feedback.ReportError("删除网络", ex, actions); + } + } + + private async Task PruneAsync() + { + if (Client is not { } client) + { + return; + } + var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + { + Title = "清理未使用的网络?", + Icon = "Docker.broom", + HostName = "", + ConfirmLabel = "开始清理", + ConfirmIcon = "Docker.broom", + Commands = ["POST /networks/prune"], + CommandNote = "等价于 docker network prune", + Consequences = + [ + new(1, "不丢数据 —— 网络只是配置。"), + new(0, $"当前有 {UnusedCount} 个自定义网络没有容器接入。"), + new(2, "已停止的 compose 项目,它的网络也在这个名单里;下次 up 会自动重建。") + ] + })).ConfigureAwait(true); + if (!confirmed) + { + return; + } + try + { + var report = await client.PruneNetworksAsync(Shell.Lifetime).ConfigureAwait(true); + Shell.Feedback.Notify(FeedbackKind.Success, "清理完成", $"删除 {report.DeletedCount} 个网络"); + await RefreshAsync(Shell.Lifetime).ConfigureAwait(true); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Shell.Feedback.ReportError("清理网络", ex); + } + } + + private async Task ConnectAsync(NetworkRow row) + { + if (Client is not { } client) + { + return; + } + var containers = await client.ListContainersAsync(true, Shell.Lifetime).ConfigureAwait(true); + HashSet already = [.. Attached.Select(a => a.Id)]; + var form = new ConnectNetworkForm( + $"{row.Name} · {row.Driver} · {row.Subnet}", + containers.Select(c => ( + c.Id, + c.Name, + Meta: already.Contains(c.Id) ? "已在这个网络里" : c.State == "running" ? "运行中" : "已停止 · 接入后下次启动生效", + Enabled: !already.Contains(c.Id), + Reason: already.Contains(c.Id) ? "已在这个网络里" : ""))); + if (!await Shell.ShowFormAsync(form).ConfigureAwait(true)) + { + return; + } + var result = await BatchRunner.RunAsync( + [.. form.SelectedIds.Select(id => (Target: id, containers.First(c => c.Id == id).Name))], + (id, ct) => client.ConnectNetworkAsync(row.Id, id, form.Aliases.Length > 0 ? form.Aliases : null, ct), + null, Shell.Lifetime).ConfigureAwait(true); + Shell.Feedback.ReportBatch("接入", result, Shell.CurrentPage == PanelPage.Networks); + await RefreshAsync(Shell.Lifetime).ConfigureAwait(true); + } + + private async Task DisconnectAsync(AttachedContainer attached) + { + if (Client is not { } client || Selected is not { } row) + { + return; + } + var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + { + Title = $"把 {attached.Name} 从 {row.Name} 上摘掉?", + Icon = "Icon.unplug", + HostName = "", + ConfirmLabel = "摘除", + ConfirmIcon = "Icon.unplug", + Commands = [$"POST /networks/{Humanize.ShortId(row.Id)}/disconnect"], + CommandNote = $"等价于 docker network disconnect {row.Name} {attached.Name}", + Consequences = + [ + new(2, "容器会立刻失去这个网络上的连通性 —— 正在进行的连接会断。"), + new(0, "不重启容器,也不丢数据。") + ] + })).ConfigureAwait(true); + if (!confirmed) + { + return; + } + try + { + await client.DisconnectNetworkAsync(row.Id, attached.Id, false, Shell.Lifetime).ConfigureAwait(true); + Shell.Feedback.Status(FeedbackKind.Success, $"已把 {attached.Name} 从 {row.Name} 摘掉"); + await RefreshAsync(Shell.Lifetime).ConfigureAwait(true); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Shell.Feedback.ReportError("摘除容器", ex); + } + } +} + +/// +/// 网络列表的列宽。默认宽度取自设计稿 08 号板的表头 +/// (名称 362 / 驱动 92 / 作用域 84 / 子网 164 / 已接入 112)。 +/// +public sealed class NetworkColumns : ListColumns +{ + + /// + public override IReadOnlyList Keys { get; } = ["name", "driver", "scope", "subnet", "attached"]; + + /// 名称列。 + public GridLength Name + { + get; + set => SetField(ref field, Clamp(value, "name")); + } = new(362); + + /// 驱动列。 + public GridLength Driver + { + get; + set => SetField(ref field, Clamp(value, "driver")); + } = new(92); + + /// 作用域列。 + public GridLength Scope + { + get; + set => SetField(ref field, Clamp(value, "scope")); + } = new(84); + + /// 子网列。 + public GridLength Subnet + { + get; + set => SetField(ref field, Clamp(value, "subnet")); + } = new(164); + + /// 已接入列。 + public GridLength Attached + { + get; + set => SetField(ref field, Clamp(value, "attached")); + } = new(112); + + /// + public override double Get(string key) => key switch + { + "name" => Name.Value, + "driver" => Driver.Value, + "scope" => Scope.Value, + "subnet" => Subnet.Value, + _ => Attached.Value + }; + + /// + public override void Set(string key, double width) + { + GridLength value = new(width); + switch (key) + { + case "name": Name = value; break; + case "driver": Driver = value; break; + case "scope": Scope = value; break; + case "subnet": Subnet = value; break; + case "attached": Attached = value; break; + } + } + + /// + public override double Min(string key) => key switch + { + "name" => 160, + "driver" => 70, + "scope" => 62, + "subnet" => 90, + _ => 70 + }; + + /// + public override double MaxAutoFit(string key) => key is "name" ? 760 : 300; + + /// + // 名称格里还坐着一枚网络图标,内置网络还多一块「内置」徽标。 + public override double Padding(string key) => key is "name" ? 84 : 18; +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/OverviewPageView.axaml b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/OverviewPageView.axaml new file mode 100644 index 0000000..4d87ba7 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/OverviewPageView.axaml @@ -0,0 +1,309 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/OverviewPageView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/OverviewPageView.axaml.cs new file mode 100644 index 0000000..0118ddb --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/OverviewPageView.axaml.cs @@ -0,0 +1,21 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; + +namespace VelaShell.Plugin.DockerPanel.Ui.Pages; + +/// 总览页的视图。 +public sealed partial class OverviewPageView : UserControl +{ + /// 建视图。 + public OverviewPageView() => AvaloniaXamlLoader.Load(this); + + /// “需要关注”里那条的动作天生是个 Action,不必为它造一个 ICommand。 + private void OnAttentionClick(object? sender, RoutedEventArgs e) + { + if (sender is Control { Tag: AttentionItem item }) + { + item.Action(); + } + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/OverviewPageViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/OverviewPageViewModel.cs new file mode 100644 index 0000000..c9b6ce3 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/OverviewPageViewModel.cs @@ -0,0 +1,635 @@ +using System.Collections.ObjectModel; +using VelaShell.Plugin.DockerPanel.Docker; + +namespace VelaShell.Plugin.DockerPanel.Ui.Pages; + +/// 事件时间线里的一条。 +public sealed class EventItem(DockerEvent source) +{ + /// 时刻。 + public string Time { get; } = source.At.ToString("HH:mm:ss"); + + /// 事件类型(start / die / pull…)。 + public string Action { get; } = source.Action ?? ""; + + /// 对象名。 + public string Target { get; } = source.DisplayName; + + /// 一句人话。 + public string Message { get; } = Describe(source); + + /// 语气。 + public RowTone Tone { get; } = Classify(source); + + private static RowTone Classify(DockerEvent source) => source.Action switch + { + "start" or "create" or "health_status: healthy" or "connect" => RowTone.Ok, + "die" or "oom" or "health_status: unhealthy" => RowTone.Danger, + "pause" or "restart" => RowTone.Warn, + "stop" or "kill" or "destroy" or "disconnect" => RowTone.Idle, + _ => RowTone.Busy + }; + + private static string Describe(DockerEvent source) + { + var exitCode = source.Actor?.Attributes?.GetValueOrDefault("exitCode"); + // 卷与网络的 create 与容器的 create 是同一个词,先按对象类型分流。 + if (source is { Type: "volume", Action: "create" }) + { + return "本地卷已创建"; + } + if (source is { Type: "network", Action: "create" }) + { + return "网络已创建"; + } + return source.Action switch + { + "start" => "容器已启动", + "stop" => "收到 SIGTERM", + "kill" => "收到 SIGKILL", + "die" => exitCode is { Length: > 0 } code ? $"退出码 {code}" : "容器已退出", + "create" => "容器已创建", + "destroy" => "容器已移除", + "pause" => "容器已暂停", + "unpause" => "容器已恢复", + "restart" => "容器已重启", + "oom" => "内存超限,被 OOM 杀掉", + "pull" => "镜像拉取完成", + "tag" => "镜像已打标签", + "untag" => "镜像标签已移除", + "delete" => "镜像已删除", + "connect" => "容器已接入网络", + "disconnect" => "容器已从网络摘除", + "exec_start" => "exec 会话开始", + { } action when action.StartsWith("health_status", StringComparison.Ordinal) => + action["health_status: ".Length..], + _ => source.Action ?? "" + }; + } +} + +/// “需要关注”里的一条。 +/// 图标。 +/// 语气。 +/// 标题。 +/// 小字。 +/// 右侧动作文字。 +/// 动作。 +public sealed record AttentionItem(string Icon, RowTone Tone, string Title, string Detail, string ActionLabel, + Action Action); + +/// Top N 排行里的一条。 +public sealed class TopItem(string name, double value, double max, string valueText, bool hot) : ObservableObject +{ + /// 名字。 + public string Name { get; } = name; + + /// 数值。 + public double Value { get; } = value; + + /// 相对最大值的比例 0–1(条按这个画,而不是按绝对百分比)。 + public double Ratio { get; } = max > 0 ? Math.Clamp(value / max, 0, 1) : 0; + + /// 右侧文字。 + public string ValueText { get; } = valueText; + + /// 要不要转成警示色。 + public bool Hot { get; } = hot; +} + +/// +/// CPU 趋势图里的一根柱子。 +/// +/// 高度在视图模型里就算好,不在 XAML 里换算 —— 纵轴要按**窗口峰值**缩放, +/// 而那个分母只有视图模型知道。 +/// +/// +/// 柱高(像素,最高 96)。 +/// 这一格采到数没有。没采到的格子留空,而不是画成 0 —— 那是两件事。 +public readonly record struct TrendBar(double Height, bool HasSample); + +/// 总览页。 +public sealed class OverviewPageViewModel : PageViewModel +{ + private const int MaxEvents = 60; + + /// 趋势图的格子数。48 × 5s = 4 分钟,与图下那条时间轴一一对上。 + private const int TrendSlots = 48; + + /// 趋势图的柱高上限(像素)。 + private const double TrendHeight = 96; + + /// + /// 纵轴的下限。 + /// + /// 一台 32 核机器的容器总占用常年在个位数,纵轴钉死 0–100% 会把任何真实负载 + /// 都压成一条贴着底边的直线 —— 那张图就什么也读不出来了。 + /// 所以纵轴跟着窗口峰值走,但不低于这个数,免得 0.1% 的抖动被放大成山峰。 + /// + /// + private const double TrendFloorPercent = 5; + + /// 算"持续高 CPU"的门槛,与行内 sparkline 转黄的阈值同一个数。 + private const double HotCpuThreshold = 30; + + /// 要连续在高位这么久才报 —— CPU 尖峰在容器世界里再正常不过。 + private static readonly TimeSpan HotCpuHold = TimeSpan.FromMinutes(2); + + private readonly List _cpuTrend = []; + private readonly Dictionary _hotSince = []; + private readonly Dictionary _hotNames = []; + private readonly Dictionary _hotPercent = []; + private double _cpuPeak; + private long _hostMemory; + private int _hostCpus; + private bool _reclaimRequested; + + /// 建总览页。 + public OverviewPageViewModel(DockerPanelViewModel shell) : base(shell) + { + RefreshCommand = new RelayCommand(_ => RefreshAsync(Shell.Lifetime)); + RunContainerCommand = new RelayCommand(_ => Shell.GoToAsync(PanelPage.Images)); + PullImageCommand = new RelayCommand(_ => Shell.ShowPullDialogAsync(null)); + CleanupCommand = new RelayCommand(_ => Shell.GoToAsync(PanelPage.System)); + ComposeCommand = new RelayCommand(_ => Shell.GoToAsync(PanelPage.Compose)); + RefreshReclaimCommand = new RelayCommand(_ => RefreshReclaimAsync(Shell.Lifetime)); + NewComposeCommand = new RelayCommand(_ => Shell.GoToAsync(PanelPage.Compose)); + OpenTerminalCommand = new RelayCommand(_ => OpenTerminalAsync()); + ExportDiagnosticsCommand = new RelayCommand(_ => ExportDiagnosticsAsync()); + } + + /// + public override PanelPage Page => PanelPage.Overview; + + /// + public override string Title => "总览"; + + /// 运行中容器。 + public string RunningText + { + get; + private set => SetField(ref field, value); + } = "—"; + + /// 运行中容器的小字。 + public string RunningDetail + { + get; + private set => SetField(ref field, value); + } = ""; + + /// CPU 总占用。 + public string CpuText + { + get; + private set => SetField(ref field, value); + } = "—"; + + /// CPU 小字。 + public string CpuDetail + { + get; + private set => SetField(ref field, value); + } = ""; + + /// 内存。 + public string MemText + { + get; + private set => SetField(ref field, value); + } = "—"; + + /// 内存小字。 + public string MemDetail + { + get; + private set => SetField(ref field, value); + } = ""; + + /// 可回收空间。 + public string ReclaimText + { + get; + private set => SetField(ref field, value); + } = "—"; + + /// 可回收小字。 + public string ReclaimDetail + { + get; + private set => SetField(ref field, value); + } = ""; + + /// CPU 趋势采样。 + /// + /// CPU 趋势图的 48 根柱子。**永远是 48 根**:还没采到的那些留空, + /// 否则刚打开面板时几根柱子挤在左边,而下面那条时间轴却横跨整张卡,对不上。 + /// + public ObservableCollection CpuTrend { get; } = []; + + /// 趋势图标题旁边那行小字 —— 得写清楚纵轴到哪儿,否则读不出量级。 + public string TrendScaleText + { + get; + private set => SetField(ref field, value); + } = "每 5 秒一个采样点"; + + /// 按当前窗口重画趋势图。 + private void RebuildTrend() + { + var peak = _cpuTrend.Count > 0 ? _cpuTrend.Max() : 0; + var scale = Math.Max(peak, TrendFloorPercent); + TrendScaleText = $"每 5 秒一个采样点 · 纵轴 0–{Humanize.Percent(scale)}"; + CpuTrend.Clear(); + for (var i = _cpuTrend.Count; i < TrendSlots; i++) + { + CpuTrend.Add(new(0, false)); + } + foreach (var sample in _cpuTrend) + { + CpuTrend.Add(new(Math.Max(2, Math.Clamp(sample / scale, 0, 1) * TrendHeight), true)); + } + } + + /// 把趋势图清成 48 个空格子(而不是清成空的)。 + private void ClearTrend() + { + _cpuTrend.Clear(); + RebuildTrend(); + } + + /// CPU 占用 Top 5。 + public ObservableCollection TopCpu { get; } = []; + + /// 实时事件。 + public ObservableCollection Events { get; } = []; + + /// 需要关注。 + public ObservableCollection Attention { get; } = []; + + /// 有没有要关注的。 + public bool HasAttention => Attention.Count > 0; + + /// 刷新。 + public RelayCommand RefreshCommand { get; } + + /// 运行容器。 + public RelayCommand RunContainerCommand { get; } + + /// 拉取镜像。 + public RelayCommand PullImageCommand { get; } + + /// 清理空间。 + public RelayCommand CleanupCommand { get; } + + /// 去 Compose。 + public RelayCommand ComposeCommand { get; } + + /// 重算可回收空间。 + public RelayCommand RefreshReclaimCommand { get; } + + /// 去 Compose 页新建项目。 + public RelayCommand NewComposeCommand { get; } + + /// 打开一个容器的终端(挑第一个在跑的)。 + public RelayCommand OpenTerminalCommand { get; } + + /// 导出一份诊断信息。 + public RelayCommand ExportDiagnosticsCommand { get; } + + /// 能不能弹本地文件对话框。 + public bool CanPickFiles => FilePicker.IsAvailable; + + /// + /// 打开终端。挑第一个在跑的容器 —— 没有在跑的就说清楚,而不是打开一个空终端。 + /// + private async Task OpenTerminalAsync() + { + await Shell.GoToAsync(PanelPage.Containers).ConfigureAwait(true); + if (Shell.Containers.View.FirstOrDefault(r => r.IsRunning) is not { } row) + { + Shell.Feedback.Status(FeedbackKind.Warning, "没有正在运行的容器 —— 终端要有个容器才能进。"); + return; + } + Shell.Containers.RowTerminalCommand.Execute(row); + } + + /// + /// 导出诊断:把面板此刻能拿到的那几份事实写成一个文本文件。 + /// + /// 用途是发给别人看。所以**不含**任何凭据、环境变量与日志正文 —— + /// 那些地方最容易夹带口令,而一份要发出去的文件不该由用户去逐行检查。 + /// + /// + private async Task ExportDiagnosticsAsync() + { + if (Client is not { } client) + { + return; + } + var target = await FilePicker + .PickSaveAsync("导出诊断信息", $"docker-diagnostics-{Shell.SelectedEndpoint?.DisplayName ?? "host"}.txt", "txt") + .ConfigureAwait(true); + if (target is null) + { + return; + } + try + { + var info = await client.InfoAsync(Shell.Lifetime).ConfigureAwait(true); + var version = await client.VersionAsync(Shell.Lifetime).ConfigureAwait(true); + var containers = await client.ListContainersAsync(true, Shell.Lifetime).ConfigureAwait(true); + var sb = new System.Text.StringBuilder(); + sb.AppendLine("# VelaShell Docker 面板 · 诊断信息"); + sb.AppendLine("# 不含凭据、环境变量与日志正文 —— 可以直接发给别人。"); + sb.AppendLine($"主机 {Shell.SelectedEndpoint?.DisplayName}"); + sb.AppendLine($"socket {Shell.SelectedEndpoint?.Endpoint.SocketPath}"); + sb.AppendLine($"Engine {version.Version} (API {version.ApiVersion})"); + sb.AppendLine($"操作系统 {info.OperatingSystem} · {info.KernelVersion}"); + sb.AppendLine($"架构 / CPU {info.Architecture} · {info.NCPU} 核 · {Humanize.Bytes(info.MemTotal)}"); + sb.AppendLine($"存储驱动 {info.Driver}"); + sb.AppendLine($"容器 / 镜像 {info.Containers} / {info.Images}"); + sb.AppendLine(); + sb.AppendLine("## 容器"); + foreach (var container in containers) + { + sb.AppendLine($"{container.Name,-28} {container.State,-10} {container.Image,-42} {container.Status}"); + } + await using var output = await target.OpenWriteAsync().ConfigureAwait(true); + await using var writer = new StreamWriter(output); + await writer.WriteAsync(sb.ToString()).ConfigureAwait(true); + Shell.Feedback.Notify(FeedbackKind.Success, "诊断信息已导出", + $"{target.Name} —— 不含凭据与日志正文。"); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Shell.Feedback.ReportError("导出诊断信息", ex); + } + } + + /// + public override async Task RefreshAsync(CancellationToken cancellationToken) + { + if (Client is not { } client) + { + return; + } + Busy = true; + try + { + var containers = await client.ListContainersAsync(true, cancellationToken).ConfigureAwait(true); + var info = await client.InfoAsync(cancellationToken).ConfigureAwait(true); + var running = containers.Count(c => c.State == "running"); + var unhealthy = containers.Count(c => (c.Status ?? "").Contains("(unhealthy)", StringComparison.OrdinalIgnoreCase)); + var failed = containers.Count(c => c.State == "exited" && !(c.Status ?? "").Contains("(0)", StringComparison.Ordinal)); + RunningText = $"{running} / {containers.Length}"; + RunningDetail = $"{unhealthy} 个不健康 · {failed} 个异常退出"; + _hostMemory = info.MemTotal; + _hostCpus = info.NCPU; + if (MemText == "—") + { + MemDetail = $"共 {Humanize.Bytes(info.MemTotal)} · {info.NCPU} 核"; + } + BuildAttention(containers); + Shell.SetContainerCount(containers.Length); + Shell.SetImageCount(info.Images); + LoadedOnce = true; + OnPropertyChanged(nameof(HasAttention)); + } + finally + { + Busy = false; + } + // 第一次进来时在后台算一次可回收空间 —— 它太慢,不能挂在刷新的主路径上。 + if (!_reclaimRequested) + { + _ = RefreshReclaimAsync(cancellationToken); + } + } + + /// + public override void Reset() + { + Events.Clear(); + Attention.Clear(); + TopCpu.Clear(); + ClearTrend(); + LoadedOnce = false; + RunningText = "—"; + CpuText = "—"; + MemText = "—"; + MemDetail = ""; + ReclaimText = "—"; + ReclaimDetail = ""; + _hostMemory = 0; + _hostCpus = 0; + _reclaimRequested = false; + OnPropertyChanged(nameof(HasAttention)); + } + + /// + public override bool WantsRefresh(DockerEvent dockerEvent) => dockerEvent.Type is "container" or "image"; + + /// 收下一条事件,进时间线。 + public void AcceptEvent(DockerEvent dockerEvent) + { + Events.Insert(0, new(dockerEvent)); + while (Events.Count > MaxEvents) + { + Events.RemoveAt(Events.Count - 1); + } + } + + /// + /// 用容器页那份已经采到的统计更新 Top 5 与趋势 —— + /// 不再单独发一轮请求。 + /// + public void AcceptStatsSnapshot(IReadOnlyList rows) + { + List running = [.. rows.Where(r => r.IsRunning && r.CpuPercent > 0)]; + var total = running.Sum(r => r.CpuPercent); + CpuText = Humanize.Percent(total); + _cpuPeak = Math.Max(_cpuPeak, total); + CpuDetail = running.Count > 0 + ? $"{(_hostCpus > 0 ? $"{_hostCpus} 核 · " : "")}峰值 {Humanize.Percent(_cpuPeak)}" + : "没有容器在用 CPU"; + TrackHotContainers(running); + // 内存卡走的是同一批采样:容器占用之和 / 宿主总量, + // 单独再问一次 daemon 只会得到同样的数字。 + var usedMemory = rows.Where(r => r.IsRunning).Sum(r => r.MemoryBytes); + MemText = usedMemory > 0 ? Humanize.Bytes(usedMemory) : "0 B"; + MemDetail = _hostMemory > 0 + ? $"容器占用 · 宿主共 {Humanize.Bytes(_hostMemory)}{(_hostCpus > 0 ? $" · {_hostCpus} 核" : "")}" + : "容器占用"; + _cpuTrend.Add(total); + while (_cpuTrend.Count > TrendSlots) + { + _cpuTrend.RemoveAt(0); + } + RebuildTrend(); + var max = running.Count > 0 ? running.Max(r => r.CpuPercent) : 0; + TopCpu.Clear(); + foreach (var row in running.OrderByDescending(r => r.CpuPercent).Take(5)) + { + TopCpu.Add(new(row.Name, row.CpuPercent, max, Humanize.Percent(row.CpuPercent), row.CpuHot)); + } + } + + /// + /// 用户在设置里关掉了实时统计。这两张卡不该继续显示一个停在过去某一刻的数字 —— + /// 说清"是被关掉了"比留一个看不出新鲜度的旧值诚实。 + /// + public void SetStatsDisabled() + { + CpuText = "—"; + CpuDetail = "实时统计已在设置里关闭"; + MemText = "—"; + MemDetail = _hostMemory > 0 ? $"宿主共 {Humanize.Bytes(_hostMemory)} · {_hostCpus} 核" : ""; + TopCpu.Clear(); + ClearTrend(); + } + + /// 更新"可回收空间"那张卡(系统页算完之后顺手喂进来)。 + public void AcceptReclaim(ReclaimBreakdown reclaim) + { + _reclaimRequested = true; + ReclaimText = Humanize.Bytes(reclaim.Total); + ReclaimDetail = reclaim.Describe(); + } + + /// + /// 自己去算一次可回收空间。 + /// + /// system df 会让 daemon 把每个卷 du 一遍,在卷多的机器上要好几秒 —— + /// 所以它跟着总览的每次刷新跑,只在第一次进来时算一遍, + /// 之后靠系统页的刷新或卡片上的重算按钮更新。 + /// + /// + public async Task RefreshReclaimAsync(CancellationToken cancellationToken) + { + if (Client is not { } client) + { + return; + } + _reclaimRequested = true; + ReclaimText = "…"; + ReclaimDetail = "正在统计"; + try + { + var usage = await client.DiskUsageAsync(cancellationToken).ConfigureAwait(true); + AcceptReclaim(DiskMath.Reclaimable(usage)); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + ReclaimText = "—"; + ReclaimDetail = "统计失败"; + Shell.Context.Log.Warn($"overview: system df failed: {ex.Message}"); + } + } + + /// + /// 记住哪些容器一直在高位。 + /// + /// 关注的是**持续**而不是某一帧:CPU 尖峰在容器世界里再正常不过, + /// 把一次 40% 报成告警只会训练用户忽略这块面板。所以要连续几轮都在高位才算。 + /// + /// + private void TrackHotContainers(IReadOnlyList running) + { + HashSet stillHot = []; + foreach (var row in running.Where(r => r.CpuPercent >= HotCpuThreshold)) + { + stillHot.Add(row.Id); + if (!_hotSince.ContainsKey(row.Id)) + { + _hotSince[row.Id] = DateTimeOffset.UtcNow; + } + _hotNames[row.Id] = row.Name; + _hotPercent[row.Id] = row.CpuPercent; + } + // 掉下去就清零 —— 「已持续 12 分钟」得是真的连续,不能把两段拼起来。 + foreach (var id in _hotSince.Keys.Where(id => !stillHot.Contains(id)).ToList()) + { + _hotSince.Remove(id); + _hotNames.Remove(id); + _hotPercent.Remove(id); + } + RebuildHotAttention(); + } + + /// 把"持续高 CPU"那几条插进关注列表,并把过期的那些拿掉。 + private void RebuildHotAttention() + { + foreach (var stale in Attention.Where(a => a.Icon == "Icon.cpu").ToList()) + { + Attention.Remove(stale); + } + foreach ((var id, var since) in _hotSince) + { + var held = DateTimeOffset.UtcNow - since; + if (held < HotCpuHold) + { + continue; + } + var name = _hotNames.GetValueOrDefault(id, Humanize.ShortId(id)); + Attention.Insert(0, new("Icon.cpu", RowTone.Warn, + $"{name} CPU 持续高于 {HotCpuThreshold:F0}%", + $"已持续 {Humanize.Duration(held)} · 当前 {Humanize.Percent(_hotPercent.GetValueOrDefault(id))}", + "查看统计", + () => OpenContainer(id))); + } + OnPropertyChanged(nameof(HasAttention)); + } + + private void BuildAttention(IReadOnlyList containers) + { + Attention.Clear(); + foreach (var container in containers) + { + var status = container.Status ?? ""; + if (status.Contains("(unhealthy)", StringComparison.OrdinalIgnoreCase)) + { + Attention.Add(new("Icon.triangle-alert", RowTone.Danger, + $"{container.Name} 健康检查失败", + $"{container.Image} · {status}", + "查看日志", + () => OpenContainer(container.Id))); + } + else if (container.State == "exited" && !status.Contains("(0)", StringComparison.Ordinal)) + { + Attention.Add(new("Docker.circle-x", RowTone.Danger, + $"{container.Name} 异常退出", + $"{container.Image} · {status}", + "查看日志", + () => OpenContainer(container.Id))); + } + else if (container.State == "paused") + { + Attention.Add(new("Icon.pause", RowTone.Warn, + $"{container.Name} 处于暂停态", + "进程还在,但不会处理任何请求。", + "去恢复", + () => OpenContainer(container.Id))); + } + } + if (Shell.Volumes.UnusedCount > 0) + { + Attention.Add(new("Docker.database", RowTone.Warn, + $"{Shell.Volumes.UnusedCount} 个卷没有容器在用", + "它们可能是刚 down 掉的项目留下的 —— 也可能就是垃圾。", + "去看看", + () => _ = Shell.GoToAsync(PanelPage.Volumes))); + } + } + + private void OpenContainer(string id) + { + _ = Shell.GoToAsync(PanelPage.Containers).ContinueWith(_ => Ui.Post(() => + { + if (Shell.Containers.View.FirstOrDefault(r => r.Id == id) is { } row) + { + Shell.Containers.OpenDetailCommand.Execute(row); + } + }), TaskScheduler.Default); + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/PullImageView.axaml b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/PullImageView.axaml new file mode 100644 index 0000000..02cc980 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/PullImageView.axaml @@ -0,0 +1,275 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/PullImageView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/PullImageView.axaml.cs new file mode 100644 index 0000000..77ebd02 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/PullImageView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace VelaShell.Plugin.DockerPanel.Ui.Pages; + +/// PullImageView 的视图。 +public sealed partial class PullImageView : UserControl +{ + /// 建视图。 + public PullImageView() => AvaloniaXamlLoader.Load(this); +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/PullImageViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/PullImageViewModel.cs new file mode 100644 index 0000000..c6f8faa --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/PullImageViewModel.cs @@ -0,0 +1,509 @@ +using System.Collections.ObjectModel; +using VelaShell.Plugin.DockerPanel.Docker; + +namespace VelaShell.Plugin.DockerPanel.Ui.Pages; + +/// 拉取对话框的三个状态。 +public enum PullStage +{ + /// 填表。 + Form, + + /// 拉取中。 + Running, + + /// 完成。 + Done +} + +/// 拉取进度里的一层。 +public sealed class PullLayerItem(string id, string status, double progress, string sizeText) : ObservableObject +{ + /// 层 id。 + public string Id { get; } = id; + + /// 状态。 + public string Status { get; private set; } = status; + + /// 进度 0–1。 + public double Progress { get; private set; } = progress; + + /// 大小文本。 + public string SizeText { get; private set; } = sizeText; + + /// 这一层是不是已经好了。 + public bool Complete => Progress >= 1; + + /// 更新。 + public void Update(string status, double progress, string sizeText) + { + Status = status; + Progress = progress; + SizeText = sizeText; + OnPropertiesChanged(nameof(Status), nameof(Progress), nameof(SizeText), nameof(Complete)); + } +} + +/// +/// 拉取镜像。 +/// +/// 同一个对话框**原地换态**,不弹第二层:填表 → 拉取中 → 完成。 +/// 「转入后台」把进度移交给顶栏的任务中心,任务继续跑 —— +/// 拉一个 2 GB 的镜像时,用户多半想一边等一边去看别的容器。 +/// +/// +public sealed class PullImageViewModel : ObservableObject, IAsyncDisposable +{ + private readonly DockerPanelViewModel _shell; + private readonly PullAggregator _aggregator = new(); + private readonly Dictionary _layerMap = []; + private CancellationTokenSource? _cts; + private PanelTask? _task; + private string _reference = ""; + private string _tag = "latest"; + private DateTimeOffset _startedAt; + private long _lastBytes; + private DateTimeOffset _lastSample; + + /// 建对话框。 + public PullImageViewModel(DockerPanelViewModel shell, string? initialReference) + { + _shell = shell; + if (!string.IsNullOrWhiteSpace(initialReference)) + { + var colon = initialReference!.LastIndexOf(':'); + var slash = initialReference.LastIndexOf('/'); + if (colon > slash && colon > 0) + { + _reference = initialReference[..colon]; + _tag = initialReference[(colon + 1)..]; + } + else + { + _reference = initialReference; + } + } + StartCommand = new RelayCommand(_ => StartAsync(), _ => Stage == PullStage.Form && Reference.Trim().Length > 0); + CancelCommand = new RelayCommand(_ => CancelAsync()); + BackgroundCommand = new RelayCommand(_ => _shell.CloseDialogCommand.Execute(null)); + CloseCommand = new RelayCommand(_ => _shell.CloseDialogCommand.Execute(null)); + _ = RefreshAuthAsync(); + } + + /// 当前状态。 + public PullStage Stage + { + get; + private set + { + if (SetField(ref field, value)) + { + OnPropertiesChanged(nameof(IsForm), nameof(IsRunning), nameof(IsDone), nameof(Title), nameof(ChipText)); + StartCommand.RaiseCanExecuteChanged(); + } + } + } = PullStage.Form; + + /// 填表态。 + public bool IsForm => Stage == PullStage.Form; + + /// 拉取中。 + public bool IsRunning => Stage == PullStage.Running; + + /// 完成态。 + public bool IsDone => Stage == PullStage.Done; + + /// 标题。 + public string Title => Stage switch + { + PullStage.Running => "拉取中", + PullStage.Done => Error.Length > 0 ? "拉取失败" : "拉取完成", + _ => "拉取镜像" + }; + + /// 标题右侧的徽章文字。 + public string ChipText => Stage switch + { + PullStage.Running => "进行中", + PullStage.Done => Error.Length > 0 ? "失败" : "成功", + _ => "" + }; + + /// 镜像引用(不含标签)。 + public string Reference + { + get => _reference; + set + { + if (SetField(ref _reference, value)) + { + StartCommand.RaiseCanExecuteChanged(); + OnPropertiesChanged(nameof(FullReference), nameof(CommandPreview), nameof(CommandNote)); + _ = RefreshAuthAsync(); + } + } + } + + /// 标签。 + public string Tag + { + get => _tag; + set + { + if (SetField(ref _tag, value)) + { + OnPropertiesChanged(nameof(FullReference), nameof(CommandPreview), nameof(CommandNote)); + } + } + } + + /// 平台。 + public string Platform + { + get; + set + { + if (SetField(ref field, value)) + { + OnPropertyChanged(nameof(CommandNote)); + } + } + } = ""; + + /// 拉取全部标签。 + public bool AllTags + { + get; + set + { + if (SetField(ref field, value)) + { + OnPropertiesChanged(nameof(FullReference), nameof(CommandPreview), nameof(CommandNote)); + } + } + } + + /// 完整引用。 + public string FullReference => AllTags ? $"{Reference}(全部标签)" : $"{Reference}:{(Tag.Length > 0 ? Tag : "latest")}"; + + /// + /// 这一拉是拉到**哪台机器**上。 + /// + /// 与确认闸门同一个理由:同时开着生产与测试两个面板时, + /// 一个不写清主机的「开始拉取」会把 8 GB 拉到错的那台机器上。 + /// + /// + public string HostName => _shell.EndpointName; + + /// 主机那一行右边的补充(传输方式 / 引擎版本)。 + public string HostDetail => + $"{_shell.EndpointDetail}{(_shell.EngineVersion.Length > 0 ? $" · {_shell.EngineVersion}" : "")}"; + + /// 等效命令。 + public string CommandPreview => + $"POST /images/create?fromImage={Reference}{(AllTags ? "" : $"&tag={(Tag.Length > 0 ? Tag : "latest")}")}"; + + /// 等价命令行。 + public string CommandNote => + $"等价于 docker pull {(AllTags ? "-a " : "")}{Reference}{(AllTags ? "" : $":{(Tag.Length > 0 ? Tag : "latest")}")}" + + $"{(Platform.Trim().Length > 0 ? $" --platform {Platform.Trim()}" : "")}"; + + /// 仓库登录状态文本。 + public string AuthText + { + get; + private set => SetField(ref field, value); + } = ""; + + /// 仓库登录状态。 + public RegistryAuthState AuthState + { + get; + private set + { + if (SetField(ref field, value)) + { + OnPropertiesChanged(nameof(AuthOk), nameof(AuthWarn), nameof(AuthTone)); + } + } + } = RegistryAuthState.NotRequired; + + /// 凭据没问题。 + public bool AuthOk => AuthState is RegistryAuthState.Available or RegistryAuthState.NotRequired; + + /// 凭据取不到。 + public bool AuthWarn => AuthState is RegistryAuthState.HelperOnly or RegistryAuthState.Missing; + + /// + /// 凭据状态的语气,界面据此上色与选图标。 + /// + /// 三档而不是两档:HelperOnly(凭据交给了 credential helper,面板取不到)与 + /// Missing(压根没有)对用户是两件不同的事,而原来两者都跟"认证失败"一个样子 —— + /// 前者往往拉公开镜像照样成功,后者一定失败。 + /// + /// + public RowTone AuthTone => AuthState switch + { + RegistryAuthState.Available or RegistryAuthState.NotRequired => RowTone.Ok, + RegistryAuthState.HelperOnly => RowTone.Warn, + _ => RowTone.Danger + }; + + /// 总进度 0–1。 + public double Progress + { + get; + private set + { + if (SetField(ref field, value)) + { + OnPropertyChanged(nameof(PercentText)); + } + } + } + + /// 百分比文本。 + public string PercentText => $"{Progress * 100:0}%"; + + /// 速率文本。 + public string SpeedText + { + get; + private set => SetField(ref field, value); + } = ""; + + /// 层摘要。 + public string SummaryText + { + get; + private set => SetField(ref field, value); + } = ""; + + /// 字节进度文本。 + public string BytesText => _aggregator.TotalBytes > 0 + ? $"{Humanize.Bytes(_aggregator.CurrentBytes)} / {Humanize.Bytes(_aggregator.TotalBytes)}" + : ""; + + /// 已复用的层数摘要。 + public string ReusedText => _aggregator.ReusedLayers > 0 + ? $"{_aggregator.ReusedLayers} 层已存在,已折叠" + : ""; + + /// 有没有折叠掉的复用层。 + public bool HasReused => _aggregator.ReusedLayers > 0; + + /// 正在动的层。 + public ObservableCollection ActiveLayers { get; } = []; + + /// 错误(拉取失败时 daemon 的原话)。 + public string Error + { + get; + private set + { + if (SetField(ref field, value)) + { + OnPropertiesChanged(nameof(HasError), nameof(Title), nameof(ChipText)); + } + } + } = ""; + + /// 有没有出错。 + public bool HasError => Error.Length > 0; + + /// 完成后的摘要:摘要串。 + public string DoneDigest + { + get; + private set => SetField(ref field, value); + } = ""; + + /// 完成后的摘要:大小。 + public string DoneSize + { + get; + private set => SetField(ref field, value); + } = ""; + + /// 完成后的摘要:用时。 + public string DoneElapsed + { + get; + private set => SetField(ref field, value); + } = ""; + + /// 开始拉取。 + public RelayCommand StartCommand { get; } + + /// 取消拉取。 + public RelayCommand CancelCommand { get; } + + /// 转入后台。 + public RelayCommand BackgroundCommand { get; } + + /// 关掉。 + public RelayCommand CloseCommand { get; } + + private async Task RefreshAuthAsync() + { + if (_shell.RegistryAuth is not { } auth || Reference.Trim().Length == 0) + { + AuthText = ""; + return; + } + try + { + var status = await auth.GetStatusAsync(Reference.Trim(), _shell.Lifetime).ConfigureAwait(true); + AuthState = status.State; + AuthText = status.State switch + { + RegistryAuthState.Available => $"{status.Registry} 已登录 · {status.Detail}", + RegistryAuthState.NotRequired => $"{status.Registry} · {status.Detail}", + RegistryAuthState.HelperOnly => $"{status.Registry} · {status.Detail}", + _ => $"{status.Registry} · {status.Detail}" + }; + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + AuthText = ""; + } + } + + private async Task StartAsync() + { + if (_shell.Client is not { } client) + { + return; + } + Stage = PullStage.Running; + Error = ""; + _startedAt = DateTimeOffset.UtcNow; + _lastSample = _startedAt; + _lastBytes = 0; + _cts = CancellationTokenSource.CreateLinkedTokenSource(_shell.Lifetime); + _task = _shell.Tasks.Start("Docker.arrow-down-to-line", $"拉取 {FullReference}", indeterminate: false); + var reference = Reference.Trim(); + var tag = Tag.Trim(); + var platform = Platform.Trim(); + var allTags = AllTags; + try + { + var header = _shell.RegistryAuth is { } auth + ? await auth.GetAuthHeaderAsync(reference, _cts.Token).ConfigureAwait(true) + : null; + await client.PullImageAsync(reference, tag, platform, allTags, header, + new DirectProgress(OnFrame), _cts.Token).ConfigureAwait(true); + _task.Finish(PanelTaskState.Succeeded, "完成", _aggregator.Summary); + DoneElapsed = Humanize.Duration(DateTimeOffset.UtcNow - _startedAt); + DoneSize = _aggregator.TotalBytes > 0 + ? $"{Humanize.Bytes(_aggregator.TotalBytes)}({_aggregator.LayerCount} 层,复用 {_aggregator.ReusedLayers} 层)" + : $"{_aggregator.LayerCount} 层,全部复用"; + Stage = PullStage.Done; + _shell.Feedback.Status(FeedbackKind.Success, $"已拉取 {FullReference} · 用时 {DoneElapsed}"); + await _shell.Images.RefreshAsync(_shell.Lifetime).ConfigureAwait(true); + DoneDigest = _shell.Images.View.FirstOrDefault(r => + $"{r.Repository}:{r.Tag}" == $"{reference}:{(tag.Length > 0 ? tag : "latest")}")?.ShortId ?? ""; + } + catch (OperationCanceledException) + { + _task?.Finish(PanelTaskState.Cancelled, "已取消"); + Error = "已取消。"; + Stage = PullStage.Done; + } + catch (Exception ex) + { + _task?.Finish(PanelTaskState.Failed, "失败", ex.Message); + Error = ex is DockerApiException api ? api.Message : ex.Message; + Stage = PullStage.Done; + } + } + + private void OnFrame(PullProgressFrame frame) + { + _aggregator.Accept(frame); + Ui.Post(() => + { + Progress = _aggregator.Progress; + SummaryText = _aggregator.Summary; + if (_task is { } task) + { + task.Progress = _aggregator.Progress; + task.Detail = _aggregator.Summary; + } + UpdateSpeed(); + SyncLayers(); + OnPropertiesChanged(nameof(BytesText), nameof(ReusedText), nameof(HasReused)); + }); + } + + private void UpdateSpeed() + { + var now = DateTimeOffset.UtcNow; + var span = now - _lastSample; + if (span < TimeSpan.FromMilliseconds(700)) + { + return; + } + var bytes = _aggregator.CurrentBytes; + var delta = bytes - _lastBytes; + _lastBytes = bytes; + _lastSample = now; + if (delta <= 0) + { + return; + } + var perSecond = delta / span.TotalSeconds; + var remaining = _aggregator.TotalBytes - bytes; + SpeedText = remaining > 0 && perSecond > 0 + ? $"{Humanize.Bytes((long)perSecond)}/s · 剩余 ~{Humanize.Duration(TimeSpan.FromSeconds(remaining / perSecond))}" + : $"{Humanize.Bytes((long)perSecond)}/s"; + } + + /// + /// 只把**正在动**的层摆进列表。复用的那些折成一行 —— + /// 一次命中缓存的拉取能有三十层 "Already exists",逐条列出来只是噪音。 + /// + private void SyncLayers() + { + foreach ((var id, var status, var progress, var sizeText) in _aggregator.Snapshot()) + { + var complete = progress >= 1; + var reused = status.Contains("Already exists", StringComparison.OrdinalIgnoreCase); + if (reused) + { + continue; + } + if (_layerMap.TryGetValue(id, out var item)) + { + item.Update(status, progress, sizeText); + } + else if (!complete || ActiveLayers.Count < 12) + { + var created = new PullLayerItem(Humanize.ShortId(id), status, progress, sizeText); + _layerMap[id] = created; + ActiveLayers.Add(created); + } + } + } + + private async Task CancelAsync() + { + if (_cts is { } cts) + { + await cts.CancelAsync().ConfigureAwait(true); + } + _task?.Cancel(); + } + + /// + public async ValueTask DisposeAsync() + { + // 关掉对话框**不取消任务** —— 进度移交给任务中心,拉取继续。 + if (_cts is { } cts && Stage != PullStage.Running) + { + await cts.CancelAsync().ConfigureAwait(false); + cts.Dispose(); + _cts = null; + } + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/SystemPageView.axaml b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/SystemPageView.axaml new file mode 100644 index 0000000..565a145 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/SystemPageView.axaml @@ -0,0 +1,248 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/SystemPageView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/SystemPageView.axaml.cs new file mode 100644 index 0000000..77d164c --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/SystemPageView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace VelaShell.Plugin.DockerPanel.Ui.Pages; + +/// SystemPageView 的视图。 +public sealed partial class SystemPageView : UserControl +{ + /// 建视图。 + public SystemPageView() => AvaloniaXamlLoader.Load(this); +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/SystemPageViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/SystemPageViewModel.cs new file mode 100644 index 0000000..c14abd2 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/SystemPageViewModel.cs @@ -0,0 +1,510 @@ +using System.Collections.ObjectModel; +using VelaShell.Plugin.DockerPanel.Docker; + +namespace VelaShell.Plugin.DockerPanel.Ui.Pages; + +/// +/// 系统页顶上那一排统计卡(设计稿的 C/StatCard)。 +/// +/// 第三行的 不是装饰:一个孤零零的「镜像 34」说明不了任何事, +/// 「其中 14 个没人用」才是让人决定要不要清理的那一句。 +/// +/// +/// 图标资源键。 +/// 名目。 +/// 主数值。 +/// 底下那行小字。 +/// 数值的语气。 +public readonly record struct SystemStat(string Icon, string Label, string Value, string Sub, RowTone Tone = RowTone.Idle); + +/// 磁盘占用表里的一行。 +/// 类型。 +/// 总数。 +/// 活跃数。 +/// 占用。 +/// 可回收。 +/// 可回收那一列的语气。 +public readonly record struct DiskRow(string Kind, string Total, string Active, string Size, string Reclaimable, RowTone Tone); + +/// 堆叠占用条里的一段。 +/// 图例文字。 +/// 字节数。 +/// 占比(用于 star 宽度)。 +/// 颜色资源键。 +public readonly record struct DiskSegment(string Label, long Bytes, double Weight, string Brush); + +/// 一张回收卡片。 +public sealed class PruneCard(string icon, string title, string description, string tag, RowTone tone, RelayCommand command) : ObservableObject +{ + + /// 图标。 + public string Icon { get; } = icon; + + /// 标题。 + public string Title { get; } = title; + + /// 说明。 + public string Description { get; } = description; + + /// 右上角的标签(安全 / 注意 / 危险 / 会丢数据)。 + public string Tag { get; } = tag; + + /// 语气。 + public RowTone Tone { get; } = tone; + + /// 可回收大小。 + public string SizeText + { + get; + set => SetField(ref field, value); + } = "统计中…"; + + /// 执行。 + public RelayCommand Command { get; } = command; +} + +/// 系统页:磁盘占用与空间回收。 +public sealed class SystemPageViewModel : PageViewModel +{ + private DiskUsage? _usage; + private SystemInfo? _info; + private SystemVersion? _version; + private long _buildCacheBytes; + + /// 建系统页。 + public SystemPageViewModel(DockerPanelViewModel shell) : base(shell) + { + RefreshCommand = new RelayCommand(_ => RefreshAsync(Shell.Lifetime)); + PruneCards = + [ + new("Icon.trash-2", "已停止的容器", "删除所有 exited 状态的容器。运行中的不受影响。", "安全", RowTone.Ok, + new RelayCommand(_ => PruneContainersAsync())), + new("Docker.circle-dashed", "悬空镜像", "没有标签、也没有容器引用的中间层。", "安全", RowTone.Ok, + new RelayCommand(_ => Shell.Images.PruneDanglingCommand.Execute(null))), + new("Icon.layers", "全部未使用镜像", "含有标签但当前无容器在用的镜像;重新拉取需要时间与带宽。", "注意", RowTone.Warn, + new RelayCommand(_ => Shell.Images.PruneAllCommand.Execute(null))), + new("Docker.database", "构建缓存", "下次构建会明显变慢。", "安全", RowTone.Ok, + new RelayCommand(_ => PruneBuildCacheAsync())), + new("Docker.broom", "以上全部", "等价于 system prune -a,不含卷。", "危险", RowTone.Danger, + new RelayCommand(_ => PruneAllAsync(withVolumes: false))), + new("Docker.shield-alert", "以上全部 + 卷", "会删除未被任何容器使用的卷 —— 数据不可恢复,需手打 delete。", "会丢数据", RowTone.Danger, + new RelayCommand(_ => PruneAllAsync(withVolumes: true))) + ]; + } + + /// + public override PanelPage Page => PanelPage.System; + + /// + public override string Title => "系统"; + + /// 顶部四张统计卡。 + public ObservableCollection Stats { get; } = []; + + private int _networkCount; + + /// 磁盘占用明细。 + public ObservableCollection DiskRows { get; } = []; + + /// 堆叠占用条。 + public ObservableCollection Segments { get; } = []; + + /// 回收卡片。 + public ObservableCollection PruneCards { get; } + + /// 引擎信息(左列)。 + public ObservableCollection EngineLeft { get; } = []; + + /// 引擎信息(右列)。 + public ObservableCollection EngineRight { get; } = []; + + /// 总可回收。 + public string ReclaimableText { get; private set; } = "—"; + + /// 磁盘占用小字。 + public string DiskSubtitle { get; private set; } = ""; + + /// 刷新。 + public RelayCommand RefreshCommand { get; } + + /// + public override async Task RefreshAsync(CancellationToken cancellationToken) + { + if (Client is not { } client) + { + return; + } + Busy = true; + try + { + // 这三条一起发:df 在镜像多的机器上要几秒,而它是这一页的主角, + // 不该等 info 与 version 串行跑完才开始。 + var usageTask = client.DiskUsageAsync(cancellationToken); + var infoTask = client.InfoAsync(cancellationToken); + var versionTask = client.VersionAsync(cancellationToken); + await Task.WhenAll(usageTask, infoTask, versionTask).ConfigureAwait(true); + _usage = usageTask.Result; + _info = infoTask.Result; + _version = versionTask.Result; + // 网络数量 df 不给。它便宜,顺手问一次;失败就当 0,不该拖垮这一页。 + try + { + _networkCount = (await client.ListNetworksAsync(cancellationToken).ConfigureAwait(true)).Length; + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + _networkCount = 0; + } + BuildStats(); + BuildDisk(); + BuildEngine(); + LoadedOnce = true; + // 宿主磁盘总量 Docker API 不给,只能另走一次 df。放在最后、失败不影响这一页。 + await LoadHostDiskAsync(client, cancellationToken).ConfigureAwait(true); + } + finally + { + Busy = false; + } + } + + /// + public override void Reset() + { + _usage = null; + _info = null; + _version = null; + Stats.Clear(); + DiskRows.Clear(); + Segments.Clear(); + EngineLeft.Clear(); + EngineRight.Clear(); + LoadedOnce = false; + } + + /// + /// 系统页刻意跟事件走。/system/df 要 daemon 把每个卷 du 一遍, + /// 在镜像多的机器上是好几秒 —— 挂在事件上等于每起一个容器就让远端算一遍磁盘。 + /// + public override bool WantsRefresh(DockerEvent dockerEvent) => false; + + private void BuildStats() + { + Stats.Clear(); + if (_info is not { } info) + { + return; + } + // 每张卡的第三行都是"这个数字的分母或去向" —— 光看「镜像 34」说明不了任何事, + // 「其中 14 个没人用」才是让人动手的那一句。 + var volumes = _usage?.Volumes?.Length ?? 0; + var volumesIdle = _usage?.Volumes?.Count(v => v.UsageData is not { RefCount: > 0 }) ?? 0; + var imagesDangling = _usage?.Images?.Count(i => i.IsDangling) ?? 0; + var cacheRecords = _usage?.BuildCache?.Length ?? 0; + Stats.Add(new("Docker.box", "容器", info.Containers.ToString(), + $"运行中 {info.ContainersRunning}", + info.ContainersRunning > 0 ? RowTone.Ok : RowTone.Idle)); + Stats.Add(new("Icon.layers", "镜像", info.Images.ToString(), + imagesDangling > 0 ? $"悬空 {imagesDangling}" : "没有悬空镜像", + imagesDangling > 0 ? RowTone.Warn : RowTone.Idle)); + Stats.Add(new("Icon.hard-drive", "卷", volumes.ToString(), + volumesIdle > 0 ? $"没人用 {volumesIdle}" : "都在使用中", + volumesIdle > 0 ? RowTone.Warn : RowTone.Idle)); + Stats.Add(new("Icon.network", "网络", _networkCount.ToString(), "含内置的三个")); + Stats.Add(new("Docker.database", "构建缓存", Humanize.Bytes(_buildCacheBytes), + $"{cacheRecords} 条记录", _buildCacheBytes > 0 ? RowTone.Warn : RowTone.Idle)); + } + + /// + /// 问一次宿主磁盘的总量与可用量。 + /// + /// Engine API 没有这个 —— system df 只讲 Docker 自己占了多少, + /// 不讲那块盘还剩多少。但"还剩 3 GB"才是决定要不要清理的那个数,所以另走一次 + /// df,查的是 Docker Root Dir 所在的那块盘(不是 /,它们常常不是同一块)。 + /// + /// 失败就不显示,不报错:它是锦上添花,不该让这一页看起来出了问题。 + /// + private async Task LoadHostDiskAsync(DockerClient client, CancellationToken cancellationToken) + { + var root = _info?.DockerRootDir is { Length: > 0 } dir ? dir : "/var/lib/docker"; + try + { + var result = await client + .ExecCaptureAsync(FirstRunningContainerId ?? "", ["/bin/sh", "-c", $"df -PB1 {Sh.Quote(root)}"], + cancellationToken: cancellationToken) + .ConfigureAwait(true); + if (result.ExitCode != 0 || ParseDf(result.StandardOutput) is not var (total, used)) + { + return; + } + HostDiskText = $"宿主盘 {Humanize.Bytes(total)} 中已用 {Humanize.Bytes(used)}"; + OnPropertyChanged(nameof(HostDiskText)); + } + catch (Exception) + { + // 没有可借的容器、镜像里没有 df、路径不存在 —— 三种都很常见,都不值得报。 + } + } + + /// 借一个正在跑的容器来执行 df —— 面板没有"在宿主上执行命令"的通道。 + private string? FirstRunningContainerId => + Shell.Containers.View.FirstOrDefault(r => r.IsRunning)?.Id; + + /// 宿主磁盘那一句;拿不到时为空。 + public string HostDiskText { get; private set; } = ""; + + private static readonly string[] request = ["POST /volumes/prune"]; + + /// 解一行 df -PB1。返回 (总量, 已用)。 + internal static (long Total, long Used)? ParseDf(string output) + { + foreach (var line in output.Split('\n').Skip(1)) + { + var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); + // Filesystem 1-blocks Used Available Capacity Mounted + if (parts.Length >= 4 && long.TryParse(parts[1], out var total) && long.TryParse(parts[2], out var used)) + { + return (total, used); + } + } + return null; + } + + private void BuildDisk() + { + DiskRows.Clear(); + Segments.Clear(); + if (_usage is not { } usage) + { + return; + } + var reclaim = DiskMath.Reclaimable(usage); + var imageTotal = usage.Images?.Sum(i => i.Size) ?? 0; + var imageReclaim = reclaim.Images; + // 可写层大小只有 /system/df 会给(/containers/json 要带 size=1 才算,那对每个容器做一次 + // diff,贵得多)—— 这一页正好是 df,所以这里能拿到真值。 + var containerTotal = usage.Containers?.Sum(c => c.SizeRw) ?? 0; + // 「已停止的容器」能回收的是**已停止那些**的可写层,不是全部容器的。 + var containerReclaim = usage.Containers?.Where(c => c.State != "running").Sum(c => c.SizeRw) ?? 0; + var volumeTotal = usage.Volumes?.Sum(v => v.UsageData is { Size: > 0 } u ? u.Size : 0) ?? 0; + var volumeReclaim = reclaim.Volumes; + _buildCacheBytes = reclaim.BuildCache; + + DiskRows.Add(new("镜像", (usage.Images?.Length ?? 0).ToString(), + (usage.Images?.Count(i => i.Containers > 0) ?? 0).ToString(), + Humanize.Bytes(usage.LayersSize > 0 ? usage.LayersSize : imageTotal), + Ratio(imageReclaim, imageTotal), imageReclaim > 0 ? RowTone.Warn : RowTone.Idle)); + DiskRows.Add(new("容器", (usage.Containers?.Length ?? 0).ToString(), + (usage.Containers?.Count(c => c.State == "running") ?? 0).ToString(), + Humanize.Bytes(containerTotal), Ratio(containerReclaim, containerTotal), + containerReclaim > 0 ? RowTone.Warn : RowTone.Idle)); + DiskRows.Add(new("本地卷", (usage.Volumes?.Length ?? 0).ToString(), + (usage.Volumes?.Count(v => v.UsageData is { RefCount: > 0 }) ?? 0).ToString(), + Humanize.Bytes(volumeTotal), Ratio(volumeReclaim, volumeTotal), + volumeReclaim > 0 ? RowTone.Warn : RowTone.Idle)); + DiskRows.Add(new("构建缓存", (usage.BuildCache?.Length ?? 0).ToString(), "0", + Humanize.Bytes(usage.BuildCache?.Sum(c => c.Size) ?? 0), + Ratio(_buildCacheBytes, usage.BuildCache?.Sum(c => c.Size) ?? 0), + _buildCacheBytes > 0 ? RowTone.Danger : RowTone.Idle)); + + var total = imageTotal + containerTotal + volumeTotal + _buildCacheBytes; + if (total > 0) + { + // 权重归一化成 0–1:界面用星形列宽画堆叠条,那条路要的是比例而不是字节。 + Segments.Add(new($"镜像 {Humanize.Bytes(imageTotal)}", imageTotal, (double)imageTotal / total, "VelaAccent")); + Segments.Add(new($"容器可写层 {Humanize.Bytes(containerTotal)}", containerTotal, + (double)containerTotal / total, "VelaInfo")); + Segments.Add(new($"卷 {Humanize.Bytes(volumeTotal)}", volumeTotal, (double)volumeTotal / total, + "VelaStatusConnected")); + Segments.Add(new($"构建缓存 {Humanize.Bytes(_buildCacheBytes)}", _buildCacheBytes, + (double)_buildCacheBytes / total, "VelaWarning")); + } + ReclaimableText = Humanize.Bytes(reclaim.Total); + DiskSubtitle = $"docker system df -v · 共 {Humanize.Bytes(total)} · 可回收 {ReclaimableText}"; + // df 是这一页跑一次就有的东西,顺手喂给总览那张卡 —— + // 让用户为了看一眼"能清多少"而必须先来系统页,是没道理的。 + Shell.Overview.AcceptReclaim(reclaim); + foreach (var card in PruneCards) + { + card.SizeText = card.Title switch + { + "已停止的容器" => Humanize.Bytes(containerReclaim), + "悬空镜像" => Humanize.Bytes(usage.Images?.Where(i => i.IsDangling).Sum(i => i.Size) ?? 0), + "全部未使用镜像" => Humanize.Bytes(imageReclaim), + "构建缓存" => Humanize.Bytes(_buildCacheBytes), + "以上全部" => Humanize.Bytes(imageReclaim + containerReclaim + _buildCacheBytes), + _ => $"{Humanize.Bytes(imageReclaim + containerReclaim + _buildCacheBytes)} + {Humanize.Bytes(volumeReclaim)}" + }; + } + OnPropertiesChanged(nameof(ReclaimableText), nameof(DiskSubtitle)); + BuildStats(); + } + + private static string Ratio(long part, long whole) => + whole <= 0 ? "—" : $"{Humanize.Bytes(part)} ({part * 100 / whole}%)"; + + private void BuildEngine() + { + EngineLeft.Clear(); + EngineRight.Clear(); + if (_version is { } version) + { + EngineLeft.Add(new("Engine 版本", $"{version.Version} ({version.GitCommit})")); + EngineLeft.Add(new("API 版本", $"{version.ApiVersion}(最低 {version.MinAPIVersion})")); + } + if (_info is { } info) + { + EngineLeft.Add(new("操作系统", $"{info.OperatingSystem} · {info.KernelVersion}")); + EngineLeft.Add(new("架构 / CPU", $"{info.Architecture} · {info.NCPU} 核 · {Humanize.Bytes(info.MemTotal)}")); + EngineRight.Add(new("存储驱动", info.Driver ?? "—")); + EngineRight.Add(new("Cgroup", $"{info.CgroupVersion} · {info.CgroupDriver}")); + EngineRight.Add(new("日志驱动", info.LoggingDriver ?? "—")); + EngineRight.Add(new("Swarm", info.Swarm?.LocalNodeState ?? "inactive")); + EngineRight.Add(new("Docker 根目录", info.DockerRootDir ?? "—")); + } + } + + private async Task PruneContainersAsync() + { + if (Client is not { } client) + { + return; + } + var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + { + Title = "清理已停止的容器?", + Icon = "Icon.trash-2", + HostName = "", + ConfirmLabel = "开始清理", + ConfirmIcon = "Docker.broom", + Commands = ["POST /containers/prune"], + CommandNote = "等价于 docker container prune", + Consequences = + [ + new(1, "只删 exited 状态的容器,运行中的不受影响。"), + new(2, "写在容器可写层里(不在卷里)的数据会随之丢失。"), + new(0, "compose 项目的容器删掉后,下次 up -d 会重建。") + ] + })).ConfigureAwait(true); + if (!confirmed) + { + return; + } + await RunPruneAsync("清理已停止容器", () => client.PruneContainersAsync(Shell.Lifetime)).ConfigureAwait(true); + } + + private async Task PruneBuildCacheAsync() + { + if (Client is not { } client) + { + return; + } + var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + { + Title = "清理构建缓存?", + Icon = "Docker.database", + HostName = "", + ConfirmLabel = "开始清理", + ConfirmIcon = "Docker.broom", + Commands = ["POST /build/prune"], + CommandNote = "等价于 docker builder prune", + Consequences = + [ + new(1, "不丢数据 —— 缓存只是加速用的。"), + new(2, "下次构建会明显变慢,每一层都要重新算。") + ] + })).ConfigureAwait(true); + if (!confirmed) + { + return; + } + await RunPruneAsync("清理构建缓存", () => client.PruneBuildCacheAsync(false, Shell.Lifetime)).ConfigureAwait(true); + } + + private async Task PruneAllAsync(bool withVolumes) + { + if (Client is not { } client) + { + return; + } + var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + { + Title = withVolumes ? "清理以上全部,并删除未使用的卷?" : "清理以上全部?", + Icon = withVolumes ? "Docker.shield-alert" : "Docker.broom", + Tier = withVolumes ? ConfirmTier.DataLoss : ConfirmTier.Destructive, + ConfirmWord = "delete", + ConfirmLabel = withVolumes ? "全部清理(含卷)" : "全部清理", + ConfirmIcon = "Docker.broom", + HostName = "", + Commands = + [ + "POST /containers/prune", + "POST /images/prune?filters={\"dangling\":[\"false\"]}", + "POST /networks/prune", + "POST /build/prune", + .. withVolumes ? request : [] + ], + CommandNote = $"等价于 docker system prune -a{(withVolumes ? " --volumes" : "")}", + Consequences = withVolumes + ? [] + : + [ + new(2, "会删掉全部未被容器使用的镜像 —— 重新拉要花时间与带宽。"), + new(1, "不含卷:数据卷不受影响。"), + new(0, $"预计回收 {ReclaimableText}。") + ], + DataLossHeadline = withVolumes ? "未被任何容器使用的卷会被删除,里面的数据永久丢失" : null, + DataLossPoints = withVolumes + ? + [ + "\"未使用\"只是说没有容器「现在」挂着它 —— 一个刚 down 掉的项目,它的数据卷就在名单里。", + "Docker 不做回收站,也没有快照。", + "只想回收镜像与缓存的话,用不带卷的那一档。" + ] + : [] + })).ConfigureAwait(true); + if (!confirmed) + { + return; + } + await RunPruneAsync(withVolumes ? "全部清理(含卷)" : "全部清理", async () => + { + var containers = await client.PruneContainersAsync(Shell.Lifetime).ConfigureAwait(false); + var images = await client.PruneImagesAsync(false, Shell.Lifetime).ConfigureAwait(false); + var networks = await client.PruneNetworksAsync(Shell.Lifetime).ConfigureAwait(false); + var cache = await client.PruneBuildCacheAsync(true, Shell.Lifetime).ConfigureAwait(false); + var volumes = withVolumes + ? await client.PruneVolumesAsync(Shell.Lifetime).ConfigureAwait(false) + : new PruneReport(); + return new PruneReport + { + SpaceReclaimed = containers.SpaceReclaimed + images.SpaceReclaimed + networks.SpaceReclaimed + + cache.SpaceReclaimed + volumes.SpaceReclaimed, + ContainersDeleted = containers.ContainersDeleted, + ImagesDeleted = images.ImagesDeleted, + NetworksDeleted = networks.NetworksDeleted, + CachesDeleted = cache.CachesDeleted, + VolumesDeleted = volumes.VolumesDeleted + }; + }).ConfigureAwait(true); + } + + private async Task RunPruneAsync(string title, Func> action) + { + var task = Shell.Tasks.Start("Docker.broom", title, indeterminate: true); + try + { + var report = await action().ConfigureAwait(true); + task.Finish(PanelTaskState.Succeeded, "完成", + $"删除 {report.DeletedCount} 项 · 回收 {Humanize.Bytes(report.SpaceReclaimed)}"); + Shell.Feedback.Notify(FeedbackKind.Success, $"{title} 完成", + $"删除 {report.DeletedCount} 项 · 回收 {Humanize.Bytes(report.SpaceReclaimed)}"); + await RefreshAsync(Shell.Lifetime).ConfigureAwait(true); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + task.Finish(PanelTaskState.Failed, "失败", ex.Message); + Shell.Feedback.ReportError(title, ex); + } + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/VolumesPageView.axaml b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/VolumesPageView.axaml new file mode 100644 index 0000000..a07f4e1 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/VolumesPageView.axaml @@ -0,0 +1,367 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/VolumesPageView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/VolumesPageView.axaml.cs new file mode 100644 index 0000000..740b6c2 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/VolumesPageView.axaml.cs @@ -0,0 +1,21 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace VelaShell.Plugin.DockerPanel.Ui.Pages; + +/// VolumesPageView 的视图。 +public sealed partial class VolumesPageView : UserControl +{ + // 列宽拖拽与抽屉布局都是四个页面共用的通用件,这里只交代这一页的一个数: + // 列宽之外的固定占位(名称列左边 12 的留白 + 行尾动作 104)。 + private readonly ColumnResizer _columns; + private readonly DrawerLayout _drawer; + + /// 建视图。 + public VolumesPageView() + { + AvaloniaXamlLoader.Load(this); + _columns = new(this, "HeaderGrid", chrome: 12 + 104 + 8); + _drawer = new(this, "Root", "DrawerSplitter"); + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/VolumesPageViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/VolumesPageViewModel.cs new file mode 100644 index 0000000..69132a5 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/VolumesPageViewModel.cs @@ -0,0 +1,619 @@ +using Avalonia.Controls; +using System.Collections.ObjectModel; +using VelaShell.Plugin.DockerPanel.Docker; + +namespace VelaShell.Plugin.DockerPanel.Ui.Pages; + +/// 卷页。 +public sealed class VolumesPageViewModel : PageViewModel +{ + private readonly List _all = []; + private readonly Dictionary> _users = []; + + /// 建卷页。 + public VolumesPageViewModel(DockerPanelViewModel shell) : base(shell) + { + SelectCommand = new RelayCommand(p => Selected = p as VolumeRow); + CreateCommand = new RelayCommand(_ => CreateAsync()); + RemoveCommand = new RelayCommand(p => p is VolumeRow row ? RemoveAsync(row) : Task.CompletedTask); + PruneCommand = new RelayCommand(_ => PruneAsync()); + BrowseCommand = new RelayCommand(p => p is VolumeRow row ? BrowseAsync(row) : Task.CompletedTask); + BackupCommand = new RelayCommand(p => p is VolumeRow backup ? BackupAsync(backup) : Task.CompletedTask); + RefreshCommand = new RelayCommand(_ => RefreshAsync(Shell.Lifetime)); + ToggleUnusedCommand = new RelayCommand(_ => + { + UnusedOnly = !UnusedOnly; + return Task.CompletedTask; + }); + } + + /// + public override PanelPage Page => PanelPage.Volumes; + + /// + public override string Title => "卷"; + + /// 过滤后的行。 + public KeyedCollection View { get; } = new(r => r.Id); + + /// 搜索词。 + public string Search + { + get; + set + { + if (SetField(ref field, value)) + { + ApplyView(); + } + } + } = ""; + + /// 只看未使用的。 + public bool UnusedOnly + { + get; + set + { + if (SetField(ref field, value)) + { + ApplyView(); + } + } + } + + /// 总数。 + public int TotalCount => _all.Count; + + /// 使用中的数量。 + public int UsedCount => _all.Count(r => r.RefCount > 0); + + /// 未使用的数量。 + public int UnusedCount => _all.Count(r => r.RefCount == 0); + + /// 总占用文本。 + public string TotalSizeText + { + get + { + var total = _all.Sum(r => r.Summary.UsageData is { Size: > 0 } u ? u.Size : 0); + return total > 0 ? Humanize.Bytes(total) : "未统计"; + } + } + + /// 列表空了。 + public bool IsEmpty => LoadedOnce && _all.Count == 0; + + /// 筛完之后没有匹配项 —— 与"这台机器上没有卷"是两回事,得分开说。 + public bool NoMatch => LoadedOnce && _all.Count > 0 && View.Count == 0; + + /// 当前选中的卷(右侧详情)。 + public VolumeRow? Selected + { + get; + private set + { + if (SetField(ref field, value)) + { + BuildSelectedDetails(value); + Drawer.IsOpen = value is not null; + OnPropertiesChanged(nameof(HasSelection), nameof(SelectedUsers), nameof(CanBrowse), nameof(CanBackup), nameof(BrowseHint)); + } + } + } + + /// 有选中。 + public bool HasSelection => Selected is not null; + + /// 列表的列宽。列头与数据行共用这一份 —— 拖列头的轨道改的就是它。 + public VolumeColumns Columns { get; } = new(); + + /// + public override ListColumns ColumnLayout => Columns; + + /// + public override IEnumerable ColumnTexts(string key) => key switch + { + "name" => View.Select(r => r.Name), + "driver" => View.Select(r => r.Driver), + "users" => View.Select(r => r.UsageText), + "size" => View.Select(r => r.SizeText), + "created" => View.Select(r => r.CreatedText), + _ => [] + }; + + /// 选中卷的标签。 + public ObservableCollection SelectedLabels { get; } = []; + + /// 选中卷的驱动选项。 + public ObservableCollection SelectedOptions { get; } = []; + + /// 选中卷的使用者。 + public IReadOnlyList SelectedUsers => + Selected is { } row && _users.TryGetValue(row.Name, out var users) ? users : []; + + /// + /// 能不能浏览卷内文件。 + /// + /// Engine API 没有"读一个卷"的端点,而为了浏览去拉一个 alpine 起临时容器, + /// 代价与副作用都不该由一次"看一眼"来承担。所以面板走一条更省的路: + /// 借一个已经挂着这个卷的运行中容器去 exec —— 有就能看,没有就明说。 + /// + /// + public bool CanBrowse => SelectedUsers.Count > 0; + + /// 不能浏览时的说明。 + public string BrowseHint => CanBrowse + ? "经一个挂着它的运行中容器浏览" + : "没有运行中的容器挂着它 —— 先起一个挂载了这个卷的容器再来看。"; + + /// 选中一行。 + public RelayCommand SelectCommand { get; } + + /// 关掉右侧详情。 + public RelayCommand ClearSelectionCommand => field ??= new(_ => + { + Selected = null; + return Task.CompletedTask; + }); + + /// 新建卷。 + public RelayCommand CreateCommand { get; } + + /// 删除卷。 + public RelayCommand RemoveCommand { get; } + + /// 清理未使用的卷。 + public RelayCommand PruneCommand { get; } + + /// 浏览卷内文件。 + public RelayCommand BrowseCommand { get; } + + /// 把卷内容备份成本地 tar。 + public RelayCommand BackupCommand { get; } + + /// 能不能备份(和浏览同一个前提:得有容器挂着它)。 + public bool CanBackup => CanBrowse && FilePicker.IsAvailable; + + /// 刷新。 + public RelayCommand RefreshCommand { get; } + + /// 切换"只看未使用"。 + public RelayCommand ToggleUnusedCommand { get; } + + /// + public override async Task RefreshAsync(CancellationToken cancellationToken) + { + if (Client is not { } client) + { + return; + } + Busy = true; + try + { + var volumes = await client.ListVolumesAsync(cancellationToken).ConfigureAwait(true); + // 引用计数得自己算:/volumes 不带 UsageData,只有 /system/df 才带, + // 而后者在镜像多的机器上要几秒 —— 不值得为一列数字每次都付这个钱。 + var containers = await client.ListContainersAsync(true, cancellationToken).ConfigureAwait(true); + _users.Clear(); + foreach (var container in containers) + { + foreach (var mount in container.Mounts ?? []) + { + if (mount.Type != "volume" || mount.Name is not { Length: > 0 } name) + { + continue; + } + if (!_users.TryGetValue(name, out var list)) + { + _users[name] = list = []; + } + list.Add($"{container.Name} → {mount.Destination}{(mount.RW ? "" : " (ro)")}"); + } + } + List incoming = + [ + .. volumes + .OrderBy(v => v.Name, StringComparer.OrdinalIgnoreCase) + .Select(v => new VolumeRow(v, _users.TryGetValue(v.Name, out var u) ? u.Count : 0)) + ]; + var previous = _all.ToDictionary(r => r.Id); + _all.Clear(); + foreach (var row in incoming) + { + if (previous.TryGetValue(row.Id, out var existing)) + { + existing.Update(row); + _all.Add(existing); + } + else + { + // 行要回指页面:列宽绑在页面上,行模板得找得到它。 + row.Owner = this; + _all.Add(row); + } + } + LoadedOnce = true; + Shell.SetVolumeCount(_all.Count); + ApplyView(); + if (Selected is { } selected && _all.All(r => r.Id != selected.Id)) + { + Selected = null; + } + OnPropertiesChanged(nameof(TotalCount), nameof(UsedCount), nameof(UnusedCount), nameof(TotalSizeText), + nameof(SelectedUsers), nameof(CanBrowse), nameof(CanBackup), nameof(BrowseHint)); + } + finally + { + Busy = false; + } + } + + /// + public override void Reset() + { + _all.Clear(); + _users.Clear(); + View.Clear(); + Selected = null; + LoadedOnce = false; + OnPropertiesChanged(nameof(TotalCount), nameof(UsedCount), nameof(UnusedCount), nameof(IsEmpty)); + } + + /// + public override bool WantsRefresh(DockerEvent dockerEvent) => + dockerEvent.Type is "volume" or "container"; + + private void ApplyView() + { + var needle = Search.Trim(); + IEnumerable filtered = _all; + if (UnusedOnly) + { + filtered = filtered.Where(r => r.RefCount == 0); + } + if (needle.Length > 0) + { + filtered = filtered.Where(r => + r.Name.Contains(needle, StringComparison.OrdinalIgnoreCase) || + r.Project.Contains(needle, StringComparison.OrdinalIgnoreCase)); + } + View.Merge([.. filtered], (_, _) => { }); + OnPropertiesChanged(nameof(IsEmpty), nameof(NoMatch), nameof(UnusedCount), nameof(UsedCount)); + } + + private async Task CreateAsync() + { + if (Client is not { } client) + { + return; + } + var form = new CreateVolumeForm(); + if (!await Shell.ShowFormAsync(form).ConfigureAwait(true)) + { + return; + } + try + { + await client.CreateVolumeAsync(form.Name, form.Driver, form.DriverOptions, form.LabelMap, Shell.Lifetime) + .ConfigureAwait(true); + Shell.Feedback.Status(FeedbackKind.Success, $"已新建卷 {form.Name}"); + await RefreshAsync(Shell.Lifetime).ConfigureAwait(true); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Shell.Feedback.ReportError("新建卷", ex); + } + } + + private async Task RemoveAsync(VolumeRow row) + { + if (Client is not { } client) + { + return; + } + var users = _users.TryGetValue(row.Name, out var list) ? list : []; + var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + { + Title = $"删除卷 {row.Name}?", + Icon = "Docker.shield-alert", + Tier = ConfirmTier.DataLoss, + ConfirmWord = "delete", + ConfirmLabel = "永久删除卷", + HostName = "", + Commands = [$"DELETE /volumes/{row.Name}"], + CommandNote = $"等价于 docker volume rm {row.Name}", + DataLossHeadline = row.SizeText == "—" + ? "卷里的数据将被永久删除,无法撤销" + : $"{row.SizeText} 数据将被永久删除,无法撤销", + DataLossPoints = + [ + "Docker 不做回收站,也没有快照 —— 删了就是删了。", + users.Count > 0 + ? $"仍有 {users.Count} 个容器引用它:{string.Join('、', users.Take(3))}" + : "当前没有容器引用它。", + "本面板没有找到这个卷的备份记录。" + ], + // 只在真备得了的时候才给这个勾选 —— 一个点了没反应的复选框比没有更糟。 + PrecautionLabel = CanBackupRow(row) ? "先备份为 tar 再删除" : null, + PrecautionDefault = true + })).ConfigureAwait(true); + if (!confirmed) + { + return; + } + if (Shell.Confirm.HasPrecaution && Shell.Confirm.Precaution) + { + // 备份失败就**不删** —— 用户勾这个框的意思正是"没有备份就别删"。 + if (!await BackupAsync(row).ConfigureAwait(true)) + { + Shell.Feedback.Notify(FeedbackKind.Warning, "没有删除", "备份没成功,卷保留原样。"); + return; + } + } + try + { + await client.RemoveVolumeAsync(row.Name, false, Shell.Lifetime).ConfigureAwait(true); + Shell.Feedback.Notify(FeedbackKind.Success, "卷已删除", row.Name); + await RefreshAsync(Shell.Lifetime).ConfigureAwait(true); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + // 409 的真实含义是"还有容器挂着它",而挂着它的是谁,这一页早就知道 —— + // 把这句话直接说出来,并给一条过去看的路。 + ToastAction[] actions = ex is DockerApiException { IsConflict: true } && _users.TryGetValue(row.Name, out var holders) && holders.Count > 0 + ? [new($"看看是谁在占({holders.Count} 个容器)", () => Selected = row)] + : []; + Shell.Feedback.ReportError("删除卷", ex, actions); + } + } + + private async Task PruneAsync() + { + if (Client is not { } client) + { + return; + } + var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + { + Title = "清理全部未使用的卷?", + Icon = "Docker.shield-alert", + Tier = ConfirmTier.DataLoss, + ConfirmWord = "delete", + ConfirmLabel = "清理未使用的卷", + HostName = "", + Commands = ["POST /volumes/prune"], + CommandNote = "等价于 docker volume prune", + DataLossHeadline = $"当前有 {UnusedCount} 个未使用的卷,它们里面的数据会被永久删除", + DataLossPoints = + [ + "\"未使用\"只是说没有容器「现在」挂着它 —— 一个刚 down 掉的 compose 项目,它的数据卷就在这个名单里。", + "Docker 不做回收站。", + .. _all.Where(r => r.RefCount == 0).Take(5).Select(r => $"将被删除:{r.Name}({r.SizeText})") + ] + })).ConfigureAwait(true); + if (!confirmed) + { + return; + } + try + { + var report = await client.PruneVolumesAsync(Shell.Lifetime).ConfigureAwait(true); + Shell.Feedback.Notify(FeedbackKind.Success, "清理完成", + $"删除 {report.DeletedCount} 个卷 · 回收 {Humanize.Bytes(report.SpaceReclaimed)}"); + await RefreshAsync(Shell.Lifetime).ConfigureAwait(true); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Shell.Feedback.ReportError("清理卷", ex); + } + } + + private async Task BrowseAsync(VolumeRow row) + { + if (await FindMountAsync(row.Name).ConfigureAwait(true) is not { } mount) + { + Shell.Feedback.Notify(FeedbackKind.Warning, "没法浏览这个卷", BrowseHint); + return; + } + (var container, var destination) = mount; + await Shell.GoToAsync(PanelPage.Containers).ConfigureAwait(true); + var target = Shell.Containers.View.FirstOrDefault(r => r.Id == container.Id); + if (target is null) + { + return; + } + // 直接落到挂载点。让用户从 / 一级一级点到 /var/lib/postgresql/data, + // 是把面板已经知道的答案又要了一遍。 + await Shell.Containers.OpenDetailAtFilesAsync(target, destination).ConfigureAwait(true); + Shell.Feedback.Status(FeedbackKind.Info, + $"经容器 {container.Name} 浏览卷 {row.Name} —— 它挂在 {destination}"); + } + + /// + /// 把卷的内容整包取到本地 tar。 + /// + /// 和浏览走的是同一条路 —— 借一个已经挂着它的运行中容器,从那个容器的 + /// /archive 端点把挂载点整个取走。Engine API 没有"读一个卷"的端点, + /// 而为了备份去拉一个 alpine 起临时容器,是在用户没要求的情况下动他的机器。 + /// + /// + /// 这一行能不能备份(有运行中的容器挂着它,且能弹文件对话框)。 + private bool CanBackupRow(VolumeRow row) => + FilePicker.IsAvailable && _users.TryGetValue(row.Name, out var users) && users.Count > 0; + + /// 备份;返回是否真的存下来了(删卷前的那个勾选靠它决定要不要继续)。 + private async Task BackupAsync(VolumeRow row) + { + if (Client is not { } client) + { + return false; + } + if (await FindMountAsync(row.Name).ConfigureAwait(true) is not { } mount) + { + Shell.Feedback.Notify(FeedbackKind.Warning, "没法备份这个卷", + "没有运行中的容器挂着它 —— 先起一个挂载了这个卷的容器再来备份。"); + return false; + } + (var container, var destination) = mount; + var target = await FilePicker + .PickSaveAsync($"把卷 {row.Name} 存成 tar", $"{row.Name}.tar", "tar") + .ConfigureAwait(true); + if (target is null) + { + return false; + } + Busy = true; + try + { + await using var archive = await client + .DownloadArchiveAsync(container.Id, destination, Shell.Lifetime).ConfigureAwait(true); + await using var output = await target.OpenWriteAsync().ConfigureAwait(true); + await archive.CopyToAsync(output, Shell.Lifetime).ConfigureAwait(true); + Shell.Feedback.Notify(FeedbackKind.Success, "卷已备份", + $"{target.Name} · 经容器 {container.Name} 的 {destination}"); + return true; + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Shell.Feedback.ReportError("备份卷", ex); + return false; + } + finally + { + Busy = false; + } + } + + /// + /// 把选中卷的标签与驱动选项摊平。 + /// + /// compose 起的卷会带上 com.docker.compose.* 那几条标签 —— + /// 它们是"这个卷归谁管"的唯一线索,删卷之前值得看一眼。 + /// + /// + private void BuildSelectedDetails(VolumeRow? row) + { + SelectedLabels.Clear(); + SelectedOptions.Clear(); + if (row is null) + { + return; + } + foreach ((var key, var value) in row.Summary.Labels ?? []) + { + SelectedLabels.Add(new(key, value)); + } + foreach ((var key, var value) in row.Summary.Options ?? []) + { + SelectedOptions.Add(new(key, value)); + } + } + + /// 找一个正在跑、并且挂着这个卷的容器,连同它的挂载点一起给出来。 + private async Task<(ContainerSummary Container, string Destination)?> FindMountAsync(string volumeName) + { + if (Client is not { } client) + { + return null; + } + var containers = await client.ListContainersAsync(false, Shell.Lifetime).ConfigureAwait(true); + foreach (var container in containers) + { + var mount = (container.Mounts ?? []) + .FirstOrDefault(m => m.Type == "volume" && m.Name == volumeName); + if (mount?.Destination is { Length: > 0 } destination) + { + return (container, destination); + } + } + return null; + } +} + +/// +/// 卷列表的列宽。默认宽度取自设计稿 07 号板的表头 +/// (名称 336 / 驱动 90 / 使用者 180 / 大小 96 / 创建 112)。 +/// +public sealed class VolumeColumns : ListColumns +{ + + /// + public override IReadOnlyList Keys { get; } = ["name", "driver", "users", "size", "created"]; + + /// 名称列。 + public GridLength Name + { + get; + set => SetField(ref field, Clamp(value, "name")); + } = new(336); + + /// 驱动列。 + public GridLength Driver + { + get; + set => SetField(ref field, Clamp(value, "driver")); + } = new(90); + + /// 使用者列。 + public GridLength Users + { + get; + set => SetField(ref field, Clamp(value, "users")); + } = new(180); + + /// 大小列。 + public GridLength Size + { + get; + set => SetField(ref field, Clamp(value, "size")); + } = new(96); + + /// 创建时间列。 + public GridLength Created + { + get; + set => SetField(ref field, Clamp(value, "created")); + } = new(112); + + /// + public override double Get(string key) => key switch + { + "name" => Name.Value, + "driver" => Driver.Value, + "users" => Users.Value, + "size" => Size.Value, + _ => Created.Value + }; + + /// + public override void Set(string key, double width) + { + GridLength value = new(width); + switch (key) + { + case "name": Name = value; break; + case "driver": Driver = value; break; + case "users": Users = value; break; + case "size": Size = value; break; + case "created": Created = value; break; + } + } + + /// + public override double Min(string key) => key switch + { + "name" => 160, + "driver" => 70, + "users" => 90, + "size" => 62, + _ => 70 + }; + + /// + public override double MaxAutoFit(string key) => key is "name" ? 760 : 300; + + /// + // 名称格里还坐着一枚卷图标。 + public override double Padding(string key) => key is "name" ? 46 : 18; +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/PanelForm.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/PanelForm.cs new file mode 100644 index 0000000..22ceb3b --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/PanelForm.cs @@ -0,0 +1,589 @@ +using System.Collections.ObjectModel; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// 表单里的一个字段。 +public abstract class FormField(string label) : ObservableObject +{ + + /// 标签。 + public string Label { get; } = label; + + /// 标签右侧的灰色提示。 + public string? Hint { get; init; } + + /// 字段下方的校验错误;为空表示没问题。 + public string? Error + { + get; + set + { + if (SetField(ref field, value)) + { + OnPropertyChanged(nameof(HasError)); + } + } + } + + /// 有没有校验错误。 + public bool HasError => !string.IsNullOrEmpty(Error); +} + +/// 一行文本。 +public sealed class TextField(string label) : FormField(label) +{ + /// 值。 + public string Value + { + get; + set + { + if (SetField(ref field, value)) + { + Error = null; + Changed?.Invoke(); + } + } + } = ""; + + /// 占位文字。 + public string Placeholder { get; init; } = ""; + + /// 是不是等宽(路径、id、命令用等宽)。 + public bool Mono { get; init; } = true; + + /// 只读(源镜像、当前名称这类"给你看清楚"的字段)。 + public bool ReadOnly { get; init; } + + /// 值变了。 + public event Action? Changed; +} + +/// 一个开关。 +public sealed class ToggleField(string label) : FormField(label) +{ + /// 值。 + public bool Value + { + get; + set + { + if (SetField(ref field, value)) + { + Changed?.Invoke(); + } + } + } + + /// 开关下面那行说明。 + public string Description { get; init; } = ""; + + /// 打开这一项是危险的(特权模式之类)。 + public bool Danger { get; init; } + + /// 值变了。 + public event Action? Changed; +} + +/// +/// 下拉 / 分段 / 单选列表里的一个选项。 +/// +/// 是类而不是 record:选中态要由界面**看得见**,而看得见就意味着这一项自己得会发通知。 +/// 换成"在模板里拿选项的值跟字段的值比一比"也行,但那需要一条多值绑定 —— +/// 把这一位状态放在选项自己身上,模板里就只剩一句 Classes.picked。 +/// +/// +/// 值。 +/// 显示文字。 +/// 补充说明(单选列表才显示)。 +/// 能不能选。 +/// 不能选的原因。 +public sealed class ChoiceOption(string value, string label, string description = "", bool enabled = true, + string disabledReason = "") : ObservableObject +{ + + /// 值。 + public string Value { get; } = value; + + /// 显示文字。 + public string Label { get; } = label; + + /// 补充说明(单选列表才显示)。 + public string Description { get; } = description; + + /// 能不能选。 + public bool Enabled { get; } = enabled; + + /// 不能选的原因。 + public string DisabledReason { get; } = disabledReason; + + /// 选中的是不是这一项。 + public bool Picked + { + get; + internal set => SetField(ref field, value); + } +} + +/// 一组互斥选项(分段控件或下拉)。 +public sealed class ChoiceField : FormField +{ + + /// 建一组互斥选项。 + public ChoiceField(string label) : base(label) + { + SelectCommand = new(p => + { + if (p is ChoiceOption { Enabled: true } option) + { + Value = option.Value; + } + }); + // 选项常常是建好字段之后才一条条加进来的(有些还要看 swarm 开没开), + // 那时候 Value 可能已经设过了 —— 加一条就得跟着把选中态对一遍。 + Options.CollectionChanged += (_, _) => SyncPicked(); + } + + /// 选项。 + public ObservableCollection Options { get; } = []; + + /// 当前值。 + public string Value + { + get; + set + { + if (SetField(ref field, value)) + { + SyncPicked(); + OnPropertyChanged(nameof(SelectedLabel)); + Changed?.Invoke(); + } + } + } = ""; + + private void SyncPicked() + { + foreach (var option in Options) + { + option.Picked = option.Value == Value; + } + } + + /// 当前值对应的显示文字。 + public string SelectedLabel => Options.FirstOrDefault(o => o.Value == Value)?.Label ?? Value; + + /// 用分段控件呈现(选项少时)还是下拉(选项多时)。 + public bool AsSegments { get; init; } + + /// 选中变了。 + public event Action? Changed; + + /// 选一个。 + public RelayCommand SelectCommand { get; } +} + +/// 带说明的单选列表(重启策略那种)。 +public sealed class RadioListField : FormField +{ + + /// 建一个单选列表。 + public RadioListField(string label) : base(label) + { + SelectCommand = new(p => + { + if (p is ChoiceOption { Enabled: true } option) + { + Value = option.Value; + } + }); + Options.CollectionChanged += (_, _) => SyncPicked(); + } + + /// 选项。 + public ObservableCollection Options { get; } = []; + + /// 当前值。 + public string Value + { + get; + set + { + if (SetField(ref field, value)) + { + SyncPicked(); + OnPropertyChanged(nameof(SelectedValue)); + } + } + } = ""; + + private void SyncPicked() + { + foreach (var option in Options) + { + option.Picked = option.Value == Value; + } + } + + /// 当前值(绑定用的别名,方便模板里做相等判断)。 + public string SelectedValue => Value; + + /// 选一个。 + public RelayCommand SelectCommand { get; } +} + +/// 键值行(端口、卷、环境变量、驱动选项共用)。 +public sealed class PairRow(string key, string value) : ObservableObject +{ + /// 左侧。 + public string Key + { + get; + set => SetField(ref field, value); + } = key; + + /// 右侧。 + public string Value + { + get; + set => SetField(ref field, value); + } = value; + + /// 第三格(协议、只读标记);不用时为空。 + public string Extra { get; set; } = ""; +} + +/// 可增删的键值列表。 +public sealed class PairListField : FormField +{ + /// 建一个键值列表。 + public PairListField(string label) : base(label) + { + AddCommand = new(_ => Rows.Add(new("", ""))); + RemoveCommand = new(p => + { + if (p is PairRow row) + { + Rows.Remove(row); + } + }); + // 增删行、以及行里任一格的改动,都要把"等效命令"重算一遍 —— + // 那条预览存在的全部意义就是让用户核对自己填的东西,它一旦滞后就成了误导。 + Rows.CollectionChanged += (_, e) => + { + foreach (var added in e.NewItems?.OfType() ?? []) + { + added.PropertyChanged += OnRowChanged; + } + foreach (var removed in e.OldItems?.OfType() ?? []) + { + removed.PropertyChanged -= OnRowChanged; + } + Changed?.Invoke(); + }; + } + + private void OnRowChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) => Changed?.Invoke(); + + /// + /// 从一段 .env 文本导入(设计稿 06 号板那个「从 .env 导入」)。 + /// + /// 只认最朴素的那一档:KEY=VALUE,跳过空行与 # 注释,剥掉值两边的引号。 + /// 做变量插值与多行值 —— dotenv 的方言各家不同,面板猜错一次就等于 + /// 悄悄改了用户的配置。认不出来的行原样跳过,并把跳过的条数报出来。 + /// + /// + public (int Imported, int Skipped) ImportDotEnv(string text) + { + var imported = 0; + var skipped = 0; + foreach (var raw in text.Replace("\r\n", "\n", StringComparison.Ordinal).Split('\n')) + { + var line = raw.Trim(); + if (line.Length == 0 || line.StartsWith('#')) + { + continue; + } + // export FOO=bar 也很常见。 + if (line.StartsWith("export ", StringComparison.Ordinal)) + { + line = line[7..].TrimStart(); + } + var equals = line.IndexOf('=', StringComparison.Ordinal); + if (equals <= 0) + { + skipped++; + continue; + } + var key = line[..equals].Trim(); + var value = line[(equals + 1)..].Trim(); + if (value.Length >= 2 && ((value[0] == '"' && value[^1] == '"') || (value[0] == '\'' && value[^1] == '\''))) + { + value = value[1..^1]; + } + // 同名覆盖:.env 里后写的赢,这里也一样。 + if (Rows.FirstOrDefault(r => r.Key == key) is { } existing) + { + existing.Value = value; + } + else + { + Rows.Add(new(key, value)); + } + imported++; + } + // 空的占位行留着没意义,导完清一遍。 + foreach (var blank in Rows.Where(r => r.Key.Length == 0 && r.Value.Length == 0).ToList()) + { + Rows.Remove(blank); + } + return (imported, skipped); + } + + /// 任一行增删或改动。 + public event Action? Changed; + + /// 行。 + public ObservableCollection Rows { get; } = []; + + /// 左侧占位。 + public string KeyPlaceholder { get; init; } = ""; + + /// 右侧占位。 + public string ValuePlaceholder { get; init; } = ""; + + /// 两格之间的符号(“→” / “=” / “:”)。 + public string Separator { get; init; } = "="; + + /// “+ 添加”的文字。 + public string AddLabel { get; init; } = "+ 添加"; + + /// 「从 .env 导入」那个按钮的文字;留空表示这一组不提供导入。 + public string ImportLabel { get; init; } = ""; + + /// 这一组支不支持导入。 + public bool CanImport => ImportLabel.Length > 0; + + /// + /// 弹一个文件对话框选 .env 并导入。结果经 报给界面。 + /// + public RelayCommand ImportCommand => field ??= new(async _ => + { + var file = + await FilePicker.PickOpenAsync("选一个 .env 文件").ConfigureAwait(true); + if (file is null) + { + return; + } + await using var stream = await file.OpenReadAsync().ConfigureAwait(true); + using var reader = new StreamReader(stream); + var text = await reader.ReadToEndAsync().ConfigureAwait(true); + (var imported, var skipped) = ImportDotEnv(text); + ImportReport = skipped == 0 + ? $"已导入 {imported} 条" + : $"已导入 {imported} 条 · 跳过 {skipped} 行(不是 KEY=VALUE)"; + OnPropertyChanged(nameof(ImportReport)); + }); + + /// 上一次导入的结果;没导过时为空。 + public string ImportReport { get; private set; } = ""; + + /// 添加一行。 + public RelayCommand AddCommand { get; } + + /// 删掉一行。 + public RelayCommand RemoveCommand { get; } + + /// 非空行(两格都填了才算)。 + public IEnumerable Filled => + Rows.Where(r => r.Key.Trim().Length > 0 && r.Value.Trim().Length > 0); +} + +/// 可多选列表里的一项。 +public sealed class SelectItem(string id, string label, string meta, bool enabled, string disabledReason = "") + : ObservableObject +{ + + /// 标识。 + public string Id { get; } = id; + + /// 显示文字。 + public string Label { get; } = label; + + /// 右侧小字。 + public string Meta { get; } = meta; + + /// 能不能选。 + public bool Enabled { get; } = enabled; + + /// 不能选的原因。 + public string DisabledReason { get; } = disabledReason; + + /// 选中了没有。 + public bool Selected + { + get; + set + { + if (Enabled) + { + SetField(ref field, value); + } + } + } +} + +/// 带搜索的多选列表。 +public sealed class SelectListField(string label) : FormField(label) +{ + + /// 全部项。 + public ObservableCollection Items { get; } = []; + + /// 过滤后的项。 + public ObservableCollection View { get; } = []; + + /// 搜索词。 + public string Search + { + get; + set + { + if (SetField(ref field, value)) + { + ApplyFilter(); + } + } + } = ""; + + /// 搜索框占位。 + public string Placeholder { get; init; } = "过滤…"; + + /// 已选的项。 + public IEnumerable SelectedItems => Items.Where(i => i.Selected); + + /// 重建过滤视图。 + public void ApplyFilter() + { + View.Clear(); + foreach (var item in Items.Where(i => + Search.Length == 0 || i.Label.Contains(Search, StringComparison.OrdinalIgnoreCase))) + { + View.Add(item); + } + } +} + +/// +/// 一个表单弹窗。 +/// +/// 全部复用同一个壳:标题栏 → 主机条 → 内容 → 页脚。主机条一行都不能省 —— +/// 同时开着两台机器时,它是最便宜的保险。 +/// +/// +public abstract class PanelForm : ObservableObject +{ + + /// 标题。 + public abstract string Title { get; } + + /// 标题图标。 + public virtual string Icon => "Icon.settings"; + + /// 确认按钮文字。 + public abstract string ConfirmLabel { get; } + + /// 确认按钮图标。 + public virtual string ConfirmIcon => "Docker.check"; + + /// 页脚左侧的提示。 + public virtual string FooterHint => "Esc 取消"; + + /// 字段。 + public ObservableCollection Fields { get; } = []; + + /// “等效命令”里显示的那条请求。 + public string CommandPreview + { + get; + protected set => SetField(ref field, value); + } = ""; + + /// 请求下面那行等价的命令行。 + public string CommandNote + { + get; + protected set => SetField(ref field, value); + } = ""; + + /// 有没有命令预览。 + public bool HasPreview => CommandPreview.Length > 0; + + /// + /// 表单顶部的一条提醒。 + /// + /// 与 不是一回事:错误说的是"这样填不行", + /// 提醒说的是"这样填可以,但会有一个你未必想要的后果"—— + /// 改一个 compose 管着的容器的名字就属于后者。 + /// + /// + public virtual string Notice => ""; + + /// 有没有提醒。 + public bool HasNotice => Notice.Length > 0; + + /// 整表级别的错误。 + public string? FormError + { + get; + set + { + if (SetField(ref field, value)) + { + OnPropertyChanged(nameof(HasFormError)); + } + } + } + + /// 有没有整表错误。 + public bool HasFormError => !string.IsNullOrEmpty(FormError); + + /// + /// 校验。返回 时把原因写在字段的 + /// 或 上 —— 让用户知道**哪一格**不对,而不是一句"输入有误"。 + /// + public virtual bool Validate() => true; + + /// 字段变动后重算命令预览。 + protected virtual void UpdatePreview() + { + } + + /// 把某个字段的变动接到预览上。 + protected void Watch(TextField field) + { + field.Changed += UpdatePreview; + Fields.Add(field); + } + + /// 把某个开关接到预览上。 + protected void Watch(ToggleField field) + { + field.Changed += UpdatePreview; + Fields.Add(field); + } + + /// 把某个选择接到预览上。 + protected void Watch(ChoiceField field) + { + field.Changed += UpdatePreview; + Fields.Add(field); + } + + /// 把某个键值列表接到预览上。 + protected void Watch(PairListField field) + { + field.Changed += UpdatePreview; + Fields.Add(field); + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/PanelFormView.axaml b/plugins/VelaShell.Plugin.DockerPanel/Ui/PanelFormView.axaml new file mode 100644 index 0000000..1460f84 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/PanelFormView.axaml @@ -0,0 +1,316 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/PanelFormView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/PanelFormView.axaml.cs new file mode 100644 index 0000000..5bb3f38 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/PanelFormView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// 表单弹窗的通用外壳视图。 +public sealed partial class PanelFormView : UserControl +{ + /// 建视图。 + public PanelFormView() => AvaloniaXamlLoader.Load(this); +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/PanelSettings.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/PanelSettings.cs new file mode 100644 index 0000000..a5ad49d --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/PanelSettings.cs @@ -0,0 +1,180 @@ +using VelaShell.PluginSdk.Storage; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// +/// 面板设置。 +/// +/// 分两档存:按主机记忆的(socket 路径 —— 换条会话再连上还是同一套)与 +/// 全局的(密度、日志行数、显示选项)。插件不写任何机密。 +/// +/// +public sealed class PanelSettings(IPluginStorage storage) : ObservableObject +{ + private const string GlobalKey = "panel.settings"; + private const string HostPrefix = "host."; + + private bool _showStopped = true; + private bool _inlineSparklines = true; + private bool _compactRows = true; + private string _logTail = "500"; + private bool _logTimestamps = true; + private bool _logWrap = true; + private bool _autoRefreshFallback = true; + + /// 列表里显示已停止的容器。 + public bool ShowStopped + { + get => _showStopped; + set { if (SetField(ref _showStopped, value)) { Changed?.Invoke(); } } + } + + /// 日志默认行数是不是 100(设置里那组分段按钮用)。 + public bool LogTailIs100 => LogTail == "100"; + + /// 日志默认行数是不是 500。 + public bool LogTailIs500 => LogTail == "500"; + + /// 日志默认行数是不是 2000。 + public bool LogTailIs2000 => LogTail == "2000"; + + /// 日志默认行数是不是「全部」。 + public bool LogTailIsAll => LogTail == "all"; + + /// 行内画 CPU / 内存 sparkline(关掉可省一点远端开销)。 + public bool InlineSparklines + { + get => _inlineSparklines; + set { if (SetField(ref _inlineSparklines, value)) { Changed?.Invoke(); } } + } + + /// 紧凑行高(32px);关掉是 40px。 + public bool CompactRows + { + get => _compactRows; + set + { + if (SetField(ref _compactRows, value)) + { + OnPropertyChanged(nameof(RowHeight)); + Changed?.Invoke(); + } + } + } + + /// 数据行高。 + public double RowHeight => _compactRows ? 32 : 40; + + /// 日志默认补多少行历史。 + public string LogTail + { + get => _logTail; + set { if (SetField(ref _logTail, value)) { Changed?.Invoke(); } } + } + + /// 日志带时间戳。 + public bool LogTimestamps + { + get => _logTimestamps; + set { if (SetField(ref _logTimestamps, value)) { Changed?.Invoke(); } } + } + + /// + /// 日志自动换行。默认开:面板里的日志既可能占满整屏,也可能挤在 440px 的详情抽屉里, + /// 而后者关掉换行就等于把长行截在右边界上 —— 看不到的那一半往往正是要看的那一半。 + /// + public bool LogWrap + { + get => _logWrap; + set { if (SetField(ref _logWrap, value)) { Changed?.Invoke(); } } + } + + /// + /// 事件流断掉时退化为 30 秒定时刷新。默认开 —— 一块静止的、看起来正常的面板 + /// 比明说"没连上"更糟。 + /// + public bool AutoRefreshFallback + { + get => _autoRefreshFallback; + set { if (SetField(ref _autoRefreshFallback, value)) { Changed?.Invoke(); } } + } + + /// 行数分段按钮的选中态跟着 走,改完要通知一遍。 + public void NotifyLogTailSegments() => + OnPropertiesChanged(nameof(LogTail), nameof(LogTailIs100), nameof(LogTailIs500), + nameof(LogTailIs2000), nameof(LogTailIsAll)); + + /// 任一项改动。 + public event Action? Changed; + + /// 读全局设置。 + public async Task LoadAsync(CancellationToken cancellationToken = default) + { + var state = await TryGetAsync(GlobalKey, cancellationToken).ConfigureAwait(false); + if (state is null) + { + return; + } + _showStopped = state.ShowStopped; + _inlineSparklines = state.InlineSparklines; + _compactRows = state.CompactRows; + _logTail = state.LogTail ?? "500"; + _logTimestamps = state.LogTimestamps; + _logWrap = state.LogWrap; + _autoRefreshFallback = state.AutoRefreshFallback; + OnPropertiesChanged(nameof(ShowStopped), nameof(InlineSparklines), nameof(CompactRows), + nameof(RowHeight), nameof(LogTail), nameof(LogTimestamps), nameof(LogWrap), nameof(AutoRefreshFallback)); + } + + /// 写全局设置。 + public Task SaveAsync(CancellationToken cancellationToken = default) => + storage.SetAsync(GlobalKey, new GlobalState + { + ShowStopped = ShowStopped, + InlineSparklines = InlineSparklines, + CompactRows = CompactRows, + LogTail = LogTail, + LogTimestamps = LogTimestamps, + LogWrap = LogWrap, + AutoRefreshFallback = AutoRefreshFallback + }, cancellationToken); + + /// 读某台主机的 socket 路径;没记过返回 + public async Task GetSocketPathAsync(string host, CancellationToken cancellationToken = default) => + (await TryGetAsync(HostPrefix + host, cancellationToken).ConfigureAwait(false))?.SocketPath; + + /// 记住某台主机的 socket 路径。 + public Task SetSocketPathAsync(string host, string socketPath, CancellationToken cancellationToken = default) => + storage.SetAsync(HostPrefix + host, new HostState { SocketPath = socketPath }, cancellationToken); + + /// + /// 读一项设置。读坏了就当没有 —— 绝不因为一份写歪的记录让面板打不开。 + /// + private async Task TryGetAsync(string key, CancellationToken cancellationToken) where T : class + { + try + { + return await storage.GetAsync(key, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + return null; + } + } + + private sealed record GlobalState + { + public bool ShowStopped { get; init; } = true; + public bool InlineSparklines { get; init; } = true; + public bool CompactRows { get; init; } = true; + public string? LogTail { get; init; } + public bool LogTimestamps { get; init; } = true; + public bool LogWrap { get; init; } = true; + public bool AutoRefreshFallback { get; init; } = true; + } + + private sealed record HostState + { + public string? SocketPath { get; init; } + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/ProcessTable.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/ProcessTable.cs new file mode 100644 index 0000000..607f1ba --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/ProcessTable.cs @@ -0,0 +1,80 @@ +using VelaShell.Plugin.DockerPanel.Docker; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// 进程表里的一行。 +/// 进程号。 +/// 属主。 +/// CPU 时间或占比,取决于 daemon 给了哪一列。 +/// 命令行。 +public readonly record struct ProcessRow(string Pid, string User, string Cpu, string Command); + +/// +/// docker top 的列归一化。 +/// +/// 之所以要归一化:/top 的列完全由远端那台机器的 ps 决定 —— +/// GNU 的 ps -efUID PID PPID C STIME TTY TIME CMD, +/// busybox 给 PID USER TIME COMMAND,ps aux 又是另一套。 +/// 界面要的是一张列宽对齐的四列表,所以在这里按标题名认列, +/// 而不是按下标 —— 按下标取会在 Alpine 容器上把 TIME 显示成用户名。 +/// +/// +public static class ProcessTable +{ + /// 把一份 top 结果拍平成四列。 + public static IReadOnlyList Normalize(ContainerTopResult? result) + { + var titles = result?.Titles ?? []; + var rows = result?.Processes ?? []; + if (rows.Length == 0) + { + return []; + } + var pid = IndexOf(titles, "PID"); + var user = IndexOf(titles, "USER", "UID", "RUSER"); + var cpu = IndexOf(titles, "%CPU", "TIME", "PCPU"); + var command = IndexOf(titles, "COMMAND", "CMD", "ARGS"); + + List normalized = [with(rows.Length)]; + foreach (var row in rows) + { + // 命令列认不出来时退回"最后一列":ps 的输出里命令永远排在末尾, + // 因为只有它能带空格。 + var commandText = command >= 0 ? Cell(row, command) + : row.Length > 0 ? row[^1] + : ""; + normalized.Add(new( + pid >= 0 ? Cell(row, pid) : "—", + user >= 0 ? Cell(row, user) : "—", + cpu >= 0 ? Cell(row, cpu) : "—", + commandText)); + } + return normalized; + } + + /// 这一列在 top 结果里叫什么(空表示这台机器的 ps 没给这一列)。 + public static string CpuColumnTitle(ContainerTopResult? result) + { + var titles = result?.Titles ?? []; + var index = IndexOf(titles, "%CPU", "TIME", "PCPU"); + return index >= 0 ? titles[index] : "CPU"; + } + + private static string Cell(string[] row, int index) => + index < row.Length ? row[index] : ""; + + private static int IndexOf(string[] titles, params string[] candidates) + { + foreach (var candidate in candidates) + { + for (var i = 0; i < titles.Length; i++) + { + if (string.Equals(titles[i], candidate, StringComparison.OrdinalIgnoreCase)) + { + return i; + } + } + } + return -1; + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Rows.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Rows.cs new file mode 100644 index 0000000..560bdcc --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Rows.cs @@ -0,0 +1,376 @@ +using System.Collections.ObjectModel; +using VelaShell.Plugin.DockerPanel.Docker; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// 行的状态色。界面按它取左侧色条、圆点与状态文字的颜色。 +public enum RowTone +{ + /// 运行中 / 健康 / 成功。 + Ok, + + /// 暂停 / 需要注意。 + Warn, + + /// 不健康 / 异常退出。 + Danger, + + /// 已停止 / 未使用。 + Idle, + + /// 过渡态(重启中、创建中)。 + Busy +} + +/// +/// 列表行的公共部分:身份、选中态、忙碌态。 +/// +/// 行是**就地合并**的(见 ):刷新只更新内容,不换实例。 +/// 用户选中三行准备批量停止,一条事件刷新一次就全丢了 —— 那是这个面板能犯的最烦人的错误。 +/// +/// +public abstract class RowBase(string id) : ObservableObject +{ + + /// 稳定身份。 + public string Id { get; } = id; + + /// 短 id。 + public string ShortId => Humanize.ShortId(Id); + + /// 是否被勾选(批量操作的目标)。 + public bool Selected + { + get; set + { + if (SetField(ref field, value)) + { + SelectionChanged?.Invoke(); + } + } + } + + /// + /// 这一行就是右侧抽屉里正开着的那一个。 + /// + /// 与 是两件事,底色也不同:勾选是"批量的目标"(强调色底), + /// 当前是"我正在看的那一个"(中性选中底)。混成一种,勾了三行再点开第四行时 + /// 就分不出哪一行会被下一步的「删除」打到。 + /// + /// + public bool Current { get; set => SetField(ref field, value); } + + /// + /// 这一行正在被操作。界面据此在行顶画一条 2px 进度条、把动作按钮换成"取消", + /// 其余按钮禁用**而不是消失** —— 位置不跳。 + /// + public bool Busy { get; set => SetField(ref field, value); } + + /// 行内进度 0–1;为 0 表示不确定型。 + public double BusyProgress { get; set => SetField(ref field, value); } + + /// 勾选状态变了。 + public event Action? SelectionChanged; +} + +/// 容器列表的一行。 +public sealed class ContainerRow(ContainerSummary summary) : RowBase(summary.Id) +{ + + /// 最近若干次 CPU 采样(行内 sparkline 用)。 + public ObservableCollection CpuSamples { get; } = []; + + /// + /// 拥有这一行的页面。 + /// + /// 存在的唯一理由是右键菜单:ContextMenu 弹在一棵**独立的 popup 树**里, + /// $parent[ItemsControl] 在那里解析不到页面的视图模型。给行一个回引, + /// 菜单项就能写成 {Binding Owner.XxxCommand}。 + /// + /// + public Pages.ContainersPageViewModel? Owner { get; set; } + + /// 底层数据。 + public ContainerSummary Summary { get; private set; } = summary; + + /// 容器名。 + public string Name => Summary.Name; + + /// 镜像引用。 + public string Image => Summary.Image ?? ""; + + /// 端口摘要。 + public string Ports => Humanize.Ports(Summary.Ports); + + /// compose 项目名;不属于任何项目时为空。 + public string Project => Summary.ComposeProject ?? ""; + + /// 有没有 compose 项目。 + public bool HasProject => Project.Length > 0; + + /// daemon 的状态串。 + public string Status => Summary.Status ?? ""; + + /// 是否在跑。 + public bool IsRunning => Summary.State == "running"; + + /// 是否暂停。 + public bool IsPaused => Summary.State == "paused"; + + /// 是否不健康。 + public bool IsUnhealthy => Status.Contains("(unhealthy)", StringComparison.OrdinalIgnoreCase); + + /// 是否异常退出。 + public bool IsFailed => Summary.State == "exited" && !Status.Contains("(0)", StringComparison.Ordinal); + + /// 行的状态色。 + public RowTone Tone => + Summary.State is "restarting" or "created" ? RowTone.Busy + : IsUnhealthy || IsFailed ? RowTone.Danger + : IsPaused ? RowTone.Warn + : IsRunning ? RowTone.Ok + : RowTone.Idle; + + /// + /// 运行时长 / 停止多久。直接用 daemon 那句人话的后半段, + /// 而不是自己算 —— 它已经把 "Up 3 days (healthy)" 里的括号处理好了。 + /// + public string Uptime + { + get + { + var status = Status; + if (status.Length == 0) + { + return "—"; + } + // 已退出的容器:括号里的退出码是这一行最要紧的信息,不能跟着括号一起被裁掉。 + if (status.StartsWith("Exited ", StringComparison.Ordinal)) + { + return "退出 " + status[7..]; + } + // 运行中的容器:括号里是健康状态,它已经由左侧色条与圆点表达了,这一列只要时长。 + var paren = status.IndexOf(" (", StringComparison.Ordinal); + var text = paren > 0 ? status[..paren] : status; + return text.StartsWith("Up ", StringComparison.Ordinal) ? text[3..] : text; + } + } + + /// CPU 占用文本。 + public string CpuText { get; private set => SetField(ref field, value); } = "—"; + + /// 内存占用文本。 + public string MemText { get; private set => SetField(ref field, value); } = "—"; + + /// CPU 百分比(决定 sparkline 是否转黄)。 + public double CpuPercent + { + get; private set + { + if (SetField(ref field, value)) + { + OnPropertyChanged(nameof(CpuHot)); + } + } + } + + /// CPU 高到该提醒的程度。 + public bool CpuHot => CpuPercent >= 30; + + /// 最近一帧的内存占用字节(总览页按它汇总,没采到时为 0)。 + public long MemoryBytes { get; private set => SetField(ref field, value); } + + /// 用新快照更新这一行。 + public void Update(ContainerRow incoming) + { + Summary = incoming.Summary; + if (!IsRunning) + { + CpuText = "—"; + MemText = "—"; + CpuPercent = 0; + MemoryBytes = 0; + if (CpuSamples.Count > 0) + { + CpuSamples.Clear(); + } + } + OnPropertiesChanged(nameof(Summary), nameof(Name), nameof(Image), nameof(Ports), nameof(Project), + nameof(HasProject), nameof(Status), nameof(IsRunning), nameof(IsPaused), nameof(IsUnhealthy), + nameof(IsFailed), nameof(Tone), nameof(Uptime)); + } + + /// 吃进一帧统计。 + public void ApplyStats(ContainerStats stats) + { + CpuPercent = stats.CpuPercent; + CpuText = Humanize.Percent(stats.CpuPercent); + MemoryBytes = (long)stats.MemoryUsed; + // 列表这一格很窄,只放绝对值;"占上限多少"留给详情抽屉,那里有横向空间。 + MemText = Humanize.Bytes(stats.MemoryUsed); + CpuSamples.Add(stats.CpuPercent); + // 只留最近 14 个点:行内那条 sparkline 就这么宽,多留的点画不出来也没人看。 + while (CpuSamples.Count > 14) + { + CpuSamples.RemoveAt(0); + } + } +} + +/// 镜像列表的一行。 +public sealed class ImageRow(ImageSummary summary) : RowBase(summary.Id) +{ + + /// 拥有这一行的页面(右键菜单用,理由同 )。 + public Pages.ImagesPageViewModel? Owner { get; set; } + + /// 底层数据。 + public ImageSummary Summary { get; private set; } = summary; + + /// 仓库名(第一个标签的前半段)。 + public string Repository => SplitTag().Repository; + + /// 标签(第一个标签的后半段)。 + public string Tag => SplitTag().Tag; + + /// 是否悬空。 + public bool IsDangling => Summary.IsDangling; + + /// 大小。 + public string SizeText => Humanize.Bytes(Summary.Size); + + /// 创建时间。 + public string CreatedText => Humanize.AgoFromUnix(Summary.Created); + + /// 被多少容器使用。 + public string UsageText => Summary.Containers switch + { + < 0 => IsDangling ? "悬空" : "—", + 0 => IsDangling ? "悬空" : "未使用", + 1 => "1 个容器", + _ => $"{Summary.Containers} 个容器" + }; + + /// 使用情况的语气。 + public RowTone Tone => IsDangling ? RowTone.Warn : Summary.Containers > 0 ? RowTone.Ok : RowTone.Idle; + + /// 全部标签(详情用)。 + public string AllTags => Summary.RepoTags is { Length: > 0 } tags ? string.Join(" · ", tags) : ""; + + /// 用新快照更新。 + public void Update(ImageRow incoming) + { + Summary = incoming.Summary; + OnPropertiesChanged(nameof(Summary), nameof(Repository), nameof(Tag), nameof(IsDangling), + nameof(SizeText), nameof(CreatedText), nameof(UsageText), nameof(Tone), nameof(AllTags)); + } + + private (string Repository, string Tag) SplitTag() + { + var first = Summary.RepoTags is { Length: > 0 } tags ? tags[0] : ":"; + var colon = first.LastIndexOf(':'); + // 冒号可能属于端口(registry:5000/foo),所以只有它出现在最后一个斜杠之后才是标签分隔。 + var slash = first.LastIndexOf('/'); + return colon > slash && colon > 0 ? (first[..colon], first[(colon + 1)..]) : (first, "latest"); + } +} + +/// 卷列表的一行。 +public sealed class VolumeRow(VolumeSummary summary, int refCount) : RowBase(summary.Name) +{ + + /// 拥有这一行的页面(列宽绑在页面上,理由同 )。 + public Pages.VolumesPageViewModel? Owner { get; set; } + + /// 底层数据。 + public VolumeSummary Summary { get; private set; } = summary; + + /// 卷名。 + public string Name => Summary.Name; + + /// 驱动。 + public string Driver => Summary.Driver ?? "local"; + + /// 挂载点。 + public string Mountpoint => Summary.Mountpoint ?? ""; + + /// 创建时间。 + public string CreatedText => Humanize.LocalTime(Summary.CreatedAt); + + /// 被多少容器引用。 + public int RefCount { get; private set; } = refCount; + + /// 使用情况文本。 + public string UsageText => RefCount switch + { + <= 0 => "未使用", + 1 => "1 个容器", + _ => $"{RefCount} 个容器" + }; + + /// 大小;/system/df 没算过时给破折号。 + public string SizeText => Summary.UsageData is { Size: >= 0 } usage ? Humanize.Bytes(usage.Size) : "—"; + + /// 语气。 + public RowTone Tone => RefCount > 0 ? RowTone.Ok : RowTone.Warn; + + /// compose 项目名。 + public string Project => Summary.Labels?.GetValueOrDefault("com.docker.compose.project") ?? ""; + + /// 用新快照更新。 + public void Update(VolumeRow incoming) + { + Summary = incoming.Summary; + RefCount = incoming.RefCount; + OnPropertiesChanged(nameof(Summary), nameof(Name), nameof(Driver), nameof(Mountpoint), + nameof(CreatedText), nameof(RefCount), nameof(UsageText), nameof(SizeText), nameof(Tone), nameof(Project)); + } +} + +/// 网络列表的一行。 +public sealed class NetworkRow(NetworkSummary summary) : RowBase(summary.Id) +{ + + /// 拥有这一行的页面(列宽绑在页面上,理由同 )。 + public Pages.NetworksPageViewModel? Owner { get; set; } + + /// 底层数据。 + public NetworkSummary Summary { get; private set; } = summary; + + /// 网络名。 + public string Name => Summary.Name; + + /// 驱动。 + public string Driver => Summary.Driver ?? ""; + + /// 作用域。 + public string Scope => Summary.Scope ?? ""; + + /// 第一段子网。 + public string Subnet => Summary.FirstSubnet ?? "—"; + + /// 是否内置(bridge/host/none —— 删不掉)。 + public bool IsPredefined => Summary.IsPredefined; + + /// 接入的容器数。 + public int AttachedCount => Summary.Containers?.Count ?? 0; + + /// 接入情况文本。 + public string AttachedText => AttachedCount switch + { + 0 => "未接入", + 1 => "1 个容器", + _ => $"{AttachedCount} 个容器" + }; + + /// 语气。 + public RowTone Tone => IsPredefined ? RowTone.Idle : AttachedCount > 0 ? RowTone.Ok : RowTone.Warn; + + /// 用新快照更新。 + public void Update(NetworkRow incoming) + { + Summary = incoming.Summary; + OnPropertiesChanged(nameof(Summary), nameof(Name), nameof(Driver), nameof(Scope), nameof(Subnet), + nameof(IsPredefined), nameof(AttachedCount), nameof(AttachedText), nameof(Tone)); + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/RunContainerForm.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/RunContainerForm.cs new file mode 100644 index 0000000..3f6d6ec --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/RunContainerForm.cs @@ -0,0 +1,333 @@ +using System.Text; +using System.Text.RegularExpressions; +using VelaShell.Plugin.DockerPanel.Docker; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// +/// 运行容器。 +/// +/// 底部那条等效命令不是装饰:面板实际发的是 POST /containers/create 加 +/// /start,而用户脑子里的模型是 docker run。把两者并排摆出来, +/// 一是让人能核对自己填的东西,二是能直接复制到终端里去验证。 +/// +/// +public sealed partial class RunContainerForm : PanelForm +{ + private readonly TextField _name; + private readonly ChoiceField _network; + private readonly ChoiceField _restart; + private readonly ToggleField _detach; + private readonly ToggleField _autoRemove; + private readonly ToggleField _tty; + private readonly ToggleField _privileged; + private readonly TextField _command; + private readonly TextField _workdir; + + /// 建表单。 + /// 镜像引用。 + /// 镜像的小字(平台、大小)。 + /// 可选网络。 + public RunContainerForm(string image, string imageDetail, IEnumerable networks) + { + Image = image; + ImageDetail = imageDetail; + // 第一格是只读的源镜像:这张表单从镜像页、命令面板、总览都点得进来, + // 而"我正要跑的是哪一个"不该只出现在最底下那条等效命令里。 + Fields.Add(new TextField("源镜像") + { + Value = image, + Hint = imageDetail, + ReadOnly = true + }); + _name = new("容器名称") { Hint = "留空由 Docker 生成", Placeholder = "my-service" }; + Ports = new("端口映射") + { + KeyPlaceholder = "8080", + ValuePlaceholder = "80", + Separator = "→", + AddLabel = "+ 添加端口" + }; + Volumes = new("卷挂载") + { + KeyPlaceholder = "/srv/data 或 卷名", + ValuePlaceholder = "/data", + Separator = "→", + AddLabel = "+ 添加挂载" + }; + Env = new("环境变量") + { + KeyPlaceholder = "KEY", + ValuePlaceholder = "value", + AddLabel = "+ 添加变量", + // 一屏十几条环境变量手敲不现实,而它们几乎总是已经躺在某个 .env 里。 + ImportLabel = "从 .env 导入" + }; + _network = new("网络") { Value = "bridge" }; + foreach (var network in networks) + { + _network.Options.Add(new(network, network)); + } + if (_network.Options.Count == 0) + { + _network.Options.Add(new("bridge", "bridge")); + } + _restart = new("重启策略") { Value = "unless-stopped" }; + foreach (var policy in new[] { "no", "on-failure", "always", "unless-stopped" }) + { + _restart.Options.Add(new(policy, policy)); + } + _detach = new("后台运行 -d") { Value = true, Description = "创建后立即启动并放到后台。" }; + _autoRemove = new("退出即删除 --rm") { Description = "容器一退出就连同可写层一起删掉。" }; + _tty = new("分配 TTY -t") { Description = "给它一个伪终端。" }; + _privileged = new("特权模式 --privileged") + { + Danger = true, + Description = "容器能做几乎任何宿主能做的事 —— 只在你确实需要时打开。" + }; + _command = new("覆盖命令") { Hint = "留空用镜像默认", Placeholder = "nginx -g 'daemon off;'" }; + _workdir = new("工作目录") { Hint = "留空用镜像默认" }; + ExtraArgs = new("额外参数") + { + Hint = "与在终端里手敲同权,不做引用", + Placeholder = "--cap-add NET_ADMIN --dns 1.1.1.1" + }; + + Watch(_name); + Watch(Ports); + Watch(Volumes); + Watch(Env); + Watch(_network); + Watch(_restart); + Watch(_detach); + Watch(_autoRemove); + Watch(_tty); + Watch(_privileged); + Watch(_command); + Watch(_workdir); + Watch(ExtraArgs); + UpdatePreview(); + } + + /// + public override string Title => "运行容器"; + + /// + public override string Icon => "Icon.play"; + + /// + public override string ConfirmLabel => "运行容器"; + + /// + public override string ConfirmIcon => "Icon.play"; + + /// + public override string FooterHint => "面板实际调用 POST /containers/create 与 /start"; + + /// 镜像引用。 + public string Image { get; } + + /// 镜像的小字。 + public string ImageDetail { get; } + + /// 端口映射。 + public PairListField Ports { get; } + + /// 卷挂载。 + public PairListField Volumes { get; } + + /// 环境变量。 + public PairListField Env { get; } + + /// 额外参数(只在等效命令里体现)。 + public TextField ExtraArgs { get; } + + /// 容器名。 + public string ContainerName => _name.Value.Trim(); + + /// 后台运行。 + public bool Detach => _detach.Value; + + /// 把表单变成一条 create 请求。 + public ContainerCreateRequest ToRequest() + { + Dictionary bindings = []; + Dictionary exposed = []; + foreach (var row in Ports.Filled) + { + var containerPort = row.Value.Trim(); + var key = containerPort.Contains('/', StringComparison.Ordinal) ? containerPort : $"{containerPort}/tcp"; + bindings[key] = [new() { HostPort = row.Key.Trim() }]; + exposed[key] = new object(); + } + string[] binds = [.. Volumes.Filled.Select(r => $"{r.Key.Trim()}:{r.Value.Trim()}")]; + string[] env = [.. Env.Filled.Select(r => $"{r.Key.Trim()}={r.Value.Trim()}")]; + var command = _command.Value.Trim(); + return new() + { + Image = Image, + Env = env.Length > 0 ? env : null, + Cmd = command.Length > 0 ? SplitArguments(command) : null, + WorkingDir = _workdir.Value.Trim() is { Length: > 0 } dir ? dir : null, + Tty = _tty.Value, + ExposedPorts = exposed.Count > 0 ? exposed : null, + HostConfig = new() + { + PortBindings = bindings.Count > 0 ? bindings : null, + Binds = binds.Length > 0 ? binds : null, + RestartPolicy = _autoRemove.Value ? null : new() { Name = _restart.Value }, + AutoRemove = _autoRemove.Value, + Privileged = _privileged.Value, + NetworkMode = _network.Value + } + }; + } + + /// + public override bool Validate() + { + if (ContainerName.Length > 0 && !NamePattern().IsMatch(ContainerName)) + { + _name.Error = "只能包含字母、数字与 _ . -,且必须以字母或数字开头。"; + return false; + } + foreach (var row in Ports.Filled) + { + if (!int.TryParse(row.Key.Trim(), out var host) || host is < 1 or > 65535) + { + Ports.Error = $"宿主端口 {row.Key} 不是一个合法端口。"; + return false; + } + var container = row.Value.Trim().Split('/')[0]; + if (!int.TryParse(container, out var inner) || inner is < 1 or > 65535) + { + Ports.Error = $"容器端口 {row.Value} 不是一个合法端口。"; + return false; + } + } + foreach (var row in Volumes.Filled) + { + if (!row.Value.Trim().StartsWith('/')) + { + Volumes.Error = $"容器内路径要用绝对路径:{row.Value}"; + return false; + } + } + if (_autoRemove.Value && _restart.Value != "no") + { + // Docker 自己会拒掉这个组合,但等它拒不如现在就说清楚。 + FormError = "--rm 与重启策略互斥:退出即删除的容器没法被重启。把重启策略改成 no,或关掉 --rm。"; + return false; + } + return true; + } + + /// + protected override void UpdatePreview() + { + var sb = new StringBuilder("docker run"); + if (_detach.Value) + { + sb.Append(" -d"); + } + if (_autoRemove.Value) + { + sb.Append(" --rm"); + } + if (_tty.Value) + { + sb.Append(" -t"); + } + if (_privileged.Value) + { + sb.Append(" --privileged"); + } + if (ContainerName.Length > 0) + { + sb.Append(" --name ").Append(ContainerName); + } + foreach (var row in Ports.Filled) + { + sb.Append(" -p ").Append(row.Key.Trim()).Append(':').Append(row.Value.Trim()); + } + foreach (var row in Volumes.Filled) + { + sb.Append(" -v ").Append(row.Key.Trim()).Append(':').Append(row.Value.Trim()); + } + foreach (var row in Env.Filled) + { + sb.Append(" -e ").Append(row.Key.Trim()).Append('=').Append(row.Value.Trim()); + } + if (_network.Value.Length > 0) + { + sb.Append(" --network ").Append(_network.Value); + } + if (!_autoRemove.Value) + { + sb.Append(" --restart ").Append(_restart.Value); + } + if (_workdir.Value.Trim().Length > 0) + { + sb.Append(" -w ").Append(_workdir.Value.Trim()); + } + if (ExtraArgs.Value.Trim().Length > 0) + { + sb.Append(' ').Append(ExtraArgs.Value.Trim()); + } + sb.Append(' ').Append(Image); + if (_command.Value.Trim().Length > 0) + { + sb.Append(' ').Append(_command.Value.Trim()); + } + CommandPreview = "POST /containers/create + POST /containers/{id}/start"; + CommandNote = $"等价于 {sb}"; + } + + /// + /// 按 shell 的规矩把一条命令切成 argv:双引号与单引号内的空格不算分隔。 + /// + internal static string[] SplitArguments(string command) + { + List parts = []; + var current = new StringBuilder(); + var quote = '\0'; + foreach (var c in command) + { + if (quote != '\0') + { + if (c == quote) + { + quote = '\0'; + } + else + { + current.Append(c); + } + } + else if (c is '"' or '\'') + { + quote = c; + } + else if (char.IsWhiteSpace(c)) + { + if (current.Length > 0) + { + parts.Add(current.ToString()); + current.Clear(); + } + } + else + { + current.Append(c); + } + } + if (current.Length > 0) + { + parts.Add(current.ToString()); + } + return [.. parts]; + } + + [GeneratedRegex("^[a-zA-Z0-9][a-zA-Z0-9_.-]*$")] + private static partial Regex NamePattern(); +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/StatsSampler.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/StatsSampler.cs new file mode 100644 index 0000000..daf3157 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/StatsSampler.cs @@ -0,0 +1,115 @@ +using VelaShell.Plugin.DockerPanel.Docker; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// +/// 列表行的 CPU / 内存采样器。 +/// +/// 为什么不给每一行开一条 stats 流。 那是最实时的做法,但一条流占一个 SSH +/// 通道,而隧道配额是每插件 16 条 —— 二十个容器的机器上,光列表就能把事件流、日志、 +/// 终端要用的通道全吃光。所以列表走定期快照(stream=0),真正的实时流只留给 +/// 详情抽屉里那一个容器。 +/// +/// +/// 并发也压到 3:HTTP 连接池只有 6,采样把它占满的话,用户点一下"停止"要排队等采样跑完。 +/// +/// +public sealed class StatsSampler(Func clientAccessor) : IAsyncDisposable +{ + private static readonly TimeSpan Interval = TimeSpan.FromSeconds(5); + private const int MaxConcurrency = 3; + + private readonly SemaphoreSlim _gate = new(MaxConcurrency, MaxConcurrency); + private CancellationTokenSource? _cts; + private Task? _loop; + private Func>? _targets; + private Action>? _onRound; + + /// 开始采样。 + /// 每一轮要采哪些行(通常是"当前可见且在跑的")。 + /// + /// 每采完一轮回调一次。总览页的 CPU 趋势与 Top 5 靠它喂 —— + /// 让总览自己再发一轮请求等于把同一批数据要两遍。 + /// + /// 面板生命周期。 + public void Start(Func> targets, Action>? onRound, + CancellationToken lifetime) + { + Stop(); + _targets = targets; + _onRound = onRound; + _cts = CancellationTokenSource.CreateLinkedTokenSource(lifetime); + var token = _cts.Token; + _loop = Task.Run(() => LoopAsync(token), token); + } + + /// 停止采样。 + public void Stop() + { + _cts?.Cancel(); + _cts?.Dispose(); + _cts = null; + _loop = null; + } + + private async Task LoopAsync(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + var client = clientAccessor(); + var rows = _targets?.Invoke() ?? []; + if (client is not null && rows.Count > 0) + { + await Task.WhenAll(rows.Select(row => SampleAsync(client, row, token))).ConfigureAwait(false); + if (!token.IsCancellationRequested && _onRound is { } onRound) + { + Ui.Post(() => onRound(rows)); + } + } + try + { + await Task.Delay(Interval, token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + return; + } + } + } + + private async Task SampleAsync(DockerClient client, ContainerRow row, CancellationToken token) + { + try + { + await _gate.WaitAsync(token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + return; + } + try + { + var stats = await client.StatsSnapshotAsync(row.Id, token).ConfigureAwait(false); + if (stats is not null) + { + Ui.Post(() => row.ApplyStats(stats)); + } + } + catch (Exception) + { + // 采样失败不该冒泡:容器可能刚好在这一刻被删了,而那不是一个要报给用户的错误。 + } + finally + { + _gate.Release(); + } + } + + /// + public ValueTask DisposeAsync() + { + Stop(); + _gate.Dispose(); + return ValueTask.CompletedTask; + } +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/TaskCenter.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/TaskCenter.cs new file mode 100644 index 0000000..7ad6308 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/TaskCenter.cs @@ -0,0 +1,347 @@ +using System.Collections.ObjectModel; +using VelaShell.Plugin.DockerPanel.Docker; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// 后台任务的状态。 +public enum PanelTaskState +{ + /// 进行中。 + Running, + + /// 成功结束。 + Succeeded, + + /// 部分成功(批量操作里有失败的目标)。 + PartiallyFailed, + + /// 失败。 + Failed, + + /// 被取消。 + Cancelled +} + +/// +/// 任务中心里的一个条目。 +/// +/// 进度分两种,界面上长得不一样:知道分母的(镜像层字节数、批量的目标个数)画确定型进度条; +/// 不知道要多久的(compose up 等健康检查、docker stop 等 SIGTERM)画不确定型 —— +/// 给后者编一个假的百分比,只会让用户在 90% 处等上两分钟。 +/// +/// +/// 建一个任务。 +public sealed class PanelTask(string icon, string title, bool indeterminate) : ObservableObject +{ + private readonly CancellationTokenSource _cts = new(); + + /// 图标资源键。 + public string Icon { get; } = icon; + + /// 任务标题。 + public string Title + { + get => title; + set => SetField(ref title, value); + } + + /// 标题下面那行小字(层数、速率、当前目标)。 + public string Detail + { + get; + set => SetField(ref field, value); + } = ""; + + /// 0–1 的进度;仅 为假时有意义。 + public double Progress + { + get; + set => SetField(ref field, Math.Clamp(value, 0, 1)); + } + + /// 是否不确定型。 + public bool Indeterminate + { + get => indeterminate; + set => SetField(ref indeterminate, value); + } + + /// 右侧的一小段文字(百分比 / “完成” / “部分失败”)。 + public string RightText + { + get; + set => SetField(ref field, value); + } = ""; + + /// 状态。 + public PanelTaskState State + { + get; + private set + { + if (SetField(ref field, value)) + { + OnPropertiesChanged(nameof(IsRunning), nameof(IsFinished), nameof(CanCancel)); + } + } + } = PanelTaskState.Running; + + /// 还在跑。 + public bool IsRunning => State == PanelTaskState.Running; + + /// 已经结束(成功、失败或取消)。 + public bool IsFinished => State != PanelTaskState.Running; + + /// 能不能取消。 + public bool CanCancel => IsRunning && !_cts.IsCancellationRequested; + + /// 任务自己的取消令牌。 + public CancellationToken Token => _cts.Token; + + /// 结束后可以点开看详情的东西(批量结果、执行记录)。 + public object? Payload { get; set; } + + /// 取消这个任务。 + public void Cancel() + { + if (!_cts.IsCancellationRequested) + { + _cts.Cancel(); + OnPropertyChanged(nameof(CanCancel)); + } + } + + /// 标记结束。 + public void Finish(PanelTaskState state, string rightText, string? detail = null) + { + State = state; + RightText = rightText; + if (detail is not null) + { + Detail = detail; + } + Indeterminate = false; + if (state == PanelTaskState.Succeeded) + { + Progress = 1; + } + } + + /// 释放取消源。 + public void Dispose() => _cts.Dispose(); +} + +/// +/// 任务中心:面板里所有"要花时间"的动作都在这儿登记。 +/// +/// 存在的理由是**关掉对话框不该等于取消任务**。拉一个 2 GB 的镜像时,用户多半想 +/// 一边等一边去看别的容器;进度移交给顶栏这枚小环,任务照跑。 +/// +/// +public sealed class TaskCenter : ObservableObject +{ + /// 已完成任务的保留条数。再多就没人看了,只会把弹层撑长。 + private const int MaxFinished = 20; + + /// 全部任务,进行中的排在前面。 + public ObservableCollection Tasks { get; } = []; + + /// 进行中的任务数。 + public int RunningCount => Tasks.Count(t => t.IsRunning); + + /// 已完成的任务数。 + public int FinishedCount => Tasks.Count(t => t.IsFinished); + + /// 有没有任务在跑(顶栏那枚指示器据此显隐)。 + public bool HasRunning => RunningCount > 0; + + /// + /// 全部进行中任务的合并进度(0–1)。不确定型的任务不参与平均 —— + /// 把一个"不知道要多久"按 0 计进去,会让整体进度看起来一直卡住。 + /// + public double OverallProgress + { + get + { + List determinate = [.. Tasks.Where(t => t is { IsRunning: true, Indeterminate: false })]; + return determinate.Count == 0 ? 0 : determinate.Average(t => t.Progress); + } + } + + /// 顶栏那枚指示器上的文字。 + public string IndicatorText => RunningCount switch + { + 0 => "", + 1 => "1 个任务", + _ => $"{RunningCount} 个任务" + }; + + /// 顶栏指示器右侧的百分比;没有确定型任务时为空。 + public string IndicatorPercent + { + get + { + var progress = OverallProgress; + return progress <= 0 ? "" : $"{progress * 100:0}%"; + } + } + + /// 登记一个新任务并放到最前面。 + public PanelTask Start(string icon, string title, bool indeterminate, string detail = "") + { + var task = new PanelTask(icon, title, indeterminate) { Detail = detail }; + task.PropertyChanged += (_, _) => RaiseSummary(); + Ui.Post(() => + { + Tasks.Insert(0, task); + Trim(); + RaiseSummary(); + }); + return task; + } + + /// 清掉已完成的。 + public void ClearFinished() + { + Ui.Post(() => + { + foreach (var task in Tasks.Where(t => t.IsFinished).ToArray()) + { + Tasks.Remove(task); + task.Dispose(); + } + RaiseSummary(); + }); + } + + /// 取消全部进行中的任务(面板关闭时)。 + public void CancelAll() + { + foreach (var task in Tasks.Where(t => t.IsRunning).ToArray()) + { + task.Cancel(); + } + } + + private void Trim() + { + PanelTask[] finished = [.. Tasks.Where(t => t.IsFinished)]; + for (var i = MaxFinished; i < finished.Length; i++) + { + Tasks.Remove(finished[i]); + finished[i].Dispose(); + } + } + + private void RaiseSummary() => Ui.Post(() => + OnPropertiesChanged(nameof(RunningCount), nameof(FinishedCount), nameof(HasRunning), + nameof(OverallProgress), nameof(IndicatorText), nameof(IndicatorPercent))); +} + +/// +/// 拉取进度的按层聚合。 +/// +/// daemon 推来的是一条平铺的 NDJSON,每层的每一次进度都是独立一帧。界面要的是 +/// "9 层,6 层复用,2 层在下载" —— 这个类负责把前者变成后者,并算出总字节进度。 +/// +/// +public sealed class PullAggregator +{ + private readonly Dictionary _layers = []; + + /// 层的状态。 + private sealed record LayerState(string Status, long Current, long Total); + + /// 吃进一帧。 + public void Accept(PullProgressFrame frame) + { + if (string.IsNullOrEmpty(frame.Id)) + { + // 没有 id 的帧是总览行("Pulling from library/nginx"、最终摘要),不计进层里。 + return; + } + var current = frame.ProgressDetail?.Current ?? 0; + var total = frame.ProgressDetail?.Total ?? 0; + var status = frame.Status ?? ""; + // "Pull complete" / "Already exists" 之后不再有字节明细, + // 但它们已经算 100% —— 沿用上一次拿到的 total,免得进度条在最后一刻回退。 + if (_layers.TryGetValue(frame.Id, out var previous)) + { + if (total == 0) + { + total = previous.Total; + } + if (IsComplete(status)) + { + current = total; + } + } + _layers[frame.Id] = new(status, current, total); + } + + /// 层总数。 + public int LayerCount => _layers.Count; + + /// 已经完成的层数(含复用)。 + public int CompletedLayers => _layers.Values.Count(l => IsComplete(l.Status)); + + /// 复用(本地已有)的层数。 + public int ReusedLayers => _layers.Values.Count(l => + l.Status.Contains("Already exists", StringComparison.OrdinalIgnoreCase)); + + /// 正在下载或解压的层数。 + public int ActiveLayers => _layers.Values.Count(l => + l.Status.StartsWith("Downloading", StringComparison.OrdinalIgnoreCase) || + l.Status.StartsWith("Extracting", StringComparison.OrdinalIgnoreCase)); + + /// 已下载字节。 + public long CurrentBytes => _layers.Values.Sum(l => l.Current); + + /// 总字节(只统计报了 total 的层)。 + public long TotalBytes => _layers.Values.Sum(l => l.Total); + + /// + /// 总进度 0–1。 + /// + /// 优先按字节算;一个字节都还没报(全是 "Already exists")时退回按层数算 —— + /// 否则一次全命中缓存的拉取会显示成 0%,然后直接跳到完成。 + /// + /// + public double Progress => + TotalBytes > 0 ? Math.Clamp((double)CurrentBytes / TotalBytes, 0, 1) + : LayerCount > 0 ? (double)CompletedLayers / LayerCount + : 0; + + /// + /// 一行摘要,给任务中心与对话框共用。 + /// + /// 写成"完成 3 / 12 层"而不是"12 层":拉一个大镜像要几分钟, + /// 用户盯着的是**还剩多少**,而层总数从头到尾都不变,说了等于没说。 + /// (CompletedLayers 早就算好了,只是一直没进这句话。) + /// + /// + public string Summary => + LayerCount == 0 + ? "正在连接仓库…" + : TotalBytes > 0 + ? $"完成 {CompletedLayers} / {LayerCount} 层 · {Humanize.Bytes(CurrentBytes)} / {Humanize.Bytes(TotalBytes)} · 复用 {ReusedLayers} 层" + : $"完成 {CompletedLayers} / {LayerCount} 层 · 复用 {ReusedLayers} 层"; + + /// 逐层快照,给对话框里的层列表用。 + public IReadOnlyList<(string Id, string Status, double Progress, string SizeText)> Snapshot() => + [.. _layers.Select(kv => ( + Id: kv.Key, + kv.Value.Status, + Progress: kv.Value.Total > 0 ? Math.Clamp((double)kv.Value.Current / kv.Value.Total, 0, 1) : IsComplete(kv.Value.Status) ? 1d : 0d, + SizeText: kv.Value.Total > 0 + ? $"{Humanize.Bytes(kv.Value.Current)} / {Humanize.Bytes(kv.Value.Total)}" + : IsComplete(kv.Value.Status) ? "已就绪" : "待定")) + .OrderBy(l => IsComplete(l.Status) ? 1 : 0) + .ThenBy(l => l.Id, StringComparer.Ordinal)]; + + private static bool IsComplete(string status) => + status.Contains("Already exists", StringComparison.OrdinalIgnoreCase) || + status.Contains("Pull complete", StringComparison.OrdinalIgnoreCase) || + status.Contains("Layer already exists", StringComparison.OrdinalIgnoreCase) || + status.Contains("Pushed", StringComparison.OrdinalIgnoreCase); +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/ThemeBrushes.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/ThemeBrushes.cs new file mode 100644 index 0000000..4bfbd82 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/ThemeBrushes.cs @@ -0,0 +1,125 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Threading; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// +/// 转换器取色的唯一出口:按令牌名给一支**长期有效**的画刷,宿主换肤时就地改色。 +/// +/// +/// 要解决的问题。转换器是绑定求值时才跑的,而绑定只在**它自己的源值**变化时重算。 +/// 主题不是绑定源 —— 所以换肤时转换器根本不会被再调一次。 +/// 从前这里直接把宿主资源里的画刷对象返回出去,而宿主换主题是**整格替换** +/// (Application.Resources.ThemeDictionaries 里换一个新的字典,里面是一批新造的 +/// SolidColorBrush),于是上一次返回出去的那个对象还在界面上挂着、颜色还是旧主题的。 +/// 表现是:切完主题,列表里的状态点、CPU 数字、危险按钮的描边全停在上一套配色上, +/// 直到那一行的数据碰巧变了才跟上来。 +/// +/// +/// +/// 为什么不是"换肤时让绑定重算"。那需要把主题做成绑定源(每个转换器后面挂一个 +/// 通知属性),十几处调用点全要改,而且每次换肤都要惊动整棵树重新求值。 +/// 这里反过来:**画刷对象本身不换**,只改它的 Color。 +/// SolidColorBrush.Color 是 styled property,改它会自动让所有用到这支画刷的地方重绘 —— +/// 绑定一次都不用重算,调用点一行都不用改。 +/// +/// +/// +/// 为什么要 Attach/Detach。订阅 Application.Current 的事件,是从**宿主**的对象 +/// 指向**插件**静态方法的一条引用。插件停用后 ALC 要能回收,这条引用就必须撤掉 —— +/// 不撤的话整个插件程序集被钉在内存里,而且下次加载会有两份。 +/// 生命周期挂在插件的激活/停用上,见 DockerPanelPlugin。 +/// +/// +/// +/// 本面板是 inProcess 的(清单里钉死),所以直接认宿主应用的资源变更就够了 —— +/// 它覆盖全部三种换肤:换具名主题、"跟随系统"下系统明暗翻转、用户改强调色。 +/// 隔离模式的插件拿不到宿主的 Application,那边要认 ctx.Theme.Changed。 +/// +/// +internal static class ThemeBrushes +{ + /// 令牌名 → 那支长期有效的画刷。只在 UI 线程上读写(转换器与资源事件都在 UI 线程)。 + private static readonly Dictionary Tracked = new(StringComparer.Ordinal); + + private static EventHandler? _hook; + + /// 开始跟随宿主换肤。插件激活时调用一次;重复调用无副作用。 + public static void Attach() + { + if (_hook is not null || Application.Current is not { } app) + { + return; + } + _hook = (_, _) => Refresh(); + app.ResourcesChanged += _hook; + } + + /// 停止跟随并清空缓存。插件停用时调用 —— 不调就是把插件程序集钉在内存里。 + public static void Detach() + { + if (_hook is { } hook && Application.Current is { } app) + { + app.ResourcesChanged -= hook; + } + _hook = null; + Tracked.Clear(); + } + + /// + /// 取令牌对应的画刷。同一个令牌**永远返回同一个实例** —— 这是整套机制成立的前提: + /// 早先绑上去的那些地方,靠的就是手里这个实例会被就地改色。 + /// + /// 宿主令牌名,如 VelaAccent。 + /// 令牌解析不到时的兜底色(宿主资源还没就位,或令牌名写错了)。 + public static IBrush Get(string token, IBrush fallback) + { + if (Tracked.TryGetValue(token, out SolidColorBrush? tracked)) + { + return tracked; + } + // 解析不到就用兜底色起头,但**照样登记**:资源可能只是还没挂上(控件尚未进可视树), + // 下一次 Refresh 会把它补正。不登记的话它就永远是兜底灰了。 + Color initial = Resolve(token) ?? (fallback as ISolidColorBrush)?.Color ?? Colors.Gray; + var brush = new SolidColorBrush(initial); + Tracked[token] = brush; + return brush; + } + + /// 宿主资源变了:把每一支跟踪中的画刷重新解析一遍。 + private static void Refresh() + { + // 资源变更可能来自任意线程的写入;改 styled property 必须回 UI 线程。 + if (!Dispatcher.UIThread.CheckAccess()) + { + Dispatcher.UIThread.Post(Refresh); + return; + } + foreach ((string token, SolidColorBrush brush) in Tracked) + { + if (Resolve(token) is { } color && brush.Color != color) + { + brush.Color = color; + } + } + } + + /// + /// 按**当前主题变体**解析一个令牌。 + /// + /// 必须带上 ActualThemeVariant:宿主的令牌分两族 —— VelaError / + /// VelaWarning / VelaShell* 写在整份主题文件里,而 VelaAccent / + /// VelaStatusConnected / VelaGauge* / VelaText* 写在 + /// ThemeDictionaries 的 Dark / Light 分支下。不带主题的那个重载按 + /// ThemeVariant.Default 查,后一族一个都查不到,于是**恰好半套颜色**静默回落成灰。 + /// + /// + private static Color? Resolve(string token) => + Application.Current is { } app + && app.TryFindResource(token, app.ActualThemeVariant, out object? value) + && value is ISolidColorBrush brush + ? brush.Color + : null; +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/WeightedStackPanel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/WeightedStackPanel.cs new file mode 100644 index 0000000..7b6d438 --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/WeightedStackPanel.cs @@ -0,0 +1,102 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.VisualTree; + +namespace VelaShell.Plugin.DockerPanel.Ui; + +/// +/// 按权重横向瓜分宽度的面板。 +/// +/// 用它而不是 Grid 的星形列,是因为段数是数据决定的 —— +/// ColumnDefinitions 绑不了集合。用它而不是固定像素,是因为面板宽度 +/// 是用户拖出来的,算好的像素在下一次拖动就错了。 +/// +/// +/// 权重从哪儿读,有两条路: +/// +/// 子元素直接写在 XAML 里(占用比例条那种两段式)—— 用它的 Tag 就够了; +/// 子元素由 ItemsControl 按集合生成 —— 必须用附加属性 +/// ,而且要设在**容器**上。 +/// +/// 第二条是一个容易踩空的地方:ItemsControl 会把每一项包进一个 +/// ContentPresenter,面板看到的孩子是那个 presenter,而 DataTemplate 里写的 +/// Tag 在它下面一层。于是每一段的权重都读成 0、整条不占宽 —— +/// 界面上就是一条空槽,而数据其实完全正确。 +/// +/// +public sealed class WeightedStackPanel : Panel +{ + /// + /// 这一段占多少(0–1 的比例,或任意同量纲的数 —— 面板只用它们的相对大小)。 + /// + /// 由 ItemsControl 生成子元素时,设在 ItemContainerTheme 里, + /// 这样它落在面板真正看得到的那一层上。 + /// + /// + public static readonly AttachedProperty WeightProperty = + AvaloniaProperty.RegisterAttached("Weight"); + + static WeightedStackPanel() => + // 权重变了要重排的是**父面板**(每一段的宽度都跟着变),不是那一段自己。 + WeightProperty.Changed.AddClassHandler((child, _) => + (child.GetVisualParent() as WeightedStackPanel)?.InvalidateArrange()); + + /// + public static double GetWeight(Control control) => control.GetValue(WeightProperty); + + /// + public static void SetWeight(Control control, double value) => control.SetValue(WeightProperty, value); + + /// + protected override Size MeasureOverride(Size availableSize) + { + double height = 0; + foreach (var child in Children) + { + child.Measure(availableSize); + height = Math.Max(height, child.DesiredSize.Height); + } + return new(double.IsInfinity(availableSize.Width) ? 0 : availableSize.Width, height); + } + + /// + protected override Size ArrangeOverride(Size finalSize) + { + double total = 0; + var last = -1; + for (var i = 0; i < Children.Count; i++) + { + var weight = Weight(Children[i]); + total += weight; + if (weight > 0) + { + last = i; + } + } + if (total <= 0) + { + return finalSize; + } + double x = 0; + for (var i = 0; i < Children.Count; i++) + { + var child = Children[i]; + // 舍入误差交给**最后一段有宽度的**吃掉,免得右边留一条一像素的缝。 + // 不能简单地给最后一个孩子:一个 0 字节的构建缓存排在末尾时, + // 它会把整条剩余宽度都画成自己的颜色 —— 明明什么都没占。 + var width = i == last + ? Math.Max(0, finalSize.Width - x) + : finalSize.Width * (Weight(child) / total); + child.Arrange(new(x, 0, width, finalSize.Height)); + x += width; + } + return finalSize; + } + + /// 附加属性优先;没设过再退回 Tag(手写子元素的那两处用的是它)。 + private static double Weight(Control child) => + child.GetValue(WeightProperty) is var attached && attached > 0 ? attached + : child.Tag is double weight && weight > 0 ? weight + : child.Tag is string text && double.TryParse(text, out var parsed) && parsed > 0 ? parsed + : 0; +} diff --git a/plugins/VelaShell.Plugin.DockerPanel/VelaShell.Plugin.DockerPanel.csproj b/plugins/VelaShell.Plugin.DockerPanel/VelaShell.Plugin.DockerPanel.csproj new file mode 100644 index 0000000..d53e48f --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/VelaShell.Plugin.DockerPanel.csproj @@ -0,0 +1,54 @@ + + + + VelaShell Docker 面板插件:经既有 SSH 会话直连远端 /var/run/docker.sock,以 Docker Engine HTTP API 管理容器、镜像、卷、网络与 Compose,含实时统计、日志、容器内文件编辑与内置控制台。 + + velashell.dockerpanel + + $(NoWarn);AVLN3001 + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/VelaShell.Plugin.DockerPanel/VelaShell.Plugin.DockerPanel.pen b/plugins/VelaShell.Plugin.DockerPanel/VelaShell.Plugin.DockerPanel.pen new file mode 100644 index 0000000..1d6a4ac --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/VelaShell.Plugin.DockerPanel.pen @@ -0,0 +1,88097 @@ +{ + "version": "2.17", + "children": [ + { + "type": "frame", + "id": "i1OTz", + "x": 0, + "y": 0, + "name": "C/Button", + "reusable": true, + "height": 26, + "fill": "#00000000", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 6, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "RQ8pM", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "play", + "library": "lucide", + "fill": "$text-secondary" + }, + { + "type": "text", + "id": "P0aUw", + "name": "Label", + "fill": "$text-secondary", + "content": "启动", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "yoK0T", + "x": 150, + "y": 0, + "name": "C/IconButton", + "reusable": true, + "width": 26, + "height": 26, + "fill": "#00000000", + "cornerRadius": "$r-sm", + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "XMGqY", + "name": "Icon", + "width": 14, + "height": 14, + "icon": "refresh-cw", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + }, + { + "type": "frame", + "id": "x0Pcos", + "x": 220, + "y": 0, + "name": "C/Chip", + "reusable": true, + "height": 17, + "fill": "$ok-dim", + "cornerRadius": 9, + "gap": 5, + "padding": [ + 0, + 7 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "kvk5k", + "name": "Dot", + "fill": "$ok", + "width": 5, + "height": 5 + }, + { + "type": "text", + "id": "ODVko", + "name": "Label", + "fill": "$ok", + "content": "运行中", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "MhYIL", + "x": 460, + "y": 0, + "name": "C/Spark", + "reusable": true, + "height": 16, + "fill": "#00000000", + "gap": 1, + "alignItems": "end", + "children": [ + { + "type": "rectangle", + "cornerRadius": 1, + "id": "fshVw", + "name": "Bar1", + "fill": "$accent", + "width": 3, + "height": 6 + }, + { + "type": "rectangle", + "cornerRadius": 1, + "id": "srvmw", + "name": "Bar2", + "fill": "$accent", + "width": 3, + "height": 6 + }, + { + "type": "rectangle", + "cornerRadius": 1, + "id": "ky1P1", + "name": "Bar3", + "fill": "$accent", + "width": 3, + "height": 6 + }, + { + "type": "rectangle", + "cornerRadius": 1, + "id": "bqMzi", + "name": "Bar4", + "fill": "$accent", + "width": 3, + "height": 6 + }, + { + "type": "rectangle", + "cornerRadius": 1, + "id": "F5Kloq", + "name": "Bar5", + "fill": "$accent", + "width": 3, + "height": 6 + }, + { + "type": "rectangle", + "cornerRadius": 1, + "id": "StdGN", + "name": "Bar6", + "fill": "$accent", + "width": 3, + "height": 6 + }, + { + "type": "rectangle", + "cornerRadius": 1, + "id": "iq3Ux", + "name": "Bar7", + "fill": "$accent", + "width": 3, + "height": 6 + }, + { + "type": "rectangle", + "cornerRadius": 1, + "id": "PHY2v", + "name": "Bar8", + "fill": "$accent", + "width": 3, + "height": 6 + }, + { + "type": "rectangle", + "cornerRadius": 1, + "id": "xqaP5", + "name": "Bar9", + "fill": "$accent", + "width": 3, + "height": 6 + }, + { + "type": "rectangle", + "cornerRadius": 1, + "id": "cZHdc", + "name": "Bar10", + "fill": "$accent", + "width": 3, + "height": 6 + }, + { + "type": "rectangle", + "cornerRadius": 1, + "id": "jhPqP", + "name": "Bar11", + "fill": "$accent", + "width": 3, + "height": 6 + }, + { + "type": "rectangle", + "cornerRadius": 1, + "id": "Za8Yw", + "name": "Bar12", + "fill": "$accent", + "width": 3, + "height": 6 + }, + { + "type": "rectangle", + "cornerRadius": 1, + "id": "zbp8o", + "name": "Bar13", + "fill": "$accent", + "width": 3, + "height": 6 + }, + { + "type": "rectangle", + "cornerRadius": 1, + "id": "kMt0y", + "name": "Bar14", + "fill": "$accent", + "width": 3, + "height": 6 + } + ] + }, + { + "type": "frame", + "id": "OHo2s", + "x": 0, + "y": 90, + "name": "C/ContainerRow", + "reusable": true, + "width": 939, + "height": 32, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "id": "QxUB9", + "name": "Strip", + "fill": "$ok", + "width": 3, + "height": 32 + }, + { + "type": "frame", + "id": "yhcM9", + "name": "Cell Select", + "clip": true, + "width": 26, + "height": 32, + "fill": "#00000000", + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "T31V2", + "name": "Checkbox", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + } + ] + }, + { + "type": "frame", + "id": "Tqazb", + "name": "Cell Name", + "clip": true, + "width": "fill_container", + "height": 32, + "fill": "#00000000", + "gap": 7, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "yMbTn", + "name": "Status Dot", + "fill": "$ok", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "L6tp5", + "name": "Name", + "fill": "$text-primary", + "content": "nginx-proxy", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "id": "VJq0n", + "type": "ref", + "ref": "x0Pcos", + "name": "Project Chip", + "fill": "$violet-dim", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "web-stack", + "fill": "$violet" + } + } + } + ] + }, + { + "type": "frame", + "id": "AuFFp", + "name": "Cell Image", + "clip": true, + "width": 190, + "height": 32, + "fill": "#00000000", + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "p4yL1G", + "name": "Image", + "fill": "$text-secondary", + "content": "nginx:1.27-alpine", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "s51yfv", + "name": "Cell Ports", + "clip": true, + "width": 118, + "height": 32, + "fill": "#00000000", + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "y2nlk", + "name": "Ports", + "fill": "$text-tertiary", + "content": "80→8080", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "L70fnt", + "name": "Cell CPU", + "clip": true, + "width": 108, + "height": 32, + "fill": "#00000000", + "gap": 8, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "id": "rANgd", + "type": "ref", + "ref": "MhYIL", + "name": "CPU Spark" + }, + { + "type": "text", + "id": "NprKu", + "name": "CPU", + "fill": "$text-secondary", + "content": "2.1%", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "BzRZ6", + "name": "Cell Mem", + "clip": true, + "width": 84, + "height": 32, + "fill": "#00000000", + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "AdR38", + "name": "Mem", + "fill": "$text-secondary", + "content": "128 MB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "FFXUq", + "name": "Cell Uptime", + "clip": true, + "width": 78, + "height": 32, + "fill": "#00000000", + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "PeLHA", + "name": "Uptime", + "fill": "$text-tertiary", + "content": "3d 4h", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "twKOg", + "name": "Cell Actions", + "clip": true, + "width": 92, + "height": 32, + "fill": "#00000000", + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "ALIhr", + "type": "ref", + "ref": "yoK0T", + "name": "Act square", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "square", + "width": 13, + "height": 13 + } + } + }, + { + "id": "gOkyO", + "type": "ref", + "ref": "yoK0T", + "name": "Act rotate-cw", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "rotate-cw", + "width": 13, + "height": 13 + } + } + }, + { + "id": "MYLUy", + "type": "ref", + "ref": "yoK0T", + "name": "Act terminal", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "terminal", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "l3qo4", + "x": 0, + "y": 150, + "name": "C/KVRow", + "reusable": true, + "width": 392, + "height": 22, + "fill": "#00000000", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "N9bNk", + "name": "Key", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": 96, + "content": "镜像", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "psgr6", + "name": "Value", + "fill": "$text-primary", + "content": "nginx:1.27-alpine", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "sr5Cb", + "x": 620, + "y": 90, + "name": "C/StatCard", + "reusable": true, + "width": 170, + "fill": "$bg-elevated", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 5, + "padding": [ + 11, + 13 + ], + "children": [ + { + "type": "frame", + "id": "P5vN5a", + "name": "Top", + "width": "fill_container", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "RbtQX", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "box", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "T2It9", + "name": "Label", + "fill": "$text-tertiary", + "content": "运行中容器", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "X8FzB", + "name": "Value", + "fill": "$text-primary", + "content": "12", + "fontFamily": "$font-ui", + "fontSize": 22, + "fontWeight": "600" + }, + { + "type": "text", + "id": "UEarH", + "name": "Sub", + "fill": "$text-muted", + "content": "共 18 个", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "MdmRT", + "x": 460, + "y": 90, + "name": "C/Tab", + "reusable": true, + "height": 24, + "fill": "$bg-active", + "cornerRadius": "$r-sm", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "aVLQQ", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "info", + "library": "lucide", + "fill": "$text-primary" + }, + { + "type": "text", + "id": "iscyV", + "name": "Label", + "fill": "$text-primary", + "content": "概览", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "nKcgr", + "x": 360, + "y": 0, + "name": "C/RailItem", + "reusable": true, + "width": 56, + "height": 48, + "fill": "#00000000", + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "cQhxS", + "name": "Pill", + "width": 46, + "height": 42, + "fill": "#00000000", + "cornerRadius": 8, + "layout": "vertical", + "gap": 3, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "F1F9E", + "name": "Icon", + "width": 17, + "height": 17, + "icon": "box", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "DE5G9", + "name": "Label", + "fill": "$text-tertiary", + "content": "容器", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Q57UHp", + "x": 1059, + "y": 0, + "name": "01 · 容器 · 暗色", + "theme": { + "mode": "dark" + }, + "clip": true, + "width": 1440, + "height": 860, + "fill": "$bg-page", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "F3AAAS", + "name": "Top Bar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "LWx7E", + "name": "Brand", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "L9e19", + "name": "Logo", + "width": 17, + "height": 17, + "icon": "container", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "wRe5y", + "name": "Title", + "fill": "$text-primary", + "content": "Docker", + "fontFamily": "$font-ui", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "rectangle", + "id": "k52VR2", + "name": "Divider", + "fill": "$border", + "width": 1, + "height": 18 + }, + { + "type": "frame", + "id": "tAKF6", + "name": "Host Switcher", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "G3jjC", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "server", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "mk0dD", + "name": "Host", + "fill": "$text-primary", + "content": "prod-sg-01", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "vFQX7", + "name": "Transport", + "fill": "$text-muted", + "content": "/var/run/docker.sock · SSH 隧道", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "xbSlh", + "name": "Caret", + "width": 12, + "height": 12, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + }, + { + "type": "frame", + "id": "DW5DR", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "aB70b", + "name": "Search", + "width": 300, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "MedoD", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "search", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "F83JX", + "name": "Placeholder", + "fill": "$text-muted", + "content": "搜索容器、镜像、卷、网络…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "tJQ24", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "dCL3n", + "name": "Kbd", + "height": 16, + "fill": "$bg-hover", + "cornerRadius": 3, + "padding": [ + 0, + 5 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "XfLVV", + "name": "Keys", + "fill": "$text-tertiary", + "content": "Ctrl K", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "id": "dMe3c", + "type": "ref", + "ref": "x0Pcos", + "name": "Live Badge", + "descendants": { + "ODVko": { + "content": "实时" + } + } + }, + { + "id": "rdRoZ", + "type": "ref", + "ref": "yoK0T", + "name": "Settings", + "descendants": { + "XMGqY": { + "icon": "settings" + } + } + } + ] + }, + { + "type": "frame", + "id": "MES2y", + "name": "Body", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "kKfyV", + "name": "Rail", + "width": 56, + "height": "fill_container", + "fill": "$bg-rail", + "stroke": "$border", + "strokeWidth": { + "right": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 2, + "padding": [ + 6, + 0 + ], + "alignItems": "center", + "children": [ + { + "id": "Ba6uq", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 总览", + "descendants": { + "F1F9E": { + "icon": "layout-dashboard", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "总览", + "fill": "$text-tertiary" + } + } + }, + { + "id": "G6OEHF", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 容器", + "descendants": { + "cQhxS": { + "fill": "$accent-dim" + }, + "F1F9E": { + "icon": "box", + "fill": "$accent" + }, + "DE5G9": { + "content": "容器", + "fill": "$accent" + } + } + }, + { + "id": "qcZes", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 镜像", + "descendants": { + "F1F9E": { + "icon": "layers", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "镜像", + "fill": "$text-tertiary" + } + } + }, + { + "id": "RxoQt", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 卷", + "descendants": { + "F1F9E": { + "icon": "hard-drive", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "卷", + "fill": "$text-tertiary" + } + } + }, + { + "id": "WV7gf", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 网络", + "descendants": { + "F1F9E": { + "icon": "network", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "网络", + "fill": "$text-tertiary" + } + } + }, + { + "id": "B3kc5b", + "type": "ref", + "ref": "nKcgr", + "name": "Nav Compose", + "descendants": { + "F1F9E": { + "icon": "boxes", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "Compose", + "fill": "$text-tertiary" + } + } + }, + { + "id": "DqKQe", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 系统", + "descendants": { + "F1F9E": { + "icon": "gauge", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "系统", + "fill": "$text-tertiary" + } + } + }, + { + "type": "frame", + "id": "z8dDTZ", + "name": "Spacer", + "width": 1, + "height": "fill_container", + "fill": "#00000000" + }, + { + "id": "Q2DEor", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 设置", + "descendants": { + "F1F9E": { + "icon": "settings" + }, + "DE5G9": { + "content": "设置" + } + } + } + ] + }, + { + "type": "frame", + "id": "ugU8K", + "name": "Main", + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "srmQ0", + "name": "Toolbar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 9, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "B9iQw", + "name": "Title", + "fill": "$text-primary", + "content": "容器", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "text", + "id": "gxBc6", + "name": "Count", + "fill": "$text-muted", + "content": "18", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "UtvVG", + "name": "Filter Segmented", + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 2, + "padding": 2, + "children": [ + { + "id": "Uuz5i", + "type": "ref", + "ref": "MdmRT", + "name": "F 全部 18", + "height": 20, + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "全部 18", + "fontSize": 11, + "fill": "$text-primary" + } + } + }, + { + "id": "rGm4w", + "type": "ref", + "ref": "MdmRT", + "name": "F 运行中 12", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "运行中 12", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + }, + { + "id": "r1IGj", + "type": "ref", + "ref": "MdmRT", + "name": "F 已停止 5", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "已停止 5", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + }, + { + "id": "dODIS", + "type": "ref", + "ref": "MdmRT", + "name": "F 异常 1", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "异常 1", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + } + ] + }, + { + "type": "frame", + "id": "LIq1I", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "KIdlf", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 拉取镜像", + "descendants": { + "RQ8pM": { + "icon": "arrow-down-to-line" + }, + "P0aUw": { + "content": "拉取镜像" + } + } + }, + { + "id": "XFEYT", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 新建 Compose", + "descendants": { + "RQ8pM": { + "icon": "file-code" + }, + "P0aUw": { + "content": "新建 Compose" + } + } + }, + { + "id": "z7B0tn", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 运行容器", + "fill": "$accent", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "plus", + "fill": "$accent-on" + }, + "P0aUw": { + "content": "运行容器…", + "fill": "$accent-on", + "fontWeight": "500" + } + } + }, + { + "id": "x9C8n", + "type": "ref", + "ref": "yoK0T", + "name": "Refresh", + "descendants": { + "XMGqY": { + "icon": "refresh-cw" + } + } + } + ] + }, + { + "type": "frame", + "id": "JQilM", + "name": "Selection Bar", + "width": "fill_container", + "height": 32, + "fill": "$accent-dim", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 7, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "g60yP", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "check", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "lvYU8", + "name": "Count", + "fill": "$accent", + "content": "已选 2 个容器", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "rectangle", + "id": "PUAMu", + "name": "Divider", + "fill": "$border-strong", + "width": 1, + "height": 16 + }, + { + "id": "t8004", + "type": "ref", + "ref": "i1OTz", + "name": "S 启动", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "play" + }, + "P0aUw": { + "content": "启动", + "fontSize": 11 + } + } + }, + { + "id": "l430jJ", + "type": "ref", + "ref": "i1OTz", + "name": "S 停止", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "square" + }, + "P0aUw": { + "content": "停止", + "fontSize": 11 + } + } + }, + { + "id": "Ugw8N", + "type": "ref", + "ref": "i1OTz", + "name": "S 重启", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "rotate-cw" + }, + "P0aUw": { + "content": "重启", + "fontSize": 11 + } + } + }, + { + "id": "RrxXW", + "type": "ref", + "ref": "i1OTz", + "name": "S 暂停", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "pause" + }, + "P0aUw": { + "content": "暂停", + "fontSize": 11 + } + } + }, + { + "id": "sQPP8", + "type": "ref", + "ref": "i1OTz", + "name": "S 删除", + "height": 22, + "stroke": "$danger-dim", + "descendants": { + "RQ8pM": { + "icon": "trash-2", + "fill": "$danger" + }, + "P0aUw": { + "content": "删除", + "fontSize": 11, + "fill": "$danger" + } + } + }, + { + "type": "frame", + "id": "z31qke", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "OfJVP", + "name": "Clear", + "fill": "$text-tertiary", + "content": "取消选择", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "c67k9e", + "name": "Header Row", + "width": "fill_container", + "height": 26, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "k6wso", + "name": "Pad", + "width": 3, + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "C0TdS", + "name": "H Select", + "width": 26, + "height": 26, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "boIA2", + "name": "Checkbox", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + } + ] + }, + { + "type": "frame", + "id": "yZLoX", + "name": "H 名称", + "clip": true, + "width": "fill_container", + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "uGtKp", + "name": "Label", + "fill": "$text-muted", + "content": "名称", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "WQVXn", + "name": "H 镜像", + "clip": true, + "width": 190, + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "D4ONAb", + "name": "Label", + "fill": "$text-muted", + "content": "镜像", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "QDJG1", + "name": "H 端口", + "clip": true, + "width": 118, + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "T5Rta", + "name": "Label", + "fill": "$text-muted", + "content": "端口", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "umbu8", + "name": "H CPU", + "clip": true, + "width": 108, + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "pEBix", + "name": "Label", + "fill": "$text-muted", + "content": "CPU", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "hsUb9", + "name": "H 内存", + "clip": true, + "width": 84, + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "z9w18B", + "name": "Label", + "fill": "$text-muted", + "content": "内存", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "pqRhI", + "name": "H 运行时长", + "clip": true, + "width": 78, + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "i2MbJr", + "name": "Label", + "fill": "$text-muted", + "content": "运行时长", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "uyyZW", + "name": "H 操作", + "clip": true, + "width": 92, + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center" + } + ] + }, + { + "type": "frame", + "id": "FH7Fs", + "name": "List", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "id": "jfJ5c", + "type": "ref", + "ref": "OHo2s", + "name": "Row nginx-proxy", + "width": "fill_container", + "fill": "$bg-active", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "nginx-proxy", + "fill": "$text-primary" + }, + "VJq0n/ODVko": { + "content": "web-stack" + }, + "p4yL1G": { + "content": "nginx:1.27-alpine", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "80→8080" + }, + "rANgd/fshVw": { + "height": 4, + "fill": "$accent" + }, + "rANgd/srvmw": { + "height": 3, + "fill": "$accent" + }, + "rANgd/ky1P1": { + "height": 5, + "fill": "$accent" + }, + "rANgd/bqMzi": { + "height": 4, + "fill": "$accent" + }, + "rANgd/F5Kloq": { + "height": 6, + "fill": "$accent" + }, + "rANgd/StdGN": { + "height": 4, + "fill": "$accent" + }, + "rANgd/iq3Ux": { + "height": 3, + "fill": "$accent" + }, + "rANgd/PHY2v": { + "height": 5, + "fill": "$accent" + }, + "rANgd/xqaP5": { + "height": 7, + "fill": "$accent" + }, + "rANgd/cZHdc": { + "height": 4, + "fill": "$accent" + }, + "rANgd/jhPqP": { + "height": 3, + "fill": "$accent" + }, + "rANgd/Za8Yw": { + "height": 4, + "fill": "$accent" + }, + "rANgd/zbp8o": { + "height": 5, + "fill": "$accent" + }, + "rANgd/kMt0y": { + "height": 4, + "fill": "$accent" + }, + "NprKu": { + "content": "2.1%", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "128 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "3d 4h" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "l6raWh", + "type": "ref", + "ref": "OHo2s", + "name": "Row api-gateway", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "api-gateway", + "fill": "$text-primary" + }, + "VJq0n/ODVko": { + "content": "web-stack" + }, + "p4yL1G": { + "content": "ghcr.io/acme/api:2.8.1", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "8000→8000" + }, + "rANgd/fshVw": { + "height": 6, + "fill": "$accent" + }, + "rANgd/srvmw": { + "height": 8, + "fill": "$accent" + }, + "rANgd/ky1P1": { + "height": 7, + "fill": "$accent" + }, + "rANgd/bqMzi": { + "height": 9, + "fill": "$accent" + }, + "rANgd/F5Kloq": { + "height": 11, + "fill": "$accent" + }, + "rANgd/StdGN": { + "height": 8, + "fill": "$accent" + }, + "rANgd/iq3Ux": { + "height": 6, + "fill": "$accent" + }, + "rANgd/PHY2v": { + "height": 7, + "fill": "$accent" + }, + "rANgd/xqaP5": { + "height": 10, + "fill": "$accent" + }, + "rANgd/cZHdc": { + "height": 12, + "fill": "$accent" + }, + "rANgd/jhPqP": { + "height": 9, + "fill": "$accent" + }, + "rANgd/Za8Yw": { + "height": 7, + "fill": "$accent" + }, + "rANgd/zbp8o": { + "height": 8, + "fill": "$accent" + }, + "rANgd/kMt0y": { + "height": 10, + "fill": "$accent" + }, + "NprKu": { + "content": "14.6%", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "512 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "3d 4h" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "HcXBg", + "type": "ref", + "ref": "OHo2s", + "name": "Row postgres-main", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "postgres-main", + "fill": "$text-primary" + }, + "VJq0n/ODVko": { + "content": "db-stack" + }, + "p4yL1G": { + "content": "postgres:16.4", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "5432" + }, + "rANgd/fshVw": { + "height": 3, + "fill": "$accent" + }, + "rANgd/srvmw": { + "height": 4, + "fill": "$accent" + }, + "rANgd/ky1P1": { + "height": 3, + "fill": "$accent" + }, + "rANgd/bqMzi": { + "height": 5, + "fill": "$accent" + }, + "rANgd/F5Kloq": { + "height": 4, + "fill": "$accent" + }, + "rANgd/StdGN": { + "height": 3, + "fill": "$accent" + }, + "rANgd/iq3Ux": { + "height": 4, + "fill": "$accent" + }, + "rANgd/PHY2v": { + "height": 6, + "fill": "$accent" + }, + "rANgd/xqaP5": { + "height": 4, + "fill": "$accent" + }, + "rANgd/cZHdc": { + "height": 3, + "fill": "$accent" + }, + "rANgd/jhPqP": { + "height": 4, + "fill": "$accent" + }, + "rANgd/Za8Yw": { + "height": 5, + "fill": "$accent" + }, + "rANgd/zbp8o": { + "height": 3, + "fill": "$accent" + }, + "rANgd/kMt0y": { + "height": 4, + "fill": "$accent" + }, + "NprKu": { + "content": "4.2%", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "1.2 GB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "12d 6h" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "T3OqM", + "type": "ref", + "ref": "OHo2s", + "name": "Row redis-cache", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "redis-cache", + "fill": "$text-primary" + }, + "VJq0n/ODVko": { + "content": "db-stack" + }, + "p4yL1G": { + "content": "redis:7.4-alpine", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "6379" + }, + "rANgd/fshVw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/srvmw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/ky1P1": { + "height": 3, + "fill": "$accent" + }, + "rANgd/bqMzi": { + "height": 2, + "fill": "$accent" + }, + "rANgd/F5Kloq": { + "height": 2, + "fill": "$accent" + }, + "rANgd/StdGN": { + "height": 3, + "fill": "$accent" + }, + "rANgd/iq3Ux": { + "height": 2, + "fill": "$accent" + }, + "rANgd/PHY2v": { + "height": 2, + "fill": "$accent" + }, + "rANgd/xqaP5": { + "height": 4, + "fill": "$accent" + }, + "rANgd/cZHdc": { + "height": 2, + "fill": "$accent" + }, + "rANgd/jhPqP": { + "height": 2, + "fill": "$accent" + }, + "rANgd/Za8Yw": { + "height": 3, + "fill": "$accent" + }, + "rANgd/zbp8o": { + "height": 2, + "fill": "$accent" + }, + "rANgd/kMt0y": { + "height": 2, + "fill": "$accent" + }, + "NprKu": { + "content": "0.8%", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "64 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "12d 6h" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "pl2n0", + "type": "ref", + "ref": "OHo2s", + "name": "Row worker-1", + "width": "fill_container", + "fill": "$accent-dim", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "T31V2": { + "fill": "$accent", + "stroke": "$accent" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "worker-1", + "fill": "$text-primary" + }, + "VJq0n/ODVko": { + "content": "jobs" + }, + "p4yL1G": { + "content": "ghcr.io/acme/worker:2.8.1", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "—" + }, + "rANgd/fshVw": { + "height": 9, + "fill": "$warn" + }, + "rANgd/srvmw": { + "height": 12, + "fill": "$warn" + }, + "rANgd/ky1P1": { + "height": 14, + "fill": "$warn" + }, + "rANgd/bqMzi": { + "height": 11, + "fill": "$warn" + }, + "rANgd/F5Kloq": { + "height": 15, + "fill": "$warn" + }, + "rANgd/StdGN": { + "height": 13, + "fill": "$warn" + }, + "rANgd/iq3Ux": { + "height": 10, + "fill": "$warn" + }, + "rANgd/PHY2v": { + "height": 14, + "fill": "$warn" + }, + "rANgd/xqaP5": { + "height": 15, + "fill": "$warn" + }, + "rANgd/cZHdc": { + "height": 12, + "fill": "$warn" + }, + "rANgd/jhPqP": { + "height": 13, + "fill": "$warn" + }, + "rANgd/Za8Yw": { + "height": 15, + "fill": "$warn" + }, + "rANgd/zbp8o": { + "height": 11, + "fill": "$warn" + }, + "rANgd/kMt0y": { + "height": 14, + "fill": "$warn" + }, + "NprKu": { + "content": "38.4%", + "fill": "$warn" + }, + "AdR38": { + "content": "890 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "6h 12m" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "n4yxHj", + "type": "ref", + "ref": "OHo2s", + "name": "Row worker-2", + "width": "fill_container", + "fill": "$accent-dim", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "T31V2": { + "fill": "$accent", + "stroke": "$accent" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "worker-2", + "fill": "$text-primary" + }, + "VJq0n/ODVko": { + "content": "jobs" + }, + "p4yL1G": { + "content": "ghcr.io/acme/worker:2.8.1", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "—" + }, + "rANgd/fshVw": { + "height": 8, + "fill": "$warn" + }, + "rANgd/srvmw": { + "height": 11, + "fill": "$warn" + }, + "rANgd/ky1P1": { + "height": 13, + "fill": "$warn" + }, + "rANgd/bqMzi": { + "height": 10, + "fill": "$warn" + }, + "rANgd/F5Kloq": { + "height": 12, + "fill": "$warn" + }, + "rANgd/StdGN": { + "height": 14, + "fill": "$warn" + }, + "rANgd/iq3Ux": { + "height": 9, + "fill": "$warn" + }, + "rANgd/PHY2v": { + "height": 11, + "fill": "$warn" + }, + "rANgd/xqaP5": { + "height": 13, + "fill": "$warn" + }, + "rANgd/cZHdc": { + "height": 10, + "fill": "$warn" + }, + "rANgd/jhPqP": { + "height": 12, + "fill": "$warn" + }, + "rANgd/Za8Yw": { + "height": 11, + "fill": "$warn" + }, + "rANgd/zbp8o": { + "height": 13, + "fill": "$warn" + }, + "rANgd/kMt0y": { + "height": 10, + "fill": "$warn" + }, + "NprKu": { + "content": "31.7%", + "fill": "$warn" + }, + "AdR38": { + "content": "812 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "6h 12m" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "X66AsD", + "type": "ref", + "ref": "OHo2s", + "name": "Row minio", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$warn" + }, + "yMbTn": { + "fill": "$warn" + }, + "L6tp5": { + "content": "minio", + "fill": "$text-primary" + }, + "VJq0n": { + "enabled": false + }, + "p4yL1G": { + "content": "minio/minio:RELEASE.2024-09", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "9000→9000" + }, + "rANgd": { + "enabled": false + }, + "NprKu": { + "content": "已暂停", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "210 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "2d 1h" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "Wb1gk", + "type": "ref", + "ref": "OHo2s", + "name": "Row grafana", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$text-muted" + }, + "yMbTn": { + "fill": "$text-muted" + }, + "L6tp5": { + "content": "grafana", + "fill": "$text-tertiary" + }, + "VJq0n/ODVko": { + "content": "observability" + }, + "p4yL1G": { + "content": "grafana/grafana:11.2.0", + "fill": "$text-muted" + }, + "y2nlk": { + "content": "3000" + }, + "rANgd": { + "enabled": false + }, + "NprKu": { + "content": "—", + "fill": "$text-muted" + }, + "AdR38": { + "content": "—", + "fill": "$text-muted" + }, + "PeLHA": { + "content": "停止 5h" + }, + "ALIhr/XMGqY": { + "icon": "play" + } + } + }, + { + "id": "FZIOq", + "type": "ref", + "ref": "OHo2s", + "name": "Row loki", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$text-muted" + }, + "yMbTn": { + "fill": "$text-muted" + }, + "L6tp5": { + "content": "loki", + "fill": "$text-tertiary" + }, + "VJq0n/ODVko": { + "content": "observability" + }, + "p4yL1G": { + "content": "grafana/loki:3.1.1", + "fill": "$text-muted" + }, + "y2nlk": { + "content": "3100" + }, + "rANgd": { + "enabled": false + }, + "NprKu": { + "content": "—", + "fill": "$text-muted" + }, + "AdR38": { + "content": "—", + "fill": "$text-muted" + }, + "PeLHA": { + "content": "停止 5h" + }, + "ALIhr/XMGqY": { + "icon": "play" + } + } + }, + { + "id": "U9Gdyv", + "type": "ref", + "ref": "OHo2s", + "name": "Row certbot", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$danger" + }, + "yMbTn": { + "fill": "$danger" + }, + "L6tp5": { + "content": "certbot", + "fill": "$text-tertiary" + }, + "VJq0n": { + "enabled": false + }, + "p4yL1G": { + "content": "certbot/certbot:v2.11.0", + "fill": "$text-muted" + }, + "y2nlk": { + "content": "—" + }, + "rANgd": { + "enabled": false + }, + "NprKu": { + "content": "—", + "fill": "$text-muted" + }, + "AdR38": { + "content": "—", + "fill": "$text-muted" + }, + "PeLHA": { + "content": "退出 (1)" + }, + "ALIhr/XMGqY": { + "icon": "play" + } + } + }, + { + "id": "uG5g2", + "type": "ref", + "ref": "OHo2s", + "name": "Row registry-mirror", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "registry-mirror", + "fill": "$text-primary" + }, + "VJq0n": { + "enabled": false + }, + "p4yL1G": { + "content": "registry:2.8.3", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "5000→5000" + }, + "rANgd/fshVw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/srvmw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/ky1P1": { + "height": 2, + "fill": "$accent" + }, + "rANgd/bqMzi": { + "height": 3, + "fill": "$accent" + }, + "rANgd/F5Kloq": { + "height": 2, + "fill": "$accent" + }, + "rANgd/StdGN": { + "height": 2, + "fill": "$accent" + }, + "rANgd/iq3Ux": { + "height": 2, + "fill": "$accent" + }, + "rANgd/PHY2v": { + "height": 2, + "fill": "$accent" + }, + "rANgd/xqaP5": { + "height": 3, + "fill": "$accent" + }, + "rANgd/cZHdc": { + "height": 2, + "fill": "$accent" + }, + "rANgd/jhPqP": { + "height": 2, + "fill": "$accent" + }, + "rANgd/Za8Yw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/zbp8o": { + "height": 2, + "fill": "$accent" + }, + "rANgd/kMt0y": { + "height": 2, + "fill": "$accent" + }, + "NprKu": { + "content": "0.4%", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "48 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "21d" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "C8r8iB", + "type": "ref", + "ref": "OHo2s", + "name": "Row watchtower", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "watchtower", + "fill": "$text-primary" + }, + "VJq0n": { + "enabled": false + }, + "p4yL1G": { + "content": "containrrr/watchtower:1.7.1", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "—" + }, + "rANgd/fshVw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/srvmw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/ky1P1": { + "height": 2, + "fill": "$accent" + }, + "rANgd/bqMzi": { + "height": 2, + "fill": "$accent" + }, + "rANgd/F5Kloq": { + "height": 2, + "fill": "$accent" + }, + "rANgd/StdGN": { + "height": 2, + "fill": "$accent" + }, + "rANgd/iq3Ux": { + "height": 2, + "fill": "$accent" + }, + "rANgd/PHY2v": { + "height": 2, + "fill": "$accent" + }, + "rANgd/xqaP5": { + "height": 2, + "fill": "$accent" + }, + "rANgd/cZHdc": { + "height": 2, + "fill": "$accent" + }, + "rANgd/jhPqP": { + "height": 2, + "fill": "$accent" + }, + "rANgd/Za8Yw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/zbp8o": { + "height": 2, + "fill": "$accent" + }, + "rANgd/kMt0y": { + "height": 2, + "fill": "$accent" + }, + "NprKu": { + "content": "0.1%", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "22 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "21d" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "VNeOl", + "name": "Detail Drawer", + "width": 440, + "height": "fill_container", + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "left": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "N0U1U", + "name": "Drawer Header", + "width": "fill_container", + "height": 44, + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "h64ZM", + "name": "Dot", + "fill": "$ok", + "width": 7, + "height": 7 + }, + { + "type": "text", + "id": "P8s7H", + "name": "Name", + "fill": "$text-primary", + "content": "nginx-proxy", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "id": "PpOUK", + "type": "ref", + "ref": "x0Pcos", + "name": "State Chip", + "descendants": { + "ODVko": { + "content": "运行中 · healthy" + } + } + }, + { + "type": "frame", + "id": "cKweG", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "sBiDa", + "type": "ref", + "ref": "yoK0T", + "name": "H pin", + "descendants": { + "XMGqY": { + "icon": "pin" + } + } + }, + { + "id": "lQZVx", + "type": "ref", + "ref": "yoK0T", + "name": "H maximize-2", + "descendants": { + "XMGqY": { + "icon": "maximize-2" + } + } + }, + { + "id": "XOQBZ", + "type": "ref", + "ref": "yoK0T", + "name": "H x", + "descendants": { + "XMGqY": { + "icon": "x" + } + } + } + ] + }, + { + "type": "frame", + "id": "hI0aP", + "name": "Drawer Subhead", + "width": "fill_container", + "height": 26, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 7, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "D2PHF", + "name": "Id", + "fill": "$text-tertiary", + "content": "a3f2c81b9d4e", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "go309", + "name": "Copy", + "width": 11, + "height": 11, + "icon": "copy", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "PkWtT", + "name": "Sep", + "fill": "$text-muted", + "content": "·", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "EPM94", + "name": "Proj", + "fill": "$violet", + "content": "web-stack / proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "w9FG1", + "name": "Drawer Tabs", + "width": "fill_container", + "height": 34, + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 2, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "id": "C0MvWh", + "type": "ref", + "ref": "MdmRT", + "name": "T 概览", + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "icon": "info", + "fill": "$text-primary" + }, + "iscyV": { + "content": "概览", + "fill": "$text-primary", + "fontWeight": "500" + } + } + }, + { + "id": "asY1g", + "type": "ref", + "ref": "MdmRT", + "name": "T 日志", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "scroll-text", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "日志", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + }, + { + "id": "csQv4", + "type": "ref", + "ref": "MdmRT", + "name": "T 文件", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "folder-tree", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "文件", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + }, + { + "id": "OPho9", + "type": "ref", + "ref": "MdmRT", + "name": "T 终端", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "terminal", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "终端", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + }, + { + "id": "xK0Qf", + "type": "ref", + "ref": "MdmRT", + "name": "T 统计", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "activity", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "统计", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + } + ] + }, + { + "type": "frame", + "id": "admPP", + "name": "Quick Actions", + "width": "fill_container", + "height": 42, + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "id": "E8ps2i", + "type": "ref", + "ref": "i1OTz", + "name": "Q 停止", + "descendants": { + "RQ8pM": { + "icon": "square" + }, + "P0aUw": { + "content": "停止" + } + } + }, + { + "id": "d5wzA1", + "type": "ref", + "ref": "i1OTz", + "name": "Q 重启", + "descendants": { + "RQ8pM": { + "icon": "rotate-cw" + }, + "P0aUw": { + "content": "重启" + } + } + }, + { + "id": "hsahv", + "type": "ref", + "ref": "i1OTz", + "name": "Q 暂停", + "descendants": { + "RQ8pM": { + "icon": "pause" + }, + "P0aUw": { + "content": "暂停" + } + } + }, + { + "id": "xwjwm", + "type": "ref", + "ref": "i1OTz", + "name": "Q 终端", + "fill": "$accent-dim", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "terminal", + "fill": "$accent" + }, + "P0aUw": { + "content": "进入终端", + "fill": "$accent" + } + } + }, + { + "type": "frame", + "id": "zTPsT", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "oZzOZ", + "type": "ref", + "ref": "yoK0T", + "name": "More", + "descendants": { + "XMGqY": { + "icon": "ellipsis" + } + } + } + ] + }, + { + "type": "frame", + "id": "JDWYM", + "name": "Drawer Content", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-elevated", + "layout": "vertical", + "gap": 10, + "padding": [ + 12, + 14 + ], + "children": [ + { + "type": "frame", + "id": "krEXe", + "name": "Live Metrics", + "width": "fill_container", + "gap": 10, + "children": [ + { + "type": "frame", + "id": "Cx0iv", + "name": "M CPU", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 6, + "padding": [ + 10, + 11 + ], + "children": [ + { + "type": "frame", + "id": "cLZia", + "name": "Top", + "width": "fill_container", + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "OJJMc", + "name": "Label", + "fill": "$text-tertiary", + "content": "CPU", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "IRSB1", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "ZK6mS", + "name": "Delta", + "fill": "$text-muted", + "content": "峰值 6.4%", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "WPauu", + "name": "Value", + "gap": 3, + "alignItems": "end", + "children": [ + { + "type": "text", + "id": "V2l7OY", + "name": "Num", + "fill": "$text-primary", + "content": "2.1", + "fontFamily": "$font-ui", + "fontSize": 20, + "fontWeight": "600" + }, + { + "type": "text", + "id": "QzDQ3", + "name": "Unit", + "fill": "$text-tertiary", + "content": "%", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "p18Bbk", + "type": "ref", + "ref": "MhYIL", + "name": "Spark", + "height": 30, + "descendants": { + "fshVw": { + "height": 8, + "width": 9, + "fill": "$accent" + }, + "srvmw": { + "height": 6, + "width": 9, + "fill": "$accent" + }, + "ky1P1": { + "height": 11, + "width": 9, + "fill": "$accent" + }, + "bqMzi": { + "height": 7, + "width": 9, + "fill": "$accent" + }, + "F5Kloq": { + "height": 14, + "width": 9, + "fill": "$accent" + }, + "StdGN": { + "height": 9, + "width": 9, + "fill": "$accent" + }, + "iq3Ux": { + "height": 6, + "width": 9, + "fill": "$accent" + }, + "PHY2v": { + "height": 10, + "width": 9, + "fill": "$accent" + }, + "xqaP5": { + "height": 18, + "width": 9, + "fill": "$accent" + }, + "cZHdc": { + "height": 8, + "width": 9, + "fill": "$accent" + }, + "jhPqP": { + "height": 7, + "width": 9, + "fill": "$accent" + }, + "Za8Yw": { + "height": 9, + "width": 9, + "fill": "$accent" + }, + "zbp8o": { + "height": 12, + "width": 9, + "fill": "$accent" + }, + "kMt0y": { + "height": 8, + "width": 9, + "fill": "$accent" + } + } + } + ] + }, + { + "type": "frame", + "id": "xMhUs", + "name": "M 内存", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 6, + "padding": [ + 10, + 11 + ], + "children": [ + { + "type": "frame", + "id": "xPXyu", + "name": "Top", + "width": "fill_container", + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Fsp9Z", + "name": "Label", + "fill": "$text-tertiary", + "content": "内存", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "cPsab", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "f6kcDa", + "name": "Delta", + "fill": "$text-muted", + "content": "24.9%", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "E9UlRN", + "name": "Value", + "gap": 3, + "alignItems": "end", + "children": [ + { + "type": "text", + "id": "KzVPc", + "name": "Num", + "fill": "$text-primary", + "content": "128", + "fontFamily": "$font-ui", + "fontSize": 20, + "fontWeight": "600" + }, + { + "type": "text", + "id": "e2yEPW", + "name": "Unit", + "fill": "$text-tertiary", + "content": "MB / 512", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "q92tc", + "type": "ref", + "ref": "MhYIL", + "name": "Spark", + "height": 30, + "descendants": { + "fshVw": { + "height": 11, + "width": 7, + "fill": "$ok" + }, + "srvmw": { + "height": 13, + "width": 7, + "fill": "$ok" + }, + "ky1P1": { + "height": 12, + "width": 7, + "fill": "$ok" + }, + "bqMzi": { + "height": 15, + "width": 7, + "fill": "$ok" + }, + "F5Kloq": { + "height": 14, + "width": 7, + "fill": "$ok" + }, + "StdGN": { + "height": 16, + "width": 7, + "fill": "$ok" + }, + "iq3Ux": { + "height": 15, + "width": 7, + "fill": "$ok" + }, + "PHY2v": { + "height": 18, + "width": 7, + "fill": "$ok" + }, + "xqaP5": { + "height": 17, + "width": 7, + "fill": "$ok" + }, + "cZHdc": { + "height": 16, + "width": 7, + "fill": "$ok" + }, + "jhPqP": { + "height": 19, + "width": 7, + "fill": "$ok" + }, + "Za8Yw": { + "height": 18, + "width": 7, + "fill": "$ok" + }, + "zbp8o": { + "height": 20, + "width": 7, + "fill": "$ok" + }, + "kMt0y": { + "height": 19, + "width": 7, + "fill": "$ok" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "egxKV", + "name": "Sec 基本信息", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "dzpJZ", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "VViFc", + "name": "Title", + "fill": "$text-secondary", + "content": "基本信息", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "mvuLz", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "ShytI", + "name": "Action", + "fill": "$accent", + "content": "完整 inspect", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "BJR2i", + "type": "ref", + "ref": "l3qo4", + "name": "KV 镜像", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "镜像" + }, + "psgr6": { + "content": "nginx:1.27-alpine", + "fill": "$text-primary" + } + } + }, + { + "id": "ITD0x", + "type": "ref", + "ref": "l3qo4", + "name": "KV 镜像摘要", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "镜像摘要" + }, + "psgr6": { + "content": "sha256:9c4f2e…1b7a", + "fill": "$text-primary" + } + } + }, + { + "id": "o5j5y", + "type": "ref", + "ref": "l3qo4", + "name": "KV 创建于", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "创建于" + }, + "psgr6": { + "content": "2026-08-18 09:12:44", + "fill": "$text-primary" + } + } + }, + { + "id": "Ap8My", + "type": "ref", + "ref": "l3qo4", + "name": "KV 启动于", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "启动于" + }, + "psgr6": { + "content": "3 天前 (3d 4h 21m)", + "fill": "$text-primary" + } + } + }, + { + "id": "P81Ykh", + "type": "ref", + "ref": "l3qo4", + "name": "KV 重启策略", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "重启策略" + }, + "psgr6": { + "content": "unless-stopped", + "fill": "$text-primary" + } + } + }, + { + "id": "r0lDb9", + "type": "ref", + "ref": "l3qo4", + "name": "KV 平台", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "平台" + }, + "psgr6": { + "content": "linux/amd64", + "fill": "$text-primary" + } + } + }, + { + "id": "HANzL", + "type": "ref", + "ref": "l3qo4", + "name": "KV 命令", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "命令" + }, + "psgr6": { + "content": "nginx -g 'daemon off;'", + "fill": "$text-primary" + } + } + } + ] + }, + { + "type": "rectangle", + "id": "HYEia", + "name": "Divider", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "dH0IW", + "name": "Sec 端口映射", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "aGfcq", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ZW9R4", + "name": "Title", + "fill": "$text-secondary", + "content": "端口映射", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "d43888", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "fdcUa", + "name": "Action", + "fill": "$accent", + "content": "编辑", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "lvqz4", + "type": "ref", + "ref": "l3qo4", + "name": "KV 8080/tcp", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "8080/tcp" + }, + "psgr6": { + "content": "0.0.0.0:8080 → 80", + "fill": "$info" + } + } + }, + { + "id": "a0NPr", + "type": "ref", + "ref": "l3qo4", + "name": "KV 8443/tcp", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "8443/tcp" + }, + "psgr6": { + "content": "[::]:8443 → 443", + "fill": "$info" + } + } + } + ] + }, + { + "type": "rectangle", + "id": "mMaso", + "name": "Divider", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "GTDlG", + "name": "Sec 挂载", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "sBNtu", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "P0uWi", + "name": "Title", + "fill": "$text-secondary", + "content": "挂载", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "UETZ3", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Ips3H", + "name": "Action", + "fill": "$accent", + "content": "浏览文件", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "A8HJy", + "name": "Mount", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "GeFAU", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "folder", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "Cx5wP", + "name": "Path", + "fill": "$text-secondary", + "content": "/srv/nginx/conf.d → /etc/nginx/conf.d", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "yVKeE", + "type": "ref", + "ref": "x0Pcos", + "name": "Tag", + "height": 15, + "fill": "#00000000", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "只读", + "fill": "$warn", + "fontSize": 9 + } + } + } + ] + }, + { + "type": "frame", + "id": "aXOIg", + "name": "Mount", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "SeVPQ", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "database", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "CzDmp", + "name": "Path", + "fill": "$text-secondary", + "content": "nginx-cache → /var/cache/nginx", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "UqUu3", + "type": "ref", + "ref": "x0Pcos", + "name": "Tag", + "height": 15, + "fill": "#00000000", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "读写", + "fill": "$ok", + "fontSize": 9 + } + } + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "DVLsi", + "name": "Divider", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "m3AOc", + "name": "Sec 网络", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "c3afBJ", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ZMRmx", + "name": "Title", + "fill": "$text-secondary", + "content": "网络", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "yxd2c", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "JEoMo", + "name": "Action", + "fill": "$accent", + "content": "接入其他网络", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "ii0Q9", + "type": "ref", + "ref": "l3qo4", + "name": "KV web-stack_default", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "网络" + }, + "psgr6": { + "content": "web-stack_default", + "fill": "$text-primary" + } + } + }, + { + "id": "dbGQs", + "type": "ref", + "ref": "l3qo4", + "name": "KV IP", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "IP 地址" + }, + "psgr6": { + "content": "172.20.0.4 · bridge · MTU 1500" + } + } + } + ] + }, + { + "type": "rectangle", + "id": "bmsbM", + "name": "Divider", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "vpieA", + "name": "Sec 健康检查", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "G5W1S", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "a7Lcxy", + "name": "Title", + "fill": "$text-secondary", + "content": "健康检查", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "suAZw", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "JnxT2", + "name": "HC", + "width": "fill_container", + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "id": "a40us", + "type": "ref", + "ref": "x0Pcos", + "name": "HC Chip", + "descendants": { + "ODVko": { + "content": "healthy" + } + } + }, + { + "type": "text", + "id": "L1JmOm", + "name": "Detail", + "fill": "$text-tertiary", + "content": "每 30s · 0 次失败 · 12s 前", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "mRh9x", + "name": "Status Bar", + "width": "fill_container", + "height": 24, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 14, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "zndfR", + "name": "Msg", + "fill": "$text-tertiary", + "content": "已停止 2 个容器 · redis-cache, worker-1", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "GtRry", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "NTJcz", + "name": "API v1.47", + "fill": "$text-muted", + "content": "API v1.47", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "X0lKD", + "name": "Engine 27.3.1", + "fill": "$text-muted", + "content": "Engine 27.3.1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "gu9om", + "name": "18 容器 · 34 镜像 · 9 卷", + "fill": "$text-muted", + "content": "18 容器 · 34 镜像 · 9 卷", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "GznQh", + "name": "事件流 已连接", + "fill": "$text-muted", + "content": "事件流 已连接", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "J3Yhjo", + "x": 2619, + "y": 0, + "name": "02 · 容器 · 亮色", + "theme": { + "mode": "light" + }, + "clip": true, + "width": 1440, + "height": 860, + "fill": "$bg-page", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "dyQOx", + "name": "Top Bar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Ky9FP", + "name": "Brand", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "k8bNzc", + "name": "Logo", + "width": 17, + "height": 17, + "icon": "container", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "o1DXP", + "name": "Title", + "fill": "$text-primary", + "content": "Docker", + "fontFamily": "$font-ui", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "rectangle", + "id": "bYGlC", + "name": "Divider", + "fill": "$border", + "width": 1, + "height": 18 + }, + { + "type": "frame", + "id": "VRMP4", + "name": "Host Switcher", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "ww5mK", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "server", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "wFgtk", + "name": "Host", + "fill": "$text-primary", + "content": "prod-sg-01", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "ub8yJ", + "name": "Transport", + "fill": "$text-muted", + "content": "/var/run/docker.sock · SSH 隧道", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "W9Zhu", + "name": "Caret", + "width": 12, + "height": 12, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + }, + { + "type": "frame", + "id": "Da0GU", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "y2PzQY", + "name": "Search", + "width": 300, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "rmX9b", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "search", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "R8QSmQ", + "name": "Placeholder", + "fill": "$text-muted", + "content": "搜索容器、镜像、卷、网络…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "cRLI2", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "OnCKs", + "name": "Kbd", + "height": 16, + "fill": "$bg-hover", + "cornerRadius": 3, + "padding": [ + 0, + 5 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "sMfUm", + "name": "Keys", + "fill": "$text-tertiary", + "content": "Ctrl K", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "id": "A2ZYCu", + "type": "ref", + "ref": "x0Pcos", + "name": "Live Badge", + "descendants": { + "ODVko": { + "content": "实时" + } + } + }, + { + "id": "k0e6V", + "type": "ref", + "ref": "yoK0T", + "name": "Settings", + "descendants": { + "XMGqY": { + "icon": "settings" + } + } + } + ] + }, + { + "type": "frame", + "id": "e1D1YO", + "name": "Body", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "LMPqF", + "name": "Rail", + "width": 56, + "height": "fill_container", + "fill": "$bg-rail", + "stroke": "$border", + "strokeWidth": { + "right": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 2, + "padding": [ + 6, + 0 + ], + "alignItems": "center", + "children": [ + { + "id": "U4YSQ", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 总览", + "descendants": { + "F1F9E": { + "icon": "layout-dashboard", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "总览", + "fill": "$text-tertiary" + } + } + }, + { + "id": "Ow4Cn", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 容器", + "descendants": { + "cQhxS": { + "fill": "$accent-dim" + }, + "F1F9E": { + "icon": "box", + "fill": "$accent" + }, + "DE5G9": { + "content": "容器", + "fill": "$accent" + } + } + }, + { + "id": "Qza4J", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 镜像", + "descendants": { + "F1F9E": { + "icon": "layers", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "镜像", + "fill": "$text-tertiary" + } + } + }, + { + "id": "pJpDo", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 卷", + "descendants": { + "F1F9E": { + "icon": "hard-drive", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "卷", + "fill": "$text-tertiary" + } + } + }, + { + "id": "wcTnn", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 网络", + "descendants": { + "F1F9E": { + "icon": "network", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "网络", + "fill": "$text-tertiary" + } + } + }, + { + "id": "QUiof", + "type": "ref", + "ref": "nKcgr", + "name": "Nav Compose", + "descendants": { + "F1F9E": { + "icon": "boxes", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "Compose", + "fill": "$text-tertiary" + } + } + }, + { + "id": "xqJTG", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 系统", + "descendants": { + "F1F9E": { + "icon": "gauge", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "系统", + "fill": "$text-tertiary" + } + } + }, + { + "type": "frame", + "id": "HfJk1", + "name": "Spacer", + "width": 1, + "height": "fill_container", + "fill": "#00000000" + }, + { + "id": "A4Eil", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 设置", + "descendants": { + "F1F9E": { + "icon": "settings" + }, + "DE5G9": { + "content": "设置" + } + } + } + ] + }, + { + "type": "frame", + "id": "UOcsT", + "name": "Main", + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "M3ArvV", + "name": "Toolbar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 9, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "n25yT", + "name": "Title", + "fill": "$text-primary", + "content": "容器", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "text", + "id": "lwiUg", + "name": "Count", + "fill": "$text-muted", + "content": "18", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "u9M5d", + "name": "Filter Segmented", + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 2, + "padding": 2, + "children": [ + { + "id": "u3mMn", + "type": "ref", + "ref": "MdmRT", + "name": "F 全部 18", + "height": 20, + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "全部 18", + "fontSize": 11, + "fill": "$text-primary" + } + } + }, + { + "id": "i91VRy", + "type": "ref", + "ref": "MdmRT", + "name": "F 运行中 12", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "运行中 12", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + }, + { + "id": "Nh5Md", + "type": "ref", + "ref": "MdmRT", + "name": "F 已停止 5", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "已停止 5", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + }, + { + "id": "HTTxw", + "type": "ref", + "ref": "MdmRT", + "name": "F 异常 1", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "异常 1", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + } + ] + }, + { + "type": "frame", + "id": "HcyvH", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "IaCqz", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 拉取镜像", + "descendants": { + "RQ8pM": { + "icon": "arrow-down-to-line" + }, + "P0aUw": { + "content": "拉取镜像" + } + } + }, + { + "id": "phpF1", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 新建 Compose", + "descendants": { + "RQ8pM": { + "icon": "file-code" + }, + "P0aUw": { + "content": "新建 Compose" + } + } + }, + { + "id": "ShvXk", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 运行容器", + "fill": "$accent", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "plus", + "fill": "$accent-on" + }, + "P0aUw": { + "content": "运行容器…", + "fill": "$accent-on", + "fontWeight": "500" + } + } + }, + { + "id": "NjxYB", + "type": "ref", + "ref": "yoK0T", + "name": "Refresh", + "descendants": { + "XMGqY": { + "icon": "refresh-cw" + } + } + } + ] + }, + { + "type": "frame", + "id": "gaxyw", + "name": "Selection Bar", + "width": "fill_container", + "height": 32, + "fill": "$accent-dim", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 7, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "dZNVZ", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "check", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "NJgzs", + "name": "Count", + "fill": "$accent", + "content": "已选 2 个容器", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "rectangle", + "id": "ZMicf", + "name": "Divider", + "fill": "$border-strong", + "width": 1, + "height": 16 + }, + { + "id": "Uukju", + "type": "ref", + "ref": "i1OTz", + "name": "S 启动", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "play" + }, + "P0aUw": { + "content": "启动", + "fontSize": 11 + } + } + }, + { + "id": "PhsNu", + "type": "ref", + "ref": "i1OTz", + "name": "S 停止", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "square" + }, + "P0aUw": { + "content": "停止", + "fontSize": 11 + } + } + }, + { + "id": "ZGHme", + "type": "ref", + "ref": "i1OTz", + "name": "S 重启", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "rotate-cw" + }, + "P0aUw": { + "content": "重启", + "fontSize": 11 + } + } + }, + { + "id": "ypGyf", + "type": "ref", + "ref": "i1OTz", + "name": "S 暂停", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "pause" + }, + "P0aUw": { + "content": "暂停", + "fontSize": 11 + } + } + }, + { + "id": "y9j8xx", + "type": "ref", + "ref": "i1OTz", + "name": "S 删除", + "height": 22, + "stroke": "$danger-dim", + "descendants": { + "RQ8pM": { + "icon": "trash-2", + "fill": "$danger" + }, + "P0aUw": { + "content": "删除", + "fontSize": 11, + "fill": "$danger" + } + } + }, + { + "type": "frame", + "id": "lBODm", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "CVwfG", + "name": "Clear", + "fill": "$text-tertiary", + "content": "取消选择", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "XxS8l", + "name": "Header Row", + "width": "fill_container", + "height": 26, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "XhFCY", + "name": "Pad", + "width": 3, + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "Go4Tw", + "name": "H Select", + "width": 26, + "height": 26, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "ojUzl", + "name": "Checkbox", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + } + ] + }, + { + "type": "frame", + "id": "Hc1nb", + "name": "H 名称", + "clip": true, + "width": "fill_container", + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "U2dCAc", + "name": "Label", + "fill": "$text-muted", + "content": "名称", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "DrcnG", + "name": "H 镜像", + "clip": true, + "width": 190, + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "afgWg", + "name": "Label", + "fill": "$text-muted", + "content": "镜像", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "gfOTy", + "name": "H 端口", + "clip": true, + "width": 118, + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "UXzJa", + "name": "Label", + "fill": "$text-muted", + "content": "端口", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "oTk1j", + "name": "H CPU", + "clip": true, + "width": 108, + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "N3z5Pw", + "name": "Label", + "fill": "$text-muted", + "content": "CPU", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "p8C6h", + "name": "H 内存", + "clip": true, + "width": 84, + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "KYZWJ", + "name": "Label", + "fill": "$text-muted", + "content": "内存", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "OvY0p", + "name": "H 运行时长", + "clip": true, + "width": 78, + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "J8x0io", + "name": "Label", + "fill": "$text-muted", + "content": "运行时长", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "hGfK2", + "name": "H 操作", + "clip": true, + "width": 92, + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center" + } + ] + }, + { + "type": "frame", + "id": "YtWOY", + "name": "List", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "id": "Cjxaj", + "type": "ref", + "ref": "OHo2s", + "name": "Row nginx-proxy", + "width": "fill_container", + "fill": "$bg-active", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "nginx-proxy", + "fill": "$text-primary" + }, + "VJq0n/ODVko": { + "content": "web-stack" + }, + "p4yL1G": { + "content": "nginx:1.27-alpine", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "80→8080" + }, + "rANgd/fshVw": { + "height": 4, + "fill": "$accent" + }, + "rANgd/srvmw": { + "height": 3, + "fill": "$accent" + }, + "rANgd/ky1P1": { + "height": 5, + "fill": "$accent" + }, + "rANgd/bqMzi": { + "height": 4, + "fill": "$accent" + }, + "rANgd/F5Kloq": { + "height": 6, + "fill": "$accent" + }, + "rANgd/StdGN": { + "height": 4, + "fill": "$accent" + }, + "rANgd/iq3Ux": { + "height": 3, + "fill": "$accent" + }, + "rANgd/PHY2v": { + "height": 5, + "fill": "$accent" + }, + "rANgd/xqaP5": { + "height": 7, + "fill": "$accent" + }, + "rANgd/cZHdc": { + "height": 4, + "fill": "$accent" + }, + "rANgd/jhPqP": { + "height": 3, + "fill": "$accent" + }, + "rANgd/Za8Yw": { + "height": 4, + "fill": "$accent" + }, + "rANgd/zbp8o": { + "height": 5, + "fill": "$accent" + }, + "rANgd/kMt0y": { + "height": 4, + "fill": "$accent" + }, + "NprKu": { + "content": "2.1%", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "128 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "3d 4h" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "UxDcr", + "type": "ref", + "ref": "OHo2s", + "name": "Row api-gateway", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "api-gateway", + "fill": "$text-primary" + }, + "VJq0n/ODVko": { + "content": "web-stack" + }, + "p4yL1G": { + "content": "ghcr.io/acme/api:2.8.1", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "8000→8000" + }, + "rANgd/fshVw": { + "height": 6, + "fill": "$accent" + }, + "rANgd/srvmw": { + "height": 8, + "fill": "$accent" + }, + "rANgd/ky1P1": { + "height": 7, + "fill": "$accent" + }, + "rANgd/bqMzi": { + "height": 9, + "fill": "$accent" + }, + "rANgd/F5Kloq": { + "height": 11, + "fill": "$accent" + }, + "rANgd/StdGN": { + "height": 8, + "fill": "$accent" + }, + "rANgd/iq3Ux": { + "height": 6, + "fill": "$accent" + }, + "rANgd/PHY2v": { + "height": 7, + "fill": "$accent" + }, + "rANgd/xqaP5": { + "height": 10, + "fill": "$accent" + }, + "rANgd/cZHdc": { + "height": 12, + "fill": "$accent" + }, + "rANgd/jhPqP": { + "height": 9, + "fill": "$accent" + }, + "rANgd/Za8Yw": { + "height": 7, + "fill": "$accent" + }, + "rANgd/zbp8o": { + "height": 8, + "fill": "$accent" + }, + "rANgd/kMt0y": { + "height": 10, + "fill": "$accent" + }, + "NprKu": { + "content": "14.6%", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "512 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "3d 4h" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "gSx1n", + "type": "ref", + "ref": "OHo2s", + "name": "Row postgres-main", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "postgres-main", + "fill": "$text-primary" + }, + "VJq0n/ODVko": { + "content": "db-stack" + }, + "p4yL1G": { + "content": "postgres:16.4", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "5432" + }, + "rANgd/fshVw": { + "height": 3, + "fill": "$accent" + }, + "rANgd/srvmw": { + "height": 4, + "fill": "$accent" + }, + "rANgd/ky1P1": { + "height": 3, + "fill": "$accent" + }, + "rANgd/bqMzi": { + "height": 5, + "fill": "$accent" + }, + "rANgd/F5Kloq": { + "height": 4, + "fill": "$accent" + }, + "rANgd/StdGN": { + "height": 3, + "fill": "$accent" + }, + "rANgd/iq3Ux": { + "height": 4, + "fill": "$accent" + }, + "rANgd/PHY2v": { + "height": 6, + "fill": "$accent" + }, + "rANgd/xqaP5": { + "height": 4, + "fill": "$accent" + }, + "rANgd/cZHdc": { + "height": 3, + "fill": "$accent" + }, + "rANgd/jhPqP": { + "height": 4, + "fill": "$accent" + }, + "rANgd/Za8Yw": { + "height": 5, + "fill": "$accent" + }, + "rANgd/zbp8o": { + "height": 3, + "fill": "$accent" + }, + "rANgd/kMt0y": { + "height": 4, + "fill": "$accent" + }, + "NprKu": { + "content": "4.2%", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "1.2 GB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "12d 6h" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "uJVH2", + "type": "ref", + "ref": "OHo2s", + "name": "Row redis-cache", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "redis-cache", + "fill": "$text-primary" + }, + "VJq0n/ODVko": { + "content": "db-stack" + }, + "p4yL1G": { + "content": "redis:7.4-alpine", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "6379" + }, + "rANgd/fshVw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/srvmw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/ky1P1": { + "height": 3, + "fill": "$accent" + }, + "rANgd/bqMzi": { + "height": 2, + "fill": "$accent" + }, + "rANgd/F5Kloq": { + "height": 2, + "fill": "$accent" + }, + "rANgd/StdGN": { + "height": 3, + "fill": "$accent" + }, + "rANgd/iq3Ux": { + "height": 2, + "fill": "$accent" + }, + "rANgd/PHY2v": { + "height": 2, + "fill": "$accent" + }, + "rANgd/xqaP5": { + "height": 4, + "fill": "$accent" + }, + "rANgd/cZHdc": { + "height": 2, + "fill": "$accent" + }, + "rANgd/jhPqP": { + "height": 2, + "fill": "$accent" + }, + "rANgd/Za8Yw": { + "height": 3, + "fill": "$accent" + }, + "rANgd/zbp8o": { + "height": 2, + "fill": "$accent" + }, + "rANgd/kMt0y": { + "height": 2, + "fill": "$accent" + }, + "NprKu": { + "content": "0.8%", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "64 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "12d 6h" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "QQPy3", + "type": "ref", + "ref": "OHo2s", + "name": "Row worker-1", + "width": "fill_container", + "fill": "$accent-dim", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "T31V2": { + "fill": "$accent", + "stroke": "$accent" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "worker-1", + "fill": "$text-primary" + }, + "VJq0n/ODVko": { + "content": "jobs" + }, + "p4yL1G": { + "content": "ghcr.io/acme/worker:2.8.1", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "—" + }, + "rANgd/fshVw": { + "height": 9, + "fill": "$warn" + }, + "rANgd/srvmw": { + "height": 12, + "fill": "$warn" + }, + "rANgd/ky1P1": { + "height": 14, + "fill": "$warn" + }, + "rANgd/bqMzi": { + "height": 11, + "fill": "$warn" + }, + "rANgd/F5Kloq": { + "height": 15, + "fill": "$warn" + }, + "rANgd/StdGN": { + "height": 13, + "fill": "$warn" + }, + "rANgd/iq3Ux": { + "height": 10, + "fill": "$warn" + }, + "rANgd/PHY2v": { + "height": 14, + "fill": "$warn" + }, + "rANgd/xqaP5": { + "height": 15, + "fill": "$warn" + }, + "rANgd/cZHdc": { + "height": 12, + "fill": "$warn" + }, + "rANgd/jhPqP": { + "height": 13, + "fill": "$warn" + }, + "rANgd/Za8Yw": { + "height": 15, + "fill": "$warn" + }, + "rANgd/zbp8o": { + "height": 11, + "fill": "$warn" + }, + "rANgd/kMt0y": { + "height": 14, + "fill": "$warn" + }, + "NprKu": { + "content": "38.4%", + "fill": "$warn" + }, + "AdR38": { + "content": "890 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "6h 12m" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "v6b7K", + "type": "ref", + "ref": "OHo2s", + "name": "Row worker-2", + "width": "fill_container", + "fill": "$accent-dim", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "T31V2": { + "fill": "$accent", + "stroke": "$accent" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "worker-2", + "fill": "$text-primary" + }, + "VJq0n/ODVko": { + "content": "jobs" + }, + "p4yL1G": { + "content": "ghcr.io/acme/worker:2.8.1", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "—" + }, + "rANgd/fshVw": { + "height": 8, + "fill": "$warn" + }, + "rANgd/srvmw": { + "height": 11, + "fill": "$warn" + }, + "rANgd/ky1P1": { + "height": 13, + "fill": "$warn" + }, + "rANgd/bqMzi": { + "height": 10, + "fill": "$warn" + }, + "rANgd/F5Kloq": { + "height": 12, + "fill": "$warn" + }, + "rANgd/StdGN": { + "height": 14, + "fill": "$warn" + }, + "rANgd/iq3Ux": { + "height": 9, + "fill": "$warn" + }, + "rANgd/PHY2v": { + "height": 11, + "fill": "$warn" + }, + "rANgd/xqaP5": { + "height": 13, + "fill": "$warn" + }, + "rANgd/cZHdc": { + "height": 10, + "fill": "$warn" + }, + "rANgd/jhPqP": { + "height": 12, + "fill": "$warn" + }, + "rANgd/Za8Yw": { + "height": 11, + "fill": "$warn" + }, + "rANgd/zbp8o": { + "height": 13, + "fill": "$warn" + }, + "rANgd/kMt0y": { + "height": 10, + "fill": "$warn" + }, + "NprKu": { + "content": "31.7%", + "fill": "$warn" + }, + "AdR38": { + "content": "812 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "6h 12m" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "IoJuu", + "type": "ref", + "ref": "OHo2s", + "name": "Row minio", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$warn" + }, + "yMbTn": { + "fill": "$warn" + }, + "L6tp5": { + "content": "minio", + "fill": "$text-primary" + }, + "VJq0n": { + "enabled": false + }, + "p4yL1G": { + "content": "minio/minio:RELEASE.2024-09", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "9000→9000" + }, + "rANgd": { + "enabled": false + }, + "NprKu": { + "content": "已暂停", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "210 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "2d 1h" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "HUBeD", + "type": "ref", + "ref": "OHo2s", + "name": "Row grafana", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$text-muted" + }, + "yMbTn": { + "fill": "$text-muted" + }, + "L6tp5": { + "content": "grafana", + "fill": "$text-tertiary" + }, + "VJq0n/ODVko": { + "content": "observability" + }, + "p4yL1G": { + "content": "grafana/grafana:11.2.0", + "fill": "$text-muted" + }, + "y2nlk": { + "content": "3000" + }, + "rANgd": { + "enabled": false + }, + "NprKu": { + "content": "—", + "fill": "$text-muted" + }, + "AdR38": { + "content": "—", + "fill": "$text-muted" + }, + "PeLHA": { + "content": "停止 5h" + }, + "ALIhr/XMGqY": { + "icon": "play" + } + } + }, + { + "id": "FBJoS", + "type": "ref", + "ref": "OHo2s", + "name": "Row loki", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$text-muted" + }, + "yMbTn": { + "fill": "$text-muted" + }, + "L6tp5": { + "content": "loki", + "fill": "$text-tertiary" + }, + "VJq0n/ODVko": { + "content": "observability" + }, + "p4yL1G": { + "content": "grafana/loki:3.1.1", + "fill": "$text-muted" + }, + "y2nlk": { + "content": "3100" + }, + "rANgd": { + "enabled": false + }, + "NprKu": { + "content": "—", + "fill": "$text-muted" + }, + "AdR38": { + "content": "—", + "fill": "$text-muted" + }, + "PeLHA": { + "content": "停止 5h" + }, + "ALIhr/XMGqY": { + "icon": "play" + } + } + }, + { + "id": "fZjnw", + "type": "ref", + "ref": "OHo2s", + "name": "Row certbot", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$danger" + }, + "yMbTn": { + "fill": "$danger" + }, + "L6tp5": { + "content": "certbot", + "fill": "$text-tertiary" + }, + "VJq0n": { + "enabled": false + }, + "p4yL1G": { + "content": "certbot/certbot:v2.11.0", + "fill": "$text-muted" + }, + "y2nlk": { + "content": "—" + }, + "rANgd": { + "enabled": false + }, + "NprKu": { + "content": "—", + "fill": "$text-muted" + }, + "AdR38": { + "content": "—", + "fill": "$text-muted" + }, + "PeLHA": { + "content": "退出 (1)" + }, + "ALIhr/XMGqY": { + "icon": "play" + } + } + }, + { + "id": "wIn4M", + "type": "ref", + "ref": "OHo2s", + "name": "Row registry-mirror", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "registry-mirror", + "fill": "$text-primary" + }, + "VJq0n": { + "enabled": false + }, + "p4yL1G": { + "content": "registry:2.8.3", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "5000→5000" + }, + "rANgd/fshVw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/srvmw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/ky1P1": { + "height": 2, + "fill": "$accent" + }, + "rANgd/bqMzi": { + "height": 3, + "fill": "$accent" + }, + "rANgd/F5Kloq": { + "height": 2, + "fill": "$accent" + }, + "rANgd/StdGN": { + "height": 2, + "fill": "$accent" + }, + "rANgd/iq3Ux": { + "height": 2, + "fill": "$accent" + }, + "rANgd/PHY2v": { + "height": 2, + "fill": "$accent" + }, + "rANgd/xqaP5": { + "height": 3, + "fill": "$accent" + }, + "rANgd/cZHdc": { + "height": 2, + "fill": "$accent" + }, + "rANgd/jhPqP": { + "height": 2, + "fill": "$accent" + }, + "rANgd/Za8Yw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/zbp8o": { + "height": 2, + "fill": "$accent" + }, + "rANgd/kMt0y": { + "height": 2, + "fill": "$accent" + }, + "NprKu": { + "content": "0.4%", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "48 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "21d" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "w9otW", + "type": "ref", + "ref": "OHo2s", + "name": "Row watchtower", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "watchtower", + "fill": "$text-primary" + }, + "VJq0n": { + "enabled": false + }, + "p4yL1G": { + "content": "containrrr/watchtower:1.7.1", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "—" + }, + "rANgd/fshVw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/srvmw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/ky1P1": { + "height": 2, + "fill": "$accent" + }, + "rANgd/bqMzi": { + "height": 2, + "fill": "$accent" + }, + "rANgd/F5Kloq": { + "height": 2, + "fill": "$accent" + }, + "rANgd/StdGN": { + "height": 2, + "fill": "$accent" + }, + "rANgd/iq3Ux": { + "height": 2, + "fill": "$accent" + }, + "rANgd/PHY2v": { + "height": 2, + "fill": "$accent" + }, + "rANgd/xqaP5": { + "height": 2, + "fill": "$accent" + }, + "rANgd/cZHdc": { + "height": 2, + "fill": "$accent" + }, + "rANgd/jhPqP": { + "height": 2, + "fill": "$accent" + }, + "rANgd/Za8Yw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/zbp8o": { + "height": 2, + "fill": "$accent" + }, + "rANgd/kMt0y": { + "height": 2, + "fill": "$accent" + }, + "NprKu": { + "content": "0.1%", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "22 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "21d" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "H0gbz", + "name": "Detail Drawer", + "width": 440, + "height": "fill_container", + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "left": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "pODQE", + "name": "Drawer Header", + "width": "fill_container", + "height": 44, + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "BpjXJ", + "name": "Dot", + "fill": "$ok", + "width": 7, + "height": 7 + }, + { + "type": "text", + "id": "n7YQ7", + "name": "Name", + "fill": "$text-primary", + "content": "nginx-proxy", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "id": "i3tgaT", + "type": "ref", + "ref": "x0Pcos", + "name": "State Chip", + "descendants": { + "ODVko": { + "content": "运行中 · healthy" + } + } + }, + { + "type": "frame", + "id": "clY6z", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "z4EBz2", + "type": "ref", + "ref": "yoK0T", + "name": "H pin", + "descendants": { + "XMGqY": { + "icon": "pin" + } + } + }, + { + "id": "WsAD9", + "type": "ref", + "ref": "yoK0T", + "name": "H maximize-2", + "descendants": { + "XMGqY": { + "icon": "maximize-2" + } + } + }, + { + "id": "T6qPL", + "type": "ref", + "ref": "yoK0T", + "name": "H x", + "descendants": { + "XMGqY": { + "icon": "x" + } + } + } + ] + }, + { + "type": "frame", + "id": "g01pV", + "name": "Drawer Subhead", + "width": "fill_container", + "height": 26, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 7, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "d5HAHv", + "name": "Id", + "fill": "$text-tertiary", + "content": "a3f2c81b9d4e", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "a7qey", + "name": "Copy", + "width": 11, + "height": 11, + "icon": "copy", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "EalAg", + "name": "Sep", + "fill": "$text-muted", + "content": "·", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "vkFix", + "name": "Proj", + "fill": "$violet", + "content": "web-stack / proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "wUUoi", + "name": "Drawer Tabs", + "width": "fill_container", + "height": 34, + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 2, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "id": "stSRj", + "type": "ref", + "ref": "MdmRT", + "name": "T 概览", + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "icon": "info", + "fill": "$text-primary" + }, + "iscyV": { + "content": "概览", + "fill": "$text-primary", + "fontWeight": "500" + } + } + }, + { + "id": "JJJRe", + "type": "ref", + "ref": "MdmRT", + "name": "T 日志", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "scroll-text", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "日志", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + }, + { + "id": "tj33e", + "type": "ref", + "ref": "MdmRT", + "name": "T 文件", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "folder-tree", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "文件", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + }, + { + "id": "QTQY5", + "type": "ref", + "ref": "MdmRT", + "name": "T 终端", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "terminal", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "终端", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + }, + { + "id": "y6eep", + "type": "ref", + "ref": "MdmRT", + "name": "T 统计", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "activity", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "统计", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + } + ] + }, + { + "type": "frame", + "id": "iXCiP", + "name": "Quick Actions", + "width": "fill_container", + "height": 42, + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "id": "JTatd", + "type": "ref", + "ref": "i1OTz", + "name": "Q 停止", + "descendants": { + "RQ8pM": { + "icon": "square" + }, + "P0aUw": { + "content": "停止" + } + } + }, + { + "id": "B0HCw", + "type": "ref", + "ref": "i1OTz", + "name": "Q 重启", + "descendants": { + "RQ8pM": { + "icon": "rotate-cw" + }, + "P0aUw": { + "content": "重启" + } + } + }, + { + "id": "pMA6R", + "type": "ref", + "ref": "i1OTz", + "name": "Q 暂停", + "descendants": { + "RQ8pM": { + "icon": "pause" + }, + "P0aUw": { + "content": "暂停" + } + } + }, + { + "id": "sjKbj", + "type": "ref", + "ref": "i1OTz", + "name": "Q 终端", + "fill": "$accent-dim", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "terminal", + "fill": "$accent" + }, + "P0aUw": { + "content": "进入终端", + "fill": "$accent" + } + } + }, + { + "type": "frame", + "id": "orCbM", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "V9YHTv", + "type": "ref", + "ref": "yoK0T", + "name": "More", + "descendants": { + "XMGqY": { + "icon": "ellipsis" + } + } + } + ] + }, + { + "type": "frame", + "id": "tetwe", + "name": "Drawer Content", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-elevated", + "layout": "vertical", + "gap": 10, + "padding": [ + 12, + 14 + ], + "children": [ + { + "type": "frame", + "id": "H3RlN", + "name": "Live Metrics", + "width": "fill_container", + "gap": 10, + "children": [ + { + "type": "frame", + "id": "c6VsG", + "name": "M CPU", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 6, + "padding": [ + 10, + 11 + ], + "children": [ + { + "type": "frame", + "id": "NPIRa", + "name": "Top", + "width": "fill_container", + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "pO03c", + "name": "Label", + "fill": "$text-tertiary", + "content": "CPU", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "yjDsE", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "v8MXM", + "name": "Delta", + "fill": "$text-muted", + "content": "峰值 6.4%", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "y9Pwh5", + "name": "Value", + "gap": 3, + "alignItems": "end", + "children": [ + { + "type": "text", + "id": "JtFQ5", + "name": "Num", + "fill": "$text-primary", + "content": "2.1", + "fontFamily": "$font-ui", + "fontSize": 20, + "fontWeight": "600" + }, + { + "type": "text", + "id": "sC81z", + "name": "Unit", + "fill": "$text-tertiary", + "content": "%", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "dGNIp", + "type": "ref", + "ref": "MhYIL", + "name": "Spark", + "height": 30, + "descendants": { + "fshVw": { + "height": 8, + "width": 9, + "fill": "$accent" + }, + "srvmw": { + "height": 6, + "width": 9, + "fill": "$accent" + }, + "ky1P1": { + "height": 11, + "width": 9, + "fill": "$accent" + }, + "bqMzi": { + "height": 7, + "width": 9, + "fill": "$accent" + }, + "F5Kloq": { + "height": 14, + "width": 9, + "fill": "$accent" + }, + "StdGN": { + "height": 9, + "width": 9, + "fill": "$accent" + }, + "iq3Ux": { + "height": 6, + "width": 9, + "fill": "$accent" + }, + "PHY2v": { + "height": 10, + "width": 9, + "fill": "$accent" + }, + "xqaP5": { + "height": 18, + "width": 9, + "fill": "$accent" + }, + "cZHdc": { + "height": 8, + "width": 9, + "fill": "$accent" + }, + "jhPqP": { + "height": 7, + "width": 9, + "fill": "$accent" + }, + "Za8Yw": { + "height": 9, + "width": 9, + "fill": "$accent" + }, + "zbp8o": { + "height": 12, + "width": 9, + "fill": "$accent" + }, + "kMt0y": { + "height": 8, + "width": 9, + "fill": "$accent" + } + } + } + ] + }, + { + "type": "frame", + "id": "tfYFY", + "name": "M 内存", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 6, + "padding": [ + 10, + 11 + ], + "children": [ + { + "type": "frame", + "id": "rB6Xg", + "name": "Top", + "width": "fill_container", + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "rM9hl", + "name": "Label", + "fill": "$text-tertiary", + "content": "内存", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Tl9nr", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "D8QaJR", + "name": "Delta", + "fill": "$text-muted", + "content": "24.9%", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "AaE8D", + "name": "Value", + "gap": 3, + "alignItems": "end", + "children": [ + { + "type": "text", + "id": "PIrlN", + "name": "Num", + "fill": "$text-primary", + "content": "128", + "fontFamily": "$font-ui", + "fontSize": 20, + "fontWeight": "600" + }, + { + "type": "text", + "id": "aOB4F", + "name": "Unit", + "fill": "$text-tertiary", + "content": "MB / 512", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "f0AAJJ", + "type": "ref", + "ref": "MhYIL", + "name": "Spark", + "height": 30, + "descendants": { + "fshVw": { + "height": 11, + "width": 7, + "fill": "$ok" + }, + "srvmw": { + "height": 13, + "width": 7, + "fill": "$ok" + }, + "ky1P1": { + "height": 12, + "width": 7, + "fill": "$ok" + }, + "bqMzi": { + "height": 15, + "width": 7, + "fill": "$ok" + }, + "F5Kloq": { + "height": 14, + "width": 7, + "fill": "$ok" + }, + "StdGN": { + "height": 16, + "width": 7, + "fill": "$ok" + }, + "iq3Ux": { + "height": 15, + "width": 7, + "fill": "$ok" + }, + "PHY2v": { + "height": 18, + "width": 7, + "fill": "$ok" + }, + "xqaP5": { + "height": 17, + "width": 7, + "fill": "$ok" + }, + "cZHdc": { + "height": 16, + "width": 7, + "fill": "$ok" + }, + "jhPqP": { + "height": 19, + "width": 7, + "fill": "$ok" + }, + "Za8Yw": { + "height": 18, + "width": 7, + "fill": "$ok" + }, + "zbp8o": { + "height": 20, + "width": 7, + "fill": "$ok" + }, + "kMt0y": { + "height": 19, + "width": 7, + "fill": "$ok" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "LNwMl", + "name": "Sec 基本信息", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "LviDe", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Q7uBhH", + "name": "Title", + "fill": "$text-secondary", + "content": "基本信息", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "shjUm", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "pJA9w", + "name": "Action", + "fill": "$accent", + "content": "完整 inspect", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "Y62dv", + "type": "ref", + "ref": "l3qo4", + "name": "KV 镜像", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "镜像" + }, + "psgr6": { + "content": "nginx:1.27-alpine", + "fill": "$text-primary" + } + } + }, + { + "id": "H3taAh", + "type": "ref", + "ref": "l3qo4", + "name": "KV 镜像摘要", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "镜像摘要" + }, + "psgr6": { + "content": "sha256:9c4f2e…1b7a", + "fill": "$text-primary" + } + } + }, + { + "id": "nAL4w", + "type": "ref", + "ref": "l3qo4", + "name": "KV 创建于", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "创建于" + }, + "psgr6": { + "content": "2026-08-18 09:12:44", + "fill": "$text-primary" + } + } + }, + { + "id": "bcs3M", + "type": "ref", + "ref": "l3qo4", + "name": "KV 启动于", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "启动于" + }, + "psgr6": { + "content": "3 天前 (3d 4h 21m)", + "fill": "$text-primary" + } + } + }, + { + "id": "IfyDA", + "type": "ref", + "ref": "l3qo4", + "name": "KV 重启策略", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "重启策略" + }, + "psgr6": { + "content": "unless-stopped", + "fill": "$text-primary" + } + } + }, + { + "id": "fSFE0", + "type": "ref", + "ref": "l3qo4", + "name": "KV 平台", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "平台" + }, + "psgr6": { + "content": "linux/amd64", + "fill": "$text-primary" + } + } + }, + { + "id": "oIC3K", + "type": "ref", + "ref": "l3qo4", + "name": "KV 命令", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "命令" + }, + "psgr6": { + "content": "nginx -g 'daemon off;'", + "fill": "$text-primary" + } + } + } + ] + }, + { + "type": "rectangle", + "id": "ritNv", + "name": "Divider", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "Cx8Yh", + "name": "Sec 端口映射", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "Ln5xy", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "i21BOT", + "name": "Title", + "fill": "$text-secondary", + "content": "端口映射", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "STurj", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "X3Uu9", + "name": "Action", + "fill": "$accent", + "content": "编辑", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "jmX2N", + "type": "ref", + "ref": "l3qo4", + "name": "KV 8080/tcp", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "8080/tcp" + }, + "psgr6": { + "content": "0.0.0.0:8080 → 80", + "fill": "$info" + } + } + }, + { + "id": "ioTIK", + "type": "ref", + "ref": "l3qo4", + "name": "KV 8443/tcp", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "8443/tcp" + }, + "psgr6": { + "content": "[::]:8443 → 443", + "fill": "$info" + } + } + } + ] + }, + { + "type": "rectangle", + "id": "LMXao", + "name": "Divider", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "BWLQ9", + "name": "Sec 挂载", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "rycbL", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "lyx1Q", + "name": "Title", + "fill": "$text-secondary", + "content": "挂载", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "U3yzrM", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "MsYTR", + "name": "Action", + "fill": "$accent", + "content": "浏览文件", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "uiVeH", + "name": "Mount", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "VIv65", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "folder", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "R56usQ", + "name": "Path", + "fill": "$text-secondary", + "content": "/srv/nginx/conf.d → /etc/nginx/conf.d", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "v71AaF", + "type": "ref", + "ref": "x0Pcos", + "name": "Tag", + "height": 15, + "fill": "#00000000", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "只读", + "fill": "$warn", + "fontSize": 9 + } + } + } + ] + }, + { + "type": "frame", + "id": "UtIfp", + "name": "Mount", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "HE0bx", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "database", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "dsAjn", + "name": "Path", + "fill": "$text-secondary", + "content": "nginx-cache → /var/cache/nginx", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "WNwA9", + "type": "ref", + "ref": "x0Pcos", + "name": "Tag", + "height": 15, + "fill": "#00000000", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "读写", + "fill": "$ok", + "fontSize": 9 + } + } + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "k0Vbir", + "name": "Divider", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "R09su", + "name": "Sec 网络", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "aYz21", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "z2CSs", + "name": "Title", + "fill": "$text-secondary", + "content": "网络", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "u0b95P", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Mm1ja", + "name": "Action", + "fill": "$accent", + "content": "接入其他网络", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "ybrUT", + "type": "ref", + "ref": "l3qo4", + "name": "KV web-stack_default", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "网络" + }, + "psgr6": { + "content": "web-stack_default", + "fill": "$text-primary" + } + } + }, + { + "id": "k3qCfY", + "type": "ref", + "ref": "l3qo4", + "name": "KV IP", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "IP 地址" + }, + "psgr6": { + "content": "172.20.0.4 · bridge · MTU 1500" + } + } + } + ] + }, + { + "type": "rectangle", + "id": "oUTaM", + "name": "Divider", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "tzwNt", + "name": "Sec 健康检查", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "HFYKs", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Z3szH", + "name": "Title", + "fill": "$text-secondary", + "content": "健康检查", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "VtQKi", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "vhQpl", + "name": "HC", + "width": "fill_container", + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "id": "iLj84", + "type": "ref", + "ref": "x0Pcos", + "name": "HC Chip", + "descendants": { + "ODVko": { + "content": "healthy" + } + } + }, + { + "type": "text", + "id": "RosyH", + "name": "Detail", + "fill": "$text-tertiary", + "content": "每 30s · 0 次失败 · 12s 前", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "bY0Ue", + "name": "Status Bar", + "width": "fill_container", + "height": 24, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 14, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "s5zf8R", + "name": "Msg", + "fill": "$text-tertiary", + "content": "已停止 2 个容器 · redis-cache, worker-1", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "O3wV8", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "VofIV", + "name": "API v1.47", + "fill": "$text-muted", + "content": "API v1.47", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "bo4uR", + "name": "Engine 27.3.1", + "fill": "$text-muted", + "content": "Engine 27.3.1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "OnS13", + "name": "18 容器 · 34 镜像 · 9 卷", + "fill": "$text-muted", + "content": "18 容器 · 34 镜像 · 9 卷", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "sBCmg", + "name": "事件流 已连接", + "fill": "$text-muted", + "content": "事件流 已连接", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "BMSxy", + "x": 1059, + "y": 980, + "name": "03 · 日志(多容器合并流)", + "theme": { + "mode": "dark" + }, + "clip": true, + "width": 1440, + "height": 860, + "fill": "$bg-page", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "gd96z", + "name": "Top Bar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "bSgCm", + "name": "Brand", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "cEJTP", + "name": "Logo", + "width": 17, + "height": 17, + "icon": "container", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "AOu3V", + "name": "Title", + "fill": "$text-primary", + "content": "Docker", + "fontFamily": "$font-ui", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "rectangle", + "id": "wlpaX", + "name": "Divider", + "fill": "$border", + "width": 1, + "height": 18 + }, + { + "type": "frame", + "id": "lUWFp", + "name": "Host Switcher", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "onycm", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "server", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "JMt9N", + "name": "Host", + "fill": "$text-primary", + "content": "prod-sg-01", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "IS8MQ", + "name": "Transport", + "fill": "$text-muted", + "content": "/var/run/docker.sock · SSH 隧道", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "gnfzy", + "name": "Caret", + "width": 12, + "height": 12, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + }, + { + "type": "frame", + "id": "f92rf", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "b2XfV3", + "name": "Search", + "width": 300, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "G0Lyp", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "search", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "iDVPU", + "name": "Placeholder", + "fill": "$text-muted", + "content": "搜索容器、镜像、卷、网络…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "XyVyO", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "pIpeE", + "name": "Kbd", + "height": 16, + "fill": "$bg-hover", + "cornerRadius": 3, + "padding": [ + 0, + 5 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "V7UKB", + "name": "Keys", + "fill": "$text-tertiary", + "content": "Ctrl K", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "id": "ueM53", + "type": "ref", + "ref": "x0Pcos", + "name": "Live Badge", + "descendants": { + "ODVko": { + "content": "实时" + } + } + }, + { + "id": "weFOQ", + "type": "ref", + "ref": "yoK0T", + "name": "Settings", + "descendants": { + "XMGqY": { + "icon": "settings" + } + } + } + ] + }, + { + "type": "frame", + "id": "Rk8jV", + "name": "Body", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "xL9S6", + "name": "Rail", + "width": 56, + "height": "fill_container", + "fill": "$bg-rail", + "stroke": "$border", + "strokeWidth": { + "right": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 2, + "padding": [ + 6, + 0 + ], + "alignItems": "center", + "children": [ + { + "id": "B6iQ0", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 总览", + "descendants": { + "F1F9E": { + "icon": "layout-dashboard", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "总览", + "fill": "$text-tertiary" + } + } + }, + { + "id": "KUA1S", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 容器", + "descendants": { + "cQhxS": { + "fill": "$accent-dim" + }, + "F1F9E": { + "icon": "box", + "fill": "$accent" + }, + "DE5G9": { + "content": "容器", + "fill": "$accent" + } + } + }, + { + "id": "iwxhX", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 镜像", + "descendants": { + "F1F9E": { + "icon": "layers", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "镜像", + "fill": "$text-tertiary" + } + } + }, + { + "id": "PBhBk", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 卷", + "descendants": { + "F1F9E": { + "icon": "hard-drive", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "卷", + "fill": "$text-tertiary" + } + } + }, + { + "id": "ezFcw", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 网络", + "descendants": { + "F1F9E": { + "icon": "network", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "网络", + "fill": "$text-tertiary" + } + } + }, + { + "id": "jmcXl", + "type": "ref", + "ref": "nKcgr", + "name": "Nav Compose", + "descendants": { + "F1F9E": { + "icon": "boxes", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "Compose", + "fill": "$text-tertiary" + } + } + }, + { + "id": "tzzgD", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 系统", + "descendants": { + "F1F9E": { + "icon": "gauge", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "系统", + "fill": "$text-tertiary" + } + } + }, + { + "type": "frame", + "id": "P9W7gQ", + "name": "Spacer", + "width": 1, + "height": "fill_container", + "fill": "#00000000" + }, + { + "id": "Q9YV7", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 设置", + "descendants": { + "F1F9E": { + "icon": "settings" + }, + "DE5G9": { + "content": "设置" + } + } + } + ] + }, + { + "type": "frame", + "id": "IqHRk", + "name": "Sources", + "width": 232, + "height": "fill_container", + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "right": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "Cc2L5", + "name": "Head", + "width": "fill_container", + "height": 36, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "waxqh", + "name": "Title", + "fill": "$text-secondary", + "content": "日志来源", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "eGQF9", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "wvskC", + "name": "All", + "fill": "$accent", + "content": "全选", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "J5zIeE", + "name": "Filter", + "width": "fill_container", + "height": 34, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "o8FvvR", + "name": "Box", + "width": "fill_container", + "height": 24, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 6, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "lMKQg", + "name": "Icon", + "width": 11, + "height": 11, + "icon": "list-filter", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "DZwm9", + "name": "Ph", + "fill": "$text-muted", + "content": "过滤容器…", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "wjfkk", + "name": "Src nginx-proxy", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "$accent-dim", + "gap": 8, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "r7lZue", + "name": "Check", + "fill": "$accent", + "width": 12, + "height": 12, + "stroke": "$accent", + "strokeWidth": 1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "okPcJ", + "name": "Swatch", + "fill": "$accent", + "width": 3, + "height": 12 + }, + { + "type": "text", + "id": "MaVY0", + "name": "Name", + "fill": "$text-primary", + "content": "nginx-proxy", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "G0Fwj", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "CBAnv", + "name": "St", + "fill": "$text-muted", + "content": "运行中", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "BXSte", + "name": "Src api-gateway", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "$accent-dim", + "gap": 8, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "lbZq0", + "name": "Check", + "fill": "$accent", + "width": 12, + "height": 12, + "stroke": "$accent", + "strokeWidth": 1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "BTJsN", + "name": "Swatch", + "fill": "$violet", + "width": 3, + "height": 12 + }, + { + "type": "text", + "id": "c1gkA", + "name": "Name", + "fill": "$text-primary", + "content": "api-gateway", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "cKZwB", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "lpnyY", + "name": "St", + "fill": "$text-muted", + "content": "运行中", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Y6QJDp", + "name": "Src postgres-main", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 8, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "Ta9eZ", + "name": "Check", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "cqKd6", + "name": "Swatch", + "fill": "$ok", + "width": 3, + "height": 12 + }, + { + "type": "text", + "id": "dBSsN", + "name": "Name", + "fill": "$text-secondary", + "content": "postgres-main", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ouSjC", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "S9UiJe", + "name": "St", + "fill": "$text-muted", + "content": "运行中", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "oWPfY", + "name": "Src redis-cache", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 8, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "CZBgl", + "name": "Check", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "GYS1x", + "name": "Swatch", + "fill": "$ok", + "width": 3, + "height": 12 + }, + { + "type": "text", + "id": "FAUkE", + "name": "Name", + "fill": "$text-secondary", + "content": "redis-cache", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Z6ZZA", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "DNYUq", + "name": "St", + "fill": "$text-muted", + "content": "运行中", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "z9f0X", + "name": "Src worker-1", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 8, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "WUGvL", + "name": "Check", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "utQrW", + "name": "Swatch", + "fill": "$warn", + "width": 3, + "height": 12 + }, + { + "type": "text", + "id": "Pc48z", + "name": "Name", + "fill": "$text-secondary", + "content": "worker-1", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "TrSXF", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "I2BiI", + "name": "St", + "fill": "$text-muted", + "content": "运行中", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "MzFja", + "name": "Src worker-2", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 8, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "Hqv3v", + "name": "Check", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "BlOHi", + "name": "Swatch", + "fill": "$warn", + "width": 3, + "height": 12 + }, + { + "type": "text", + "id": "STFDh", + "name": "Name", + "fill": "$text-secondary", + "content": "worker-2", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "oJpQ1", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "tFebr", + "name": "St", + "fill": "$text-muted", + "content": "运行中", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "x7jFt", + "name": "Src minio", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 8, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "BErLd", + "name": "Check", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "k6VMvH", + "name": "Swatch", + "fill": "$text-muted", + "width": 3, + "height": 12 + }, + { + "type": "text", + "id": "KvNuq", + "name": "Name", + "fill": "$text-secondary", + "content": "minio", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "XDHi4", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "pjdyC", + "name": "St", + "fill": "$text-muted", + "content": "已暂停", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "FI7iL", + "name": "Src grafana", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 8, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "YJo0C", + "name": "Check", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "SHIFb", + "name": "Swatch", + "fill": "$text-muted", + "width": 3, + "height": 12 + }, + { + "type": "text", + "id": "p1U3ln", + "name": "Name", + "fill": "$text-secondary", + "content": "grafana", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "x4GyQ", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "QrcRv", + "name": "St", + "fill": "$text-muted", + "content": "已停止", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "kCUrG", + "name": "Src loki", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 8, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "ujwxl", + "name": "Check", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "V1fede", + "name": "Swatch", + "fill": "$text-muted", + "width": 3, + "height": 12 + }, + { + "type": "text", + "id": "mUYxj", + "name": "Name", + "fill": "$text-secondary", + "content": "loki", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "L6ABk", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "SME7B", + "name": "St", + "fill": "$text-muted", + "content": "已停止", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "qKzez", + "name": "Src certbot", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 8, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "HVl8i", + "name": "Check", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "ozVXF", + "name": "Swatch", + "fill": "$danger", + "width": 3, + "height": 12 + }, + { + "type": "text", + "id": "ZNnUg", + "name": "Name", + "fill": "$text-secondary", + "content": "certbot", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "zmueC", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "d8ICa", + "name": "St", + "fill": "$text-muted", + "content": "退出 1", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "fWJHE", + "name": "Spacer", + "width": "fill_container", + "height": "fill_container", + "fill": "#00000000" + }, + { + "type": "frame", + "id": "xsfZx", + "name": "Foot", + "width": "fill_container", + "height": 30, + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "rLcfv", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "git-merge", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "iCN7N", + "name": "T", + "fill": "$text-secondary", + "content": "合并 2 条流", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "OVD2w", + "name": "Main", + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "Uwz0R", + "name": "Log Toolbar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "qSWAr", + "name": "Icon", + "width": 15, + "height": 15, + "icon": "scroll-text", + "library": "lucide", + "fill": "$text-secondary" + }, + { + "type": "text", + "id": "h01hm", + "name": "Title", + "fill": "$text-primary", + "content": "日志", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "id": "iKHTi", + "type": "ref", + "ref": "x0Pcos", + "name": "Chip nginx-proxy", + "fill": "#00000000", + "stroke": "$border", + "strokeWidth": 1, + "descendants": { + "kvk5k": { + "fill": "$accent" + }, + "ODVko": { + "content": "nginx-proxy", + "fill": "$text-secondary" + } + } + }, + { + "id": "nP7GV", + "type": "ref", + "ref": "x0Pcos", + "name": "Chip api-gateway", + "fill": "#00000000", + "stroke": "$border", + "strokeWidth": 1, + "descendants": { + "kvk5k": { + "fill": "$violet" + }, + "ODVko": { + "content": "api-gateway", + "fill": "$text-secondary" + } + } + }, + { + "type": "frame", + "id": "Wlwfe", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "slOfQ", + "name": "Tail Segmented", + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 2, + "padding": 2, + "children": [ + { + "id": "ghtQV", + "type": "ref", + "ref": "MdmRT", + "name": "N 100", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "100", + "fontSize": 10, + "fontFamily": "$font-mono", + "fill": "$text-tertiary" + } + } + }, + { + "id": "NbN06", + "type": "ref", + "ref": "MdmRT", + "name": "N 500", + "height": 20, + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "500", + "fontSize": 10, + "fontFamily": "$font-mono", + "fill": "$text-primary" + } + } + }, + { + "id": "Ai3Np", + "type": "ref", + "ref": "MdmRT", + "name": "N 2000", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "2000", + "fontSize": 10, + "fontFamily": "$font-mono", + "fill": "$text-tertiary" + } + } + }, + { + "id": "phaYd", + "type": "ref", + "ref": "MdmRT", + "name": "N 10k", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "10k", + "fontSize": 10, + "fontFamily": "$font-mono", + "fill": "$text-tertiary" + } + } + } + ] + }, + { + "id": "o0cPeN", + "type": "ref", + "ref": "i1OTz", + "name": "T 时间戳", + "height": 22, + "fill": "$accent-dim", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "clock", + "fill": "$accent" + }, + "P0aUw": { + "content": "时间戳", + "fontSize": 11, + "fill": "$accent" + } + } + }, + { + "id": "nz7S1", + "type": "ref", + "ref": "i1OTz", + "name": "T 换行", + "height": 22, + "fill": "#00000000", + "stroke": "$border", + "descendants": { + "RQ8pM": { + "icon": "wrap-text", + "fill": "$text-tertiary" + }, + "P0aUw": { + "content": "换行", + "fontSize": 11, + "fill": "$text-secondary" + } + } + }, + { + "id": "voFfN", + "type": "ref", + "ref": "i1OTz", + "name": "T 仅错误", + "height": 22, + "fill": "#00000000", + "stroke": "$border", + "descendants": { + "RQ8pM": { + "icon": "triangle-alert", + "fill": "$text-tertiary" + }, + "P0aUw": { + "content": "仅错误", + "fontSize": 11, + "fill": "$text-secondary" + } + } + }, + { + "type": "frame", + "id": "d4208g", + "name": "Search", + "width": 210, + "height": 24, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$accent", + "strokeWidth": 1, + "gap": 6, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "l78I0", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "search", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "bWR6S", + "name": "Q", + "fill": "$text-primary", + "content": "upstream", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "pqhzI", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "T7XMn", + "name": "Cnt", + "fill": "$text-muted", + "content": "2/7", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "fnTtb", + "name": "Re", + "fill": "$accent", + "content": ".*", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "m9dB4", + "type": "ref", + "ref": "i1OTz", + "name": "Follow", + "height": 24, + "fill": "$ok-dim", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "activity", + "fill": "$ok" + }, + "P0aUw": { + "content": "跟随中", + "fontSize": 11, + "fill": "$ok", + "fontWeight": "500" + } + } + }, + { + "id": "R3hmnB", + "type": "ref", + "ref": "yoK0T", + "name": "A download", + "descendants": { + "XMGqY": { + "icon": "download" + } + } + }, + { + "id": "vLTei", + "type": "ref", + "ref": "yoK0T", + "name": "A copy", + "descendants": { + "XMGqY": { + "icon": "copy" + } + } + }, + { + "id": "bIP78", + "type": "ref", + "ref": "yoK0T", + "name": "A eraser", + "descendants": { + "XMGqY": { + "icon": "eraser" + } + } + } + ] + }, + { + "type": "frame", + "id": "Upzal", + "name": "Log Body", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-terminal", + "layout": "vertical", + "padding": [ + 8, + 0 + ], + "children": [ + { + "type": "frame", + "id": "RKC3B", + "name": "Spacer", + "width": "fill_container", + "height": "fill_container", + "fill": "#00000000" + }, + { + "type": "frame", + "id": "MlKfB", + "name": "L 09:40:58.114", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "F8hgj", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:40:58.114", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "FyBqh", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "aekbH", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Z4xexi", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "UxiOp", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"GET /health HTTP/1.1\" 200 2 \"-\" \"kube-probe/1.30\" rt=0.001", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "zjCN1", + "name": "L 09:41:02.118", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "BRfmL", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:02.118", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "TFlXe", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "k3Mpc4", + "name": "Lv", + "fill": "$info", + "textGrowth": "fixed-width", + "width": 38, + "content": "INFO", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "lBjTk", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "vKTVl", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "D33qdM", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "DXKId", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "eiFNL", + "name": "T", + "fill": "$ok", + "content": "\"cache warm complete\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ihcqP", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "HeBSC", + "name": "T", + "fill": "$info", + "content": "\"keys\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Ts9Ia", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "gCyCN", + "name": "T", + "fill": "$warn", + "content": "18422", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "KLjrQ", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "rvPMX", + "name": "T", + "fill": "$info", + "content": "\"dur_ms\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "g9OvC", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "PBynY", + "name": "T", + "fill": "$warn", + "content": "214", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "AbAJB", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "tqlff", + "name": "L 09:41:03.442", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "T7XzrN", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:03.442", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Bd8Nq", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "DZZ0l", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "UEo04", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "XAg7e", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"GET /assets/app.8f31c2.js HTTP/1.1\" 200 184213 rt=0.012", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "zt87T", + "name": "L 09:41:05.907", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Kexmb", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:05.907", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "YUSH3", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "JppoO", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "r4o1Ph", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "oKpEg", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"GET /api/v1/orders?page=2 HTTP/1.1\" 200 4821 rt=0.043", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "upnJw", + "name": "L 09:41:06.118", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "KEKVy", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:06.118", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "b18NMk", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "q84RV", + "name": "Lv", + "fill": "$info", + "textGrowth": "fixed-width", + "width": 38, + "content": "INFO", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "eam4m", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "kUvQE", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "U8UVAl", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "tlTyh", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "B18aU", + "name": "T", + "fill": "$ok", + "content": "\"request completed\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Aa0fs", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "b275OK", + "name": "T", + "fill": "$info", + "content": "\"route\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "R0VaVI", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "s1JjHV", + "name": "T", + "fill": "$ok", + "content": "\"/v1/orders\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "NHVU6", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "OTSKQ", + "name": "T", + "fill": "$info", + "content": "\"status\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Ek1VH", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "zgJl6", + "name": "T", + "fill": "$warn", + "content": "200", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "e0e8yB", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "nYzfL", + "name": "T", + "fill": "$info", + "content": "\"dur_ms\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "QxQA0", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "sSUOf", + "name": "T", + "fill": "$warn", + "content": "38", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "vERIY", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "EvPD0", + "name": "L 09:41:08.331", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "QoeeN", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:08.331", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "jSfak", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "jEaiO", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "content": "DEBUG", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "aZVB2", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "XjrUU", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "V1xaE", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "r6cGcz", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "i9mzm", + "name": "T", + "fill": "$ok", + "content": "\"db query\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "w1wxN", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Wu1um", + "name": "T", + "fill": "$info", + "content": "\"sql\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "URSOV", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "vxueX", + "name": "T", + "fill": "$ok", + "content": "\"SELECT * FROM orders LIMIT 50\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "j5yso8", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Ix4XF", + "name": "T", + "fill": "$info", + "content": "\"dur_ms\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "VpyWv", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "nRH3k", + "name": "T", + "fill": "$warn", + "content": "11", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "F8GY9", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "zyoVy", + "name": "L 09:41:11.002", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "X7tro", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:11.002", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "UbHZS", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "K8pBkQ", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Tbom1", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "YbSXJ", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"POST /api/v1/checkout HTTP/1.1\" 201 318 rt=0.211", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "RlrDs", + "name": "L 09:41:11.220", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "LTM7m", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:11.220", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "BoRTH", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "I11Jsn", + "name": "Lv", + "fill": "$info", + "textGrowth": "fixed-width", + "width": 38, + "content": "INFO", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "LrtOu", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "nVgUB", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "grr1n", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Z7thJ", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "zG8de", + "name": "T", + "fill": "$ok", + "content": "\"payment authorized\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "gwjDU", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "dNeHJ", + "name": "T", + "fill": "$info", + "content": "\"order\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "kvX9k", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "j3rxah", + "name": "T", + "fill": "$ok", + "content": "\"ord_8812\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Wa59i", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "OO1xZ", + "name": "T", + "fill": "$info", + "content": "\"amount\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "VV44e", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "TzL2u", + "name": "T", + "fill": "$warn", + "content": "4299", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "LiPLn", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "qzJfd", + "name": "T", + "fill": "$info", + "content": "\"currency\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Niiyl", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "aVQIB", + "name": "T", + "fill": "$ok", + "content": "\"SGD\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "n4uszu", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "QbqM0", + "name": "L 09:41:12.554", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "E5NQP", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:12.554", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "pkXnA", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "A8S9Ee", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "R9voMs", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "kZfum", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"GET /api/v1/cart HTTP/1.1\" 200 992 rt=0.008", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "L9CWva", + "name": "L 09:41:14.771", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "xH78F", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:14.771", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "fgaav", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "wgHPo", + "name": "Lv", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 38, + "content": "WARN", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "B7HX3", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "oHahQ", + "name": "T", + "fill": "$warn", + "content": "[warn] 29#29: *18421 ", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "JKK1h", + "name": "HL", + "height": 15, + "fill": "$warn-dim", + "cornerRadius": 2, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "uhlbc", + "name": "T", + "fill": "$warn", + "content": "upstream", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "Z3eQBF", + "name": "T", + "fill": "$warn", + "content": " server temporarily disabled while reading response header", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "M024NR", + "name": "L 09:41:14.772", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "$warn-dim", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "s4G7qf", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:14.772", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "XUgsF", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "G4R7CI", + "name": "Lv", + "fill": "$danger", + "textGrowth": "fixed-width", + "width": 38, + "content": "ERROR", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "hxzZ9", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "R7bihM", + "name": "T", + "fill": "$danger", + "content": "[error] 29#29: *18421 connect() failed (111: Connection refused) while connecting to ", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Y2wJH", + "name": "HL", + "height": 15, + "fill": "$warn-dim", + "cornerRadius": 2, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "j84Exg", + "name": "T", + "fill": "$warn", + "content": "upstream", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "RXBv4", + "name": "T", + "fill": "$danger", + "content": ", client: 172.20.0.1", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "G4Jr0U", + "name": "L 09:41:14.930", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "QKHU8", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:14.930", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "VZKJ4", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "y7aHFv", + "name": "Lv", + "fill": "$danger", + "textGrowth": "fixed-width", + "width": 38, + "content": "ERROR", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "BSiKW", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "H7KYf", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ediyv", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "B4AyuC", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "pKSOf", + "name": "T", + "fill": "$ok", + "content": "\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ZNQCR", + "name": "HL", + "height": 15, + "fill": "$warn-dim", + "cornerRadius": 2, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "eLpHg", + "name": "T", + "fill": "$warn", + "content": "upstream", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "nPYpK", + "name": "T", + "fill": "$ok", + "content": " unavailable\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "N45ohx", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "qYiJI", + "name": "T", + "fill": "$info", + "content": "\"target\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "CcRQD", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "GGMYm", + "name": "T", + "fill": "$ok", + "content": "\"payments-svc:8443\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "iWSea", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "GrAol", + "name": "T", + "fill": "$info", + "content": "\"retry_in_ms\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "UtTVw", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ud6x4", + "name": "T", + "fill": "$warn", + "content": "500", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "nq0I3", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "HjaZh", + "name": "L 09:41:15.433", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "X0yZ8", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:15.433", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "qlaQD", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "haEjS", + "name": "Lv", + "fill": "$info", + "textGrowth": "fixed-width", + "width": 38, + "content": "INFO", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "QlC76", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "x5cJQI", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "V1nnE", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "toe8c", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "mqrG8", + "name": "T", + "fill": "$ok", + "content": "\"retry succeeded\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "pmYNJ", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "uhHid", + "name": "T", + "fill": "$info", + "content": "\"attempt\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "m5D4uu", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "JOK3Z", + "name": "T", + "fill": "$warn", + "content": "2", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "rxdbp", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "pizdQ", + "name": "T", + "fill": "$info", + "content": "\"target\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ODY8A", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "iiXAK", + "name": "T", + "fill": "$ok", + "content": "\"payments-svc:8443\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Hs7Iu", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "M1jKf6", + "name": "L 09:41:15.601", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mnV5q", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:15.601", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "BgR10", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "T0K9US", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "M6uUhC", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "u9GeO", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"POST /api/v1/checkout HTTP/1.1\" 201 318 rt=0.664", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "kRu6H", + "name": "L 09:41:18.204", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "T6vSD", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:18.204", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "tVOxE", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "m3TaGq", + "name": "Lv", + "fill": "$info", + "textGrowth": "fixed-width", + "width": 38, + "content": "INFO", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "fuYKy", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Q9UYoV", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "BpXaA", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "rjBrE", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "d3H0m", + "name": "T", + "fill": "$ok", + "content": "\"order confirmed\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "L2Vhkt", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "zlYqy", + "name": "T", + "fill": "$info", + "content": "\"order\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "yOqeo", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "xzxOX", + "name": "T", + "fill": "$ok", + "content": "\"ord_8812\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "w3sVDr", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Imrce", + "name": "T", + "fill": "$info", + "content": "\"items\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "LxEVW", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "V2zKr", + "name": "T", + "fill": "$warn", + "content": "3", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "CAwet", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "R50JNn", + "name": "T", + "fill": "$info", + "content": "\"total\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "I1bIvP", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "RGTi1", + "name": "T", + "fill": "$warn", + "content": "4299", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "pZ4xC", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "cqzGv", + "name": "L 09:41:19.775", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "knCbC", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:19.775", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "AgvB0", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Oy5q5", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "hKqWY", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "e0S7eJ", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"GET /api/v1/orders/ord_8812 HTTP/1.1\" 200 1204 rt=0.019", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "T6DC6", + "name": "L 09:41:22.118", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "V7tBW", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:22.118", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "xVDQJ", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "UFgYz", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "content": "DEBUG", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "PLil3", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "os9Ef", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "i8ZcF1", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "dKZ72", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "VqkNP", + "name": "T", + "fill": "$ok", + "content": "\"emitting webhook\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "DGkmN", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "sUAtH", + "name": "T", + "fill": "$info", + "content": "\"event\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "pGOOH", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "SNlnI", + "name": "T", + "fill": "$ok", + "content": "\"order.confirmed\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "uF6FV", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "VnBue", + "name": "T", + "fill": "$info", + "content": "\"targets\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "bFNzP", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "rlM1s", + "name": "T", + "fill": "$warn", + "content": "2", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "sLSq7", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "gyR8H", + "name": "L 09:41:22.930", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "yD0Bo", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:22.930", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "HK8MU", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "v7nr9U", + "name": "Lv", + "fill": "$info", + "textGrowth": "fixed-width", + "width": 38, + "content": "INFO", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "E9JHQ", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "AQX5L", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "H5V1p", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "UCeBP", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "DoErG", + "name": "T", + "fill": "$ok", + "content": "\"webhook delivered\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ZOhPT", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "eOgNR", + "name": "T", + "fill": "$info", + "content": "\"url\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "P2AD7T", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "QZQfN", + "name": "T", + "fill": "$ok", + "content": "\"https://hooks.acme.io/orders\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "OcYKt", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ovKxQ", + "name": "T", + "fill": "$info", + "content": "\"status\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "EGh3X", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "FIgyT", + "name": "T", + "fill": "$warn", + "content": "200", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "i8qBY", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "miCL7", + "name": "L 09:41:24.006", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mnGvK", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:24.006", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "mxUqF", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "QoEqT", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "SD9yk", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dERDG", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"GET /health HTTP/1.1\" 200 2 \"-\" \"kube-probe/1.30\" rt=0.001", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "l1GlY", + "name": "L 09:41:27.441", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "h4Tv8O", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:27.441", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "fhke9", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "SUVaY", + "name": "Lv", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 38, + "content": "WARN", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "gnjfa", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "fdbAx", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "bNhv3", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Tz9WK", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "I3VF0Q", + "name": "T", + "fill": "$ok", + "content": "\"connection pool near limit\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "TBsuE", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "hrMrO", + "name": "T", + "fill": "$info", + "content": "\"in_use\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "gmRIb", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "P7paUN", + "name": "T", + "fill": "$warn", + "content": "47", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "fGAkD", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "xsMXe", + "name": "T", + "fill": "$info", + "content": "\"max\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "P0rZx", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "lNNgi", + "name": "T", + "fill": "$warn", + "content": "50", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "NxnWr", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "l346Y", + "name": "L 09:41:29.118", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "f0rC7v", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:29.118", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "f8DBCE", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "B630HF", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "tWhvp", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "jSul8", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"GET /api/v1/products?cat=shoes HTTP/1.1\" 200 22841 rt=0.087", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "cejYg", + "name": "L 09:41:31.552", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "LamaO", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:31.552", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "AAHWy", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "W1m2Xt", + "name": "Lv", + "fill": "$info", + "textGrowth": "fixed-width", + "width": 38, + "content": "INFO", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "qj8wN", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "JvmZh", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "J47sPd", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "FTLvK", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "WOcEJ", + "name": "T", + "fill": "$ok", + "content": "\"request completed\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "bYjvE", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "x3Odg9", + "name": "T", + "fill": "$info", + "content": "\"route\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "NrQzi", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "uGmT5", + "name": "T", + "fill": "$ok", + "content": "\"/v1/products\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "X4UBN7", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "dnevV", + "name": "T", + "fill": "$info", + "content": "\"status\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "HdLYe", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "T2G7Q", + "name": "T", + "fill": "$warn", + "content": "200", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "S4iFo", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "mw3WX", + "name": "T", + "fill": "$info", + "content": "\"dur_ms\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "V8R8oI", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "mYNAT", + "name": "T", + "fill": "$warn", + "content": "81", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "qIlLB", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "oXHB2", + "name": "L 09:41:33.884", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ofLz5", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:33.884", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "lWACi", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "q7wlxM", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "s68Qq", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ht4tU", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"GET /assets/hero.4a91.webp HTTP/1.1\" 200 421884 rt=0.041", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "xXkvm", + "name": "L 09:41:35.117", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "kFTlv", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:35.117", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "b5oLbM", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "a5eF6w", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "content": "DEBUG", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "rQK4P", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "zGtcF", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "WUOZG", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "aJNPT", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "A9nEo5", + "name": "T", + "fill": "$ok", + "content": "\"cache hit\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "F5El1", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "V07wCn", + "name": "T", + "fill": "$info", + "content": "\"key\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "JoQU7", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "vZzHu", + "name": "T", + "fill": "$ok", + "content": "\"products:cat=shoes:p1\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "WbYzH", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "iNVuN", + "name": "T", + "fill": "$info", + "content": "\"ttl_s\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "XTWaQ", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Rhx24", + "name": "T", + "fill": "$warn", + "content": "284", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "pW97e", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "KTJOR", + "name": "L 09:41:38.229", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Q9zYp1", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:38.229", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ybWsK", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "OtV2v", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "fCtZW", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "PF20G", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"GET /health HTTP/1.1\" 200 2 \"-\" \"kube-probe/1.30\" rt=0.001", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "b5WPq", + "name": "L 09:41:40.663", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "A0Zvr", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:40.663", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "c7D2Y", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "B4oEyW", + "name": "Lv", + "fill": "$info", + "textGrowth": "fixed-width", + "width": 38, + "content": "INFO", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "DHptI", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "C0EVt3", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "S8mnua", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "v37bqg", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "o7SyNI", + "name": "T", + "fill": "$ok", + "content": "\"scheduled job started\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "y4Yikq", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "nneV3", + "name": "T", + "fill": "$info", + "content": "\"job\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "VIDqv", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "E1XMBr", + "name": "T", + "fill": "$ok", + "content": "\"reconcile-inventory\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "X7rPYR", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "EzBC3", + "name": "L 09:41:44.901", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "wU3rA", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:44.901", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "K0Rzyr", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "yOlyZ", + "name": "Lv", + "fill": "$info", + "textGrowth": "fixed-width", + "width": 38, + "content": "INFO", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "TE4np", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "K6A2y", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "hJ4aB", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Ofm79", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "HykEe", + "name": "T", + "fill": "$ok", + "content": "\"scheduled job finished\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "kypYH", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "LCKaL", + "name": "T", + "fill": "$info", + "content": "\"job\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "kG1FQ", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "uw0bL", + "name": "T", + "fill": "$ok", + "content": "\"reconcile-inventory\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "U13bgq", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "GzJZx", + "name": "T", + "fill": "$info", + "content": "\"dur_ms\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "FlbZQ", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "GIBR5", + "name": "T", + "fill": "$warn", + "content": "4238", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ttpqV", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ViNcy", + "name": "L 09:41:46.220", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "H8hXvG", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:46.220", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "b6yirK", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "EiYD4", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "sxZwd", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "a75GgW", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"GET /api/v1/cart HTTP/1.1\" 200 992 rt=0.006", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "v6o4e", + "name": "L 09:41:48.774", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "MvMjF", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:48.774", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "IwNS4", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ol2iC", + "name": "Lv", + "fill": "$info", + "textGrowth": "fixed-width", + "width": 38, + "content": "INFO", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "faCk2", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "LG0BC", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Drs1J", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "q8dv8", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "RrtJV", + "name": "T", + "fill": "$ok", + "content": "\"request completed\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "I78Uq", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "RZMY6", + "name": "T", + "fill": "$info", + "content": "\"route\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "JJG0C", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "YzjNr", + "name": "T", + "fill": "$ok", + "content": "\"/v1/cart\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "c9qqZ", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "IbM6X", + "name": "T", + "fill": "$info", + "content": "\"status\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "c9yzz", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "cs9V1", + "name": "T", + "fill": "$warn", + "content": "200", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "fEM8n", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "wVAo0", + "name": "T", + "fill": "$info", + "content": "\"dur_ms\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "NfUuf", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "NdGoK", + "name": "T", + "fill": "$warn", + "content": "6", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "URzXg", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "pvg0u", + "name": "L 09:41:52.118", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "HDB4y", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:52.118", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "hjMrT", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "p8OEAK", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "XHleq", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "VVYLX", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"GET /health HTTP/1.1\" 200 2 \"-\" \"kube-probe/1.30\" rt=0.001", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "GzIw5", + "name": "Log Foot", + "width": "fill_container", + "height": 26, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 12, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "bstUG", + "name": "Live", + "fill": "$ok", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "TYp2g", + "name": "S", + "fill": "$ok", + "content": "跟随中 · 新行即时到达", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "xua3E", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "d7sx1f", + "name": "缓冲 318 KB / 512 KB(按行截断)", + "fill": "$text-muted", + "content": "缓冲 318 KB / 512 KB(按行截断)", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "TTA1x", + "name": "12,481 行", + "fill": "$text-muted", + "content": "12,481 行", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "wIELG", + "name": "stdout + stderr 按到达顺序合并", + "fill": "$text-muted", + "content": "stdout + stderr 按到达顺序合并", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "NRxWb", + "name": "Status Bar", + "width": "fill_container", + "height": 24, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 14, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "EmGqK", + "name": "Msg", + "fill": "$text-tertiary", + "content": "docker logs -f · 2 条流 · 已运行 4m 12s", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "gOLiz", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "R7EoH", + "name": "API v1.47", + "fill": "$text-muted", + "content": "API v1.47", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "dMVco", + "name": "Engine 27.3.1", + "fill": "$text-muted", + "content": "Engine 27.3.1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "peAM0", + "name": "18 容器 · 34 镜像 · 9 卷", + "fill": "$text-muted", + "content": "18 容器 · 34 镜像 · 9 卷", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "GGZcC", + "name": "事件流 已连接", + "fill": "$text-muted", + "content": "事件流 已连接", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ay9WV", + "x": 1059, + "y": 1960, + "name": "04 · 容器文件浏览与编辑", + "theme": { + "mode": "dark" + }, + "clip": true, + "width": 1440, + "height": 860, + "fill": "$bg-page", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "eltbb", + "name": "Top Bar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "kfA5Z", + "name": "Brand", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "z9gt3", + "name": "Logo", + "width": 17, + "height": 17, + "icon": "container", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "G8BmX", + "name": "Title", + "fill": "$text-primary", + "content": "Docker", + "fontFamily": "$font-ui", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "rectangle", + "id": "z1WjN6", + "name": "Divider", + "fill": "$border", + "width": 1, + "height": 18 + }, + { + "type": "frame", + "id": "azcZL", + "name": "Host Switcher", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "SRQ0G", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "server", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "rkgEN", + "name": "Host", + "fill": "$text-primary", + "content": "prod-sg-01", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "cvCOn", + "name": "Transport", + "fill": "$text-muted", + "content": "/var/run/docker.sock · SSH 隧道", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "nDgC8", + "name": "Caret", + "width": 12, + "height": 12, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + }, + { + "type": "frame", + "id": "Z4IpJR", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "S7O4L", + "name": "Search", + "width": 300, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "UcNdX", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "search", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "xxMZc", + "name": "Placeholder", + "fill": "$text-muted", + "content": "搜索容器、镜像、卷、网络…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "T0UoQ", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "LFnpe", + "name": "Kbd", + "height": 16, + "fill": "$bg-hover", + "cornerRadius": 3, + "padding": [ + 0, + 5 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "n0f51", + "name": "Keys", + "fill": "$text-tertiary", + "content": "Ctrl K", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "id": "IbSjI", + "type": "ref", + "ref": "x0Pcos", + "name": "Live Badge", + "descendants": { + "ODVko": { + "content": "实时" + } + } + }, + { + "id": "Rgj6w", + "type": "ref", + "ref": "yoK0T", + "name": "Settings", + "descendants": { + "XMGqY": { + "icon": "settings" + } + } + } + ] + }, + { + "type": "frame", + "id": "J2gy7", + "name": "Body", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "vjday", + "name": "Rail", + "width": 56, + "height": "fill_container", + "fill": "$bg-rail", + "stroke": "$border", + "strokeWidth": { + "right": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 2, + "padding": [ + 6, + 0 + ], + "alignItems": "center", + "children": [ + { + "id": "qN8rM", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 总览", + "descendants": { + "F1F9E": { + "icon": "layout-dashboard", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "总览", + "fill": "$text-tertiary" + } + } + }, + { + "id": "WlHxG", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 容器", + "descendants": { + "cQhxS": { + "fill": "$accent-dim" + }, + "F1F9E": { + "icon": "box", + "fill": "$accent" + }, + "DE5G9": { + "content": "容器", + "fill": "$accent" + } + } + }, + { + "id": "U1NUp", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 镜像", + "descendants": { + "F1F9E": { + "icon": "layers", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "镜像", + "fill": "$text-tertiary" + } + } + }, + { + "id": "UVKUr", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 卷", + "descendants": { + "F1F9E": { + "icon": "hard-drive", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "卷", + "fill": "$text-tertiary" + } + } + }, + { + "id": "iZeq6", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 网络", + "descendants": { + "F1F9E": { + "icon": "network", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "网络", + "fill": "$text-tertiary" + } + } + }, + { + "id": "UJLzC", + "type": "ref", + "ref": "nKcgr", + "name": "Nav Compose", + "descendants": { + "F1F9E": { + "icon": "boxes", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "Compose", + "fill": "$text-tertiary" + } + } + }, + { + "id": "wUb8t", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 系统", + "descendants": { + "F1F9E": { + "icon": "gauge", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "系统", + "fill": "$text-tertiary" + } + } + }, + { + "type": "frame", + "id": "e0vz2U", + "name": "Spacer", + "width": 1, + "height": "fill_container", + "fill": "#00000000" + }, + { + "id": "g8gsx", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 设置", + "descendants": { + "F1F9E": { + "icon": "settings" + }, + "DE5G9": { + "content": "设置" + } + } + } + ] + }, + { + "type": "frame", + "id": "g1qQgp", + "name": "File Tree", + "width": 300, + "height": "fill_container", + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "right": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "vTn1x", + "name": "Head", + "width": "fill_container", + "height": 36, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "vbIYu", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "folder-tree", + "library": "lucide", + "fill": "$text-secondary" + }, + { + "type": "text", + "id": "Iydss", + "name": "T", + "fill": "$text-secondary", + "content": "nginx-proxy 文件系统", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "xcPwY", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "BwX7I", + "type": "ref", + "ref": "yoK0T", + "name": "Up", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "upload", + "width": 12, + "height": 12 + } + } + }, + { + "id": "CmYTa", + "type": "ref", + "ref": "yoK0T", + "name": "Ref", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "refresh-cw", + "width": 12, + "height": 12 + } + } + } + ] + }, + { + "type": "frame", + "id": "Oawrp", + "name": "DiffFilter", + "width": "fill_container", + "height": 30, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "rmbA4", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "git-compare", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "text", + "id": "g93ojl", + "name": "T", + "fill": "$text-secondary", + "content": "相对镜像已变更 7 项", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "g85HqG", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "fOamB", + "name": "O", + "fill": "$accent", + "content": "只看变更", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "D2Y7ec", + "name": "F /", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "#00000000", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "tvDXa", + "name": "Indent", + "width": 1, + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "r1ROJ", + "name": "Caret", + "width": 11, + "height": 11, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "icon", + "id": "Ii8Pk", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "folder", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "G4xkGp", + "name": "N", + "fill": "$text-secondary", + "content": "/", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "YZVzA", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "SKVj3", + "name": "F etc", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "#00000000", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "JmNXi", + "name": "Indent", + "width": 14, + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "nvBvU", + "name": "Caret", + "width": 11, + "height": 11, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "icon", + "id": "NlQIB", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "folder", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "Mjnyn", + "name": "N", + "fill": "$text-secondary", + "content": "etc", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "xvUeL", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "KFmod", + "name": "F nginx", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "#00000000", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "KBdaa", + "name": "Indent", + "width": 27, + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "tZUqJ", + "name": "Caret", + "width": 11, + "height": 11, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "icon", + "id": "i1XCy2", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "folder", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "xikJT", + "name": "N", + "fill": "$text-secondary", + "content": "nginx", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "HNI2c", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "hOnBr", + "name": "Mk", + "fill": "$warn", + "content": "C", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "DVnB2", + "name": "F nginx.conf", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "#00000000", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "i9Ziwx", + "name": "Indent", + "width": 40, + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "MRmQX", + "name": "NoCaret", + "width": 11, + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "mimEy", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "file-text", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "MPoON", + "name": "N", + "fill": "$text-secondary", + "content": "nginx.conf", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "OGtbi", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "dKK0v", + "name": "F conf.d", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "#00000000", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "m7gugJ", + "name": "Indent", + "width": 40, + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "r0wif", + "name": "Caret", + "width": 11, + "height": 11, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "icon", + "id": "ugIr2", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "folder", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "OUtPv", + "name": "N", + "fill": "$text-secondary", + "content": "conf.d", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ivttX", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "UO0Xb", + "name": "Mk", + "fill": "$warn", + "content": "C", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "s91vNU", + "name": "F default.conf", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "$accent-dim", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "vY7Oi", + "name": "Indent", + "width": 53, + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "Nqgqf", + "name": "NoCaret", + "width": 11, + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "IDB0a", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "file-text", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "r6sUU", + "name": "N", + "fill": "$text-primary", + "content": "default.conf", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "N16PO", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "ruzee", + "name": "Mk", + "fill": "$warn", + "content": "C", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "m120t3", + "name": "F upstreams.conf", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "#00000000", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "uZ4Ic", + "name": "Indent", + "width": 53, + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "daEiJ", + "name": "NoCaret", + "width": 11, + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "cuprJ", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "file-text", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "px2fg", + "name": "N", + "fill": "$text-secondary", + "content": "upstreams.conf", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "I50pwQ", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "djS9g", + "name": "Mk", + "fill": "$ok", + "content": "A", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "v9kUU", + "name": "F ssl", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "#00000000", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "hesOL", + "name": "Indent", + "width": 40, + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "m5bD79", + "name": "Caret", + "width": 11, + "height": 11, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "icon", + "id": "TnTQy", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "folder", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "Y2kcZW", + "name": "N", + "fill": "$text-secondary", + "content": "ssl", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "X9yNsX", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "FyLYE", + "name": "Mk", + "fill": "$ok", + "content": "A", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "vnx5k", + "name": "F ssl", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "#00000000", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "PY6pf", + "name": "Indent", + "width": 27, + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "b3Qlz", + "name": "Caret", + "width": 11, + "height": 11, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "icon", + "id": "CjSrS", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "folder", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "Wyaai", + "name": "N", + "fill": "$text-secondary", + "content": "ssl", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "N7raS", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "gK2s2", + "name": "Mk", + "fill": "$warn", + "content": "C", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "EHDHn", + "name": "F var", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "#00000000", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "e4MYB", + "name": "Indent", + "width": 14, + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "TJUhP", + "name": "Caret", + "width": 11, + "height": 11, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "icon", + "id": "c9raGD", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "folder", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "RFWrU", + "name": "N", + "fill": "$text-secondary", + "content": "var", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "G1imD", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "krNDc", + "name": "F cache", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "#00000000", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Ryaf4", + "name": "Indent", + "width": 27, + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "npiPB", + "name": "Caret", + "width": 11, + "height": 11, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "icon", + "id": "c13Xa7", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "folder", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "x4kXv", + "name": "N", + "fill": "$text-secondary", + "content": "cache", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "O73ju", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "qbaAs", + "name": "F nginx", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "#00000000", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "P5x7R1", + "name": "Indent", + "width": 40, + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "pmSf2", + "name": "Caret", + "width": 11, + "height": 11, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "icon", + "id": "mgEvu", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "folder", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "j41aBR", + "name": "N", + "fill": "$text-secondary", + "content": "nginx", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "zZJrf", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "pbFCd", + "name": "Mk", + "fill": "$warn", + "content": "C", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "z6YEJ", + "name": "F log", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "#00000000", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "XFmXk", + "name": "Indent", + "width": 27, + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "cne73", + "name": "Caret", + "width": 11, + "height": 11, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "icon", + "id": "bk7sS", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "folder", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "JEnRS", + "name": "N", + "fill": "$text-secondary", + "content": "log", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "lO2hb", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "IDJzP", + "name": "F access.log", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "#00000000", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "PMjeT", + "name": "Indent", + "width": 40, + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "m5GaCO", + "name": "NoCaret", + "width": 11, + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "g77qC", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "file-text", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "cvH6z", + "name": "N", + "fill": "$text-secondary", + "content": "access.log", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "G91KE", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Q5FJVy", + "name": "Mk", + "fill": "$warn", + "content": "C", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "eCMcC", + "name": "F error.log", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "#00000000", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "eHtod", + "name": "Indent", + "width": 40, + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "XhoVx", + "name": "NoCaret", + "width": 11, + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "wmg0U", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "file-text", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "dKtej", + "name": "N", + "fill": "$text-secondary", + "content": "error.log", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "p1Lfl", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "dSGO0", + "name": "Mk", + "fill": "$warn", + "content": "C", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "zMSmn", + "name": "F usr", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "#00000000", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "wtBtD", + "name": "Indent", + "width": 14, + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "O8vYuq", + "name": "Caret", + "width": 11, + "height": 11, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "icon", + "id": "wBX45", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "folder", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "BNIZC", + "name": "N", + "fill": "$text-secondary", + "content": "usr", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "N91AE", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "P6LeG5", + "name": "F srv", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "#00000000", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "usYNj", + "name": "Indent", + "width": 14, + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "I6GqgC", + "name": "Caret", + "width": 11, + "height": 11, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "icon", + "id": "B0Oz8b", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "folder", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "JijTN", + "name": "N", + "fill": "$text-secondary", + "content": "srv", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Fe9i1", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "tXXue", + "name": "F www", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "#00000000", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "bF1cj", + "name": "Indent", + "width": 27, + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "rnuX0", + "name": "Caret", + "width": 11, + "height": 11, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "icon", + "id": "SvFp8", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "folder", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "pHAxd", + "name": "N", + "fill": "$text-secondary", + "content": "www", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "VCUQm", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "YISXY", + "name": "Mk", + "fill": "$ok", + "content": "A", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "Cwqgf", + "name": "F tmp", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "#00000000", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "H2jzT", + "name": "Indent", + "width": 14, + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "cFkxk", + "name": "Caret", + "width": 11, + "height": 11, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "icon", + "id": "IW9NR", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "folder", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "NjzMA", + "name": "N", + "fill": "$text-secondary", + "content": "tmp", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "k1hFHC", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "jv6rU", + "name": "Mk", + "fill": "$danger", + "content": "D", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "gkZ2g", + "name": "F .dockerenv", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "#00000000", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "eapJx", + "name": "Indent", + "width": 14, + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "pA6Sh", + "name": "NoCaret", + "width": 11, + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "jtOJd", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "file-text", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "K5F0Oh", + "name": "N", + "fill": "$text-secondary", + "content": ".dockerenv", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "jJZfC", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Iotrn", + "name": "Main", + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "hQf46", + "name": "Editor Head", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "G8hg8S", + "name": "Icon", + "width": 14, + "height": 14, + "icon": "file-code", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "text", + "id": "fNAkm", + "name": "Dir", + "fill": "$text-tertiary", + "content": "/etc/nginx/conf.d/", + "fontFamily": "$font-mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "N4hg0", + "name": "File", + "fill": "$text-primary", + "content": "default.conf", + "fontFamily": "$font-mono", + "fontSize": 12, + "fontWeight": "600" + }, + { + "id": "Uad5d", + "type": "ref", + "ref": "x0Pcos", + "name": "Modified", + "fill": "$warn-dim", + "descendants": { + "kvk5k": { + "fill": "$warn" + }, + "ODVko": { + "content": "未保存 · +2 −1", + "fill": "$warn" + } + } + }, + { + "type": "frame", + "id": "u2QIt", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "zXICe", + "name": "Mode", + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 2, + "padding": 2, + "children": [ + { + "id": "Liy0J", + "type": "ref", + "ref": "MdmRT", + "name": "M 编辑", + "height": 20, + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "icon": "pencil", + "width": 11, + "height": 11, + "fill": "$text-primary" + }, + "iscyV": { + "content": "编辑", + "fontSize": 11, + "fill": "$text-primary" + } + } + }, + { + "id": "Zn9if", + "type": "ref", + "ref": "MdmRT", + "name": "M 差异", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "git-compare", + "width": 11, + "height": 11, + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "差异", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + } + ] + }, + { + "id": "u4fY6", + "type": "ref", + "ref": "i1OTz", + "name": "E 撤销", + "descendants": { + "RQ8pM": { + "icon": "undo-2" + }, + "P0aUw": { + "content": "撤销" + } + } + }, + { + "id": "ferzD", + "type": "ref", + "ref": "i1OTz", + "name": "E 保存", + "fill": "$accent", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "save", + "fill": "$accent-on" + }, + "P0aUw": { + "content": "保存…", + "fill": "$accent-on", + "fontWeight": "500" + } + } + } + ] + }, + { + "type": "frame", + "id": "zgBur", + "name": "Code", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-terminal", + "layout": "vertical", + "padding": [ + 8, + 0 + ], + "children": [ + { + "type": "frame", + "id": "FJgL7", + "name": "CL1", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "n3ymD", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "1", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "OnPdK", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ZVzzE", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "HWWm9", + "name": "C", + "fill": "$text-muted", + "content": "# managed by docker-panel · 上次写入 2026-08-21 09:12:44", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "C9RKb", + "name": "CL3", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "IbWw4", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "3", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "N2P6V", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "oqwSG", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "wrO4c", + "name": "t", + "fill": "$info", + "content": "upstream", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "xtgz9", + "name": "t", + "fill": "$ok", + "content": "api_backend", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "l8G4ym", + "name": "t", + "fill": "$text-tertiary", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "mAXYF", + "name": "CL4", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "lLqYR", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "4", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "fRvAl", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Nk0rc", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "j4vdaL", + "name": "Ind", + "width": 19.86, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "VwvXT", + "name": "t", + "fill": "$info", + "content": "least_conn;", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Oagec", + "name": "CL5", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "$ok-dim", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "FAniM", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "5", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "k03DON", + "name": "Mk", + "fill": "$ok", + "textGrowth": "fixed-width", + "width": 18, + "content": "+", + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "sZjJh", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "M0siE", + "name": "Ind", + "width": 19.86, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Wt0rT", + "name": "t", + "fill": "$info", + "content": "server", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "mMw5T", + "name": "t", + "fill": "$ok", + "content": "api-gateway:8000", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "CXFV6", + "name": "t", + "fill": "$ok", + "content": "max_fails=3", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Wd05X", + "name": "t", + "fill": "$ok", + "content": "fail_timeout=15s;", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "As9dL", + "name": "CL6", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "$ok-dim", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "np8Ft", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "6", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "q83NKA", + "name": "Mk", + "fill": "$ok", + "textGrowth": "fixed-width", + "width": 18, + "content": "+", + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "jELYl", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "S9TrpL", + "name": "Ind", + "width": 19.86, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "h9mJeC", + "name": "t", + "fill": "$info", + "content": "server", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "reM86", + "name": "t", + "fill": "$ok", + "content": "api-gateway-canary:8000", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "o9iFZk", + "name": "t", + "fill": "$ok", + "content": "backup;", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "wssQR", + "name": "CL7", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "n7ma0", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "7", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "j458dz", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "iY25z", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "sxvMz", + "name": "Ind", + "width": 19.86, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "KVa61", + "name": "t", + "fill": "$info", + "content": "keepalive", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "O7QE0q", + "name": "t", + "fill": "$warn", + "content": "32;", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "bD8i6", + "name": "CL8", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Ph2o8", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "8", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "YvBUR", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "xn945", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Zsz4U", + "name": "t", + "fill": "$text-tertiary", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "L9qE5", + "name": "CL9", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "pGW0c", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "9", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "IPt1T", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "TGEiX", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center" + } + ] + }, + { + "type": "frame", + "id": "Ki8He", + "name": "CL10", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "uWTww", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "10", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "BrBy2", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Z3TTdz", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "P9lSfl", + "name": "t", + "fill": "$info", + "content": "server", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Amvg3", + "name": "t", + "fill": "$text-tertiary", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Y8OPIv", + "name": "CL11", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "WYzoD", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "11", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "S5NjhG", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "AMSNw", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "t1LSBG", + "name": "Ind", + "width": 19.86, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "hpSF3", + "name": "t", + "fill": "$info", + "content": "listen", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "WH5CY", + "name": "t", + "fill": "$warn", + "content": "80;", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "p0xnv", + "name": "CL12", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "fRZzF", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "12", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "i4AjC", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "nKFMv", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "TH2t0", + "name": "Ind", + "width": 19.86, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "X1n3MZ", + "name": "t", + "fill": "$info", + "content": "listen", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "DtJQn", + "name": "t", + "fill": "$warn", + "content": "[::]:80;", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "d2NpJ", + "name": "CL13", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "RyUoE", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "13", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "J5foA", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "r8JDe", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "r2Sxrl", + "name": "Ind", + "width": 19.86, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "tV7eT", + "name": "t", + "fill": "$info", + "content": "server_name", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "fV5EM", + "name": "t", + "fill": "$ok", + "content": "shop.acme.io;", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "fUU3Q", + "name": "CL14", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "kgs8G", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "14", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "r9aeTL", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "tFHPT", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center" + } + ] + }, + { + "type": "frame", + "id": "gz3nG", + "name": "CL15", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "fmcBg", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "15", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "WzoUF", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "zYgal", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "IhTIp", + "name": "Ind", + "width": 19.86, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "t8EFgY", + "name": "t", + "fill": "$info", + "content": "access_log", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "D8IB0f", + "name": "t", + "fill": "$ok", + "content": "/var/log/nginx/access.log", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "JMDBn", + "name": "t", + "fill": "$ok", + "content": "main;", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "d2kY1B", + "name": "CL16", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "wKNqa", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "16", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "PVtfX", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ZCy8Y", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "wfjRH", + "name": "Ind", + "width": 19.86, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Kc3EX", + "name": "t", + "fill": "$info", + "content": "error_log", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "kuWpc", + "name": "t", + "fill": "$ok", + "content": "/var/log/nginx/error.log", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "xwIK9", + "name": "t", + "fill": "$ok", + "content": "warn;", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "uoJum", + "name": "CL17", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "YeEYZ", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "17", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "M9a3B1", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "yVnwC", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center" + } + ] + }, + { + "type": "frame", + "id": "RGSAa", + "name": "CL18", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "$warn-dim", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "xwgiY", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "18", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ahZmu", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "content": "~", + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "L3PNHC", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "XpoNe", + "name": "Ind", + "width": 19.86, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "A014x", + "name": "t", + "fill": "$info", + "content": "client_max_body_size", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "y1ZwK", + "name": "t", + "fill": "$warn", + "content": "25m;", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "l9QKRw", + "name": "CL19", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "s3TgBJ", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "19", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "bitp5", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "v1F6g", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Fex5F", + "name": "Ind", + "width": 19.86, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "KHL6F", + "name": "t", + "fill": "$info", + "content": "gzip", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Pfv20", + "name": "t", + "fill": "$ok", + "content": "on;", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "j1k52", + "name": "CL20", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "i4W8l", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "20", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "N9OU7p", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "M566T", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "OLJXS", + "name": "Ind", + "width": 19.86, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Cr38r", + "name": "t", + "fill": "$info", + "content": "gzip_types", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "hM3Wp", + "name": "t", + "fill": "$ok", + "content": "text/css", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "bxtWs", + "name": "t", + "fill": "$ok", + "content": "application/javascript", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "f3emt", + "name": "t", + "fill": "$ok", + "content": "application/json;", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "BpkgO", + "name": "CL21", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "cs0r6", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "21", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "hqpBy", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "JXDyh", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center" + } + ] + }, + { + "type": "frame", + "id": "P9LhLr", + "name": "CL22", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ddXST", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "22", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "m7VlK", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "nGNNR", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "x4o2y", + "name": "Ind", + "width": 19.86, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "FIxyg", + "name": "t", + "fill": "$info", + "content": "location", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "wtxWK", + "name": "t", + "fill": "$ok", + "content": "/", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "hJTDP", + "name": "t", + "fill": "$text-tertiary", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Zo1Yq", + "name": "CL23", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "W2dBuU", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "23", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "vcRrK", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "KK0Xw", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "f4uIJK", + "name": "Ind", + "width": 46.34, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Hppcy", + "name": "t", + "fill": "$info", + "content": "root", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "YYWU0", + "name": "t", + "fill": "$ok", + "content": "/usr/share/nginx/html;", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "D8qduf", + "name": "CL24", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "JmzSI", + "name": "No", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 44, + "content": "24", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "QDdQj", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "n2iBc", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "milnN", + "name": "Ind", + "width": 46.34, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "FVSVB", + "name": "t", + "fill": "$info", + "content": "index", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "OikDz", + "name": "t", + "fill": "$ok", + "content": "index.html;", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "B2DQh", + "name": "CL25", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "tVeCH", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "25", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "LOmLH", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "nrnBf", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "RfR3w", + "name": "Ind", + "width": 46.34, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "T7UTV", + "name": "t", + "fill": "$info", + "content": "try_files", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "O3IJOl", + "name": "t", + "fill": "$violet", + "content": "$uri", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "A7oho", + "name": "t", + "fill": "$violet", + "content": "$uri/", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "R5AAr4", + "name": "t", + "fill": "$ok", + "content": "/index.html;", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ZLeCa", + "name": "CL26", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Gt7zi", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "26", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "UttuZ", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "EhAZd", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Z2nAwP", + "name": "Ind", + "width": 19.86, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "JI9DQ", + "name": "t", + "fill": "$text-tertiary", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "w10Ol", + "name": "CL28", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "M6mRH", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "28", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "kZowq", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "F6W13b", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "dBobm", + "name": "Ind", + "width": 19.86, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Rav2p", + "name": "t", + "fill": "$info", + "content": "location", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "C4LN5", + "name": "t", + "fill": "$ok", + "content": "/api/", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "QA7gC", + "name": "t", + "fill": "$text-tertiary", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Hytem", + "name": "CL29", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "C7dKb", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "29", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "l5RrsQ", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "gXxc7", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Bxavf", + "name": "Ind", + "width": 46.34, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "x6z6iS", + "name": "t", + "fill": "$info", + "content": "proxy_pass", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "NwfcJ", + "name": "t", + "fill": "$ok", + "content": "http://api_backend;", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "i5skiD", + "name": "CL30", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "VfmBm", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "30", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "JQAta", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "usAW0", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "P3TsTN", + "name": "Ind", + "width": 46.34, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "DLdHn", + "name": "t", + "fill": "$info", + "content": "proxy_http_version", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "XWdVR", + "name": "t", + "fill": "$warn", + "content": "1.1;", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "YyqWb", + "name": "CL31", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "K3ZU3p", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "31", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "dJ8b3", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "FBg44", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "LokMy", + "name": "Ind", + "width": 46.34, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "v6ELm", + "name": "t", + "fill": "$info", + "content": "proxy_set_header", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ILWWI", + "name": "t", + "fill": "$ok", + "content": "Host", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "F8XJj", + "name": "t", + "fill": "$violet", + "content": "$host;", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "q41L6F", + "name": "CL32", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "J4EW6", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "32", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "kCy0j", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "k8RZvD", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "dch7h", + "name": "Ind", + "width": 46.34, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "gaivG", + "name": "t", + "fill": "$info", + "content": "proxy_set_header", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "odtUr", + "name": "t", + "fill": "$ok", + "content": "X-Real-IP", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "KNCyg", + "name": "t", + "fill": "$violet", + "content": "$remote_addr;", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "wqh6E", + "name": "CL33", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "bfaIl", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "33", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "v0Z5W", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "FimND", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "D1Y98s", + "name": "Ind", + "width": 46.34, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "c8HFwv", + "name": "t", + "fill": "$info", + "content": "proxy_set_header", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "MatEK", + "name": "t", + "fill": "$ok", + "content": "X-Forwarded-For", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "WfFEz", + "name": "t", + "fill": "$violet", + "content": "$proxy_add_x_forwarded_for;", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "WUXy0", + "name": "CL34", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "a1Bk5", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "34", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "iLu7Z", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "CBY8m", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Z8rZD", + "name": "Ind", + "width": 46.34, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "bb3Si", + "name": "t", + "fill": "$info", + "content": "proxy_read_timeout", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "P4ajlA", + "name": "t", + "fill": "$warn", + "content": "60s;", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "cLww2", + "name": "CL35", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "X5LjTP", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "35", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "gBWVS", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "fzlRK", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "ceFxD", + "name": "Ind", + "width": 19.86, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "j365r0", + "name": "t", + "fill": "$text-tertiary", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "R5jwU", + "name": "CL36", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "C97nJ", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "36", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "gmviP", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "wDRam", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center" + } + ] + }, + { + "type": "frame", + "id": "LNg1r", + "name": "CL37", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "vmZVn", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "37", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "GYljG", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "cL3h5", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "ZVsSF", + "name": "Ind", + "width": 19.86, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "NCIvu", + "name": "t", + "fill": "$info", + "content": "location", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "RCiUU", + "name": "t", + "fill": "$ok", + "content": "/health", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Ljhau", + "name": "t", + "fill": "$text-tertiary", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "gnexh", + "name": "CL38", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "e8AzC", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "38", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "PumX3", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "O7Dw2l", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "n3fAU", + "name": "Ind", + "width": 46.34, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "ZDxcU", + "name": "t", + "fill": "$info", + "content": "access_log", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "vB7im", + "name": "t", + "fill": "$ok", + "content": "off;", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "VLQFj", + "name": "CL39", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "z4bgz", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "39", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "eCpJR", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "yc7lA", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "OWDeE", + "name": "Ind", + "width": 46.34, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "bqclv", + "name": "t", + "fill": "$info", + "content": "return", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "n6wZb3", + "name": "t", + "fill": "$warn", + "content": "200", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "QVbzo", + "name": "t", + "fill": "$ok", + "content": "\"ok\";", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Uopk2", + "name": "CL40", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "hecM0", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "40", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Onmhi", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "B5QxyF", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "KV4vC", + "name": "Ind", + "width": 19.86, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "x1fda", + "name": "t", + "fill": "$text-tertiary", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Xwbe1", + "name": "CL41", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "jWons", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 44, + "content": "41", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "scjJD", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 18, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "JHaT2", + "name": "Tokens", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mWOLx", + "name": "t", + "fill": "$text-tertiary", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "k68RAO", + "name": "Editor Foot", + "width": "fill_container", + "height": 26, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 14, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "LlTGZ", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "shield-alert", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "text", + "id": "I5CWxh", + "name": "W", + "fill": "$warn", + "content": "覆盖容器内文件属于会丢数据的操作 —— 保存需手打 save 确认", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "WEDwE", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "OmbCa", + "name": "行 24, 列 18", + "fill": "$text-muted", + "content": "行 24, 列 18", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "VxkQA", + "name": "已修改 3 行", + "fill": "$text-muted", + "content": "已修改 3 行", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ZqVQb", + "name": "UTF-8", + "fill": "$text-muted", + "content": "UTF-8", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "McjC0", + "name": "LF", + "fill": "$text-muted", + "content": "LF", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "h0u1d4", + "name": "nginx", + "fill": "$text-muted", + "content": "nginx", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "E04Dz", + "name": "File Info", + "clip": true, + "width": 290, + "height": "fill_container", + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "left": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 10, + "padding": [ + 12, + 14 + ], + "children": [ + { + "type": "frame", + "id": "R1K9TW", + "name": "Sec 文件", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "T6i5oN", + "name": "H", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "wvxkR", + "name": "T", + "fill": "$text-secondary", + "content": "文件", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "DMuLd", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "id": "gXUDH", + "type": "ref", + "ref": "l3qo4", + "name": "KV 大小", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "大小", + "width": 80 + }, + "psgr6": { + "content": "1.8 KB", + "fill": "$text-primary" + } + } + }, + { + "id": "Q32WR", + "type": "ref", + "ref": "l3qo4", + "name": "KV 权限", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "权限", + "width": 80 + }, + "psgr6": { + "content": "0644", + "fill": "$text-primary" + } + } + }, + { + "id": "H199kI", + "type": "ref", + "ref": "l3qo4", + "name": "KV 属主", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "属主", + "width": 80 + }, + "psgr6": { + "content": "root:root", + "fill": "$text-primary" + } + } + }, + { + "id": "J56KGX", + "type": "ref", + "ref": "l3qo4", + "name": "KV 修改时间", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "修改时间", + "width": 80 + }, + "psgr6": { + "content": "09:12:44", + "fill": "$text-primary" + } + } + }, + { + "id": "AagL6", + "type": "ref", + "ref": "l3qo4", + "name": "KV 相对镜像", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "相对镜像", + "width": 80 + }, + "psgr6": { + "content": "已修改 (C)", + "fill": "$text-primary" + } + } + } + ] + }, + { + "type": "rectangle", + "id": "OeXIH", + "name": "Div", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "MlhqZ", + "name": "Sec 写回方式", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "r5nNhE", + "name": "H", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ib2As", + "name": "T", + "fill": "$text-secondary", + "content": "写回方式", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "ScOdA", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "JAXN4", + "name": "Box", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 5, + "padding": [ + 9, + 10 + ], + "children": [ + { + "type": "text", + "id": "BfVbR", + "name": "T1", + "fill": "$info", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "PUT /containers/{id}/archive", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Z9bSbf", + "name": "T2", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "整个文件以 tar 流写回容器可写层。不经过 shell,不受登录 shell 与引用规则影响。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "GIBaH", + "name": "Div", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "p3NgEP", + "name": "Sec 保存后", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "Y2XVY", + "name": "H", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "wvWJn", + "name": "T", + "fill": "$text-secondary", + "content": "保存后", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "ksvmz", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "lI2xR", + "name": "Box", + "width": "fill_container", + "fill": "$warn-dim", + "cornerRadius": "$r-md", + "layout": "vertical", + "gap": 6, + "padding": [ + 9, + 10 + ], + "children": [ + { + "type": "text", + "id": "AG1Kn", + "name": "T", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "nginx 不会自动重载配置", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "KSXGN", + "name": "T2", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "保存后可一并执行 nginx -s reload,或重启容器。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "jmNrz", + "name": "Row", + "width": "fill_container", + "gap": 6, + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "UAtL4", + "name": "Check", + "fill": "$warn", + "width": 12, + "height": 12, + "stroke": "$warn", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "TqL0q", + "name": "L", + "fill": "$warn", + "content": "保存后执行 nginx -s reload", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "I1HEkc", + "name": "Div", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "iOWHZ", + "name": "Sec 本面板写入历史", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "u5sgH", + "name": "H", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "c4WdF", + "name": "T", + "fill": "$text-secondary", + "content": "本面板写入历史", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "qTXcH", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "x2LpT8", + "name": "H 09:12:44", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "x3XWIj", + "name": "T", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 74, + "content": "09:12:44", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "XYqCJ", + "name": "V", + "fill": "$text-secondary", + "content": "default.conf · +2 −1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "hzS2p", + "name": "H 昨天 18:02", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ZQoEe", + "name": "T", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 74, + "content": "昨天 18:02", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "hpohE", + "name": "V", + "fill": "$text-secondary", + "content": "upstreams.conf · 新建", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "g2Nkp", + "name": "H 08-19 11:30", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "RGl9N", + "name": "T", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 74, + "content": "08-19 11:30", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "rTdBE", + "name": "V", + "fill": "$text-secondary", + "content": "default.conf · +6 −4", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Xlxul", + "name": "Spacer", + "width": "fill_container", + "height": "fill_container", + "fill": "#00000000" + }, + { + "type": "frame", + "id": "d0Zwx6", + "name": "Upload", + "width": "fill_container", + "fill": "#00000000", + "cornerRadius": "$r-md", + "stroke": "$border-strong", + "strokeWidth": 1, + "layout": "vertical", + "gap": 5, + "padding": [ + 14, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "M5sTn", + "name": "I", + "width": 16, + "height": 16, + "icon": "upload", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "q6U9JW", + "name": "T", + "fill": "$text-tertiary", + "content": "拖入文件替换,或上传到当前目录", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "qV0L8", + "name": "Status Bar", + "width": "fill_container", + "height": 24, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 14, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "yGrAZ", + "name": "Msg", + "fill": "$text-tertiary", + "content": "写回将通过 PUT /containers/a3f2c81b/archive · 容器仍在运行,nginx 需 reload 才生效", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "eHI9I", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "APEVt", + "name": "API v1.47", + "fill": "$text-muted", + "content": "API v1.47", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "T5byK", + "name": "Engine 27.3.1", + "fill": "$text-muted", + "content": "Engine 27.3.1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "jKiBX", + "name": "18 容器 · 34 镜像 · 9 卷", + "fill": "$text-muted", + "content": "18 容器 · 34 镜像 · 9 卷", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "MNZfl", + "name": "事件流 已连接", + "fill": "$text-muted", + "content": "事件流 已连接", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "BVGD8", + "x": 1059, + "y": 2940, + "name": "05 · 容器终端(exec / WebSocket)", + "theme": { + "mode": "dark" + }, + "clip": true, + "width": 1440, + "height": 860, + "fill": "$bg-page", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "ghwzr", + "name": "Top Bar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "j2F1kp", + "name": "Brand", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "Sd12j", + "name": "Logo", + "width": 17, + "height": 17, + "icon": "container", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "Npk6G", + "name": "Title", + "fill": "$text-primary", + "content": "Docker", + "fontFamily": "$font-ui", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "rectangle", + "id": "XkIcL", + "name": "Divider", + "fill": "$border", + "width": 1, + "height": 18 + }, + { + "type": "frame", + "id": "X8desc", + "name": "Host Switcher", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "KL609", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "server", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "qdM5G", + "name": "Host", + "fill": "$text-primary", + "content": "prod-sg-01", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "LaDP1", + "name": "Transport", + "fill": "$text-muted", + "content": "/var/run/docker.sock · SSH 隧道", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "zCBGW", + "name": "Caret", + "width": 12, + "height": 12, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + }, + { + "type": "frame", + "id": "VPyLP", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "yDGEE", + "name": "Search", + "width": 300, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "CnbRR", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "search", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "e4d3O", + "name": "Placeholder", + "fill": "$text-muted", + "content": "搜索容器、镜像、卷、网络…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "KXHJO", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "X5I50y", + "name": "Kbd", + "height": 16, + "fill": "$bg-hover", + "cornerRadius": 3, + "padding": [ + 0, + 5 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "AmkEp", + "name": "Keys", + "fill": "$text-tertiary", + "content": "Ctrl K", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "id": "DmDDk", + "type": "ref", + "ref": "x0Pcos", + "name": "Live Badge", + "descendants": { + "ODVko": { + "content": "实时" + } + } + }, + { + "id": "yRwZr", + "type": "ref", + "ref": "yoK0T", + "name": "Settings", + "descendants": { + "XMGqY": { + "icon": "settings" + } + } + } + ] + }, + { + "type": "frame", + "id": "VoWzd", + "name": "Body", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "Ivo7U", + "name": "Rail", + "width": 56, + "height": "fill_container", + "fill": "$bg-rail", + "stroke": "$border", + "strokeWidth": { + "right": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 2, + "padding": [ + 6, + 0 + ], + "alignItems": "center", + "children": [ + { + "id": "dG3cY", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 总览", + "descendants": { + "F1F9E": { + "icon": "layout-dashboard", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "总览", + "fill": "$text-tertiary" + } + } + }, + { + "id": "vPEqa", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 容器", + "descendants": { + "cQhxS": { + "fill": "$accent-dim" + }, + "F1F9E": { + "icon": "box", + "fill": "$accent" + }, + "DE5G9": { + "content": "容器", + "fill": "$accent" + } + } + }, + { + "id": "E7Bdb", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 镜像", + "descendants": { + "F1F9E": { + "icon": "layers", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "镜像", + "fill": "$text-tertiary" + } + } + }, + { + "id": "g50kIE", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 卷", + "descendants": { + "F1F9E": { + "icon": "hard-drive", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "卷", + "fill": "$text-tertiary" + } + } + }, + { + "id": "D5LsLy", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 网络", + "descendants": { + "F1F9E": { + "icon": "network", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "网络", + "fill": "$text-tertiary" + } + } + }, + { + "id": "FsiVP", + "type": "ref", + "ref": "nKcgr", + "name": "Nav Compose", + "descendants": { + "F1F9E": { + "icon": "boxes", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "Compose", + "fill": "$text-tertiary" + } + } + }, + { + "id": "Rf4qg", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 系统", + "descendants": { + "F1F9E": { + "icon": "gauge", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "系统", + "fill": "$text-tertiary" + } + } + }, + { + "type": "frame", + "id": "QlujM", + "name": "Spacer", + "width": 1, + "height": "fill_container", + "fill": "#00000000" + }, + { + "id": "k0oX3", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 设置", + "descendants": { + "F1F9E": { + "icon": "settings" + }, + "DE5G9": { + "content": "设置" + } + } + } + ] + }, + { + "type": "frame", + "id": "uRuXD", + "name": "Main", + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "hLEO1", + "name": "Term Toolbar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "cxQrf", + "name": "I", + "width": 15, + "height": 15, + "icon": "terminal", + "library": "lucide", + "fill": "$text-secondary" + }, + { + "type": "text", + "id": "cdi8a", + "name": "T", + "fill": "$text-primary", + "content": "终端", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "text", + "id": "g9UoD2", + "name": "Sub", + "fill": "$text-tertiary", + "content": "nginx-proxy · /bin/bash · root · /etc/nginx", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Pl1pG", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "wGH9K", + "type": "ref", + "ref": "x0Pcos", + "name": "Conn", + "descendants": { + "ODVko": { + "content": "已连接" + } + } + }, + { + "id": "zRaIj", + "type": "ref", + "ref": "i1OTz", + "name": "B 切换用户", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "user" + }, + "P0aUw": { + "content": "切换用户", + "fontSize": 11 + } + } + }, + { + "id": "HNrWH", + "type": "ref", + "ref": "i1OTz", + "name": "B 工作目录", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "folder" + }, + "P0aUw": { + "content": "工作目录", + "fontSize": 11 + } + } + }, + { + "id": "havQb", + "type": "ref", + "ref": "yoK0T", + "name": "A eraser", + "descendants": { + "XMGqY": { + "icon": "eraser" + } + } + }, + { + "id": "zv4a6", + "type": "ref", + "ref": "yoK0T", + "name": "A copy", + "descendants": { + "XMGqY": { + "icon": "copy" + } + } + }, + { + "id": "jQ2X8", + "type": "ref", + "ref": "yoK0T", + "name": "A external-link", + "descendants": { + "XMGqY": { + "icon": "external-link" + } + } + }, + { + "id": "BtYZK", + "type": "ref", + "ref": "yoK0T", + "name": "A power", + "descendants": { + "XMGqY": { + "icon": "power" + } + } + } + ] + }, + { + "type": "frame", + "id": "MqfDe", + "name": "Session Tabs", + "width": "fill_container", + "height": 30, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 2, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "id": "mNoA4", + "type": "ref", + "ref": "MdmRT", + "name": "S bash · nginx-proxy", + "height": 24, + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "icon": "terminal", + "width": 11, + "height": 11, + "fill": "$ok" + }, + "iscyV": { + "content": "bash · nginx-proxy", + "fontSize": 11, + "fontFamily": "$font-mono", + "fill": "$text-primary" + } + } + }, + { + "id": "i4DVn", + "type": "ref", + "ref": "MdmRT", + "name": "S sh · redis-cache", + "height": 24, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "terminal", + "width": 11, + "height": 11, + "fill": "$text-muted" + }, + "iscyV": { + "content": "sh · redis-cache", + "fontSize": 11, + "fontFamily": "$font-mono", + "fill": "$text-tertiary" + } + } + }, + { + "id": "OLXX8", + "type": "ref", + "ref": "yoK0T", + "name": "New", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "plus", + "width": 12, + "height": 12 + } + } + } + ] + }, + { + "type": "frame", + "id": "XunrP", + "name": "Term Body", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-terminal", + "layout": "vertical", + "padding": [ + 10, + 14 + ], + "children": [ + { + "type": "frame", + "id": "IZvGH", + "name": "TL", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "yyP1B", + "name": "t", + "fill": "$text-muted", + "content": "Docker exec 会话已建立 · TTY 80x24 · /bin/bash", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "rvTUa", + "name": "TL", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "n1O1u", + "name": "t", + "fill": "$text-terminal", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "rZeYK", + "name": "TL", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "I3kXF8", + "name": "t", + "fill": "$ok", + "content": "root@a3f2c81b9d4e:/etc/nginx#", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Ahzy8", + "name": "t", + "fill": "$text-terminal", + "content": "nginx -t", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "leoQU", + "name": "TL", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "WC4Sq", + "name": "t", + "fill": "$text-tertiary", + "content": "nginx: the configuration file /etc/nginx/nginx.conf syntax is ok", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "UTmMO", + "name": "TL", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "v7XmV", + "name": "t", + "fill": "$text-tertiary", + "content": "nginx: configuration file /etc/nginx/nginx.conf test is successful", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "mYQoq", + "name": "TL", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "xmxG1", + "name": "t", + "fill": "$ok", + "content": "root@a3f2c81b9d4e:/etc/nginx#", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "hah1g", + "name": "t", + "fill": "$text-terminal", + "content": "nginx -s reload", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "hJ3Mt", + "name": "TL", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "o8ZX9l", + "name": "t", + "fill": "$ok", + "content": "root@a3f2c81b9d4e:/etc/nginx#", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "jFlgU", + "name": "t", + "fill": "$text-terminal", + "content": "ls -l conf.d/", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "UZ6Ac", + "name": "TL", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dr53e", + "name": "t", + "fill": "$text-tertiary", + "content": "total 12", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "EU6Jv", + "name": "TL", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "PvIbG", + "name": "t", + "fill": "$text-tertiary", + "content": "-rw-r--r-- 1 root root 1842 Aug 21 09:12", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "zRJw4", + "name": "t", + "fill": "$info", + "content": "default.conf", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "xQzGz", + "name": "TL", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "i7L6g", + "name": "t", + "fill": "$text-tertiary", + "content": "-rw-r--r-- 1 root root 318 Aug 21 09:12", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "G3YZBe", + "name": "t", + "fill": "$ok", + "content": "upstreams.conf", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "hRwBl", + "name": "TL", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "U0Ae8", + "name": "t", + "fill": "$ok", + "content": "root@a3f2c81b9d4e:/etc/nginx#", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Q8JGG", + "name": "t", + "fill": "$text-terminal", + "content": "curl -s -o /dev/null -w '%{http_code}\\n' localhost/health", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "zb6oW", + "name": "TL", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ChIIU", + "name": "t", + "fill": "$warn", + "content": "200", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "oPXnT", + "name": "TL", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Q7U9nP", + "name": "t", + "fill": "$ok", + "content": "root@a3f2c81b9d4e:/etc/nginx#", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "XMAZs", + "name": "t", + "fill": "$text-terminal", + "content": "ps aux | head -5", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "VjEuL", + "name": "TL", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "CBDh7", + "name": "t", + "fill": "$text-muted", + "content": "USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "O3NSL", + "name": "TL", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "y4HgOI", + "name": "t", + "fill": "$text-tertiary", + "content": "root 1 0.0 0.1 11048 6420 ? Ss 09:12 0:00 nginx: master process", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Z1MSl", + "name": "TL", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "vPP9d", + "name": "t", + "fill": "$text-tertiary", + "content": "nginx 29 0.1 0.2 11732 8104 ? S 09:12 0:04 nginx: worker process", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "qYjSK", + "name": "TL", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "IoKAf", + "name": "t", + "fill": "$text-tertiary", + "content": "nginx 30 0.1 0.2 11732 8096 ? S 09:12 0:03 nginx: worker process", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "gwaTg", + "name": "TL", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "PuHdu", + "name": "t", + "fill": "$ok", + "content": "root@a3f2c81b9d4e:/etc/nginx#", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "y4khGo", + "name": "t", + "fill": "$text-terminal", + "content": "df -h /var/cache/nginx", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "JlBiZ", + "name": "TL", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "nHB8l", + "name": "t", + "fill": "$text-muted", + "content": "Filesystem Size Used Avail Use% Mounted on", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "lSfqs", + "name": "TL", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "uq1H7", + "name": "t", + "fill": "$text-tertiary", + "content": "/dev/sda1 94G 38G 52G 43% /var/cache/nginx", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "k8zD2", + "name": "TL", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 6.62, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "q7JKYi", + "name": "t", + "fill": "$ok", + "content": "root@a3f2c81b9d4e:/etc/nginx#", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ySxsd", + "name": "t", + "fill": "$text-terminal", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "LtfIW", + "name": "Spacer", + "width": "fill_container", + "height": "fill_container", + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "xpFQi", + "name": "Term Foot", + "width": "fill_container", + "height": 26, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 14, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "i9BPJ", + "name": "I", + "width": 12, + "height": 12, + "icon": "info", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "zMyoF", + "name": "T", + "fill": "$text-muted", + "content": "命令与输出不写入宿主会话日志 · Ctrl+D 或关闭标签即结束 exec", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "zMptX", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "fyaNB", + "name": "80 × 24", + "fill": "$text-muted", + "content": "80 × 24", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "jziG1", + "name": "UTF-8", + "fill": "$text-muted", + "content": "UTF-8", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "mpVSk", + "name": "bash 5.2.15", + "fill": "$text-muted", + "content": "bash 5.2.15", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "vIRkx", + "name": "exec c81f…9a2", + "fill": "$text-muted", + "content": "exec c81f…9a2", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "XsqCG", + "name": "Detail Drawer", + "width": 440, + "height": "fill_container", + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "left": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "ozqNR", + "name": "Drawer Header", + "width": "fill_container", + "height": 44, + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "iF0sd", + "name": "Dot", + "fill": "$ok", + "width": 7, + "height": 7 + }, + { + "type": "text", + "id": "UOHLh", + "name": "Name", + "fill": "$text-primary", + "content": "nginx-proxy", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "id": "qTUuz", + "type": "ref", + "ref": "x0Pcos", + "name": "State Chip", + "descendants": { + "ODVko": { + "content": "运行中 · healthy" + } + } + }, + { + "type": "frame", + "id": "H5K6s", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "Jp1mn", + "type": "ref", + "ref": "yoK0T", + "name": "H pin", + "descendants": { + "XMGqY": { + "icon": "pin" + } + } + }, + { + "id": "RoYgr", + "type": "ref", + "ref": "yoK0T", + "name": "H maximize-2", + "descendants": { + "XMGqY": { + "icon": "maximize-2" + } + } + }, + { + "id": "kTFhF", + "type": "ref", + "ref": "yoK0T", + "name": "H x", + "descendants": { + "XMGqY": { + "icon": "x" + } + } + } + ] + }, + { + "type": "frame", + "id": "WqF1a", + "name": "Drawer Subhead", + "width": "fill_container", + "height": 26, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 7, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "JA9Af", + "name": "Id", + "fill": "$text-tertiary", + "content": "a3f2c81b9d4e", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "V9JD7J", + "name": "Copy", + "width": 11, + "height": 11, + "icon": "copy", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "hLLvF", + "name": "Sep", + "fill": "$text-muted", + "content": "·", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "fbUoD", + "name": "Proj", + "fill": "$violet", + "content": "web-stack / proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "EQKtT", + "name": "Drawer Tabs", + "width": "fill_container", + "height": 34, + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 2, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "id": "DzR30", + "type": "ref", + "ref": "MdmRT", + "name": "T 概览", + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "icon": "info", + "fill": "$text-primary" + }, + "iscyV": { + "content": "概览", + "fill": "$text-primary", + "fontWeight": "500" + } + } + }, + { + "id": "DV6vN", + "type": "ref", + "ref": "MdmRT", + "name": "T 日志", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "scroll-text", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "日志", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + }, + { + "id": "EUTqR", + "type": "ref", + "ref": "MdmRT", + "name": "T 文件", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "folder-tree", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "文件", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + }, + { + "id": "S3rlle", + "type": "ref", + "ref": "MdmRT", + "name": "T 终端", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "terminal", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "终端", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + }, + { + "id": "q2O78U", + "type": "ref", + "ref": "MdmRT", + "name": "T 统计", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "activity", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "统计", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + } + ] + }, + { + "type": "frame", + "id": "BTOAh", + "name": "Quick Actions", + "width": "fill_container", + "height": 42, + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "id": "JqxLA", + "type": "ref", + "ref": "i1OTz", + "name": "Q 停止", + "descendants": { + "RQ8pM": { + "icon": "square" + }, + "P0aUw": { + "content": "停止" + } + } + }, + { + "id": "efVXD", + "type": "ref", + "ref": "i1OTz", + "name": "Q 重启", + "descendants": { + "RQ8pM": { + "icon": "rotate-cw" + }, + "P0aUw": { + "content": "重启" + } + } + }, + { + "id": "wfa72", + "type": "ref", + "ref": "i1OTz", + "name": "Q 暂停", + "descendants": { + "RQ8pM": { + "icon": "pause" + }, + "P0aUw": { + "content": "暂停" + } + } + }, + { + "id": "CDfIA", + "type": "ref", + "ref": "i1OTz", + "name": "Q 终端", + "fill": "$accent-dim", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "terminal", + "fill": "$accent" + }, + "P0aUw": { + "content": "进入终端", + "fill": "$accent" + } + } + }, + { + "type": "frame", + "id": "jMWaG", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "dBnfM", + "type": "ref", + "ref": "yoK0T", + "name": "More", + "descendants": { + "XMGqY": { + "icon": "ellipsis" + } + } + } + ] + }, + { + "type": "frame", + "id": "akscw", + "name": "Drawer Content", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-elevated", + "layout": "vertical", + "gap": 10, + "padding": [ + 12, + 14 + ], + "children": [ + { + "type": "frame", + "id": "MDhPf", + "name": "Live Metrics", + "width": "fill_container", + "gap": 10, + "children": [ + { + "type": "frame", + "id": "spjBS", + "name": "M CPU", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 6, + "padding": [ + 10, + 11 + ], + "children": [ + { + "type": "frame", + "id": "w2kdY", + "name": "Top", + "width": "fill_container", + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Z5YngG", + "name": "Label", + "fill": "$text-tertiary", + "content": "CPU", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "A9fP0", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "U9Z6A", + "name": "Delta", + "fill": "$text-muted", + "content": "峰值 6.4%", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "UoL6w", + "name": "Value", + "gap": 3, + "alignItems": "end", + "children": [ + { + "type": "text", + "id": "jhGMx", + "name": "Num", + "fill": "$text-primary", + "content": "2.1", + "fontFamily": "$font-ui", + "fontSize": 20, + "fontWeight": "600" + }, + { + "type": "text", + "id": "OEWAo", + "name": "Unit", + "fill": "$text-tertiary", + "content": "%", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "TawsT", + "type": "ref", + "ref": "MhYIL", + "name": "Spark", + "height": 30, + "descendants": { + "fshVw": { + "height": 8, + "width": 9, + "fill": "$accent" + }, + "srvmw": { + "height": 6, + "width": 9, + "fill": "$accent" + }, + "ky1P1": { + "height": 11, + "width": 9, + "fill": "$accent" + }, + "bqMzi": { + "height": 7, + "width": 9, + "fill": "$accent" + }, + "F5Kloq": { + "height": 14, + "width": 9, + "fill": "$accent" + }, + "StdGN": { + "height": 9, + "width": 9, + "fill": "$accent" + }, + "iq3Ux": { + "height": 6, + "width": 9, + "fill": "$accent" + }, + "PHY2v": { + "height": 10, + "width": 9, + "fill": "$accent" + }, + "xqaP5": { + "height": 18, + "width": 9, + "fill": "$accent" + }, + "cZHdc": { + "height": 8, + "width": 9, + "fill": "$accent" + }, + "jhPqP": { + "height": 7, + "width": 9, + "fill": "$accent" + }, + "Za8Yw": { + "height": 9, + "width": 9, + "fill": "$accent" + }, + "zbp8o": { + "height": 12, + "width": 9, + "fill": "$accent" + }, + "kMt0y": { + "height": 8, + "width": 9, + "fill": "$accent" + } + } + } + ] + }, + { + "type": "frame", + "id": "jvHAv", + "name": "M 内存", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 6, + "padding": [ + 10, + 11 + ], + "children": [ + { + "type": "frame", + "id": "kkfoL", + "name": "Top", + "width": "fill_container", + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "YVeMY", + "name": "Label", + "fill": "$text-tertiary", + "content": "内存", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "neDp8", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "huzZZ", + "name": "Delta", + "fill": "$text-muted", + "content": "24.9%", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "x1DgM", + "name": "Value", + "gap": 3, + "alignItems": "end", + "children": [ + { + "type": "text", + "id": "pc5cv", + "name": "Num", + "fill": "$text-primary", + "content": "128", + "fontFamily": "$font-ui", + "fontSize": 20, + "fontWeight": "600" + }, + { + "type": "text", + "id": "jrKDo", + "name": "Unit", + "fill": "$text-tertiary", + "content": "MB / 512", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "ChRXN", + "type": "ref", + "ref": "MhYIL", + "name": "Spark", + "height": 30, + "descendants": { + "fshVw": { + "height": 11, + "width": 7, + "fill": "$ok" + }, + "srvmw": { + "height": 13, + "width": 7, + "fill": "$ok" + }, + "ky1P1": { + "height": 12, + "width": 7, + "fill": "$ok" + }, + "bqMzi": { + "height": 15, + "width": 7, + "fill": "$ok" + }, + "F5Kloq": { + "height": 14, + "width": 7, + "fill": "$ok" + }, + "StdGN": { + "height": 16, + "width": 7, + "fill": "$ok" + }, + "iq3Ux": { + "height": 15, + "width": 7, + "fill": "$ok" + }, + "PHY2v": { + "height": 18, + "width": 7, + "fill": "$ok" + }, + "xqaP5": { + "height": 17, + "width": 7, + "fill": "$ok" + }, + "cZHdc": { + "height": 16, + "width": 7, + "fill": "$ok" + }, + "jhPqP": { + "height": 19, + "width": 7, + "fill": "$ok" + }, + "Za8Yw": { + "height": 18, + "width": 7, + "fill": "$ok" + }, + "zbp8o": { + "height": 20, + "width": 7, + "fill": "$ok" + }, + "kMt0y": { + "height": 19, + "width": 7, + "fill": "$ok" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "qJmOP", + "name": "Sec 基本信息", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "mHYYo", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "VMGY6", + "name": "Title", + "fill": "$text-secondary", + "content": "基本信息", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "WXtJ5", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "doP9x", + "name": "Action", + "fill": "$accent", + "content": "完整 inspect", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "t4HeJc", + "type": "ref", + "ref": "l3qo4", + "name": "KV 镜像", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "镜像" + }, + "psgr6": { + "content": "nginx:1.27-alpine", + "fill": "$text-primary" + } + } + }, + { + "id": "TnjC7", + "type": "ref", + "ref": "l3qo4", + "name": "KV 镜像摘要", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "镜像摘要" + }, + "psgr6": { + "content": "sha256:9c4f2e…1b7a", + "fill": "$text-primary" + } + } + }, + { + "id": "y4JPmb", + "type": "ref", + "ref": "l3qo4", + "name": "KV 创建于", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "创建于" + }, + "psgr6": { + "content": "2026-08-18 09:12:44", + "fill": "$text-primary" + } + } + }, + { + "id": "pgdfG", + "type": "ref", + "ref": "l3qo4", + "name": "KV 启动于", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "启动于" + }, + "psgr6": { + "content": "3 天前 (3d 4h 21m)", + "fill": "$text-primary" + } + } + }, + { + "id": "Y5o3DO", + "type": "ref", + "ref": "l3qo4", + "name": "KV 重启策略", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "重启策略" + }, + "psgr6": { + "content": "unless-stopped", + "fill": "$text-primary" + } + } + }, + { + "id": "XJDoP", + "type": "ref", + "ref": "l3qo4", + "name": "KV 平台", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "平台" + }, + "psgr6": { + "content": "linux/amd64", + "fill": "$text-primary" + } + } + }, + { + "id": "NlSvv", + "type": "ref", + "ref": "l3qo4", + "name": "KV 命令", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "命令" + }, + "psgr6": { + "content": "nginx -g 'daemon off;'", + "fill": "$text-primary" + } + } + } + ] + }, + { + "type": "rectangle", + "id": "zsQbz", + "name": "Divider", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "gIqjq", + "name": "Sec 端口映射", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "uturI", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "y9fOb", + "name": "Title", + "fill": "$text-secondary", + "content": "端口映射", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "hexKH", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Rrczv", + "name": "Action", + "fill": "$accent", + "content": "编辑", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "fhQ7l", + "type": "ref", + "ref": "l3qo4", + "name": "KV 8080/tcp", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "8080/tcp" + }, + "psgr6": { + "content": "0.0.0.0:8080 → 80", + "fill": "$info" + } + } + }, + { + "id": "Rf513", + "type": "ref", + "ref": "l3qo4", + "name": "KV 8443/tcp", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "8443/tcp" + }, + "psgr6": { + "content": "[::]:8443 → 443", + "fill": "$info" + } + } + } + ] + }, + { + "type": "rectangle", + "id": "HMjS5", + "name": "Divider", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "bI4VD", + "name": "Sec 挂载", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "d6m1Vl", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "qNIhs", + "name": "Title", + "fill": "$text-secondary", + "content": "挂载", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "pc6OX", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "FpIr2", + "name": "Action", + "fill": "$accent", + "content": "浏览文件", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "lwQcf", + "name": "Mount", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "V00o6", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "folder", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "xJmn2", + "name": "Path", + "fill": "$text-secondary", + "content": "/srv/nginx/conf.d → /etc/nginx/conf.d", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "bCUnn", + "type": "ref", + "ref": "x0Pcos", + "name": "Tag", + "height": 15, + "fill": "#00000000", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "只读", + "fill": "$warn", + "fontSize": 9 + } + } + } + ] + }, + { + "type": "frame", + "id": "hDoXe", + "name": "Mount", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "CP86j", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "database", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "IkYBJ", + "name": "Path", + "fill": "$text-secondary", + "content": "nginx-cache → /var/cache/nginx", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "vGgh0", + "type": "ref", + "ref": "x0Pcos", + "name": "Tag", + "height": 15, + "fill": "#00000000", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "读写", + "fill": "$ok", + "fontSize": 9 + } + } + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "aq6O8", + "name": "Divider", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "jj3Yf", + "name": "Sec 网络", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "paNKc", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "othN0", + "name": "Title", + "fill": "$text-secondary", + "content": "网络", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "DzEDs", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "f7KLEY", + "name": "Action", + "fill": "$accent", + "content": "接入其他网络", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "GUiJZ", + "type": "ref", + "ref": "l3qo4", + "name": "KV web-stack_default", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "网络" + }, + "psgr6": { + "content": "web-stack_default", + "fill": "$text-primary" + } + } + }, + { + "id": "OqrT7", + "type": "ref", + "ref": "l3qo4", + "name": "KV IP", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "IP 地址" + }, + "psgr6": { + "content": "172.20.0.4 · bridge · MTU 1500" + } + } + } + ] + }, + { + "type": "rectangle", + "id": "H5ugpx", + "name": "Divider", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "bUcfY", + "name": "Sec 健康检查", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "DzujQ", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "H00A2", + "name": "Title", + "fill": "$text-secondary", + "content": "健康检查", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "wtQEW", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "lc3ST", + "name": "HC", + "width": "fill_container", + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "id": "A7W2b", + "type": "ref", + "ref": "x0Pcos", + "name": "HC Chip", + "descendants": { + "ODVko": { + "content": "healthy" + } + } + }, + { + "type": "text", + "id": "FoAzv", + "name": "Detail", + "fill": "$text-tertiary", + "content": "每 30s · 0 次失败 · 12s 前", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "f5wfx", + "name": "Status Bar", + "width": "fill_container", + "height": 24, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 14, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "J5U6i", + "name": "Msg", + "fill": "$text-tertiary", + "content": "exec 会话 · TTY 已分配 · 通过 SSH 隧道上的 /var/run/docker.sock", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "uivOn", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "ImhYa", + "name": "API v1.47", + "fill": "$text-muted", + "content": "API v1.47", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "H5qLt", + "name": "Engine 27.3.1", + "fill": "$text-muted", + "content": "Engine 27.3.1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "V772X", + "name": "18 容器 · 34 镜像 · 9 卷", + "fill": "$text-muted", + "content": "18 容器 · 34 镜像 · 9 卷", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "H4McE", + "name": "事件流 已连接", + "fill": "$text-muted", + "content": "事件流 已连接", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "YgkX1", + "x": 0, + "y": 230, + "name": "C/ImageRow", + "reusable": true, + "width": 904, + "height": 32, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "v07HF", + "name": "Cell Select", + "clip": true, + "width": 26, + "height": 32, + "fill": "#00000000", + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "tKZcB", + "name": "Checkbox", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + } + ] + }, + { + "type": "frame", + "id": "E5x3J", + "name": "Cell Repo", + "clip": true, + "width": "fill_container", + "height": 32, + "fill": "#00000000", + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "WqYI3", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "layers", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "u8Bzdb", + "name": "Repo", + "fill": "$text-primary", + "content": "nginx", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "id": "R3W97", + "type": "ref", + "ref": "x0Pcos", + "name": "Tag", + "fill": "$accent-dim", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "1.27-alpine", + "fill": "$accent", + "fontFamily": "$font-mono" + } + } + } + ] + }, + { + "type": "frame", + "id": "Jm27m", + "name": "Cell Id", + "clip": true, + "width": 104, + "height": 32, + "fill": "#00000000", + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "e5FXq", + "name": "Id", + "fill": "$text-tertiary", + "content": "sha256:9c4f2e", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "DM19l", + "name": "Cell Size", + "clip": true, + "width": 84, + "height": 32, + "fill": "#00000000", + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "VObJ2", + "name": "Size", + "fill": "$text-secondary", + "content": "48.2 MB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "iLZIe", + "name": "Cell Created", + "clip": true, + "width": 118, + "height": 32, + "fill": "#00000000", + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "tISft", + "name": "Created", + "fill": "$text-tertiary", + "content": "3 周前", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "nuy7H", + "name": "Cell Used", + "clip": true, + "width": 96, + "height": 32, + "fill": "#00000000", + "gap": 6, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "YvpTs", + "name": "Used", + "fill": "$ok", + "content": "2 个容器", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "O1FJC8", + "name": "Cell Actions", + "clip": true, + "width": 120, + "height": 32, + "fill": "#00000000", + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "jtSZB", + "type": "ref", + "ref": "yoK0T", + "name": "A play", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "play", + "width": 13, + "height": 13 + } + } + }, + { + "id": "QyLyB", + "type": "ref", + "ref": "yoK0T", + "name": "A tag", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "tag", + "width": 13, + "height": 13 + } + } + }, + { + "id": "Rm5GE", + "type": "ref", + "ref": "yoK0T", + "name": "A upload", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "upload", + "width": 13, + "height": 13 + } + } + }, + { + "id": "H2A4q5", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "ARwD6", + "x": 0, + "y": 290, + "name": "C/Field", + "reusable": true, + "width": 420, + "fill": "#00000000", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "frame", + "id": "xSoVm", + "name": "LabelRow", + "width": "fill_container", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "T0xmy", + "name": "Label", + "fill": "$text-secondary", + "content": "容器名称", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "G4vkue", + "name": "Hint", + "fill": "$text-muted", + "content": "留空由 Docker 生成", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "S4IOy", + "name": "Input", + "width": "fill_container", + "height": 28, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "oLZwi", + "name": "Value", + "fill": "$text-primary", + "content": "nginx-proxy-2", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Ux3W7", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "jdH16", + "x": 0, + "y": 0, + "name": "Trail", + "enabled": false, + "width": 12, + "height": 12, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "HNFQ1", + "x": 1059, + "y": 3920, + "name": "06 · 镜像 + 运行容器表单", + "theme": { + "mode": "dark" + }, + "clip": true, + "width": 1440, + "height": 860, + "fill": "$bg-page", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "AeB1r", + "name": "Top Bar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "ZtdCE", + "name": "Brand", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "p5nSQ", + "name": "Logo", + "width": 17, + "height": 17, + "icon": "container", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "W7aBU", + "name": "Title", + "fill": "$text-primary", + "content": "Docker", + "fontFamily": "$font-ui", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "rectangle", + "id": "qhVtM", + "name": "Divider", + "fill": "$border", + "width": 1, + "height": 18 + }, + { + "type": "frame", + "id": "D4naV", + "name": "Host Switcher", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "l6Ex3e", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "server", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "iWlI1", + "name": "Host", + "fill": "$text-primary", + "content": "prod-sg-01", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "V5dP9h", + "name": "Transport", + "fill": "$text-muted", + "content": "/var/run/docker.sock · SSH 隧道", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "XxdA9", + "name": "Caret", + "width": 12, + "height": 12, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + }, + { + "type": "frame", + "id": "BdCeQ", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "VPQou", + "name": "Search", + "width": 300, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "phP01", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "search", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "gCbtI", + "name": "Placeholder", + "fill": "$text-muted", + "content": "搜索容器、镜像、卷、网络…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "KIoG9", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "MY54p", + "name": "Kbd", + "height": 16, + "fill": "$bg-hover", + "cornerRadius": 3, + "padding": [ + 0, + 5 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Ix3AM", + "name": "Keys", + "fill": "$text-tertiary", + "content": "Ctrl K", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "id": "x475P", + "type": "ref", + "ref": "x0Pcos", + "name": "Live Badge", + "descendants": { + "ODVko": { + "content": "实时" + } + } + }, + { + "id": "dxmqF", + "type": "ref", + "ref": "yoK0T", + "name": "Settings", + "descendants": { + "XMGqY": { + "icon": "settings" + } + } + } + ] + }, + { + "type": "frame", + "id": "lAYft", + "name": "Body", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "Du6Gy", + "name": "Rail", + "width": 56, + "height": "fill_container", + "fill": "$bg-rail", + "stroke": "$border", + "strokeWidth": { + "right": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 2, + "padding": [ + 6, + 0 + ], + "alignItems": "center", + "children": [ + { + "id": "YjiZ5", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 总览", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "layout-dashboard", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "总览", + "fill": "$text-tertiary" + } + } + }, + { + "id": "r8iwE", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 容器", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "box", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "容器", + "fill": "$text-tertiary" + } + } + }, + { + "id": "ByQh9", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 镜像", + "descendants": { + "cQhxS": { + "fill": "$accent-dim" + }, + "F1F9E": { + "icon": "layers", + "fill": "$accent" + }, + "DE5G9": { + "content": "镜像", + "fill": "$accent" + } + } + }, + { + "id": "tNDz6", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 卷", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "hard-drive", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "卷", + "fill": "$text-tertiary" + } + } + }, + { + "id": "KXpga", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 网络", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "network", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "网络", + "fill": "$text-tertiary" + } + } + }, + { + "id": "HS0eu", + "type": "ref", + "ref": "nKcgr", + "name": "Nav Compose", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "boxes", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "Compose", + "fill": "$text-tertiary" + } + } + }, + { + "id": "bwnQT", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 系统", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "gauge", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "系统", + "fill": "$text-tertiary" + } + } + }, + { + "type": "frame", + "id": "SLac3", + "name": "Spacer", + "width": 1, + "height": "fill_container", + "fill": "#00000000" + }, + { + "id": "vVm9t", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 设置", + "descendants": { + "F1F9E": { + "icon": "settings" + }, + "DE5G9": { + "content": "设置" + } + } + } + ] + }, + { + "type": "frame", + "id": "kafsF", + "name": "Main", + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "blLVO", + "name": "Toolbar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 9, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "pttGX", + "name": "Title", + "fill": "$text-primary", + "content": "镜像", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "text", + "id": "IsCdc", + "name": "Count", + "fill": "$text-muted", + "content": "34", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "LAtQe", + "name": "Filter Segmented", + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 2, + "padding": 2, + "children": [ + { + "id": "VHloL", + "type": "ref", + "ref": "MdmRT", + "name": "F 全部 18", + "height": 20, + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "全部 18", + "fontSize": 11, + "fill": "$text-primary" + } + } + }, + { + "id": "HrtNI", + "type": "ref", + "ref": "MdmRT", + "name": "F 运行中 12", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "运行中 12", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + }, + { + "id": "Bf34t", + "type": "ref", + "ref": "MdmRT", + "name": "F 已停止 5", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "已停止 5", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + }, + { + "id": "wxvgT", + "type": "ref", + "ref": "MdmRT", + "name": "F 异常 1", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "异常 1", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + } + ] + }, + { + "type": "frame", + "id": "ZgPfS", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "AlGXv", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 拉取镜像", + "descendants": { + "RQ8pM": { + "icon": "arrow-down-to-line" + }, + "P0aUw": { + "content": "拉取镜像" + } + } + }, + { + "id": "v4pkQ5", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 新建 Compose", + "descendants": { + "RQ8pM": { + "icon": "file-code" + }, + "P0aUw": { + "content": "新建 Compose" + } + } + }, + { + "id": "jHUOE", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 运行容器", + "fill": "$accent", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "plus", + "fill": "$accent-on" + }, + "P0aUw": { + "content": "运行容器…", + "fill": "$accent-on", + "fontWeight": "500" + } + } + }, + { + "id": "QTvDs", + "type": "ref", + "ref": "yoK0T", + "name": "Refresh", + "descendants": { + "XMGqY": { + "icon": "refresh-cw" + } + } + } + ] + }, + { + "type": "frame", + "id": "N9FBKX", + "name": "Header Row", + "width": "fill_container", + "height": 26, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "LtQuW", + "name": "Pad", + "width": 26, + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "SrJST", + "name": "H 仓库 / 标签", + "clip": true, + "width": "fill_container", + "height": 26, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "kBXu1", + "name": "L", + "fill": "$text-muted", + "content": "仓库 / 标签", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "M3LfR", + "name": "H 镜像 ID", + "clip": true, + "width": 104, + "height": 26, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "o1B9y5", + "name": "L", + "fill": "$text-muted", + "content": "镜像 ID", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "QwqBO", + "name": "H 大小", + "clip": true, + "width": 84, + "height": 26, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "bfUao", + "name": "L", + "fill": "$text-muted", + "content": "大小", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "k8MlS", + "name": "H 创建", + "clip": true, + "width": 118, + "height": 26, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "hwEov", + "name": "L", + "fill": "$text-muted", + "content": "创建", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "AZc8x", + "name": "H 使用中", + "clip": true, + "width": 96, + "height": 26, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "aClp2", + "name": "L", + "fill": "$text-muted", + "content": "使用中", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "HKDs8", + "name": "H 操作", + "clip": true, + "width": 120, + "height": 26, + "padding": [ + 0, + 4 + ], + "alignItems": "center" + } + ] + }, + { + "type": "frame", + "id": "JnA65", + "name": "List", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "id": "YVLAV", + "type": "ref", + "ref": "YgkX1", + "name": "IR nginx:1.27-alpine", + "width": "fill_container", + "fill": "$accent-dim", + "descendants": { + "tKZcB": { + "fill": "$accent", + "stroke": "$accent" + }, + "WqYI3": { + "icon": "layers", + "fill": "$accent" + }, + "u8Bzdb": { + "content": "nginx", + "fill": "$text-primary" + }, + "R3W97": { + "fill": "$accent-dim" + }, + "R3W97/ODVko": { + "content": "1.27-alpine", + "fill": "$accent" + }, + "e5FXq": { + "content": "9c4f2e1b7a" + }, + "VObJ2": { + "content": "48.2 MB" + }, + "tISft": { + "content": "3 周前" + }, + "YvpTs": { + "content": "2 个容器", + "fill": "$ok" + } + } + }, + { + "id": "f1agxv", + "type": "ref", + "ref": "YgkX1", + "name": "IR ghcr.io/acme/api:2.8.1", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "WqYI3": { + "icon": "layers", + "fill": "$accent" + }, + "u8Bzdb": { + "content": "ghcr.io/acme/api", + "fill": "$text-primary" + }, + "R3W97": { + "fill": "$accent-dim" + }, + "R3W97/ODVko": { + "content": "2.8.1", + "fill": "$accent" + }, + "e5FXq": { + "content": "3f81a92c04" + }, + "VObJ2": { + "content": "214 MB" + }, + "tISft": { + "content": "2 天前" + }, + "YvpTs": { + "content": "1 个容器", + "fill": "$ok" + } + } + }, + { + "id": "rS9qg", + "type": "ref", + "ref": "YgkX1", + "name": "IR ghcr.io/acme/api:2.8.0", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "WqYI3": { + "icon": "layers", + "fill": "$accent" + }, + "u8Bzdb": { + "content": "ghcr.io/acme/api", + "fill": "$text-primary" + }, + "R3W97": { + "fill": "$accent-dim" + }, + "R3W97/ODVko": { + "content": "2.8.0", + "fill": "$accent" + }, + "e5FXq": { + "content": "81b2d4e770" + }, + "VObJ2": { + "content": "213 MB" + }, + "tISft": { + "content": "9 天前" + }, + "YvpTs": { + "content": "未使用", + "fill": "$text-muted" + } + } + }, + { + "id": "qWjIB", + "type": "ref", + "ref": "YgkX1", + "name": "IR ghcr.io/acme/worker:2.8.1", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "WqYI3": { + "icon": "layers", + "fill": "$accent" + }, + "u8Bzdb": { + "content": "ghcr.io/acme/worker", + "fill": "$text-primary" + }, + "R3W97": { + "fill": "$accent-dim" + }, + "R3W97/ODVko": { + "content": "2.8.1", + "fill": "$accent" + }, + "e5FXq": { + "content": "aa02f81cc9" + }, + "VObJ2": { + "content": "198 MB" + }, + "tISft": { + "content": "2 天前" + }, + "YvpTs": { + "content": "2 个容器", + "fill": "$ok" + } + } + }, + { + "id": "pHoow", + "type": "ref", + "ref": "YgkX1", + "name": "IR postgres:16.4", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "WqYI3": { + "icon": "layers", + "fill": "$accent" + }, + "u8Bzdb": { + "content": "postgres", + "fill": "$text-primary" + }, + "R3W97": { + "fill": "$accent-dim" + }, + "R3W97/ODVko": { + "content": "16.4", + "fill": "$accent" + }, + "e5FXq": { + "content": "5d0f11ab83" + }, + "VObJ2": { + "content": "438 MB" + }, + "tISft": { + "content": "5 周前" + }, + "YvpTs": { + "content": "1 个容器", + "fill": "$ok" + } + } + }, + { + "id": "Yx3yR", + "type": "ref", + "ref": "YgkX1", + "name": "IR redis:7.4-alpine", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "WqYI3": { + "icon": "layers", + "fill": "$accent" + }, + "u8Bzdb": { + "content": "redis", + "fill": "$text-primary" + }, + "R3W97": { + "fill": "$accent-dim" + }, + "R3W97/ODVko": { + "content": "7.4-alpine", + "fill": "$accent" + }, + "e5FXq": { + "content": "c81f9a2b30" + }, + "VObJ2": { + "content": "41.8 MB" + }, + "tISft": { + "content": "刚刚" + }, + "YvpTs": { + "content": "1 个容器", + "fill": "$ok" + } + } + }, + { + "id": "gysKo", + "type": "ref", + "ref": "YgkX1", + "name": "IR grafana/grafana:11.2.0", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "WqYI3": { + "icon": "layers", + "fill": "$accent" + }, + "u8Bzdb": { + "content": "grafana/grafana", + "fill": "$text-primary" + }, + "R3W97": { + "fill": "$accent-dim" + }, + "R3W97/ODVko": { + "content": "11.2.0", + "fill": "$accent" + }, + "e5FXq": { + "content": "7b3e0d5f21" + }, + "VObJ2": { + "content": "622 MB" + }, + "tISft": { + "content": "1 个月前" + }, + "YvpTs": { + "content": "1 个容器", + "fill": "$ok" + } + } + }, + { + "id": "AqxHs", + "type": "ref", + "ref": "YgkX1", + "name": "IR grafana/loki:3.1.1", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "WqYI3": { + "icon": "layers", + "fill": "$accent" + }, + "u8Bzdb": { + "content": "grafana/loki", + "fill": "$text-primary" + }, + "R3W97": { + "fill": "$accent-dim" + }, + "R3W97/ODVko": { + "content": "3.1.1", + "fill": "$accent" + }, + "e5FXq": { + "content": "2ac9e81b44" + }, + "VObJ2": { + "content": "112 MB" + }, + "tISft": { + "content": "1 个月前" + }, + "YvpTs": { + "content": "1 个容器", + "fill": "$ok" + } + } + }, + { + "id": "C56dkJ", + "type": "ref", + "ref": "YgkX1", + "name": "IR minio/minio:RELEASE.2024-09", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "WqYI3": { + "icon": "layers", + "fill": "$accent" + }, + "u8Bzdb": { + "content": "minio/minio", + "fill": "$text-primary" + }, + "R3W97": { + "fill": "$accent-dim" + }, + "R3W97/ODVko": { + "content": "RELEASE.2024-09", + "fill": "$accent" + }, + "e5FXq": { + "content": "f0a71c93de" + }, + "VObJ2": { + "content": "184 MB" + }, + "tISft": { + "content": "6 周前" + }, + "YvpTs": { + "content": "1 个容器", + "fill": "$ok" + } + } + }, + { + "id": "QMazU", + "type": "ref", + "ref": "YgkX1", + "name": "IR registry:2.8.3", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "WqYI3": { + "icon": "layers", + "fill": "$accent" + }, + "u8Bzdb": { + "content": "registry", + "fill": "$text-primary" + }, + "R3W97": { + "fill": "$accent-dim" + }, + "R3W97/ODVko": { + "content": "2.8.3", + "fill": "$accent" + }, + "e5FXq": { + "content": "d4e8b2f105" + }, + "VObJ2": { + "content": "24.1 MB" + }, + "tISft": { + "content": "3 个月前" + }, + "YvpTs": { + "content": "1 个容器", + "fill": "$ok" + } + } + }, + { + "id": "A6PF7", + "type": "ref", + "ref": "YgkX1", + "name": "IR certbot/certbot:v2.11.0", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "WqYI3": { + "icon": "layers", + "fill": "$accent" + }, + "u8Bzdb": { + "content": "certbot/certbot", + "fill": "$text-primary" + }, + "R3W97": { + "fill": "$accent-dim" + }, + "R3W97/ODVko": { + "content": "v2.11.0", + "fill": "$accent" + }, + "e5FXq": { + "content": "91c0ab73ff" + }, + "VObJ2": { + "content": "132 MB" + }, + "tISft": { + "content": "2 个月前" + }, + "YvpTs": { + "content": "未使用", + "fill": "$text-muted" + } + } + }, + { + "id": "DkoHl", + "type": "ref", + "ref": "YgkX1", + "name": "IR containrrr/watchtower:1.7.1", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "WqYI3": { + "icon": "layers", + "fill": "$accent" + }, + "u8Bzdb": { + "content": "containrrr/watchtower", + "fill": "$text-primary" + }, + "R3W97": { + "fill": "$accent-dim" + }, + "R3W97/ODVko": { + "content": "1.7.1", + "fill": "$accent" + }, + "e5FXq": { + "content": "6ef3a0b829" + }, + "VObJ2": { + "content": "14.6 MB" + }, + "tISft": { + "content": "4 个月前" + }, + "YvpTs": { + "content": "1 个容器", + "fill": "$ok" + } + } + }, + { + "id": "ykxd7", + "type": "ref", + "ref": "YgkX1", + "name": "IR :", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "WqYI3": { + "icon": "circle-dashed", + "fill": "$warn" + }, + "u8Bzdb": { + "content": "", + "fill": "$text-tertiary" + }, + "R3W97": { + "fill": "#00000000" + }, + "R3W97/ODVko": { + "content": "", + "fill": "$text-muted" + }, + "e5FXq": { + "content": "8b21fc90aa" + }, + "VObJ2": { + "content": "198 MB" + }, + "tISft": { + "content": "9 天前" + }, + "YvpTs": { + "content": "悬空", + "fill": "$warn" + } + } + }, + { + "id": "xnDaC", + "type": "ref", + "ref": "YgkX1", + "name": "IR :", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "WqYI3": { + "icon": "circle-dashed", + "fill": "$warn" + }, + "u8Bzdb": { + "content": "", + "fill": "$text-tertiary" + }, + "R3W97": { + "fill": "#00000000" + }, + "R3W97/ODVko": { + "content": "", + "fill": "$text-muted" + }, + "e5FXq": { + "content": "44de91b307" + }, + "VObJ2": { + "content": "213 MB" + }, + "tISft": { + "content": "3 周前" + }, + "YvpTs": { + "content": "悬空", + "fill": "$warn" + } + } + }, + { + "id": "IvBNO", + "type": "ref", + "ref": "YgkX1", + "name": "IR node:22-slim", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "WqYI3": { + "icon": "layers", + "fill": "$accent" + }, + "u8Bzdb": { + "content": "node", + "fill": "$text-primary" + }, + "R3W97": { + "fill": "$accent-dim" + }, + "R3W97/ODVko": { + "content": "22-slim", + "fill": "$accent" + }, + "e5FXq": { + "content": "0af31b8ce4" + }, + "VObJ2": { + "content": "238 MB" + }, + "tISft": { + "content": "2 个月前" + }, + "YvpTs": { + "content": "未使用", + "fill": "$text-muted" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "i183U", + "name": "Run Drawer", + "width": 480, + "height": "fill_container", + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "left": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "k86efA", + "name": "Head", + "width": "fill_container", + "height": 44, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "vGU3G", + "name": "I", + "width": 15, + "height": 15, + "icon": "play", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "RXvGF", + "name": "T", + "fill": "$text-primary", + "content": "运行容器", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "U6TbIP", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "I5tOH", + "type": "ref", + "ref": "yoK0T", + "name": "X", + "descendants": { + "XMGqY": { + "icon": "x" + } + } + } + ] + }, + { + "type": "frame", + "id": "bC6vH", + "name": "Sub", + "width": "fill_container", + "height": 30, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 7, + "padding": [ + 0, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "CTBiQ", + "name": "I", + "width": 12, + "height": 12, + "icon": "layers", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "htQYL", + "name": "T", + "fill": "$text-secondary", + "content": "nginx:1.27-alpine", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "fjNgp", + "name": "T2", + "fill": "$text-muted", + "content": "· linux/amd64 · 48.2 MB", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "seQT1", + "name": "Form", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 11, + "padding": [ + 12, + 16 + ], + "children": [ + { + "id": "UkdmM", + "type": "ref", + "ref": "ARwD6", + "name": "F 容器名称", + "width": "fill_container", + "descendants": { + "T0xmy": { + "content": "容器名称" + }, + "G4vkue": { + "content": "留空由 Docker 生成", + "enabled": true + }, + "oLZwi": { + "content": "nginx-proxy-2", + "fill": "$text-primary" + }, + "jdH16": { + "enabled": false, + "icon": "chevron-down" + } + } + }, + { + "type": "frame", + "id": "TGpo5", + "name": "G 端口映射", + "width": "fill_container", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "frame", + "id": "nEdqd", + "name": "H", + "width": "fill_container", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "m386P", + "name": "L", + "fill": "$text-secondary", + "content": "端口映射", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "elE3y", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Dn28e", + "name": "A", + "fill": "$accent", + "content": "+ 添加", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "tJLvr", + "name": "P", + "width": "fill_container", + "height": 26, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "ks6RZ", + "name": "In", + "clip": true, + "width": 116, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "I49GI", + "name": "V", + "fill": "$text-primary", + "content": "8081", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "icon", + "id": "bDfT8", + "name": "Ar", + "width": 12, + "height": 12, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "frame", + "id": "rhex1", + "name": "In", + "clip": true, + "width": 116, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "wKUjg", + "name": "V", + "fill": "$text-primary", + "content": "80", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ZA1Ep", + "name": "In", + "clip": true, + "width": 64, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "uc2Mm", + "name": "V", + "fill": "$text-muted", + "content": "tcp", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "IBHyt", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "nwjWU", + "type": "ref", + "ref": "yoK0T", + "name": "Rm", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "x", + "width": 12, + "height": 12 + } + } + } + ] + }, + { + "type": "frame", + "id": "a14WXw", + "name": "P", + "width": "fill_container", + "height": 26, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "YPW5c", + "name": "In", + "clip": true, + "width": 116, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "L8X1bi", + "name": "V", + "fill": "$text-primary", + "content": "8444", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "icon", + "id": "VXEVA", + "name": "Ar", + "width": 12, + "height": 12, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "frame", + "id": "jcmyJ", + "name": "In", + "clip": true, + "width": 116, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "O0qd7O", + "name": "V", + "fill": "$text-primary", + "content": "443", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "M1MIP", + "name": "In", + "clip": true, + "width": 64, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "W5yjyf", + "name": "V", + "fill": "$text-muted", + "content": "tcp", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "nKeXm", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "tmVr5", + "type": "ref", + "ref": "yoK0T", + "name": "Rm", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "x", + "width": 12, + "height": 12 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "I5fkL", + "name": "G 卷挂载", + "width": "fill_container", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "frame", + "id": "b2mQN", + "name": "H", + "width": "fill_container", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "y5GQIW", + "name": "L", + "fill": "$text-secondary", + "content": "卷挂载", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "apDlm", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "fzb7t", + "name": "A", + "fill": "$accent", + "content": "+ 添加", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "c7sKgt", + "name": "V", + "width": "fill_container", + "height": 26, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "NpUUy", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "d0P7E", + "name": "V", + "fill": "$text-primary", + "content": "/srv/nginx/conf.d", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "icon", + "id": "xAwm3", + "name": "Ar", + "width": 12, + "height": 12, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "frame", + "id": "Vd50m", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "w55gUp", + "name": "V", + "fill": "$text-primary", + "content": "/etc/nginx/conf.d", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "gC3xW", + "name": "In", + "clip": true, + "width": 50, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Z3bhF", + "name": "V", + "fill": "$text-muted", + "content": "ro", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "id": "s2dZU", + "type": "ref", + "ref": "yoK0T", + "name": "Rm", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "x", + "width": 12, + "height": 12 + } + } + } + ] + }, + { + "type": "frame", + "id": "I8LW5", + "name": "V", + "width": "fill_container", + "height": 26, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "eRUhw", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "xLqxn", + "name": "V", + "fill": "$text-primary", + "content": "nginx-cache", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "icon", + "id": "zT1S1", + "name": "Ar", + "width": 12, + "height": 12, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "frame", + "id": "Zs3OB", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "oVZJ9", + "name": "V", + "fill": "$text-primary", + "content": "/var/cache/nginx", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "CvC23", + "name": "In", + "clip": true, + "width": 50, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "LQO09", + "name": "V", + "fill": "$text-muted", + "content": "rw", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "id": "J3JGW", + "type": "ref", + "ref": "yoK0T", + "name": "Rm", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "x", + "width": 12, + "height": 12 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "qqiJn", + "name": "G 环境变量", + "width": "fill_container", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "frame", + "id": "j96CFJ", + "name": "H", + "width": "fill_container", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "arZLb", + "name": "L", + "fill": "$text-secondary", + "content": "环境变量", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "R6Cqn3", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "rpEEC", + "name": "A", + "fill": "$accent", + "content": "+ 添加 / 从 .env 导入", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "N8D6sq", + "name": "E", + "width": "fill_container", + "height": 26, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "MG5pG", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Y3tB1", + "name": "V", + "fill": "$text-primary", + "content": "NGINX_ENTRYPOINT_QUIET_LOGS", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "Mcl0b", + "name": "Eq", + "fill": "$text-muted", + "content": "=", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Yj89R", + "name": "In", + "clip": true, + "width": 110, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "L7YPPD", + "name": "V", + "fill": "$text-primary", + "content": "1", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "id": "OLyXV", + "type": "ref", + "ref": "yoK0T", + "name": "Rm", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "x", + "width": 12, + "height": 12 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "r4sjEx", + "name": "Two", + "width": "fill_container", + "gap": 10, + "children": [ + { + "id": "jno0q", + "type": "ref", + "ref": "ARwD6", + "name": "F 网络", + "width": "fill_container", + "descendants": { + "T0xmy": { + "content": "网络" + }, + "G4vkue": { + "enabled": false + }, + "oLZwi": { + "content": "web-stack_default" + }, + "jdH16": { + "enabled": true + } + } + }, + { + "id": "OoGkw", + "type": "ref", + "ref": "ARwD6", + "name": "F 重启策略", + "width": "fill_container", + "descendants": { + "T0xmy": { + "content": "重启策略" + }, + "G4vkue": { + "enabled": false + }, + "oLZwi": { + "content": "unless-stopped" + }, + "jdH16": { + "enabled": true + } + } + } + ] + }, + { + "type": "frame", + "id": "HnBkN", + "name": "Toggles", + "width": "fill_container", + "layout": "vertical", + "gap": 6, + "children": [ + { + "type": "text", + "id": "ernYa", + "name": "L", + "fill": "$text-secondary", + "content": "运行选项", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "toVzN", + "name": "TR", + "width": "fill_container", + "gap": 10, + "children": [ + { + "type": "frame", + "id": "hCytA", + "name": "T", + "width": "fill_container", + "height": 24, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "fce29", + "name": "Sw", + "width": 26, + "height": 15, + "fill": "$accent", + "cornerRadius": 8, + "stroke": "#00000000", + "strokeWidth": 1, + "padding": [ + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "X62VG", + "name": "Knob", + "fill": "$accent-on", + "width": 11, + "height": 11 + } + ] + }, + { + "type": "text", + "id": "vKjc5", + "name": "L", + "fill": "$text-primary", + "content": "后台运行 -d", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "lUCXW", + "name": "T", + "width": "fill_container", + "height": 24, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Ar6v7", + "name": "Sw", + "width": 26, + "height": 15, + "fill": "$bg-active", + "cornerRadius": 8, + "stroke": "$border-strong", + "strokeWidth": 1, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "pQxKr", + "name": "Knob", + "fill": "$text-muted", + "width": 11, + "height": 11 + } + ] + }, + { + "type": "text", + "id": "op6AG", + "name": "L", + "fill": "$text-secondary", + "content": "退出即删除 --rm", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "DoIXo", + "name": "TR", + "width": "fill_container", + "gap": 10, + "children": [ + { + "type": "frame", + "id": "pS6u1", + "name": "T", + "width": "fill_container", + "height": 24, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Gx7tI", + "name": "Sw", + "width": 26, + "height": 15, + "fill": "$bg-active", + "cornerRadius": 8, + "stroke": "$border-strong", + "strokeWidth": 1, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "vdSzG", + "name": "Knob", + "fill": "$text-muted", + "width": 11, + "height": 11 + } + ] + }, + { + "type": "text", + "id": "vIepq", + "name": "L", + "fill": "$text-secondary", + "content": "分配 TTY -t", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "oOeVu", + "name": "T", + "width": "fill_container", + "height": 24, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "PsNxZ", + "name": "Sw", + "width": 26, + "height": 15, + "fill": "$bg-active", + "cornerRadius": 8, + "stroke": "$border-strong", + "strokeWidth": 1, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "DcAkq", + "name": "Knob", + "fill": "$text-muted", + "width": 11, + "height": 11 + } + ] + }, + { + "type": "text", + "id": "hXX16", + "name": "L", + "fill": "$danger", + "content": "特权模式 --privileged", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "id": "OarB2", + "type": "ref", + "ref": "ARwD6", + "name": "F 额外参数", + "width": "fill_container", + "descendants": { + "T0xmy": { + "content": "额外参数" + }, + "G4vkue": { + "content": "与在终端里手敲同权,不做引用", + "enabled": true + }, + "oLZwi": { + "content": "例:--cap-add NET_ADMIN --dns 1.1.1.1", + "fill": "$text-muted" + }, + "jdH16": { + "enabled": false, + "icon": "chevron-down" + } + } + } + ] + }, + { + "type": "frame", + "id": "msoFy", + "name": "Foot", + "width": "fill_container", + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 8, + "padding": [ + 10, + 16 + ], + "children": [ + { + "type": "frame", + "id": "PeXXi", + "name": "H", + "width": "fill_container", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "cJX3j", + "name": "L", + "fill": "$text-tertiary", + "content": "等效命令", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "bNXUB", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "T97d6", + "name": "C", + "width": 11, + "height": 11, + "icon": "copy", + "library": "lucide", + "fill": "$text-muted" + } + ] + }, + { + "type": "frame", + "id": "XiCUd", + "name": "Preview", + "width": "fill_container", + "fill": "$bg-terminal", + "cornerRadius": "$r-sm", + "layout": "vertical", + "padding": [ + 8, + 10 + ], + "children": [ + { + "type": "text", + "id": "F3mVul", + "name": "Cmd", + "fill": "$text-terminal", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "docker run -d --name nginx-proxy-2 -p 8081:80 -p 8444:443 \\\n -v /srv/nginx/conf.d:/etc/nginx/conf.d:ro -v nginx-cache:/var/cache/nginx \\\n -e NGINX_ENTRYPOINT_QUIET_LOGS=1 --network web-stack_default \\\n --restart unless-stopped nginx:1.27-alpine", + "lineHeight": 1.6, + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "N1RRbB", + "name": "Note", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "面板实际调用 POST /containers/create 与 /start;等效命令仅供核对与复制。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "m86MJ9", + "name": "Buttons", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "kGhvz", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "Sfers", + "type": "ref", + "ref": "i1OTz", + "name": "Cancel", + "height": 28, + "descendants": { + "RQ8pM": { + "enabled": false + }, + "P0aUw": { + "content": "取消" + } + } + }, + { + "id": "LUnau", + "type": "ref", + "ref": "i1OTz", + "name": "Run", + "height": 28, + "fill": "$accent", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "play", + "fill": "$accent-on" + }, + "P0aUw": { + "content": "运行容器", + "fill": "$accent-on", + "fontWeight": "500" + } + } + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "z1nKam", + "name": "Status Bar", + "width": "fill_container", + "height": 24, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 14, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "uYv7R", + "name": "Msg", + "fill": "$text-tertiary", + "content": "POST /images/create?fromImage=redis&tag=7.4-alpine · 已完成 · 复用 6 层", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "kTcys", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "B1PbGY", + "name": "API v1.47", + "fill": "$text-muted", + "content": "API v1.47", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "SfNZF", + "name": "Engine 27.3.1", + "fill": "$text-muted", + "content": "Engine 27.3.1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "LphOp", + "name": "18 容器 · 34 镜像 · 9 卷", + "fill": "$text-muted", + "content": "18 容器 · 34 镜像 · 9 卷", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "YAmQv", + "name": "事件流 已连接", + "fill": "$text-muted", + "content": "事件流 已连接", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "m44Xw6", + "x": 1059, + "y": 4900, + "name": "07 · 卷(含卷内文件浏览)", + "theme": { + "mode": "dark" + }, + "clip": true, + "width": 1440, + "height": 860, + "fill": "$bg-page", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "RXdcN", + "name": "Top Bar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Lc86j", + "name": "Brand", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "ZMDm7", + "name": "Logo", + "width": 17, + "height": 17, + "icon": "container", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "ENAfx", + "name": "Title", + "fill": "$text-primary", + "content": "Docker", + "fontFamily": "$font-ui", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "rectangle", + "id": "agRAe", + "name": "Divider", + "fill": "$border", + "width": 1, + "height": 18 + }, + { + "type": "frame", + "id": "Kg36v", + "name": "Host Switcher", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "VdDsU", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "server", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "kLk3f", + "name": "Host", + "fill": "$text-primary", + "content": "prod-sg-01", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "Ps7bH", + "name": "Transport", + "fill": "$text-muted", + "content": "/var/run/docker.sock · SSH 隧道", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "XlgHz", + "name": "Caret", + "width": 12, + "height": 12, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + }, + { + "type": "frame", + "id": "SY2Yg", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "a6jT5", + "name": "Search", + "width": 300, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "J7cWqQ", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "search", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "h0sJcq", + "name": "Placeholder", + "fill": "$text-muted", + "content": "搜索容器、镜像、卷、网络…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Z7Nra", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "idKSr", + "name": "Kbd", + "height": 16, + "fill": "$bg-hover", + "cornerRadius": 3, + "padding": [ + 0, + 5 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "X5haHD", + "name": "Keys", + "fill": "$text-tertiary", + "content": "Ctrl K", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "id": "HJgAH", + "type": "ref", + "ref": "x0Pcos", + "name": "Live Badge", + "descendants": { + "ODVko": { + "content": "实时" + } + } + }, + { + "id": "WoIyD", + "type": "ref", + "ref": "yoK0T", + "name": "Settings", + "descendants": { + "XMGqY": { + "icon": "settings" + } + } + } + ] + }, + { + "type": "frame", + "id": "CkmLF", + "name": "Body", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "Ekxhl", + "name": "Rail", + "width": 56, + "height": "fill_container", + "fill": "$bg-rail", + "stroke": "$border", + "strokeWidth": { + "right": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 2, + "padding": [ + 6, + 0 + ], + "alignItems": "center", + "children": [ + { + "id": "m1LJiU", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 总览", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "layout-dashboard", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "总览", + "fill": "$text-tertiary" + } + } + }, + { + "id": "x2mUGu", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 容器", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "box", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "容器", + "fill": "$text-tertiary" + } + } + }, + { + "id": "u7ZvJ", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 镜像", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "layers", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "镜像", + "fill": "$text-tertiary" + } + } + }, + { + "id": "QdLE8", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 卷", + "descendants": { + "cQhxS": { + "fill": "$accent-dim" + }, + "F1F9E": { + "icon": "hard-drive", + "fill": "$accent" + }, + "DE5G9": { + "content": "卷", + "fill": "$accent" + } + } + }, + { + "id": "C7cXFG", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 网络", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "network", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "网络", + "fill": "$text-tertiary" + } + } + }, + { + "id": "NLrtA", + "type": "ref", + "ref": "nKcgr", + "name": "Nav Compose", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "boxes", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "Compose", + "fill": "$text-tertiary" + } + } + }, + { + "id": "EP1as", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 系统", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "gauge", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "系统", + "fill": "$text-tertiary" + } + } + }, + { + "type": "frame", + "id": "PnkTJ", + "name": "Spacer", + "width": 1, + "height": "fill_container", + "fill": "#00000000" + }, + { + "id": "amYbt", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 设置", + "descendants": { + "F1F9E": { + "icon": "settings" + }, + "DE5G9": { + "content": "设置" + } + } + } + ] + }, + { + "type": "frame", + "id": "Ujywz", + "name": "Main", + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "Z5YZY", + "name": "Toolbar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 9, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Pq4ds", + "name": "Title", + "fill": "$text-primary", + "content": "卷", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "text", + "id": "V4aboj", + "name": "Count", + "fill": "$text-muted", + "content": "9", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "BnBtP", + "name": "Filter Segmented", + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 2, + "padding": 2, + "children": [ + { + "id": "TPcJa", + "type": "ref", + "ref": "MdmRT", + "name": "F 全部 18", + "height": 20, + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "全部 18", + "fontSize": 11, + "fill": "$text-primary" + } + } + }, + { + "id": "xUwgn", + "type": "ref", + "ref": "MdmRT", + "name": "F 运行中 12", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "运行中 12", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + }, + { + "id": "wemnM", + "type": "ref", + "ref": "MdmRT", + "name": "F 已停止 5", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "已停止 5", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + }, + { + "id": "XEWyP", + "type": "ref", + "ref": "MdmRT", + "name": "F 异常 1", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "异常 1", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + } + ] + }, + { + "type": "frame", + "id": "dEeY6", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "vqnSB", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 拉取镜像", + "descendants": { + "RQ8pM": { + "icon": "arrow-down-to-line" + }, + "P0aUw": { + "content": "拉取镜像" + } + } + }, + { + "id": "YrNtX", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 新建 Compose", + "descendants": { + "RQ8pM": { + "icon": "file-code" + }, + "P0aUw": { + "content": "新建 Compose" + } + } + }, + { + "id": "FLG6l", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 运行容器", + "fill": "$accent", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "plus", + "fill": "$accent-on" + }, + "P0aUw": { + "content": "运行容器…", + "fill": "$accent-on", + "fontWeight": "500" + } + } + }, + { + "id": "t44pp", + "type": "ref", + "ref": "yoK0T", + "name": "Refresh", + "descendants": { + "XMGqY": { + "icon": "refresh-cw" + } + } + } + ] + }, + { + "type": "frame", + "id": "RnuYA", + "name": "Header Row", + "width": "fill_container", + "height": 26, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "vn9Pg", + "name": "Pad", + "width": 26, + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "RnrvA", + "name": "H 名称", + "clip": true, + "width": "fill_container", + "height": 26, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Q8XgA", + "name": "L", + "fill": "$text-muted", + "content": "名称", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "moYnv", + "name": "H 驱动", + "clip": true, + "width": 90, + "height": 26, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "icsQV", + "name": "L", + "fill": "$text-muted", + "content": "驱动", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "DaOUu", + "name": "H 使用者", + "clip": true, + "width": 180, + "height": 26, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "B69p7I", + "name": "L", + "fill": "$text-muted", + "content": "使用者", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "V5l7d", + "name": "H 大小", + "clip": true, + "width": 96, + "height": 26, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "AhmHU", + "name": "L", + "fill": "$text-muted", + "content": "大小", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "vx88j", + "name": "H 创建", + "clip": true, + "width": 112, + "height": 26, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "NuhhB", + "name": "L", + "fill": "$text-muted", + "content": "创建", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "r2htA5", + "name": "H 操作", + "clip": true, + "width": 104, + "height": 26, + "padding": [ + 0, + 4 + ], + "alignItems": "center" + } + ] + }, + { + "type": "frame", + "id": "JEPlK", + "name": "List", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "zwijC", + "name": "VR nginx-cache", + "width": "fill_container", + "height": 32, + "fill": "$bg-active", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "FFatS", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "m09gJq", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "hnc5u", + "name": "I", + "width": 13, + "height": 13, + "icon": "database", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "yjRAx", + "name": "N", + "fill": "$text-primary", + "content": "nginx-cache", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "jesWR", + "name": "C", + "clip": true, + "width": 90, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "syo2E", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "O3cmJN", + "name": "C", + "clip": true, + "width": 180, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Mcy6m", + "name": "T", + "fill": "$ok", + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "O6Muac", + "name": "C", + "clip": true, + "width": 96, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "uL2xg", + "name": "T", + "fill": "$text-secondary", + "content": "318 MB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "V4M74i", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "thr0Z", + "name": "T", + "fill": "$text-tertiary", + "content": "3 周前", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "MZKjV", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "yHZLk", + "type": "ref", + "ref": "yoK0T", + "name": "A folder-open", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "folder-open", + "width": 13, + "height": 13 + } + } + }, + { + "id": "HsZu4", + "type": "ref", + "ref": "yoK0T", + "name": "A download", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "download", + "width": 13, + "height": 13 + } + } + }, + { + "id": "uyDUV", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "ZzKuK", + "name": "VR pg-data", + "width": "fill_container", + "height": 32, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "M1QCO3", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "XVEfX", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "j8Byi", + "name": "I", + "width": 13, + "height": 13, + "icon": "database", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "zmSqQ", + "name": "N", + "fill": "$text-primary", + "content": "pg-data", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "PBUCn", + "name": "C", + "clip": true, + "width": 90, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "k5oNMd", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "C7a14", + "name": "C", + "clip": true, + "width": 180, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "HpBVf", + "name": "T", + "fill": "$ok", + "content": "postgres-main", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "amMdG", + "name": "C", + "clip": true, + "width": 96, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "cQ2Gu", + "name": "T", + "fill": "$text-secondary", + "content": "18.4 GB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "zB2uY", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "BPVnu", + "name": "T", + "fill": "$text-tertiary", + "content": "3 个月前", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "xPUPJ", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "dac4K", + "type": "ref", + "ref": "yoK0T", + "name": "A folder-open", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "folder-open", + "width": 13, + "height": 13 + } + } + }, + { + "id": "fKNbo", + "type": "ref", + "ref": "yoK0T", + "name": "A download", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "download", + "width": 13, + "height": 13 + } + } + }, + { + "id": "t58d8I", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "VBe0P", + "name": "VR redis-data", + "width": "fill_container", + "height": 32, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "oQ4eY", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "pj7xi", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "KK6oA", + "name": "I", + "width": 13, + "height": 13, + "icon": "database", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "EHqsn", + "name": "N", + "fill": "$text-primary", + "content": "redis-data", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "JNXzq", + "name": "C", + "clip": true, + "width": 90, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "qY51R", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "a8uNd", + "name": "C", + "clip": true, + "width": 180, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "BfJ7s", + "name": "T", + "fill": "$ok", + "content": "redis-cache", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Z9udz8", + "name": "C", + "clip": true, + "width": 96, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "qj919", + "name": "T", + "fill": "$text-secondary", + "content": "642 MB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "f3FTF", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Dy1AF", + "name": "T", + "fill": "$text-tertiary", + "content": "3 个月前", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "wcQX9", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "w1hyx", + "type": "ref", + "ref": "yoK0T", + "name": "A folder-open", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "folder-open", + "width": 13, + "height": 13 + } + } + }, + { + "id": "nLs2A", + "type": "ref", + "ref": "yoK0T", + "name": "A download", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "download", + "width": 13, + "height": 13 + } + } + }, + { + "id": "FTgmb", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "Zlxgp", + "name": "VR grafana-storage", + "width": "fill_container", + "height": 32, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "UcC3l", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "rHQTs", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "Momkb", + "name": "I", + "width": 13, + "height": 13, + "icon": "database", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "H7rFti", + "name": "N", + "fill": "$text-primary", + "content": "grafana-storage", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "V3lE0l", + "name": "C", + "clip": true, + "width": 90, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dpxRR", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "k4Cyo", + "name": "C", + "clip": true, + "width": 180, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "y2Czm", + "name": "T", + "fill": "$ok", + "content": "grafana", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "M43F5", + "name": "C", + "clip": true, + "width": 96, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "IIEHz", + "name": "T", + "fill": "$text-secondary", + "content": "94 MB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ypVgm", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "EbwSM", + "name": "T", + "fill": "$text-tertiary", + "content": "1 个月前", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "cUMmd", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "BSTZd", + "type": "ref", + "ref": "yoK0T", + "name": "A folder-open", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "folder-open", + "width": 13, + "height": 13 + } + } + }, + { + "id": "wnVKm", + "type": "ref", + "ref": "yoK0T", + "name": "A download", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "download", + "width": 13, + "height": 13 + } + } + }, + { + "id": "qxnHh", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "jZJh0", + "name": "VR loki-data", + "width": "fill_container", + "height": 32, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "C435m", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "USD6D", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "oXp78", + "name": "I", + "width": 13, + "height": 13, + "icon": "database", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "s0a6M", + "name": "N", + "fill": "$text-primary", + "content": "loki-data", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "Lni8s", + "name": "C", + "clip": true, + "width": 90, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "r866AD", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "AuNT2", + "name": "C", + "clip": true, + "width": 180, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "lXG7W", + "name": "T", + "fill": "$ok", + "content": "loki", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "pizj9", + "name": "C", + "clip": true, + "width": 96, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "L4TQd", + "name": "T", + "fill": "$text-secondary", + "content": "4.1 GB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "u0ANA", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "W9Li7F", + "name": "T", + "fill": "$text-tertiary", + "content": "1 个月前", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "IYU7t", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "FVB6r", + "type": "ref", + "ref": "yoK0T", + "name": "A folder-open", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "folder-open", + "width": 13, + "height": 13 + } + } + }, + { + "id": "h0UQa1", + "type": "ref", + "ref": "yoK0T", + "name": "A download", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "download", + "width": 13, + "height": 13 + } + } + }, + { + "id": "M27Nv2", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "q1OeBh", + "name": "VR minio-data", + "width": "fill_container", + "height": 32, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "R5uwP8", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "tEsOF", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "E7nO1", + "name": "I", + "width": 13, + "height": 13, + "icon": "database", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "X1lEx", + "name": "N", + "fill": "$text-primary", + "content": "minio-data", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "yg7Ei", + "name": "C", + "clip": true, + "width": 90, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "u7ZaM", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "NB8lt", + "name": "C", + "clip": true, + "width": 180, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "E420xX", + "name": "T", + "fill": "$ok", + "content": "minio", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "mIUno", + "name": "C", + "clip": true, + "width": 96, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mzssm", + "name": "T", + "fill": "$text-secondary", + "content": "1.2 GB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "VOrdE", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dNKY9", + "name": "T", + "fill": "$text-tertiary", + "content": "6 周前", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "wTOtp", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "fWBA9", + "type": "ref", + "ref": "yoK0T", + "name": "A folder-open", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "folder-open", + "width": 13, + "height": 13 + } + } + }, + { + "id": "NJg1v", + "type": "ref", + "ref": "yoK0T", + "name": "A download", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "download", + "width": 13, + "height": 13 + } + } + }, + { + "id": "O5DGCa", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "WFx3k", + "name": "VR registry-data", + "width": "fill_container", + "height": 32, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "fg6y3", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "expf8", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "U0retl", + "name": "I", + "width": 13, + "height": 13, + "icon": "database", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "RMLwy", + "name": "N", + "fill": "$text-primary", + "content": "registry-data", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "b3lNv", + "name": "C", + "clip": true, + "width": 90, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "tU9CP", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "AXefe", + "name": "C", + "clip": true, + "width": 180, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "kW8yK", + "name": "T", + "fill": "$ok", + "content": "registry-mirror", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "zmMNf", + "name": "C", + "clip": true, + "width": 96, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "s1D64P", + "name": "T", + "fill": "$text-secondary", + "content": "218 MB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "jENYF", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ySxfw", + "name": "T", + "fill": "$text-tertiary", + "content": "3 个月前", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "KVjUO", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "Wj5L8", + "type": "ref", + "ref": "yoK0T", + "name": "A folder-open", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "folder-open", + "width": 13, + "height": 13 + } + } + }, + { + "id": "X6mYfq", + "type": "ref", + "ref": "yoK0T", + "name": "A download", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "download", + "width": 13, + "height": 13 + } + } + }, + { + "id": "g5JWnW", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "vyh4V", + "name": "VR acme-certs", + "width": "fill_container", + "height": 32, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "t2EY1", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "u31CmH", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "nB6cK", + "name": "I", + "width": 13, + "height": 13, + "icon": "database", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "text", + "id": "PtHEh", + "name": "N", + "fill": "$text-primary", + "content": "acme-certs", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "kNFOG", + "name": "C", + "clip": true, + "width": 90, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "J6UNp", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "G4CP7", + "name": "C", + "clip": true, + "width": 180, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "fVMQl", + "name": "T", + "fill": "$warn", + "content": "未使用", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "glm0a", + "name": "C", + "clip": true, + "width": 96, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "uoEUi", + "name": "T", + "fill": "$text-secondary", + "content": "1.8 MB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "hBjTv", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "rhOpT", + "name": "T", + "fill": "$text-tertiary", + "content": "2 个月前", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "u9pMM", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "fGJuT", + "type": "ref", + "ref": "yoK0T", + "name": "A folder-open", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "folder-open", + "width": 13, + "height": 13 + } + } + }, + { + "id": "wKPKB", + "type": "ref", + "ref": "yoK0T", + "name": "A download", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "download", + "width": 13, + "height": 13 + } + } + }, + { + "id": "CJjnO", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "FM1Ux", + "name": "VR tmp-restore-2408", + "width": "fill_container", + "height": 32, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Y2Il1", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "PulRI", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "JOrty", + "name": "I", + "width": 13, + "height": 13, + "icon": "database", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "text", + "id": "Rwalf", + "name": "N", + "fill": "$text-primary", + "content": "tmp-restore-2408", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "d343VZ", + "name": "C", + "clip": true, + "width": 90, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "R2f9b", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Q1rsq", + "name": "C", + "clip": true, + "width": 180, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "iVU9R", + "name": "T", + "fill": "$warn", + "content": "未使用", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "BqVnh", + "name": "C", + "clip": true, + "width": 96, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "R5OaG", + "name": "T", + "fill": "$text-secondary", + "content": "12 KB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "p6l0j", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "wTPWH", + "name": "T", + "fill": "$text-tertiary", + "content": "9 天前", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "M0ez6", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "EKPNX", + "type": "ref", + "ref": "yoK0T", + "name": "A folder-open", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "folder-open", + "width": 13, + "height": 13 + } + } + }, + { + "id": "Dva3N", + "type": "ref", + "ref": "yoK0T", + "name": "A download", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "download", + "width": 13, + "height": 13 + } + } + }, + { + "id": "u10Nx", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "E9U4yV", + "name": "Detail Drawer", + "width": 440, + "height": "fill_container", + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "left": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "hhhgE", + "name": "Head", + "width": "fill_container", + "height": 44, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "f5AAkH", + "name": "I", + "width": 15, + "height": 15, + "icon": "database", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "g4Nkb", + "name": "N", + "fill": "$text-primary", + "content": "nginx-cache", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "id": "soh8D", + "type": "ref", + "ref": "x0Pcos", + "name": "Chip", + "descendants": { + "ODVko": { + "content": "使用中" + } + } + }, + { + "type": "frame", + "id": "fsRSq", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "dUFb5", + "type": "ref", + "ref": "yoK0T", + "name": "H pin", + "descendants": { + "XMGqY": { + "icon": "pin" + } + } + }, + { + "id": "Z2E4hf", + "type": "ref", + "ref": "yoK0T", + "name": "H x", + "descendants": { + "XMGqY": { + "icon": "x" + } + } + } + ] + }, + { + "type": "frame", + "id": "S05Il", + "name": "Tabs", + "width": "fill_container", + "height": 34, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 2, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "id": "c0U6m", + "type": "ref", + "ref": "MdmRT", + "name": "T 概览", + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "icon": "info", + "fill": "$text-primary" + }, + "iscyV": { + "content": "概览", + "fill": "$text-primary", + "fontWeight": "500" + } + } + }, + { + "id": "WitFf", + "type": "ref", + "ref": "MdmRT", + "name": "T 卷内文件", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "folder-tree", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "卷内文件", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + }, + { + "id": "Z79bt", + "type": "ref", + "ref": "MdmRT", + "name": "T 备份", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "archive", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "备份", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + } + ] + }, + { + "type": "frame", + "id": "ZVB3s", + "name": "Actions", + "width": "fill_container", + "height": 42, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "id": "nJ7Qd", + "type": "ref", + "ref": "i1OTz", + "name": "A 浏览", + "fill": "$accent-dim", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "folder-open", + "fill": "$accent" + }, + "P0aUw": { + "content": "浏览卷内文件", + "fill": "$accent" + } + } + }, + { + "id": "q28q8", + "type": "ref", + "ref": "i1OTz", + "name": "A 备份", + "descendants": { + "RQ8pM": { + "icon": "download" + }, + "P0aUw": { + "content": "备份为 tar" + } + } + }, + { + "type": "frame", + "id": "xZWl7", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "OeB65", + "type": "ref", + "ref": "yoK0T", + "name": "More", + "descendants": { + "XMGqY": { + "icon": "ellipsis" + } + } + } + ] + }, + { + "type": "frame", + "id": "s2i9l4", + "name": "Content", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 10, + "padding": [ + 12, + 14 + ], + "children": [ + { + "type": "frame", + "id": "M5z5fN", + "name": "Sec 基本信息", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "jH0Gq", + "name": "H", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "i8zQeB", + "name": "T", + "fill": "$text-secondary", + "content": "基本信息", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "IMrvE", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "id": "p5MDl", + "type": "ref", + "ref": "l3qo4", + "name": "KV 驱动", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "驱动", + "width": 74 + }, + "psgr6": { + "content": "local", + "fill": "$text-primary", + "fontSize": 10 + } + } + }, + { + "id": "U78y2p", + "type": "ref", + "ref": "l3qo4", + "name": "KV 作用域", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "作用域", + "width": 74 + }, + "psgr6": { + "content": "local", + "fill": "$text-primary", + "fontSize": 10 + } + } + }, + { + "id": "rgIxA", + "type": "ref", + "ref": "l3qo4", + "name": "KV 挂载点", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "挂载点", + "width": 74 + }, + "psgr6": { + "content": "/var/lib/docker/volumes/nginx-cache/_data", + "fill": "$text-primary", + "fontSize": 10 + } + } + }, + { + "id": "mnu5V", + "type": "ref", + "ref": "l3qo4", + "name": "KV 大小", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "大小", + "width": 74 + }, + "psgr6": { + "content": "318 MB · 4,182 个文件", + "fill": "$text-primary", + "fontSize": 10 + } + } + }, + { + "id": "RyNnD", + "type": "ref", + "ref": "l3qo4", + "name": "KV 创建于", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "创建于", + "width": 74 + }, + "psgr6": { + "content": "2026-07-31 14:02:11", + "fill": "$text-primary", + "fontSize": 10 + } + } + } + ] + }, + { + "type": "rectangle", + "id": "J9adg6", + "name": "Div", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "ceVVG", + "name": "Sec 使用者", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "plRde", + "name": "H", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ST4lx", + "name": "T", + "fill": "$text-secondary", + "content": "使用者", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "XLEcc", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "W1nSyM", + "name": "A", + "fill": "$accent", + "content": "查看容器", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "QM03J", + "name": "U", + "clip": true, + "width": "fill_container", + "height": 24, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "Q8cFS", + "name": "D", + "fill": "$ok", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "Ghogs", + "name": "N", + "fill": "$text-primary", + "content": "nginx-proxy", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "FZnVx", + "name": "Ar", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "b2Jub", + "name": "M", + "fill": "$text-secondary", + "content": "/var/cache/nginx", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "cUmnK", + "type": "ref", + "ref": "x0Pcos", + "name": "Mode", + "height": 15, + "fill": "#00000000", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "rw", + "fontSize": 9, + "fill": "$ok" + } + } + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "PFfta", + "name": "Div", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "O0bcRD", + "name": "Sec 标签", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "bcc44", + "name": "H", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "FWUg6", + "name": "T", + "fill": "$text-secondary", + "content": "标签", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "xKcg0", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "id": "Ab9si", + "type": "ref", + "ref": "l3qo4", + "name": "KV project", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "project", + "width": 74 + }, + "psgr6": { + "content": "web-stack", + "fill": "$violet", + "fontSize": 10 + } + } + }, + { + "id": "o0Yz6y", + "type": "ref", + "ref": "l3qo4", + "name": "KV volume", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "volume", + "width": 74 + }, + "psgr6": { + "content": "nginx-cache", + "fill": "$violet", + "fontSize": 10 + } + } + } + ] + }, + { + "type": "rectangle", + "id": "BGWvt", + "name": "Div", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "lSg0N", + "name": "Sec 驱动选项", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "RWiUS", + "name": "H", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "SQyAv", + "name": "T", + "fill": "$text-secondary", + "content": "驱动选项", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "KhsWi", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "id": "w9aUqH", + "type": "ref", + "ref": "l3qo4", + "name": "KV (无)", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "(无)", + "width": 74 + }, + "psgr6": { + "content": "使用默认 local 驱动", + "fill": "$text-muted", + "fontSize": 10 + } + } + } + ] + }, + { + "type": "frame", + "id": "k6qLM", + "name": "S", + "width": "fill_container", + "height": "fill_container", + "fill": "#00000000" + }, + { + "type": "frame", + "id": "X6hBX", + "name": "Danger", + "width": "fill_container", + "fill": "$danger-dim", + "cornerRadius": "$r-md", + "stroke": "$danger", + "strokeWidth": 1, + "layout": "vertical", + "gap": 7, + "padding": [ + 10, + 12 + ], + "children": [ + { + "type": "frame", + "id": "okyOf", + "name": "H", + "width": "fill_container", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "uOsTS", + "name": "I", + "width": 13, + "height": 13, + "icon": "triangle-alert", + "library": "lucide", + "fill": "$danger" + }, + { + "type": "text", + "id": "Vi145", + "name": "T", + "fill": "$danger", + "content": "危险操作", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "YM1oj", + "name": "D", + "fill": "$danger", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "删除卷会永久丢失其中 318 MB 数据,且无法撤销。确认框中需手打 delete。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "TEhlE", + "type": "ref", + "ref": "i1OTz", + "name": "Del", + "height": 26, + "stroke": "$danger", + "descendants": { + "RQ8pM": { + "icon": "trash-2", + "fill": "$danger" + }, + "P0aUw": { + "content": "删除卷…", + "fill": "$danger", + "fontSize": 11 + } + } + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "mM7yH", + "name": "Status Bar", + "width": "fill_container", + "height": 24, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 14, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "g24tZM", + "name": "Msg", + "fill": "$text-tertiary", + "content": "GET /volumes · 9 个卷 · 24.8 GB · 2 个未被任何容器使用", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "u3T23l", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "sdwNZ", + "name": "API v1.47", + "fill": "$text-muted", + "content": "API v1.47", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "gpWAN", + "name": "Engine 27.3.1", + "fill": "$text-muted", + "content": "Engine 27.3.1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Bo2RU", + "name": "18 容器 · 34 镜像 · 9 卷", + "fill": "$text-muted", + "content": "18 容器 · 34 镜像 · 9 卷", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "GrXhg", + "name": "事件流 已连接", + "fill": "$text-muted", + "content": "事件流 已连接", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Y39WLa", + "x": 1059, + "y": 5880, + "name": "08 · 网络", + "theme": { + "mode": "dark" + }, + "clip": true, + "width": 1440, + "height": 860, + "fill": "$bg-page", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "cjmgf", + "name": "Top Bar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Jx7WS", + "name": "Brand", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "TaIJD", + "name": "Logo", + "width": 17, + "height": 17, + "icon": "container", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "ffaAu", + "name": "Title", + "fill": "$text-primary", + "content": "Docker", + "fontFamily": "$font-ui", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "rectangle", + "id": "GvpTu", + "name": "Divider", + "fill": "$border", + "width": 1, + "height": 18 + }, + { + "type": "frame", + "id": "b9qus1", + "name": "Host Switcher", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "RJC26", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "server", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "B7VYN", + "name": "Host", + "fill": "$text-primary", + "content": "prod-sg-01", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "PpIIx", + "name": "Transport", + "fill": "$text-muted", + "content": "/var/run/docker.sock · SSH 隧道", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "MZGgc", + "name": "Caret", + "width": 12, + "height": 12, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + }, + { + "type": "frame", + "id": "d3Etq", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "Onso4", + "name": "Search", + "width": 300, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "D557lL", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "search", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "p80RE", + "name": "Placeholder", + "fill": "$text-muted", + "content": "搜索容器、镜像、卷、网络…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "pKADk", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "nZXe0", + "name": "Kbd", + "height": 16, + "fill": "$bg-hover", + "cornerRadius": 3, + "padding": [ + 0, + 5 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dirCj", + "name": "Keys", + "fill": "$text-tertiary", + "content": "Ctrl K", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "id": "NYGk4", + "type": "ref", + "ref": "x0Pcos", + "name": "Live Badge", + "descendants": { + "ODVko": { + "content": "实时" + } + } + }, + { + "id": "Hhr4R", + "type": "ref", + "ref": "yoK0T", + "name": "Settings", + "descendants": { + "XMGqY": { + "icon": "settings" + } + } + } + ] + }, + { + "type": "frame", + "id": "HifzH", + "name": "Body", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "d6EzjT", + "name": "Rail", + "width": 56, + "height": "fill_container", + "fill": "$bg-rail", + "stroke": "$border", + "strokeWidth": { + "right": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 2, + "padding": [ + 6, + 0 + ], + "alignItems": "center", + "children": [ + { + "id": "yndmg", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 总览", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "layout-dashboard", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "总览", + "fill": "$text-tertiary" + } + } + }, + { + "id": "tysHw", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 容器", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "box", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "容器", + "fill": "$text-tertiary" + } + } + }, + { + "id": "Wy14X", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 镜像", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "layers", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "镜像", + "fill": "$text-tertiary" + } + } + }, + { + "id": "pbuVt", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 卷", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "hard-drive", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "卷", + "fill": "$text-tertiary" + } + } + }, + { + "id": "I6Pdp", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 网络", + "descendants": { + "cQhxS": { + "fill": "$accent-dim" + }, + "F1F9E": { + "icon": "network", + "fill": "$accent" + }, + "DE5G9": { + "content": "网络", + "fill": "$accent" + } + } + }, + { + "id": "ZYoWC", + "type": "ref", + "ref": "nKcgr", + "name": "Nav Compose", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "boxes", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "Compose", + "fill": "$text-tertiary" + } + } + }, + { + "id": "g3Pqm5", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 系统", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "gauge", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "系统", + "fill": "$text-tertiary" + } + } + }, + { + "type": "frame", + "id": "EbwXw", + "name": "Spacer", + "width": 1, + "height": "fill_container", + "fill": "#00000000" + }, + { + "id": "DMZQc", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 设置", + "descendants": { + "F1F9E": { + "icon": "settings" + }, + "DE5G9": { + "content": "设置" + } + } + } + ] + }, + { + "type": "frame", + "id": "BCGrh", + "name": "Main", + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "IRAG5", + "name": "Toolbar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 9, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "KaUAK", + "name": "Title", + "fill": "$text-primary", + "content": "网络", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "text", + "id": "im9W5", + "name": "Count", + "fill": "$text-muted", + "content": "6", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "qvGmO", + "name": "Filter Segmented", + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 2, + "padding": 2, + "children": [ + { + "id": "rmSTi", + "type": "ref", + "ref": "MdmRT", + "name": "F 全部 18", + "height": 20, + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "全部 18", + "fontSize": 11, + "fill": "$text-primary" + } + } + }, + { + "id": "k7QMl", + "type": "ref", + "ref": "MdmRT", + "name": "F 运行中 12", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "运行中 12", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + }, + { + "id": "sdr2q", + "type": "ref", + "ref": "MdmRT", + "name": "F 已停止 5", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "已停止 5", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + }, + { + "id": "X8dndT", + "type": "ref", + "ref": "MdmRT", + "name": "F 异常 1", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "异常 1", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + } + ] + }, + { + "type": "frame", + "id": "KIrpH", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "kJCfT", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 拉取镜像", + "descendants": { + "RQ8pM": { + "icon": "arrow-down-to-line" + }, + "P0aUw": { + "content": "拉取镜像" + } + } + }, + { + "id": "mmQ6b", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 新建 Compose", + "descendants": { + "RQ8pM": { + "icon": "file-code" + }, + "P0aUw": { + "content": "新建 Compose" + } + } + }, + { + "id": "enw60", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 运行容器", + "fill": "$accent", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "plus", + "fill": "$accent-on" + }, + "P0aUw": { + "content": "运行容器…", + "fill": "$accent-on", + "fontWeight": "500" + } + } + }, + { + "id": "q1eTG4", + "type": "ref", + "ref": "yoK0T", + "name": "Refresh", + "descendants": { + "XMGqY": { + "icon": "refresh-cw" + } + } + } + ] + }, + { + "type": "frame", + "id": "KHTgE", + "name": "Header Row", + "width": "fill_container", + "height": 26, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "VQEQ1", + "name": "Pad", + "width": 26, + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "y7my9k", + "name": "H 名称", + "clip": true, + "width": "fill_container", + "height": 26, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "nh9fx", + "name": "L", + "fill": "$text-muted", + "content": "名称", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "D83CN", + "name": "H 驱动", + "clip": true, + "width": 92, + "height": 26, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "jWaJ5", + "name": "L", + "fill": "$text-muted", + "content": "驱动", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "M5vHjQ", + "name": "H 作用域", + "clip": true, + "width": 84, + "height": 26, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mODxS", + "name": "L", + "fill": "$text-muted", + "content": "作用域", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "lcHWF", + "name": "H 子网", + "clip": true, + "width": 164, + "height": 26, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "P6Odk", + "name": "L", + "fill": "$text-muted", + "content": "子网", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "q879kY", + "name": "H 已接入", + "clip": true, + "width": 112, + "height": 26, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "yALpK", + "name": "L", + "fill": "$text-muted", + "content": "已接入", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "Gk4mx", + "name": "H 操作", + "clip": true, + "width": 104, + "height": 26, + "padding": [ + 0, + 4 + ], + "alignItems": "center" + } + ] + }, + { + "type": "frame", + "id": "GHozF", + "name": "List", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "xLie8", + "name": "NR web-stack_default", + "width": "fill_container", + "height": 32, + "fill": "$bg-active", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "f8nle0", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "w2dhN", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "uMGGp", + "name": "I", + "width": 13, + "height": 13, + "icon": "network", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "hu4oi", + "name": "N", + "fill": "$text-primary", + "content": "web-stack_default", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "uKuUI", + "name": "C", + "clip": true, + "width": 92, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "PqWwU", + "name": "T", + "fill": "$text-secondary", + "content": "bridge", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "RT3K7", + "name": "C", + "clip": true, + "width": 84, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ZKt1F", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "f2CQKf", + "name": "C", + "clip": true, + "width": 164, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "rpYNB", + "name": "T", + "fill": "$info", + "content": "172.20.0.0/16", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "K6Nth", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Cih8f", + "name": "T", + "fill": "$ok", + "content": "3 个容器", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "pvmkh", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "vKPcY", + "type": "ref", + "ref": "yoK0T", + "name": "A plug", + "width": 22, + "height": 22, + "opacity": 1, + "descendants": { + "XMGqY": { + "icon": "plug", + "width": 13, + "height": 13 + } + } + }, + { + "id": "ukGhO", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "opacity": 1, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "NUUIG", + "name": "NR db-stack_default", + "width": "fill_container", + "height": 32, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "zxFhU", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "NCVSz", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "k8ppiW", + "name": "I", + "width": 13, + "height": 13, + "icon": "network", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "xRu8o", + "name": "N", + "fill": "$text-primary", + "content": "db-stack_default", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "v5imCZ", + "name": "C", + "clip": true, + "width": 92, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "k0ZksW", + "name": "T", + "fill": "$text-secondary", + "content": "bridge", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "WnpKf", + "name": "C", + "clip": true, + "width": 84, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "JN4jJ", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ricdj", + "name": "C", + "clip": true, + "width": 164, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "T5utag", + "name": "T", + "fill": "$info", + "content": "172.21.0.0/16", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Fwfrl", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "UTme2", + "name": "T", + "fill": "$ok", + "content": "2 个容器", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "JOPiE", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "HG09q", + "type": "ref", + "ref": "yoK0T", + "name": "A plug", + "width": 22, + "height": 22, + "opacity": 1, + "descendants": { + "XMGqY": { + "icon": "plug", + "width": 13, + "height": 13 + } + } + }, + { + "id": "k87w2D", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "opacity": 1, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "W0zdP0", + "name": "NR monitoring", + "width": "fill_container", + "height": 32, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "RYxIm", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "GH0uN", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "YMjBj", + "name": "I", + "width": 13, + "height": 13, + "icon": "network", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "LxRF9", + "name": "N", + "fill": "$text-primary", + "content": "monitoring", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "R1K5G9", + "name": "C", + "clip": true, + "width": 92, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "DSUSY", + "name": "T", + "fill": "$text-secondary", + "content": "bridge", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "D2eyT", + "name": "C", + "clip": true, + "width": 84, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "UaUHi", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Iusa7", + "name": "C", + "clip": true, + "width": 164, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "oHxls", + "name": "T", + "fill": "$info", + "content": "172.22.0.0/16", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "uy7Hu", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "GerZo", + "name": "T", + "fill": "$ok", + "content": "2 个容器", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "T9JCz", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "j6ACOU", + "type": "ref", + "ref": "yoK0T", + "name": "A plug", + "width": 22, + "height": 22, + "opacity": 1, + "descendants": { + "XMGqY": { + "icon": "plug", + "width": 13, + "height": 13 + } + } + }, + { + "id": "i90Dm", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "opacity": 1, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "PmHXE", + "name": "NR edge-macvlan", + "width": "fill_container", + "height": 32, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "PsP3V", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "t8ASlV", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "lnEER", + "name": "I", + "width": 13, + "height": 13, + "icon": "network", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "b3LCg", + "name": "N", + "fill": "$text-primary", + "content": "edge-macvlan", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "my3xA", + "name": "C", + "clip": true, + "width": 92, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "LdnNl", + "name": "T", + "fill": "$text-secondary", + "content": "macvlan", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "xOF2f", + "name": "C", + "clip": true, + "width": 84, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Nm0PP", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "y5xOIp", + "name": "C", + "clip": true, + "width": 164, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "TKFtm", + "name": "T", + "fill": "$info", + "content": "192.168.10.0/24", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "PxD1m", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "k26sA", + "name": "T", + "fill": "$warn", + "content": "未接入", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "QOh6W", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "hyLEe", + "type": "ref", + "ref": "yoK0T", + "name": "A plug", + "width": 22, + "height": 22, + "opacity": 1, + "descendants": { + "XMGqY": { + "icon": "plug", + "width": 13, + "height": 13 + } + } + }, + { + "id": "yQRWp", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "opacity": 1, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "SBaJA", + "name": "NR bridge", + "width": "fill_container", + "height": 32, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "p8KZL", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "Y1kpMO", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "Nc1KK", + "name": "I", + "width": 13, + "height": 13, + "icon": "network", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "E6tuR", + "name": "N", + "fill": "$text-secondary", + "content": "bridge", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "id": "rm2G9", + "type": "ref", + "ref": "x0Pcos", + "name": "B", + "height": 15, + "fill": "#00000000", + "stroke": "$border", + "strokeWidth": 1, + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "内置", + "fontSize": 9, + "fill": "$text-muted" + } + } + } + ] + }, + { + "type": "frame", + "id": "gBrSe", + "name": "C", + "clip": true, + "width": 92, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Z5OQe", + "name": "T", + "fill": "$text-secondary", + "content": "bridge", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Y6UBaN", + "name": "C", + "clip": true, + "width": 84, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "g2g1YW", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "V3yXG6", + "name": "C", + "clip": true, + "width": 164, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Mq1QC", + "name": "T", + "fill": "$info", + "content": "172.17.0.0/16", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "rnccs", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "yXM3T", + "name": "T", + "fill": "$text-tertiary", + "content": "2 个容器", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "V5Jmd", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "QyalS", + "type": "ref", + "ref": "yoK0T", + "name": "A plug", + "width": 22, + "height": 22, + "opacity": 1, + "descendants": { + "XMGqY": { + "icon": "plug", + "width": 13, + "height": 13 + } + } + }, + { + "id": "RdTDC", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "opacity": 0.25, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "xD8qm", + "name": "NR host", + "width": "fill_container", + "height": 32, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "zvBro", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "wAiVb", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "sS8Uy", + "name": "I", + "width": 13, + "height": 13, + "icon": "network", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "vfR2b", + "name": "N", + "fill": "$text-secondary", + "content": "host", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "id": "PODto", + "type": "ref", + "ref": "x0Pcos", + "name": "B", + "height": 15, + "fill": "#00000000", + "stroke": "$border", + "strokeWidth": 1, + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "内置", + "fontSize": 9, + "fill": "$text-muted" + } + } + } + ] + }, + { + "type": "frame", + "id": "cIGpg", + "name": "C", + "clip": true, + "width": 92, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "CbHeE", + "name": "T", + "fill": "$text-secondary", + "content": "host", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "tGBnr", + "name": "C", + "clip": true, + "width": 84, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "TOf5b", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "yhKaf", + "name": "C", + "clip": true, + "width": 164, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "izaX3", + "name": "T", + "fill": "$info", + "content": "—", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "k9LTR", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "YxhFq", + "name": "T", + "fill": "$text-tertiary", + "content": "1 个容器", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "JShJ0", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "vX7en", + "type": "ref", + "ref": "yoK0T", + "name": "A plug", + "width": 22, + "height": 22, + "opacity": 1, + "descendants": { + "XMGqY": { + "icon": "plug", + "width": 13, + "height": 13 + } + } + }, + { + "id": "u3ODPo", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "opacity": 0.25, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "y8F9k", + "name": "NR none", + "width": "fill_container", + "height": 32, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "r2dSd", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "OixEf", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "RxNnQ", + "name": "I", + "width": 13, + "height": 13, + "icon": "network", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "R68OeP", + "name": "N", + "fill": "$text-secondary", + "content": "none", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "id": "s1mbJA", + "type": "ref", + "ref": "x0Pcos", + "name": "B", + "height": 15, + "fill": "#00000000", + "stroke": "$border", + "strokeWidth": 1, + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "内置", + "fontSize": 9, + "fill": "$text-muted" + } + } + } + ] + }, + { + "type": "frame", + "id": "DRKcd", + "name": "C", + "clip": true, + "width": 92, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "B7Jua", + "name": "T", + "fill": "$text-secondary", + "content": "null", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "n9QTEP", + "name": "C", + "clip": true, + "width": 84, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "c5je8", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Prxd4", + "name": "C", + "clip": true, + "width": 164, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "rZb3E", + "name": "T", + "fill": "$info", + "content": "—", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "gbLjJ", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "SM1AX", + "name": "T", + "fill": "$text-muted", + "content": "未接入", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "QzhcH", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "G00xu", + "type": "ref", + "ref": "yoK0T", + "name": "A plug", + "width": 22, + "height": 22, + "opacity": 1, + "descendants": { + "XMGqY": { + "icon": "plug", + "width": 13, + "height": 13 + } + } + }, + { + "id": "RBjMS", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "opacity": 0.25, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "H7ma9", + "name": "Detail Drawer", + "width": 440, + "height": "fill_container", + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "left": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "e3Uiz", + "name": "Head", + "width": "fill_container", + "height": 44, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "V9HDx5", + "name": "I", + "width": 15, + "height": 15, + "icon": "network", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "AXHCR", + "name": "N", + "fill": "$text-primary", + "content": "web-stack_default", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "id": "PEZf1", + "type": "ref", + "ref": "x0Pcos", + "name": "Chip", + "fill": "$accent-dim", + "descendants": { + "kvk5k": { + "fill": "$accent" + }, + "ODVko": { + "content": "bridge", + "fill": "$accent" + } + } + }, + { + "type": "frame", + "id": "J6FMzO", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "mrfTr", + "type": "ref", + "ref": "yoK0T", + "name": "H pin", + "descendants": { + "XMGqY": { + "icon": "pin" + } + } + }, + { + "id": "iaYQk", + "type": "ref", + "ref": "yoK0T", + "name": "H x", + "descendants": { + "XMGqY": { + "icon": "x" + } + } + } + ] + }, + { + "type": "frame", + "id": "NT26O", + "name": "Tabs", + "width": "fill_container", + "height": 34, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 2, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "id": "WsMa8", + "type": "ref", + "ref": "MdmRT", + "name": "T 概览", + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "icon": "info", + "fill": "$text-primary" + }, + "iscyV": { + "content": "概览", + "fill": "$text-primary", + "fontWeight": "500" + } + } + }, + { + "id": "ycwIO", + "type": "ref", + "ref": "MdmRT", + "name": "T 拓扑", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "share-2", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "拓扑", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + }, + { + "id": "A91iN8", + "type": "ref", + "ref": "MdmRT", + "name": "T 原始 JSON", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "file-json", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "原始 JSON", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + } + ] + }, + { + "type": "frame", + "id": "XGYOv", + "name": "Actions", + "width": "fill_container", + "height": 42, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "id": "jt5T0", + "type": "ref", + "ref": "i1OTz", + "name": "A 接入", + "fill": "$accent-dim", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "plug", + "fill": "$accent" + }, + "P0aUw": { + "content": "接入容器…", + "fill": "$accent" + } + } + }, + { + "id": "FTsfB", + "type": "ref", + "ref": "i1OTz", + "name": "A 导出", + "descendants": { + "RQ8pM": { + "icon": "file-json" + }, + "P0aUw": { + "content": "导出配置" + } + } + }, + { + "type": "frame", + "id": "jXVoK", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "bEq50", + "type": "ref", + "ref": "yoK0T", + "name": "More", + "descendants": { + "XMGqY": { + "icon": "ellipsis" + } + } + } + ] + }, + { + "type": "frame", + "id": "IRj6Q", + "name": "Content", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 10, + "padding": [ + 12, + 14 + ], + "children": [ + { + "type": "frame", + "id": "IRmSc", + "name": "Sec 基本信息", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "XveiB", + "name": "H", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "t4xA2", + "name": "T", + "fill": "$text-secondary", + "content": "基本信息", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "cJG25", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "id": "githa", + "type": "ref", + "ref": "l3qo4", + "name": "KV 网络 ID", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "网络 ID", + "width": 78 + }, + "psgr6": { + "content": "8f2a71c0d349", + "fill": "$text-primary", + "fontSize": 10 + } + } + }, + { + "id": "r01dWW", + "type": "ref", + "ref": "l3qo4", + "name": "KV 驱动", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "驱动", + "width": 78 + }, + "psgr6": { + "content": "bridge", + "fill": "$text-primary", + "fontSize": 10 + } + } + }, + { + "id": "iXZQ1", + "type": "ref", + "ref": "l3qo4", + "name": "KV 作用域", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "作用域", + "width": 78 + }, + "psgr6": { + "content": "local", + "fill": "$text-primary", + "fontSize": 10 + } + } + }, + { + "id": "z45iZz", + "type": "ref", + "ref": "l3qo4", + "name": "KV 创建于", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "创建于", + "width": 78 + }, + "psgr6": { + "content": "2026-07-31 14:02:08", + "fill": "$text-primary", + "fontSize": 10 + } + } + }, + { + "id": "g1xXA0", + "type": "ref", + "ref": "l3qo4", + "name": "KV internal", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "internal", + "width": 78 + }, + "psgr6": { + "content": "否 · 可访问外网", + "fill": "$text-primary", + "fontSize": 10 + } + } + }, + { + "id": "xu06r", + "type": "ref", + "ref": "l3qo4", + "name": "KV attachable", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "attachable", + "width": 78 + }, + "psgr6": { + "content": "是", + "fill": "$text-primary", + "fontSize": 10 + } + } + }, + { + "id": "Bc44q", + "type": "ref", + "ref": "l3qo4", + "name": "KV IPv6", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "IPv6", + "width": 78 + }, + "psgr6": { + "content": "关闭", + "fill": "$text-primary", + "fontSize": 10 + } + } + } + ] + }, + { + "type": "rectangle", + "id": "V99V8", + "name": "Div", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "gHwyq", + "name": "Sec IPAM", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "mpWk9", + "name": "H", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "tXxej", + "name": "T", + "fill": "$text-secondary", + "content": "IPAM", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "kr6QT", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "id": "sW80e", + "type": "ref", + "ref": "l3qo4", + "name": "KV 子网", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "子网", + "width": 78 + }, + "psgr6": { + "content": "172.20.0.0/16", + "fill": "$info", + "fontSize": 10 + } + } + }, + { + "id": "sIS6f", + "type": "ref", + "ref": "l3qo4", + "name": "KV 网关", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "网关", + "width": 78 + }, + "psgr6": { + "content": "172.20.0.1", + "fill": "$info", + "fontSize": 10 + } + } + }, + { + "id": "SCrrq", + "type": "ref", + "ref": "l3qo4", + "name": "KV IP 范围", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "IP 范围", + "width": 78 + }, + "psgr6": { + "content": "(未限制)", + "fill": "$info", + "fontSize": 10 + } + } + }, + { + "id": "Kpo7K", + "type": "ref", + "ref": "l3qo4", + "name": "KV 已分配", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "已分配", + "width": 78 + }, + "psgr6": { + "content": "3 / 65533", + "fill": "$info", + "fontSize": 10 + } + } + } + ] + }, + { + "type": "rectangle", + "id": "wm1zb", + "name": "Div", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "vnHsX", + "name": "Sec 已接入容器 (3)", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "jU5DT", + "name": "H", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "HISve", + "name": "T", + "fill": "$text-secondary", + "content": "已接入容器 (3)", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "KQkVd", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "wvOCk", + "name": "A", + "fill": "$accent", + "content": "全部摘除", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "UvbWt", + "name": "U nginx-proxy", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "kvt0k", + "name": "D", + "fill": "$ok", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "fmhIX", + "name": "N", + "fill": "$text-primary", + "content": "nginx-proxy", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "q1nce", + "name": "IP", + "fill": "$info", + "content": "172.20.0.4", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "jJV6P", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "gTAwX", + "name": "Al", + "fill": "$text-muted", + "content": "别名 proxy", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "id": "xedxB", + "type": "ref", + "ref": "yoK0T", + "name": "Rm", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "unplug", + "width": 12, + "height": 12 + } + } + } + ] + }, + { + "type": "frame", + "id": "kYfIj", + "name": "U api-gateway", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "lwy2i", + "name": "D", + "fill": "$ok", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "jHcJd", + "name": "N", + "fill": "$text-primary", + "content": "api-gateway", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "aHnQN", + "name": "IP", + "fill": "$info", + "content": "172.20.0.5", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "vSuLn", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "N40cCZ", + "name": "Al", + "fill": "$text-muted", + "content": "别名 api", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "id": "M9WgI", + "type": "ref", + "ref": "yoK0T", + "name": "Rm", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "unplug", + "width": 12, + "height": 12 + } + } + } + ] + }, + { + "type": "frame", + "id": "uAq8g", + "name": "U worker-1", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "frLPd", + "name": "D", + "fill": "$ok", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "VbCNZ", + "name": "N", + "fill": "$text-primary", + "content": "worker-1", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ijc9O", + "name": "IP", + "fill": "$info", + "content": "172.20.0.9", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ONVEY", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "uiCH7", + "name": "Al", + "fill": "$text-muted", + "content": "别名 —", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "id": "YTpHc", + "type": "ref", + "ref": "yoK0T", + "name": "Rm", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "unplug", + "width": 12, + "height": 12 + } + } + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "c86gw", + "name": "Div", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "EFhiB", + "name": "Sec 标签", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "l68Tn", + "name": "H", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ZSJmD", + "name": "T", + "fill": "$text-secondary", + "content": "标签", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "h5zBVW", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "id": "OEXsp", + "type": "ref", + "ref": "l3qo4", + "name": "KV project", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "project", + "width": 78 + }, + "psgr6": { + "content": "web-stack", + "fill": "$violet", + "fontSize": 10 + } + } + } + ] + }, + { + "type": "frame", + "id": "r4NZrf", + "name": "S", + "width": "fill_container", + "height": "fill_container", + "fill": "#00000000" + }, + { + "type": "frame", + "id": "y6u32", + "name": "Danger", + "width": "fill_container", + "fill": "$danger-dim", + "cornerRadius": "$r-md", + "stroke": "$danger", + "strokeWidth": 1, + "layout": "vertical", + "gap": 7, + "padding": [ + 10, + 12 + ], + "children": [ + { + "type": "frame", + "id": "N2P2NH", + "name": "H", + "width": "fill_container", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "IJEe8", + "name": "I", + "width": 13, + "height": 13, + "icon": "triangle-alert", + "library": "lucide", + "fill": "$danger" + }, + { + "type": "text", + "id": "LCaDJ", + "name": "T", + "fill": "$danger", + "content": "删除网络", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "ifIdN", + "name": "D", + "fill": "$danger", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "仍有 3 个容器接入,必须先摘除或停止它们。删除本身不会丢数据。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "OfV8u", + "type": "ref", + "ref": "i1OTz", + "name": "Del", + "height": 26, + "stroke": "$danger", + "opacity": 0.4, + "descendants": { + "RQ8pM": { + "icon": "trash-2", + "fill": "$danger" + }, + "P0aUw": { + "content": "删除网络(不可用)", + "fill": "$danger", + "fontSize": 11 + } + } + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "xHmom", + "name": "Status Bar", + "width": "fill_container", + "height": 24, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 14, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "C3mVzC", + "name": "Msg", + "fill": "$text-tertiary", + "content": "GET /networks · 6 个网络 · 内置的 bridge / host / none 不可删除", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "yhWWg", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "CODwq", + "name": "API v1.47", + "fill": "$text-muted", + "content": "API v1.47", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ta4Xs", + "name": "Engine 27.3.1", + "fill": "$text-muted", + "content": "Engine 27.3.1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "PJH8U", + "name": "18 容器 · 34 镜像 · 9 卷", + "fill": "$text-muted", + "content": "18 容器 · 34 镜像 · 9 卷", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "QYZKj", + "name": "事件流 已连接", + "fill": "$text-muted", + "content": "事件流 已连接", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "jrnWL", + "x": 1059, + "y": 6860, + "name": "09 · Compose(项目 / YAML / 实时输出)", + "theme": { + "mode": "dark" + }, + "clip": true, + "width": 1440, + "height": 860, + "fill": "$bg-page", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "rZIIf", + "name": "Top Bar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "X6cNg", + "name": "Brand", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "cxT8w", + "name": "Logo", + "width": 17, + "height": 17, + "icon": "container", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "yRVl8", + "name": "Title", + "fill": "$text-primary", + "content": "Docker", + "fontFamily": "$font-ui", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "rectangle", + "id": "HvLPl", + "name": "Divider", + "fill": "$border", + "width": 1, + "height": 18 + }, + { + "type": "frame", + "id": "vd90W", + "name": "Host Switcher", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "VgR6E", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "server", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "h1NgJ", + "name": "Host", + "fill": "$text-primary", + "content": "prod-sg-01", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "itMFL", + "name": "Transport", + "fill": "$text-muted", + "content": "/var/run/docker.sock · SSH 隧道", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "g18QB7", + "name": "Caret", + "width": 12, + "height": 12, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + }, + { + "type": "frame", + "id": "QirYf", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "Qdn9F", + "name": "Search", + "width": 300, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "SSpbH", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "search", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "Iwhlc", + "name": "Placeholder", + "fill": "$text-muted", + "content": "搜索容器、镜像、卷、网络…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "aZWy6", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "lc3Nf", + "name": "Kbd", + "height": 16, + "fill": "$bg-hover", + "cornerRadius": 3, + "padding": [ + 0, + 5 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mSwaP", + "name": "Keys", + "fill": "$text-tertiary", + "content": "Ctrl K", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "id": "j75sjM", + "type": "ref", + "ref": "x0Pcos", + "name": "Live Badge", + "descendants": { + "ODVko": { + "content": "实时" + } + } + }, + { + "id": "Xagi0", + "type": "ref", + "ref": "yoK0T", + "name": "Settings", + "descendants": { + "XMGqY": { + "icon": "settings" + } + } + } + ] + }, + { + "type": "frame", + "id": "z5k0Qu", + "name": "Body", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "t4VEc", + "name": "Rail", + "width": 56, + "height": "fill_container", + "fill": "$bg-rail", + "stroke": "$border", + "strokeWidth": { + "right": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 2, + "padding": [ + 6, + 0 + ], + "alignItems": "center", + "children": [ + { + "id": "ONEGj", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 总览", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "layout-dashboard", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "总览", + "fill": "$text-tertiary" + } + } + }, + { + "id": "l2lhM", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 容器", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "box", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "容器", + "fill": "$text-tertiary" + } + } + }, + { + "id": "g3KkI", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 镜像", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "layers", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "镜像", + "fill": "$text-tertiary" + } + } + }, + { + "id": "KDWP5", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 卷", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "hard-drive", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "卷", + "fill": "$text-tertiary" + } + } + }, + { + "id": "nBc6V", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 网络", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "network", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "网络", + "fill": "$text-tertiary" + } + } + }, + { + "id": "wmZb2", + "type": "ref", + "ref": "nKcgr", + "name": "Nav Compose", + "descendants": { + "cQhxS": { + "fill": "$accent-dim" + }, + "F1F9E": { + "icon": "boxes", + "fill": "$accent" + }, + "DE5G9": { + "content": "Compose", + "fill": "$accent" + } + } + }, + { + "id": "KfwA2", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 系统", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "gauge", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "系统", + "fill": "$text-tertiary" + } + } + }, + { + "type": "frame", + "id": "czWbA", + "name": "Spacer", + "width": 1, + "height": "fill_container", + "fill": "#00000000" + }, + { + "id": "N9LiK", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 设置", + "descendants": { + "F1F9E": { + "icon": "settings" + }, + "DE5G9": { + "content": "设置" + } + } + } + ] + }, + { + "type": "frame", + "id": "V3KF6y", + "name": "Projects", + "width": 260, + "height": "fill_container", + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "right": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "u3mzXL", + "name": "H", + "width": "fill_container", + "height": 40, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 6, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Cz3Yj", + "name": "T", + "fill": "$text-primary", + "content": "Compose 项目", + "fontFamily": "$font-ui", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "text", + "id": "CKlz0", + "name": "C", + "fill": "$text-muted", + "content": "4", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "EYMlc", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "fZh4f", + "type": "ref", + "ref": "yoK0T", + "name": "Add", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "plus", + "width": 13, + "height": 13 + } + } + } + ] + }, + { + "type": "frame", + "id": "emq25", + "name": "P web-stack", + "clip": true, + "width": "fill_container", + "height": 52, + "fill": "$accent-dim", + "layout": "vertical", + "gap": 3, + "padding": [ + 0, + 12 + ], + "justifyContent": "center", + "children": [ + { + "type": "frame", + "id": "IRNWp", + "name": "T", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "ac30B", + "name": "I", + "width": 13, + "height": 13, + "icon": "boxes", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "lhJja", + "name": "N", + "fill": "$text-primary", + "content": "web-stack", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "C9BWa", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "w0ccVg", + "name": "St", + "fill": "$ok", + "content": "3/3 运行中", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "sqw6A", + "name": "Path", + "fill": "$text-muted", + "content": "/srv/stacks/web-stack", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "pFSxw", + "name": "P db-stack", + "clip": true, + "width": "fill_container", + "height": 52, + "fill": "#00000000", + "layout": "vertical", + "gap": 3, + "padding": [ + 0, + 12 + ], + "justifyContent": "center", + "children": [ + { + "type": "frame", + "id": "NEWfn", + "name": "T", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "yZHAd", + "name": "I", + "width": 13, + "height": 13, + "icon": "boxes", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "eYWM3", + "name": "N", + "fill": "$text-secondary", + "content": "db-stack", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "g5RNKq", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "A1MyEQ", + "name": "St", + "fill": "$ok", + "content": "2/2 运行中", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "fUJgU", + "name": "Path", + "fill": "$text-muted", + "content": "/srv/stacks/db-stack", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "M3WE05", + "name": "P observability", + "clip": true, + "width": "fill_container", + "height": 52, + "fill": "#00000000", + "layout": "vertical", + "gap": 3, + "padding": [ + 0, + 12 + ], + "justifyContent": "center", + "children": [ + { + "type": "frame", + "id": "bGVRn", + "name": "T", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "BZVrl", + "name": "I", + "width": 13, + "height": 13, + "icon": "boxes", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "BzUeF", + "name": "N", + "fill": "$text-secondary", + "content": "observability", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "lG3jp", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "A3joB", + "name": "St", + "fill": "$text-muted", + "content": "0/2 已停止", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "lEtzV", + "name": "Path", + "fill": "$text-muted", + "content": "/srv/stacks/obs", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "wKzIn", + "name": "P jobs", + "clip": true, + "width": "fill_container", + "height": 52, + "fill": "#00000000", + "layout": "vertical", + "gap": 3, + "padding": [ + 0, + 12 + ], + "justifyContent": "center", + "children": [ + { + "type": "frame", + "id": "cLL8d", + "name": "T", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "jTizZ", + "name": "I", + "width": 13, + "height": 13, + "icon": "boxes", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "M8PeB", + "name": "N", + "fill": "$text-secondary", + "content": "jobs", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "OuVYC", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "tuK1i", + "name": "St", + "fill": "$ok", + "content": "2/2 运行中", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "tsVAk", + "name": "Path", + "fill": "$text-muted", + "content": "/srv/stacks/jobs", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "uuFf0", + "name": "S", + "width": "fill_container", + "height": "fill_container", + "fill": "#00000000" + }, + { + "type": "frame", + "id": "GqwXp", + "name": "Foot", + "width": "fill_container", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 5, + "padding": [ + 10, + 12 + ], + "children": [ + { + "type": "text", + "id": "TfaXT", + "name": "T", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "compose ls 只认得起过至少一次的项目", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "id": "MlQ8z", + "type": "ref", + "ref": "i1OTz", + "name": "Open", + "height": 24, + "width": "fill_container", + "descendants": { + "RQ8pM": { + "icon": "folder-open" + }, + "P0aUw": { + "content": "按路径打开…", + "fontSize": 11 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "nBJTH", + "name": "Main", + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "k1zO3E", + "name": "Proj Header", + "width": "fill_container", + "height": 48, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "QN8rJ", + "name": "L", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "frame", + "id": "bzL7h", + "name": "R", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "Bmrwr", + "name": "I", + "width": 15, + "height": 15, + "icon": "boxes", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "z2PJ4", + "name": "N", + "fill": "$text-primary", + "content": "web-stack", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "id": "emeaa", + "type": "ref", + "ref": "x0Pcos", + "name": "Chip", + "descendants": { + "ODVko": { + "content": "3/3 运行中" + } + } + } + ] + }, + { + "type": "text", + "id": "gByvY", + "name": "P", + "fill": "$text-muted", + "content": "/srv/stacks/web-stack · compose.yaml", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "q3zmWD", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "EJ4zk", + "type": "ref", + "ref": "i1OTz", + "name": "B pull", + "height": 26, + "descendants": { + "RQ8pM": { + "icon": "download" + }, + "P0aUw": { + "content": "pull", + "fontFamily": "$font-mono", + "fontSize": 11 + } + } + }, + { + "id": "fHZ0v", + "type": "ref", + "ref": "i1OTz", + "name": "B build", + "height": 26, + "descendants": { + "RQ8pM": { + "icon": "hammer" + }, + "P0aUw": { + "content": "build", + "fontFamily": "$font-mono", + "fontSize": 11 + } + } + }, + { + "id": "E4UFT0", + "type": "ref", + "ref": "i1OTz", + "name": "B restart", + "height": 26, + "descendants": { + "RQ8pM": { + "icon": "rotate-cw" + }, + "P0aUw": { + "content": "restart", + "fontFamily": "$font-mono", + "fontSize": 11 + } + } + }, + { + "id": "hFkFL", + "type": "ref", + "ref": "i1OTz", + "name": "B stop", + "height": 26, + "descendants": { + "RQ8pM": { + "icon": "square" + }, + "P0aUw": { + "content": "stop", + "fontFamily": "$font-mono", + "fontSize": 11 + } + } + }, + { + "id": "R1b22O", + "type": "ref", + "ref": "i1OTz", + "name": "B down", + "height": 26, + "stroke": "$danger-dim", + "descendants": { + "RQ8pM": { + "icon": "trash-2", + "fill": "$danger" + }, + "P0aUw": { + "content": "down", + "fontFamily": "$font-mono", + "fontSize": 11, + "fill": "$danger" + } + } + }, + { + "id": "V135C", + "type": "ref", + "ref": "i1OTz", + "name": "B up", + "height": 26, + "fill": "$accent", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "play", + "fill": "$accent-on" + }, + "P0aUw": { + "content": "up -d", + "fontFamily": "$font-mono", + "fontSize": 11, + "fill": "$accent-on", + "fontWeight": "500" + } + } + } + ] + }, + { + "type": "frame", + "id": "LAwXL", + "name": "Tabs", + "width": "fill_container", + "height": 32, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 2, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "id": "CGKn2", + "type": "ref", + "ref": "MdmRT", + "name": "T compose.yaml", + "height": 24, + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "icon": "file-code", + "width": 11, + "height": 11, + "fill": "$warn" + }, + "iscyV": { + "content": "compose.yaml", + "fontSize": 11, + "fontFamily": "$font-mono", + "fill": "$text-primary" + } + } + }, + { + "id": "L7TKqW", + "type": "ref", + "ref": "MdmRT", + "name": "T .env", + "height": 24, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "file-key", + "width": 11, + "height": 11, + "fill": "$text-tertiary" + }, + "iscyV": { + "content": ".env", + "fontSize": 11, + "fontFamily": "$font-mono", + "fill": "$text-tertiary" + } + } + }, + { + "id": "avILZ", + "type": "ref", + "ref": "MdmRT", + "name": "T 服务 (3)", + "height": 24, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "list-tree", + "width": 11, + "height": 11, + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "服务 (3)", + "fontSize": 11, + "fontFamily": "$font-mono", + "fill": "$text-tertiary" + } + } + }, + { + "id": "P26uB", + "type": "ref", + "ref": "MdmRT", + "name": "T config 展开", + "height": 24, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "file-search", + "width": 11, + "height": 11, + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "config 展开", + "fontSize": 11, + "fontFamily": "$font-mono", + "fill": "$text-tertiary" + } + } + }, + { + "id": "RSE0n", + "type": "ref", + "ref": "MdmRT", + "name": "T 合并日志", + "height": 24, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "scroll-text", + "width": 11, + "height": 11, + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "合并日志", + "fontSize": 11, + "fontFamily": "$font-mono", + "fill": "$text-tertiary" + } + } + }, + { + "type": "frame", + "id": "JXhIc", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "Wppdz", + "type": "ref", + "ref": "x0Pcos", + "name": "Dirty", + "height": 18, + "fill": "$warn-dim", + "descendants": { + "kvk5k": { + "fill": "$warn" + }, + "ODVko": { + "content": "未保存 · 保存需手打 save", + "fill": "$warn", + "fontSize": 9 + } + } + } + ] + }, + { + "type": "frame", + "id": "pvDGk", + "name": "Editor", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-terminal", + "layout": "vertical", + "padding": [ + 8, + 0 + ], + "children": [ + { + "type": "frame", + "id": "b2eKL", + "name": "YL2", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "TfEBg", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "2", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "UHTvn", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Z0c5rR", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "hQfOm", + "name": "K", + "fill": "$violet", + "content": "services:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "G3vN7O", + "name": "YL3", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "pfOm4", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "3", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Qwt0A", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "dCxjH", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "C6f5V", + "name": "I", + "width": 13.24, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "F5pYw", + "name": "K", + "fill": "$info", + "content": "proxy:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Sv5gG", + "name": "YL4", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "LgsnF", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "4", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "uxHbs", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "FJtXl", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "UDEFB", + "name": "I", + "width": 26.48, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "ik7Y6", + "name": "K", + "fill": "$info", + "content": "image:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "a8ulbH", + "name": "V", + "fill": "$ok", + "content": " nginx:1.27-alpine", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "qqoDC", + "name": "YL5", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "e1uJYT", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "5", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "HCs0d", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "j5PHd", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "I4FFkt", + "name": "I", + "width": 26.48, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Q3ZU0I", + "name": "K", + "fill": "$info", + "content": "container_name:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "XKeIb", + "name": "V", + "fill": "$ok", + "content": " nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Y1Op8", + "name": "YL6", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "qqAdB", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "6", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ECjOO", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "VuaU8", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "F8dAa2", + "name": "I", + "width": 26.48, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Ijidg", + "name": "K", + "fill": "$info", + "content": "restart:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "irvZ2", + "name": "V", + "fill": "$ok", + "content": " unless-stopped", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Rj1zs", + "name": "YL7", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "EaT9S", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "7", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "DOy4L", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "GjndZ", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "XaPSq", + "name": "I", + "width": 26.48, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "UMPy1", + "name": "K", + "fill": "$info", + "content": "ports:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "jqzD3", + "name": "YL8", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "V3yFwt", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "8", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Q8rie", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "DzAnQ", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "URDO0", + "name": "I", + "width": 39.72, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "BY5Fa", + "name": "D", + "fill": "$text-tertiary", + "content": "- ", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "p0W6e9", + "name": "V", + "fill": "$ok", + "content": "\"8080:80\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "C4kaG", + "name": "YL9", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "$ok-dim", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "sn0HN", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "9", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "S16Ntg", + "name": "Mk", + "fill": "$ok", + "textGrowth": "fixed-width", + "width": 16, + "content": "+", + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "oMJfK", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "miJm3", + "name": "I", + "width": 39.72, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "o0jhfh", + "name": "D", + "fill": "$text-tertiary", + "content": "- ", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Qoxvj", + "name": "V", + "fill": "$ok", + "content": "\"8443:443\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "paPpx", + "name": "YL10", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "fptYD", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "10", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "s25q2J", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Y7ePb1", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Ch38P", + "name": "I", + "width": 26.48, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Yws5Y", + "name": "K", + "fill": "$info", + "content": "volumes:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "mef2f", + "name": "YL11", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "AX23C", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "11", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ea7rc", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Ikwq6", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "okFhC", + "name": "I", + "width": 39.72, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "AZy8u", + "name": "D", + "fill": "$text-tertiary", + "content": "- ", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "EzUoS", + "name": "V", + "fill": "$ok", + "content": "./conf.d:/etc/nginx/conf.d:ro", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Bbznx", + "name": "YL12", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "e9np9", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "12", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "BmlaN", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "HguIw", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "GZWR0", + "name": "I", + "width": 39.72, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "vH5mH", + "name": "D", + "fill": "$text-tertiary", + "content": "- ", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "sEo64", + "name": "V", + "fill": "$ok", + "content": "nginx-cache:/var/cache/nginx", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "d6LvWC", + "name": "YL13", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "OL60x", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "13", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "CLH4j", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "DcDDV", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "J65MU", + "name": "I", + "width": 26.48, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "x4g1cK", + "name": "K", + "fill": "$info", + "content": "depends_on:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "e3MKC", + "name": "YL14", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "eWb3o", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "14", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "U6zxC", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "WAYO9", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "zvvEa", + "name": "I", + "width": 39.72, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Xo2TO", + "name": "D", + "fill": "$text-tertiary", + "content": "- ", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "B8LOB", + "name": "V", + "fill": "$ok", + "content": "api", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "gZEow", + "name": "YL15", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "qCn2a", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "15", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "iRcEd", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "PLMyD", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "d6uOf", + "name": "I", + "width": 26.48, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "D30hhO", + "name": "K", + "fill": "$info", + "content": "healthcheck:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "r4s1e", + "name": "YL16", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "il4tm", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "16", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "D6P3K", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "vexVx", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "YLYlH", + "name": "I", + "width": 39.72, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Joe2P", + "name": "K", + "fill": "$info", + "content": "test:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "FZNEI", + "name": "V", + "fill": "$ok", + "content": " [\"CMD\", \"curl\", \"-f\", \"http://localhost/health\"]", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "bqrmg", + "name": "YL17", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "LDjtR", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "17", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "txwdu", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "D3kEDc", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "HMvWT", + "name": "I", + "width": 39.72, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "mbXDG", + "name": "K", + "fill": "$info", + "content": "interval:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "IMzZC", + "name": "V", + "fill": "$ok", + "content": " 30s", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ekhcP", + "name": "YL18", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "w8ybY", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "18", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "GmHlq", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "lhNWd", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "RkW75", + "name": "I", + "width": 39.72, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "ccVTh", + "name": "K", + "fill": "$info", + "content": "timeout:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "KooFR", + "name": "V", + "fill": "$ok", + "content": " 3s", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "bP0Yj", + "name": "YL19", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Mgq7U", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "19", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "aM5Kl", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "EukKZ", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "VDsqe", + "name": "I", + "width": 39.72, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "l9S9n", + "name": "K", + "fill": "$info", + "content": "retries:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "I9ujlR", + "name": "V", + "fill": "$ok", + "content": " 3", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "J5tdf", + "name": "YL21", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "K9qVS9", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "21", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "I3leji", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "SAVWe", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "mqFo4", + "name": "I", + "width": 13.24, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "UMH8k", + "name": "K", + "fill": "$info", + "content": "api:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "v4kAp", + "name": "YL22", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "$warn-dim", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "IRQlm", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "22", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "nL7cf", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "content": "~", + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "rkOoH", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Dx9UT", + "name": "I", + "width": 26.48, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "CFlgT", + "name": "K", + "fill": "$info", + "content": "image:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "UUSKm", + "name": "V", + "fill": "$ok", + "content": " ghcr.io/acme/api:2.8.1", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ekYdU", + "name": "YL23", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "agOGI", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "23", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "aIzQ6", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "hN8QZ", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "zjRYv", + "name": "I", + "width": 26.48, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "A9r6CX", + "name": "K", + "fill": "$info", + "content": "container_name:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "CD140", + "name": "V", + "fill": "$ok", + "content": " api-gateway", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "X5dWr", + "name": "YL24", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "m6Acl", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "24", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "TzvWc", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "AWSC7", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "bpyVD", + "name": "I", + "width": 26.48, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Pv7QO", + "name": "K", + "fill": "$info", + "content": "restart:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "kcVDb", + "name": "V", + "fill": "$ok", + "content": " unless-stopped", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "m4ap7p", + "name": "YL25", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "lTnje", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "25", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Z7QgHo", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "fKmjA", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Kd1Nd", + "name": "I", + "width": 26.48, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "uBNdh", + "name": "K", + "fill": "$info", + "content": "env_file:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "lDvNn", + "name": "V", + "fill": "$ok", + "content": " .env", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "b0kGr", + "name": "YL26", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "S5PJE", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "26", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "khy8e", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "C0oUah", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "j83uzz", + "name": "I", + "width": 26.48, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "HNlXM", + "name": "K", + "fill": "$info", + "content": "environment:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "HgoxP", + "name": "YL27", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "q20gsR", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "27", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "o8yCma", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "C3LX0T", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "ACIsA", + "name": "I", + "width": 39.72, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "TCNDU", + "name": "K", + "fill": "$info", + "content": "DATABASE_URL:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "vh7XH", + "name": "V", + "fill": "$ok", + "content": " postgres://app@postgres-main:5432/shop", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "n2RjgJ", + "name": "YL28", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "O3FReP", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "28", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "xRI6K", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "URhPt", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "FDHtM", + "name": "I", + "width": 39.72, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "hicFp", + "name": "K", + "fill": "$info", + "content": "REDIS_URL:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Jo6uR", + "name": "V", + "fill": "$ok", + "content": " redis://redis-cache:6379/0", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "y83szu", + "name": "YL29", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "KJkeT", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "29", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ycqrE", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "vOCna", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "mfcdZ", + "name": "I", + "width": 26.48, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "uhHGw", + "name": "K", + "fill": "$info", + "content": "expose:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "z6XSAe", + "name": "YL30", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "VfF2s", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "30", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "FtHh1", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "m9p6z8", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "j99oJY", + "name": "I", + "width": 39.72, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "zbLVW", + "name": "D", + "fill": "$text-tertiary", + "content": "- ", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "I8OdV", + "name": "V", + "fill": "$ok", + "content": "\"8000\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "b1Dsj", + "name": "YL36", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "K0OTW", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "36", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "BU5vK", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "JuxzW", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "QhJjN", + "name": "K", + "fill": "$violet", + "content": "volumes:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "eLEnC", + "name": "YL37", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "NwZZf", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "37", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "e3r19X", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "U309cu", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "R5T8A", + "name": "I", + "width": 13.24, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "z1WAy", + "name": "K", + "fill": "$info", + "content": "nginx-cache:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "S9Lzg", + "name": "YL39", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "cCtAt", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "39", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "S6SrkH", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "wvetz", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "kJZmX", + "name": "K", + "fill": "$violet", + "content": "networks:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Q0OPFc", + "name": "YL40", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "JAZMV", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "40", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "dgw7C", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "hUvjS", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "v3HmLg", + "name": "I", + "width": 13.24, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Q3awc", + "name": "K", + "fill": "$info", + "content": "default:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "W3uFdG", + "name": "YL41", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "cq1MK", + "name": "No", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 42, + "content": "41", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "yisng", + "name": "Mk", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 16, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "z7K6C", + "name": "Tok", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "HVx5v", + "name": "I", + "width": 26.48, + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "CSH5v", + "name": "K", + "fill": "$info", + "content": "name:", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "XR3oI", + "name": "V", + "fill": "$ok", + "content": " web-stack_default", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "iRVpR", + "name": "Services", + "width": "fill_container", + "height": 114, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "JRSX3", + "name": "H", + "width": "fill_container", + "height": 30, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 7, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "KpGiE", + "name": "I", + "width": 13, + "height": 13, + "icon": "list-tree", + "library": "lucide", + "fill": "$text-secondary" + }, + { + "type": "text", + "id": "RMd6m", + "name": "T", + "fill": "$text-secondary", + "content": "服务", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "q52cF3", + "name": "C", + "fill": "$text-muted", + "content": "compose ps -a", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "hNSU1", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "c0z72", + "name": "A", + "fill": "$accent", + "content": "展开为容器列表", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "A4VzKe", + "name": "S proxy", + "clip": true, + "width": "fill_container", + "height": 30, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "P9VViT", + "name": "C", + "clip": true, + "width": 104, + "height": 30, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "D2DUm", + "name": "D", + "fill": "$ok", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "T25o5", + "name": "T", + "fill": "$text-primary", + "content": "proxy", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "GYujL", + "name": "C", + "clip": true, + "width": 170, + "height": 30, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "sr2fB", + "name": "T", + "fill": "$text-secondary", + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "la2LO", + "name": "C", + "clip": true, + "width": 154, + "height": 30, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "U7lxz", + "name": "T", + "fill": "$ok", + "content": "running (healthy)", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "tazAo", + "name": "C", + "clip": true, + "width": "fill_container", + "height": 30, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "OftqI", + "name": "T", + "fill": "$info", + "content": "8080→80, 8443→443", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "fP2D9", + "name": "CA", + "width": 80, + "height": 30, + "gap": 1, + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "khewi", + "type": "ref", + "ref": "yoK0T", + "name": "A scroll-text", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "scroll-text", + "width": 13, + "height": 13 + } + } + }, + { + "id": "Mlgmg", + "type": "ref", + "ref": "yoK0T", + "name": "A terminal", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "terminal", + "width": 13, + "height": 13 + } + } + }, + { + "id": "yk2HA", + "type": "ref", + "ref": "yoK0T", + "name": "A rotate-cw", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "rotate-cw", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "lfZwI", + "name": "S api", + "clip": true, + "width": "fill_container", + "height": 30, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Mjm5h", + "name": "C", + "clip": true, + "width": 104, + "height": 30, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "MS945", + "name": "D", + "fill": "$ok", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "ZJnGO", + "name": "T", + "fill": "$text-primary", + "content": "api", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "pm0k6", + "name": "C", + "clip": true, + "width": 170, + "height": 30, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "nWL2P", + "name": "T", + "fill": "$text-secondary", + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "xumE7", + "name": "C", + "clip": true, + "width": 154, + "height": 30, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "l8zQj", + "name": "T", + "fill": "$ok", + "content": "running", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "s54Ib", + "name": "C", + "clip": true, + "width": "fill_container", + "height": 30, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "w01KA", + "name": "T", + "fill": "$info", + "content": "8000/tcp", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "H0RQH", + "name": "CA", + "width": 80, + "height": 30, + "gap": 1, + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "cPYpP", + "type": "ref", + "ref": "yoK0T", + "name": "A scroll-text", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "scroll-text", + "width": 13, + "height": 13 + } + } + }, + { + "id": "o3n097", + "type": "ref", + "ref": "yoK0T", + "name": "A terminal", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "terminal", + "width": 13, + "height": 13 + } + } + }, + { + "id": "m7nUyq", + "type": "ref", + "ref": "yoK0T", + "name": "A rotate-cw", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "rotate-cw", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "lwmdp", + "name": "Output", + "width": 400, + "height": "fill_container", + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "left": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "Y8HSus", + "name": "H", + "width": "fill_container", + "height": 44, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "mGKnH", + "name": "I", + "width": 15, + "height": 15, + "icon": "square-terminal", + "library": "lucide", + "fill": "$text-secondary" + }, + { + "type": "text", + "id": "FDTee", + "name": "T", + "fill": "$text-primary", + "content": "执行记录", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "id": "i3toH", + "type": "ref", + "ref": "x0Pcos", + "name": "Chip", + "descendants": { + "ODVko": { + "content": "退出码 0" + } + } + }, + { + "type": "frame", + "id": "GUkqd", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "q6sQQ", + "type": "ref", + "ref": "yoK0T", + "name": "A copy", + "descendants": { + "XMGqY": { + "icon": "copy" + } + } + }, + { + "id": "ewuCX", + "type": "ref", + "ref": "yoK0T", + "name": "A eraser", + "descendants": { + "XMGqY": { + "icon": "eraser" + } + } + } + ] + }, + { + "type": "frame", + "id": "SGDq5", + "name": "Body", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-terminal", + "layout": "vertical", + "padding": [ + 10, + 12 + ], + "children": [ + { + "type": "frame", + "id": "k7jVv", + "name": "OL", + "clip": true, + "width": "fill_container", + "height": 17, + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "nmFDd", + "name": "t", + "fill": "$text-muted", + "content": "09:38:02", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "h18Kz", + "name": "t", + "fill": "$ok", + "content": "$", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "sLwGW", + "name": "t", + "fill": "$text-terminal", + "content": "docker compose -p web-stack \\", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "LKaXr", + "name": "OL", + "clip": true, + "width": "fill_container", + "height": 17, + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "fjwiK", + "name": "t", + "fill": "$text-muted", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "NjFLE", + "name": "t", + "fill": "$text-terminal", + "content": " -f compose.yaml --project-dir /srv/… up -d", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "MHl2s", + "name": "OL", + "clip": true, + "width": "fill_container", + "height": 17, + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "fUDvN", + "name": "t", + "fill": "$text-terminal", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "VcvK6", + "name": "OL", + "clip": true, + "width": "fill_container", + "height": 17, + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "W3KQxB", + "name": "t", + "fill": "$text-secondary", + "content": "[+] Running 6/6", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "W8XFFO", + "name": "OL", + "clip": true, + "width": "fill_container", + "height": 17, + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "oz1wB", + "name": "t", + "fill": "$ok", + "content": " ✔", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "THPp4", + "name": "t", + "fill": "$text-tertiary", + "content": "Network web-stack_default Created", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "gWZa2", + "name": "t", + "fill": "$text-muted", + "content": "0.1s", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Ab3dO", + "name": "OL", + "clip": true, + "width": "fill_container", + "height": 17, + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "WULXV", + "name": "t", + "fill": "$ok", + "content": " ✔", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "R9qlrH", + "name": "t", + "fill": "$text-tertiary", + "content": "Volume nginx-cache Created", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "PekWR", + "name": "t", + "fill": "$text-muted", + "content": "0.0s", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "DprQs", + "name": "OL", + "clip": true, + "width": "fill_container", + "height": 17, + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "qWxqU", + "name": "t", + "fill": "$ok", + "content": " ✔", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "x5bcU", + "name": "t", + "fill": "$text-tertiary", + "content": "Container web-stack-migrate-1 Started", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "auzIQ", + "name": "t", + "fill": "$text-muted", + "content": "0.9s", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "sqaP3", + "name": "OL", + "clip": true, + "width": "fill_container", + "height": 17, + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "uodBk", + "name": "t", + "fill": "$ok", + "content": " ✔", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "sciAT", + "name": "t", + "fill": "$text-tertiary", + "content": "Container api-gateway Started", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Sb4HJ", + "name": "t", + "fill": "$text-muted", + "content": "1.4s", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "N2sEGL", + "name": "OL", + "clip": true, + "width": "fill_container", + "height": 17, + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "YFMf9", + "name": "t", + "fill": "$ok", + "content": " ✔", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "c6TBoJ", + "name": "t", + "fill": "$text-tertiary", + "content": "Container nginx-proxy Started", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "C7azJi", + "name": "t", + "fill": "$text-muted", + "content": "1.8s", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Nbg5v", + "name": "OL", + "clip": true, + "width": "fill_container", + "height": 17, + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "g4kBh", + "name": "t", + "fill": "$ok", + "content": " ✔", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "uT5qh", + "name": "t", + "fill": "$text-tertiary", + "content": "Container nginx-proxy Healthy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "dckFl", + "name": "t", + "fill": "$text-muted", + "content": "6.2s", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "WeFVA", + "name": "OL", + "clip": true, + "width": "fill_container", + "height": 17, + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "lFlxF", + "name": "t", + "fill": "$text-terminal", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Ejo6b", + "name": "OL", + "clip": true, + "width": "fill_container", + "height": 17, + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "uMFck", + "name": "t", + "fill": "$text-muted", + "content": "09:38:09", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "jBxGT", + "name": "t", + "fill": "$ok", + "content": "✔", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "EWW57", + "name": "t", + "fill": "$ok", + "content": "up -d 完成 · 用时 6.4s · 退出码 0", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "exeux", + "name": "OL", + "clip": true, + "width": "fill_container", + "height": 17, + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "p2fk6e", + "name": "t", + "fill": "$text-terminal", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "jt28x", + "name": "OL", + "clip": true, + "width": "fill_container", + "height": 17, + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "z4zTBi", + "name": "t", + "fill": "$text-muted", + "content": "09:36:44", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "se4hv", + "name": "t", + "fill": "$ok", + "content": "$", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "kzJMh", + "name": "t", + "fill": "$text-terminal", + "content": "docker compose … pull", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "g6FJa", + "name": "OL", + "clip": true, + "width": "fill_container", + "height": 17, + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "LSEEW", + "name": "t", + "fill": "$text-tertiary", + "content": "proxy Pulling", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "T98aF2", + "name": "OL", + "clip": true, + "width": "fill_container", + "height": 17, + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "QQit8", + "name": "t", + "fill": "$text-muted", + "content": " 9c4f2e1b7a Already exists", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "z8S4TN", + "name": "OL", + "clip": true, + "width": "fill_container", + "height": 17, + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "W4mJB", + "name": "t", + "fill": "$text-tertiary", + "content": " 3f81a92c04 Pull complete", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "YJmee", + "name": "OL", + "clip": true, + "width": "fill_container", + "height": 17, + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "koLem", + "name": "t", + "fill": "$text-tertiary", + "content": "api Pulled", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "MrXkX", + "name": "OL", + "clip": true, + "width": "fill_container", + "height": 18, + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "aTqsU", + "name": "t", + "fill": "$ok", + "content": "✔ 2 个服务已是最新 · 1 个已更新", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "qAyGv", + "name": "OL", + "clip": true, + "width": "fill_container", + "height": 17, + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "TinAu", + "name": "t", + "fill": "$text-terminal", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "MNxv1", + "name": "OL", + "clip": true, + "width": "fill_container", + "height": 17, + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "d5az8G", + "name": "t", + "fill": "$text-muted", + "content": "09:31:12", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "lr8tg", + "name": "t", + "fill": "$warn", + "content": "!", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Wvqg5", + "name": "t", + "fill": "$warn", + "content": "down 被取消 —— 未通过确认闸门", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "d1Hht", + "name": "S", + "width": "fill_container", + "height": "fill_container", + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "B7flv", + "name": "Foot", + "width": "fill_container", + "height": 26, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "smkM9", + "name": "T", + "fill": "$text-muted", + "content": "保留最近 50 条 · stdout 与 stderr 分色", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "AyimA", + "name": "Status Bar", + "width": "fill_container", + "height": 24, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 14, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "PsBBO", + "name": "Msg", + "fill": "$text-tertiary", + "content": "compose 命令固定钉住 -p / -f / --project-directory · 三者缺一都会出错", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "LmuZa", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "c6pTxN", + "name": "API v1.47", + "fill": "$text-muted", + "content": "API v1.47", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "S34gp", + "name": "Engine 27.3.1", + "fill": "$text-muted", + "content": "Engine 27.3.1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "qiyq3", + "name": "18 容器 · 34 镜像 · 9 卷", + "fill": "$text-muted", + "content": "18 容器 · 34 镜像 · 9 卷", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "DSL2x", + "name": "事件流 已连接", + "fill": "$text-muted", + "content": "事件流 已连接", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "eMiOs", + "x": 1059, + "y": 7840, + "name": "10 · 系统 · 磁盘与回收", + "theme": { + "mode": "dark" + }, + "clip": true, + "width": 1440, + "height": 860, + "fill": "$bg-page", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "UMCvq", + "name": "Top Bar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "r4WgI", + "name": "Brand", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "kGBSW", + "name": "Logo", + "width": 17, + "height": 17, + "icon": "container", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "U88P5p", + "name": "Title", + "fill": "$text-primary", + "content": "Docker", + "fontFamily": "$font-ui", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "rectangle", + "id": "LQaM4", + "name": "Divider", + "fill": "$border", + "width": 1, + "height": 18 + }, + { + "type": "frame", + "id": "v02PkH", + "name": "Host Switcher", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "SaGRb", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "server", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "L50km", + "name": "Host", + "fill": "$text-primary", + "content": "prod-sg-01", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "KpNFN", + "name": "Transport", + "fill": "$text-muted", + "content": "/var/run/docker.sock · SSH 隧道", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "l6QMl", + "name": "Caret", + "width": 12, + "height": 12, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + }, + { + "type": "frame", + "id": "IYf68", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "TbUIr", + "name": "Search", + "width": 300, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "aUMXR", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "search", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "WFha0", + "name": "Placeholder", + "fill": "$text-muted", + "content": "搜索容器、镜像、卷、网络…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "G1SUD", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "V6RYf", + "name": "Kbd", + "height": 16, + "fill": "$bg-hover", + "cornerRadius": 3, + "padding": [ + 0, + 5 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "TPfWl", + "name": "Keys", + "fill": "$text-tertiary", + "content": "Ctrl K", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "id": "a0T0ei", + "type": "ref", + "ref": "x0Pcos", + "name": "Live Badge", + "descendants": { + "ODVko": { + "content": "实时" + } + } + }, + { + "id": "eMVi6", + "type": "ref", + "ref": "yoK0T", + "name": "Settings", + "descendants": { + "XMGqY": { + "icon": "settings" + } + } + } + ] + }, + { + "type": "frame", + "id": "Coxee", + "name": "Body", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "L8W2Y", + "name": "Rail", + "width": 56, + "height": "fill_container", + "fill": "$bg-rail", + "stroke": "$border", + "strokeWidth": { + "right": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 2, + "padding": [ + 6, + 0 + ], + "alignItems": "center", + "children": [ + { + "id": "D5UNQ1", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 总览", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "layout-dashboard", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "总览", + "fill": "$text-tertiary" + } + } + }, + { + "id": "r2UeVa", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 容器", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "box", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "容器", + "fill": "$text-tertiary" + } + } + }, + { + "id": "h5bemn", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 镜像", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "layers", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "镜像", + "fill": "$text-tertiary" + } + } + }, + { + "id": "HE9k3", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 卷", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "hard-drive", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "卷", + "fill": "$text-tertiary" + } + } + }, + { + "id": "ouRsN", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 网络", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "network", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "网络", + "fill": "$text-tertiary" + } + } + }, + { + "id": "i69u7", + "type": "ref", + "ref": "nKcgr", + "name": "Nav Compose", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "boxes", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "Compose", + "fill": "$text-tertiary" + } + } + }, + { + "id": "S0DOc", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 系统", + "descendants": { + "cQhxS": { + "fill": "$accent-dim" + }, + "F1F9E": { + "icon": "gauge", + "fill": "$accent" + }, + "DE5G9": { + "content": "系统", + "fill": "$accent" + } + } + }, + { + "type": "frame", + "id": "QAG9C", + "name": "Spacer", + "width": 1, + "height": "fill_container", + "fill": "#00000000" + }, + { + "id": "CCpCw", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 设置", + "descendants": { + "F1F9E": { + "icon": "settings" + }, + "DE5G9": { + "content": "设置" + } + } + } + ] + }, + { + "type": "frame", + "id": "rDG2c", + "name": "Main", + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "p3sDN8", + "name": "Toolbar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 9, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "XpV07", + "name": "Title", + "fill": "$text-primary", + "content": "系统", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "text", + "id": "lx4V6", + "name": "Count", + "fill": "$text-muted", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "asZhO", + "name": "Filter Segmented", + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 2, + "padding": 2, + "children": [ + { + "id": "RO9Tq", + "type": "ref", + "ref": "MdmRT", + "name": "F 全部 18", + "height": 20, + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "全部 18", + "fontSize": 11, + "fill": "$text-primary" + } + } + }, + { + "id": "ixuie", + "type": "ref", + "ref": "MdmRT", + "name": "F 运行中 12", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "运行中 12", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + }, + { + "id": "h88MF2", + "type": "ref", + "ref": "MdmRT", + "name": "F 已停止 5", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "已停止 5", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + }, + { + "id": "V9dlxt", + "type": "ref", + "ref": "MdmRT", + "name": "F 异常 1", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "异常 1", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + } + ] + }, + { + "type": "frame", + "id": "whJL1", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "UqMda", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 拉取镜像", + "descendants": { + "RQ8pM": { + "icon": "arrow-down-to-line" + }, + "P0aUw": { + "content": "拉取镜像" + } + } + }, + { + "id": "k1trS", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 新建 Compose", + "descendants": { + "RQ8pM": { + "icon": "file-code" + }, + "P0aUw": { + "content": "新建 Compose" + } + } + }, + { + "id": "mUuvP", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 运行容器", + "fill": "$accent", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "plus", + "fill": "$accent-on" + }, + "P0aUw": { + "content": "运行容器…", + "fill": "$accent-on", + "fontWeight": "500" + } + } + }, + { + "id": "NUS3P", + "type": "ref", + "ref": "yoK0T", + "name": "Refresh", + "descendants": { + "XMGqY": { + "icon": "refresh-cw" + } + } + } + ] + }, + { + "type": "frame", + "id": "KFmRV", + "name": "Content", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-page", + "layout": "vertical", + "gap": 14, + "padding": 16, + "children": [ + { + "type": "frame", + "id": "NeOD5", + "name": "Stats", + "width": "fill_container", + "gap": 12, + "children": [ + { + "id": "micuQ", + "type": "ref", + "ref": "sr5Cb", + "name": "St 容器", + "width": "fill_container", + "descendants": { + "RbtQX": { + "icon": "box", + "fill": "$accent" + }, + "T2It9": { + "content": "容器" + }, + "X8FzB": { + "content": "18" + }, + "UEarH": { + "content": "运行 12 · 停止 5 · 异常 1" + } + } + }, + { + "id": "aJRvS", + "type": "ref", + "ref": "sr5Cb", + "name": "St 镜像", + "width": "fill_container", + "descendants": { + "RbtQX": { + "icon": "layers", + "fill": "$violet" + }, + "T2It9": { + "content": "镜像" + }, + "X8FzB": { + "content": "34" + }, + "UEarH": { + "content": "20 使用中 · 2 悬空" + } + } + }, + { + "id": "Znjyc", + "type": "ref", + "ref": "sr5Cb", + "name": "St 卷", + "width": "fill_container", + "descendants": { + "RbtQX": { + "icon": "hard-drive", + "fill": "$ok" + }, + "T2It9": { + "content": "卷" + }, + "X8FzB": { + "content": "9" + }, + "UEarH": { + "content": "24.8 GB · 2 未使用" + } + } + }, + { + "id": "WyaPo", + "type": "ref", + "ref": "sr5Cb", + "name": "St 网络", + "width": "fill_container", + "descendants": { + "RbtQX": { + "icon": "network", + "fill": "$info" + }, + "T2It9": { + "content": "网络" + }, + "X8FzB": { + "content": "6" + }, + "UEarH": { + "content": "3 自定义 · 3 内置" + } + } + }, + { + "id": "MDLki", + "type": "ref", + "ref": "sr5Cb", + "name": "St 构建缓存", + "width": "fill_container", + "descendants": { + "RbtQX": { + "icon": "database-backup", + "fill": "$warn" + }, + "T2It9": { + "content": "构建缓存" + }, + "X8FzB": { + "content": "6.2 GB" + }, + "UEarH": { + "content": "全部可回收" + } + } + } + ] + }, + { + "type": "frame", + "id": "AFkAY", + "name": "Sec 磁盘占用", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "TsggW", + "name": "H", + "width": "fill_container", + "height": 22, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "UXOYf", + "name": "T", + "fill": "$text-primary", + "content": "磁盘占用", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "RYxFy", + "name": "Sub", + "fill": "$text-muted", + "content": "docker system df -v · 总 94 GB 中已用 38.4 GB", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "I6ymH", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "B1CVFo", + "name": "A", + "fill": "$accent", + "content": "查看明细", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "NC6cK", + "name": "Bar", + "clip": true, + "width": "fill_container", + "height": 26, + "cornerRadius": "$r-sm", + "gap": 2, + "children": [ + { + "type": "frame", + "id": "Xr0oo", + "name": "镜像 14.2 GB", + "width": "fill_container", + "height": 26, + "fill": "$accent" + }, + { + "type": "frame", + "id": "LTnLq", + "name": "容器可写层 3.8 GB", + "width": "fill_container", + "height": 26, + "fill": "$violet" + }, + { + "type": "frame", + "id": "CnnfL", + "name": "卷 24.8 GB", + "width": "fill_container", + "height": 26, + "fill": "$ok" + }, + { + "type": "frame", + "id": "E60FKZ", + "name": "构建缓存 6.2 GB", + "width": "fill_container", + "height": 26, + "fill": "$warn" + }, + { + "type": "frame", + "id": "aavFv", + "name": "空闲 55.6 GB", + "width": "fill_container", + "height": 26, + "fill": "$bg-active" + } + ] + }, + { + "type": "frame", + "id": "RNmD4", + "name": "Legend", + "width": "fill_container", + "gap": 16, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "lYmZ3", + "name": "L", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 2, + "id": "Rhzmu", + "name": "S", + "fill": "$accent", + "width": 9, + "height": 9 + }, + { + "type": "text", + "id": "xtKPx", + "name": "T", + "fill": "$text-tertiary", + "content": "镜像 14.2 GB", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "SfMwX", + "name": "L", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 2, + "id": "u2xlwN", + "name": "S", + "fill": "$violet", + "width": 9, + "height": 9 + }, + { + "type": "text", + "id": "NA8JB", + "name": "T", + "fill": "$text-tertiary", + "content": "容器可写层 3.8 GB", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "WMXrk", + "name": "L", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 2, + "id": "HlZa1", + "name": "S", + "fill": "$ok", + "width": 9, + "height": 9 + }, + { + "type": "text", + "id": "OVK2d", + "name": "T", + "fill": "$text-tertiary", + "content": "卷 24.8 GB", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "OfInD", + "name": "L", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 2, + "id": "m8nFi", + "name": "S", + "fill": "$warn", + "width": 9, + "height": 9 + }, + { + "type": "text", + "id": "K7RBnS", + "name": "T", + "fill": "$text-tertiary", + "content": "构建缓存 6.2 GB", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "P5f0e", + "name": "L", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 2, + "id": "Zvj9s", + "name": "S", + "fill": "$bg-active", + "width": 9, + "height": 9 + }, + { + "type": "text", + "id": "cjEcF", + "name": "T", + "fill": "$text-tertiary", + "content": "空闲 55.6 GB", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "xNPrR", + "name": "Table", + "clip": true, + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "CoLOq", + "name": "TH", + "width": "fill_container", + "height": 28, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "n5PTti", + "name": "H", + "width": "fill_container", + "height": 28, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Zbkhz", + "name": "T", + "fill": "$text-muted", + "content": "类型", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "DAUeP", + "name": "H", + "width": 110, + "height": 28, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "S6q6M9", + "name": "T", + "fill": "$text-muted", + "content": "总数", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "C4n9C", + "name": "H", + "width": 110, + "height": 28, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "lOFwT", + "name": "T", + "fill": "$text-muted", + "content": "活跃", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "bwTYK", + "name": "H", + "width": 130, + "height": 28, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "WW63o", + "name": "T", + "fill": "$text-muted", + "content": "占用", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "ZFP7u", + "name": "H", + "width": 180, + "height": 28, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "JXwrO", + "name": "T", + "fill": "$text-muted", + "content": "可回收", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "frame", + "id": "nwZKk", + "name": "R 镜像", + "width": "fill_container", + "height": 30, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "KF98c", + "name": "C", + "clip": true, + "width": "fill_container", + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "H4GSt", + "name": "T", + "fill": "$text-primary", + "content": "镜像", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "e8R0e", + "name": "C", + "clip": true, + "width": 110, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "t57jO", + "name": "T", + "fill": "$text-secondary", + "content": "34", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "xhB3X", + "name": "C", + "clip": true, + "width": 110, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "eWl74", + "name": "T", + "fill": "$text-secondary", + "content": "20", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "piRzS", + "name": "C", + "clip": true, + "width": 130, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "cR3br", + "name": "T", + "fill": "$text-primary", + "content": "14.2 GB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "F4tlH", + "name": "C", + "clip": true, + "width": 180, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "WolPK", + "name": "T", + "fill": "$warn", + "content": "5.8 GB (41%)", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "l12SS", + "name": "R 容器", + "width": "fill_container", + "height": 30, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "KUlrE", + "name": "C", + "clip": true, + "width": "fill_container", + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "YVW1W", + "name": "T", + "fill": "$text-primary", + "content": "容器", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "DWQkm", + "name": "C", + "clip": true, + "width": 110, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "L2GMM", + "name": "T", + "fill": "$text-secondary", + "content": "18", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Y3hdb3", + "name": "C", + "clip": true, + "width": 110, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "UFZ0j", + "name": "T", + "fill": "$text-secondary", + "content": "12", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "lavoT", + "name": "C", + "clip": true, + "width": 130, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "vJyF4", + "name": "T", + "fill": "$text-primary", + "content": "3.8 GB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "W2yEbJ", + "name": "C", + "clip": true, + "width": 180, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "FE7KL", + "name": "T", + "fill": "$warn", + "content": "1.1 GB (29%)", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "qYtIG", + "name": "R 本地卷", + "width": "fill_container", + "height": 30, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "bUb9p", + "name": "C", + "clip": true, + "width": "fill_container", + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "cUghx", + "name": "T", + "fill": "$text-primary", + "content": "本地卷", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "G7gYE", + "name": "C", + "clip": true, + "width": 110, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "q6anP", + "name": "T", + "fill": "$text-secondary", + "content": "9", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "U0YczG", + "name": "C", + "clip": true, + "width": 110, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Y0DQC", + "name": "T", + "fill": "$text-secondary", + "content": "7", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "c5rcvC", + "name": "C", + "clip": true, + "width": 130, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "MIMx9", + "name": "T", + "fill": "$text-primary", + "content": "24.8 GB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "h0LUZ", + "name": "C", + "clip": true, + "width": 180, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "WSt9g", + "name": "T", + "fill": "$text-muted", + "content": "1.8 MB (0%)", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "aLwxe", + "name": "R 构建缓存", + "width": "fill_container", + "height": 30, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "qRjcf", + "name": "C", + "clip": true, + "width": "fill_container", + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "B6COhn", + "name": "T", + "fill": "$text-primary", + "content": "构建缓存", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ct1bN", + "name": "C", + "clip": true, + "width": 110, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "PeYus", + "name": "T", + "fill": "$text-secondary", + "content": "218", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "hBBY3", + "name": "C", + "clip": true, + "width": 110, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "t2RQf", + "name": "T", + "fill": "$text-secondary", + "content": "0", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "b7ZvQ", + "name": "C", + "clip": true, + "width": 130, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "CW2Xi", + "name": "T", + "fill": "$text-primary", + "content": "6.2 GB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "G6AAe", + "name": "C", + "clip": true, + "width": 180, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "SQE9M", + "name": "T", + "fill": "$danger", + "content": "6.2 GB (100%)", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "l86wJ", + "name": "Sec 空间回收", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "SuXX1", + "name": "H", + "width": "fill_container", + "height": 22, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "qmAg3", + "name": "T", + "fill": "$text-primary", + "content": "空间回收", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "BBRrU", + "name": "Sub", + "fill": "$text-muted", + "content": "每一项都会先弹出面板内确认框,里面写清将要执行的动作与后果", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "J0z8dQ", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "laTcz", + "name": "PR0", + "width": "fill_container", + "gap": 12, + "children": [ + { + "type": "frame", + "id": "ytSwV", + "name": "P 已停止的容器", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 7, + "padding": [ + 11, + 13 + ], + "children": [ + { + "type": "frame", + "id": "GNwmH", + "name": "H", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "DJUXE", + "name": "I", + "width": 13, + "height": 13, + "icon": "trash-2", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "wChXf", + "name": "T", + "fill": "$text-primary", + "content": "已停止的容器", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "i0kI3", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "QUOoU", + "type": "ref", + "ref": "x0Pcos", + "name": "Tag", + "height": 16, + "fill": "$ok-dim", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "安全", + "fill": "$ok", + "fontSize": 9 + } + } + } + ] + }, + { + "type": "text", + "id": "xtlyv", + "name": "D", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "删除所有 exited 状态的容器。运行中的不受影响。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "zPR6i", + "name": "F", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "UC678", + "name": "Sz", + "fill": "$text-secondary", + "content": "可回收 1.1 GB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "q34iM", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "XullY", + "type": "ref", + "ref": "i1OTz", + "name": "Go", + "height": 24, + "stroke": "$border", + "descendants": { + "RQ8pM": { + "enabled": false + }, + "P0aUw": { + "content": "回收…", + "fontSize": 11, + "fill": "$text-secondary" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "J1r74T", + "name": "P 悬空镜像", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 7, + "padding": [ + 11, + 13 + ], + "children": [ + { + "type": "frame", + "id": "axhl3", + "name": "H", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "G8vF2", + "name": "I", + "width": 13, + "height": 13, + "icon": "circle-dashed", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "yj65j", + "name": "T", + "fill": "$text-primary", + "content": "悬空镜像", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "USf4d", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "ZFII8", + "type": "ref", + "ref": "x0Pcos", + "name": "Tag", + "height": 16, + "fill": "$ok-dim", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "安全", + "fill": "$ok", + "fontSize": 9 + } + } + } + ] + }, + { + "type": "text", + "id": "pivZo", + "name": "D", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "没有标签、也没有容器引用的中间层。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "S4OCT", + "name": "F", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "bK0vX", + "name": "Sz", + "fill": "$text-secondary", + "content": "可回收 2.4 GB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "JWa9k", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "Zz1YM", + "type": "ref", + "ref": "i1OTz", + "name": "Go", + "height": 24, + "stroke": "$border", + "descendants": { + "RQ8pM": { + "enabled": false + }, + "P0aUw": { + "content": "回收…", + "fontSize": 11, + "fill": "$text-secondary" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "ZbxOa", + "name": "P 全部未使用镜像", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 7, + "padding": [ + 11, + 13 + ], + "children": [ + { + "type": "frame", + "id": "Vd5NX", + "name": "H", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "vSVk6", + "name": "I", + "width": 13, + "height": 13, + "icon": "layers", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "text", + "id": "f9I6Ba", + "name": "T", + "fill": "$text-primary", + "content": "全部未使用镜像", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "Tsi4L", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "kiRm5", + "type": "ref", + "ref": "x0Pcos", + "name": "Tag", + "height": 16, + "fill": "$warn-dim", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "注意", + "fill": "$warn", + "fontSize": 9 + } + } + } + ] + }, + { + "type": "text", + "id": "Y9Up7", + "name": "D", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "含有标签但当前无容器在用的镜像;重新拉取需要时间与带宽。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "IM1xI", + "name": "F", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "XBNCI", + "name": "Sz", + "fill": "$warn", + "content": "可回收 5.8 GB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "voM9J", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "f6h1sB", + "type": "ref", + "ref": "i1OTz", + "name": "Go", + "height": 24, + "stroke": "$border", + "descendants": { + "RQ8pM": { + "enabled": false + }, + "P0aUw": { + "content": "回收…", + "fontSize": 11, + "fill": "$text-secondary" + } + } + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "acWJw", + "name": "PR1", + "width": "fill_container", + "gap": 12, + "children": [ + { + "type": "frame", + "id": "cHFif", + "name": "P 构建缓存", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 7, + "padding": [ + 11, + 13 + ], + "children": [ + { + "type": "frame", + "id": "pZFzz", + "name": "H", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "FU9se", + "name": "I", + "width": 13, + "height": 13, + "icon": "database-backup", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "BRxpB", + "name": "T", + "fill": "$text-primary", + "content": "构建缓存", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "p6Zz1Q", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "uET4K", + "type": "ref", + "ref": "x0Pcos", + "name": "Tag", + "height": 16, + "fill": "$ok-dim", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "安全", + "fill": "$ok", + "fontSize": 9 + } + } + } + ] + }, + { + "type": "text", + "id": "P7Sdl", + "name": "D", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "218 条缓存记录。下次构建会明显变慢。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "kFaNF", + "name": "F", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "vyU5W", + "name": "Sz", + "fill": "$text-secondary", + "content": "可回收 6.2 GB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "H3IHp", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "Pyir1", + "type": "ref", + "ref": "i1OTz", + "name": "Go", + "height": 24, + "stroke": "$border", + "descendants": { + "RQ8pM": { + "enabled": false + }, + "P0aUw": { + "content": "回收…", + "fontSize": 11, + "fill": "$text-secondary" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "ucv39", + "name": "P 以上全部", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$danger-dim", + "strokeWidth": 1, + "layout": "vertical", + "gap": 7, + "padding": [ + 11, + 13 + ], + "children": [ + { + "type": "frame", + "id": "ox5pc", + "name": "H", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "tNuS2", + "name": "I", + "width": 13, + "height": 13, + "icon": "brush-cleaning", + "library": "lucide", + "fill": "$danger" + }, + { + "type": "text", + "id": "rMo7j", + "name": "T", + "fill": "$text-primary", + "content": "以上全部", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "ZCIVs", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "W7JBW", + "type": "ref", + "ref": "x0Pcos", + "name": "Tag", + "height": 16, + "fill": "$danger-dim", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "危险", + "fill": "$danger", + "fontSize": 9 + } + } + } + ] + }, + { + "type": "text", + "id": "Gv8p5", + "name": "D", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "等价于 system prune -a,不含卷。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ODFqn", + "name": "F", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "pQ4aF", + "name": "Sz", + "fill": "$danger", + "content": "可回收 12.4 GB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "DmFAA", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "c0uduO", + "type": "ref", + "ref": "i1OTz", + "name": "Go", + "height": 24, + "stroke": "$danger", + "descendants": { + "RQ8pM": { + "enabled": false + }, + "P0aUw": { + "content": "回收…", + "fontSize": 11, + "fill": "$danger" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "EoKd4", + "name": "P 以上全部 + 卷", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$danger-dim", + "strokeWidth": 1, + "layout": "vertical", + "gap": 7, + "padding": [ + 11, + 13 + ], + "children": [ + { + "type": "frame", + "id": "Y4WinF", + "name": "H", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "SPbKg", + "name": "I", + "width": 13, + "height": 13, + "icon": "shield-alert", + "library": "lucide", + "fill": "$danger" + }, + { + "type": "text", + "id": "q83EN", + "name": "T", + "fill": "$text-primary", + "content": "以上全部 + 卷", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "tbu9L", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "GZalX", + "type": "ref", + "ref": "x0Pcos", + "name": "Tag", + "height": 16, + "fill": "$danger-dim", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "会丢数据", + "fill": "$danger", + "fontSize": 9 + } + } + } + ] + }, + { + "type": "text", + "id": "hH3GO", + "name": "D", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "会删除未被任何容器使用的卷 —— 数据不可恢复,需手打 delete。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "mrRLi", + "name": "F", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "X3aEUJ", + "name": "Sz", + "fill": "$danger", + "content": "可回收 12.4 GB + 1.8 MB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "TRbzV", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "zDGqc", + "type": "ref", + "ref": "i1OTz", + "name": "Go", + "height": 24, + "stroke": "$danger", + "descendants": { + "RQ8pM": { + "enabled": false + }, + "P0aUw": { + "content": "回收…", + "fontSize": 11, + "fill": "$danger" + } + } + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "tEOtS", + "name": "Sec 引擎信息", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "bYQJR", + "name": "H", + "width": "fill_container", + "height": 22, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "SZD7T", + "name": "T", + "fill": "$text-primary", + "content": "引擎信息", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "E4E3gU", + "name": "Sub", + "fill": "$text-muted", + "content": "GET /version · GET /info", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "UxopO", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "hNPYf", + "name": "Two", + "width": "fill_container", + "gap": 24, + "children": [ + { + "type": "frame", + "id": "UIdbE", + "name": "Col", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "id": "BrAKh", + "type": "ref", + "ref": "l3qo4", + "name": "KV Engine 版本", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "Engine 版本", + "width": 110 + }, + "psgr6": { + "content": "27.3.1 (build 41ca978)" + } + } + }, + { + "id": "K5Al6", + "type": "ref", + "ref": "l3qo4", + "name": "KV API 版本", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "API 版本", + "width": 110 + }, + "psgr6": { + "content": "1.47 (最低 1.24)" + } + } + }, + { + "id": "eOSZC", + "type": "ref", + "ref": "l3qo4", + "name": "KV 操作系统", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "操作系统", + "width": 110 + }, + "psgr6": { + "content": "Ubuntu 24.04.1 LTS · 6.8.0-45" + } + } + }, + { + "id": "VZ6ig", + "type": "ref", + "ref": "l3qo4", + "name": "KV 架构 / CPU", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "架构 / CPU", + "width": 110 + }, + "psgr6": { + "content": "x86_64 · 16 核 · 62.7 GB" + } + } + } + ] + }, + { + "type": "frame", + "id": "aFYDW", + "name": "Col", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "id": "XBCI5", + "type": "ref", + "ref": "l3qo4", + "name": "KV 存储驱动", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "存储驱动", + "width": 110 + }, + "psgr6": { + "content": "overlay2 (extfs)" + } + } + }, + { + "id": "gCUIy", + "type": "ref", + "ref": "l3qo4", + "name": "KV Cgroup", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "Cgroup", + "width": 110 + }, + "psgr6": { + "content": "v2 · systemd" + } + } + }, + { + "id": "JYY48", + "type": "ref", + "ref": "l3qo4", + "name": "KV 日志驱动", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "日志驱动", + "width": 110 + }, + "psgr6": { + "content": "json-file · max-size 10m" + } + } + }, + { + "id": "HbEFx", + "type": "ref", + "ref": "l3qo4", + "name": "KV Rootless / Swarm", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "Rootless / Swarm", + "width": 110 + }, + "psgr6": { + "content": "否 / inactive" + } + } + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "M1jLny", + "name": "Status Bar", + "width": "fill_container", + "height": 24, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 14, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "I9RMRT", + "name": "Msg", + "fill": "$text-tertiary", + "content": "GET /system/df · 上次统计 2 分钟前 · 可回收 12.4 GB", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "VaEOj", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "l5p4Em", + "name": "API v1.47", + "fill": "$text-muted", + "content": "API v1.47", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "k1aMI4", + "name": "Engine 27.3.1", + "fill": "$text-muted", + "content": "Engine 27.3.1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Q45yin", + "name": "18 容器 · 34 镜像 · 9 卷", + "fill": "$text-muted", + "content": "18 容器 · 34 镜像 · 9 卷", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "saAf1", + "name": "事件流 已连接", + "fill": "$text-muted", + "content": "事件流 已连接", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "u1ryOf", + "x": 1059, + "y": 8820, + "name": "11 · 总览", + "theme": { + "mode": "dark" + }, + "clip": true, + "width": 1440, + "height": 860, + "fill": "$bg-page", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "KPNZb", + "name": "Top Bar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "N72uV", + "name": "Brand", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "Y2USe", + "name": "Logo", + "width": 17, + "height": 17, + "icon": "container", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "g9WnNG", + "name": "Title", + "fill": "$text-primary", + "content": "Docker", + "fontFamily": "$font-ui", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "rectangle", + "id": "z8JIv", + "name": "Divider", + "fill": "$border", + "width": 1, + "height": 18 + }, + { + "type": "frame", + "id": "Ob03h", + "name": "Host Switcher", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "VxR3h", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "server", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "TPqvS", + "name": "Host", + "fill": "$text-primary", + "content": "prod-sg-01", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "x8p7pt", + "name": "Transport", + "fill": "$text-muted", + "content": "/var/run/docker.sock · SSH 隧道", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "ToBSx", + "name": "Caret", + "width": 12, + "height": 12, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + }, + { + "type": "frame", + "id": "q2lGL", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "L6PO4", + "name": "Search", + "width": 300, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "jHmHr", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "search", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "vorJX", + "name": "Placeholder", + "fill": "$text-muted", + "content": "搜索容器、镜像、卷、网络…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "b8ahPp", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "V7sbf", + "name": "Kbd", + "height": 16, + "fill": "$bg-hover", + "cornerRadius": 3, + "padding": [ + 0, + 5 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "yamTW", + "name": "Keys", + "fill": "$text-tertiary", + "content": "Ctrl K", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "id": "jOrVA", + "type": "ref", + "ref": "x0Pcos", + "name": "Live Badge", + "descendants": { + "ODVko": { + "content": "实时" + } + } + }, + { + "id": "QGRMu", + "type": "ref", + "ref": "yoK0T", + "name": "Settings", + "descendants": { + "XMGqY": { + "icon": "settings" + } + } + } + ] + }, + { + "type": "frame", + "id": "fyUEP", + "name": "Body", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "gbWDq", + "name": "Rail", + "width": 56, + "height": "fill_container", + "fill": "$bg-rail", + "stroke": "$border", + "strokeWidth": { + "right": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 2, + "padding": [ + 6, + 0 + ], + "alignItems": "center", + "children": [ + { + "id": "grKEz", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 总览", + "descendants": { + "cQhxS": { + "fill": "$accent-dim" + }, + "F1F9E": { + "icon": "layout-dashboard", + "fill": "$accent" + }, + "DE5G9": { + "content": "总览", + "fill": "$accent" + } + } + }, + { + "id": "g124JR", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 容器", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "box", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "容器", + "fill": "$text-tertiary" + } + } + }, + { + "id": "hc3wq", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 镜像", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "layers", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "镜像", + "fill": "$text-tertiary" + } + } + }, + { + "id": "uIzyx", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 卷", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "hard-drive", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "卷", + "fill": "$text-tertiary" + } + } + }, + { + "id": "U5xV2j", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 网络", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "network", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "网络", + "fill": "$text-tertiary" + } + } + }, + { + "id": "daa4a", + "type": "ref", + "ref": "nKcgr", + "name": "Nav Compose", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "boxes", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "Compose", + "fill": "$text-tertiary" + } + } + }, + { + "id": "o4juWR", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 系统", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "gauge", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "系统", + "fill": "$text-tertiary" + } + } + }, + { + "type": "frame", + "id": "g8Sxb", + "name": "Spacer", + "width": 1, + "height": "fill_container", + "fill": "#00000000" + }, + { + "id": "MogcY", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 设置", + "descendants": { + "F1F9E": { + "icon": "settings" + }, + "DE5G9": { + "content": "设置" + } + } + } + ] + }, + { + "type": "frame", + "id": "J4FgMB", + "name": "Main", + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "B1RqiA", + "name": "Toolbar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 9, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "W69CBJ", + "name": "Title", + "fill": "$text-primary", + "content": "总览", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "text", + "id": "kvNVN", + "name": "Count", + "fill": "$text-muted", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "g7mQEo", + "name": "Filter Segmented", + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 2, + "padding": 2, + "children": [ + { + "id": "nE5dw", + "type": "ref", + "ref": "MdmRT", + "name": "F 全部 18", + "height": 20, + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "全部 18", + "fontSize": 11, + "fill": "$text-primary" + } + } + }, + { + "id": "QmOCg", + "type": "ref", + "ref": "MdmRT", + "name": "F 运行中 12", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "运行中 12", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + }, + { + "id": "FHdqq", + "type": "ref", + "ref": "MdmRT", + "name": "F 已停止 5", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "已停止 5", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + }, + { + "id": "nTyDV", + "type": "ref", + "ref": "MdmRT", + "name": "F 异常 1", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "异常 1", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + } + ] + }, + { + "type": "frame", + "id": "xj0NH", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "EUG1I", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 拉取镜像", + "descendants": { + "RQ8pM": { + "icon": "arrow-down-to-line" + }, + "P0aUw": { + "content": "拉取镜像" + } + } + }, + { + "id": "p8cWlx", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 新建 Compose", + "descendants": { + "RQ8pM": { + "icon": "file-code" + }, + "P0aUw": { + "content": "新建 Compose" + } + } + }, + { + "id": "mnEVx", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 运行容器", + "fill": "$accent", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "plus", + "fill": "$accent-on" + }, + "P0aUw": { + "content": "运行容器…", + "fill": "$accent-on", + "fontWeight": "500" + } + } + }, + { + "id": "GqujT", + "type": "ref", + "ref": "yoK0T", + "name": "Refresh", + "descendants": { + "XMGqY": { + "icon": "refresh-cw" + } + } + } + ] + }, + { + "type": "frame", + "id": "JbSNE", + "name": "Content", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-page", + "layout": "vertical", + "gap": 14, + "padding": 16, + "children": [ + { + "type": "frame", + "id": "i7ZVAY", + "name": "Stats", + "width": "fill_container", + "gap": 12, + "children": [ + { + "id": "kMNqK", + "type": "ref", + "ref": "sr5Cb", + "name": "St 运行中容器", + "width": "fill_container", + "descendants": { + "RbtQX": { + "icon": "box", + "fill": "$ok" + }, + "T2It9": { + "content": "运行中容器" + }, + "X8FzB": { + "content": "12 / 18" + }, + "UEarH": { + "content": "1 个不健康 · 1 个异常退出" + } + } + }, + { + "id": "e3rgPe", + "type": "ref", + "ref": "sr5Cb", + "name": "St CPU 总占用", + "width": "fill_container", + "descendants": { + "RbtQX": { + "icon": "cpu", + "fill": "$accent" + }, + "T2It9": { + "content": "CPU 总占用" + }, + "X8FzB": { + "content": "24.6%" + }, + "UEarH": { + "content": "16 核 · 峰值 61%" + } + } + }, + { + "id": "G336xG", + "type": "ref", + "ref": "sr5Cb", + "name": "St 内存", + "width": "fill_container", + "descendants": { + "RbtQX": { + "icon": "memory-stick", + "fill": "$violet" + }, + "T2It9": { + "content": "内存" + }, + "X8FzB": { + "content": "6.2 GB" + }, + "UEarH": { + "content": "共 62.7 GB · 9.9%" + } + } + }, + { + "id": "Uk2jP", + "type": "ref", + "ref": "sr5Cb", + "name": "St 可回收空间", + "width": "fill_container", + "descendants": { + "RbtQX": { + "icon": "hard-drive", + "fill": "$warn" + }, + "T2It9": { + "content": "可回收空间" + }, + "X8FzB": { + "content": "12.4 GB" + }, + "UEarH": { + "content": "镜像 5.8 · 缓存 6.2 · 容器 1.1" + } + } + } + ] + }, + { + "type": "frame", + "id": "Es5by", + "name": "Mid", + "width": "fill_container", + "height": "fill_container", + "gap": 12, + "children": [ + { + "type": "frame", + "id": "RxjVy", + "name": "Left", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 12, + "children": [ + { + "type": "frame", + "id": "rjgRq", + "name": "Card 资源趋势", + "clip": true, + "width": "fill_container", + "height": 216, + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 9, + "padding": [ + 12, + 14 + ], + "children": [ + { + "type": "frame", + "id": "Ypel3", + "name": "H", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "B673aU", + "name": "T", + "fill": "$text-primary", + "content": "资源趋势", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "W2knO", + "name": "Sub", + "fill": "$text-muted", + "content": "近 30 分钟 · 每 15s 一个采样点", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "a1KaC", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "ufG4a", + "name": "Chart", + "width": "fill_container", + "height": "fill_container", + "gap": 2, + "alignItems": "end", + "children": [ + { + "type": "rectangle", + "cornerRadius": 2, + "id": "OR3cM", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 37.4 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "z3M4C", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 44.199999999999996 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "V1NRSZ", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 40.8 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "InNxX", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 51 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "Enl9X", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 47.6 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "A0uBcw", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 57.8 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "N0qQM", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 52.699999999999996 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "lE7N4", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 49.3 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "ipUVU", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 64.6 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "Jsu0H", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 74.8 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "LUyMw", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 69.7 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "d8L565", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 61.199999999999996 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "vGE0A", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 56.1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "i9V49", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 66.3 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "irQaf", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 79.89999999999999 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "WAH0F", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 88.39999999999999 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "m1DMN3", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 81.6 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "WfMQO", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 73.1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "OBcaz", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 68 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "NGgEj", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 78.2 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "hDWvk", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 93.5 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "YmlMj", + "name": "B", + "fill": "$warn", + "width": "fill_container", + "height": 105.39999999999999 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "aiYMD", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 98.6 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "DtgvV", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 86.7 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "f2rRI3", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 79.89999999999999 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "OP0xN", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 74.8 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "sK42r", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 83.3 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "hIryg", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 96.89999999999999 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "VMBT5", + "name": "B", + "fill": "$warn", + "width": "fill_container", + "height": 112.2 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "nxVqW", + "name": "B", + "fill": "$warn", + "width": "fill_container", + "height": 122.39999999999999 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "VygfO", + "name": "B", + "fill": "$warn", + "width": "fill_container", + "height": 108.8 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "D7Mc5w", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 93.5 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "uGjDA", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 85 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "iqve7", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 78.2 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "eyaDt", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 73.1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "G6X54", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 81.6 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "iSwv2", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 91.8 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "FoUyQ", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 83.3 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "KPV3B", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 74.8 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "yBzT3", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 69.7 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "OpwBz", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 64.6 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "Q2rWtp", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 71.39999999999999 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "sT0km", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 79.89999999999999 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "hBTXz", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 73.1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "qeagi", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 66.3 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "kswsP", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 61.199999999999996 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "u6nVh", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 56.1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "PB7Hr", + "name": "B", + "fill": "$accent", + "width": "fill_container", + "height": 52.699999999999996 + } + ] + }, + { + "type": "frame", + "id": "VV9zM", + "name": "Axis", + "width": "fill_container", + "justifyContent": "space_between", + "children": [ + { + "type": "text", + "id": "NRh4a", + "name": "T", + "fill": "$text-muted", + "content": "-30m", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "hXSwL", + "name": "T", + "fill": "$text-muted", + "content": "-22m", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ji3tn", + "name": "T", + "fill": "$text-muted", + "content": "-15m", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "U0iHOM", + "name": "T", + "fill": "$text-muted", + "content": "-7m", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "iOJAj", + "name": "T", + "fill": "$text-muted", + "content": "现在", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "T2AH8", + "name": "Card 容器 CPU 占用 Top 5", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 9, + "padding": [ + 12, + 14 + ], + "children": [ + { + "type": "frame", + "id": "pzn0t", + "name": "H", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "XNvtk", + "name": "T", + "fill": "$text-primary", + "content": "容器 CPU 占用 Top 5", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "qi2gf", + "name": "Sub", + "fill": "$text-muted", + "content": "docker stats 流", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "AGam7", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "yrb3W", + "name": "T worker-1", + "width": "fill_container", + "height": 24, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "N4dV3", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 110, + "content": "worker-1", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "p6Umk", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 8, + "fill": "$bg-active", + "cornerRadius": 4, + "children": [ + { + "type": "rectangle", + "cornerRadius": 4, + "id": "p5AMe", + "name": "Fill", + "fill": "$warn", + "width": "fill_container", + "height": 8 + }, + { + "type": "frame", + "id": "z7B4vg", + "name": "Rest", + "width": "fill_container", + "height": 8, + "fill": "#00000000" + } + ] + }, + { + "type": "text", + "id": "dkFIK", + "name": "V", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 52, + "content": "38.4%", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ozR3F", + "name": "T worker-2", + "width": "fill_container", + "height": 24, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "zUhPV", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 110, + "content": "worker-2", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "tBoDy", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 8, + "fill": "$bg-active", + "cornerRadius": 4, + "children": [ + { + "type": "rectangle", + "cornerRadius": 4, + "id": "ZBrPz", + "name": "Fill", + "fill": "$warn", + "width": "fill_container", + "height": 8 + }, + { + "type": "frame", + "id": "Nmvdt", + "name": "Rest", + "width": "fill_container", + "height": 8, + "fill": "#00000000" + } + ] + }, + { + "type": "text", + "id": "p7EsT", + "name": "V", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 52, + "content": "31.7%", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "kRMP8", + "name": "T api-gateway", + "width": "fill_container", + "height": 24, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "MxwPL", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 110, + "content": "api-gateway", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "otxDr", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 8, + "fill": "$bg-active", + "cornerRadius": 4, + "children": [ + { + "type": "rectangle", + "cornerRadius": 4, + "id": "i2O9aR", + "name": "Fill", + "fill": "$accent", + "width": "fill_container", + "height": 8 + }, + { + "type": "frame", + "id": "nkIhk", + "name": "Rest", + "width": "fill_container", + "height": 8, + "fill": "#00000000" + } + ] + }, + { + "type": "text", + "id": "Q9Md0S", + "name": "V", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 52, + "content": "14.6%", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "iFj9f", + "name": "T postgres-main", + "width": "fill_container", + "height": 24, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "kXs7X", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 110, + "content": "postgres-main", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "h7jb5", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 8, + "fill": "$bg-active", + "cornerRadius": 4, + "children": [ + { + "type": "rectangle", + "cornerRadius": 4, + "id": "NUMq9", + "name": "Fill", + "fill": "$accent", + "width": "fill_container", + "height": 8 + }, + { + "type": "frame", + "id": "wa02u", + "name": "Rest", + "width": "fill_container", + "height": 8, + "fill": "#00000000" + } + ] + }, + { + "type": "text", + "id": "h4yIYw", + "name": "V", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 52, + "content": "4.2%", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "UES6Y", + "name": "T nginx-proxy", + "width": "fill_container", + "height": 24, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "B3jNJo", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 110, + "content": "nginx-proxy", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "GkLZM", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 8, + "fill": "$bg-active", + "cornerRadius": 4, + "children": [ + { + "type": "rectangle", + "cornerRadius": 4, + "id": "hCwBy", + "name": "Fill", + "fill": "$accent", + "width": "fill_container", + "height": 8 + }, + { + "type": "frame", + "id": "wPD72", + "name": "Rest", + "width": "fill_container", + "height": 8, + "fill": "#00000000" + } + ] + }, + { + "type": "text", + "id": "U7PCu", + "name": "V", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 52, + "content": "2.1%", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "P3lcC", + "name": "Right", + "width": 420, + "height": "fill_container", + "layout": "vertical", + "gap": 12, + "children": [ + { + "type": "frame", + "id": "Cxr2K", + "name": "Card 实时事件", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 9, + "padding": [ + 12, + 14 + ], + "children": [ + { + "type": "frame", + "id": "UHmxS", + "name": "H", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Yb5N0", + "name": "T", + "fill": "$text-primary", + "content": "实时事件", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "F82rB", + "name": "Sub", + "fill": "$text-muted", + "content": "docker events", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ukIX8", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "SYoMX", + "name": "E 09:41:52", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "srcDT", + "name": "T", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 52, + "content": "09:41:52", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "e9kPwR", + "name": "S", + "fill": "$ok", + "width": 3, + "height": 14 + }, + { + "type": "frame", + "id": "Oa65A", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "fPg6M", + "name": "R", + "width": "fill_container", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "kiNK5", + "name": "Ty", + "fill": "$ok", + "content": "start", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "bBvg2", + "name": "O", + "fill": "$text-secondary", + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "XtuiR", + "name": "M", + "fill": "$text-muted", + "content": "容器已启动", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "oGq3v", + "name": "E 09:41:49", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "X1uIpT", + "name": "T", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 52, + "content": "09:41:49", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "c7o25", + "name": "S", + "fill": "$ok", + "width": 3, + "height": 14 + }, + { + "type": "frame", + "id": "uXJSK", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "ex1WG", + "name": "R", + "width": "fill_container", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "BwYfS", + "name": "Ty", + "fill": "$ok", + "content": "health_status", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Y4pj9X", + "name": "O", + "fill": "$text-secondary", + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "Ufchq", + "name": "M", + "fill": "$text-muted", + "content": "healthy", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "D86b4T", + "name": "E 09:41:44", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "SZnAH", + "name": "T", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 52, + "content": "09:41:44", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "HuYKS", + "name": "S", + "fill": "$accent", + "width": 3, + "height": 14 + }, + { + "type": "frame", + "id": "gakan", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "H7aOR", + "name": "R", + "width": "fill_container", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "XCbaj", + "name": "Ty", + "fill": "$accent", + "content": "create", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "c80Ht", + "name": "O", + "fill": "$text-secondary", + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "g3u47", + "name": "M", + "fill": "$text-muted", + "content": "由 compose up -d 创建", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Od6z3", + "name": "E 09:41:44", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "YwzPk", + "name": "T", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 52, + "content": "09:41:44", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "SUFBo", + "name": "S", + "fill": "$text-muted", + "width": 3, + "height": 14 + }, + { + "type": "frame", + "id": "A3MfLt", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "EpNeB", + "name": "R", + "width": "fill_container", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "YucRw", + "name": "Ty", + "fill": "$text-muted", + "content": "destroy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "HOPdc", + "name": "O", + "fill": "$text-secondary", + "content": "nginx-proxy-old", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "tAyaM", + "name": "M", + "fill": "$text-muted", + "content": "旧容器已移除", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "tz5EX", + "name": "E 09:38:09", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "eSrOc", + "name": "T", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 52, + "content": "09:38:09", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "vczrT", + "name": "S", + "fill": "$violet", + "width": 3, + "height": 14 + }, + { + "type": "frame", + "id": "s7Ogpn", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "riOh3", + "name": "R", + "width": "fill_container", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "a1xSMN", + "name": "Ty", + "fill": "$violet", + "content": "pull", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "X1fNz", + "name": "O", + "fill": "$text-secondary", + "content": "ghcr.io/acme/api:2.8.1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "FP8hg", + "name": "M", + "fill": "$text-muted", + "content": "镜像拉取完成", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "WSM2S", + "name": "E 09:36:12", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dzL4J", + "name": "T", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 52, + "content": "09:36:12", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "CG3TE", + "name": "S", + "fill": "$danger", + "width": 3, + "height": 14 + }, + { + "type": "frame", + "id": "xJo9r", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "aCoPY", + "name": "R", + "width": "fill_container", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Q1W1Gy", + "name": "Ty", + "fill": "$danger", + "content": "die", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "HSD57", + "name": "O", + "fill": "$text-secondary", + "content": "certbot", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "cRGS3", + "name": "M", + "fill": "$text-muted", + "content": "退出码 1", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "N9pbB", + "name": "E 09:31:40", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "DBrG6", + "name": "T", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 52, + "content": "09:31:40", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "Oqgie", + "name": "S", + "fill": "$text-muted", + "width": 3, + "height": 14 + }, + { + "type": "frame", + "id": "vmnxT", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "oSStO", + "name": "R", + "width": "fill_container", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "xwLPZ", + "name": "Ty", + "fill": "$text-muted", + "content": "stop", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "QpNlu", + "name": "O", + "fill": "$text-secondary", + "content": "grafana", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "fDfVz", + "name": "M", + "fill": "$text-muted", + "content": "收到 SIGTERM", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "StOZC", + "name": "E 09:31:38", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "g7xn7x", + "name": "T", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 52, + "content": "09:31:38", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "ceLxK", + "name": "S", + "fill": "$text-muted", + "width": 3, + "height": 14 + }, + { + "type": "frame", + "id": "a99fE", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "L6Dpq", + "name": "R", + "width": "fill_container", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "RxpZk", + "name": "Ty", + "fill": "$text-muted", + "content": "stop", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "GKED6", + "name": "O", + "fill": "$text-secondary", + "content": "loki", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "Axi2v", + "name": "M", + "fill": "$text-muted", + "content": "收到 SIGTERM", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Wpml1", + "name": "E 09:28:02", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "lsndv", + "name": "T", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 52, + "content": "09:28:02", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "tGP2y", + "name": "S", + "fill": "$ok", + "width": 3, + "height": 14 + }, + { + "type": "frame", + "id": "O60erB", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "zrtS9", + "name": "R", + "width": "fill_container", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "E2X7S9", + "name": "Ty", + "fill": "$ok", + "content": "volume/create", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "nzv7s", + "name": "O", + "fill": "$text-secondary", + "content": "nginx-cache", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "IFHO4", + "name": "M", + "fill": "$text-muted", + "content": "本地卷已创建", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "rooyk", + "name": "E 09:27:55", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "kBsnL", + "name": "T", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 52, + "content": "09:27:55", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "dgC0B", + "name": "S", + "fill": "$info", + "width": 3, + "height": 14 + }, + { + "type": "frame", + "id": "e0trI", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "z0PITa", + "name": "R", + "width": "fill_container", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "KPb78", + "name": "Ty", + "fill": "$info", + "content": "network/connect", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "v8mHd", + "name": "O", + "fill": "$text-secondary", + "content": "web-stack_default", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "IDaAk", + "name": "M", + "fill": "$text-muted", + "content": "api-gateway 已接入", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "zXccz", + "name": "E 09:22:31", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "g1fisH", + "name": "T", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 52, + "content": "09:22:31", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "EVstB", + "name": "S", + "fill": "$text-tertiary", + "width": 3, + "height": 14 + }, + { + "type": "frame", + "id": "PJ5k1", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "JerNd", + "name": "R", + "width": "fill_container", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "S7Tojy", + "name": "Ty", + "fill": "$text-tertiary", + "content": "exec_start", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "QXfeU", + "name": "O", + "fill": "$text-secondary", + "content": "postgres-main", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "DTvxa", + "name": "M", + "fill": "$text-muted", + "content": "psql 会话", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Z1F8r", + "name": "E 09:18:04", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "b0yaMV", + "name": "T", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 52, + "content": "09:18:04", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "ctEYs", + "name": "S", + "fill": "$warn", + "width": 3, + "height": 14 + }, + { + "type": "frame", + "id": "Jx8yh", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "huyDb", + "name": "R", + "width": "fill_container", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "K8K1pd", + "name": "Ty", + "fill": "$warn", + "content": "pause", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "SwbTD", + "name": "O", + "fill": "$text-secondary", + "content": "minio", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "o0AyYK", + "name": "M", + "fill": "$text-muted", + "content": "容器已暂停", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "jKe6L", + "name": "Bottom", + "width": "fill_container", + "height": 146, + "gap": 12, + "children": [ + { + "type": "frame", + "id": "DGNq9", + "name": "Card 需要关注", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 8, + "padding": [ + 12, + 14 + ], + "children": [ + { + "type": "frame", + "id": "XqBEr", + "name": "H", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "MX8YL", + "name": "T", + "fill": "$text-primary", + "content": "需要关注", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "Q3JRDC", + "name": "Sub", + "fill": "$text-muted", + "content": "3 项", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "s6c78P", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "wuT9Z", + "name": "A", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "dNWB3", + "name": "I", + "width": 13, + "height": 13, + "icon": "circle-x", + "library": "lucide", + "fill": "$danger" + }, + { + "type": "text", + "id": "jOIzo", + "name": "T", + "fill": "$text-primary", + "content": "certbot 退出码 1", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "Lu6bj", + "name": "D", + "fill": "$text-muted", + "content": "2 小时前 · certbot/certbot:v2.11.0 · 证书续期失败", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "gn5Cd", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "rqLTW", + "name": "A", + "fill": "$accent", + "content": "查看日志", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "mv3Pm", + "name": "A", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "uF7mn", + "name": "I", + "width": 13, + "height": 13, + "icon": "triangle-alert", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "text", + "id": "k6h4Tn", + "name": "T", + "fill": "$text-primary", + "content": "worker-1 CPU 持续高于 30%", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "a36KJt", + "name": "D", + "fill": "$text-muted", + "content": "已持续 12 分钟 · 内存 890 MB / 1 GB 上限", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "n3pkcx", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "hxt5k", + "name": "A", + "fill": "$accent", + "content": "查看统计", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "yG3Uo", + "name": "A", + "clip": true, + "width": "fill_container", + "height": 28, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "E4TUu", + "name": "I", + "width": 13, + "height": 13, + "icon": "clock-alert", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "lrNgb", + "name": "T", + "fill": "$text-primary", + "content": "acme-certs 卷 2 个月未被使用", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "MTCYu", + "name": "D", + "fill": "$text-muted", + "content": "1.8 MB · 无容器挂载", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "i1m78d", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "mcUvw", + "name": "A", + "fill": "$accent", + "content": "去清理", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "baLR3", + "name": "Card 快捷动作", + "clip": true, + "width": 420, + "height": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 8, + "padding": [ + 12, + 14 + ], + "children": [ + { + "type": "frame", + "id": "OzNgN", + "name": "H", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "gyaKz", + "name": "T", + "fill": "$text-primary", + "content": "快捷动作", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "L7G4KB", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "SZyuz", + "name": "QR", + "width": "fill_container", + "gap": 8, + "children": [ + { + "id": "EVtkD", + "type": "ref", + "ref": "i1OTz", + "name": "Q 运行容器", + "width": "fill_container", + "height": 30, + "descendants": { + "RQ8pM": { + "icon": "play" + }, + "P0aUw": { + "content": "运行容器", + "fontSize": 11 + } + } + }, + { + "id": "hXjod", + "type": "ref", + "ref": "i1OTz", + "name": "Q 拉取镜像", + "width": "fill_container", + "height": 30, + "descendants": { + "RQ8pM": { + "icon": "arrow-down-to-line" + }, + "P0aUw": { + "content": "拉取镜像", + "fontSize": 11 + } + } + }, + { + "id": "EmuTo", + "type": "ref", + "ref": "i1OTz", + "name": "Q 新建 Compose", + "width": "fill_container", + "height": 30, + "descendants": { + "RQ8pM": { + "icon": "file-code" + }, + "P0aUw": { + "content": "新建 Compose", + "fontSize": 11 + } + } + } + ] + }, + { + "type": "frame", + "id": "Z2pua3", + "name": "QR", + "width": "fill_container", + "gap": 8, + "children": [ + { + "id": "Ax0Rh", + "type": "ref", + "ref": "i1OTz", + "name": "Q 清理空间", + "width": "fill_container", + "height": 30, + "descendants": { + "RQ8pM": { + "icon": "brush-cleaning" + }, + "P0aUw": { + "content": "清理空间", + "fontSize": 11 + } + } + }, + { + "id": "Nfds2", + "type": "ref", + "ref": "i1OTz", + "name": "Q 打开终端", + "width": "fill_container", + "height": 30, + "descendants": { + "RQ8pM": { + "icon": "terminal" + }, + "P0aUw": { + "content": "打开终端", + "fontSize": 11 + } + } + }, + { + "id": "GnF13", + "type": "ref", + "ref": "i1OTz", + "name": "Q 导出诊断", + "width": "fill_container", + "height": 30, + "descendants": { + "RQ8pM": { + "icon": "file-down" + }, + "P0aUw": { + "content": "导出诊断", + "fontSize": 11 + } + } + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "KjEQI", + "name": "Status Bar", + "width": "fill_container", + "height": 24, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 14, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ayRrg", + "name": "Msg", + "fill": "$text-tertiary", + "content": "事件流已接入 4m12s · 最近事件 3 秒前 · 自动刷新已关闭(由事件驱动)", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "p5k8Du", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "c0B4IC", + "name": "API v1.47", + "fill": "$text-muted", + "content": "API v1.47", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "HhKuY", + "name": "Engine 27.3.1", + "fill": "$text-muted", + "content": "Engine 27.3.1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "bEzy0", + "name": "18 容器 · 34 镜像 · 9 卷", + "fill": "$text-muted", + "content": "18 容器 · 34 镜像 · 9 卷", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "uRe8n", + "name": "事件流 已连接", + "fill": "$text-muted", + "content": "事件流 已连接", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "siub7", + "x": 1059, + "y": 9800, + "name": "12 · 确认闸门 · 一般破坏性", + "theme": { + "mode": "dark" + }, + "clip": true, + "width": 1440, + "height": 860, + "fill": "$bg-page", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "X9NkmN", + "name": "Top Bar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "ry55Y", + "name": "Brand", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "QIFy7", + "name": "Logo", + "width": 17, + "height": 17, + "icon": "container", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "B0xQq", + "name": "Title", + "fill": "$text-primary", + "content": "Docker", + "fontFamily": "$font-ui", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "rectangle", + "id": "GoKfI", + "name": "Divider", + "fill": "$border", + "width": 1, + "height": 18 + }, + { + "type": "frame", + "id": "JOpjk", + "name": "Host Switcher", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "OyP8W", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "server", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "VkN5K", + "name": "Host", + "fill": "$text-primary", + "content": "prod-sg-01", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "aA75B", + "name": "Transport", + "fill": "$text-muted", + "content": "/var/run/docker.sock · SSH 隧道", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "MNzEF", + "name": "Caret", + "width": 12, + "height": 12, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + }, + { + "type": "frame", + "id": "K9nMZT", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "KUp9a", + "name": "Search", + "width": 300, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "qrujb", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "search", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "i0kc3F", + "name": "Placeholder", + "fill": "$text-muted", + "content": "搜索容器、镜像、卷、网络…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Or31O", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "PsJK1", + "name": "Kbd", + "height": 16, + "fill": "$bg-hover", + "cornerRadius": 3, + "padding": [ + 0, + 5 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "CU3dt", + "name": "Keys", + "fill": "$text-tertiary", + "content": "Ctrl K", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "id": "LgiiB", + "type": "ref", + "ref": "x0Pcos", + "name": "Live Badge", + "descendants": { + "ODVko": { + "content": "实时" + } + } + }, + { + "id": "G49fhh", + "type": "ref", + "ref": "yoK0T", + "name": "Settings", + "descendants": { + "XMGqY": { + "icon": "settings" + } + } + } + ] + }, + { + "type": "frame", + "id": "eJ8V7", + "name": "Body", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "qqZu5", + "name": "Rail", + "width": 56, + "height": "fill_container", + "fill": "$bg-rail", + "stroke": "$border", + "strokeWidth": { + "right": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 2, + "padding": [ + 6, + 0 + ], + "alignItems": "center", + "children": [ + { + "id": "eZtIc", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 总览", + "descendants": { + "F1F9E": { + "icon": "layout-dashboard", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "总览", + "fill": "$text-tertiary" + } + } + }, + { + "id": "mTPjO", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 容器", + "descendants": { + "cQhxS": { + "fill": "$accent-dim" + }, + "F1F9E": { + "icon": "box", + "fill": "$accent" + }, + "DE5G9": { + "content": "容器", + "fill": "$accent" + } + } + }, + { + "id": "gpWgr", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 镜像", + "descendants": { + "F1F9E": { + "icon": "layers", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "镜像", + "fill": "$text-tertiary" + } + } + }, + { + "id": "lba6y", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 卷", + "descendants": { + "F1F9E": { + "icon": "hard-drive", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "卷", + "fill": "$text-tertiary" + } + } + }, + { + "id": "L1Tiy", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 网络", + "descendants": { + "F1F9E": { + "icon": "network", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "网络", + "fill": "$text-tertiary" + } + } + }, + { + "id": "p7kehN", + "type": "ref", + "ref": "nKcgr", + "name": "Nav Compose", + "descendants": { + "F1F9E": { + "icon": "boxes", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "Compose", + "fill": "$text-tertiary" + } + } + }, + { + "id": "kftpO", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 系统", + "descendants": { + "F1F9E": { + "icon": "gauge", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "系统", + "fill": "$text-tertiary" + } + } + }, + { + "type": "frame", + "id": "cf0Ba", + "name": "Spacer", + "width": 1, + "height": "fill_container", + "fill": "#00000000" + }, + { + "id": "YDXNN", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 设置", + "descendants": { + "F1F9E": { + "icon": "settings" + }, + "DE5G9": { + "content": "设置" + } + } + } + ] + }, + { + "type": "frame", + "id": "m3STZ", + "name": "Main", + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "w5U27R", + "name": "Toolbar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 9, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "MZ9RQ", + "name": "Title", + "fill": "$text-primary", + "content": "容器", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "text", + "id": "j1jUJY", + "name": "Count", + "fill": "$text-muted", + "content": "18", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "oqfGZ", + "name": "Filter Segmented", + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 2, + "padding": 2, + "children": [ + { + "id": "f3og2", + "type": "ref", + "ref": "MdmRT", + "name": "F 全部 18", + "height": 20, + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "全部 18", + "fontSize": 11, + "fill": "$text-primary" + } + } + }, + { + "id": "BSu4v", + "type": "ref", + "ref": "MdmRT", + "name": "F 运行中 12", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "运行中 12", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + }, + { + "id": "TENBx", + "type": "ref", + "ref": "MdmRT", + "name": "F 已停止 5", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "已停止 5", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + }, + { + "id": "O4BXHE", + "type": "ref", + "ref": "MdmRT", + "name": "F 异常 1", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "异常 1", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + } + ] + }, + { + "type": "frame", + "id": "Q27FFM", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "qPuJC", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 拉取镜像", + "descendants": { + "RQ8pM": { + "icon": "arrow-down-to-line" + }, + "P0aUw": { + "content": "拉取镜像" + } + } + }, + { + "id": "n93bdH", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 新建 Compose", + "descendants": { + "RQ8pM": { + "icon": "file-code" + }, + "P0aUw": { + "content": "新建 Compose" + } + } + }, + { + "id": "sCSfV", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 运行容器", + "fill": "$accent", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "plus", + "fill": "$accent-on" + }, + "P0aUw": { + "content": "运行容器…", + "fill": "$accent-on", + "fontWeight": "500" + } + } + }, + { + "id": "Y0ZQF", + "type": "ref", + "ref": "yoK0T", + "name": "Refresh", + "descendants": { + "XMGqY": { + "icon": "refresh-cw" + } + } + } + ] + }, + { + "type": "frame", + "id": "V4QhMW", + "name": "Selection Bar", + "width": "fill_container", + "height": 32, + "fill": "$accent-dim", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 7, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "do3Cc", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "check", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "N9CsyV", + "name": "Count", + "fill": "$accent", + "content": "已选 2 个容器", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "rectangle", + "id": "KsGZ6", + "name": "Divider", + "fill": "$border-strong", + "width": 1, + "height": 16 + }, + { + "id": "S9yhFN", + "type": "ref", + "ref": "i1OTz", + "name": "S 启动", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "play" + }, + "P0aUw": { + "content": "启动", + "fontSize": 11 + } + } + }, + { + "id": "u7B2X", + "type": "ref", + "ref": "i1OTz", + "name": "S 停止", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "square" + }, + "P0aUw": { + "content": "停止", + "fontSize": 11 + } + } + }, + { + "id": "PxBGI", + "type": "ref", + "ref": "i1OTz", + "name": "S 重启", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "rotate-cw" + }, + "P0aUw": { + "content": "重启", + "fontSize": 11 + } + } + }, + { + "id": "I82dRY", + "type": "ref", + "ref": "i1OTz", + "name": "S 暂停", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "pause" + }, + "P0aUw": { + "content": "暂停", + "fontSize": 11 + } + } + }, + { + "id": "oz0y2", + "type": "ref", + "ref": "i1OTz", + "name": "S 删除", + "height": 22, + "stroke": "$danger-dim", + "descendants": { + "RQ8pM": { + "icon": "trash-2", + "fill": "$danger" + }, + "P0aUw": { + "content": "删除", + "fontSize": 11, + "fill": "$danger" + } + } + }, + { + "type": "frame", + "id": "sFtnn", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "WVEON", + "name": "Clear", + "fill": "$text-tertiary", + "content": "取消选择", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "q1b0iU", + "name": "Header Row", + "width": "fill_container", + "height": 26, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "wNlgF", + "name": "Pad", + "width": 3, + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "dNBjn", + "name": "H Select", + "width": 26, + "height": 26, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "b1iPjX", + "name": "Checkbox", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + } + ] + }, + { + "type": "frame", + "id": "oZqAz", + "name": "H 名称", + "clip": true, + "width": "fill_container", + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "X8bEpv", + "name": "Label", + "fill": "$text-muted", + "content": "名称", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "z172Jh", + "name": "H 镜像", + "clip": true, + "width": 190, + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "JETFo", + "name": "Label", + "fill": "$text-muted", + "content": "镜像", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "cPGnp", + "name": "H 端口", + "clip": true, + "width": 118, + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "XNHjI", + "name": "Label", + "fill": "$text-muted", + "content": "端口", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "p688sF", + "name": "H CPU", + "clip": true, + "width": 108, + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Rwbqv", + "name": "Label", + "fill": "$text-muted", + "content": "CPU", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "ZLA3S", + "name": "H 内存", + "clip": true, + "width": 84, + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "H48KS", + "name": "Label", + "fill": "$text-muted", + "content": "内存", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "saLn7", + "name": "H 运行时长", + "clip": true, + "width": 78, + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "aBfRg", + "name": "Label", + "fill": "$text-muted", + "content": "运行时长", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "E1Pc9X", + "name": "H 操作", + "clip": true, + "width": 92, + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center" + } + ] + }, + { + "type": "frame", + "id": "qFjkv", + "name": "List", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "id": "Jt9IA", + "type": "ref", + "ref": "OHo2s", + "name": "Row nginx-proxy", + "width": "fill_container", + "fill": "$bg-active", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "nginx-proxy", + "fill": "$text-primary" + }, + "VJq0n/ODVko": { + "content": "web-stack" + }, + "p4yL1G": { + "content": "nginx:1.27-alpine", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "80→8080" + }, + "rANgd/fshVw": { + "height": 4, + "fill": "$accent" + }, + "rANgd/srvmw": { + "height": 3, + "fill": "$accent" + }, + "rANgd/ky1P1": { + "height": 5, + "fill": "$accent" + }, + "rANgd/bqMzi": { + "height": 4, + "fill": "$accent" + }, + "rANgd/F5Kloq": { + "height": 6, + "fill": "$accent" + }, + "rANgd/StdGN": { + "height": 4, + "fill": "$accent" + }, + "rANgd/iq3Ux": { + "height": 3, + "fill": "$accent" + }, + "rANgd/PHY2v": { + "height": 5, + "fill": "$accent" + }, + "rANgd/xqaP5": { + "height": 7, + "fill": "$accent" + }, + "rANgd/cZHdc": { + "height": 4, + "fill": "$accent" + }, + "rANgd/jhPqP": { + "height": 3, + "fill": "$accent" + }, + "rANgd/Za8Yw": { + "height": 4, + "fill": "$accent" + }, + "rANgd/zbp8o": { + "height": 5, + "fill": "$accent" + }, + "rANgd/kMt0y": { + "height": 4, + "fill": "$accent" + }, + "NprKu": { + "content": "2.1%", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "128 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "3d 4h" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "Rd1Mg", + "type": "ref", + "ref": "OHo2s", + "name": "Row api-gateway", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "api-gateway", + "fill": "$text-primary" + }, + "VJq0n/ODVko": { + "content": "web-stack" + }, + "p4yL1G": { + "content": "ghcr.io/acme/api:2.8.1", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "8000→8000" + }, + "rANgd/fshVw": { + "height": 6, + "fill": "$accent" + }, + "rANgd/srvmw": { + "height": 8, + "fill": "$accent" + }, + "rANgd/ky1P1": { + "height": 7, + "fill": "$accent" + }, + "rANgd/bqMzi": { + "height": 9, + "fill": "$accent" + }, + "rANgd/F5Kloq": { + "height": 11, + "fill": "$accent" + }, + "rANgd/StdGN": { + "height": 8, + "fill": "$accent" + }, + "rANgd/iq3Ux": { + "height": 6, + "fill": "$accent" + }, + "rANgd/PHY2v": { + "height": 7, + "fill": "$accent" + }, + "rANgd/xqaP5": { + "height": 10, + "fill": "$accent" + }, + "rANgd/cZHdc": { + "height": 12, + "fill": "$accent" + }, + "rANgd/jhPqP": { + "height": 9, + "fill": "$accent" + }, + "rANgd/Za8Yw": { + "height": 7, + "fill": "$accent" + }, + "rANgd/zbp8o": { + "height": 8, + "fill": "$accent" + }, + "rANgd/kMt0y": { + "height": 10, + "fill": "$accent" + }, + "NprKu": { + "content": "14.6%", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "512 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "3d 4h" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "D0CFo", + "type": "ref", + "ref": "OHo2s", + "name": "Row postgres-main", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "postgres-main", + "fill": "$text-primary" + }, + "VJq0n/ODVko": { + "content": "db-stack" + }, + "p4yL1G": { + "content": "postgres:16.4", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "5432" + }, + "rANgd/fshVw": { + "height": 3, + "fill": "$accent" + }, + "rANgd/srvmw": { + "height": 4, + "fill": "$accent" + }, + "rANgd/ky1P1": { + "height": 3, + "fill": "$accent" + }, + "rANgd/bqMzi": { + "height": 5, + "fill": "$accent" + }, + "rANgd/F5Kloq": { + "height": 4, + "fill": "$accent" + }, + "rANgd/StdGN": { + "height": 3, + "fill": "$accent" + }, + "rANgd/iq3Ux": { + "height": 4, + "fill": "$accent" + }, + "rANgd/PHY2v": { + "height": 6, + "fill": "$accent" + }, + "rANgd/xqaP5": { + "height": 4, + "fill": "$accent" + }, + "rANgd/cZHdc": { + "height": 3, + "fill": "$accent" + }, + "rANgd/jhPqP": { + "height": 4, + "fill": "$accent" + }, + "rANgd/Za8Yw": { + "height": 5, + "fill": "$accent" + }, + "rANgd/zbp8o": { + "height": 3, + "fill": "$accent" + }, + "rANgd/kMt0y": { + "height": 4, + "fill": "$accent" + }, + "NprKu": { + "content": "4.2%", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "1.2 GB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "12d 6h" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "R914SH", + "type": "ref", + "ref": "OHo2s", + "name": "Row redis-cache", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "redis-cache", + "fill": "$text-primary" + }, + "VJq0n/ODVko": { + "content": "db-stack" + }, + "p4yL1G": { + "content": "redis:7.4-alpine", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "6379" + }, + "rANgd/fshVw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/srvmw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/ky1P1": { + "height": 3, + "fill": "$accent" + }, + "rANgd/bqMzi": { + "height": 2, + "fill": "$accent" + }, + "rANgd/F5Kloq": { + "height": 2, + "fill": "$accent" + }, + "rANgd/StdGN": { + "height": 3, + "fill": "$accent" + }, + "rANgd/iq3Ux": { + "height": 2, + "fill": "$accent" + }, + "rANgd/PHY2v": { + "height": 2, + "fill": "$accent" + }, + "rANgd/xqaP5": { + "height": 4, + "fill": "$accent" + }, + "rANgd/cZHdc": { + "height": 2, + "fill": "$accent" + }, + "rANgd/jhPqP": { + "height": 2, + "fill": "$accent" + }, + "rANgd/Za8Yw": { + "height": 3, + "fill": "$accent" + }, + "rANgd/zbp8o": { + "height": 2, + "fill": "$accent" + }, + "rANgd/kMt0y": { + "height": 2, + "fill": "$accent" + }, + "NprKu": { + "content": "0.8%", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "64 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "12d 6h" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "q87qGM", + "type": "ref", + "ref": "OHo2s", + "name": "Row worker-1", + "width": "fill_container", + "fill": "$accent-dim", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "T31V2": { + "fill": "$accent", + "stroke": "$accent" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "worker-1", + "fill": "$text-primary" + }, + "VJq0n/ODVko": { + "content": "jobs" + }, + "p4yL1G": { + "content": "ghcr.io/acme/worker:2.8.1", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "—" + }, + "rANgd/fshVw": { + "height": 9, + "fill": "$warn" + }, + "rANgd/srvmw": { + "height": 12, + "fill": "$warn" + }, + "rANgd/ky1P1": { + "height": 14, + "fill": "$warn" + }, + "rANgd/bqMzi": { + "height": 11, + "fill": "$warn" + }, + "rANgd/F5Kloq": { + "height": 15, + "fill": "$warn" + }, + "rANgd/StdGN": { + "height": 13, + "fill": "$warn" + }, + "rANgd/iq3Ux": { + "height": 10, + "fill": "$warn" + }, + "rANgd/PHY2v": { + "height": 14, + "fill": "$warn" + }, + "rANgd/xqaP5": { + "height": 15, + "fill": "$warn" + }, + "rANgd/cZHdc": { + "height": 12, + "fill": "$warn" + }, + "rANgd/jhPqP": { + "height": 13, + "fill": "$warn" + }, + "rANgd/Za8Yw": { + "height": 15, + "fill": "$warn" + }, + "rANgd/zbp8o": { + "height": 11, + "fill": "$warn" + }, + "rANgd/kMt0y": { + "height": 14, + "fill": "$warn" + }, + "NprKu": { + "content": "38.4%", + "fill": "$warn" + }, + "AdR38": { + "content": "890 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "6h 12m" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "W6XPqp", + "type": "ref", + "ref": "OHo2s", + "name": "Row worker-2", + "width": "fill_container", + "fill": "$accent-dim", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "T31V2": { + "fill": "$accent", + "stroke": "$accent" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "worker-2", + "fill": "$text-primary" + }, + "VJq0n/ODVko": { + "content": "jobs" + }, + "p4yL1G": { + "content": "ghcr.io/acme/worker:2.8.1", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "—" + }, + "rANgd/fshVw": { + "height": 8, + "fill": "$warn" + }, + "rANgd/srvmw": { + "height": 11, + "fill": "$warn" + }, + "rANgd/ky1P1": { + "height": 13, + "fill": "$warn" + }, + "rANgd/bqMzi": { + "height": 10, + "fill": "$warn" + }, + "rANgd/F5Kloq": { + "height": 12, + "fill": "$warn" + }, + "rANgd/StdGN": { + "height": 14, + "fill": "$warn" + }, + "rANgd/iq3Ux": { + "height": 9, + "fill": "$warn" + }, + "rANgd/PHY2v": { + "height": 11, + "fill": "$warn" + }, + "rANgd/xqaP5": { + "height": 13, + "fill": "$warn" + }, + "rANgd/cZHdc": { + "height": 10, + "fill": "$warn" + }, + "rANgd/jhPqP": { + "height": 12, + "fill": "$warn" + }, + "rANgd/Za8Yw": { + "height": 11, + "fill": "$warn" + }, + "rANgd/zbp8o": { + "height": 13, + "fill": "$warn" + }, + "rANgd/kMt0y": { + "height": 10, + "fill": "$warn" + }, + "NprKu": { + "content": "31.7%", + "fill": "$warn" + }, + "AdR38": { + "content": "812 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "6h 12m" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "SmDrD", + "type": "ref", + "ref": "OHo2s", + "name": "Row minio", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$warn" + }, + "yMbTn": { + "fill": "$warn" + }, + "L6tp5": { + "content": "minio", + "fill": "$text-primary" + }, + "VJq0n": { + "enabled": false + }, + "p4yL1G": { + "content": "minio/minio:RELEASE.2024-09", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "9000→9000" + }, + "rANgd": { + "enabled": false + }, + "NprKu": { + "content": "已暂停", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "210 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "2d 1h" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "piF7Z", + "type": "ref", + "ref": "OHo2s", + "name": "Row grafana", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$text-muted" + }, + "yMbTn": { + "fill": "$text-muted" + }, + "L6tp5": { + "content": "grafana", + "fill": "$text-tertiary" + }, + "VJq0n/ODVko": { + "content": "observability" + }, + "p4yL1G": { + "content": "grafana/grafana:11.2.0", + "fill": "$text-muted" + }, + "y2nlk": { + "content": "3000" + }, + "rANgd": { + "enabled": false + }, + "NprKu": { + "content": "—", + "fill": "$text-muted" + }, + "AdR38": { + "content": "—", + "fill": "$text-muted" + }, + "PeLHA": { + "content": "停止 5h" + }, + "ALIhr/XMGqY": { + "icon": "play" + } + } + }, + { + "id": "d1mE0F", + "type": "ref", + "ref": "OHo2s", + "name": "Row loki", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$text-muted" + }, + "yMbTn": { + "fill": "$text-muted" + }, + "L6tp5": { + "content": "loki", + "fill": "$text-tertiary" + }, + "VJq0n/ODVko": { + "content": "observability" + }, + "p4yL1G": { + "content": "grafana/loki:3.1.1", + "fill": "$text-muted" + }, + "y2nlk": { + "content": "3100" + }, + "rANgd": { + "enabled": false + }, + "NprKu": { + "content": "—", + "fill": "$text-muted" + }, + "AdR38": { + "content": "—", + "fill": "$text-muted" + }, + "PeLHA": { + "content": "停止 5h" + }, + "ALIhr/XMGqY": { + "icon": "play" + } + } + }, + { + "id": "F685np", + "type": "ref", + "ref": "OHo2s", + "name": "Row certbot", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$danger" + }, + "yMbTn": { + "fill": "$danger" + }, + "L6tp5": { + "content": "certbot", + "fill": "$text-tertiary" + }, + "VJq0n": { + "enabled": false + }, + "p4yL1G": { + "content": "certbot/certbot:v2.11.0", + "fill": "$text-muted" + }, + "y2nlk": { + "content": "—" + }, + "rANgd": { + "enabled": false + }, + "NprKu": { + "content": "—", + "fill": "$text-muted" + }, + "AdR38": { + "content": "—", + "fill": "$text-muted" + }, + "PeLHA": { + "content": "退出 (1)" + }, + "ALIhr/XMGqY": { + "icon": "play" + } + } + }, + { + "id": "fmYsZ", + "type": "ref", + "ref": "OHo2s", + "name": "Row registry-mirror", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "registry-mirror", + "fill": "$text-primary" + }, + "VJq0n": { + "enabled": false + }, + "p4yL1G": { + "content": "registry:2.8.3", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "5000→5000" + }, + "rANgd/fshVw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/srvmw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/ky1P1": { + "height": 2, + "fill": "$accent" + }, + "rANgd/bqMzi": { + "height": 3, + "fill": "$accent" + }, + "rANgd/F5Kloq": { + "height": 2, + "fill": "$accent" + }, + "rANgd/StdGN": { + "height": 2, + "fill": "$accent" + }, + "rANgd/iq3Ux": { + "height": 2, + "fill": "$accent" + }, + "rANgd/PHY2v": { + "height": 2, + "fill": "$accent" + }, + "rANgd/xqaP5": { + "height": 3, + "fill": "$accent" + }, + "rANgd/cZHdc": { + "height": 2, + "fill": "$accent" + }, + "rANgd/jhPqP": { + "height": 2, + "fill": "$accent" + }, + "rANgd/Za8Yw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/zbp8o": { + "height": 2, + "fill": "$accent" + }, + "rANgd/kMt0y": { + "height": 2, + "fill": "$accent" + }, + "NprKu": { + "content": "0.4%", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "48 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "21d" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "gnoYF", + "type": "ref", + "ref": "OHo2s", + "name": "Row watchtower", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "watchtower", + "fill": "$text-primary" + }, + "VJq0n": { + "enabled": false + }, + "p4yL1G": { + "content": "containrrr/watchtower:1.7.1", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "—" + }, + "rANgd/fshVw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/srvmw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/ky1P1": { + "height": 2, + "fill": "$accent" + }, + "rANgd/bqMzi": { + "height": 2, + "fill": "$accent" + }, + "rANgd/F5Kloq": { + "height": 2, + "fill": "$accent" + }, + "rANgd/StdGN": { + "height": 2, + "fill": "$accent" + }, + "rANgd/iq3Ux": { + "height": 2, + "fill": "$accent" + }, + "rANgd/PHY2v": { + "height": 2, + "fill": "$accent" + }, + "rANgd/xqaP5": { + "height": 2, + "fill": "$accent" + }, + "rANgd/cZHdc": { + "height": 2, + "fill": "$accent" + }, + "rANgd/jhPqP": { + "height": 2, + "fill": "$accent" + }, + "rANgd/Za8Yw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/zbp8o": { + "height": 2, + "fill": "$accent" + }, + "rANgd/kMt0y": { + "height": 2, + "fill": "$accent" + }, + "NprKu": { + "content": "0.1%", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "22 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "21d" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "o4ohHp", + "name": "Detail Drawer", + "width": 440, + "height": "fill_container", + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "left": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "wHnKP", + "name": "Drawer Header", + "width": "fill_container", + "height": 44, + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "YuqXq", + "name": "Dot", + "fill": "$ok", + "width": 7, + "height": 7 + }, + { + "type": "text", + "id": "H7tLN", + "name": "Name", + "fill": "$text-primary", + "content": "nginx-proxy", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "id": "W1KApi", + "type": "ref", + "ref": "x0Pcos", + "name": "State Chip", + "descendants": { + "ODVko": { + "content": "运行中 · healthy" + } + } + }, + { + "type": "frame", + "id": "F0kiQ", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "To1ki", + "type": "ref", + "ref": "yoK0T", + "name": "H pin", + "descendants": { + "XMGqY": { + "icon": "pin" + } + } + }, + { + "id": "RlSO4", + "type": "ref", + "ref": "yoK0T", + "name": "H maximize-2", + "descendants": { + "XMGqY": { + "icon": "maximize-2" + } + } + }, + { + "id": "WCTQR", + "type": "ref", + "ref": "yoK0T", + "name": "H x", + "descendants": { + "XMGqY": { + "icon": "x" + } + } + } + ] + }, + { + "type": "frame", + "id": "hmhLx", + "name": "Drawer Subhead", + "width": "fill_container", + "height": 26, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 7, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "oi6wj", + "name": "Id", + "fill": "$text-tertiary", + "content": "a3f2c81b9d4e", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "W4avFB", + "name": "Copy", + "width": 11, + "height": 11, + "icon": "copy", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "QJfbM", + "name": "Sep", + "fill": "$text-muted", + "content": "·", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "L7lymp", + "name": "Proj", + "fill": "$violet", + "content": "web-stack / proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Hm7xV", + "name": "Drawer Tabs", + "width": "fill_container", + "height": 34, + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 2, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "id": "N14Ld", + "type": "ref", + "ref": "MdmRT", + "name": "T 概览", + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "icon": "info", + "fill": "$text-primary" + }, + "iscyV": { + "content": "概览", + "fill": "$text-primary", + "fontWeight": "500" + } + } + }, + { + "id": "ytcel", + "type": "ref", + "ref": "MdmRT", + "name": "T 日志", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "scroll-text", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "日志", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + }, + { + "id": "D8QXEJ", + "type": "ref", + "ref": "MdmRT", + "name": "T 文件", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "folder-tree", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "文件", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + }, + { + "id": "hBDIr", + "type": "ref", + "ref": "MdmRT", + "name": "T 终端", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "terminal", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "终端", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + }, + { + "id": "qenLo", + "type": "ref", + "ref": "MdmRT", + "name": "T 统计", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "activity", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "统计", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + } + ] + }, + { + "type": "frame", + "id": "ApW48", + "name": "Quick Actions", + "width": "fill_container", + "height": 42, + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "id": "pzQbR", + "type": "ref", + "ref": "i1OTz", + "name": "Q 停止", + "descendants": { + "RQ8pM": { + "icon": "square" + }, + "P0aUw": { + "content": "停止" + } + } + }, + { + "id": "WS89n", + "type": "ref", + "ref": "i1OTz", + "name": "Q 重启", + "descendants": { + "RQ8pM": { + "icon": "rotate-cw" + }, + "P0aUw": { + "content": "重启" + } + } + }, + { + "id": "L3Lgn", + "type": "ref", + "ref": "i1OTz", + "name": "Q 暂停", + "descendants": { + "RQ8pM": { + "icon": "pause" + }, + "P0aUw": { + "content": "暂停" + } + } + }, + { + "id": "M7pi5I", + "type": "ref", + "ref": "i1OTz", + "name": "Q 终端", + "fill": "$accent-dim", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "terminal", + "fill": "$accent" + }, + "P0aUw": { + "content": "进入终端", + "fill": "$accent" + } + } + }, + { + "type": "frame", + "id": "zlrAq", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "kVWwS", + "type": "ref", + "ref": "yoK0T", + "name": "More", + "descendants": { + "XMGqY": { + "icon": "ellipsis" + } + } + } + ] + }, + { + "type": "frame", + "id": "i2yST", + "name": "Drawer Content", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-elevated", + "layout": "vertical", + "gap": 10, + "padding": [ + 12, + 14 + ], + "children": [ + { + "type": "frame", + "id": "vQrEg", + "name": "Live Metrics", + "width": "fill_container", + "gap": 10, + "children": [ + { + "type": "frame", + "id": "G1T4Dn", + "name": "M CPU", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 6, + "padding": [ + 10, + 11 + ], + "children": [ + { + "type": "frame", + "id": "bzHYt", + "name": "Top", + "width": "fill_container", + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "O0OqC3", + "name": "Label", + "fill": "$text-tertiary", + "content": "CPU", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "dl5bJ", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "ktmmK", + "name": "Delta", + "fill": "$text-muted", + "content": "峰值 6.4%", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ypze9", + "name": "Value", + "gap": 3, + "alignItems": "end", + "children": [ + { + "type": "text", + "id": "nmHf0", + "name": "Num", + "fill": "$text-primary", + "content": "2.1", + "fontFamily": "$font-ui", + "fontSize": 20, + "fontWeight": "600" + }, + { + "type": "text", + "id": "H6oFNQ", + "name": "Unit", + "fill": "$text-tertiary", + "content": "%", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "S4ky0j", + "type": "ref", + "ref": "MhYIL", + "name": "Spark", + "height": 30, + "descendants": { + "fshVw": { + "height": 8, + "width": 9, + "fill": "$accent" + }, + "srvmw": { + "height": 6, + "width": 9, + "fill": "$accent" + }, + "ky1P1": { + "height": 11, + "width": 9, + "fill": "$accent" + }, + "bqMzi": { + "height": 7, + "width": 9, + "fill": "$accent" + }, + "F5Kloq": { + "height": 14, + "width": 9, + "fill": "$accent" + }, + "StdGN": { + "height": 9, + "width": 9, + "fill": "$accent" + }, + "iq3Ux": { + "height": 6, + "width": 9, + "fill": "$accent" + }, + "PHY2v": { + "height": 10, + "width": 9, + "fill": "$accent" + }, + "xqaP5": { + "height": 18, + "width": 9, + "fill": "$accent" + }, + "cZHdc": { + "height": 8, + "width": 9, + "fill": "$accent" + }, + "jhPqP": { + "height": 7, + "width": 9, + "fill": "$accent" + }, + "Za8Yw": { + "height": 9, + "width": 9, + "fill": "$accent" + }, + "zbp8o": { + "height": 12, + "width": 9, + "fill": "$accent" + }, + "kMt0y": { + "height": 8, + "width": 9, + "fill": "$accent" + } + } + } + ] + }, + { + "type": "frame", + "id": "fChra", + "name": "M 内存", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 6, + "padding": [ + 10, + 11 + ], + "children": [ + { + "type": "frame", + "id": "YLkvD", + "name": "Top", + "width": "fill_container", + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "jSAO0", + "name": "Label", + "fill": "$text-tertiary", + "content": "内存", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "WxPV0", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Wg7It", + "name": "Delta", + "fill": "$text-muted", + "content": "24.9%", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "oFV9e", + "name": "Value", + "gap": 3, + "alignItems": "end", + "children": [ + { + "type": "text", + "id": "dOgeP", + "name": "Num", + "fill": "$text-primary", + "content": "128", + "fontFamily": "$font-ui", + "fontSize": 20, + "fontWeight": "600" + }, + { + "type": "text", + "id": "u0VKvV", + "name": "Unit", + "fill": "$text-tertiary", + "content": "MB / 512", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "v5214F", + "type": "ref", + "ref": "MhYIL", + "name": "Spark", + "height": 30, + "descendants": { + "fshVw": { + "height": 11, + "width": 7, + "fill": "$ok" + }, + "srvmw": { + "height": 13, + "width": 7, + "fill": "$ok" + }, + "ky1P1": { + "height": 12, + "width": 7, + "fill": "$ok" + }, + "bqMzi": { + "height": 15, + "width": 7, + "fill": "$ok" + }, + "F5Kloq": { + "height": 14, + "width": 7, + "fill": "$ok" + }, + "StdGN": { + "height": 16, + "width": 7, + "fill": "$ok" + }, + "iq3Ux": { + "height": 15, + "width": 7, + "fill": "$ok" + }, + "PHY2v": { + "height": 18, + "width": 7, + "fill": "$ok" + }, + "xqaP5": { + "height": 17, + "width": 7, + "fill": "$ok" + }, + "cZHdc": { + "height": 16, + "width": 7, + "fill": "$ok" + }, + "jhPqP": { + "height": 19, + "width": 7, + "fill": "$ok" + }, + "Za8Yw": { + "height": 18, + "width": 7, + "fill": "$ok" + }, + "zbp8o": { + "height": 20, + "width": 7, + "fill": "$ok" + }, + "kMt0y": { + "height": 19, + "width": 7, + "fill": "$ok" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "ZeylJ", + "name": "Sec 基本信息", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "HUoZA", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "jWjkI", + "name": "Title", + "fill": "$text-secondary", + "content": "基本信息", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "SGcnM", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "NWPh0", + "name": "Action", + "fill": "$accent", + "content": "完整 inspect", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "Usld8", + "type": "ref", + "ref": "l3qo4", + "name": "KV 镜像", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "镜像" + }, + "psgr6": { + "content": "nginx:1.27-alpine", + "fill": "$text-primary" + } + } + }, + { + "id": "p9hu7", + "type": "ref", + "ref": "l3qo4", + "name": "KV 镜像摘要", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "镜像摘要" + }, + "psgr6": { + "content": "sha256:9c4f2e…1b7a", + "fill": "$text-primary" + } + } + }, + { + "id": "Z2zuH", + "type": "ref", + "ref": "l3qo4", + "name": "KV 创建于", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "创建于" + }, + "psgr6": { + "content": "2026-08-18 09:12:44", + "fill": "$text-primary" + } + } + }, + { + "id": "f6isTG", + "type": "ref", + "ref": "l3qo4", + "name": "KV 启动于", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "启动于" + }, + "psgr6": { + "content": "3 天前 (3d 4h 21m)", + "fill": "$text-primary" + } + } + }, + { + "id": "ZMDHM", + "type": "ref", + "ref": "l3qo4", + "name": "KV 重启策略", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "重启策略" + }, + "psgr6": { + "content": "unless-stopped", + "fill": "$text-primary" + } + } + }, + { + "id": "B6jKq", + "type": "ref", + "ref": "l3qo4", + "name": "KV 平台", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "平台" + }, + "psgr6": { + "content": "linux/amd64", + "fill": "$text-primary" + } + } + }, + { + "id": "NBl7W", + "type": "ref", + "ref": "l3qo4", + "name": "KV 命令", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "命令" + }, + "psgr6": { + "content": "nginx -g 'daemon off;'", + "fill": "$text-primary" + } + } + } + ] + }, + { + "type": "rectangle", + "id": "NdLt7", + "name": "Divider", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "JuJ4X", + "name": "Sec 端口映射", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "EIYj2", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "y0JmXv", + "name": "Title", + "fill": "$text-secondary", + "content": "端口映射", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "tiyGn", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "g3UGlb", + "name": "Action", + "fill": "$accent", + "content": "编辑", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "dNPy7", + "type": "ref", + "ref": "l3qo4", + "name": "KV 8080/tcp", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "8080/tcp" + }, + "psgr6": { + "content": "0.0.0.0:8080 → 80", + "fill": "$info" + } + } + }, + { + "id": "PTkab", + "type": "ref", + "ref": "l3qo4", + "name": "KV 8443/tcp", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "8443/tcp" + }, + "psgr6": { + "content": "[::]:8443 → 443", + "fill": "$info" + } + } + } + ] + }, + { + "type": "rectangle", + "id": "zdnG0", + "name": "Divider", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "k9IzWj", + "name": "Sec 挂载", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "jKGR4", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Frmr4", + "name": "Title", + "fill": "$text-secondary", + "content": "挂载", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "qoFme", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "wO8nz", + "name": "Action", + "fill": "$accent", + "content": "浏览文件", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "qfKc7", + "name": "Mount", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "u0NB5", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "folder", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "yMHNM", + "name": "Path", + "fill": "$text-secondary", + "content": "/srv/nginx/conf.d → /etc/nginx/conf.d", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "U3fhc", + "type": "ref", + "ref": "x0Pcos", + "name": "Tag", + "height": 15, + "fill": "#00000000", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "只读", + "fill": "$warn", + "fontSize": 9 + } + } + } + ] + }, + { + "type": "frame", + "id": "agcXz", + "name": "Mount", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "pLAH4", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "database", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "TbbFH", + "name": "Path", + "fill": "$text-secondary", + "content": "nginx-cache → /var/cache/nginx", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "t9i7Gh", + "type": "ref", + "ref": "x0Pcos", + "name": "Tag", + "height": 15, + "fill": "#00000000", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "读写", + "fill": "$ok", + "fontSize": 9 + } + } + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "wafj0", + "name": "Divider", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "AQSue", + "name": "Sec 网络", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "UgGKx", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "OwVyk", + "name": "Title", + "fill": "$text-secondary", + "content": "网络", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "UB13E", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "QhT8X", + "name": "Action", + "fill": "$accent", + "content": "接入其他网络", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "fruRQ", + "type": "ref", + "ref": "l3qo4", + "name": "KV web-stack_default", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "网络" + }, + "psgr6": { + "content": "web-stack_default", + "fill": "$text-primary" + } + } + }, + { + "id": "qvCcA", + "type": "ref", + "ref": "l3qo4", + "name": "KV IP", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "IP 地址" + }, + "psgr6": { + "content": "172.20.0.4 · bridge · MTU 1500" + } + } + } + ] + }, + { + "type": "rectangle", + "id": "OomOh", + "name": "Divider", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "CCtw1", + "name": "Sec 健康检查", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "t3rZGX", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "f7WqEM", + "name": "Title", + "fill": "$text-secondary", + "content": "健康检查", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "rdiWw", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "OTA8i", + "name": "HC", + "width": "fill_container", + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "id": "o3cjQ", + "type": "ref", + "ref": "x0Pcos", + "name": "HC Chip", + "descendants": { + "ODVko": { + "content": "healthy" + } + } + }, + { + "type": "text", + "id": "dJ2jl", + "name": "Detail", + "fill": "$text-tertiary", + "content": "每 30s · 0 次失败 · 12s 前", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "W5Ry7", + "layoutPosition": "absolute", + "x": 0, + "y": 0, + "name": "Scrim", + "fill": "#05070ACC", + "width": 1440, + "height": 796 + }, + { + "type": "frame", + "id": "DmP62", + "layoutPosition": "absolute", + "x": 452, + "y": 150, + "name": "Gate", + "clip": true, + "width": 560, + "fill": "$bg-elevated", + "cornerRadius": "$r-lg", + "stroke": "$border-strong", + "strokeWidth": 1, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000080", + "offset": { + "x": 0, + "y": 12 + }, + "blur": 36 + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "aUyq4", + "name": "H", + "width": "fill_container", + "height": 52, + "fill": "$danger-dim", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 10, + "padding": [ + 0, + 18 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "Y1pUSM", + "name": "I", + "width": 17, + "height": 17, + "icon": "trash-2", + "library": "lucide", + "fill": "$danger" + }, + { + "type": "text", + "id": "H3A7IS", + "name": "T", + "fill": "$text-primary", + "content": "删除 2 个容器?", + "fontFamily": "$font-ui", + "fontSize": 15, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "P1CN3k", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "GsCqm", + "type": "ref", + "ref": "yoK0T", + "name": "X", + "descendants": { + "XMGqY": { + "icon": "x" + } + } + } + ] + }, + { + "type": "frame", + "id": "umCrL", + "name": "Host", + "width": "fill_container", + "height": 34, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 18 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "nSihj", + "name": "I", + "width": 13, + "height": 13, + "icon": "server", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "W66DS", + "name": "L", + "fill": "$text-tertiary", + "content": "目标主机", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "KZiPe", + "name": "H", + "fill": "$text-primary", + "content": "prod-sg-01", + "fontFamily": "$font-mono", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "VSU3B", + "name": "P", + "fill": "$text-muted", + "content": "· 10.24.8.11 · /var/run/docker.sock(SSH 隧道)", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "m2qINL", + "name": "Body", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": [ + 16, + 18 + ], + "children": [ + { + "type": "text", + "id": "q4v7D", + "name": "L", + "fill": "$text-secondary", + "content": "将要执行", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "FqBMz", + "name": "Cmd", + "width": "fill_container", + "fill": "$bg-terminal", + "cornerRadius": "$r-sm", + "layout": "vertical", + "gap": 3, + "padding": [ + 10, + 12 + ], + "children": [ + { + "type": "text", + "id": "Dc3fh", + "name": "C", + "fill": "$text-terminal", + "content": "DELETE /containers/worker-1?v=false&force=false", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "c0Wfs", + "name": "C", + "fill": "$text-terminal", + "content": "DELETE /containers/worker-2?v=false&force=false", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "JovQH", + "name": "N", + "fill": "$text-muted", + "content": "等价于 docker rm worker-1 worker-2", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "s486z", + "name": "L", + "fill": "$text-secondary", + "content": "目标", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "u3GdpU", + "name": "Targets", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "Y1Q4G", + "name": "T worker-1", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "JleA2", + "name": "D", + "fill": "$text-muted", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "roSnW", + "name": "N", + "fill": "$text-primary", + "content": "worker-1", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "hhPW5", + "name": "I", + "fill": "$text-muted", + "content": "ghcr.io/acme/worker:2.8.1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "grmju", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "SYdCw", + "name": "St", + "fill": "$text-tertiary", + "content": "已停止 · 6h 前退出 (0)", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ImCK9", + "name": "T worker-2", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "E8j1H", + "name": "D", + "fill": "$text-muted", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "EAF42", + "name": "N", + "fill": "$text-primary", + "content": "worker-2", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "I3gb2", + "name": "I", + "fill": "$text-muted", + "content": "ghcr.io/acme/worker:2.8.1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "R59DGv", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "JZHL5", + "name": "St", + "fill": "$text-tertiary", + "content": "已停止 · 6h 前退出 (0)", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "text", + "id": "Beg6t", + "name": "L", + "fill": "$text-secondary", + "content": "后果", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "Ztoir", + "name": "Consequences", + "width": "fill_container", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "frame", + "id": "IkIcn", + "name": "C", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "icon", + "id": "Sriq8", + "name": "I", + "width": 13, + "height": 13, + "icon": "circle-x", + "library": "lucide", + "fill": "$danger" + }, + { + "type": "text", + "id": "F6vot", + "name": "T", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "容器及其可写层会被删除,写在容器里(不在卷里)的数据丢失。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "N3Wck", + "name": "C", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "icon", + "id": "p7oN5", + "name": "I", + "width": 13, + "height": 13, + "icon": "shield-check", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "ulrIA", + "name": "T", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "挂载的命名卷不受影响 —— 本次不带 -v。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "yxige", + "name": "C", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "icon", + "id": "cG9Lk", + "name": "I", + "width": 13, + "height": 13, + "icon": "info", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "I06YAE", + "name": "T", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "它们属于 compose 项目 jobs,下次 up -d 会按 compose.yaml 重建。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "FwwCV", + "name": "Foot", + "width": "fill_container", + "height": 56, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 9, + "padding": [ + 0, + 18 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "KzBlZ", + "name": "I", + "width": 13, + "height": 13, + "icon": "keyboard", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "M3ZV4w", + "name": "K", + "fill": "$text-muted", + "content": "Esc 取消", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "L9gPt", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "bEmjr", + "type": "ref", + "ref": "i1OTz", + "name": "Cancel", + "height": 30, + "descendants": { + "RQ8pM": { + "enabled": false + }, + "P0aUw": { + "content": "取消", + "fontSize": 12 + } + } + }, + { + "id": "n1CtNf", + "type": "ref", + "ref": "i1OTz", + "name": "Confirm", + "height": 30, + "fill": "$danger", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "trash-2", + "fill": "#FFFFFF" + }, + "P0aUw": { + "content": "删除 2 个容器", + "fill": "#FFFFFF", + "fontWeight": "500", + "fontSize": 12 + } + } + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "ENbC5", + "name": "Status Bar", + "width": "fill_container", + "height": 24, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 14, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "S42iMC", + "name": "Msg", + "fill": "$text-tertiary", + "content": "等待确认 · 未执行任何动作", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "jD1ox", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "sghhb", + "name": "API v1.47", + "fill": "$text-muted", + "content": "API v1.47", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "hQ4wm", + "name": "Engine 27.3.1", + "fill": "$text-muted", + "content": "Engine 27.3.1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "xJalf", + "name": "18 容器 · 34 镜像 · 9 卷", + "fill": "$text-muted", + "content": "18 容器 · 34 镜像 · 9 卷", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "SViB9", + "name": "事件流 已连接", + "fill": "$text-muted", + "content": "事件流 已连接", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Mwn6O", + "x": 1059, + "y": 10780, + "name": "13 · 确认闸门 · 会丢数据(需手打)", + "theme": { + "mode": "dark" + }, + "clip": true, + "width": 1440, + "height": 860, + "fill": "$bg-page", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "bzE10", + "name": "Top Bar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "N1HE1", + "name": "Brand", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "J8OKRm", + "name": "Logo", + "width": 17, + "height": 17, + "icon": "container", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "q8nJof", + "name": "Title", + "fill": "$text-primary", + "content": "Docker", + "fontFamily": "$font-ui", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "rectangle", + "id": "A74Px", + "name": "Divider", + "fill": "$border", + "width": 1, + "height": 18 + }, + { + "type": "frame", + "id": "jwNz4", + "name": "Host Switcher", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "u5tSiS", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "server", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "ZjlqK", + "name": "Host", + "fill": "$text-primary", + "content": "prod-sg-01", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "Alp7t", + "name": "Transport", + "fill": "$text-muted", + "content": "/var/run/docker.sock · SSH 隧道", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "hsUQK", + "name": "Caret", + "width": 12, + "height": 12, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + }, + { + "type": "frame", + "id": "kvRQk", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "Hxv4T", + "name": "Search", + "width": 300, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "HFfnx", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "search", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "Hwokq", + "name": "Placeholder", + "fill": "$text-muted", + "content": "搜索容器、镜像、卷、网络…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "M8mzS1", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "eK8hl", + "name": "Kbd", + "height": 16, + "fill": "$bg-hover", + "cornerRadius": 3, + "padding": [ + 0, + 5 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "C7BnFL", + "name": "Keys", + "fill": "$text-tertiary", + "content": "Ctrl K", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "id": "TorHy", + "type": "ref", + "ref": "x0Pcos", + "name": "Live Badge", + "descendants": { + "ODVko": { + "content": "实时" + } + } + }, + { + "id": "p7Ec9", + "type": "ref", + "ref": "yoK0T", + "name": "Settings", + "descendants": { + "XMGqY": { + "icon": "settings" + } + } + } + ] + }, + { + "type": "frame", + "id": "DMmm9", + "name": "Body", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "HtdaB", + "name": "Rail", + "width": 56, + "height": "fill_container", + "fill": "$bg-rail", + "stroke": "$border", + "strokeWidth": { + "right": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 2, + "padding": [ + 6, + 0 + ], + "alignItems": "center", + "children": [ + { + "id": "l9LhL", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 总览", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "layout-dashboard", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "总览", + "fill": "$text-tertiary" + } + } + }, + { + "id": "WtWb9", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 容器", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "box", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "容器", + "fill": "$text-tertiary" + } + } + }, + { + "id": "XR1tY", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 镜像", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "layers", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "镜像", + "fill": "$text-tertiary" + } + } + }, + { + "id": "m8L2m", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 卷", + "descendants": { + "cQhxS": { + "fill": "$accent-dim" + }, + "F1F9E": { + "icon": "hard-drive", + "fill": "$accent" + }, + "DE5G9": { + "content": "卷", + "fill": "$accent" + } + } + }, + { + "id": "VK6hJ", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 网络", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "network", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "网络", + "fill": "$text-tertiary" + } + } + }, + { + "id": "J26Xmw", + "type": "ref", + "ref": "nKcgr", + "name": "Nav Compose", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "boxes", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "Compose", + "fill": "$text-tertiary" + } + } + }, + { + "id": "VnlGQ", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 系统", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "gauge", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "系统", + "fill": "$text-tertiary" + } + } + }, + { + "type": "frame", + "id": "rs2pH", + "name": "Spacer", + "width": 1, + "height": "fill_container", + "fill": "#00000000" + }, + { + "id": "GyMYm", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 设置", + "descendants": { + "F1F9E": { + "icon": "settings" + }, + "DE5G9": { + "content": "设置" + } + } + } + ] + }, + { + "type": "frame", + "id": "kW5j7", + "name": "Main", + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "S0ICp", + "name": "Toolbar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 9, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "oJl8e", + "name": "Title", + "fill": "$text-primary", + "content": "卷", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "text", + "id": "DDXrI", + "name": "Count", + "fill": "$text-muted", + "content": "9", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Iu7Uu", + "name": "Filter Segmented", + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 2, + "padding": 2, + "children": [ + { + "id": "G2wGzF", + "type": "ref", + "ref": "MdmRT", + "name": "F 全部 18", + "height": 20, + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "全部 18", + "fontSize": 11, + "fill": "$text-primary" + } + } + }, + { + "id": "rauFS", + "type": "ref", + "ref": "MdmRT", + "name": "F 运行中 12", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "运行中 12", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + }, + { + "id": "rn0ci", + "type": "ref", + "ref": "MdmRT", + "name": "F 已停止 5", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "已停止 5", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + }, + { + "id": "LLxBe", + "type": "ref", + "ref": "MdmRT", + "name": "F 异常 1", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "异常 1", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + } + ] + }, + { + "type": "frame", + "id": "UA50p", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "dXZx3", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 拉取镜像", + "descendants": { + "RQ8pM": { + "icon": "arrow-down-to-line" + }, + "P0aUw": { + "content": "拉取镜像" + } + } + }, + { + "id": "XDQoY", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 新建 Compose", + "descendants": { + "RQ8pM": { + "icon": "file-code" + }, + "P0aUw": { + "content": "新建 Compose" + } + } + }, + { + "id": "R5R9S", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 运行容器", + "fill": "$accent", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "plus", + "fill": "$accent-on" + }, + "P0aUw": { + "content": "运行容器…", + "fill": "$accent-on", + "fontWeight": "500" + } + } + }, + { + "id": "BRhfD", + "type": "ref", + "ref": "yoK0T", + "name": "Refresh", + "descendants": { + "XMGqY": { + "icon": "refresh-cw" + } + } + } + ] + }, + { + "type": "frame", + "id": "aVbCK", + "name": "Header Row", + "width": "fill_container", + "height": 26, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "V8wZy", + "name": "Pad", + "width": 26, + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "sVWT9", + "name": "H 名称", + "clip": true, + "width": "fill_container", + "height": 26, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "LqbSX", + "name": "L", + "fill": "$text-muted", + "content": "名称", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "TnxTD", + "name": "H 驱动", + "clip": true, + "width": 90, + "height": 26, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Cch0V", + "name": "L", + "fill": "$text-muted", + "content": "驱动", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "Z8S14", + "name": "H 使用者", + "clip": true, + "width": 180, + "height": 26, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "C4ob7", + "name": "L", + "fill": "$text-muted", + "content": "使用者", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "V33nhu", + "name": "H 大小", + "clip": true, + "width": 96, + "height": 26, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "X0aQkj", + "name": "L", + "fill": "$text-muted", + "content": "大小", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "R2i3pB", + "name": "H 创建", + "clip": true, + "width": 112, + "height": 26, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Dn2Ls", + "name": "L", + "fill": "$text-muted", + "content": "创建", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "fwZAl", + "name": "H 操作", + "clip": true, + "width": 104, + "height": 26, + "padding": [ + 0, + 4 + ], + "alignItems": "center" + } + ] + }, + { + "type": "frame", + "id": "n7lKA", + "name": "List", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "BZE1E", + "name": "VR nginx-cache", + "width": "fill_container", + "height": 32, + "fill": "$bg-active", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "hSg28", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "QdCQw", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "VZFG4", + "name": "I", + "width": 13, + "height": 13, + "icon": "database", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "U3opVZ", + "name": "N", + "fill": "$text-primary", + "content": "nginx-cache", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "vrxFv", + "name": "C", + "clip": true, + "width": 90, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Fh066", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "GqybC", + "name": "C", + "clip": true, + "width": 180, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "itr44", + "name": "T", + "fill": "$ok", + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "RIdJ7", + "name": "C", + "clip": true, + "width": 96, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "JZ9HS", + "name": "T", + "fill": "$text-secondary", + "content": "318 MB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "tq0i2", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "fqNLZ", + "name": "T", + "fill": "$text-tertiary", + "content": "3 周前", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "g6Hsi", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "yoQjG", + "type": "ref", + "ref": "yoK0T", + "name": "A folder-open", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "folder-open", + "width": 13, + "height": 13 + } + } + }, + { + "id": "utdHR", + "type": "ref", + "ref": "yoK0T", + "name": "A download", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "download", + "width": 13, + "height": 13 + } + } + }, + { + "id": "dWXUj", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "MXYxk", + "name": "VR pg-data", + "width": "fill_container", + "height": 32, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "OsQAf", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "YUhdq", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "Z0HFr", + "name": "I", + "width": 13, + "height": 13, + "icon": "database", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "zjd2k", + "name": "N", + "fill": "$text-primary", + "content": "pg-data", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "T0nDVM", + "name": "C", + "clip": true, + "width": 90, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "jhVM5", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "VrmbN", + "name": "C", + "clip": true, + "width": 180, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "RazCU", + "name": "T", + "fill": "$ok", + "content": "postgres-main", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "NvU0h", + "name": "C", + "clip": true, + "width": 96, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "xlFMb", + "name": "T", + "fill": "$text-secondary", + "content": "18.4 GB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "QMUvq", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "VdXXd", + "name": "T", + "fill": "$text-tertiary", + "content": "3 个月前", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "c8gQg", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "G0B8eR", + "type": "ref", + "ref": "yoK0T", + "name": "A folder-open", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "folder-open", + "width": 13, + "height": 13 + } + } + }, + { + "id": "dkkO5", + "type": "ref", + "ref": "yoK0T", + "name": "A download", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "download", + "width": 13, + "height": 13 + } + } + }, + { + "id": "W5UIl", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "rGvbK", + "name": "VR redis-data", + "width": "fill_container", + "height": 32, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "O3GcK", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "fm0ak", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "Ya19u", + "name": "I", + "width": 13, + "height": 13, + "icon": "database", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "ARykk", + "name": "N", + "fill": "$text-primary", + "content": "redis-data", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "W5eSzC", + "name": "C", + "clip": true, + "width": 90, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "W75zQC", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "K980sJ", + "name": "C", + "clip": true, + "width": 180, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "lzuLE", + "name": "T", + "fill": "$ok", + "content": "redis-cache", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "UBpUm", + "name": "C", + "clip": true, + "width": 96, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "iLSAj", + "name": "T", + "fill": "$text-secondary", + "content": "642 MB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "yupCd", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "T7VZO", + "name": "T", + "fill": "$text-tertiary", + "content": "3 个月前", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "qMfrT", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "yISfP", + "type": "ref", + "ref": "yoK0T", + "name": "A folder-open", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "folder-open", + "width": 13, + "height": 13 + } + } + }, + { + "id": "y75u7", + "type": "ref", + "ref": "yoK0T", + "name": "A download", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "download", + "width": 13, + "height": 13 + } + } + }, + { + "id": "yqiD2", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "aQAuD", + "name": "VR grafana-storage", + "width": "fill_container", + "height": 32, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "CUkw0", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "XHM7q", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "PfFNF", + "name": "I", + "width": 13, + "height": 13, + "icon": "database", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "g0ajj", + "name": "N", + "fill": "$text-primary", + "content": "grafana-storage", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "Xi9gG", + "name": "C", + "clip": true, + "width": 90, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "WZsEJ", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "HXNK9", + "name": "C", + "clip": true, + "width": 180, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "FkbMe", + "name": "T", + "fill": "$ok", + "content": "grafana", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "nhiBT", + "name": "C", + "clip": true, + "width": 96, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "BUzBl", + "name": "T", + "fill": "$text-secondary", + "content": "94 MB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "bSLqK", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "rtpbu", + "name": "T", + "fill": "$text-tertiary", + "content": "1 个月前", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "O5jTXF", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "nxPBf", + "type": "ref", + "ref": "yoK0T", + "name": "A folder-open", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "folder-open", + "width": 13, + "height": 13 + } + } + }, + { + "id": "ufTWY", + "type": "ref", + "ref": "yoK0T", + "name": "A download", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "download", + "width": 13, + "height": 13 + } + } + }, + { + "id": "R2Jn1U", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "g2uipu", + "name": "VR loki-data", + "width": "fill_container", + "height": 32, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "epBnh", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "W5DrH", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "TEhlc", + "name": "I", + "width": 13, + "height": 13, + "icon": "database", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "hxqrT", + "name": "N", + "fill": "$text-primary", + "content": "loki-data", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "F0dDft", + "name": "C", + "clip": true, + "width": 90, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "PTCjH", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "G3z6z", + "name": "C", + "clip": true, + "width": 180, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "LpKr1", + "name": "T", + "fill": "$ok", + "content": "loki", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "eVvMK", + "name": "C", + "clip": true, + "width": 96, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ymJC8", + "name": "T", + "fill": "$text-secondary", + "content": "4.1 GB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "M5qEol", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Usuwl", + "name": "T", + "fill": "$text-tertiary", + "content": "1 个月前", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "wcC3M", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "aQxmO", + "type": "ref", + "ref": "yoK0T", + "name": "A folder-open", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "folder-open", + "width": 13, + "height": 13 + } + } + }, + { + "id": "EAEet", + "type": "ref", + "ref": "yoK0T", + "name": "A download", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "download", + "width": 13, + "height": 13 + } + } + }, + { + "id": "HLDBp", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "y96t8", + "name": "VR minio-data", + "width": "fill_container", + "height": 32, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "fZKFC", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "r6QTHN", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "d8D7Mb", + "name": "I", + "width": 13, + "height": 13, + "icon": "database", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "wleSr", + "name": "N", + "fill": "$text-primary", + "content": "minio-data", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "WkzT8", + "name": "C", + "clip": true, + "width": 90, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "miWGT", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "L3fA4j", + "name": "C", + "clip": true, + "width": 180, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "c3pJnR", + "name": "T", + "fill": "$ok", + "content": "minio", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "uu32v", + "name": "C", + "clip": true, + "width": 96, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "FfbmZ", + "name": "T", + "fill": "$text-secondary", + "content": "1.2 GB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "zkODT", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "d8uhB", + "name": "T", + "fill": "$text-tertiary", + "content": "6 周前", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "epAyq", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "DMKMR", + "type": "ref", + "ref": "yoK0T", + "name": "A folder-open", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "folder-open", + "width": 13, + "height": 13 + } + } + }, + { + "id": "LKAYG", + "type": "ref", + "ref": "yoK0T", + "name": "A download", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "download", + "width": 13, + "height": 13 + } + } + }, + { + "id": "IYu7u", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "uTg06", + "name": "VR registry-data", + "width": "fill_container", + "height": 32, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "FCUUk", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "c8vz1l", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "zeDYS", + "name": "I", + "width": 13, + "height": 13, + "icon": "database", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "D97RX", + "name": "N", + "fill": "$text-primary", + "content": "registry-data", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "y2eMBo", + "name": "C", + "clip": true, + "width": 90, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "nVd4r", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ITrdq", + "name": "C", + "clip": true, + "width": 180, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "j0gKwF", + "name": "T", + "fill": "$ok", + "content": "registry-mirror", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ckbCI", + "name": "C", + "clip": true, + "width": 96, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "hzZS9", + "name": "T", + "fill": "$text-secondary", + "content": "218 MB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "oMovR", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "X75ZQ", + "name": "T", + "fill": "$text-tertiary", + "content": "3 个月前", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "tmfeD", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "ogVPw", + "type": "ref", + "ref": "yoK0T", + "name": "A folder-open", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "folder-open", + "width": 13, + "height": 13 + } + } + }, + { + "id": "DxNcu", + "type": "ref", + "ref": "yoK0T", + "name": "A download", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "download", + "width": 13, + "height": 13 + } + } + }, + { + "id": "m4blgg", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "rs8LY", + "name": "VR acme-certs", + "width": "fill_container", + "height": 32, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "D3U54", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "VYRpB", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "AlD6H", + "name": "I", + "width": 13, + "height": 13, + "icon": "database", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "text", + "id": "zCk09", + "name": "N", + "fill": "$text-primary", + "content": "acme-certs", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "xvPYs", + "name": "C", + "clip": true, + "width": 90, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "VkxqU", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "L2SG9", + "name": "C", + "clip": true, + "width": 180, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "SKVbG", + "name": "T", + "fill": "$warn", + "content": "未使用", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "BIJj7", + "name": "C", + "clip": true, + "width": 96, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "aMfpC", + "name": "T", + "fill": "$text-secondary", + "content": "1.8 MB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "XeaZB", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "oUjhC", + "name": "T", + "fill": "$text-tertiary", + "content": "2 个月前", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "pBl5V", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "nlJQo", + "type": "ref", + "ref": "yoK0T", + "name": "A folder-open", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "folder-open", + "width": 13, + "height": 13 + } + } + }, + { + "id": "FFhKe", + "type": "ref", + "ref": "yoK0T", + "name": "A download", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "download", + "width": 13, + "height": 13 + } + } + }, + { + "id": "pbGVs", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "we3nZ", + "name": "VR tmp-restore-2408", + "width": "fill_container", + "height": 32, + "fill": "#00000000", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "t1URTC", + "name": "Sel", + "width": 26, + "height": 32, + "justifyContent": "center", + "alignItems": "center" + }, + { + "type": "frame", + "id": "wBznW", + "name": "C1", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 7, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "EUM5m", + "name": "I", + "width": 13, + "height": 13, + "icon": "database", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "text", + "id": "Zyy28", + "name": "N", + "fill": "$text-primary", + "content": "tmp-restore-2408", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "Q38r7", + "name": "C", + "clip": true, + "width": 90, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "w8fAcd", + "name": "T", + "fill": "$text-tertiary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "C0mHUd", + "name": "C", + "clip": true, + "width": 180, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "zQvGd", + "name": "T", + "fill": "$warn", + "content": "未使用", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ZnvMa", + "name": "C", + "clip": true, + "width": 96, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "f8DD4", + "name": "T", + "fill": "$text-secondary", + "content": "12 KB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ky9UX", + "name": "C", + "clip": true, + "width": 112, + "height": 32, + "padding": [ + 0, + 4 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "CLE3h", + "name": "T", + "fill": "$text-tertiary", + "content": "9 天前", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "JJh7f", + "name": "CA", + "width": 104, + "height": 32, + "gap": 1, + "padding": [ + 0, + 6 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "s5wor6", + "type": "ref", + "ref": "yoK0T", + "name": "A folder-open", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "folder-open", + "width": 13, + "height": 13 + } + } + }, + { + "id": "l43ATE", + "type": "ref", + "ref": "yoK0T", + "name": "A download", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "download", + "width": 13, + "height": 13 + } + } + }, + { + "id": "HCZFf", + "type": "ref", + "ref": "yoK0T", + "name": "A trash-2", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "trash-2", + "width": 13, + "height": 13 + } + } + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "LZlIP", + "name": "Detail Drawer", + "width": 440, + "height": "fill_container", + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "left": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "F2mZq", + "name": "Head", + "width": "fill_container", + "height": 44, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "ILGBl", + "name": "I", + "width": 15, + "height": 15, + "icon": "database", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "qgwgI", + "name": "N", + "fill": "$text-primary", + "content": "nginx-cache", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "id": "pF8Zk", + "type": "ref", + "ref": "x0Pcos", + "name": "Chip", + "descendants": { + "ODVko": { + "content": "使用中" + } + } + }, + { + "type": "frame", + "id": "w2solj", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "mhCK8", + "type": "ref", + "ref": "yoK0T", + "name": "H pin", + "descendants": { + "XMGqY": { + "icon": "pin" + } + } + }, + { + "id": "LiJJW", + "type": "ref", + "ref": "yoK0T", + "name": "H x", + "descendants": { + "XMGqY": { + "icon": "x" + } + } + } + ] + }, + { + "type": "frame", + "id": "lDy0v", + "name": "Tabs", + "width": "fill_container", + "height": 34, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 2, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "id": "Q7TLij", + "type": "ref", + "ref": "MdmRT", + "name": "T 概览", + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "icon": "info", + "fill": "$text-primary" + }, + "iscyV": { + "content": "概览", + "fill": "$text-primary", + "fontWeight": "500" + } + } + }, + { + "id": "Z1Wary", + "type": "ref", + "ref": "MdmRT", + "name": "T 卷内文件", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "folder-tree", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "卷内文件", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + }, + { + "id": "JgKNT", + "type": "ref", + "ref": "MdmRT", + "name": "T 备份", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "archive", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "备份", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + } + ] + }, + { + "type": "frame", + "id": "hQNLS", + "name": "Actions", + "width": "fill_container", + "height": 42, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "id": "vdV1b", + "type": "ref", + "ref": "i1OTz", + "name": "A 浏览", + "fill": "$accent-dim", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "folder-open", + "fill": "$accent" + }, + "P0aUw": { + "content": "浏览卷内文件", + "fill": "$accent" + } + } + }, + { + "id": "sH7Wh", + "type": "ref", + "ref": "i1OTz", + "name": "A 备份", + "descendants": { + "RQ8pM": { + "icon": "download" + }, + "P0aUw": { + "content": "备份为 tar" + } + } + }, + { + "type": "frame", + "id": "iY2fj", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "MvIdU", + "type": "ref", + "ref": "yoK0T", + "name": "More", + "descendants": { + "XMGqY": { + "icon": "ellipsis" + } + } + } + ] + }, + { + "type": "frame", + "id": "yICiT", + "name": "Content", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 10, + "padding": [ + 12, + 14 + ], + "children": [ + { + "type": "frame", + "id": "Y9yjIn", + "name": "Sec 基本信息", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "O8ejC", + "name": "H", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "c4YCo5", + "name": "T", + "fill": "$text-secondary", + "content": "基本信息", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "K2nIj", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "id": "eyo6x", + "type": "ref", + "ref": "l3qo4", + "name": "KV 驱动", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "驱动", + "width": 74 + }, + "psgr6": { + "content": "local", + "fill": "$text-primary", + "fontSize": 10 + } + } + }, + { + "id": "rd3XR", + "type": "ref", + "ref": "l3qo4", + "name": "KV 作用域", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "作用域", + "width": 74 + }, + "psgr6": { + "content": "local", + "fill": "$text-primary", + "fontSize": 10 + } + } + }, + { + "id": "FV24I", + "type": "ref", + "ref": "l3qo4", + "name": "KV 挂载点", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "挂载点", + "width": 74 + }, + "psgr6": { + "content": "/var/lib/docker/volumes/nginx-cache/_data", + "fill": "$text-primary", + "fontSize": 10 + } + } + }, + { + "id": "l6NzQ", + "type": "ref", + "ref": "l3qo4", + "name": "KV 大小", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "大小", + "width": 74 + }, + "psgr6": { + "content": "318 MB · 4,182 个文件", + "fill": "$text-primary", + "fontSize": 10 + } + } + }, + { + "id": "O5IhYF", + "type": "ref", + "ref": "l3qo4", + "name": "KV 创建于", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "创建于", + "width": 74 + }, + "psgr6": { + "content": "2026-07-31 14:02:11", + "fill": "$text-primary", + "fontSize": 10 + } + } + } + ] + }, + { + "type": "rectangle", + "id": "Z0nMh0", + "name": "Div", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "augOV", + "name": "Sec 使用者", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "VvSNS", + "name": "H", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "xJP7r", + "name": "T", + "fill": "$text-secondary", + "content": "使用者", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "GN648", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "QOt8q", + "name": "A", + "fill": "$accent", + "content": "查看容器", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "b4ySF", + "name": "U", + "clip": true, + "width": "fill_container", + "height": 24, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "PFAsc", + "name": "D", + "fill": "$ok", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "Hf5xs", + "name": "N", + "fill": "$text-primary", + "content": "nginx-proxy", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "o93M6", + "name": "Ar", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "Qpsl1", + "name": "M", + "fill": "$text-secondary", + "content": "/var/cache/nginx", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "PECYo", + "type": "ref", + "ref": "x0Pcos", + "name": "Mode", + "height": 15, + "fill": "#00000000", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "rw", + "fontSize": 9, + "fill": "$ok" + } + } + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "jAvLG", + "name": "Div", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "C6gQ0", + "name": "Sec 标签", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "c3GFPM", + "name": "H", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mnMTo", + "name": "T", + "fill": "$text-secondary", + "content": "标签", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "YOkRc", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "id": "rIl8q", + "type": "ref", + "ref": "l3qo4", + "name": "KV project", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "project", + "width": 74 + }, + "psgr6": { + "content": "web-stack", + "fill": "$violet", + "fontSize": 10 + } + } + }, + { + "id": "gDjCL", + "type": "ref", + "ref": "l3qo4", + "name": "KV volume", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "volume", + "width": 74 + }, + "psgr6": { + "content": "nginx-cache", + "fill": "$violet", + "fontSize": 10 + } + } + } + ] + }, + { + "type": "rectangle", + "id": "dE4ET", + "name": "Div", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "RJ0j0", + "name": "Sec 驱动选项", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "Z0iNRi", + "name": "H", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Ea529", + "name": "T", + "fill": "$text-secondary", + "content": "驱动选项", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "M4LM5r", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "id": "bf9Yl", + "type": "ref", + "ref": "l3qo4", + "name": "KV (无)", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "(无)", + "width": 74 + }, + "psgr6": { + "content": "使用默认 local 驱动", + "fill": "$text-muted", + "fontSize": 10 + } + } + } + ] + }, + { + "type": "frame", + "id": "DCDih", + "name": "S", + "width": "fill_container", + "height": "fill_container", + "fill": "#00000000" + }, + { + "type": "frame", + "id": "a3AogI", + "name": "Danger", + "width": "fill_container", + "fill": "$danger-dim", + "cornerRadius": "$r-md", + "stroke": "$danger", + "strokeWidth": 1, + "layout": "vertical", + "gap": 7, + "padding": [ + 10, + 12 + ], + "children": [ + { + "type": "frame", + "id": "rJQcr", + "name": "H", + "width": "fill_container", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "mN8yQ", + "name": "I", + "width": 13, + "height": 13, + "icon": "triangle-alert", + "library": "lucide", + "fill": "$danger" + }, + { + "type": "text", + "id": "SaiZw", + "name": "T", + "fill": "$danger", + "content": "危险操作", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "QK3Wj", + "name": "D", + "fill": "$danger", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "删除卷会永久丢失其中 318 MB 数据,且无法撤销。确认框中需手打 delete。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "AtXEx", + "type": "ref", + "ref": "i1OTz", + "name": "Del", + "height": 26, + "stroke": "$danger", + "descendants": { + "RQ8pM": { + "icon": "trash-2", + "fill": "$danger" + }, + "P0aUw": { + "content": "删除卷…", + "fill": "$danger", + "fontSize": 11 + } + } + } + ] + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "alFgr", + "layoutPosition": "absolute", + "x": 0, + "y": 0, + "name": "Scrim", + "fill": "#05070ACC", + "width": 1440, + "height": 796 + }, + { + "type": "frame", + "id": "N6dltZ", + "layoutPosition": "absolute", + "x": 452, + "y": 136, + "name": "Gate", + "clip": true, + "width": 560, + "fill": "$bg-elevated", + "cornerRadius": "$r-lg", + "stroke": "$danger", + "strokeWidth": 1, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000080", + "offset": { + "x": 0, + "y": 12 + }, + "blur": 36 + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "b4Kdc", + "name": "H", + "width": "fill_container", + "height": 52, + "fill": "$danger-dim", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 10, + "padding": [ + 0, + 18 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "T4pSSP", + "name": "I", + "width": 17, + "height": 17, + "icon": "shield-alert", + "library": "lucide", + "fill": "$danger" + }, + { + "type": "text", + "id": "fMvXG", + "name": "T", + "fill": "$text-primary", + "content": "删除卷 loki-data?", + "fontFamily": "$font-ui", + "fontSize": 15, + "fontWeight": "600" + }, + { + "id": "nLJ47", + "type": "ref", + "ref": "x0Pcos", + "name": "Tier", + "height": 18, + "fill": "$danger", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "会丢数据", + "fill": "#FFFFFF", + "fontSize": 10, + "fontWeight": "600" + } + } + }, + { + "type": "frame", + "id": "LBdor", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "JzwlP", + "type": "ref", + "ref": "yoK0T", + "name": "X", + "descendants": { + "XMGqY": { + "icon": "x" + } + } + } + ] + }, + { + "type": "frame", + "id": "eplUA", + "name": "Host", + "width": "fill_container", + "height": 34, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 18 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "gp2wN", + "name": "I", + "width": 13, + "height": 13, + "icon": "server", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "EHw3i", + "name": "L", + "fill": "$text-tertiary", + "content": "目标主机", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "pzyak", + "name": "H", + "fill": "$text-primary", + "content": "prod-sg-01", + "fontFamily": "$font-mono", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "AdqnH", + "name": "P", + "fill": "$warn", + "content": "· 10.24.8.11 · 这不是 staging", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "H3HID", + "name": "Body", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": [ + 16, + 18 + ], + "children": [ + { + "type": "text", + "id": "Fjjzq", + "name": "L", + "fill": "$text-secondary", + "content": "将要执行", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "IS2rA", + "name": "Cmd", + "width": "fill_container", + "fill": "$bg-terminal", + "cornerRadius": "$r-sm", + "layout": "vertical", + "gap": 3, + "padding": [ + 10, + 12 + ], + "children": [ + { + "type": "text", + "id": "zqm2I", + "name": "C", + "fill": "$text-terminal", + "content": "DELETE /volumes/loki-data", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "QWI6Y", + "name": "N", + "fill": "$text-muted", + "content": "等价于 docker volume rm loki-data", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ifnXP", + "name": "Warn", + "width": "fill_container", + "fill": "$danger-dim", + "cornerRadius": "$r-md", + "stroke": "$danger", + "strokeWidth": 1, + "layout": "vertical", + "gap": 6, + "padding": [ + 12, + 14 + ], + "children": [ + { + "type": "frame", + "id": "aMDDy", + "name": "H", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "kZ2dV", + "name": "I", + "width": 14, + "height": 14, + "icon": "database-zap", + "library": "lucide", + "fill": "$danger" + }, + { + "type": "text", + "id": "XiWLv", + "name": "T", + "fill": "$danger", + "content": "4.1 GB 数据将被永久删除,无法撤销", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "u8cGbs", + "name": "T", + "fill": "$danger", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "· 卷内 4.1 GB 日志索引会被抹掉;Docker 不做回收站,也没有快照。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "O5Z5RP", + "name": "T", + "fill": "$danger", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "· 容器 loki 已停止但仍引用它 —— 下次 compose up 会新建一个空卷。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "LNxbL", + "name": "T", + "fill": "$danger", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "· 本面板没有找到这个卷的备份记录。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "NFCdb", + "name": "Backup", + "width": "fill_container", + "gap": 7, + "padding": [ + 8, + 0, + 0, + 0 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "zhlwX", + "name": "I", + "width": 12, + "height": 12, + "icon": "download", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "TOlhG", + "name": "T", + "fill": "$accent", + "content": "先备份为 tar 再删除", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "text", + "id": "ra7EU", + "name": "L", + "fill": "$text-secondary", + "content": "请输入 delete 以确认", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "SvzV1", + "name": "Confirm Input", + "width": "fill_container", + "height": 34, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$danger", + "strokeWidth": 1, + "gap": 8, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Ijm39", + "name": "V", + "fill": "$text-primary", + "content": "dele", + "fontFamily": "$font-mono", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "rectangle", + "id": "L0EKQ", + "name": "Caret", + "fill": "$accent", + "width": 1.5, + "height": 16 + }, + { + "type": "frame", + "id": "F3aeUL", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "BPKhk", + "name": "Hint", + "fill": "$text-muted", + "content": "还差 2 个字符", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "m1iuIW", + "name": "Foot", + "width": "fill_container", + "height": 56, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 9, + "padding": [ + 0, + 18 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "Fw6Vh", + "name": "I", + "width": 13, + "height": 13, + "icon": "keyboard", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "wcihO", + "name": "K", + "fill": "$text-muted", + "content": "Esc 取消 · 回车不会触发", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "zLWe3", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "wzxOY", + "type": "ref", + "ref": "i1OTz", + "name": "Cancel", + "height": 30, + "descendants": { + "RQ8pM": { + "enabled": false + }, + "P0aUw": { + "content": "取消", + "fontSize": 12 + } + } + }, + { + "id": "YhDNn", + "type": "ref", + "ref": "i1OTz", + "name": "Confirm", + "height": 30, + "fill": "$danger", + "stroke": "#00000000", + "opacity": 0.35, + "descendants": { + "RQ8pM": { + "icon": "trash-2", + "fill": "#FFFFFF" + }, + "P0aUw": { + "content": "永久删除卷", + "fill": "#FFFFFF", + "fontWeight": "500", + "fontSize": 12 + } + } + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "gDX5a", + "name": "Status Bar", + "width": "fill_container", + "height": 24, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 14, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "rQhLB", + "name": "Msg", + "fill": "$text-tertiary", + "content": "等待手打确认 · 未执行任何动作", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "j4Y41O", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "M4KjZ", + "name": "API v1.47", + "fill": "$text-muted", + "content": "API v1.47", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "BN5fB", + "name": "Engine 27.3.1", + "fill": "$text-muted", + "content": "Engine 27.3.1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "VaWPw", + "name": "18 容器 · 34 镜像 · 9 卷", + "fill": "$text-muted", + "content": "18 容器 · 34 镜像 · 9 卷", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "uZv3i", + "name": "事件流 已连接", + "fill": "$text-muted", + "content": "事件流 已连接", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Z8Q0h", + "x": 1059, + "y": 11760, + "name": "14 · 状态与边界", + "theme": { + "mode": "dark" + }, + "clip": true, + "width": 1440, + "height": 860, + "fill": "$bg-page", + "layout": "vertical", + "gap": 14, + "padding": 24, + "children": [ + { + "type": "frame", + "id": "y7sHV0", + "name": "Title", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "xvIpB", + "name": "T", + "fill": "$text-primary", + "content": "状态与边界", + "fontFamily": "$font-ui", + "fontSize": 18, + "fontWeight": "600" + }, + { + "type": "text", + "id": "m1IISK", + "name": "S", + "fill": "$text-tertiary", + "content": "每个数据面都必须能表达这六种状态。一块静止的、看起来正常的面板,比明说「没连上」更糟。", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "hDi5b", + "name": "Row 1", + "width": "fill_container", + "height": "fill_container", + "gap": 14, + "children": [ + { + "type": "frame", + "id": "cCeso", + "name": "S 未选择会话", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "HGs4x", + "name": "H", + "width": "fill_container", + "height": 30, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 7, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "MRSDf", + "name": "N", + "fill": "$text-muted", + "content": "01", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "AtbW3", + "name": "T", + "fill": "$text-secondary", + "content": "未选择会话", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "ZBFBm", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "ellipse", + "id": "Ed8Da", + "name": "D", + "fill": "$text-muted", + "width": 6, + "height": 6 + } + ] + }, + { + "type": "frame", + "id": "XYCCz", + "name": "Body", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 10, + "padding": 18, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "n4Y2T", + "name": "IconWrap", + "width": 44, + "height": 44, + "fill": "$accent-dim", + "cornerRadius": 22, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "FjQKs", + "name": "I", + "width": 20, + "height": 20, + "icon": "plug-zap", + "library": "lucide", + "fill": "$accent" + } + ] + }, + { + "type": "text", + "id": "QnQB5", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "选一条已连接的 SSH 会话开始", + "textAlign": "center", + "fontFamily": "$font-ui", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "text", + "id": "Yyb1h", + "name": "D", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "面板不自己发起连接 —— 凭据永远不出宿主核心。选定后会在这条会话上打开一条到 docker.sock 的通道。", + "lineHeight": 1.6, + "textAlign": "center", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "FOx55", + "name": "Btns", + "gap": 8, + "justifyContent": "center", + "children": [ + { + "id": "JIS1c", + "type": "ref", + "ref": "i1OTz", + "name": "B0", + "height": 28, + "fill": "$accent", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "chevron-down", + "fill": "$accent-on" + }, + "P0aUw": { + "content": "选择会话…", + "fill": "$accent-on", + "fontSize": 11, + "fontWeight": "500" + } + } + }, + { + "id": "h4esW", + "type": "ref", + "ref": "i1OTz", + "name": "B1", + "height": 28, + "fill": "#00000000", + "stroke": "$border", + "descendants": { + "RQ8pM": { + "icon": "monitor", + "fill": "$text-secondary" + }, + "P0aUw": { + "content": "管理本机 Docker", + "fill": "$text-secondary", + "fontSize": 11, + "fontWeight": "400" + } + } + } + ] + }, + { + "type": "text", + "id": "vvYbO", + "name": "N", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "当前宿主有 3 条已连接会话", + "lineHeight": 1.6, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "PdR6L", + "name": "S 建立通道中", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "F4xRA", + "name": "H", + "width": "fill_container", + "height": 30, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 7, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "IZcO0", + "name": "N", + "fill": "$text-muted", + "content": "02", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "a4HWjK", + "name": "T", + "fill": "$text-secondary", + "content": "建立通道中", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "JElWH", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "ellipse", + "id": "ExNUW", + "name": "D", + "fill": "$accent", + "width": 6, + "height": 6 + } + ] + }, + { + "type": "frame", + "id": "Fvi7K", + "name": "Body", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 10, + "padding": 18, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "bGe0N", + "name": "Skeleton", + "width": "fill_container", + "layout": "vertical", + "gap": 9, + "children": [ + { + "type": "frame", + "id": "pVtHd", + "name": "R", + "width": "fill_container", + "height": 10, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 4, + "id": "poceS", + "name": "A", + "fill": "$bg-active", + "width": "fill_container", + "height": 9 + }, + { + "type": "rectangle", + "cornerRadius": 4, + "id": "PETkl", + "name": "B", + "fill": "$bg-hover", + "width": "fill_container", + "height": 9 + }, + { + "type": "rectangle", + "cornerRadius": 4, + "id": "DCBpb", + "name": "C", + "fill": "$bg-hover", + "width": "fill_container", + "height": 9 + } + ] + }, + { + "type": "frame", + "id": "W9YvFw", + "name": "R", + "width": "fill_container", + "height": 10, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 4, + "id": "RbUf3", + "name": "A", + "fill": "$bg-active", + "width": "fill_container", + "height": 9 + }, + { + "type": "rectangle", + "cornerRadius": 4, + "id": "JtmL6", + "name": "B", + "fill": "$bg-hover", + "width": "fill_container", + "height": 9 + }, + { + "type": "rectangle", + "cornerRadius": 4, + "id": "OF3SP", + "name": "C", + "fill": "$bg-hover", + "width": "fill_container", + "height": 9 + } + ] + }, + { + "type": "frame", + "id": "KtXo6", + "name": "R", + "width": "fill_container", + "height": 10, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 4, + "id": "zHbzw", + "name": "A", + "fill": "$bg-active", + "width": "fill_container", + "height": 9 + }, + { + "type": "rectangle", + "cornerRadius": 4, + "id": "M2nhrR", + "name": "B", + "fill": "$bg-hover", + "width": "fill_container", + "height": 9 + }, + { + "type": "rectangle", + "cornerRadius": 4, + "id": "L96Gfl", + "name": "C", + "fill": "$bg-hover", + "width": "fill_container", + "height": 9 + } + ] + }, + { + "type": "frame", + "id": "BKVRC", + "name": "R", + "width": "fill_container", + "height": 10, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 4, + "id": "NXk2c", + "name": "A", + "fill": "$bg-active", + "width": "fill_container", + "height": 9 + }, + { + "type": "rectangle", + "cornerRadius": 4, + "id": "i8lbd4", + "name": "B", + "fill": "$bg-hover", + "width": "fill_container", + "height": 9 + }, + { + "type": "rectangle", + "cornerRadius": 4, + "id": "jdaTL", + "name": "C", + "fill": "$bg-hover", + "width": "fill_container", + "height": 9 + } + ] + }, + { + "type": "frame", + "id": "x5S00U", + "name": "R", + "width": "fill_container", + "height": 10, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 4, + "id": "T2lVl", + "name": "A", + "fill": "$bg-active", + "width": "fill_container", + "height": 9 + }, + { + "type": "rectangle", + "cornerRadius": 4, + "id": "pXPMl", + "name": "B", + "fill": "$bg-hover", + "width": "fill_container", + "height": 9 + }, + { + "type": "rectangle", + "cornerRadius": 4, + "id": "D9tUXp", + "name": "C", + "fill": "$bg-hover", + "width": "fill_container", + "height": 9 + } + ] + }, + { + "type": "frame", + "id": "NXJf6", + "name": "R", + "width": "fill_container", + "height": 10, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 4, + "id": "E3ccuB", + "name": "A", + "fill": "$bg-active", + "width": "fill_container", + "height": 9 + }, + { + "type": "rectangle", + "cornerRadius": 4, + "id": "wlrtl", + "name": "B", + "fill": "$bg-hover", + "width": "fill_container", + "height": 9 + }, + { + "type": "rectangle", + "cornerRadius": 4, + "id": "sU4lO", + "name": "C", + "fill": "$bg-hover", + "width": "fill_container", + "height": 9 + } + ] + } + ] + }, + { + "type": "text", + "id": "wY3B0", + "name": "T", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "正在通过 prod-sg-01 打开 /var/run/docker.sock…", + "textAlign": "center", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "l2mxXH", + "name": "N", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "骨架屏保留真实列宽 —— 数据到达时不跳版", + "lineHeight": 1.6, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "tKy6z", + "name": "S 找不到 docker", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "dzY1u", + "name": "H", + "width": "fill_container", + "height": 30, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 7, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "K051jY", + "name": "N", + "fill": "$text-muted", + "content": "03", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "N4WeiV", + "name": "T", + "fill": "$text-secondary", + "content": "找不到 docker", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "dCpQB", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "ellipse", + "id": "x6cxkx", + "name": "D", + "fill": "$danger", + "width": 6, + "height": 6 + } + ] + }, + { + "type": "frame", + "id": "p6dNN", + "name": "Body", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 10, + "padding": 18, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "rK1iT", + "name": "IconWrap", + "width": 44, + "height": 44, + "fill": "$danger-dim", + "cornerRadius": 22, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "wjoF9", + "name": "I", + "width": 20, + "height": 20, + "icon": "circle-x", + "library": "lucide", + "fill": "$danger" + } + ] + }, + { + "type": "text", + "id": "p0und", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "这台机器上找不到 docker.sock", + "textAlign": "center", + "fontFamily": "$font-ui", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "text", + "id": "w9oTAT", + "name": "D", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "路径不存在,也没有可用的 DOCKER_HOST。远端可能压根没装 Docker,或者 daemon 没在跑。", + "lineHeight": 1.6, + "textAlign": "center", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "LoVF4", + "name": "Btns", + "gap": 8, + "justifyContent": "center", + "children": [ + { + "id": "HJuQI", + "type": "ref", + "ref": "i1OTz", + "name": "B0", + "height": 28, + "fill": "#00000000", + "stroke": "$border", + "descendants": { + "RQ8pM": { + "icon": "terminal", + "fill": "$text-secondary" + }, + "P0aUw": { + "content": "在终端里检查", + "fill": "$text-secondary", + "fontSize": 11, + "fontWeight": "400" + } + } + }, + { + "id": "AAICx", + "type": "ref", + "ref": "i1OTz", + "name": "B1", + "height": 28, + "fill": "#00000000", + "stroke": "$border", + "descendants": { + "RQ8pM": { + "icon": "settings", + "fill": "$text-secondary" + }, + "P0aUw": { + "content": "设置 DOCKER_HOST", + "fill": "$text-secondary", + "fontSize": 11, + "fontWeight": "400" + } + } + } + ] + }, + { + "type": "text", + "id": "aumHo", + "name": "N", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "stat /var/run/docker.sock: no such file or directory", + "lineHeight": 1.6, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "pBTrw", + "name": "Row 2", + "width": "fill_container", + "height": "fill_container", + "gap": 14, + "children": [ + { + "type": "frame", + "id": "gHDbn", + "name": "S 没有权限", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "UQoPu", + "name": "H", + "width": "fill_container", + "height": 30, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 7, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "I0IxRB", + "name": "N", + "fill": "$text-muted", + "content": "04", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "l31pg", + "name": "T", + "fill": "$text-secondary", + "content": "没有权限", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "K7Rxi6", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "ellipse", + "id": "owahz", + "name": "D", + "fill": "$warn", + "width": 6, + "height": 6 + } + ] + }, + { + "type": "frame", + "id": "f4enxa", + "name": "Body", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 10, + "padding": 18, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "o916Y", + "name": "IconWrap", + "width": 44, + "height": 44, + "fill": "$warn-dim", + "cornerRadius": 22, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "AdvFe", + "name": "I", + "width": 20, + "height": 20, + "icon": "lock", + "library": "lucide", + "fill": "$warn" + } + ] + }, + { + "type": "text", + "id": "CO9mx", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "当前账号没有 docker.sock 的读写权限", + "textAlign": "center", + "fontFamily": "$font-ui", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "text", + "id": "UI1MC", + "name": "D", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "账号 deploy 不在 docker 组。两条出路,选一条 —— 而不是给你一个红叉。", + "lineHeight": 1.6, + "textAlign": "center", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "zdcxl", + "name": "Btns", + "gap": 8, + "justifyContent": "center", + "children": [ + { + "id": "akWIi", + "type": "ref", + "ref": "i1OTz", + "name": "B0", + "height": 28, + "fill": "#00000000", + "stroke": "$border", + "descendants": { + "RQ8pM": { + "icon": "users", + "fill": "$text-secondary" + }, + "P0aUw": { + "content": "加进 docker 组", + "fill": "$text-secondary", + "fontSize": 11, + "fontWeight": "400" + } + } + }, + { + "id": "SH3Y6", + "type": "ref", + "ref": "i1OTz", + "name": "B1", + "height": 28, + "fill": "$accent", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "shield", + "fill": "$accent-on" + }, + "P0aUw": { + "content": "改用 sudo -n 转发", + "fill": "$accent-on", + "fontSize": 11, + "fontWeight": "500" + } + } + } + ] + }, + { + "type": "text", + "id": "qhaPj", + "name": "N", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "permission denied while trying to connect to the Docker daemon socket", + "lineHeight": 1.6, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "mLsH9", + "name": "S 空列表", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "s0Y0D", + "name": "H", + "width": "fill_container", + "height": 30, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 7, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "fHIUA", + "name": "N", + "fill": "$text-muted", + "content": "05", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "J3IDaf", + "name": "T", + "fill": "$text-secondary", + "content": "空列表", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "WoJUx", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "ellipse", + "id": "xIos5", + "name": "D", + "fill": "$text-muted", + "width": 6, + "height": 6 + } + ] + }, + { + "type": "frame", + "id": "gM0Fo", + "name": "Body", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 10, + "padding": 18, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "j88ZGx", + "name": "IconWrap", + "width": 44, + "height": 44, + "fill": "$bg-active", + "cornerRadius": 22, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "i0E1iE", + "name": "I", + "width": 20, + "height": 20, + "icon": "box", + "library": "lucide", + "fill": "$text-muted" + } + ] + }, + { + "type": "text", + "id": "CuvFO", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "这台机器上还没有任何容器", + "textAlign": "center", + "fontFamily": "$font-ui", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "text", + "id": "cjAsQ", + "name": "D", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "镜像有 34 个,但一个容器都没起来。", + "lineHeight": 1.6, + "textAlign": "center", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "zJRcB", + "name": "Btns", + "gap": 8, + "justifyContent": "center", + "children": [ + { + "id": "MIjeN", + "type": "ref", + "ref": "i1OTz", + "name": "B0", + "height": 28, + "fill": "$accent", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "play", + "fill": "$accent-on" + }, + "P0aUw": { + "content": "运行容器…", + "fill": "$accent-on", + "fontSize": 11, + "fontWeight": "500" + } + } + }, + { + "id": "d9qmlZ", + "type": "ref", + "ref": "i1OTz", + "name": "B1", + "height": 28, + "fill": "#00000000", + "stroke": "$border", + "descendants": { + "RQ8pM": { + "icon": "file-code", + "fill": "$text-secondary" + }, + "P0aUw": { + "content": "从 compose 起一套", + "fill": "$text-secondary", + "fontSize": 11, + "fontWeight": "400" + } + } + } + ] + }, + { + "type": "text", + "id": "uNKCo", + "name": "N", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "空列表也要给出下一步,而不是一片留白", + "lineHeight": 1.6, + "textAlign": "center", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "q7alq", + "name": "S 事件流断开 · 退化", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "CHA5B", + "name": "H", + "width": "fill_container", + "height": 30, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 7, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "GqoL5", + "name": "N", + "fill": "$text-muted", + "content": "06", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "P4fGm", + "name": "T", + "fill": "$text-secondary", + "content": "事件流断开 · 退化", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "XEWMQ", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "ellipse", + "id": "QM2Z9", + "name": "D", + "fill": "$warn", + "width": 6, + "height": 6 + } + ] + }, + { + "type": "frame", + "id": "YPRJE", + "name": "Body", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 10, + "padding": 18, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "aeG85", + "name": "Banner", + "width": "fill_container", + "fill": "$warn-dim", + "cornerRadius": "$r-sm", + "stroke": "$warn", + "strokeWidth": 1, + "gap": 9, + "padding": [ + 10, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "KdFMz", + "name": "I", + "width": 15, + "height": 15, + "icon": "wifi-off", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "frame", + "id": "hs4VC", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "DbYaM", + "name": "T", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "事件流已断开,已退化为 30 秒定时刷新", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "FqPl8", + "name": "D", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "看到的状态最多可能滞后 30 秒。", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "ItsgR", + "type": "ref", + "ref": "i1OTz", + "name": "Retry", + "height": 24, + "stroke": "$warn", + "descendants": { + "RQ8pM": { + "icon": "refresh-cw", + "fill": "$warn" + }, + "P0aUw": { + "content": "重连", + "fontSize": 11, + "fill": "$warn" + } + } + } + ] + }, + { + "type": "text", + "id": "JDueW", + "name": "X", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "头部的「实时」徽章同时转灰 —— 徽章亮着,才代表事件流真的接上了。", + "lineHeight": 1.6, + "textAlign": "center", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "mHGD8", + "x": 1059, + "y": 12740, + "name": "15 · 设计系统", + "theme": { + "mode": "dark" + }, + "clip": true, + "width": 1440, + "height": 1607, + "fill": "$bg-page", + "layout": "vertical", + "gap": 16, + "padding": 24, + "children": [ + { + "type": "frame", + "id": "OIeXg", + "name": "Title", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "J2PYhU", + "name": "T", + "fill": "$text-primary", + "content": "设计系统 · Docker Panel", + "fontFamily": "$font-ui", + "fontSize": 18, + "fontWeight": "600" + }, + { + "type": "text", + "id": "DEEbj", + "name": "S", + "fill": "$text-tertiary", + "content": "所有颜色都是带 mode 轴的主题变量,暗/亮两套同名解析。实现时一律映射到宿主的 {DynamicResource Vela*},切主题即时跟随。", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "k2ycw", + "name": "Row A", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "TBFx3", + "name": "C 颜色令牌", + "clip": true, + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "N02AZ", + "name": "H", + "width": "fill_container", + "height": 32, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "qOXGF", + "name": "T", + "fill": "$text-primary", + "content": "颜色令牌", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "Qn8CR", + "name": "S", + "fill": "$text-muted", + "content": "36 个 · mode: dark / light", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "YfB7Z", + "name": "B", + "width": "fill_container", + "layout": "vertical", + "gap": 10, + "padding": 14, + "children": [ + { + "type": "frame", + "id": "EzJsf", + "name": "Head", + "width": "fill_container", + "height": 20, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "I2ndqh", + "name": "H", + "width": 132, + "height": 20, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "gMBr1", + "name": "T", + "fill": "$text-muted", + "content": "变量", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "tpOwg", + "name": "H", + "width": 132, + "height": 20, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "d6QmYD", + "name": "T", + "fill": "$text-muted", + "content": "暗色 / dark", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "evtYN", + "name": "H", + "width": 132, + "height": 20, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "czN9A", + "name": "T", + "fill": "$text-muted", + "content": "亮色 / light", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "BAKAA", + "name": "H", + "width": "fill_container", + "height": 20, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "t4llll", + "name": "T", + "fill": "$text-muted", + "content": "用途", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "frame", + "id": "EwR0Q", + "name": "CT bg-page", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "SToz9", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 132, + "content": "bg-page", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "T58IZQ", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "RWUWu", + "name": "S", + "fill": "#0F1113", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "DuKnm", + "name": "X", + "fill": "$text-muted", + "content": "#0F1113", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "FgwR9", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "b8BYnK", + "name": "S", + "fill": "#F2F4F7", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "pFDAB", + "name": "X", + "fill": "$text-muted", + "content": "#F2F4F7", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "LY1BJ", + "name": "U", + "fill": "$text-tertiary", + "content": "面板底", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "DA2nP", + "name": "CT bg-surface", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "EYvfD", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 132, + "content": "bg-surface", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "q2VmJ", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "M2mvVk", + "name": "S", + "fill": "#16181C", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "mIkg5", + "name": "X", + "fill": "$text-muted", + "content": "#16181C", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "czrNZ", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "qgiMr", + "name": "S", + "fill": "#FFFFFF", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "PF0nl", + "name": "X", + "fill": "$text-muted", + "content": "#FFFFFF", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "INst8", + "name": "U", + "fill": "$text-tertiary", + "content": "列表 / 工具条", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "iZuZ5", + "name": "CT bg-elevated", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "RKaHJ", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 132, + "content": "bg-elevated", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "HkVB8", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "PrZo6", + "name": "S", + "fill": "#1B1E23", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "WTphz", + "name": "X", + "fill": "$text-muted", + "content": "#1B1E23", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "vzZIo", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "qL7Dx", + "name": "S", + "fill": "#FFFFFF", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "cRfCr", + "name": "X", + "fill": "$text-muted", + "content": "#FFFFFF", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "xq52c", + "name": "U", + "fill": "$text-tertiary", + "content": "抽屉 / 卡片 / 闸门", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Gh21m", + "name": "CT bg-rail", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "TNovy", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 132, + "content": "bg-rail", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "kXuLT", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "A4LMLy", + "name": "S", + "fill": "#121417", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "S7MCwK", + "name": "X", + "fill": "$text-muted", + "content": "#121417", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ii6QD", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "DVWnA", + "name": "S", + "fill": "#EAEDF1", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "w755v", + "name": "X", + "fill": "$text-muted", + "content": "#EAEDF1", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "T1xr58", + "name": "U", + "fill": "$text-tertiary", + "content": "左导航栏", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "A3cnj", + "name": "CT bg-input", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "zfj9T", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 132, + "content": "bg-input", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "l9mqk", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "rqFuO", + "name": "S", + "fill": "#0F1114", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "SDFEf", + "name": "X", + "fill": "$text-muted", + "content": "#0F1114", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "qUFd8", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "tUdIi", + "name": "S", + "fill": "#FFFFFF", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "TSJY4", + "name": "X", + "fill": "$text-muted", + "content": "#FFFFFF", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "CYWgV", + "name": "U", + "fill": "$text-tertiary", + "content": "输入框", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "brDcy", + "name": "CT bg-hover", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "UCKyk", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 132, + "content": "bg-hover", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "zDhFf", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "bYLO3", + "name": "S", + "fill": "#1F232A", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "i1SjL", + "name": "X", + "fill": "$text-muted", + "content": "#1F232A", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "y5CUzN", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "R9pIFC", + "name": "S", + "fill": "#EEF1F5", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "dj0Hp", + "name": "X", + "fill": "$text-muted", + "content": "#EEF1F5", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "Grrty", + "name": "U", + "fill": "$text-tertiary", + "content": "悬停", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "xrq9C", + "name": "CT bg-active", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "HRmJc", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 132, + "content": "bg-active", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "c2RtWx", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "r2o6r", + "name": "S", + "fill": "#252A32", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "bBZZa", + "name": "X", + "fill": "$text-muted", + "content": "#252A32", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "jXFtP", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "b3fuop", + "name": "S", + "fill": "#E1E8F2", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "Nfqoe", + "name": "X", + "fill": "$text-muted", + "content": "#E1E8F2", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "Eaygf", + "name": "U", + "fill": "$text-tertiary", + "content": "选中行 / 当前页签", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Uv8Gh", + "name": "CT bg-terminal", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "jMUOj", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 132, + "content": "bg-terminal", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "udJO5", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "mbJYV", + "name": "S", + "fill": "#0B0D0F", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "pQ5k8", + "name": "X", + "fill": "$text-muted", + "content": "#0B0D0F", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "P7CMN", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "mgfMv", + "name": "S", + "fill": "#1A1D21", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "DIzs6", + "name": "X", + "fill": "$text-muted", + "content": "#1A1D21", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "K4zDa", + "name": "U", + "fill": "$text-tertiary", + "content": "日志 / 终端 / 代码(亮色也保持深底)", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "j77VMN", + "name": "CT border", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "y1r1A", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 132, + "content": "border", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "veoMd", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "y24Bu", + "name": "S", + "fill": "#262A31", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "y6doPC", + "name": "X", + "fill": "$text-muted", + "content": "#262A31", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "S2mbO", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "U2dye2", + "name": "S", + "fill": "#E3E6EB", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "MEJSF", + "name": "X", + "fill": "$text-muted", + "content": "#E3E6EB", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "mysC9", + "name": "U", + "fill": "$text-tertiary", + "content": "常规分隔", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "UNSi4", + "name": "CT border-strong", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "zQWbj", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 132, + "content": "border-strong", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "qRxlW", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "OzOIh", + "name": "S", + "fill": "#343A44", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "m6sPky", + "name": "X", + "fill": "$text-muted", + "content": "#343A44", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "AXpkY", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "ki8Sz", + "name": "S", + "fill": "#C9CFD8", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "O2anT", + "name": "X", + "fill": "$text-muted", + "content": "#C9CFD8", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "pWeWC", + "name": "U", + "fill": "$text-tertiary", + "content": "控件描边", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "RylLA", + "name": "CT text-primary", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "B0xn0j", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 132, + "content": "text-primary", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "RqFPL", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "k6LyC", + "name": "S", + "fill": "#E7EAEF", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "IQ5AJ", + "name": "X", + "fill": "$text-muted", + "content": "#E7EAEF", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "wI8qN", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "YaDtB", + "name": "S", + "fill": "#14171C", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "evfeW", + "name": "X", + "fill": "$text-muted", + "content": "#14171C", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "pTB5k", + "name": "U", + "fill": "$text-tertiary", + "content": "名称 / 数值", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "hbIMy", + "name": "CT text-secondary", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "CjNxJ", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 132, + "content": "text-secondary", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "cckYC", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "OCE6p", + "name": "S", + "fill": "#A2AAB6", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "R7EQcR", + "name": "X", + "fill": "$text-muted", + "content": "#A2AAB6", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "R9xSxS", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "WXX8H", + "name": "S", + "fill": "#4B545F", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "bZJF7", + "name": "X", + "fill": "$text-muted", + "content": "#4B545F", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "WTbFW", + "name": "U", + "fill": "$text-tertiary", + "content": "正文 / 单元格", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "k3txPs", + "name": "CT text-tertiary", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "GqRHJ", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 132, + "content": "text-tertiary", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Ipl9V", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "m5zeP", + "name": "S", + "fill": "#6E7681", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "ZQ8NT", + "name": "X", + "fill": "$text-muted", + "content": "#6E7681", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "btwdP", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "lph8I", + "name": "S", + "fill": "#79838F", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "xa3db", + "name": "X", + "fill": "$text-muted", + "content": "#79838F", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "q1wHkW", + "name": "U", + "fill": "$text-tertiary", + "content": "元信息", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "t6VTBm", + "name": "CT text-muted", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "n5IYD", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 132, + "content": "text-muted", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "V9BSrn", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "T1zCk", + "name": "S", + "fill": "#545C67", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "XFiGI", + "name": "X", + "fill": "$text-muted", + "content": "#545C67", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "okMEc", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "QUvsg", + "name": "S", + "fill": "#9AA3AE", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "SjchR", + "name": "X", + "fill": "$text-muted", + "content": "#9AA3AE", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "uHgBD", + "name": "U", + "fill": "$text-tertiary", + "content": "列头 / 时间戳", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "QMXow", + "name": "CT accent", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "JaKFz", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 132, + "content": "accent", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "N7jr4K", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "B5DRge", + "name": "S", + "fill": "#2496ED", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "KZtLe", + "name": "X", + "fill": "$text-muted", + "content": "#2496ED", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ueMxG", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "cpOUV", + "name": "S", + "fill": "#0B72C4", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "APcP2", + "name": "X", + "fill": "$text-muted", + "content": "#0B72C4", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "yjt2u", + "name": "U", + "fill": "$text-tertiary", + "content": "主色 · Docker 蓝(亮色降深保对比)", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "f9Rbx", + "name": "CT accent-dim", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "W7yN5", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 132, + "content": "accent-dim", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "bxcEU", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "qa052", + "name": "S", + "fill": "#2496ED2E", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "z2hhR", + "name": "X", + "fill": "$text-muted", + "content": "#2496ED2E", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "QQxWM", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "Bp1uR", + "name": "S", + "fill": "#0B72C41F", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "yPPR3", + "name": "X", + "fill": "$text-muted", + "content": "#0B72C41F", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "y5QVL", + "name": "U", + "fill": "$text-tertiary", + "content": "选中底 / 次要强调", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "r6fz5", + "name": "CT ok", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "wFlJb", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 132, + "content": "ok", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "s0HvH0", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "OuNQ7", + "name": "S", + "fill": "#3FB950", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "HaZMm", + "name": "X", + "fill": "$text-muted", + "content": "#3FB950", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "rWW8d", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "HE4Km", + "name": "S", + "fill": "#1A7F37", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "CJzCo", + "name": "X", + "fill": "$text-muted", + "content": "#1A7F37", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "FtMCK", + "name": "U", + "fill": "$text-tertiary", + "content": "运行中 / 健康 / 成功", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "U73OK", + "name": "CT warn", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Cl6gl", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 132, + "content": "warn", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Z848J3", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "AvBZH", + "name": "S", + "fill": "#D29922", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "CyAJn", + "name": "X", + "fill": "$text-muted", + "content": "#D29922", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "LurLE", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "zFktQ", + "name": "S", + "fill": "#9A6700", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "u1rCj", + "name": "X", + "fill": "$text-muted", + "content": "#9A6700", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "rKDfq", + "name": "U", + "fill": "$text-tertiary", + "content": "暂停 / 高负载 / 已修改", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "tZz34", + "name": "CT danger", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "S6fT8s", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 132, + "content": "danger", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "oUP7n", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "IbNx8", + "name": "S", + "fill": "#F0524B", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "S1g8XV", + "name": "X", + "fill": "$text-muted", + "content": "#F0524B", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "WYkeC", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "npKna", + "name": "S", + "fill": "#CF222E", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "Hgg8T", + "name": "X", + "fill": "$text-muted", + "content": "#CF222E", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "JAHo5", + "name": "U", + "fill": "$text-tertiary", + "content": "异常 / 破坏性动作", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "kYGHP", + "name": "CT info", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "oLutV", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 132, + "content": "info", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "AY1QP", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "WOE2C", + "name": "S", + "fill": "#58A6FF", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "jJ2Pf", + "name": "X", + "fill": "$text-muted", + "content": "#58A6FF", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Ym2WK", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "SXnYy", + "name": "S", + "fill": "#0969DA", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "bkPwZ", + "name": "X", + "fill": "$text-muted", + "content": "#0969DA", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "cszwy", + "name": "U", + "fill": "$text-tertiary", + "content": "端口 / IP / 结构化键", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "A65jP", + "name": "CT violet", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "N07bB", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 132, + "content": "violet", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "AbFdz", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "UQFZE", + "name": "S", + "fill": "#A371F7", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "fqCeX", + "name": "X", + "fill": "$text-muted", + "content": "#A371F7", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "D0yKB", + "name": "SW", + "width": 132, + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "zDX3M", + "name": "S", + "fill": "#8250DF", + "width": 26, + "height": 14, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "HU9zr", + "name": "X", + "fill": "$text-muted", + "content": "#8250DF", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "KcXBY", + "name": "U", + "fill": "$text-tertiary", + "content": "Compose 项目归属", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "HDggt", + "name": "Right", + "width": 440, + "layout": "vertical", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "vBzy9", + "name": "C 排版", + "clip": true, + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "wHbxt", + "name": "H", + "width": "fill_container", + "height": 32, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mhcau", + "name": "T", + "fill": "$text-primary", + "content": "排版", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "eOcJl", + "name": "S", + "fill": "$text-muted", + "content": "Inter · JetBrains Mono", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "rmvwH", + "name": "B", + "width": "fill_container", + "layout": "vertical", + "gap": 10, + "padding": 14, + "children": [ + { + "type": "text", + "id": "dQTnY", + "name": "T18", + "fill": "$text-primary", + "content": "18 / 600 · 页面标题", + "fontFamily": "$font-ui", + "fontSize": 18, + "fontWeight": "600" + }, + { + "type": "text", + "id": "W2mz2", + "name": "T15", + "fill": "$text-primary", + "content": "15 / 600 · 闸门标题", + "fontFamily": "$font-ui", + "fontSize": 15, + "fontWeight": "600" + }, + { + "type": "text", + "id": "Qalkb", + "name": "T14", + "fill": "$text-primary", + "content": "14 / 600 · 区块标题", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "text", + "id": "uRq45", + "name": "T12", + "fill": "$text-primary", + "content": "12 / 500 · 行名称 · 按钮", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "ReH6c", + "name": "T12", + "fill": "$text-primary", + "content": "12 / 400 · 正文", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "yZTyM", + "name": "T11", + "fill": "$text-primary", + "content": "11 / 400 · 单元格 · 表单", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "OM42e", + "name": "T10", + "fill": "$text-primary", + "content": "10 / 400 · 元信息 · 列头", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "c4XNB", + "name": "T9", + "fill": "$text-primary", + "content": "9 / 400 · 徽章 · 轴标", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "GhjyH", + "name": "C 几何", + "clip": true, + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "z7K3mh", + "name": "H", + "width": "fill_container", + "height": 32, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "m5sfCZ", + "name": "T", + "fill": "$text-primary", + "content": "几何", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "tME5t", + "name": "S", + "fill": "$text-muted", + "content": "与宿主对齐", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "u68Mb", + "name": "B", + "width": "fill_container", + "layout": "vertical", + "gap": 10, + "padding": 14, + "children": [ + { + "id": "yKlDy", + "type": "ref", + "ref": "l3qo4", + "name": "G 顶栏 / 工具条", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "顶栏 / 工具条", + "width": 150, + "fontSize": 10 + }, + "psgr6": { + "content": "40 px", + "fontSize": 10 + } + } + }, + { + "id": "q7MoMG", + "type": "ref", + "ref": "l3qo4", + "name": "G 页签条 / 服务行", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "页签条 / 服务行", + "width": 150, + "fontSize": 10 + }, + "psgr6": { + "content": "30–34 px", + "fontSize": 10 + } + } + }, + { + "id": "T7h3Pc", + "type": "ref", + "ref": "l3qo4", + "name": "G 数据行", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "数据行", + "width": 150, + "fontSize": 10 + }, + "psgr6": { + "content": "32 px", + "fontSize": 10 + } + } + }, + { + "id": "ABmEv", + "type": "ref", + "ref": "l3qo4", + "name": "G 列头", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "列头", + "width": 150, + "fontSize": 10 + }, + "psgr6": { + "content": "26 px", + "fontSize": 10 + } + } + }, + { + "id": "vj2FC", + "type": "ref", + "ref": "l3qo4", + "name": "G 状态条", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "状态条", + "width": 150, + "fontSize": 10 + }, + "psgr6": { + "content": "24 px", + "fontSize": 10 + } + } + }, + { + "id": "y0lpr", + "type": "ref", + "ref": "l3qo4", + "name": "G 左导航栏宽", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "左导航栏宽", + "width": 150, + "fontSize": 10 + }, + "psgr6": { + "content": "56 px", + "fontSize": 10 + } + } + }, + { + "id": "kK30b", + "type": "ref", + "ref": "l3qo4", + "name": "G 详情抽屉宽", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "详情抽屉宽", + "width": 150, + "fontSize": 10 + }, + "psgr6": { + "content": "440 px(镜像表单 480)", + "fontSize": 10 + } + } + }, + { + "id": "Lf4rH", + "type": "ref", + "ref": "l3qo4", + "name": "G 圆角", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "圆角", + "width": 150, + "fontSize": 10 + }, + "psgr6": { + "content": "4 / 6 / 10 px", + "fontSize": 10 + } + } + }, + { + "id": "ICVqG", + "type": "ref", + "ref": "l3qo4", + "name": "G 内边距梯度", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "内边距梯度", + "width": 150, + "fontSize": 10 + }, + "psgr6": { + "content": "6 · 10 · 14 · 16 px", + "fontSize": 10 + } + } + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "GMZWR", + "name": "Row B", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "tPaKC", + "name": "C 组件", + "clip": true, + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "BMlUD", + "name": "H", + "width": "fill_container", + "height": 32, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "aWuas", + "name": "T", + "fill": "$text-primary", + "content": "组件", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "cEduQ", + "name": "S", + "fill": "$text-muted", + "content": "7 个 reusable · 其余由布局组合", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "fmBGA", + "name": "B", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": 14, + "children": [ + { + "type": "frame", + "id": "oan1I", + "name": "G C/Button · 常规 / 主要 / 危险 / 禁用 / 激活态", + "width": "fill_container", + "layout": "vertical", + "gap": 7, + "children": [ + { + "type": "text", + "id": "FAtzs", + "name": "L", + "fill": "$text-muted", + "content": "C/Button · 常规 / 主要 / 危险 / 禁用 / 激活态", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "Z5w33L", + "name": "R", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "id": "gGMDX", + "type": "ref", + "ref": "i1OTz", + "name": "B1", + "descendants": { + "RQ8pM": { + "icon": "square" + }, + "P0aUw": { + "content": "停止" + } + } + }, + { + "id": "xWJDA", + "type": "ref", + "ref": "i1OTz", + "name": "B2", + "fill": "$accent", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "plus", + "fill": "$accent-on" + }, + "P0aUw": { + "content": "运行容器…", + "fill": "$accent-on", + "fontWeight": "500" + } + } + }, + { + "id": "xjuzr", + "type": "ref", + "ref": "i1OTz", + "name": "B3", + "stroke": "$danger", + "descendants": { + "RQ8pM": { + "icon": "trash-2", + "fill": "$danger" + }, + "P0aUw": { + "content": "删除", + "fill": "$danger" + } + } + }, + { + "id": "UdqLU", + "type": "ref", + "ref": "i1OTz", + "name": "B4", + "opacity": 0.35, + "descendants": { + "RQ8pM": { + "icon": "pause" + }, + "P0aUw": { + "content": "暂停" + } + } + }, + { + "id": "a2sSpJ", + "type": "ref", + "ref": "i1OTz", + "name": "B5", + "fill": "$ok-dim", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "activity", + "fill": "$ok" + }, + "P0aUw": { + "content": "跟随中", + "fill": "$ok", + "fontWeight": "500" + } + } + }, + { + "id": "YVTG9", + "type": "ref", + "ref": "yoK0T", + "name": "IB1", + "descendants": { + "XMGqY": { + "icon": "refresh-cw" + } + } + }, + { + "id": "lVS5v", + "type": "ref", + "ref": "yoK0T", + "name": "IB2", + "fill": "$bg-hover", + "descendants": { + "XMGqY": { + "icon": "ellipsis" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "aZ1nV", + "name": "G C/Chip · 状态 / 项目 / 标记", + "width": "fill_container", + "layout": "vertical", + "gap": 7, + "children": [ + { + "type": "text", + "id": "pGpNP", + "name": "L", + "fill": "$text-muted", + "content": "C/Chip · 状态 / 项目 / 标记", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "zADHd", + "name": "R", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "id": "wEeKw", + "type": "ref", + "ref": "x0Pcos", + "name": "Ch", + "fill": "$ok-dim", + "descendants": { + "kvk5k": { + "fill": "$ok", + "enabled": true + }, + "ODVko": { + "content": "运行中", + "fill": "$ok" + } + } + }, + { + "id": "b2e35F", + "type": "ref", + "ref": "x0Pcos", + "name": "Ch", + "fill": "$warn-dim", + "descendants": { + "kvk5k": { + "fill": "$warn", + "enabled": true + }, + "ODVko": { + "content": "已暂停", + "fill": "$warn" + } + } + }, + { + "id": "JXLIq", + "type": "ref", + "ref": "x0Pcos", + "name": "Ch", + "fill": "$danger-dim", + "descendants": { + "kvk5k": { + "fill": "$danger", + "enabled": true + }, + "ODVko": { + "content": "不健康", + "fill": "$danger" + } + } + }, + { + "id": "HoH3d", + "type": "ref", + "ref": "x0Pcos", + "name": "Ch", + "fill": "$bg-active", + "descendants": { + "kvk5k": { + "fill": "$text-muted", + "enabled": true + }, + "ODVko": { + "content": "已停止", + "fill": "$text-muted" + } + } + }, + { + "id": "FHgVp", + "type": "ref", + "ref": "x0Pcos", + "name": "Ch", + "fill": "$violet-dim", + "descendants": { + "kvk5k": { + "fill": "$violet", + "enabled": false + }, + "ODVko": { + "content": "web-stack", + "fill": "$violet" + } + } + }, + { + "id": "SnjZd", + "type": "ref", + "ref": "x0Pcos", + "name": "Ch", + "fill": "$warn-dim", + "descendants": { + "kvk5k": { + "fill": "$warn", + "enabled": true + }, + "ODVko": { + "content": "未保存 · +2 −1", + "fill": "$warn" + } + } + }, + { + "id": "hykzt", + "type": "ref", + "ref": "x0Pcos", + "name": "Ch", + "fill": "$bg-active", + "descendants": { + "kvk5k": { + "fill": "$text-muted", + "enabled": false + }, + "ODVko": { + "content": "内置", + "fill": "$text-muted" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "Y8MVy8", + "name": "G C/Tab · 页签 / 分段控件", + "width": "fill_container", + "layout": "vertical", + "gap": 7, + "children": [ + { + "type": "text", + "id": "K6fGKi", + "name": "L", + "fill": "$text-muted", + "content": "C/Tab · 页签 / 分段控件", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "lxDwd", + "name": "R", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "id": "hUDCJ", + "type": "ref", + "ref": "MdmRT", + "name": "T", + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "icon": "info", + "fill": "$text-primary" + }, + "iscyV": { + "content": "概览", + "fill": "$text-primary", + "fontWeight": "500" + } + } + }, + { + "id": "GcIx6", + "type": "ref", + "ref": "MdmRT", + "name": "T", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "scroll-text", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "日志", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + }, + { + "id": "MMI4n", + "type": "ref", + "ref": "MdmRT", + "name": "T", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "folder-tree", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "文件", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + }, + { + "id": "AlXej", + "type": "ref", + "ref": "MdmRT", + "name": "T", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "terminal", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "终端", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + }, + { + "type": "frame", + "id": "qbpBv", + "name": "Seg", + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 2, + "padding": 2, + "children": [ + { + "id": "T66jlm", + "type": "ref", + "ref": "MdmRT", + "name": "S", + "height": 20, + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "全部", + "fontSize": 11, + "fill": "$text-primary" + } + } + }, + { + "id": "gAxdW", + "type": "ref", + "ref": "MdmRT", + "name": "S", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "运行中", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + }, + { + "id": "U89cX1", + "type": "ref", + "ref": "MdmRT", + "name": "S", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "已停止", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "dXdeO", + "name": "G C/Field · 输入 / 下拉 / 开关 / 复选", + "width": "fill_container", + "layout": "vertical", + "gap": 7, + "children": [ + { + "type": "text", + "id": "YVprF", + "name": "L", + "fill": "$text-muted", + "content": "C/Field · 输入 / 下拉 / 开关 / 复选", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "FS8JV", + "name": "R", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "id": "y4lR1", + "type": "ref", + "ref": "ARwD6", + "name": "F1", + "width": 200, + "descendants": { + "T0xmy": { + "content": "容器名称" + }, + "G4vkue": { + "enabled": false + }, + "oLZwi": { + "content": "nginx-proxy-2" + } + } + }, + { + "id": "UE8TV", + "type": "ref", + "ref": "ARwD6", + "name": "F2", + "width": 200, + "descendants": { + "T0xmy": { + "content": "重启策略" + }, + "G4vkue": { + "enabled": false + }, + "oLZwi": { + "content": "unless-stopped" + }, + "jdH16": { + "enabled": true + } + } + }, + { + "type": "frame", + "id": "y0oJo", + "name": "Sw", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "text", + "id": "h3EvH", + "name": "L", + "fill": "$text-secondary", + "content": "开关 / 复选", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "lh2dP", + "name": "R", + "height": 28, + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "uKN2p", + "name": "S", + "width": 26, + "height": 15, + "fill": "$accent", + "cornerRadius": 8, + "stroke": "#00000000", + "strokeWidth": 1, + "padding": [ + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "xiEAX", + "name": "K", + "fill": "$accent-on", + "width": 11, + "height": 11 + } + ] + }, + { + "type": "frame", + "id": "MwCLX", + "name": "S", + "width": 26, + "height": 15, + "fill": "$bg-active", + "cornerRadius": 8, + "stroke": "$border-strong", + "strokeWidth": 1, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "az36T", + "name": "K", + "fill": "$text-muted", + "width": 11, + "height": 11 + } + ] + }, + { + "type": "rectangle", + "cornerRadius": 3, + "id": "eJDNj", + "name": "Cb", + "fill": "$accent", + "width": 12, + "height": 12, + "stroke": "$accent", + "strokeWidth": 1 + }, + { + "type": "rectangle", + "cornerRadius": 3, + "id": "j2e4OA", + "name": "Cb", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "eKlGv", + "name": "G C/ContainerRow · C/ImageRow · C/Spark · C/StatCard · C/KVRow", + "width": "fill_container", + "layout": "vertical", + "gap": 7, + "children": [ + { + "type": "text", + "id": "Y2mH9", + "name": "L", + "fill": "$text-muted", + "content": "C/ContainerRow · C/ImageRow · C/Spark · C/StatCard · C/KVRow", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "id": "zAGjT", + "type": "ref", + "ref": "OHo2s", + "name": "RowSample", + "width": "fill_container", + "fill": "$bg-active" + }, + { + "id": "dW2iI", + "type": "ref", + "ref": "YgkX1", + "name": "ImgSample", + "width": "fill_container" + } + ] + } + ] + }, + { + "type": "frame", + "id": "vYcAM", + "name": "C 状态语义", + "clip": true, + "width": 440, + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "mZOs9", + "name": "H", + "width": "fill_container", + "height": 32, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "eisCg", + "name": "T", + "fill": "$text-primary", + "content": "状态语义", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "x0rR1", + "name": "B", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": 14, + "children": [ + { + "type": "frame", + "id": "NaonJ", + "name": "S 运行中 · healthy", + "clip": true, + "width": "fill_container", + "height": 34, + "gap": 9, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "id": "ub8RY", + "name": "Bar", + "fill": "$ok", + "width": 3, + "height": 22 + }, + { + "type": "ellipse", + "id": "pQWj9", + "name": "D", + "fill": "$ok", + "width": 6, + "height": 6 + }, + { + "type": "frame", + "id": "mtYdD", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "text", + "id": "c5MKn", + "name": "T", + "fill": "$text-primary", + "content": "运行中 · healthy", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "meVNu", + "name": "D", + "fill": "$text-muted", + "content": "绿:左色条 + 圆点 + 徽章", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "JuM8Y", + "name": "S 运行中 · unhealthy", + "clip": true, + "width": "fill_container", + "height": 34, + "gap": 9, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "id": "aze7v", + "name": "Bar", + "fill": "$danger", + "width": 3, + "height": 22 + }, + { + "type": "ellipse", + "id": "j13FGF", + "name": "D", + "fill": "$danger", + "width": 6, + "height": 6 + }, + { + "type": "frame", + "id": "dXAdy", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "text", + "id": "BDhU0", + "name": "T", + "fill": "$text-primary", + "content": "运行中 · unhealthy", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "j68tdF", + "name": "D", + "fill": "$text-muted", + "content": "红:健康检查连续失败", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "PKa0V", + "name": "S 已暂停", + "clip": true, + "width": "fill_container", + "height": 34, + "gap": 9, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "id": "IBK5E", + "name": "Bar", + "fill": "$warn", + "width": 3, + "height": 22 + }, + { + "type": "ellipse", + "id": "YyRK6", + "name": "D", + "fill": "$warn", + "width": 6, + "height": 6 + }, + { + "type": "frame", + "id": "ysviM", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "text", + "id": "i8CRue", + "name": "T", + "fill": "$text-primary", + "content": "已暂停", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "DuOBQ", + "name": "D", + "fill": "$text-muted", + "content": "黄:SIGSTOP,进程还在", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "owRAW", + "name": "S 已停止 · exit 0", + "clip": true, + "width": "fill_container", + "height": 34, + "gap": 9, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "id": "xU34D", + "name": "Bar", + "fill": "$text-muted", + "width": 3, + "height": 22 + }, + { + "type": "ellipse", + "id": "pT80Z", + "name": "D", + "fill": "$text-muted", + "width": 6, + "height": 6 + }, + { + "type": "frame", + "id": "VUt4J", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "text", + "id": "hfe8N", + "name": "T", + "fill": "$text-primary", + "content": "已停止 · exit 0", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "TDG2d", + "name": "D", + "fill": "$text-muted", + "content": "灰:正常退出", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "nTCt2", + "name": "S 已停止 · exit ≠ 0", + "clip": true, + "width": "fill_container", + "height": 34, + "gap": 9, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "id": "w5FG1", + "name": "Bar", + "fill": "$danger", + "width": 3, + "height": 22 + }, + { + "type": "ellipse", + "id": "DUfCs", + "name": "D", + "fill": "$danger", + "width": 6, + "height": 6 + }, + { + "type": "frame", + "id": "CFoOV", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "text", + "id": "uEB1W", + "name": "T", + "fill": "$text-primary", + "content": "已停止 · exit ≠ 0", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "S7EEVP", + "name": "D", + "fill": "$text-muted", + "content": "红:异常退出,附退出码", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "j8XNgb", + "name": "S 重启中 / 创建中", + "clip": true, + "width": "fill_container", + "height": 34, + "gap": 9, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "id": "AZDQo", + "name": "Bar", + "fill": "$accent", + "width": 3, + "height": 22 + }, + { + "type": "ellipse", + "id": "g2rAA", + "name": "D", + "fill": "$accent", + "width": 6, + "height": 6 + }, + { + "type": "frame", + "id": "fzuxo", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "text", + "id": "oPqDo", + "name": "T", + "fill": "$text-primary", + "content": "重启中 / 创建中", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "k7OrV5", + "name": "D", + "fill": "$text-muted", + "content": "蓝:过渡态,动作按钮暂禁", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "OwoM7", + "name": "C 令牌映射", + "clip": true, + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "NCjfd", + "name": "H", + "width": "fill_container", + "height": 32, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "w1SNLV", + "name": "T", + "fill": "$text-primary", + "content": "落地映射 · 设计变量 → 宿主 {DynamicResource Vela*}", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "D5HTSt", + "name": "S", + "fill": "$text-muted", + "content": "面板本就该长得像宿主的一部分。Docker 蓝只留给左上角品牌标,别处一律用宿主令牌", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ljYmq", + "name": "B", + "width": "fill_container", + "gap": 24, + "padding": 14, + "children": [ + { + "type": "frame", + "id": "sQqBM", + "name": "Col", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "frame", + "id": "a6haFQ", + "name": "M bg-page", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "xBZ9f", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "JZjDu", + "name": "A", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 124, + "content": "bg-page", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "jHAms", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "sXKI1", + "name": "B", + "fill": "$accent", + "content": "VelaBgDockDocument", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "H3JpT6", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "面板底,与文档区同色", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "piz4r", + "name": "M bg-surface", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "dIcMY", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "a3rPEa", + "name": "A", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 124, + "content": "bg-surface", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "cgZ32", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "NYb27", + "name": "B", + "fill": "$accent", + "content": "VelaBgSurface", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "cH2u9", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "工具条 / 列表底", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "GEOOm", + "name": "M bg-elevated", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "gUXBJ", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "WUGvh", + "name": "A", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 124, + "content": "bg-elevated", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "jhYhW", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "tqA8l", + "name": "B", + "fill": "$accent", + "content": "VelaBgContentHeader", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "WyiOM", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "抽屉 / 卡片 / 闸门", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ZJgyr", + "name": "M bg-rail", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "oRJGr", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "SeNvd", + "name": "A", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 124, + "content": "bg-rail", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "WNOcC", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "qAJ42", + "name": "B", + "fill": "$accent", + "content": "VelaBgPage", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "b3pp4I", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "左导航栏比面板底再深一档", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "m4w0G", + "name": "M bg-input", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "iOrlg", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "U3JGj", + "name": "A", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 124, + "content": "bg-input", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "UtzSq", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "B2jEW2", + "name": "B", + "fill": "$accent", + "content": "VelaBgInput", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "n0VfoR", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "输入框", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "FFQ0H", + "name": "M bg-hover", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "SQD2y", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "d4myP", + "name": "A", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 124, + "content": "bg-hover", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "C9qUD", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "QzSde", + "name": "B", + "fill": "$accent", + "content": "VelaBgHover", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "T4cYzB", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "悬停", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "cSFY6", + "name": "M bg-active", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "Atuqq", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "cCb9n", + "name": "A", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 124, + "content": "bg-active", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "ALE8t", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "I4eNg", + "name": "B", + "fill": "$accent", + "content": "VelaBgActive", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "WSv4l", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "选中行 / 当前页签", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "o5Zfqd", + "name": "M bg-terminal", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "HVtOS", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "q9EWfu", + "name": "A", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 124, + "content": "bg-terminal", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "B2F14d", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "JB4ZX", + "name": "B", + "fill": "$accent", + "content": "VelaBgTerminal", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "cZWoX", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "日志 / 终端 / 代码", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "sQvwd", + "name": "Col", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "frame", + "id": "RGll7", + "name": "M border", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "mUc0Y", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "df3bL", + "name": "A", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 124, + "content": "border", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "XCaUe", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "LRk9x", + "name": "B", + "fill": "$accent", + "content": "VelaBorderPrimary", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "R8dv4v", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "全部分隔线", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "C8KXh", + "name": "M text-primary", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "GwuI2", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "iCS5p", + "name": "A", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 124, + "content": "text-primary", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "G49QB", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "T6e2s", + "name": "B", + "fill": "$accent", + "content": "VelaTextPrimary", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "FPEVi", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "—", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "yYeX8", + "name": "M text-secondary", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "H5ykcF", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "a4lsh", + "name": "A", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 124, + "content": "text-secondary", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "UT1tk", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "CRNFh", + "name": "B", + "fill": "$accent", + "content": "VelaTextSecondary", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "cnNKG", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "—", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "WrJFt", + "name": "M text-tertiary", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "BqIAq", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "trOT3", + "name": "A", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 124, + "content": "text-tertiary", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "e5TzTw", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "YhqIw", + "name": "B", + "fill": "$accent", + "content": "VelaTextTertiary", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "M4S2Uv", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "—", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "S711Xi", + "name": "M text-muted", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "VJJhF", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "x3dRc", + "name": "A", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 124, + "content": "text-muted", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "leWke", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "BjJwp", + "name": "B", + "fill": "$accent", + "content": "VelaTextMuted", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "fV1Ma", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "—", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "VSJ7I", + "name": "M accent / accent-dim", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "ZW36d", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "R3byx", + "name": "A", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 124, + "content": "accent / accent-dim", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "mQ8eu", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "OyytW", + "name": "B", + "fill": "$accent", + "content": "VelaAccent / VelaAccentDim", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "qHCPS", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "宿主实为 Dracula 紫 #BD93F9 / Alucard —— 不是 Docker 蓝", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "WCBte", + "name": "M ok", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "tFCFY", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "m7NBQc", + "name": "A", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 124, + "content": "ok", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "rRH5m", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "RpGgy", + "name": "B", + "fill": "$accent", + "content": "VelaStatusConnected", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "BoSNB", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "运行中沿用宿主的「已连接」绿", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "PUXAY", + "name": "M warn / danger", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "dDeHZ", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "j9ttuz", + "name": "A", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 124, + "content": "warn / danger", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "L6GYz", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "k83DI", + "name": "B", + "fill": "$accent", + "content": "VelaWarning / VelaError", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "T9QH9", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "—", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "EmHPd", + "name": "Col", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "frame", + "id": "gWNGt", + "name": "M border-strong", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "cCrku", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "MbHFQ", + "name": "A", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 124, + "content": "border-strong", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "fhGJ3", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "pLNP5", + "name": "B", + "fill": "$warn", + "content": "← 派生", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "TRGxs", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "宿主无对应令牌:由 VelaBorderPrimary 提亮 12% 生成", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "eKid2", + "name": "M info", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "cIOcb", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Z34Kk", + "name": "A", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 124, + "content": "info", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "u3nsQ", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "z9dYO", + "name": "B", + "fill": "$warn", + "content": "← 派生", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "u8oCu", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "端口 / IP / JSON 键;由 VelaAccent 提亮", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "S9E1GW", + "name": "M violet", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "VETuF", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "G84BW", + "name": "A", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 124, + "content": "violet", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "STjYh", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "liObP", + "name": "B", + "fill": "$warn", + "content": "← 插件私有", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "Sq3R3", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Compose 归属色,宿主没有语义位;实现用 VelaShellMagenta", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "g3kynA", + "name": "M font-ui", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "ZefWz", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dRXR5", + "name": "A", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 124, + "content": "font-ui", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "o7gOCh", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "AjERB", + "name": "B", + "fill": "$accent", + "content": "系统 UI 字体", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "Msk1q", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "不覆盖宿主字体设置", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "IkuZx", + "name": "M font-mono", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "S6QwB", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ThR1J", + "name": "A", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 124, + "content": "font-mono", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "BlVkq", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "WgxCE", + "name": "B", + "fill": "$accent", + "content": "VelaUiMonoFont", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "NlBeu", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "所有 ID / 路径 / 数值一律等宽", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "JXPwn", + "name": "M 字号 9–14", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "MhyLD", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "DYDFz", + "name": "A", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 124, + "content": "字号 9–14", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "Q8xin", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "x3eHV", + "name": "B", + "fill": "$accent", + "content": "VelaFontSize9…14", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "QGy5p", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "跟随宿主字号设置缩放", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "MQU7K", + "name": "M 行高 32 / 26 / 24", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "IElrv", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "S6E2s", + "name": "A", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 124, + "content": "行高 32 / 26 / 24", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "fcmkb", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "Uy0g0", + "name": "B", + "fill": "$accent", + "content": "DESIGN.md 几何", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "y8wxM", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "与宿主标签区、状态条对齐", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "GGFFF", + "name": "M 圆角 4 / 6 / 10", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "nCzE1", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "S2IxZ", + "name": "A", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 124, + "content": "圆角 4 / 6 / 10", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "cEDYM", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "IZgbU", + "name": "B", + "fill": "$accent", + "content": "插件私有", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "v39udU", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "宿主未定义圆角令牌", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "p3cYJ", + "x": 2619, + "y": 7840, + "name": "10L · 系统 · 亮色", + "theme": { + "mode": "light" + }, + "clip": true, + "width": 1440, + "height": 860, + "fill": "$bg-page", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "sYlEn", + "name": "Top Bar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "WfBST", + "name": "Brand", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "LfWLr", + "name": "Logo", + "width": 17, + "height": 17, + "icon": "container", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "X89vJC", + "name": "Title", + "fill": "$text-primary", + "content": "Docker", + "fontFamily": "$font-ui", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "rectangle", + "id": "CM4ng", + "name": "Divider", + "fill": "$border", + "width": 1, + "height": 18 + }, + { + "type": "frame", + "id": "PvlJh", + "name": "Host Switcher", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "z7d6aA", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "server", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "qI0cW", + "name": "Host", + "fill": "$text-primary", + "content": "prod-sg-01", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "a2HhC", + "name": "Transport", + "fill": "$text-muted", + "content": "/var/run/docker.sock · SSH 隧道", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "ZD3xB", + "name": "Caret", + "width": 12, + "height": 12, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + }, + { + "type": "frame", + "id": "GuOOP", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "XME9y", + "name": "Search", + "width": 300, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "nW7xv", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "search", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "gisXG", + "name": "Placeholder", + "fill": "$text-muted", + "content": "搜索容器、镜像、卷、网络…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "QNuug", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "LfOBj", + "name": "Kbd", + "height": 16, + "fill": "$bg-hover", + "cornerRadius": 3, + "padding": [ + 0, + 5 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "HB7v2", + "name": "Keys", + "fill": "$text-tertiary", + "content": "Ctrl K", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "id": "hOwui", + "type": "ref", + "ref": "x0Pcos", + "name": "Live Badge", + "descendants": { + "ODVko": { + "content": "实时" + } + } + }, + { + "id": "m2YkLf", + "type": "ref", + "ref": "yoK0T", + "name": "Settings", + "descendants": { + "XMGqY": { + "icon": "settings" + } + } + } + ] + }, + { + "type": "frame", + "id": "uqWyj", + "name": "Body", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "Ojhqx", + "name": "Rail", + "width": 56, + "height": "fill_container", + "fill": "$bg-rail", + "stroke": "$border", + "strokeWidth": { + "right": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 2, + "padding": [ + 6, + 0 + ], + "alignItems": "center", + "children": [ + { + "id": "VVOpb", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 总览", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "layout-dashboard", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "总览", + "fill": "$text-tertiary" + } + } + }, + { + "id": "hstRE", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 容器", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "box", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "容器", + "fill": "$text-tertiary" + } + } + }, + { + "id": "rlsrn", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 镜像", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "layers", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "镜像", + "fill": "$text-tertiary" + } + } + }, + { + "id": "Fe5Ek", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 卷", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "hard-drive", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "卷", + "fill": "$text-tertiary" + } + } + }, + { + "id": "B0PXA", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 网络", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "network", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "网络", + "fill": "$text-tertiary" + } + } + }, + { + "id": "kF3qe", + "type": "ref", + "ref": "nKcgr", + "name": "Nav Compose", + "descendants": { + "cQhxS": { + "fill": "#00000000" + }, + "F1F9E": { + "icon": "boxes", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "Compose", + "fill": "$text-tertiary" + } + } + }, + { + "id": "Rr0ov", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 系统", + "descendants": { + "cQhxS": { + "fill": "$accent-dim" + }, + "F1F9E": { + "icon": "gauge", + "fill": "$accent" + }, + "DE5G9": { + "content": "系统", + "fill": "$accent" + } + } + }, + { + "type": "frame", + "id": "U00Pu", + "name": "Spacer", + "width": 1, + "height": "fill_container", + "fill": "#00000000" + }, + { + "id": "SmCWM", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 设置", + "descendants": { + "F1F9E": { + "icon": "settings" + }, + "DE5G9": { + "content": "设置" + } + } + } + ] + }, + { + "type": "frame", + "id": "ZTCgL", + "name": "Main", + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "bIRsy", + "name": "Toolbar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 9, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "T7oiJy", + "name": "Title", + "fill": "$text-primary", + "content": "系统", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "text", + "id": "bqpzB", + "name": "Count", + "fill": "$text-muted", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "yqeCo", + "name": "Filter Segmented", + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 2, + "padding": 2, + "children": [ + { + "id": "DoMws", + "type": "ref", + "ref": "MdmRT", + "name": "F 全部 18", + "height": 20, + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "全部 18", + "fontSize": 11, + "fill": "$text-primary" + } + } + }, + { + "id": "JkYNh", + "type": "ref", + "ref": "MdmRT", + "name": "F 运行中 12", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "运行中 12", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + }, + { + "id": "P4p8O9", + "type": "ref", + "ref": "MdmRT", + "name": "F 已停止 5", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "已停止 5", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + }, + { + "id": "gmRyA", + "type": "ref", + "ref": "MdmRT", + "name": "F 异常 1", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "异常 1", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + } + ] + }, + { + "type": "frame", + "id": "YYfJL", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "MJ1YN", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 拉取镜像", + "descendants": { + "RQ8pM": { + "icon": "arrow-down-to-line" + }, + "P0aUw": { + "content": "拉取镜像" + } + } + }, + { + "id": "kHH80", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 新建 Compose", + "descendants": { + "RQ8pM": { + "icon": "file-code" + }, + "P0aUw": { + "content": "新建 Compose" + } + } + }, + { + "id": "f5QeO", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 运行容器", + "fill": "$accent", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "plus", + "fill": "$accent-on" + }, + "P0aUw": { + "content": "运行容器…", + "fill": "$accent-on", + "fontWeight": "500" + } + } + }, + { + "id": "z20jK", + "type": "ref", + "ref": "yoK0T", + "name": "Refresh", + "descendants": { + "XMGqY": { + "icon": "refresh-cw" + } + } + } + ] + }, + { + "type": "frame", + "id": "JPRYK", + "name": "Content", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-page", + "layout": "vertical", + "gap": 14, + "padding": 16, + "children": [ + { + "type": "frame", + "id": "uEY5K", + "name": "Stats", + "width": "fill_container", + "gap": 12, + "children": [ + { + "id": "ouDyX", + "type": "ref", + "ref": "sr5Cb", + "name": "St 容器", + "width": "fill_container", + "descendants": { + "RbtQX": { + "icon": "box", + "fill": "$accent" + }, + "T2It9": { + "content": "容器" + }, + "X8FzB": { + "content": "18" + }, + "UEarH": { + "content": "运行 12 · 停止 5 · 异常 1" + } + } + }, + { + "id": "ivYSK", + "type": "ref", + "ref": "sr5Cb", + "name": "St 镜像", + "width": "fill_container", + "descendants": { + "RbtQX": { + "icon": "layers", + "fill": "$violet" + }, + "T2It9": { + "content": "镜像" + }, + "X8FzB": { + "content": "34" + }, + "UEarH": { + "content": "20 使用中 · 2 悬空" + } + } + }, + { + "id": "WWZ1j", + "type": "ref", + "ref": "sr5Cb", + "name": "St 卷", + "width": "fill_container", + "descendants": { + "RbtQX": { + "icon": "hard-drive", + "fill": "$ok" + }, + "T2It9": { + "content": "卷" + }, + "X8FzB": { + "content": "9" + }, + "UEarH": { + "content": "24.8 GB · 2 未使用" + } + } + }, + { + "id": "XfJ2h", + "type": "ref", + "ref": "sr5Cb", + "name": "St 网络", + "width": "fill_container", + "descendants": { + "RbtQX": { + "icon": "network", + "fill": "$info" + }, + "T2It9": { + "content": "网络" + }, + "X8FzB": { + "content": "6" + }, + "UEarH": { + "content": "3 自定义 · 3 内置" + } + } + }, + { + "id": "tyO3r", + "type": "ref", + "ref": "sr5Cb", + "name": "St 构建缓存", + "width": "fill_container", + "descendants": { + "RbtQX": { + "icon": "database-backup", + "fill": "$warn" + }, + "T2It9": { + "content": "构建缓存" + }, + "X8FzB": { + "content": "6.2 GB" + }, + "UEarH": { + "content": "全部可回收" + } + } + } + ] + }, + { + "type": "frame", + "id": "PSyOx", + "name": "Sec 磁盘占用", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "e51maf", + "name": "H", + "width": "fill_container", + "height": 22, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "NyyVs", + "name": "T", + "fill": "$text-primary", + "content": "磁盘占用", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "ZxSVx", + "name": "Sub", + "fill": "$text-muted", + "content": "docker system df -v · 总 94 GB 中已用 38.4 GB", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "fNt20", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "t0yxVk", + "name": "A", + "fill": "$accent", + "content": "查看明细", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "EAcHe", + "name": "Bar", + "clip": true, + "width": "fill_container", + "height": 26, + "cornerRadius": "$r-sm", + "gap": 2, + "children": [ + { + "type": "frame", + "id": "p5wQl", + "name": "镜像 14.2 GB", + "width": "fill_container", + "height": 26, + "fill": "$accent" + }, + { + "type": "frame", + "id": "LudjF", + "name": "容器可写层 3.8 GB", + "width": "fill_container", + "height": 26, + "fill": "$violet" + }, + { + "type": "frame", + "id": "BAeur", + "name": "卷 24.8 GB", + "width": "fill_container", + "height": 26, + "fill": "$ok" + }, + { + "type": "frame", + "id": "X2lrJ", + "name": "构建缓存 6.2 GB", + "width": "fill_container", + "height": 26, + "fill": "$warn" + }, + { + "type": "frame", + "id": "yIF0v", + "name": "空闲 55.6 GB", + "width": "fill_container", + "height": 26, + "fill": "$bg-active" + } + ] + }, + { + "type": "frame", + "id": "tCi0d", + "name": "Legend", + "width": "fill_container", + "gap": 16, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "lrlNf", + "name": "L", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 2, + "id": "q2Jje", + "name": "S", + "fill": "$accent", + "width": 9, + "height": 9 + }, + { + "type": "text", + "id": "lwgms", + "name": "T", + "fill": "$text-tertiary", + "content": "镜像 14.2 GB", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "bvTEc", + "name": "L", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 2, + "id": "dwTcF", + "name": "S", + "fill": "$violet", + "width": 9, + "height": 9 + }, + { + "type": "text", + "id": "oFSEz", + "name": "T", + "fill": "$text-tertiary", + "content": "容器可写层 3.8 GB", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Uf5R7", + "name": "L", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 2, + "id": "NuUcy", + "name": "S", + "fill": "$ok", + "width": 9, + "height": 9 + }, + { + "type": "text", + "id": "jH5Mw", + "name": "T", + "fill": "$text-tertiary", + "content": "卷 24.8 GB", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "M4KW1", + "name": "L", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 2, + "id": "XWYWR", + "name": "S", + "fill": "$warn", + "width": 9, + "height": 9 + }, + { + "type": "text", + "id": "k2dEP", + "name": "T", + "fill": "$text-tertiary", + "content": "构建缓存 6.2 GB", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "R7j19", + "name": "L", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 2, + "id": "Bm8Yn", + "name": "S", + "fill": "$bg-active", + "width": 9, + "height": 9 + }, + { + "type": "text", + "id": "ClPh1", + "name": "T", + "fill": "$text-tertiary", + "content": "空闲 55.6 GB", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "EMcdS", + "name": "Table", + "clip": true, + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "V7SSr", + "name": "TH", + "width": "fill_container", + "height": 28, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "CX5lA", + "name": "H", + "width": "fill_container", + "height": 28, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "k20Oti", + "name": "T", + "fill": "$text-muted", + "content": "类型", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "Qf7S9", + "name": "H", + "width": 110, + "height": 28, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "pJK7c", + "name": "T", + "fill": "$text-muted", + "content": "总数", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "jgXgj", + "name": "H", + "width": 110, + "height": 28, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "BKewP", + "name": "T", + "fill": "$text-muted", + "content": "活跃", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "UbAzy", + "name": "H", + "width": 130, + "height": 28, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "h3Yd0u", + "name": "T", + "fill": "$text-muted", + "content": "占用", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "X2joH", + "name": "H", + "width": 180, + "height": 28, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "r30tM3", + "name": "T", + "fill": "$text-muted", + "content": "可回收", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "frame", + "id": "llbVX", + "name": "R 镜像", + "width": "fill_container", + "height": 30, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "c5mvg", + "name": "C", + "clip": true, + "width": "fill_container", + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "lJj2p", + "name": "T", + "fill": "$text-primary", + "content": "镜像", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "CanTd", + "name": "C", + "clip": true, + "width": 110, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "tQDKJ", + "name": "T", + "fill": "$text-secondary", + "content": "34", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "KVvfm", + "name": "C", + "clip": true, + "width": 110, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "rlloq", + "name": "T", + "fill": "$text-secondary", + "content": "20", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "vuQNq", + "name": "C", + "clip": true, + "width": 130, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "DAvnu", + "name": "T", + "fill": "$text-primary", + "content": "14.2 GB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "w5cHN", + "name": "C", + "clip": true, + "width": 180, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "BZME1", + "name": "T", + "fill": "$warn", + "content": "5.8 GB (41%)", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "DYHIu", + "name": "R 容器", + "width": "fill_container", + "height": 30, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "XCL8k", + "name": "C", + "clip": true, + "width": "fill_container", + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "l6gvmj", + "name": "T", + "fill": "$text-primary", + "content": "容器", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "MW9eO", + "name": "C", + "clip": true, + "width": 110, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "C5vAv", + "name": "T", + "fill": "$text-secondary", + "content": "18", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "zflkj", + "name": "C", + "clip": true, + "width": 110, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "XoltL", + "name": "T", + "fill": "$text-secondary", + "content": "12", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "sly4e", + "name": "C", + "clip": true, + "width": 130, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "v3bvb", + "name": "T", + "fill": "$text-primary", + "content": "3.8 GB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "K663u", + "name": "C", + "clip": true, + "width": 180, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Uo0wx", + "name": "T", + "fill": "$warn", + "content": "1.1 GB (29%)", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "pysDw", + "name": "R 本地卷", + "width": "fill_container", + "height": 30, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "KVChx", + "name": "C", + "clip": true, + "width": "fill_container", + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "u2eDi1", + "name": "T", + "fill": "$text-primary", + "content": "本地卷", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ERrgC", + "name": "C", + "clip": true, + "width": 110, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "qwoPE", + "name": "T", + "fill": "$text-secondary", + "content": "9", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "b8TRjh", + "name": "C", + "clip": true, + "width": 110, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "GBekP", + "name": "T", + "fill": "$text-secondary", + "content": "7", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "F2kUTR", + "name": "C", + "clip": true, + "width": 130, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "jHjlE", + "name": "T", + "fill": "$text-primary", + "content": "24.8 GB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "xOedk", + "name": "C", + "clip": true, + "width": 180, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Og5Q9", + "name": "T", + "fill": "$text-muted", + "content": "1.8 MB (0%)", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "V67goG", + "name": "R 构建缓存", + "width": "fill_container", + "height": 30, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "xrwqC", + "name": "C", + "clip": true, + "width": "fill_container", + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "q59FC7", + "name": "T", + "fill": "$text-primary", + "content": "构建缓存", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "jF32o", + "name": "C", + "clip": true, + "width": 110, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "M0vJI1", + "name": "T", + "fill": "$text-secondary", + "content": "218", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "rtkLN", + "name": "C", + "clip": true, + "width": 110, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "X8mhPx", + "name": "T", + "fill": "$text-secondary", + "content": "0", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "M20Gi", + "name": "C", + "clip": true, + "width": 130, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "a09s3", + "name": "T", + "fill": "$text-primary", + "content": "6.2 GB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "OjYYr", + "name": "C", + "clip": true, + "width": 180, + "height": 30, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ZcTKO", + "name": "T", + "fill": "$danger", + "content": "6.2 GB (100%)", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "sy8Jy", + "name": "Sec 空间回收", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "H8eFB", + "name": "H", + "width": "fill_container", + "height": 22, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ksEKg", + "name": "T", + "fill": "$text-primary", + "content": "空间回收", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "ySSmH", + "name": "Sub", + "fill": "$text-muted", + "content": "每一项都会先弹出面板内确认框,里面写清将要执行的动作与后果", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Koprw", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "yOK0G", + "name": "PR0", + "width": "fill_container", + "gap": 12, + "children": [ + { + "type": "frame", + "id": "D3V2g", + "name": "P 已停止的容器", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 7, + "padding": [ + 11, + 13 + ], + "children": [ + { + "type": "frame", + "id": "Fc7vr", + "name": "H", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "dcsN8", + "name": "I", + "width": 13, + "height": 13, + "icon": "trash-2", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "TWI61", + "name": "T", + "fill": "$text-primary", + "content": "已停止的容器", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "jJKec", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "j207Co", + "type": "ref", + "ref": "x0Pcos", + "name": "Tag", + "height": 16, + "fill": "$ok-dim", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "安全", + "fill": "$ok", + "fontSize": 9 + } + } + } + ] + }, + { + "type": "text", + "id": "Z5Blah", + "name": "D", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "删除所有 exited 状态的容器。运行中的不受影响。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Pa8zi", + "name": "F", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "FkNkb", + "name": "Sz", + "fill": "$text-secondary", + "content": "可回收 1.1 GB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "RE5Yp", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "K6Thn6", + "type": "ref", + "ref": "i1OTz", + "name": "Go", + "height": 24, + "stroke": "$border", + "descendants": { + "RQ8pM": { + "enabled": false + }, + "P0aUw": { + "content": "回收…", + "fontSize": 11, + "fill": "$text-secondary" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "MJn3P", + "name": "P 悬空镜像", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 7, + "padding": [ + 11, + 13 + ], + "children": [ + { + "type": "frame", + "id": "vHxIP", + "name": "H", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "S4HWH", + "name": "I", + "width": 13, + "height": 13, + "icon": "circle-dashed", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "owTnX", + "name": "T", + "fill": "$text-primary", + "content": "悬空镜像", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "xTUWF", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "VQP6v", + "type": "ref", + "ref": "x0Pcos", + "name": "Tag", + "height": 16, + "fill": "$ok-dim", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "安全", + "fill": "$ok", + "fontSize": 9 + } + } + } + ] + }, + { + "type": "text", + "id": "hK3yr", + "name": "D", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "没有标签、也没有容器引用的中间层。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "eM95p", + "name": "F", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "JDGzA", + "name": "Sz", + "fill": "$text-secondary", + "content": "可回收 2.4 GB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "rPWcw", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "RwIby", + "type": "ref", + "ref": "i1OTz", + "name": "Go", + "height": 24, + "stroke": "$border", + "descendants": { + "RQ8pM": { + "enabled": false + }, + "P0aUw": { + "content": "回收…", + "fontSize": 11, + "fill": "$text-secondary" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "tfwHe", + "name": "P 全部未使用镜像", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 7, + "padding": [ + 11, + 13 + ], + "children": [ + { + "type": "frame", + "id": "kMotp", + "name": "H", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "Rnxtv", + "name": "I", + "width": 13, + "height": 13, + "icon": "layers", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "text", + "id": "QPqdz", + "name": "T", + "fill": "$text-primary", + "content": "全部未使用镜像", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "gBcWc", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "WcGyA", + "type": "ref", + "ref": "x0Pcos", + "name": "Tag", + "height": 16, + "fill": "$warn-dim", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "注意", + "fill": "$warn", + "fontSize": 9 + } + } + } + ] + }, + { + "type": "text", + "id": "xcgW3", + "name": "D", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "含有标签但当前无容器在用的镜像;重新拉取需要时间与带宽。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "m4TESm", + "name": "F", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "sA1r3", + "name": "Sz", + "fill": "$warn", + "content": "可回收 5.8 GB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "o5txZO", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "Q6fzt", + "type": "ref", + "ref": "i1OTz", + "name": "Go", + "height": 24, + "stroke": "$border", + "descendants": { + "RQ8pM": { + "enabled": false + }, + "P0aUw": { + "content": "回收…", + "fontSize": 11, + "fill": "$text-secondary" + } + } + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "c8b9e7", + "name": "PR1", + "width": "fill_container", + "gap": 12, + "children": [ + { + "type": "frame", + "id": "G5KViD", + "name": "P 构建缓存", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 7, + "padding": [ + 11, + 13 + ], + "children": [ + { + "type": "frame", + "id": "XYRQg", + "name": "H", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "ieOXV", + "name": "I", + "width": 13, + "height": 13, + "icon": "database-backup", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "xfq7G", + "name": "T", + "fill": "$text-primary", + "content": "构建缓存", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "kmgsq", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "pU1yR", + "type": "ref", + "ref": "x0Pcos", + "name": "Tag", + "height": 16, + "fill": "$ok-dim", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "安全", + "fill": "$ok", + "fontSize": 9 + } + } + } + ] + }, + { + "type": "text", + "id": "vGyxD", + "name": "D", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "218 条缓存记录。下次构建会明显变慢。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "A3eRvb", + "name": "F", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "xTDNY", + "name": "Sz", + "fill": "$text-secondary", + "content": "可回收 6.2 GB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "jpdhI", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "Lb7Vf", + "type": "ref", + "ref": "i1OTz", + "name": "Go", + "height": 24, + "stroke": "$border", + "descendants": { + "RQ8pM": { + "enabled": false + }, + "P0aUw": { + "content": "回收…", + "fontSize": 11, + "fill": "$text-secondary" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "WIArC", + "name": "P 以上全部", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$danger-dim", + "strokeWidth": 1, + "layout": "vertical", + "gap": 7, + "padding": [ + 11, + 13 + ], + "children": [ + { + "type": "frame", + "id": "btoEk", + "name": "H", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "w3fgkB", + "name": "I", + "width": 13, + "height": 13, + "icon": "brush-cleaning", + "library": "lucide", + "fill": "$danger" + }, + { + "type": "text", + "id": "SVlI1", + "name": "T", + "fill": "$text-primary", + "content": "以上全部", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "WfBAh", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "GyQTl", + "type": "ref", + "ref": "x0Pcos", + "name": "Tag", + "height": 16, + "fill": "$danger-dim", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "危险", + "fill": "$danger", + "fontSize": 9 + } + } + } + ] + }, + { + "type": "text", + "id": "XRj2J", + "name": "D", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "等价于 system prune -a,不含卷。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "KYael", + "name": "F", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "eN49u", + "name": "Sz", + "fill": "$danger", + "content": "可回收 12.4 GB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "aea1b", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "NRhBN", + "type": "ref", + "ref": "i1OTz", + "name": "Go", + "height": 24, + "stroke": "$danger", + "descendants": { + "RQ8pM": { + "enabled": false + }, + "P0aUw": { + "content": "回收…", + "fontSize": 11, + "fill": "$danger" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "MGazT", + "name": "P 以上全部 + 卷", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$danger-dim", + "strokeWidth": 1, + "layout": "vertical", + "gap": 7, + "padding": [ + 11, + 13 + ], + "children": [ + { + "type": "frame", + "id": "XeLfA", + "name": "H", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "bd2ZN", + "name": "I", + "width": 13, + "height": 13, + "icon": "shield-alert", + "library": "lucide", + "fill": "$danger" + }, + { + "type": "text", + "id": "eg969", + "name": "T", + "fill": "$text-primary", + "content": "以上全部 + 卷", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "fviVN", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "L3ina", + "type": "ref", + "ref": "x0Pcos", + "name": "Tag", + "height": 16, + "fill": "$danger-dim", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "会丢数据", + "fill": "$danger", + "fontSize": 9 + } + } + } + ] + }, + { + "type": "text", + "id": "g5Nmp", + "name": "D", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "会删除未被任何容器使用的卷 —— 数据不可恢复,需手打 delete。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "j1as7i", + "name": "F", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "q8rhP", + "name": "Sz", + "fill": "$danger", + "content": "可回收 12.4 GB + 1.8 MB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "V62kMM", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "ZLfky", + "type": "ref", + "ref": "i1OTz", + "name": "Go", + "height": 24, + "stroke": "$danger", + "descendants": { + "RQ8pM": { + "enabled": false + }, + "P0aUw": { + "content": "回收…", + "fontSize": 11, + "fill": "$danger" + } + } + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "NMfYn", + "name": "Sec 引擎信息", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "frame", + "id": "XIIr3", + "name": "H", + "width": "fill_container", + "height": 22, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "FrxKK", + "name": "T", + "fill": "$text-primary", + "content": "引擎信息", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "fhzeL", + "name": "Sub", + "fill": "$text-muted", + "content": "GET /version · GET /info", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "efvR1", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "elFis", + "name": "Two", + "width": "fill_container", + "gap": 24, + "children": [ + { + "type": "frame", + "id": "G4CMbN", + "name": "Col", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "id": "W6WwR", + "type": "ref", + "ref": "l3qo4", + "name": "KV Engine 版本", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "Engine 版本", + "width": 110 + }, + "psgr6": { + "content": "27.3.1 (build 41ca978)" + } + } + }, + { + "id": "aoFMe", + "type": "ref", + "ref": "l3qo4", + "name": "KV API 版本", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "API 版本", + "width": 110 + }, + "psgr6": { + "content": "1.47 (最低 1.24)" + } + } + }, + { + "id": "w7KQEX", + "type": "ref", + "ref": "l3qo4", + "name": "KV 操作系统", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "操作系统", + "width": 110 + }, + "psgr6": { + "content": "Ubuntu 24.04.1 LTS · 6.8.0-45" + } + } + }, + { + "id": "iFR1c", + "type": "ref", + "ref": "l3qo4", + "name": "KV 架构 / CPU", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "架构 / CPU", + "width": 110 + }, + "psgr6": { + "content": "x86_64 · 16 核 · 62.7 GB" + } + } + } + ] + }, + { + "type": "frame", + "id": "n8BkJ2", + "name": "Col", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "id": "JRMqA", + "type": "ref", + "ref": "l3qo4", + "name": "KV 存储驱动", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "存储驱动", + "width": 110 + }, + "psgr6": { + "content": "overlay2 (extfs)" + } + } + }, + { + "id": "Pl6eu", + "type": "ref", + "ref": "l3qo4", + "name": "KV Cgroup", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "Cgroup", + "width": 110 + }, + "psgr6": { + "content": "v2 · systemd" + } + } + }, + { + "id": "CYCxL", + "type": "ref", + "ref": "l3qo4", + "name": "KV 日志驱动", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "日志驱动", + "width": 110 + }, + "psgr6": { + "content": "json-file · max-size 10m" + } + } + }, + { + "id": "QPJgl", + "type": "ref", + "ref": "l3qo4", + "name": "KV Rootless / Swarm", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "Rootless / Swarm", + "width": 110 + }, + "psgr6": { + "content": "否 / inactive" + } + } + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "Gj1P4", + "name": "Status Bar", + "width": "fill_container", + "height": 24, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 14, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "uWGSK", + "name": "Msg", + "fill": "$text-tertiary", + "content": "GET /system/df · 上次统计 2 分钟前 · 可回收 12.4 GB", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "xKPeM", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "dneFS", + "name": "API v1.47", + "fill": "$text-muted", + "content": "API v1.47", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "R1QD5j", + "name": "Engine 27.3.1", + "fill": "$text-muted", + "content": "Engine 27.3.1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "QBdFQ", + "name": "18 容器 · 34 镜像 · 9 卷", + "fill": "$text-muted", + "content": "18 容器 · 34 镜像 · 9 卷", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "GPin4", + "name": "事件流 已连接", + "fill": "$text-muted", + "content": "事件流 已连接", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "o5Fxh", + "x": 2619, + "y": 980, + "name": "03L · 日志 · 亮色", + "theme": { + "mode": "light" + }, + "clip": true, + "width": 1440, + "height": 860, + "fill": "$bg-page", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "g4O87", + "name": "Top Bar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "AlWhx", + "name": "Brand", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "vveNp", + "name": "Logo", + "width": 17, + "height": 17, + "icon": "container", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "q6ITaF", + "name": "Title", + "fill": "$text-primary", + "content": "Docker", + "fontFamily": "$font-ui", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "rectangle", + "id": "wQDgu", + "name": "Divider", + "fill": "$border", + "width": 1, + "height": 18 + }, + { + "type": "frame", + "id": "E8NMc", + "name": "Host Switcher", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "hDBOB", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "server", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "PNJwX", + "name": "Host", + "fill": "$text-primary", + "content": "prod-sg-01", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "K85gXv", + "name": "Transport", + "fill": "$text-muted", + "content": "/var/run/docker.sock · SSH 隧道", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "A1zqG", + "name": "Caret", + "width": 12, + "height": 12, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + }, + { + "type": "frame", + "id": "Lwz9L", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "I5qs5Y", + "name": "Search", + "width": 300, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "dE5al", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "search", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "kSTYE", + "name": "Placeholder", + "fill": "$text-muted", + "content": "搜索容器、镜像、卷、网络…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Glfo1", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "uB63t", + "name": "Kbd", + "height": 16, + "fill": "$bg-hover", + "cornerRadius": 3, + "padding": [ + 0, + 5 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "kD4Qt", + "name": "Keys", + "fill": "$text-tertiary", + "content": "Ctrl K", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "id": "sqebk", + "type": "ref", + "ref": "x0Pcos", + "name": "Live Badge", + "descendants": { + "ODVko": { + "content": "实时" + } + } + }, + { + "id": "ln3JK", + "type": "ref", + "ref": "yoK0T", + "name": "Settings", + "descendants": { + "XMGqY": { + "icon": "settings" + } + } + } + ] + }, + { + "type": "frame", + "id": "E1bzTI", + "name": "Body", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "dwdcZ", + "name": "Rail", + "width": 56, + "height": "fill_container", + "fill": "$bg-rail", + "stroke": "$border", + "strokeWidth": { + "right": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 2, + "padding": [ + 6, + 0 + ], + "alignItems": "center", + "children": [ + { + "id": "dSwBd", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 总览", + "descendants": { + "F1F9E": { + "icon": "layout-dashboard", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "总览", + "fill": "$text-tertiary" + } + } + }, + { + "id": "WF216", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 容器", + "descendants": { + "cQhxS": { + "fill": "$accent-dim" + }, + "F1F9E": { + "icon": "box", + "fill": "$accent" + }, + "DE5G9": { + "content": "容器", + "fill": "$accent" + } + } + }, + { + "id": "sMzaz", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 镜像", + "descendants": { + "F1F9E": { + "icon": "layers", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "镜像", + "fill": "$text-tertiary" + } + } + }, + { + "id": "iT3zL", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 卷", + "descendants": { + "F1F9E": { + "icon": "hard-drive", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "卷", + "fill": "$text-tertiary" + } + } + }, + { + "id": "C5dILM", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 网络", + "descendants": { + "F1F9E": { + "icon": "network", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "网络", + "fill": "$text-tertiary" + } + } + }, + { + "id": "zFImv", + "type": "ref", + "ref": "nKcgr", + "name": "Nav Compose", + "descendants": { + "F1F9E": { + "icon": "boxes", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "Compose", + "fill": "$text-tertiary" + } + } + }, + { + "id": "Pec9t", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 系统", + "descendants": { + "F1F9E": { + "icon": "gauge", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "系统", + "fill": "$text-tertiary" + } + } + }, + { + "type": "frame", + "id": "Y0bnJj", + "name": "Spacer", + "width": 1, + "height": "fill_container", + "fill": "#00000000" + }, + { + "id": "xjDfM", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 设置", + "descendants": { + "F1F9E": { + "icon": "settings" + }, + "DE5G9": { + "content": "设置" + } + } + } + ] + }, + { + "type": "frame", + "id": "i6Wsr", + "name": "Sources", + "width": 232, + "height": "fill_container", + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "right": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "Rf0V0", + "name": "Head", + "width": "fill_container", + "height": 36, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ybLVQ", + "name": "Title", + "fill": "$text-secondary", + "content": "日志来源", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "kAMAk", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "W6hgn", + "name": "All", + "fill": "$accent", + "content": "全选", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "js720", + "name": "Filter", + "width": "fill_container", + "height": 34, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "mM4Sn", + "name": "Box", + "width": "fill_container", + "height": 24, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 6, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "aUHmG", + "name": "Icon", + "width": 11, + "height": 11, + "icon": "list-filter", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "Q8UtC", + "name": "Ph", + "fill": "$text-muted", + "content": "过滤容器…", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "m9iCeq", + "name": "Src nginx-proxy", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "$accent-dim", + "gap": 8, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "Jkqxt", + "name": "Check", + "fill": "$accent", + "width": 12, + "height": 12, + "stroke": "$accent", + "strokeWidth": 1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "u8IU7", + "name": "Swatch", + "fill": "$accent", + "width": 3, + "height": 12 + }, + { + "type": "text", + "id": "RzpPx", + "name": "Name", + "fill": "$text-primary", + "content": "nginx-proxy", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "T4YUcK", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "uDkb8", + "name": "St", + "fill": "$text-muted", + "content": "运行中", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "fH6Ls", + "name": "Src api-gateway", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "$accent-dim", + "gap": 8, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "q4XZmf", + "name": "Check", + "fill": "$accent", + "width": 12, + "height": 12, + "stroke": "$accent", + "strokeWidth": 1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "R5U2D", + "name": "Swatch", + "fill": "$violet", + "width": 3, + "height": 12 + }, + { + "type": "text", + "id": "WVg4K", + "name": "Name", + "fill": "$text-primary", + "content": "api-gateway", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "b7p8kw", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "iTbnm", + "name": "St", + "fill": "$text-muted", + "content": "运行中", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "suPay", + "name": "Src postgres-main", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 8, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "NcSel", + "name": "Check", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "Bnoz0", + "name": "Swatch", + "fill": "$ok", + "width": 3, + "height": 12 + }, + { + "type": "text", + "id": "sT6Jl", + "name": "Name", + "fill": "$text-secondary", + "content": "postgres-main", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "mho4a", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "CKJje", + "name": "St", + "fill": "$text-muted", + "content": "运行中", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "v93cq", + "name": "Src redis-cache", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 8, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "GRxx0", + "name": "Check", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "e4DPd", + "name": "Swatch", + "fill": "$ok", + "width": 3, + "height": 12 + }, + { + "type": "text", + "id": "FxMTB", + "name": "Name", + "fill": "$text-secondary", + "content": "redis-cache", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "NpDDs", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "QhOgG", + "name": "St", + "fill": "$text-muted", + "content": "运行中", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "K1ZrZL", + "name": "Src worker-1", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 8, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "TdngH", + "name": "Check", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "THPin", + "name": "Swatch", + "fill": "$warn", + "width": 3, + "height": 12 + }, + { + "type": "text", + "id": "lRAvD", + "name": "Name", + "fill": "$text-secondary", + "content": "worker-1", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "D2jFj", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "EgVOH", + "name": "St", + "fill": "$text-muted", + "content": "运行中", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Puh0M", + "name": "Src worker-2", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 8, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "WAAdA", + "name": "Check", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "kIRaP", + "name": "Swatch", + "fill": "$warn", + "width": 3, + "height": 12 + }, + { + "type": "text", + "id": "u71nB", + "name": "Name", + "fill": "$text-secondary", + "content": "worker-2", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "jEAgg", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "wP7eX", + "name": "St", + "fill": "$text-muted", + "content": "运行中", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "G35MLg", + "name": "Src minio", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 8, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "z3evq", + "name": "Check", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "U1wTks", + "name": "Swatch", + "fill": "$text-muted", + "width": 3, + "height": 12 + }, + { + "type": "text", + "id": "yCe7f", + "name": "Name", + "fill": "$text-secondary", + "content": "minio", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "aVOfR", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "LL4XB", + "name": "St", + "fill": "$text-muted", + "content": "已暂停", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "GcNBR", + "name": "Src grafana", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 8, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "M4Bb3", + "name": "Check", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "JsCcq", + "name": "Swatch", + "fill": "$text-muted", + "width": 3, + "height": 12 + }, + { + "type": "text", + "id": "xCL8M", + "name": "Name", + "fill": "$text-secondary", + "content": "grafana", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "cXW32", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Hhy80", + "name": "St", + "fill": "$text-muted", + "content": "已停止", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "y5eA9K", + "name": "Src loki", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 8, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "xkypS", + "name": "Check", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "yQJ52", + "name": "Swatch", + "fill": "$text-muted", + "width": 3, + "height": 12 + }, + { + "type": "text", + "id": "E2IiCn", + "name": "Name", + "fill": "$text-secondary", + "content": "loki", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "qQ1K1", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "zc5jR", + "name": "St", + "fill": "$text-muted", + "content": "已停止", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "tQEax", + "name": "Src certbot", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 8, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "XW2zx", + "name": "Check", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "rectangle", + "cornerRadius": 2, + "id": "DdhSl", + "name": "Swatch", + "fill": "$danger", + "width": 3, + "height": 12 + }, + { + "type": "text", + "id": "aOYhS", + "name": "Name", + "fill": "$text-secondary", + "content": "certbot", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "rEHn2", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "n8t8NY", + "name": "St", + "fill": "$text-muted", + "content": "退出 1", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "yJNtr", + "name": "Spacer", + "width": "fill_container", + "height": "fill_container", + "fill": "#00000000" + }, + { + "type": "frame", + "id": "xS0x8", + "name": "Foot", + "width": "fill_container", + "height": 30, + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 6, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "FkOJN", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "git-merge", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "k8tFGn", + "name": "T", + "fill": "$text-secondary", + "content": "合并 2 条流", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "GM0jX", + "name": "Main", + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "w0qm1", + "name": "Log Toolbar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "jrKU7", + "name": "Icon", + "width": 15, + "height": 15, + "icon": "scroll-text", + "library": "lucide", + "fill": "$text-secondary" + }, + { + "type": "text", + "id": "MLRwC", + "name": "Title", + "fill": "$text-primary", + "content": "日志", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "id": "AACUf", + "type": "ref", + "ref": "x0Pcos", + "name": "Chip nginx-proxy", + "fill": "#00000000", + "stroke": "$border", + "strokeWidth": 1, + "descendants": { + "kvk5k": { + "fill": "$accent" + }, + "ODVko": { + "content": "nginx-proxy", + "fill": "$text-secondary" + } + } + }, + { + "id": "RkYvo", + "type": "ref", + "ref": "x0Pcos", + "name": "Chip api-gateway", + "fill": "#00000000", + "stroke": "$border", + "strokeWidth": 1, + "descendants": { + "kvk5k": { + "fill": "$violet" + }, + "ODVko": { + "content": "api-gateway", + "fill": "$text-secondary" + } + } + }, + { + "type": "frame", + "id": "r8Pex3", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "nMBLs", + "name": "Tail Segmented", + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 2, + "padding": 2, + "children": [ + { + "id": "iaZl2", + "type": "ref", + "ref": "MdmRT", + "name": "N 100", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "100", + "fontSize": 10, + "fontFamily": "$font-mono", + "fill": "$text-tertiary" + } + } + }, + { + "id": "gMSft", + "type": "ref", + "ref": "MdmRT", + "name": "N 500", + "height": 20, + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "500", + "fontSize": 10, + "fontFamily": "$font-mono", + "fill": "$text-primary" + } + } + }, + { + "id": "tznaM", + "type": "ref", + "ref": "MdmRT", + "name": "N 2000", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "2000", + "fontSize": 10, + "fontFamily": "$font-mono", + "fill": "$text-tertiary" + } + } + }, + { + "id": "ZFNkV", + "type": "ref", + "ref": "MdmRT", + "name": "N 10k", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "10k", + "fontSize": 10, + "fontFamily": "$font-mono", + "fill": "$text-tertiary" + } + } + } + ] + }, + { + "id": "D9s6ZL", + "type": "ref", + "ref": "i1OTz", + "name": "T 时间戳", + "height": 22, + "fill": "$accent-dim", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "clock", + "fill": "$accent" + }, + "P0aUw": { + "content": "时间戳", + "fontSize": 11, + "fill": "$accent" + } + } + }, + { + "id": "l24AL", + "type": "ref", + "ref": "i1OTz", + "name": "T 换行", + "height": 22, + "fill": "#00000000", + "stroke": "$border", + "descendants": { + "RQ8pM": { + "icon": "wrap-text", + "fill": "$text-tertiary" + }, + "P0aUw": { + "content": "换行", + "fontSize": 11, + "fill": "$text-secondary" + } + } + }, + { + "id": "f05Ez5", + "type": "ref", + "ref": "i1OTz", + "name": "T 仅错误", + "height": 22, + "fill": "#00000000", + "stroke": "$border", + "descendants": { + "RQ8pM": { + "icon": "triangle-alert", + "fill": "$text-tertiary" + }, + "P0aUw": { + "content": "仅错误", + "fontSize": 11, + "fill": "$text-secondary" + } + } + }, + { + "type": "frame", + "id": "pEZfv", + "name": "Search", + "width": 210, + "height": 24, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$accent", + "strokeWidth": 1, + "gap": 6, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "LaVOa", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "search", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "R8GrO", + "name": "Q", + "fill": "$text-primary", + "content": "upstream", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Ctteg", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "OAY0L", + "name": "Cnt", + "fill": "$text-muted", + "content": "2/7", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ASnbv", + "name": "Re", + "fill": "$accent", + "content": ".*", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "eJVPG", + "type": "ref", + "ref": "i1OTz", + "name": "Follow", + "height": 24, + "fill": "$ok-dim", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "activity", + "fill": "$ok" + }, + "P0aUw": { + "content": "跟随中", + "fontSize": 11, + "fill": "$ok", + "fontWeight": "500" + } + } + }, + { + "id": "ACUjl", + "type": "ref", + "ref": "yoK0T", + "name": "A download", + "descendants": { + "XMGqY": { + "icon": "download" + } + } + }, + { + "id": "WVokE", + "type": "ref", + "ref": "yoK0T", + "name": "A copy", + "descendants": { + "XMGqY": { + "icon": "copy" + } + } + }, + { + "id": "vMyt8", + "type": "ref", + "ref": "yoK0T", + "name": "A eraser", + "descendants": { + "XMGqY": { + "icon": "eraser" + } + } + } + ] + }, + { + "type": "frame", + "id": "LGhPO", + "name": "Log Body", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-terminal", + "layout": "vertical", + "padding": [ + 8, + 0 + ], + "children": [ + { + "type": "frame", + "id": "kMniR", + "name": "Spacer", + "width": "fill_container", + "height": "fill_container", + "fill": "#00000000" + }, + { + "type": "frame", + "id": "Q3Dl8", + "name": "L 09:40:58.114", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "q5mNxk", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:40:58.114", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Z1sWrg", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "sMtrt", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "YXwf3", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "uZXKU", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"GET /health HTTP/1.1\" 200 2 \"-\" \"kube-probe/1.30\" rt=0.001", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Qgp7Q", + "name": "L 09:41:02.118", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "SMRF8", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:02.118", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "xgyKd", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "CPuNn", + "name": "Lv", + "fill": "$info", + "textGrowth": "fixed-width", + "width": 38, + "content": "INFO", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "F0FBT", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "xdi2c", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "kCPLR", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "KU2RD", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Py8Fr", + "name": "T", + "fill": "$ok", + "content": "\"cache warm complete\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "OahwC", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "dWA9f", + "name": "T", + "fill": "$info", + "content": "\"keys\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "A1MeH8", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Spvtq", + "name": "T", + "fill": "$warn", + "content": "18422", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "cGUWY", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "uIrMm", + "name": "T", + "fill": "$info", + "content": "\"dur_ms\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "xX7k9", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ymPiD", + "name": "T", + "fill": "$warn", + "content": "214", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "dapEJ", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "viru7", + "name": "L 09:41:03.442", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "s2iVCa", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:03.442", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "k5Kgw", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "oCIfV", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "s1yL5a", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "teIU5", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"GET /assets/app.8f31c2.js HTTP/1.1\" 200 184213 rt=0.012", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "NmrVP", + "name": "L 09:41:05.907", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "io9fi", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:05.907", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "x97MCr", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "tCkhl", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "WMRzW", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "QDqZm", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"GET /api/v1/orders?page=2 HTTP/1.1\" 200 4821 rt=0.043", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "w9qxx", + "name": "L 09:41:06.118", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "MA9fG", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:06.118", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "MHzEJ", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "qo08I", + "name": "Lv", + "fill": "$info", + "textGrowth": "fixed-width", + "width": 38, + "content": "INFO", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "KceF1", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "d0afV1", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "RxN07", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "uEUQ4", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "A3VDDv", + "name": "T", + "fill": "$ok", + "content": "\"request completed\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "jmOWI", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "K0m4p", + "name": "T", + "fill": "$info", + "content": "\"route\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ob3IE", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "XlIkR", + "name": "T", + "fill": "$ok", + "content": "\"/v1/orders\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "TvHBM", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "NsXqr", + "name": "T", + "fill": "$info", + "content": "\"status\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "jmpyP", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "dZJmb", + "name": "T", + "fill": "$warn", + "content": "200", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "C58Nhk", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "lNVqh", + "name": "T", + "fill": "$info", + "content": "\"dur_ms\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "F92RK", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ik2aQ", + "name": "T", + "fill": "$warn", + "content": "38", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "zpqJU", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "H1siNo", + "name": "L 09:41:08.331", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "DOWSz", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:08.331", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "f7I0kA", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "G6OArM", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "content": "DEBUG", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "zHWDp", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "U5FJK", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "pScpC", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "qfpqp", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "D5cPUx", + "name": "T", + "fill": "$ok", + "content": "\"db query\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "wRqcl", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "j5j3B", + "name": "T", + "fill": "$info", + "content": "\"sql\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "f18MI", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "XhgkL", + "name": "T", + "fill": "$ok", + "content": "\"SELECT * FROM orders LIMIT 50\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "m9kuwx", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "WEEnz", + "name": "T", + "fill": "$info", + "content": "\"dur_ms\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Bbx2N", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "zEkTG", + "name": "T", + "fill": "$warn", + "content": "11", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "uQHVU", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "l8dVc5", + "name": "L 09:41:11.002", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "wkwMr", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:11.002", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "bXlz5", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "STeY1", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Eh410", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "jkTqd", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"POST /api/v1/checkout HTTP/1.1\" 201 318 rt=0.211", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Y0yZn", + "name": "L 09:41:11.220", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "y7dAhT", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:11.220", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Knk8a", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "B2r9tN", + "name": "Lv", + "fill": "$info", + "textGrowth": "fixed-width", + "width": 38, + "content": "INFO", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ktSE1", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ukPaW", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "f1tac", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "g6Xb7E", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "r4nKl", + "name": "T", + "fill": "$ok", + "content": "\"payment authorized\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Dpe9S", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "xfdsl", + "name": "T", + "fill": "$info", + "content": "\"order\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "U1vkuu", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "PDdIS", + "name": "T", + "fill": "$ok", + "content": "\"ord_8812\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "P1ODFA", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "b5fvPZ", + "name": "T", + "fill": "$info", + "content": "\"amount\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "dC6Du", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "pDZgB", + "name": "T", + "fill": "$warn", + "content": "4299", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Ad3UY", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "GbD2o", + "name": "T", + "fill": "$info", + "content": "\"currency\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "PoQgQ", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "geYtB", + "name": "T", + "fill": "$ok", + "content": "\"SGD\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "VXKGJ", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Y8VVZ", + "name": "L 09:41:12.554", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dXcot", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:12.554", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "W4KVR", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "nUoJt", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ZbRTl", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "UPja3", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"GET /api/v1/cart HTTP/1.1\" 200 992 rt=0.008", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "k1ErTO", + "name": "L 09:41:14.771", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "kT145", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:14.771", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "x2my7I", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "kLnFq", + "name": "Lv", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 38, + "content": "WARN", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "nQ68M", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "bV6Tk", + "name": "T", + "fill": "$warn", + "content": "[warn] 29#29: *18421 ", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "dx2Di", + "name": "HL", + "height": 15, + "fill": "$warn-dim", + "cornerRadius": 2, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "uwbwg", + "name": "T", + "fill": "$warn", + "content": "upstream", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "B3rvfL", + "name": "T", + "fill": "$warn", + "content": " server temporarily disabled while reading response header", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "gQBN5", + "name": "L 09:41:14.772", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "$warn-dim", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "WySHO", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:14.772", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ObiCS", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "mPjbA", + "name": "Lv", + "fill": "$danger", + "textGrowth": "fixed-width", + "width": 38, + "content": "ERROR", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "A3vXnI", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "PMuXH", + "name": "T", + "fill": "$danger", + "content": "[error] 29#29: *18421 connect() failed (111: Connection refused) while connecting to ", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "dGKCM", + "name": "HL", + "height": 15, + "fill": "$warn-dim", + "cornerRadius": 2, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "XhMJp", + "name": "T", + "fill": "$warn", + "content": "upstream", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "u88ta4", + "name": "T", + "fill": "$danger", + "content": ", client: 172.20.0.1", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "i9jtXl", + "name": "L 09:41:14.930", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "IGzQi", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:14.930", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "B4q6m", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "pwL1g", + "name": "Lv", + "fill": "$danger", + "textGrowth": "fixed-width", + "width": 38, + "content": "ERROR", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "NHji3", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "g63APc", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "a0Gw91", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "oNpJO", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "BpMiR", + "name": "T", + "fill": "$ok", + "content": "\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Z5NKui", + "name": "HL", + "height": 15, + "fill": "$warn-dim", + "cornerRadius": 2, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "npWZ0", + "name": "T", + "fill": "$warn", + "content": "upstream", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "XBPNG", + "name": "T", + "fill": "$ok", + "content": " unavailable\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "tObgK", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "k6AKGs", + "name": "T", + "fill": "$info", + "content": "\"target\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "dWKyO", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Q24SI8", + "name": "T", + "fill": "$ok", + "content": "\"payments-svc:8443\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Fjled", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ioHDc", + "name": "T", + "fill": "$info", + "content": "\"retry_in_ms\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Q83sJR", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "PfO8O", + "name": "T", + "fill": "$warn", + "content": "500", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "n2lz6", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "xQtFQ", + "name": "L 09:41:15.433", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "fNFCG", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:15.433", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "f1mH9y", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Q0Y7a", + "name": "Lv", + "fill": "$info", + "textGrowth": "fixed-width", + "width": 38, + "content": "INFO", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Zp58R", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "aoWGC", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Mb1gj", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "OZxVB", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "f7d6j", + "name": "T", + "fill": "$ok", + "content": "\"retry succeeded\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "K8NkQF", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "JQeuK", + "name": "T", + "fill": "$info", + "content": "\"attempt\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "AB9LO", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "WKWFI", + "name": "T", + "fill": "$warn", + "content": "2", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "X2Xnz", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Qlrhn", + "name": "T", + "fill": "$info", + "content": "\"target\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "DYb5f", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "nkBiX", + "name": "T", + "fill": "$ok", + "content": "\"payments-svc:8443\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "K7yecg", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "z1tAfa", + "name": "L 09:41:15.601", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "uKymN", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:15.601", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "z1O3H", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "H1og6", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "iYFrA", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "i0TRT", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"POST /api/v1/checkout HTTP/1.1\" 201 318 rt=0.664", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "sYizp", + "name": "L 09:41:18.204", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "W2g3u", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:18.204", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "S1NDjL", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "GAiDX", + "name": "Lv", + "fill": "$info", + "textGrowth": "fixed-width", + "width": 38, + "content": "INFO", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "NTwJc", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "FHnCW", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "z5o7a", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "W7HNn", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Zqax1", + "name": "T", + "fill": "$ok", + "content": "\"order confirmed\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "V6CpB", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "vrJzY", + "name": "T", + "fill": "$info", + "content": "\"order\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "zwdnu", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "AvTpk", + "name": "T", + "fill": "$ok", + "content": "\"ord_8812\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "sd83d", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "TN0SB", + "name": "T", + "fill": "$info", + "content": "\"items\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "dQmPc", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "H0gCWl", + "name": "T", + "fill": "$warn", + "content": "3", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "yd70E", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Y7Cz6l", + "name": "T", + "fill": "$info", + "content": "\"total\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "S35J6K", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "L6l2Y", + "name": "T", + "fill": "$warn", + "content": "4299", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "NG9cp", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "EhGbB", + "name": "L 09:41:19.775", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "VPiWs", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:19.775", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "DMykw", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "fmbUS", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "bekvr", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "VV05z", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"GET /api/v1/orders/ord_8812 HTTP/1.1\" 200 1204 rt=0.019", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "YFs4A", + "name": "L 09:41:22.118", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "U2uJM", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:22.118", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "MOYpZ", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "o753p0", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "content": "DEBUG", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "SPjqe", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Gp8gA", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "eVN42", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ywy2a", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "xP7AP", + "name": "T", + "fill": "$ok", + "content": "\"emitting webhook\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "nOWHU", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "DA958", + "name": "T", + "fill": "$info", + "content": "\"event\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "JI4cQ", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "dm0cr", + "name": "T", + "fill": "$ok", + "content": "\"order.confirmed\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "UIAjP", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Hqr1h", + "name": "T", + "fill": "$info", + "content": "\"targets\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "d6O0Kj", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "DhxdU", + "name": "T", + "fill": "$warn", + "content": "2", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "O5R2j", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "KQaTy", + "name": "L 09:41:22.930", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Ebmmp", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:22.930", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "BbbGg", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "j2sjMT", + "name": "Lv", + "fill": "$info", + "textGrowth": "fixed-width", + "width": 38, + "content": "INFO", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "MlRXe", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "e7WKS", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "p5SWqH", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "paxNB", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "WmRmd", + "name": "T", + "fill": "$ok", + "content": "\"webhook delivered\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "rJJZR", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "hJaMn", + "name": "T", + "fill": "$info", + "content": "\"url\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "G1Vt2I", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "q3ouWn", + "name": "T", + "fill": "$ok", + "content": "\"https://hooks.acme.io/orders\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Hi99X", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "sxI8u", + "name": "T", + "fill": "$info", + "content": "\"status\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "nrG9K", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "qmBhP", + "name": "T", + "fill": "$warn", + "content": "200", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "lhV5Y", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "RmGGa", + "name": "L 09:41:24.006", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "i6Fwn8", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:24.006", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "VC9Eq", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "NU0Hx", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "atwhN", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "G48wqA", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"GET /health HTTP/1.1\" 200 2 \"-\" \"kube-probe/1.30\" rt=0.001", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ndNdB", + "name": "L 09:41:27.441", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "CYzfi", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:27.441", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "LqVfH", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "axOyz", + "name": "Lv", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": 38, + "content": "WARN", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "JLRxB", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "myAzG", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "U9R24I", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Z2H3g", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Pt1eY", + "name": "T", + "fill": "$ok", + "content": "\"connection pool near limit\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "PwFDx", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "R0EMF", + "name": "T", + "fill": "$info", + "content": "\"in_use\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "amamO", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "fVaao", + "name": "T", + "fill": "$warn", + "content": "47", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "tPhYC", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "e4hVF7", + "name": "T", + "fill": "$info", + "content": "\"max\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ohH2C", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "wOG1A", + "name": "T", + "fill": "$warn", + "content": "50", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "kIgdo", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "y1byOg", + "name": "L 09:41:29.118", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "iAuYr", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:29.118", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "T6znqJ", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "MNooF", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "G9k7OY", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mmemO", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"GET /api/v1/products?cat=shoes HTTP/1.1\" 200 22841 rt=0.087", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "PpjSH", + "name": "L 09:41:31.552", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "GbLvV", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:31.552", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "x53mY9", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "D1g4AZ", + "name": "Lv", + "fill": "$info", + "textGrowth": "fixed-width", + "width": 38, + "content": "INFO", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "giHKp", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "R4IjqU", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "g7zsfg", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "FK7RU", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "G8UEq", + "name": "T", + "fill": "$ok", + "content": "\"request completed\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Ce2Jg", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "wMI3S", + "name": "T", + "fill": "$info", + "content": "\"route\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "M3mTc", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "O3ZgRi", + "name": "T", + "fill": "$ok", + "content": "\"/v1/products\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "t6Fuu", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "rqaiq", + "name": "T", + "fill": "$info", + "content": "\"status\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "iMIfK", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "S9dTAC", + "name": "T", + "fill": "$warn", + "content": "200", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "LLeJz", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "G8l5eG", + "name": "T", + "fill": "$info", + "content": "\"dur_ms\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "b62wfy", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "e2lts", + "name": "T", + "fill": "$warn", + "content": "81", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "y5H4a", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "E00oH", + "name": "L 09:41:33.884", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "snoHU", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:33.884", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "t9CsL", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ZEFkv", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "v28Ke", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "rtz1j", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"GET /assets/hero.4a91.webp HTTP/1.1\" 200 421884 rt=0.041", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "mT12F", + "name": "L 09:41:35.117", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "xT8vf", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:35.117", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "pzJO4", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "X1EYhj", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "content": "DEBUG", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "iYNTI", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "inOT6", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "LHFGp", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "aA4hs", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "x28G6", + "name": "T", + "fill": "$ok", + "content": "\"cache hit\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Nq7Hx", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "lBNUw", + "name": "T", + "fill": "$info", + "content": "\"key\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "lbqPx", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "REf9p", + "name": "T", + "fill": "$ok", + "content": "\"products:cat=shoes:p1\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "V27W1", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "iAKtQ", + "name": "T", + "fill": "$info", + "content": "\"ttl_s\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "SU2lv", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "n5LCI", + "name": "T", + "fill": "$warn", + "content": "284", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "g2McP", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "L81kv", + "name": "L 09:41:38.229", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "h6Vmqf", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:38.229", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "q2heMB", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "RDJem", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "JQ3dk", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dFe8u", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"GET /health HTTP/1.1\" 200 2 \"-\" \"kube-probe/1.30\" rt=0.001", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "j3BElk", + "name": "L 09:41:40.663", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "k28Ao3", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:40.663", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "V0pQeM", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "VNevu", + "name": "Lv", + "fill": "$info", + "textGrowth": "fixed-width", + "width": 38, + "content": "INFO", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Z72RCk", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "bMp3j", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "zqqi8", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "i9vkGv", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "RQLgt", + "name": "T", + "fill": "$ok", + "content": "\"scheduled job started\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "T4hxRn", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "xBSr5", + "name": "T", + "fill": "$info", + "content": "\"job\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "c9BQG", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "oogHl", + "name": "T", + "fill": "$ok", + "content": "\"reconcile-inventory\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "MePwd", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "n0Usz", + "name": "L 09:41:44.901", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "irt2S", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:44.901", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "p8Ggkr", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "bHVfW", + "name": "Lv", + "fill": "$info", + "textGrowth": "fixed-width", + "width": 38, + "content": "INFO", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "NBxMa", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Qjqk9", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "H8mdcI", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "SydJ5", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "k5FjBQ", + "name": "T", + "fill": "$ok", + "content": "\"scheduled job finished\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "DDHcp", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "QaoFy", + "name": "T", + "fill": "$info", + "content": "\"job\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "xd5Ec", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "cbf6J", + "name": "T", + "fill": "$ok", + "content": "\"reconcile-inventory\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "n6j2n", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "LaTHK", + "name": "T", + "fill": "$info", + "content": "\"dur_ms\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "bY0Pk", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "lQjQU", + "name": "T", + "fill": "$warn", + "content": "4238", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "RYlpE", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "PaomA", + "name": "L 09:41:46.220", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "pUuPN", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:46.220", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Q0jA9l", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "E5nH2", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ip2n8", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "HmCdR", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"GET /api/v1/cart HTTP/1.1\" 200 992 rt=0.006", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "L1pFtq", + "name": "L 09:41:48.774", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "l8tfY", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:48.774", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "BGX5V", + "name": "Src", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 78, + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "HGJC3", + "name": "Lv", + "fill": "$info", + "textGrowth": "fixed-width", + "width": 38, + "content": "INFO", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "A2mHpT", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "tSWzT", + "name": "T", + "fill": "$text-muted", + "content": "{", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "y9RgSB", + "name": "T", + "fill": "$info", + "content": "\"msg\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "EjRPT", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "LddJT", + "name": "T", + "fill": "$ok", + "content": "\"request completed\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ARA5T", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "TFFQx", + "name": "T", + "fill": "$info", + "content": "\"route\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "EvK19", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "kUs6V", + "name": "T", + "fill": "$ok", + "content": "\"/v1/cart\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "VyIpw", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Co2pp", + "name": "T", + "fill": "$info", + "content": "\"status\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "eroJt", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Khhzx", + "name": "T", + "fill": "$warn", + "content": "200", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Yu9Y3", + "name": "T", + "fill": "$text-muted", + "content": ",", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "c9AJF", + "name": "T", + "fill": "$info", + "content": "\"dur_ms\"", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "IJcvy", + "name": "T", + "fill": "$text-muted", + "content": ":", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "c8VdWN", + "name": "T", + "fill": "$warn", + "content": "6", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "TsdFi", + "name": "T", + "fill": "$text-muted", + "content": "}", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "G3JAsA", + "name": "L 09:41:52.118", + "clip": true, + "width": "fill_container", + "height": 18, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mPXzb", + "name": "Ts", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 76, + "content": "09:41:52.118", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "X0rh2", + "name": "Src", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 78, + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "LqjYE", + "name": "Lv", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 38, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "vbDcy", + "name": "Msg", + "clip": true, + "width": "fill_container", + "height": 18, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "xbsn3", + "name": "T", + "fill": "$text-terminal", + "content": "172.20.0.1 - - \"GET /health HTTP/1.1\" 200 2 \"-\" \"kube-probe/1.30\" rt=0.001", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "b51EX", + "name": "Log Foot", + "width": "fill_container", + "height": 26, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 12, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "V8uF8e", + "name": "Live", + "fill": "$ok", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "d4sSE", + "name": "S", + "fill": "$ok", + "content": "跟随中 · 新行即时到达", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "dfgnH", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "tsEh6", + "name": "缓冲 318 KB / 512 KB(按行截断)", + "fill": "$text-muted", + "content": "缓冲 318 KB / 512 KB(按行截断)", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "jPy8N", + "name": "12,481 行", + "fill": "$text-muted", + "content": "12,481 行", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "E8O9tH", + "name": "stdout + stderr 按到达顺序合并", + "fill": "$text-muted", + "content": "stdout + stderr 按到达顺序合并", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "k4m5I", + "name": "Status Bar", + "width": "fill_container", + "height": 24, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 14, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "p3nTu", + "name": "Msg", + "fill": "$text-tertiary", + "content": "docker logs -f · 2 条流 · 已运行 4m 12s", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "h1fY3", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "ynkBe", + "name": "API v1.47", + "fill": "$text-muted", + "content": "API v1.47", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "a6YaJ", + "name": "Engine 27.3.1", + "fill": "$text-muted", + "content": "Engine 27.3.1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "l8Ux8", + "name": "18 容器 · 34 镜像 · 9 卷", + "fill": "$text-muted", + "content": "18 容器 · 34 镜像 · 9 卷", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "X1RFr", + "name": "事件流 已连接", + "fill": "$text-muted", + "content": "事件流 已连接", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "gLmQ5", + "x": 2619, + "y": 9800, + "name": "12L · 确认闸门 · 亮色", + "theme": { + "mode": "light" + }, + "clip": true, + "width": 1440, + "height": 860, + "fill": "$bg-page", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "KzpRB", + "name": "Top Bar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 10, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "rYuUC", + "name": "Brand", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "Y2MYuo", + "name": "Logo", + "width": 17, + "height": 17, + "icon": "container", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "urWcY", + "name": "Title", + "fill": "$text-primary", + "content": "Docker", + "fontFamily": "$font-ui", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "rectangle", + "id": "znx5c", + "name": "Divider", + "fill": "$border", + "width": 1, + "height": 18 + }, + { + "type": "frame", + "id": "IICE0", + "name": "Host Switcher", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "WDshE", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "server", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "F8Mfa7", + "name": "Host", + "fill": "$text-primary", + "content": "prod-sg-01", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "ZLaNJ", + "name": "Transport", + "fill": "$text-muted", + "content": "/var/run/docker.sock · SSH 隧道", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "Ws3tb", + "name": "Caret", + "width": 12, + "height": 12, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + }, + { + "type": "frame", + "id": "w4QFU", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "JM1gF", + "name": "Search", + "width": 300, + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "rl7KN", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "search", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "Ray3Q", + "name": "Placeholder", + "fill": "$text-muted", + "content": "搜索容器、镜像、卷、网络…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "qodMp", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "SOkNN", + "name": "Kbd", + "height": 16, + "fill": "$bg-hover", + "cornerRadius": 3, + "padding": [ + 0, + 5 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "KxBVk", + "name": "Keys", + "fill": "$text-tertiary", + "content": "Ctrl K", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "id": "kYHJT", + "type": "ref", + "ref": "x0Pcos", + "name": "Live Badge", + "descendants": { + "ODVko": { + "content": "实时" + } + } + }, + { + "id": "jPSjD", + "type": "ref", + "ref": "yoK0T", + "name": "Settings", + "descendants": { + "XMGqY": { + "icon": "settings" + } + } + } + ] + }, + { + "type": "frame", + "id": "L6Fib", + "name": "Body", + "width": "fill_container", + "height": "fill_container", + "children": [ + { + "type": "frame", + "id": "m7Yep", + "name": "Rail", + "width": 56, + "height": "fill_container", + "fill": "$bg-rail", + "stroke": "$border", + "strokeWidth": { + "right": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 2, + "padding": [ + 6, + 0 + ], + "alignItems": "center", + "children": [ + { + "id": "QROl2", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 总览", + "descendants": { + "F1F9E": { + "icon": "layout-dashboard", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "总览", + "fill": "$text-tertiary" + } + } + }, + { + "id": "J95nG", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 容器", + "descendants": { + "cQhxS": { + "fill": "$accent-dim" + }, + "F1F9E": { + "icon": "box", + "fill": "$accent" + }, + "DE5G9": { + "content": "容器", + "fill": "$accent" + } + } + }, + { + "id": "U6sV4s", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 镜像", + "descendants": { + "F1F9E": { + "icon": "layers", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "镜像", + "fill": "$text-tertiary" + } + } + }, + { + "id": "GZgOR", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 卷", + "descendants": { + "F1F9E": { + "icon": "hard-drive", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "卷", + "fill": "$text-tertiary" + } + } + }, + { + "id": "Ag0rf", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 网络", + "descendants": { + "F1F9E": { + "icon": "network", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "网络", + "fill": "$text-tertiary" + } + } + }, + { + "id": "QMbD1", + "type": "ref", + "ref": "nKcgr", + "name": "Nav Compose", + "descendants": { + "F1F9E": { + "icon": "boxes", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "Compose", + "fill": "$text-tertiary" + } + } + }, + { + "id": "YXbCv", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 系统", + "descendants": { + "F1F9E": { + "icon": "gauge", + "fill": "$text-tertiary" + }, + "DE5G9": { + "content": "系统", + "fill": "$text-tertiary" + } + } + }, + { + "type": "frame", + "id": "oHx3o", + "name": "Spacer", + "width": 1, + "height": "fill_container", + "fill": "#00000000" + }, + { + "id": "KXYbk", + "type": "ref", + "ref": "nKcgr", + "name": "Nav 设置", + "descendants": { + "F1F9E": { + "icon": "settings" + }, + "DE5G9": { + "content": "设置" + } + } + } + ] + }, + { + "type": "frame", + "id": "Y99VV5", + "name": "Main", + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "l7ybj", + "name": "Toolbar", + "width": "fill_container", + "height": 40, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 9, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "YQEQq", + "name": "Title", + "fill": "$text-primary", + "content": "容器", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "type": "text", + "id": "E3lE3n", + "name": "Count", + "fill": "$text-muted", + "content": "18", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "k3MFm5", + "name": "Filter Segmented", + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 2, + "padding": 2, + "children": [ + { + "id": "dEDEv", + "type": "ref", + "ref": "MdmRT", + "name": "F 全部 18", + "height": 20, + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "全部 18", + "fontSize": 11, + "fill": "$text-primary" + } + } + }, + { + "id": "KZOCH", + "type": "ref", + "ref": "MdmRT", + "name": "F 运行中 12", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "运行中 12", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + }, + { + "id": "l2XB9", + "type": "ref", + "ref": "MdmRT", + "name": "F 已停止 5", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "已停止 5", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + }, + { + "id": "daP4m", + "type": "ref", + "ref": "MdmRT", + "name": "F 异常 1", + "height": 20, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "异常 1", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + } + ] + }, + { + "type": "frame", + "id": "jA8U4", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "Rdjsc", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 拉取镜像", + "descendants": { + "RQ8pM": { + "icon": "arrow-down-to-line" + }, + "P0aUw": { + "content": "拉取镜像" + } + } + }, + { + "id": "asKsc", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 新建 Compose", + "descendants": { + "RQ8pM": { + "icon": "file-code" + }, + "P0aUw": { + "content": "新建 Compose" + } + } + }, + { + "id": "uE3Kl", + "type": "ref", + "ref": "i1OTz", + "name": "Btn 运行容器", + "fill": "$accent", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "plus", + "fill": "$accent-on" + }, + "P0aUw": { + "content": "运行容器…", + "fill": "$accent-on", + "fontWeight": "500" + } + } + }, + { + "id": "th7dq", + "type": "ref", + "ref": "yoK0T", + "name": "Refresh", + "descendants": { + "XMGqY": { + "icon": "refresh-cw" + } + } + } + ] + }, + { + "type": "frame", + "id": "QKaxc", + "name": "Selection Bar", + "width": "fill_container", + "height": 32, + "fill": "$accent-dim", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 7, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "q52ZTi", + "name": "Icon", + "width": 13, + "height": 13, + "icon": "check", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "AQZxk", + "name": "Count", + "fill": "$accent", + "content": "已选 2 个容器", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "rectangle", + "id": "F74IzT", + "name": "Divider", + "fill": "$border-strong", + "width": 1, + "height": 16 + }, + { + "id": "t8uX8t", + "type": "ref", + "ref": "i1OTz", + "name": "S 启动", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "play" + }, + "P0aUw": { + "content": "启动", + "fontSize": 11 + } + } + }, + { + "id": "o7gHnK", + "type": "ref", + "ref": "i1OTz", + "name": "S 停止", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "square" + }, + "P0aUw": { + "content": "停止", + "fontSize": 11 + } + } + }, + { + "id": "VZW0x", + "type": "ref", + "ref": "i1OTz", + "name": "S 重启", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "rotate-cw" + }, + "P0aUw": { + "content": "重启", + "fontSize": 11 + } + } + }, + { + "id": "VF0hA", + "type": "ref", + "ref": "i1OTz", + "name": "S 暂停", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "pause" + }, + "P0aUw": { + "content": "暂停", + "fontSize": 11 + } + } + }, + { + "id": "FWKZ3", + "type": "ref", + "ref": "i1OTz", + "name": "S 删除", + "height": 22, + "stroke": "$danger-dim", + "descendants": { + "RQ8pM": { + "icon": "trash-2", + "fill": "$danger" + }, + "P0aUw": { + "content": "删除", + "fontSize": 11, + "fill": "$danger" + } + } + }, + { + "type": "frame", + "id": "KckjK", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "bAjK7", + "name": "Clear", + "fill": "$text-tertiary", + "content": "取消选择", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "dOp0U", + "name": "Header Row", + "width": "fill_container", + "height": 26, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "ZayzZ", + "name": "Pad", + "width": 3, + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "tDNyf", + "name": "H Select", + "width": 26, + "height": 26, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "f0Fq9d", + "name": "Checkbox", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + } + ] + }, + { + "type": "frame", + "id": "GbRHv", + "name": "H 名称", + "clip": true, + "width": "fill_container", + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "VzQHp", + "name": "Label", + "fill": "$text-muted", + "content": "名称", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "ox0wj", + "name": "H 镜像", + "clip": true, + "width": 190, + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "rCXBC", + "name": "Label", + "fill": "$text-muted", + "content": "镜像", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "Jv9Wu", + "name": "H 端口", + "clip": true, + "width": 118, + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "UjqJ9", + "name": "Label", + "fill": "$text-muted", + "content": "端口", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "wK9xT", + "name": "H CPU", + "clip": true, + "width": 108, + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "jtyk7", + "name": "Label", + "fill": "$text-muted", + "content": "CPU", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "LGl23", + "name": "H 内存", + "clip": true, + "width": 84, + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "iGpzM", + "name": "Label", + "fill": "$text-muted", + "content": "内存", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "yu5dB", + "name": "H 运行时长", + "clip": true, + "width": 78, + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "v5YVn4", + "name": "Label", + "fill": "$text-muted", + "content": "运行时长", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "Z01lUH", + "name": "H 操作", + "clip": true, + "width": 92, + "height": 26, + "padding": [ + 0, + 2 + ], + "alignItems": "center" + } + ] + }, + { + "type": "frame", + "id": "F1L8qG", + "name": "List", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "id": "Q0hRML", + "type": "ref", + "ref": "OHo2s", + "name": "Row nginx-proxy", + "width": "fill_container", + "fill": "$bg-active", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "nginx-proxy", + "fill": "$text-primary" + }, + "VJq0n/ODVko": { + "content": "web-stack" + }, + "p4yL1G": { + "content": "nginx:1.27-alpine", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "80→8080" + }, + "rANgd/fshVw": { + "height": 4, + "fill": "$accent" + }, + "rANgd/srvmw": { + "height": 3, + "fill": "$accent" + }, + "rANgd/ky1P1": { + "height": 5, + "fill": "$accent" + }, + "rANgd/bqMzi": { + "height": 4, + "fill": "$accent" + }, + "rANgd/F5Kloq": { + "height": 6, + "fill": "$accent" + }, + "rANgd/StdGN": { + "height": 4, + "fill": "$accent" + }, + "rANgd/iq3Ux": { + "height": 3, + "fill": "$accent" + }, + "rANgd/PHY2v": { + "height": 5, + "fill": "$accent" + }, + "rANgd/xqaP5": { + "height": 7, + "fill": "$accent" + }, + "rANgd/cZHdc": { + "height": 4, + "fill": "$accent" + }, + "rANgd/jhPqP": { + "height": 3, + "fill": "$accent" + }, + "rANgd/Za8Yw": { + "height": 4, + "fill": "$accent" + }, + "rANgd/zbp8o": { + "height": 5, + "fill": "$accent" + }, + "rANgd/kMt0y": { + "height": 4, + "fill": "$accent" + }, + "NprKu": { + "content": "2.1%", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "128 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "3d 4h" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "dosFY", + "type": "ref", + "ref": "OHo2s", + "name": "Row api-gateway", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "api-gateway", + "fill": "$text-primary" + }, + "VJq0n/ODVko": { + "content": "web-stack" + }, + "p4yL1G": { + "content": "ghcr.io/acme/api:2.8.1", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "8000→8000" + }, + "rANgd/fshVw": { + "height": 6, + "fill": "$accent" + }, + "rANgd/srvmw": { + "height": 8, + "fill": "$accent" + }, + "rANgd/ky1P1": { + "height": 7, + "fill": "$accent" + }, + "rANgd/bqMzi": { + "height": 9, + "fill": "$accent" + }, + "rANgd/F5Kloq": { + "height": 11, + "fill": "$accent" + }, + "rANgd/StdGN": { + "height": 8, + "fill": "$accent" + }, + "rANgd/iq3Ux": { + "height": 6, + "fill": "$accent" + }, + "rANgd/PHY2v": { + "height": 7, + "fill": "$accent" + }, + "rANgd/xqaP5": { + "height": 10, + "fill": "$accent" + }, + "rANgd/cZHdc": { + "height": 12, + "fill": "$accent" + }, + "rANgd/jhPqP": { + "height": 9, + "fill": "$accent" + }, + "rANgd/Za8Yw": { + "height": 7, + "fill": "$accent" + }, + "rANgd/zbp8o": { + "height": 8, + "fill": "$accent" + }, + "rANgd/kMt0y": { + "height": 10, + "fill": "$accent" + }, + "NprKu": { + "content": "14.6%", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "512 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "3d 4h" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "dtOOY", + "type": "ref", + "ref": "OHo2s", + "name": "Row postgres-main", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "postgres-main", + "fill": "$text-primary" + }, + "VJq0n/ODVko": { + "content": "db-stack" + }, + "p4yL1G": { + "content": "postgres:16.4", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "5432" + }, + "rANgd/fshVw": { + "height": 3, + "fill": "$accent" + }, + "rANgd/srvmw": { + "height": 4, + "fill": "$accent" + }, + "rANgd/ky1P1": { + "height": 3, + "fill": "$accent" + }, + "rANgd/bqMzi": { + "height": 5, + "fill": "$accent" + }, + "rANgd/F5Kloq": { + "height": 4, + "fill": "$accent" + }, + "rANgd/StdGN": { + "height": 3, + "fill": "$accent" + }, + "rANgd/iq3Ux": { + "height": 4, + "fill": "$accent" + }, + "rANgd/PHY2v": { + "height": 6, + "fill": "$accent" + }, + "rANgd/xqaP5": { + "height": 4, + "fill": "$accent" + }, + "rANgd/cZHdc": { + "height": 3, + "fill": "$accent" + }, + "rANgd/jhPqP": { + "height": 4, + "fill": "$accent" + }, + "rANgd/Za8Yw": { + "height": 5, + "fill": "$accent" + }, + "rANgd/zbp8o": { + "height": 3, + "fill": "$accent" + }, + "rANgd/kMt0y": { + "height": 4, + "fill": "$accent" + }, + "NprKu": { + "content": "4.2%", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "1.2 GB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "12d 6h" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "xZJzR", + "type": "ref", + "ref": "OHo2s", + "name": "Row redis-cache", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "redis-cache", + "fill": "$text-primary" + }, + "VJq0n/ODVko": { + "content": "db-stack" + }, + "p4yL1G": { + "content": "redis:7.4-alpine", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "6379" + }, + "rANgd/fshVw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/srvmw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/ky1P1": { + "height": 3, + "fill": "$accent" + }, + "rANgd/bqMzi": { + "height": 2, + "fill": "$accent" + }, + "rANgd/F5Kloq": { + "height": 2, + "fill": "$accent" + }, + "rANgd/StdGN": { + "height": 3, + "fill": "$accent" + }, + "rANgd/iq3Ux": { + "height": 2, + "fill": "$accent" + }, + "rANgd/PHY2v": { + "height": 2, + "fill": "$accent" + }, + "rANgd/xqaP5": { + "height": 4, + "fill": "$accent" + }, + "rANgd/cZHdc": { + "height": 2, + "fill": "$accent" + }, + "rANgd/jhPqP": { + "height": 2, + "fill": "$accent" + }, + "rANgd/Za8Yw": { + "height": 3, + "fill": "$accent" + }, + "rANgd/zbp8o": { + "height": 2, + "fill": "$accent" + }, + "rANgd/kMt0y": { + "height": 2, + "fill": "$accent" + }, + "NprKu": { + "content": "0.8%", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "64 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "12d 6h" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "wa6D6", + "type": "ref", + "ref": "OHo2s", + "name": "Row worker-1", + "width": "fill_container", + "fill": "$accent-dim", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "T31V2": { + "fill": "$accent", + "stroke": "$accent" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "worker-1", + "fill": "$text-primary" + }, + "VJq0n/ODVko": { + "content": "jobs" + }, + "p4yL1G": { + "content": "ghcr.io/acme/worker:2.8.1", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "—" + }, + "rANgd/fshVw": { + "height": 9, + "fill": "$warn" + }, + "rANgd/srvmw": { + "height": 12, + "fill": "$warn" + }, + "rANgd/ky1P1": { + "height": 14, + "fill": "$warn" + }, + "rANgd/bqMzi": { + "height": 11, + "fill": "$warn" + }, + "rANgd/F5Kloq": { + "height": 15, + "fill": "$warn" + }, + "rANgd/StdGN": { + "height": 13, + "fill": "$warn" + }, + "rANgd/iq3Ux": { + "height": 10, + "fill": "$warn" + }, + "rANgd/PHY2v": { + "height": 14, + "fill": "$warn" + }, + "rANgd/xqaP5": { + "height": 15, + "fill": "$warn" + }, + "rANgd/cZHdc": { + "height": 12, + "fill": "$warn" + }, + "rANgd/jhPqP": { + "height": 13, + "fill": "$warn" + }, + "rANgd/Za8Yw": { + "height": 15, + "fill": "$warn" + }, + "rANgd/zbp8o": { + "height": 11, + "fill": "$warn" + }, + "rANgd/kMt0y": { + "height": 14, + "fill": "$warn" + }, + "NprKu": { + "content": "38.4%", + "fill": "$warn" + }, + "AdR38": { + "content": "890 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "6h 12m" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "A4qpk", + "type": "ref", + "ref": "OHo2s", + "name": "Row worker-2", + "width": "fill_container", + "fill": "$accent-dim", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "T31V2": { + "fill": "$accent", + "stroke": "$accent" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "worker-2", + "fill": "$text-primary" + }, + "VJq0n/ODVko": { + "content": "jobs" + }, + "p4yL1G": { + "content": "ghcr.io/acme/worker:2.8.1", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "—" + }, + "rANgd/fshVw": { + "height": 8, + "fill": "$warn" + }, + "rANgd/srvmw": { + "height": 11, + "fill": "$warn" + }, + "rANgd/ky1P1": { + "height": 13, + "fill": "$warn" + }, + "rANgd/bqMzi": { + "height": 10, + "fill": "$warn" + }, + "rANgd/F5Kloq": { + "height": 12, + "fill": "$warn" + }, + "rANgd/StdGN": { + "height": 14, + "fill": "$warn" + }, + "rANgd/iq3Ux": { + "height": 9, + "fill": "$warn" + }, + "rANgd/PHY2v": { + "height": 11, + "fill": "$warn" + }, + "rANgd/xqaP5": { + "height": 13, + "fill": "$warn" + }, + "rANgd/cZHdc": { + "height": 10, + "fill": "$warn" + }, + "rANgd/jhPqP": { + "height": 12, + "fill": "$warn" + }, + "rANgd/Za8Yw": { + "height": 11, + "fill": "$warn" + }, + "rANgd/zbp8o": { + "height": 13, + "fill": "$warn" + }, + "rANgd/kMt0y": { + "height": 10, + "fill": "$warn" + }, + "NprKu": { + "content": "31.7%", + "fill": "$warn" + }, + "AdR38": { + "content": "812 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "6h 12m" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "SoM11", + "type": "ref", + "ref": "OHo2s", + "name": "Row minio", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$warn" + }, + "yMbTn": { + "fill": "$warn" + }, + "L6tp5": { + "content": "minio", + "fill": "$text-primary" + }, + "VJq0n": { + "enabled": false + }, + "p4yL1G": { + "content": "minio/minio:RELEASE.2024-09", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "9000→9000" + }, + "rANgd": { + "enabled": false + }, + "NprKu": { + "content": "已暂停", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "210 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "2d 1h" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "zr51X", + "type": "ref", + "ref": "OHo2s", + "name": "Row grafana", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$text-muted" + }, + "yMbTn": { + "fill": "$text-muted" + }, + "L6tp5": { + "content": "grafana", + "fill": "$text-tertiary" + }, + "VJq0n/ODVko": { + "content": "observability" + }, + "p4yL1G": { + "content": "grafana/grafana:11.2.0", + "fill": "$text-muted" + }, + "y2nlk": { + "content": "3000" + }, + "rANgd": { + "enabled": false + }, + "NprKu": { + "content": "—", + "fill": "$text-muted" + }, + "AdR38": { + "content": "—", + "fill": "$text-muted" + }, + "PeLHA": { + "content": "停止 5h" + }, + "ALIhr/XMGqY": { + "icon": "play" + } + } + }, + { + "id": "xKmaY", + "type": "ref", + "ref": "OHo2s", + "name": "Row loki", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$text-muted" + }, + "yMbTn": { + "fill": "$text-muted" + }, + "L6tp5": { + "content": "loki", + "fill": "$text-tertiary" + }, + "VJq0n/ODVko": { + "content": "observability" + }, + "p4yL1G": { + "content": "grafana/loki:3.1.1", + "fill": "$text-muted" + }, + "y2nlk": { + "content": "3100" + }, + "rANgd": { + "enabled": false + }, + "NprKu": { + "content": "—", + "fill": "$text-muted" + }, + "AdR38": { + "content": "—", + "fill": "$text-muted" + }, + "PeLHA": { + "content": "停止 5h" + }, + "ALIhr/XMGqY": { + "icon": "play" + } + } + }, + { + "id": "ta5da", + "type": "ref", + "ref": "OHo2s", + "name": "Row certbot", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$danger" + }, + "yMbTn": { + "fill": "$danger" + }, + "L6tp5": { + "content": "certbot", + "fill": "$text-tertiary" + }, + "VJq0n": { + "enabled": false + }, + "p4yL1G": { + "content": "certbot/certbot:v2.11.0", + "fill": "$text-muted" + }, + "y2nlk": { + "content": "—" + }, + "rANgd": { + "enabled": false + }, + "NprKu": { + "content": "—", + "fill": "$text-muted" + }, + "AdR38": { + "content": "—", + "fill": "$text-muted" + }, + "PeLHA": { + "content": "退出 (1)" + }, + "ALIhr/XMGqY": { + "icon": "play" + } + } + }, + { + "id": "RK8xD", + "type": "ref", + "ref": "OHo2s", + "name": "Row registry-mirror", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "registry-mirror", + "fill": "$text-primary" + }, + "VJq0n": { + "enabled": false + }, + "p4yL1G": { + "content": "registry:2.8.3", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "5000→5000" + }, + "rANgd/fshVw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/srvmw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/ky1P1": { + "height": 2, + "fill": "$accent" + }, + "rANgd/bqMzi": { + "height": 3, + "fill": "$accent" + }, + "rANgd/F5Kloq": { + "height": 2, + "fill": "$accent" + }, + "rANgd/StdGN": { + "height": 2, + "fill": "$accent" + }, + "rANgd/iq3Ux": { + "height": 2, + "fill": "$accent" + }, + "rANgd/PHY2v": { + "height": 2, + "fill": "$accent" + }, + "rANgd/xqaP5": { + "height": 3, + "fill": "$accent" + }, + "rANgd/cZHdc": { + "height": 2, + "fill": "$accent" + }, + "rANgd/jhPqP": { + "height": 2, + "fill": "$accent" + }, + "rANgd/Za8Yw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/zbp8o": { + "height": 2, + "fill": "$accent" + }, + "rANgd/kMt0y": { + "height": 2, + "fill": "$accent" + }, + "NprKu": { + "content": "0.4%", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "48 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "21d" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + }, + { + "id": "e8s6d", + "type": "ref", + "ref": "OHo2s", + "name": "Row watchtower", + "width": "fill_container", + "fill": "#00000000", + "descendants": { + "QxUB9": { + "fill": "$ok" + }, + "yMbTn": { + "fill": "$ok" + }, + "L6tp5": { + "content": "watchtower", + "fill": "$text-primary" + }, + "VJq0n": { + "enabled": false + }, + "p4yL1G": { + "content": "containrrr/watchtower:1.7.1", + "fill": "$text-secondary" + }, + "y2nlk": { + "content": "—" + }, + "rANgd/fshVw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/srvmw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/ky1P1": { + "height": 2, + "fill": "$accent" + }, + "rANgd/bqMzi": { + "height": 2, + "fill": "$accent" + }, + "rANgd/F5Kloq": { + "height": 2, + "fill": "$accent" + }, + "rANgd/StdGN": { + "height": 2, + "fill": "$accent" + }, + "rANgd/iq3Ux": { + "height": 2, + "fill": "$accent" + }, + "rANgd/PHY2v": { + "height": 2, + "fill": "$accent" + }, + "rANgd/xqaP5": { + "height": 2, + "fill": "$accent" + }, + "rANgd/cZHdc": { + "height": 2, + "fill": "$accent" + }, + "rANgd/jhPqP": { + "height": 2, + "fill": "$accent" + }, + "rANgd/Za8Yw": { + "height": 2, + "fill": "$accent" + }, + "rANgd/zbp8o": { + "height": 2, + "fill": "$accent" + }, + "rANgd/kMt0y": { + "height": 2, + "fill": "$accent" + }, + "NprKu": { + "content": "0.1%", + "fill": "$text-secondary" + }, + "AdR38": { + "content": "22 MB", + "fill": "$text-secondary" + }, + "PeLHA": { + "content": "21d" + }, + "ALIhr/XMGqY": { + "icon": "square" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "W7zlvS", + "name": "Detail Drawer", + "width": 440, + "height": "fill_container", + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "left": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "OXVDd", + "name": "Drawer Header", + "width": "fill_container", + "height": 44, + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "MRyTn", + "name": "Dot", + "fill": "$ok", + "width": 7, + "height": 7 + }, + { + "type": "text", + "id": "eX4yZ", + "name": "Name", + "fill": "$text-primary", + "content": "nginx-proxy", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "id": "mspei", + "type": "ref", + "ref": "x0Pcos", + "name": "State Chip", + "descendants": { + "ODVko": { + "content": "运行中 · healthy" + } + } + }, + { + "type": "frame", + "id": "p5QPTI", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "XNaSk", + "type": "ref", + "ref": "yoK0T", + "name": "H pin", + "descendants": { + "XMGqY": { + "icon": "pin" + } + } + }, + { + "id": "uzoaN", + "type": "ref", + "ref": "yoK0T", + "name": "H maximize-2", + "descendants": { + "XMGqY": { + "icon": "maximize-2" + } + } + }, + { + "id": "xQcma", + "type": "ref", + "ref": "yoK0T", + "name": "H x", + "descendants": { + "XMGqY": { + "icon": "x" + } + } + } + ] + }, + { + "type": "frame", + "id": "F1nQ4B", + "name": "Drawer Subhead", + "width": "fill_container", + "height": 26, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 7, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "W0fq9", + "name": "Id", + "fill": "$text-tertiary", + "content": "a3f2c81b9d4e", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "Lim2R", + "name": "Copy", + "width": 11, + "height": 11, + "icon": "copy", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "byt4n", + "name": "Sep", + "fill": "$text-muted", + "content": "·", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "F8pmd1", + "name": "Proj", + "fill": "$violet", + "content": "web-stack / proxy", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "d5hA9", + "name": "Drawer Tabs", + "width": "fill_container", + "height": 34, + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 2, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "id": "XFyjt", + "type": "ref", + "ref": "MdmRT", + "name": "T 概览", + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "icon": "info", + "fill": "$text-primary" + }, + "iscyV": { + "content": "概览", + "fill": "$text-primary", + "fontWeight": "500" + } + } + }, + { + "id": "l7iSIg", + "type": "ref", + "ref": "MdmRT", + "name": "T 日志", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "scroll-text", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "日志", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + }, + { + "id": "QlOC9", + "type": "ref", + "ref": "MdmRT", + "name": "T 文件", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "folder-tree", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "文件", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + }, + { + "id": "KPAJC", + "type": "ref", + "ref": "MdmRT", + "name": "T 终端", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "terminal", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "终端", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + }, + { + "id": "sZ47v", + "type": "ref", + "ref": "MdmRT", + "name": "T 统计", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "icon": "activity", + "fill": "$text-tertiary" + }, + "iscyV": { + "content": "统计", + "fill": "$text-tertiary", + "fontWeight": "400" + } + } + } + ] + }, + { + "type": "frame", + "id": "ajDwa", + "name": "Quick Actions", + "width": "fill_container", + "height": 42, + "fill": "$bg-elevated", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 6, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "id": "SjV4q", + "type": "ref", + "ref": "i1OTz", + "name": "Q 停止", + "descendants": { + "RQ8pM": { + "icon": "square" + }, + "P0aUw": { + "content": "停止" + } + } + }, + { + "id": "haAj9", + "type": "ref", + "ref": "i1OTz", + "name": "Q 重启", + "descendants": { + "RQ8pM": { + "icon": "rotate-cw" + }, + "P0aUw": { + "content": "重启" + } + } + }, + { + "id": "vGWDi", + "type": "ref", + "ref": "i1OTz", + "name": "Q 暂停", + "descendants": { + "RQ8pM": { + "icon": "pause" + }, + "P0aUw": { + "content": "暂停" + } + } + }, + { + "id": "nDvoB", + "type": "ref", + "ref": "i1OTz", + "name": "Q 终端", + "fill": "$accent-dim", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "terminal", + "fill": "$accent" + }, + "P0aUw": { + "content": "进入终端", + "fill": "$accent" + } + } + }, + { + "type": "frame", + "id": "DNj6G", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "MzJ8W", + "type": "ref", + "ref": "yoK0T", + "name": "More", + "descendants": { + "XMGqY": { + "icon": "ellipsis" + } + } + } + ] + }, + { + "type": "frame", + "id": "n81YsS", + "name": "Drawer Content", + "clip": true, + "width": "fill_container", + "height": "fill_container", + "fill": "$bg-elevated", + "layout": "vertical", + "gap": 10, + "padding": [ + 12, + 14 + ], + "children": [ + { + "type": "frame", + "id": "C1loI", + "name": "Live Metrics", + "width": "fill_container", + "gap": 10, + "children": [ + { + "type": "frame", + "id": "YSrNc", + "name": "M CPU", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 6, + "padding": [ + 10, + 11 + ], + "children": [ + { + "type": "frame", + "id": "bDdUl", + "name": "Top", + "width": "fill_container", + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "r6iG1", + "name": "Label", + "fill": "$text-tertiary", + "content": "CPU", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "f8wUl", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "GxWFy", + "name": "Delta", + "fill": "$text-muted", + "content": "峰值 6.4%", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "rnzIw", + "name": "Value", + "gap": 3, + "alignItems": "end", + "children": [ + { + "type": "text", + "id": "X2Yktq", + "name": "Num", + "fill": "$text-primary", + "content": "2.1", + "fontFamily": "$font-ui", + "fontSize": 20, + "fontWeight": "600" + }, + { + "type": "text", + "id": "Q9ROim", + "name": "Unit", + "fill": "$text-tertiary", + "content": "%", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "uAevF", + "type": "ref", + "ref": "MhYIL", + "name": "Spark", + "height": 30, + "descendants": { + "fshVw": { + "height": 8, + "width": 9, + "fill": "$accent" + }, + "srvmw": { + "height": 6, + "width": 9, + "fill": "$accent" + }, + "ky1P1": { + "height": 11, + "width": 9, + "fill": "$accent" + }, + "bqMzi": { + "height": 7, + "width": 9, + "fill": "$accent" + }, + "F5Kloq": { + "height": 14, + "width": 9, + "fill": "$accent" + }, + "StdGN": { + "height": 9, + "width": 9, + "fill": "$accent" + }, + "iq3Ux": { + "height": 6, + "width": 9, + "fill": "$accent" + }, + "PHY2v": { + "height": 10, + "width": 9, + "fill": "$accent" + }, + "xqaP5": { + "height": 18, + "width": 9, + "fill": "$accent" + }, + "cZHdc": { + "height": 8, + "width": 9, + "fill": "$accent" + }, + "jhPqP": { + "height": 7, + "width": 9, + "fill": "$accent" + }, + "Za8Yw": { + "height": 9, + "width": 9, + "fill": "$accent" + }, + "zbp8o": { + "height": 12, + "width": 9, + "fill": "$accent" + }, + "kMt0y": { + "height": 8, + "width": 9, + "fill": "$accent" + } + } + } + ] + }, + { + "type": "frame", + "id": "jXOGB", + "name": "M 内存", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 6, + "padding": [ + 10, + 11 + ], + "children": [ + { + "type": "frame", + "id": "SF1Bd", + "name": "Top", + "width": "fill_container", + "gap": 5, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "SeA1X", + "name": "Label", + "fill": "$text-tertiary", + "content": "内存", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "hS5nv", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "sMb7F", + "name": "Delta", + "fill": "$text-muted", + "content": "24.9%", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "hSwMV", + "name": "Value", + "gap": 3, + "alignItems": "end", + "children": [ + { + "type": "text", + "id": "g2cZx7", + "name": "Num", + "fill": "$text-primary", + "content": "128", + "fontFamily": "$font-ui", + "fontSize": 20, + "fontWeight": "600" + }, + { + "type": "text", + "id": "avXH1", + "name": "Unit", + "fill": "$text-tertiary", + "content": "MB / 512", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "y99byC", + "type": "ref", + "ref": "MhYIL", + "name": "Spark", + "height": 30, + "descendants": { + "fshVw": { + "height": 11, + "width": 7, + "fill": "$ok" + }, + "srvmw": { + "height": 13, + "width": 7, + "fill": "$ok" + }, + "ky1P1": { + "height": 12, + "width": 7, + "fill": "$ok" + }, + "bqMzi": { + "height": 15, + "width": 7, + "fill": "$ok" + }, + "F5Kloq": { + "height": 14, + "width": 7, + "fill": "$ok" + }, + "StdGN": { + "height": 16, + "width": 7, + "fill": "$ok" + }, + "iq3Ux": { + "height": 15, + "width": 7, + "fill": "$ok" + }, + "PHY2v": { + "height": 18, + "width": 7, + "fill": "$ok" + }, + "xqaP5": { + "height": 17, + "width": 7, + "fill": "$ok" + }, + "cZHdc": { + "height": 16, + "width": 7, + "fill": "$ok" + }, + "jhPqP": { + "height": 19, + "width": 7, + "fill": "$ok" + }, + "Za8Yw": { + "height": 18, + "width": 7, + "fill": "$ok" + }, + "zbp8o": { + "height": 20, + "width": 7, + "fill": "$ok" + }, + "kMt0y": { + "height": 19, + "width": 7, + "fill": "$ok" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "r55iUx", + "name": "Sec 基本信息", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "wSgBi", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "KSvrL", + "name": "Title", + "fill": "$text-secondary", + "content": "基本信息", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "vDrtJ", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "pDGP7", + "name": "Action", + "fill": "$accent", + "content": "完整 inspect", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "Ya5kg", + "type": "ref", + "ref": "l3qo4", + "name": "KV 镜像", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "镜像" + }, + "psgr6": { + "content": "nginx:1.27-alpine", + "fill": "$text-primary" + } + } + }, + { + "id": "jMfUB", + "type": "ref", + "ref": "l3qo4", + "name": "KV 镜像摘要", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "镜像摘要" + }, + "psgr6": { + "content": "sha256:9c4f2e…1b7a", + "fill": "$text-primary" + } + } + }, + { + "id": "k8UY3", + "type": "ref", + "ref": "l3qo4", + "name": "KV 创建于", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "创建于" + }, + "psgr6": { + "content": "2026-08-18 09:12:44", + "fill": "$text-primary" + } + } + }, + { + "id": "pB5wU", + "type": "ref", + "ref": "l3qo4", + "name": "KV 启动于", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "启动于" + }, + "psgr6": { + "content": "3 天前 (3d 4h 21m)", + "fill": "$text-primary" + } + } + }, + { + "id": "J4MO8", + "type": "ref", + "ref": "l3qo4", + "name": "KV 重启策略", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "重启策略" + }, + "psgr6": { + "content": "unless-stopped", + "fill": "$text-primary" + } + } + }, + { + "id": "MZG65", + "type": "ref", + "ref": "l3qo4", + "name": "KV 平台", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "平台" + }, + "psgr6": { + "content": "linux/amd64", + "fill": "$text-primary" + } + } + }, + { + "id": "yPjku", + "type": "ref", + "ref": "l3qo4", + "name": "KV 命令", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "命令" + }, + "psgr6": { + "content": "nginx -g 'daemon off;'", + "fill": "$text-primary" + } + } + } + ] + }, + { + "type": "rectangle", + "id": "jLFlf", + "name": "Divider", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "Q4itLV", + "name": "Sec 端口映射", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "ZkOC4", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "iJDdT", + "name": "Title", + "fill": "$text-secondary", + "content": "端口映射", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "NnSEo", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "V6vUkr", + "name": "Action", + "fill": "$accent", + "content": "编辑", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "gkDwZ", + "type": "ref", + "ref": "l3qo4", + "name": "KV 8080/tcp", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "8080/tcp" + }, + "psgr6": { + "content": "0.0.0.0:8080 → 80", + "fill": "$info" + } + } + }, + { + "id": "zHFEH", + "type": "ref", + "ref": "l3qo4", + "name": "KV 8443/tcp", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "8443/tcp" + }, + "psgr6": { + "content": "[::]:8443 → 443", + "fill": "$info" + } + } + } + ] + }, + { + "type": "rectangle", + "id": "v4bMe1", + "name": "Divider", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "sKeFG", + "name": "Sec 挂载", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "p8o6uG", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "R94Va", + "name": "Title", + "fill": "$text-secondary", + "content": "挂载", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "f9YSM", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "giDGf", + "name": "Action", + "fill": "$accent", + "content": "浏览文件", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "YE7r3", + "name": "Mount", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "snl96", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "folder", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "y9ydI", + "name": "Path", + "fill": "$text-secondary", + "content": "/srv/nginx/conf.d → /etc/nginx/conf.d", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "RjpFE", + "type": "ref", + "ref": "x0Pcos", + "name": "Tag", + "height": 15, + "fill": "#00000000", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "只读", + "fill": "$warn", + "fontSize": 9 + } + } + } + ] + }, + { + "type": "frame", + "id": "u3fUHj", + "name": "Mount", + "clip": true, + "width": "fill_container", + "height": 22, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "wUclI", + "name": "Icon", + "width": 12, + "height": 12, + "icon": "database", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "I6dRCw", + "name": "Path", + "fill": "$text-secondary", + "content": "nginx-cache → /var/cache/nginx", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "nz0BL", + "type": "ref", + "ref": "x0Pcos", + "name": "Tag", + "height": 15, + "fill": "#00000000", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "读写", + "fill": "$ok", + "fontSize": 9 + } + } + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "yHI2d", + "name": "Divider", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "Mx2mM", + "name": "Sec 网络", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "uWkbX", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "oYCbr", + "name": "Title", + "fill": "$text-secondary", + "content": "网络", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "IivoK", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "ia3yp", + "name": "Action", + "fill": "$accent", + "content": "接入其他网络", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "WWuHt", + "type": "ref", + "ref": "l3qo4", + "name": "KV web-stack_default", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "网络" + }, + "psgr6": { + "content": "web-stack_default", + "fill": "$text-primary" + } + } + }, + { + "id": "aeGLy", + "type": "ref", + "ref": "l3qo4", + "name": "KV IP", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "IP 地址" + }, + "psgr6": { + "content": "172.20.0.4 · bridge · MTU 1500" + } + } + } + ] + }, + { + "type": "rectangle", + "id": "UmF87", + "name": "Divider", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "Ca6e4", + "name": "Sec 健康检查", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "Uo7WQ", + "name": "Head", + "width": "fill_container", + "height": 24, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "tgCLN", + "name": "Title", + "fill": "$text-secondary", + "content": "健康检查", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "kK9y2", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "Fiiia", + "name": "HC", + "width": "fill_container", + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "id": "l4Tfv", + "type": "ref", + "ref": "x0Pcos", + "name": "HC Chip", + "descendants": { + "ODVko": { + "content": "healthy" + } + } + }, + { + "type": "text", + "id": "wi4lP", + "name": "Detail", + "fill": "$text-tertiary", + "content": "每 30s · 0 次失败 · 12s 前", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "U0oPt5", + "layoutPosition": "absolute", + "x": 0, + "y": 0, + "name": "Scrim", + "fill": "#05070ACC", + "width": 1440, + "height": 796 + }, + { + "type": "frame", + "id": "lWAA1", + "layoutPosition": "absolute", + "x": 452, + "y": 150, + "name": "Gate", + "clip": true, + "width": 560, + "fill": "$bg-elevated", + "cornerRadius": "$r-lg", + "stroke": "$border-strong", + "strokeWidth": 1, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000080", + "offset": { + "x": 0, + "y": 12 + }, + "blur": 36 + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "mH9XO", + "name": "H", + "width": "fill_container", + "height": 52, + "fill": "$danger-dim", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 10, + "padding": [ + 0, + 18 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "fTB8z", + "name": "I", + "width": 17, + "height": 17, + "icon": "trash-2", + "library": "lucide", + "fill": "$danger" + }, + { + "type": "text", + "id": "ij5vv", + "name": "T", + "fill": "$text-primary", + "content": "删除 2 个容器?", + "fontFamily": "$font-ui", + "fontSize": 15, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "sYHgH", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "O8Bt0", + "type": "ref", + "ref": "yoK0T", + "name": "X", + "descendants": { + "XMGqY": { + "icon": "x" + } + } + } + ] + }, + { + "type": "frame", + "id": "O248BQ", + "name": "Host", + "width": "fill_container", + "height": 34, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 18 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "yxggW", + "name": "I", + "width": 13, + "height": 13, + "icon": "server", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "TRRMT", + "name": "L", + "fill": "$text-tertiary", + "content": "目标主机", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "MHr0S", + "name": "H", + "fill": "$text-primary", + "content": "prod-sg-01", + "fontFamily": "$font-mono", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "em6Fh", + "name": "P", + "fill": "$text-muted", + "content": "· 10.24.8.11 · /var/run/docker.sock(SSH 隧道)", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Z7797", + "name": "Body", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": [ + 16, + 18 + ], + "children": [ + { + "type": "text", + "id": "pe0rH", + "name": "L", + "fill": "$text-secondary", + "content": "将要执行", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "jr5pP", + "name": "Cmd", + "width": "fill_container", + "fill": "$bg-terminal", + "cornerRadius": "$r-sm", + "layout": "vertical", + "gap": 3, + "padding": [ + 10, + 12 + ], + "children": [ + { + "type": "text", + "id": "WrxaP", + "name": "C", + "fill": "$text-terminal", + "content": "DELETE /containers/worker-1?v=false&force=false", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "YBCdg", + "name": "C", + "fill": "$text-terminal", + "content": "DELETE /containers/worker-2?v=false&force=false", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "BB6qQ", + "name": "N", + "fill": "$text-muted", + "content": "等价于 docker rm worker-1 worker-2", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "IyVaS", + "name": "L", + "fill": "$text-secondary", + "content": "目标", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "qgdsS", + "name": "Targets", + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "nNQ6d", + "name": "T worker-1", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "j56jFk", + "name": "D", + "fill": "$text-muted", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "g4r1fo", + "name": "N", + "fill": "$text-primary", + "content": "worker-1", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "CBm3W", + "name": "I", + "fill": "$text-muted", + "content": "ghcr.io/acme/worker:2.8.1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "AJizs", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "K2r8G", + "name": "St", + "fill": "$text-tertiary", + "content": "已停止 · 6h 前退出 (0)", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "F3BqHm", + "name": "T worker-2", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "htlOT", + "name": "D", + "fill": "$text-muted", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "eDcX2", + "name": "N", + "fill": "$text-primary", + "content": "worker-2", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "k702v0", + "name": "I", + "fill": "$text-muted", + "content": "ghcr.io/acme/worker:2.8.1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "kVDOn", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "I0PaM", + "name": "St", + "fill": "$text-tertiary", + "content": "已停止 · 6h 前退出 (0)", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "text", + "id": "QzkF4", + "name": "L", + "fill": "$text-secondary", + "content": "后果", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "vVHvf", + "name": "Consequences", + "width": "fill_container", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "frame", + "id": "hcoeW", + "name": "C", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "icon", + "id": "aBgIe", + "name": "I", + "width": 13, + "height": 13, + "icon": "circle-x", + "library": "lucide", + "fill": "$danger" + }, + { + "type": "text", + "id": "oDGbk", + "name": "T", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "容器及其可写层会被删除,写在容器里(不在卷里)的数据丢失。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "hpVez", + "name": "C", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "icon", + "id": "P6R6IH", + "name": "I", + "width": 13, + "height": 13, + "icon": "shield-check", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "Z015O0", + "name": "T", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "挂载的命名卷不受影响 —— 本次不带 -v。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "c75TZc", + "name": "C", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "icon", + "id": "HLSXN", + "name": "I", + "width": 13, + "height": 13, + "icon": "info", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "WCpAw", + "name": "T", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "它们属于 compose 项目 jobs,下次 up -d 会按 compose.yaml 重建。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "EOR7d", + "name": "Foot", + "width": "fill_container", + "height": 56, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 9, + "padding": [ + 0, + 18 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "fuwG4", + "name": "I", + "width": 13, + "height": 13, + "icon": "keyboard", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "R3J1M", + "name": "K", + "fill": "$text-muted", + "content": "Esc 取消", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "MCoAs", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "k5Ubtm", + "type": "ref", + "ref": "i1OTz", + "name": "Cancel", + "height": 30, + "descendants": { + "RQ8pM": { + "enabled": false + }, + "P0aUw": { + "content": "取消", + "fontSize": 12 + } + } + }, + { + "id": "oTPqV", + "type": "ref", + "ref": "i1OTz", + "name": "Confirm", + "height": 30, + "fill": "$danger", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "trash-2", + "fill": "#FFFFFF" + }, + "P0aUw": { + "content": "删除 2 个容器", + "fill": "#FFFFFF", + "fontWeight": "500", + "fontSize": 12 + } + } + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "AAI5y", + "name": "Status Bar", + "width": "fill_container", + "height": 24, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 14, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "L5wJqx", + "name": "Msg", + "fill": "$text-tertiary", + "content": "等待确认 · 未执行任何动作", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "catA1", + "name": "Spacer", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "i2JPv", + "name": "API v1.47", + "fill": "$text-muted", + "content": "API v1.47", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "sSKAd", + "name": "Engine 27.3.1", + "fill": "$text-muted", + "content": "Engine 27.3.1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "JzOhl", + "name": "18 容器 · 34 镜像 · 9 卷", + "fill": "$text-muted", + "content": "18 容器 · 34 镜像 · 9 卷", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "x74wgp", + "name": "事件流 已连接", + "fill": "$text-muted", + "content": "事件流 已连接", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ZtOv1", + "x": 820, + "y": 0, + "name": "C/Progress", + "reusable": true, + "width": 300, + "fill": "#00000000", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "frame", + "id": "hli60", + "name": "Top", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "krfQF", + "name": "Label", + "fill": "$text-secondary", + "content": "下载中", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "NjIXr", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "I75B2", + "name": "Value", + "fill": "$text-primary", + "content": "42%", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Cp8dm", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 6, + "fill": "$bg-active", + "cornerRadius": 3, + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "JbiI8", + "name": "Fill", + "fill": "$accent", + "width": "fill_container", + "height": 6 + }, + { + "type": "frame", + "id": "tfpEI", + "name": "Rest", + "width": "fill_container", + "height": 6, + "fill": "#00000000" + } + ] + } + ] + }, + { + "type": "frame", + "id": "obPIB", + "x": 820, + "y": 90, + "name": "C/Dialog", + "reusable": true, + "clip": true, + "width": 440, + "fill": "$bg-elevated", + "cornerRadius": "$r-lg", + "stroke": "$border-strong", + "strokeWidth": 1, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000080", + "offset": { + "x": 0, + "y": 12 + }, + "blur": 36 + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "qA5qA", + "name": "Head", + "width": "fill_container", + "height": 48, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 9, + "padding": [ + 0, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "ysZdT", + "name": "Icon", + "width": 16, + "height": 16, + "icon": "arrow-down-to-line", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "V5qqj", + "name": "Title", + "fill": "$text-primary", + "content": "拉取镜像", + "fontFamily": "$font-ui", + "fontSize": 14, + "fontWeight": "600" + }, + { + "id": "NUsd4", + "type": "ref", + "ref": "x0Pcos", + "name": "Chip", + "enabled": false, + "descendants": { + "ODVko": { + "content": "进行中" + } + } + }, + { + "type": "frame", + "id": "Syryk", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "Vg2Jl", + "type": "ref", + "ref": "yoK0T", + "name": "Close", + "descendants": { + "XMGqY": { + "icon": "x" + } + } + } + ] + }, + { + "type": "frame", + "id": "sk6uC", + "name": "Host", + "width": "fill_container", + "height": 30, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 7, + "padding": [ + 0, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "CfHk5", + "name": "I", + "width": 12, + "height": 12, + "icon": "server", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "y3SX0", + "name": "H", + "fill": "$text-primary", + "content": "prod-sg-01", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "yW57g", + "name": "P", + "fill": "$text-muted", + "content": "· Engine 27.3.1 · linux/amd64", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "slcE7", + "name": "Body", + "slot": [], + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": [ + 16, + 18 + ], + "children": [ + { + "type": "text", + "id": "bp1Wf", + "name": "Placeholder", + "fill": "$text-muted", + "content": "(内容)", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "JSWkk", + "name": "Foot", + "width": "fill_container", + "height": 56, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 9, + "padding": [ + 0, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "WAXqz", + "name": "Hint", + "fill": "$text-muted", + "content": "Esc 取消", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "G575ED", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "Uk4Ly", + "type": "ref", + "ref": "i1OTz", + "name": "Cancel", + "height": 30, + "descendants": { + "RQ8pM": { + "enabled": false + }, + "P0aUw": { + "content": "取消", + "fontSize": 12 + } + } + }, + { + "id": "C08Vw", + "type": "ref", + "ref": "i1OTz", + "name": "Confirm", + "height": 30, + "fill": "$accent", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "arrow-down-to-line", + "fill": "$accent-on" + }, + "P0aUw": { + "content": "开始拉取", + "fill": "$accent-on", + "fontWeight": "500", + "fontSize": 12 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "tM8MZ", + "x": 1059, + "y": 14467, + "name": "16 · 拉取镜像 · 三态", + "theme": { + "mode": "dark" + }, + "clip": true, + "width": 1440, + "height": 703, + "fill": "$bg-page", + "layout": "vertical", + "gap": 16, + "padding": 24, + "children": [ + { + "type": "frame", + "id": "mJwy8", + "name": "Title", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "n2tuKp", + "name": "T", + "fill": "$text-primary", + "content": "二级弹窗 · 拉取镜像的三个状态", + "fontFamily": "$font-ui", + "fontSize": 18, + "fontWeight": "600" + }, + { + "type": "text", + "id": "Wi3Gq", + "name": "S", + "fill": "$text-tertiary", + "content": "同一个对话框原地换态,不弹第二层。进行中可以「转入后台」——任务继续跑,对话框关掉,进度移交给顶栏的任务中心。", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "MYGgr", + "name": "Row", + "width": "fill_container", + "gap": 36, + "children": [ + { + "id": "XxY2H", + "type": "ref", + "ref": "obPIB", + "name": "D 表单", + "width": "fill_container", + "descendants": { + "ysZdT": { + "icon": "arrow-down-to-line", + "fill": "$accent" + }, + "V5qqj": { + "content": "拉取镜像" + }, + "NUsd4": { + "enabled": false + }, + "slcE7": { + "type": "frame", + "id": "yatWd", + "name": "Body", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": [ + 16, + 18 + ], + "children": [ + { + "type": "text", + "id": "JiKNl", + "name": "L", + "fill": "$text-secondary", + "content": "镜像引用", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "cmiSu", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 30, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$accent", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "m1IUe", + "name": "V", + "fill": "$text-primary", + "content": "ghcr.io/acme/api", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "rectangle", + "id": "FBJbi", + "name": "Caret", + "fill": "$accent", + "width": 1.5, + "height": 14 + }, + { + "type": "frame", + "id": "oD441", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "mg9TT", + "name": "Autocomplete", + "clip": true, + "width": "fill_container", + "fill": "$bg-elevated", + "cornerRadius": "$r-sm", + "stroke": "$border-strong", + "strokeWidth": 1, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000066", + "offset": { + "x": 0, + "y": 6 + }, + "blur": 18 + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "JdBjT", + "name": "AC", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "$accent-dim", + "gap": 8, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "xlha5", + "name": "I", + "width": 12, + "height": 12, + "icon": "layers", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "gfylB", + "name": "T", + "fill": "$text-primary", + "content": "ghcr.io/acme/api", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "WIgr6", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "igniu", + "name": "M", + "fill": "$text-muted", + "content": "本地已有 2.8.1 · 2.8.0", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "MQlev", + "name": "AC", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 8, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "H7AWh4", + "name": "I", + "width": 12, + "height": 12, + "icon": "layers", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "xN21P", + "name": "T", + "fill": "$text-secondary", + "content": "ghcr.io/acme/api-worker", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "V2FEbz", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Mjg7C", + "name": "M", + "fill": "$text-muted", + "content": "本地已有 2.8.1", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "PmBg1", + "name": "AC", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 8, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "D2FO2", + "name": "I", + "width": 12, + "height": 12, + "icon": "cloud", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "TOmt9", + "name": "T", + "fill": "$text-secondary", + "content": "ghcr.io/acme/apigw", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "jXZDM", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "MdkyK", + "name": "M", + "fill": "$text-muted", + "content": "远端 · 未拉取过", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "K0yMDU", + "name": "AC", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 8, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "VGzsj", + "name": "I", + "width": 12, + "height": 12, + "icon": "cloud", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "X6qz1", + "name": "T", + "fill": "$text-secondary", + "content": "docker.io/library/api", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "maO7D", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "AznEu", + "name": "M", + "fill": "$text-muted", + "content": "公共库 · 名称相近", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "gbVej", + "name": "Two", + "width": "fill_container", + "gap": 10, + "children": [ + { + "type": "frame", + "id": "Gsy9p", + "name": "F 标签", + "width": "fill_container", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "text", + "id": "hoXnj", + "name": "L", + "fill": "$text-secondary", + "content": "标签", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "m7XGu2", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 30, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "QPbOr", + "name": "V", + "fill": "$text-primary", + "content": "2.8.2", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "fh75R", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "ma6m1", + "name": "T", + "width": 12, + "height": 12, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Fjgup", + "name": "F 平台", + "width": "fill_container", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "text", + "id": "kFEjO", + "name": "L", + "fill": "$text-secondary", + "content": "平台", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "eFdiS", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 30, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "fSwKp", + "name": "V", + "fill": "$text-primary", + "content": "linux/amd64", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "gOlLc", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "i58dZp", + "name": "T", + "width": 12, + "height": 12, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + } + ] + } + ] + }, + { + "type": "text", + "id": "WmbDK", + "name": "PlatHint", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "平台与远端主机架构一致(amd64),不会拉到跑不起来的镜像。", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "fYiUg", + "name": "Toggle", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 9, + "padding": [ + 10, + 12 + ], + "children": [ + { + "type": "frame", + "id": "jdEyX", + "name": "Sw", + "width": 26, + "height": 15, + "fill": "$bg-active", + "cornerRadius": 8, + "stroke": "$border-strong", + "strokeWidth": 1, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "F9nCGT", + "name": "K", + "fill": "$text-muted", + "width": 11, + "height": 11 + } + ] + }, + { + "type": "frame", + "id": "ADHzI", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "fVofp", + "name": "T", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "拉取全部标签 --all-tags", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "O331pH", + "name": "D", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "这个仓库有 47 个标签,合计约 8.2 GB。", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "I8ZHx", + "name": "Registry", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "sgUHM", + "name": "I", + "width": 13, + "height": 13, + "icon": "shield-check", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "hSpUm", + "name": "T", + "fill": "$ok", + "content": "ghcr.io 已登录", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "oKCZI", + "name": "D", + "fill": "$text-muted", + "content": "凭据来自远端 ~/.docker/config.json", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "VR3z5", + "name": "Preview", + "width": "fill_container", + "fill": "$bg-terminal", + "cornerRadius": "$r-sm", + "layout": "vertical", + "gap": 3, + "padding": [ + 9, + 11 + ], + "children": [ + { + "type": "text", + "id": "J3uRh7", + "name": "C", + "fill": "$text-terminal", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "POST /images/create?fromImage=ghcr.io/acme/api&tag=2.8.2", + "lineHeight": 1.5, + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "CTCif", + "name": "N", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "等价于 docker pull ghcr.io/acme/api:2.8.2 --platform linux/amd64", + "lineHeight": 1.5, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + "WAXqz": { + "content": "Esc 取消 · Enter 开始" + }, + "Uk4Ly/RQ8pM": { + "enabled": false + }, + "Uk4Ly/P0aUw": { + "content": "取消", + "fill": "$text-secondary" + }, + "C08Vw": { + "fill": "$accent" + }, + "C08Vw/RQ8pM": { + "icon": "arrow-down-to-line", + "fill": "$accent-on" + }, + "C08Vw/P0aUw": { + "content": "开始拉取" + } + } + }, + { + "id": "GG6kW", + "type": "ref", + "ref": "obPIB", + "name": "D 进行中", + "width": "fill_container", + "descendants": { + "ysZdT": { + "icon": "arrow-down-to-line", + "fill": "$accent" + }, + "V5qqj": { + "content": "拉取中" + }, + "NUsd4": { + "enabled": true, + "fill": "$accent-dim" + }, + "NUsd4/kvk5k": { + "fill": "$accent" + }, + "NUsd4/ODVko": { + "content": "进行中", + "fill": "$accent" + }, + "slcE7": { + "type": "frame", + "id": "u6LLZ", + "name": "Body", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": [ + 16, + 18 + ], + "children": [ + { + "type": "text", + "id": "ESXkp", + "name": "Ref", + "fill": "$text-primary", + "content": "ghcr.io/acme/api:2.8.2", + "fontFamily": "$font-mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "o8j9nf", + "name": "Overall", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 8, + "padding": [ + 12, + 13 + ], + "children": [ + { + "type": "frame", + "id": "m0whVm", + "name": "T", + "width": "fill_container", + "gap": 8, + "alignItems": "end", + "children": [ + { + "type": "text", + "id": "SrsgS", + "name": "P", + "fill": "$text-primary", + "content": "42%", + "fontFamily": "$font-ui", + "fontSize": 26, + "fontWeight": "600" + }, + { + "type": "text", + "id": "YDWNu", + "name": "B", + "fill": "$text-tertiary", + "content": "89.6 / 214 MB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "F56QG", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "mp9qM", + "name": "R", + "layout": "vertical", + "gap": 2, + "alignItems": "end", + "children": [ + { + "type": "text", + "id": "cRYoE", + "name": "Sp", + "fill": "$accent", + "content": "12.4 MB/s", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "WjChS", + "name": "Et", + "fill": "$text-muted", + "content": "剩余 ~18s", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "haBmg", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 8, + "fill": "$bg-active", + "cornerRadius": 4, + "children": [ + { + "type": "rectangle", + "cornerRadius": 4, + "id": "v8m3v", + "name": "Fill", + "fill": "$accent", + "width": "fill_container", + "height": 8 + }, + { + "type": "frame", + "id": "s4Z5Ap", + "name": "Rest", + "width": "fill_container", + "height": 8, + "fill": "#00000000" + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "qTUef", + "name": "Div", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "F9aPy", + "name": "LayerHead", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "vh7ga", + "name": "T", + "fill": "$text-secondary", + "content": "9 层", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "Ff9Lw", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "TnTFX", + "name": "M", + "fill": "$text-muted", + "content": "6 层复用 · 2 层进行中 · 1 层等待", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "RMa6U", + "name": "Collapsed", + "width": "fill_container", + "height": 26, + "fill": "$bg-surface", + "cornerRadius": "$r-sm", + "gap": 8, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "CMl7w", + "name": "C", + "width": 12, + "height": 12, + "icon": "chevron-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "icon", + "id": "uVm7e", + "name": "I", + "width": 12, + "height": 12, + "icon": "check", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "T5ist", + "name": "T", + "fill": "$text-tertiary", + "content": "6 层已存在,已折叠", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "elGa5", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "QXl66", + "name": "M", + "fill": "$text-muted", + "content": "复用 124 MB", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "EpHsv", + "name": "L d4e8b2f105", + "clip": true, + "width": "fill_container", + "height": 24, + "gap": 9, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "P3Sja", + "name": "Id", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": 74, + "content": "d4e8b2f105", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "j4EtZ4", + "name": "St", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 70, + "content": "Downloading", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "vy8rN", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 5, + "fill": "$bg-active", + "cornerRadius": 3, + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "dHxmW", + "name": "F", + "fill": "$accent", + "width": "fill_container", + "height": 5 + }, + { + "type": "frame", + "id": "o9yqSi", + "name": "R", + "width": "fill_container", + "height": 5, + "fill": "#00000000" + } + ] + }, + { + "type": "text", + "id": "gEBtA", + "name": "P", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 30, + "content": "62%", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "pc56s", + "name": "Sz", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 84, + "content": "38.2 / 61.4 MB", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "H7MKtj", + "name": "L 6ef3a0b829", + "clip": true, + "width": "fill_container", + "height": 24, + "gap": 9, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ZYBPu", + "name": "Id", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": 74, + "content": "6ef3a0b829", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Pvqxw", + "name": "St", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 70, + "content": "Extracting", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "HPwqE", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 5, + "fill": "$bg-active", + "cornerRadius": 3, + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "S7wK0", + "name": "F", + "fill": "$violet", + "width": "fill_container", + "height": 5 + }, + { + "type": "frame", + "id": "Vyc32", + "name": "R", + "width": "fill_container", + "height": 5, + "fill": "#00000000" + } + ] + }, + { + "type": "text", + "id": "l2bW5", + "name": "P", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 30, + "content": "28%", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "bGcGP", + "name": "Sz", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 84, + "content": "14.6 MB", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "fO8Nj", + "name": "L 0af31b8ce4", + "clip": true, + "width": "fill_container", + "height": 24, + "gap": 9, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "p030X1", + "name": "Id", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": 74, + "content": "0af31b8ce4", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "jM5ya", + "name": "St", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 70, + "content": "Waiting", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ZCRdI", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 5, + "fill": "$bg-active", + "cornerRadius": 3, + "children": [ + { + "type": "frame", + "id": "g6Pkoj", + "name": "R", + "width": "fill_container", + "height": 5, + "fill": "#00000000" + } + ] + }, + { + "type": "text", + "id": "ZNIED", + "name": "P", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 30, + "content": "—", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ksuTO", + "name": "Sz", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 84, + "content": "待定", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + "WAXqz": { + "content": "关闭对话框不会取消 —— 任务转入顶栏任务中心" + }, + "Uk4Ly/RQ8pM": { + "enabled": true, + "icon": "minimize-2" + }, + "Uk4Ly/P0aUw": { + "content": "转入后台" + }, + "C08Vw": { + "fill": "$danger" + }, + "C08Vw/RQ8pM": { + "icon": "x", + "fill": "$accent-on" + }, + "C08Vw/P0aUw": { + "content": "取消拉取" + } + } + }, + { + "id": "yybGR", + "type": "ref", + "ref": "obPIB", + "name": "D 完成", + "width": "fill_container", + "descendants": { + "ysZdT": { + "icon": "circle-check-big", + "fill": "$ok" + }, + "V5qqj": { + "content": "拉取完成" + }, + "NUsd4": { + "enabled": true, + "fill": "$ok-dim" + }, + "NUsd4/kvk5k": { + "fill": "$ok" + }, + "NUsd4/ODVko": { + "content": "成功", + "fill": "$ok" + }, + "slcE7": { + "type": "frame", + "id": "Ad3PS", + "name": "Body", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": [ + 16, + 18 + ], + "children": [ + { + "type": "frame", + "id": "QCJxR", + "name": "Success", + "width": "fill_container", + "fill": "$ok-dim", + "cornerRadius": "$r-md", + "gap": 10, + "padding": [ + 11, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "aiB2P", + "name": "I", + "width": 20, + "height": 20, + "icon": "circle-check-big", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "frame", + "id": "waEos", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "R0wdU", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "ghcr.io/acme/api:2.8.2", + "fontFamily": "$font-mono", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "VozE4", + "name": "D", + "fill": "$ok", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "用时 24.3s · 新下载 89.6 MB · 复用 124 MB", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "id": "z3823", + "type": "ref", + "ref": "l3qo4", + "name": "KV 摘要", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "摘要", + "width": 74, + "fontSize": 10 + }, + "psgr6": { + "content": "sha256:3f81a9…c04e", + "fontSize": 10 + } + } + }, + { + "id": "B4IisH", + "type": "ref", + "ref": "l3qo4", + "name": "KV 大小", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "大小", + "width": 74, + "fontSize": 10 + }, + "psgr6": { + "content": "214 MB(9 层)", + "fontSize": 10 + } + } + }, + { + "id": "q8Z0Jd", + "type": "ref", + "ref": "l3qo4", + "name": "KV 平台", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "平台", + "width": 74, + "fontSize": 10 + }, + "psgr6": { + "content": "linux/amd64", + "fontSize": 10 + } + } + }, + { + "id": "DQeAa", + "type": "ref", + "ref": "l3qo4", + "name": "KV 创建于", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "创建于", + "width": 74, + "fontSize": 10 + }, + "psgr6": { + "content": "2026-08-20 22:41 (镜像本身)", + "fontSize": 10 + } + } + }, + { + "id": "k0pZdC", + "type": "ref", + "ref": "l3qo4", + "name": "KV 本地标签", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "本地标签", + "width": 74, + "fontSize": 10 + }, + "psgr6": { + "content": "2.8.2 · 2.8.1 · 2.8.0", + "fontSize": 10 + } + } + }, + { + "type": "frame", + "id": "agdFs", + "name": "Warn", + "width": "fill_container", + "height": 28, + "fill": "$warn-dim", + "cornerRadius": "$r-sm", + "gap": 8, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "y4f0ME", + "name": "C", + "width": 12, + "height": 12, + "icon": "chevron-right", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "icon", + "id": "StPzn", + "name": "I", + "width": 12, + "height": 12, + "icon": "triangle-alert", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "text", + "id": "YXS8m", + "name": "T", + "fill": "$warn", + "content": "1 条 registry 警告", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "N9AJD0", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "F5Oxi", + "name": "M", + "fill": "$warn", + "content": "展开", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "rectangle", + "id": "L3vEt5", + "name": "Div", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "text", + "id": "c8gsEp", + "name": "NextL", + "fill": "$text-secondary", + "content": "下一步", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "fTcqy", + "name": "Next", + "width": "fill_container", + "gap": 8, + "children": [ + { + "id": "qHD5V", + "type": "ref", + "ref": "i1OTz", + "name": "N 运行容器…", + "height": 28, + "fill": "$accent-dim", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "play", + "fill": "$accent" + }, + "P0aUw": { + "content": "运行容器…", + "fontSize": 11, + "fill": "$accent" + } + } + }, + { + "id": "j2Nms", + "type": "ref", + "ref": "i1OTz", + "name": "N 打标签", + "height": 28, + "fill": "#00000000", + "stroke": "$border", + "descendants": { + "RQ8pM": { + "icon": "tag", + "fill": "$text-secondary" + }, + "P0aUw": { + "content": "打标签", + "fontSize": 11, + "fill": "$text-secondary" + } + } + }, + { + "id": "tgxcN", + "type": "ref", + "ref": "i1OTz", + "name": "N 重建用它的容器", + "height": 28, + "fill": "#00000000", + "stroke": "$border", + "descendants": { + "RQ8pM": { + "icon": "rotate-cw", + "fill": "$text-secondary" + }, + "P0aUw": { + "content": "重建用它的容器", + "fontSize": 11, + "fill": "$text-secondary" + } + } + } + ] + } + ] + }, + "WAXqz": { + "content": "已写入执行记录" + }, + "Uk4Ly/RQ8pM": { + "enabled": true, + "icon": "square-terminal" + }, + "Uk4Ly/P0aUw": { + "content": "查看执行记录" + }, + "C08Vw": { + "fill": "$accent" + }, + "C08Vw/RQ8pM": { + "icon": "check", + "fill": "$accent-on" + }, + "C08Vw/P0aUw": { + "content": "完成" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "TxwQS", + "x": 1059, + "y": 15387, + "name": "17 · 进度体系", + "theme": { + "mode": "dark" + }, + "clip": true, + "width": 1440, + "height": 1149, + "fill": "$bg-page", + "layout": "vertical", + "gap": 16, + "padding": 24, + "children": [ + { + "type": "frame", + "id": "jAwb6", + "name": "Title", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "aC9gN", + "name": "T", + "fill": "$text-primary", + "content": "进度体系", + "fontFamily": "$font-ui", + "fontSize": 18, + "fontWeight": "600" + }, + { + "type": "text", + "id": "czdDc", + "name": "S", + "fill": "$text-tertiary", + "content": "三个层级:对话框内的详细进度 → 顶栏任务中心的并行任务 → 状态栏与行内的就地反馈。任何一个动作都不允许「按下去没反应」。", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Sgzkb", + "name": "Row 1", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "ReNq2", + "name": "C 顶栏任务中心", + "clip": true, + "width": 430, + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "yACHR", + "name": "H", + "width": "fill_container", + "height": 32, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "p5OpV", + "name": "T", + "fill": "$text-primary", + "content": "顶栏任务中心", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "vRQ8a", + "name": "S", + "fill": "$text-muted", + "content": "收起态在顶栏,点开是弹层", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "P4hrU", + "name": "B", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": 14, + "children": [ + { + "type": "frame", + "id": "S3MBTv", + "name": "TopBar Mock", + "width": "fill_container", + "height": 40, + "fill": "$bg-elevated", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 9, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "mTg4X", + "name": "L", + "width": 15, + "height": 15, + "icon": "container", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "X6KO3", + "name": "T", + "fill": "$text-primary", + "content": "Docker", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "CWgW8", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "H67E8", + "name": "Task Indicator", + "height": 26, + "fill": "$accent-dim", + "cornerRadius": "$r-sm", + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "Ri5CE", + "name": "Ring", + "innerRadius": 0.6, + "startAngle": 90, + "sweepAngle": -152, + "fill": "$accent", + "width": 13, + "height": 13 + }, + { + "type": "text", + "id": "T7gn60", + "name": "T", + "fill": "$accent", + "content": "2 个任务", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "JDEs0", + "name": "P", + "fill": "$accent", + "content": "42%", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "NOwvi", + "type": "ref", + "ref": "x0Pcos", + "name": "Live", + "descendants": { + "ODVko": { + "content": "实时" + } + } + } + ] + }, + { + "type": "text", + "id": "BF7n1", + "name": "Note", + "fill": "$text-muted", + "content": "↓ 点开后的弹层", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "jbwy8", + "name": "Popover", + "clip": true, + "width": "fill_container", + "fill": "$bg-elevated", + "cornerRadius": "$r-md", + "stroke": "$border-strong", + "strokeWidth": 1, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000073", + "offset": { + "x": 0, + "y": 8 + }, + "blur": 24 + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "o3Z6H", + "name": "H", + "width": "fill_container", + "height": 34, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 7, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "N7RODi", + "name": "T", + "fill": "$text-primary", + "content": "任务", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "ZvzFV", + "name": "C", + "fill": "$text-muted", + "content": "2 进行中 · 3 已完成", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "wcXYV", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "ddF9g", + "name": "A", + "fill": "$accent", + "content": "清空已完成", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "TmROe", + "name": "T 拉取 ghcr.io/acme/api:2.8.2", + "width": "fill_container", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 6, + "padding": [ + 10, + 12 + ], + "children": [ + { + "type": "frame", + "id": "h6I7nJ", + "name": "H", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "CiDa2", + "name": "I", + "width": 13, + "height": 13, + "icon": "arrow-down-to-line", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "frame", + "id": "GWPpr", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "text", + "id": "yUs7a", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "拉取 ghcr.io/acme/api:2.8.2", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "iv1gQ", + "name": "S", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "9 层 · 89.6 / 214 MB · 12.4 MB/s", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "pXbts", + "name": "R", + "fill": "$accent", + "content": "42%", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "uM0PV", + "type": "ref", + "ref": "yoK0T", + "name": "A", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "x", + "width": 12, + "height": 12 + } + } + } + ] + }, + { + "type": "frame", + "id": "TlLPm", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 5, + "fill": "$bg-active", + "cornerRadius": 4, + "children": [ + { + "type": "rectangle", + "cornerRadius": 4, + "id": "yUYqM", + "name": "F", + "fill": "$accent", + "width": "fill_container", + "height": 5 + }, + { + "type": "frame", + "id": "Y0sSFh", + "name": "R", + "width": "fill_container", + "height": 5, + "fill": "#00000000" + } + ] + } + ] + }, + { + "type": "frame", + "id": "cfIGV", + "name": "T compose up -d · web-stack", + "width": "fill_container", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 6, + "padding": [ + 10, + 12 + ], + "children": [ + { + "type": "frame", + "id": "IzwiM", + "name": "H", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "A6ut8", + "name": "I", + "width": 13, + "height": 13, + "icon": "boxes", + "library": "lucide", + "fill": "$violet" + }, + { + "type": "frame", + "id": "zlR18", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "text", + "id": "xUZLZ", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "compose up -d · web-stack", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "JBO5L", + "name": "S", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "已运行 4.2s · 正在等待 nginx-proxy 健康", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "id": "N4Fo5", + "type": "ref", + "ref": "yoK0T", + "name": "A", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "x", + "width": 12, + "height": 12 + } + } + } + ] + }, + { + "type": "frame", + "id": "fNqK5", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 5, + "fill": "$bg-active", + "cornerRadius": 4, + "children": [ + { + "type": "frame", + "id": "G8SbW", + "name": "A", + "width": "fill_container", + "height": 5, + "fill": "#00000000" + }, + { + "type": "rectangle", + "cornerRadius": 4, + "id": "C2Me82", + "name": "F", + "fill": "$accent", + "width": "fill_container", + "height": 5 + }, + { + "type": "frame", + "id": "HAwVe", + "name": "B", + "width": "fill_container", + "height": 5, + "fill": "#00000000" + } + ] + } + ] + }, + { + "type": "frame", + "id": "BCgrm", + "name": "T 拉取 redis:7.4-alpine", + "width": "fill_container", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 6, + "padding": [ + 10, + 12 + ], + "children": [ + { + "type": "frame", + "id": "x036n", + "name": "H", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "rBTnl", + "name": "I", + "width": 13, + "height": 13, + "icon": "circle-check-big", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "frame", + "id": "kbFaM", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "text", + "id": "lKEn4", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "拉取 redis:7.4-alpine", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "U83pF", + "name": "S", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "41.8 MB · 用时 6.1s", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "qYuLE", + "name": "R", + "fill": "$ok", + "content": "完成", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "HGWwK", + "name": "T 停止 10 个容器", + "width": "fill_container", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 6, + "padding": [ + 10, + 12 + ], + "children": [ + { + "type": "frame", + "id": "Nle84", + "name": "H", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "USJGv", + "name": "I", + "width": 13, + "height": 13, + "icon": "triangle-alert", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "frame", + "id": "yzXou", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "text", + "id": "nMAjW", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "停止 10 个容器", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "j0ktSb", + "name": "S", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "成功 8 · 失败 2", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "kE0Vj", + "name": "R", + "fill": "$warn", + "content": "部分失败", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "OUlIC", + "type": "ref", + "ref": "yoK0T", + "name": "A", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "chevron-right", + "width": 12, + "height": 12 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "XXFpv", + "name": "T 导出卷 pg-data → tar", + "width": "fill_container", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 6, + "padding": [ + 10, + 12 + ], + "children": [ + { + "type": "frame", + "id": "h8eo8F", + "name": "H", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "JtiCG", + "name": "I", + "width": 13, + "height": 13, + "icon": "circle-check-big", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "frame", + "id": "vKSiZ", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "text", + "id": "TJuYY", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "导出卷 pg-data → tar", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "b83jm", + "name": "S", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "18.4 GB · 用时 3m 12s", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "IZu85", + "name": "R", + "fill": "$ok", + "content": "完成", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "dn8CM", + "name": "F", + "width": "fill_container", + "height": 30, + "gap": 7, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "D3rjt", + "name": "T", + "fill": "$text-muted", + "content": "任务只属于当前主机 —— 换会话不会串台", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "K2bWF", + "name": "C 批量操作 · 选中条原地变进度条", + "clip": true, + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "yIsNH", + "name": "H", + "width": "fill_container", + "height": 32, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "iLnKH", + "name": "T", + "fill": "$text-primary", + "content": "批量操作 · 选中条原地变进度条", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "QNH6R", + "name": "S", + "fill": "$text-muted", + "content": "不弹窗、不遮挡列表", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "IyT4u", + "name": "B", + "width": "fill_container", + "layout": "vertical", + "gap": 14, + "padding": 14, + "children": [ + { + "type": "frame", + "id": "S5Drj", + "name": "Step", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "fvEjO", + "name": "N", + "fill": "$text-muted", + "content": "01", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "nhVE4", + "name": "T", + "fill": "$text-tertiary", + "content": "选中后,选中条出现", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "t7bxt", + "name": "Sel", + "width": "fill_container", + "height": 32, + "fill": "$accent-dim", + "cornerRadius": "$r-sm", + "gap": 7, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "AyBwG", + "name": "I", + "width": 13, + "height": 13, + "icon": "check", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "wwZKd", + "name": "T", + "fill": "$accent", + "content": "已选 10 个容器", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "rectangle", + "id": "DDMam", + "name": "D", + "fill": "$border-strong", + "width": 1, + "height": 16 + }, + { + "id": "uqN8K", + "type": "ref", + "ref": "i1OTz", + "name": "B 启动", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "play" + }, + "P0aUw": { + "content": "启动", + "fontSize": 11 + } + } + }, + { + "id": "Y6136", + "type": "ref", + "ref": "i1OTz", + "name": "B 停止", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "square" + }, + "P0aUw": { + "content": "停止", + "fontSize": 11 + } + } + }, + { + "id": "ALwfy", + "type": "ref", + "ref": "i1OTz", + "name": "B 重启", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "rotate-cw" + }, + "P0aUw": { + "content": "重启", + "fontSize": 11 + } + } + }, + { + "id": "QP0Ri", + "type": "ref", + "ref": "i1OTz", + "name": "B 删除", + "height": 22, + "stroke": "$danger-dim", + "descendants": { + "RQ8pM": { + "icon": "trash-2", + "fill": "$danger" + }, + "P0aUw": { + "content": "删除", + "fontSize": 11, + "fill": "$danger" + } + } + }, + { + "type": "frame", + "id": "eK7xV", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "L1ehHL", + "name": "C", + "fill": "$text-tertiary", + "content": "取消选择", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "IRDS0", + "name": "Step", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "L3Ol8B", + "name": "N", + "fill": "$text-muted", + "content": "02", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "iJx4y", + "name": "T", + "fill": "$text-tertiary", + "content": "按下「停止」——同一条变成进度条,列表不动、选中不丢", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "i0xIEo", + "name": "Running", + "clip": true, + "width": "fill_container", + "fill": "$accent-dim", + "cornerRadius": "$r-sm", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "F1Doaz", + "name": "Bar", + "width": "fill_container", + "height": 32, + "gap": 8, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "KGNsO", + "name": "Ring", + "innerRadius": 0.6, + "startAngle": 90, + "sweepAngle": -216, + "fill": "$accent", + "width": 13, + "height": 13 + }, + { + "type": "text", + "id": "RD6Wh", + "name": "T", + "fill": "$accent", + "content": "正在停止 10 个容器…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "oIEOM", + "name": "P", + "fill": "$accent", + "content": "6 / 10", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "K0QbD", + "name": "Cur", + "fill": "$text-tertiary", + "content": "当前:worker-2 · 等待 SIGTERM(10s 超时)", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "J6UGmq", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "wUCLA", + "type": "ref", + "ref": "i1OTz", + "name": "Cancel", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "x" + }, + "P0aUw": { + "content": "取消剩余", + "fontSize": 11 + } + } + } + ] + }, + { + "type": "frame", + "id": "T5kmtH", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 3, + "fill": "$bg-active", + "children": [ + { + "type": "rectangle", + "id": "ojoPk", + "name": "F", + "fill": "$accent", + "width": "fill_container", + "height": 3 + }, + { + "type": "frame", + "id": "U4CRGV", + "name": "R", + "width": "fill_container", + "height": 3, + "fill": "#00000000" + } + ] + } + ] + }, + { + "type": "frame", + "id": "fQVsm", + "name": "Step", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "qWO6F", + "name": "N", + "fill": "$text-muted", + "content": "03", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "DhH2J", + "name": "T", + "fill": "$text-tertiary", + "content": "完成 —— 逐个目标判定,失败的单独列出来,而不是把整批说成「失败」", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "RJmD0", + "name": "Result", + "clip": true, + "width": "fill_container", + "fill": "$warn-dim", + "cornerRadius": "$r-sm", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "h2Oi0", + "name": "Bar", + "width": "fill_container", + "height": 32, + "gap": 8, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "F2yob5", + "name": "I", + "width": 13, + "height": 13, + "icon": "triangle-alert", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "text", + "id": "vPZTS", + "name": "T", + "fill": "$warn", + "content": "已停止 8 个,2 个失败", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "ra0aF", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "SVhZl", + "type": "ref", + "ref": "i1OTz", + "name": "Retry", + "height": 22, + "stroke": "$warn", + "descendants": { + "RQ8pM": { + "icon": "rotate-cw", + "fill": "$warn" + }, + "P0aUw": { + "content": "重试失败的 2 个", + "fontSize": 11, + "fill": "$warn" + } + } + }, + { + "id": "KTTnZ", + "type": "ref", + "ref": "yoK0T", + "name": "X", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "x", + "width": 12, + "height": 12 + } + } + } + ] + }, + { + "type": "frame", + "id": "isoyf", + "name": "Detail", + "width": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "yLFZw", + "name": "R worker-1", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "zuT1a", + "name": "I", + "width": 12, + "height": 12, + "icon": "circle-check-big", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "ZTof1", + "name": "N", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": 120, + "content": "worker-1", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "l9FME", + "name": "M", + "fill": "$text-muted", + "content": "已停止 · 用时 0.8s", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "QgTeq", + "name": "R worker-2", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "kGvCy", + "name": "I", + "width": 12, + "height": 12, + "icon": "circle-check-big", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "eu66N", + "name": "N", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": 120, + "content": "worker-2", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "MJfTL", + "name": "M", + "fill": "$text-muted", + "content": "已停止 · 用时 9.6s(等到超时后 SIGKILL)", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Z47Fnq", + "name": "R postgres-main", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "kW29l", + "name": "I", + "width": 12, + "height": 12, + "icon": "circle-x", + "library": "lucide", + "fill": "$danger" + }, + { + "type": "text", + "id": "sDE2r", + "name": "N", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": 120, + "content": "postgres-main", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "L5Ee40", + "name": "M", + "fill": "$danger", + "content": "conflict: 容器被 api-gateway 通过 depends_on 依赖", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "s8Dn5", + "name": "R minio", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "EJdj5", + "name": "I", + "width": 12, + "height": 12, + "icon": "circle-x", + "library": "lucide", + "fill": "$danger" + }, + { + "type": "text", + "id": "HgNaL", + "name": "N", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": 120, + "content": "minio", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "UCyGE", + "name": "M", + "fill": "$danger", + "content": "container is paused —— 先恢复再停止", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "EL9Z9", + "name": "More", + "fill": "$text-muted", + "content": "+ 6 个成功已折叠", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "O0q7i1", + "name": "Row 2", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "CEpnx", + "name": "C 行内进度", + "clip": true, + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "nlonD", + "name": "H", + "width": "fill_container", + "height": 32, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "cexej", + "name": "T", + "fill": "$text-primary", + "content": "行内进度", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "WKX2V", + "name": "S", + "fill": "$text-muted", + "content": "单个目标的动作,反馈就在那一行上", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "SoqaC", + "name": "B", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": 14, + "children": [ + { + "type": "frame", + "id": "vOxeR", + "name": "List", + "clip": true, + "width": "fill_container", + "fill": "$bg-elevated", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "cD62Q", + "name": "W nginx-proxy", + "width": "fill_container", + "fill": "$accent-dim", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "zUEUs", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 2, + "fill": "$bg-active", + "children": [ + { + "type": "frame", + "id": "VIpoU", + "name": "A", + "width": "fill_container", + "height": 2, + "fill": "#00000000" + }, + { + "type": "rectangle", + "id": "GpwMj", + "name": "F", + "fill": "$accent", + "width": "fill_container", + "height": 2 + }, + { + "type": "frame", + "id": "eOSPT", + "name": "B", + "width": "fill_container", + "height": 2, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "sHQ6k", + "name": "R", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 9, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "A6xHU", + "name": "Ring", + "innerRadius": 0.6, + "startAngle": 90, + "sweepAngle": -200, + "fill": "$accent", + "width": 11, + "height": 11 + }, + { + "type": "text", + "id": "yey8a", + "name": "N", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": 132, + "content": "nginx-proxy", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "fj62p", + "name": "I", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 186, + "content": "nginx:1.27-alpine", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "k00d4", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "X049xB", + "name": "St", + "fill": "$accent", + "content": "重启中 · 等待健康检查 6s", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "pQerp", + "type": "ref", + "ref": "i1OTz", + "name": "Cancel", + "height": 20, + "descendants": { + "RQ8pM": { + "enabled": false + }, + "P0aUw": { + "content": "取消", + "fontSize": 10 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "g548IA", + "name": "W api-gateway", + "width": "fill_container", + "fill": "#00000000", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "T365M5", + "name": "R", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 9, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "VRTxf", + "name": "D", + "fill": "$ok", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "YweuT", + "name": "N", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": 132, + "content": "api-gateway", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "XJxLb", + "name": "I", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 186, + "content": "ghcr.io/acme/api:2.8.1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "bqzHw", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Rp7Vq", + "name": "St", + "fill": "$ok", + "content": "运行中 · 3d 4h", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "veQIO", + "type": "ref", + "ref": "yoK0T", + "name": "A", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "square", + "width": 12, + "height": 12 + } + } + }, + { + "id": "R9qeS", + "type": "ref", + "ref": "yoK0T", + "name": "A", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "rotate-cw", + "width": 12, + "height": 12 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "R88TkP", + "name": "W postgres-main", + "width": "fill_container", + "fill": "$accent-dim", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "L8V5n", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 2, + "fill": "$bg-active", + "children": [ + { + "type": "rectangle", + "id": "kjCxk", + "name": "F", + "fill": "$accent", + "width": "fill_container", + "height": 2 + }, + { + "type": "frame", + "id": "g1dmQe", + "name": "R", + "width": "fill_container", + "height": 2, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "Mr3SM", + "name": "R", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 9, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "q3Y0b", + "name": "Ring", + "innerRadius": 0.6, + "startAngle": 90, + "sweepAngle": -200, + "fill": "$accent", + "width": 11, + "height": 11 + }, + { + "type": "text", + "id": "j2Duph", + "name": "N", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": 132, + "content": "postgres-main", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "rc6eO", + "name": "I", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 186, + "content": "postgres:16.4", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "E1Idj", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "e36N3h", + "name": "St", + "fill": "$accent", + "content": "拉取新镜像 42% · 12.4 MB/s", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "Z1jLK", + "type": "ref", + "ref": "i1OTz", + "name": "Cancel", + "height": 20, + "descendants": { + "RQ8pM": { + "enabled": false + }, + "P0aUw": { + "content": "取消", + "fontSize": 10 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "JPXBp", + "name": "W redis-cache", + "width": "fill_container", + "fill": "#00000000", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "QuX3o", + "name": "R", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 9, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "aBjB7", + "name": "D", + "fill": "$ok", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "UNp4F", + "name": "N", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": 132, + "content": "redis-cache", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "GVz1i", + "name": "I", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 186, + "content": "redis:7.4-alpine", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "x5OyRl", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "F0RtH4", + "name": "St", + "fill": "$ok", + "content": "运行中 · 12d 6h", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "P6UpS5", + "type": "ref", + "ref": "yoK0T", + "name": "A", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "square", + "width": 12, + "height": 12 + } + } + }, + { + "id": "nLCuB", + "type": "ref", + "ref": "yoK0T", + "name": "A", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "rotate-cw", + "width": 12, + "height": 12 + } + } + } + ] + } + ] + } + ] + }, + { + "type": "text", + "id": "plsQY", + "name": "N", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "进行中的行:2px 顶部进度条 + 圆环 + 状态列写清当前在等什么。动作按钮换成「取消」,其余按钮禁用而不是消失 —— 位置不跳。", + "lineHeight": 1.6, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "rectangle", + "id": "B55ZQ", + "name": "Div", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "text", + "id": "u9JPg", + "name": "L", + "fill": "$text-secondary", + "content": "确定型 / 不确定型", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "YR1JB", + "name": "G", + "width": "fill_container", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "frame", + "id": "UVeRG", + "name": "H", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "f5eyU", + "name": "T", + "fill": "$text-primary", + "content": "确定型 —— 知道分母", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Karey", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "p97uZ", + "name": "D", + "fill": "$text-muted", + "content": "镜像层字节数、批量操作的目标个数、文件传输大小", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "thAqx", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 6, + "fill": "$bg-active", + "cornerRadius": 4, + "children": [ + { + "type": "rectangle", + "cornerRadius": 4, + "id": "OoEU2", + "name": "F", + "fill": "$accent", + "width": "fill_container", + "height": 6 + }, + { + "type": "frame", + "id": "KZTnj", + "name": "R", + "width": "fill_container", + "height": 6, + "fill": "#00000000" + } + ] + } + ] + }, + { + "type": "frame", + "id": "s27c9", + "name": "G", + "width": "fill_container", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "frame", + "id": "h9DtN", + "name": "H", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Y1zQ5O", + "name": "T", + "fill": "$text-primary", + "content": "不确定型 —— 不知道要多久", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "jFR0Q", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "DaqRA", + "name": "D", + "fill": "$text-muted", + "content": "compose up 等健康检查、docker stop 等 SIGTERM、镜像构建", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Q0uAc", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 6, + "fill": "$bg-active", + "cornerRadius": 4, + "children": [ + { + "type": "frame", + "id": "HAcE6", + "name": "A", + "width": "fill_container", + "height": 6, + "fill": "#00000000" + }, + { + "type": "rectangle", + "cornerRadius": 4, + "id": "Y7UYl", + "name": "F", + "fill": "$accent", + "width": "fill_container", + "height": 6 + }, + { + "type": "frame", + "id": "tydp2", + "name": "B", + "width": "fill_container", + "height": 6, + "fill": "#00000000" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "K5hmIF", + "name": "C 结果反馈", + "clip": true, + "width": 460, + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "WoHxM", + "name": "H", + "width": "fill_container", + "height": 32, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ty5aL", + "name": "T", + "fill": "$text-primary", + "content": "结果反馈", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "RePxF", + "name": "S", + "fill": "$text-muted", + "content": "状态栏常驻 · toast 只在你已经离开现场时用", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "O4iOoO", + "name": "B", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": 14, + "children": [ + { + "type": "text", + "id": "Qi1CI", + "name": "L1", + "fill": "$text-secondary", + "content": "状态栏 —— 永远说最后一件事的结果", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "W1wqI", + "name": "SB", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "$bg-elevated", + "cornerRadius": 3, + "stroke": "$border", + "strokeWidth": 1, + "gap": 8, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "M4H4Q", + "name": "I", + "width": 11, + "height": 11, + "icon": "circle-check-big", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "a7d8eV", + "name": "T", + "fill": "$text-tertiary", + "content": "已停止 8 个容器 · 用时 11.4s", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "sLZb6", + "name": "SB", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "$bg-elevated", + "cornerRadius": 3, + "stroke": "$border", + "strokeWidth": 1, + "gap": 8, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "mqOnA", + "name": "I", + "width": 11, + "height": 11, + "icon": "triangle-alert", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "text", + "id": "jsEVt", + "name": "T", + "fill": "$warn", + "content": "已停止 8 个,2 个失败:postgres-main 被 api-gateway 依赖", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Pkuvu", + "name": "SB", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "$bg-elevated", + "cornerRadius": 3, + "stroke": "$border", + "strokeWidth": 1, + "gap": 8, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "QmJAn", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-down-to-line", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "aAPgW", + "name": "T", + "fill": "$accent", + "content": "拉取 ghcr.io/acme/api:2.8.2 · 42% · 12.4 MB/s", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "rectangle", + "id": "KH5jg", + "name": "Div", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "text", + "id": "LbwDd", + "name": "L2", + "fill": "$text-secondary", + "content": "Toast —— 贴在面板右下角,不飘在屏幕中央", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "HRKvP", + "name": "Toast 拉取完成 · ghcr.io/acme/api:2.8.2", + "width": "fill_container", + "fill": "$bg-elevated", + "cornerRadius": "$r-md", + "stroke": "$ok", + "strokeWidth": 1, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000066", + "offset": { + "x": 0, + "y": 6 + }, + "blur": 20 + }, + "gap": 9, + "padding": [ + 11, + 12 + ], + "children": [ + { + "type": "icon", + "id": "DZqcQ", + "name": "I", + "width": 15, + "height": 15, + "icon": "circle-check-big", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "frame", + "id": "G36ub5", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "cFiqX", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "拉取完成 · ghcr.io/acme/api:2.8.2", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "GDaWb", + "name": "D", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "214 MB · 用时 24.3s · 复用 6 层", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "m31tY", + "name": "A", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "B7UiHu", + "name": "A", + "fill": "$accent", + "content": "运行容器…", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + }, + { + "type": "text", + "id": "d4YCF", + "name": "A", + "fill": "$accent", + "content": "查看镜像", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + } + ] + }, + { + "id": "L9xnik", + "type": "ref", + "ref": "yoK0T", + "name": "X", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "x", + "width": 12, + "height": 12 + } + } + } + ] + }, + { + "type": "frame", + "id": "UmNiB", + "name": "Toast 停止 10 个容器 · 2 个失败", + "width": "fill_container", + "fill": "$bg-elevated", + "cornerRadius": "$r-md", + "stroke": "$warn", + "strokeWidth": 1, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000066", + "offset": { + "x": 0, + "y": 6 + }, + "blur": 20 + }, + "gap": 9, + "padding": [ + 11, + 12 + ], + "children": [ + { + "type": "icon", + "id": "Fbn8z", + "name": "I", + "width": 15, + "height": 15, + "icon": "triangle-alert", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "frame", + "id": "LaAJh", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "srOFM", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "停止 10 个容器 · 2 个失败", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "AyVMb", + "name": "D", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "postgres-main:被 api-gateway 依赖\nminio:容器处于暂停态", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "nvj2r", + "name": "A", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "J8zXN", + "name": "A", + "fill": "$warn", + "content": "查看详情", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + }, + { + "type": "text", + "id": "WD79Y", + "name": "A", + "fill": "$warn", + "content": "重试失败的 2 个", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + } + ] + }, + { + "id": "KzZ1J", + "type": "ref", + "ref": "yoK0T", + "name": "X", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "x", + "width": 12, + "height": 12 + } + } + } + ] + }, + { + "type": "frame", + "id": "O4BF8t", + "name": "Toast compose up 失败 · web-stack", + "width": "fill_container", + "fill": "$bg-elevated", + "cornerRadius": "$r-md", + "stroke": "$danger", + "strokeWidth": 1, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000066", + "offset": { + "x": 0, + "y": 6 + }, + "blur": 20 + }, + "gap": 9, + "padding": [ + 11, + 12 + ], + "children": [ + { + "type": "icon", + "id": "wBcx7", + "name": "I", + "width": 15, + "height": 15, + "icon": "circle-x", + "library": "lucide", + "fill": "$danger" + }, + { + "type": "frame", + "id": "vcYbR", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "aU1j4", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "compose up 失败 · web-stack", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "Tz5bB", + "name": "D", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "service proxy: port 8080 已被 caddy 占用", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "tS5Sv", + "name": "A", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "FVN3M", + "name": "A", + "fill": "$danger", + "content": "查看执行记录", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + }, + { + "type": "text", + "id": "pzsYJ", + "name": "A", + "fill": "$danger", + "content": "编辑 compose.yaml", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + } + ] + }, + { + "id": "R4iUhW", + "type": "ref", + "ref": "yoK0T", + "name": "X", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "x", + "width": 12, + "height": 12 + } + } + } + ] + }, + { + "type": "text", + "id": "HDR2A", + "name": "N", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "规则:动作发起点还在视野里 → 只更新状态栏和那一行;你已经切走了(换了页签、关了对话框)→ 才弹 toast。成功 toast 4s 自动消失,失败的不自动消失。", + "lineHeight": 1.6, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "tR95e", + "x": 1059, + "y": 16656, + "name": "18 · 表单弹窗合集", + "theme": { + "mode": "dark" + }, + "clip": true, + "width": 1440, + "height": 1238, + "fill": "$bg-page", + "layout": "vertical", + "gap": 16, + "padding": 24, + "children": [ + { + "type": "frame", + "id": "nmi0T", + "name": "Title", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "blzpr", + "name": "T", + "fill": "$text-primary", + "content": "二级弹窗 · 表单合集", + "fontFamily": "$font-ui", + "fontSize": 18, + "fontWeight": "600" + }, + { + "type": "text", + "id": "fuehQ", + "name": "S", + "fill": "$text-tertiary", + "content": "全部复用同一个 C/Dialog 壳:标题栏 → 主机条 → 内容 → 页脚。主机条一行都不能省 —— 同时开着两台机器时,它是最便宜的保险。", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "gJmvC", + "name": "Row 1", + "width": "fill_container", + "gap": 36, + "children": [ + { + "id": "Y7vhUT", + "type": "ref", + "ref": "obPIB", + "name": "D 打标签", + "width": "fill_container", + "descendants": { + "ysZdT": { + "icon": "tag", + "fill": "$accent" + }, + "V5qqj": { + "content": "打标签" + }, + "NUsd4": { + "enabled": false + }, + "slcE7": { + "type": "frame", + "id": "W2BtR", + "name": "Body", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": [ + 16, + 18 + ], + "children": [ + { + "type": "frame", + "id": "t2CKL", + "name": "LR", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dMo9h", + "name": "L", + "fill": "$text-secondary", + "content": "源镜像", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "V2w4W", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 30, + "fill": "$bg-surface", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "usCNk", + "name": "V", + "fill": "$text-tertiary", + "content": "ghcr.io/acme/api:2.8.2 · sha256:3f81a9…", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "QT18r", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "O3vTcd", + "name": "L", + "width": 11, + "height": 11, + "icon": "lock", + "library": "lucide", + "fill": "$text-muted" + } + ] + }, + { + "type": "frame", + "id": "s0XGh", + "name": "Two", + "width": "fill_container", + "gap": 10, + "children": [ + { + "type": "frame", + "id": "oZDYX", + "name": "F", + "width": "fill_container", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "frame", + "id": "KBzDU", + "name": "LR", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "y8vbs", + "name": "L", + "fill": "$text-secondary", + "content": "新仓库", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "o8ToP", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 30, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$accent", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "yMgZK", + "name": "V", + "fill": "$text-primary", + "content": "registry.internal/acme/api", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "rectangle", + "id": "y3h8Fz", + "name": "Caret", + "fill": "$accent", + "width": 1.5, + "height": 14 + }, + { + "type": "frame", + "id": "Eie9w", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + } + ] + }, + { + "type": "frame", + "id": "NTZIH", + "name": "F", + "width": "fill_container", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "frame", + "id": "riOWs", + "name": "LR", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "kHOHM", + "name": "L", + "fill": "$text-secondary", + "content": "新标签", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "g1lcxE", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 30, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "F06pi", + "name": "V", + "fill": "$text-primary", + "content": "stable", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ymEoQ", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "B7HE4p", + "name": "Preview", + "width": "fill_container", + "fill": "$bg-terminal", + "cornerRadius": "$r-sm", + "layout": "vertical", + "gap": 3, + "padding": [ + 9, + 11 + ], + "children": [ + { + "type": "text", + "id": "kgKjI", + "name": "C", + "fill": "$text-terminal", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "POST /images/{name}/tag?repo=registry.internal/acme/api&tag=stable", + "lineHeight": 1.5, + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "DMdfn", + "name": "N", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "等价于 docker tag ghcr.io/acme/api:2.8.2 registry.internal/acme/api:stable", + "lineHeight": 1.5, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "KEBqj", + "name": "Note", + "width": "fill_container", + "gap": 8, + "children": [ + { + "type": "icon", + "id": "TYybZ", + "name": "I", + "width": 13, + "height": 13, + "icon": "info", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "TCJZg", + "name": "T", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "标签只是指针 —— 磁盘不会多占 214 MB。删掉旧标签也不会删掉镜像层。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + "WAXqz": { + "content": "不复制镜像,只加一个引用" + }, + "Uk4Ly/RQ8pM": { + "enabled": false + }, + "Uk4Ly/P0aUw": { + "content": "取消" + }, + "C08Vw": { + "fill": "$accent" + }, + "C08Vw/RQ8pM": { + "icon": "tag", + "fill": "$accent-on" + }, + "C08Vw/P0aUw": { + "content": "打标签" + } + } + }, + { + "id": "bKZNa", + "type": "ref", + "ref": "obPIB", + "name": "D 重命名", + "width": "fill_container", + "descendants": { + "ysZdT": { + "icon": "pencil", + "fill": "$accent" + }, + "V5qqj": { + "content": "重命名容器" + }, + "NUsd4": { + "enabled": false + }, + "slcE7": { + "type": "frame", + "id": "cjUVu", + "name": "Body", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": [ + 16, + 18 + ], + "children": [ + { + "type": "frame", + "id": "ZRQSl", + "name": "LR", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "c24jd", + "name": "L", + "fill": "$text-secondary", + "content": "当前名称", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "BAWPA", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 30, + "fill": "$bg-surface", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "cWENc", + "name": "V", + "fill": "$text-tertiary", + "content": "web-stack-proxy-1", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "bR0Zn", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "GHIoh", + "name": "L", + "width": 11, + "height": 11, + "icon": "lock", + "library": "lucide", + "fill": "$text-muted" + } + ] + }, + { + "type": "frame", + "id": "z6n7YZ", + "name": "LR", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "J7pNOB", + "name": "L", + "fill": "$text-secondary", + "content": "新名称", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "slYes", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 30, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$accent", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Vbmhw", + "name": "V", + "fill": "$text-primary", + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "rectangle", + "id": "tO9zz", + "name": "Caret", + "fill": "$accent", + "width": 1.5, + "height": 14 + }, + { + "type": "frame", + "id": "W93v9R", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "VcGF5", + "name": "Valid", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "ELuB4", + "name": "I", + "width": 12, + "height": 12, + "icon": "check", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "AGpJl", + "name": "T", + "fill": "$ok", + "content": "可用 · 符合命名规则且未被占用", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "r2wlMU", + "name": "Warn", + "width": "fill_container", + "fill": "$warn-dim", + "cornerRadius": "$r-sm", + "stroke": "$warn", + "strokeWidth": 1, + "gap": 9, + "padding": [ + 10, + 12 + ], + "children": [ + { + "type": "icon", + "id": "P6xQP", + "name": "I", + "width": 14, + "height": 14, + "icon": "triangle-alert", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "frame", + "id": "T4hOVA", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 3, + "children": [ + { + "type": "text", + "id": "jDyUm", + "name": "T", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "这个容器由 compose 项目 web-stack 管理", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "iv5ju", + "name": "D", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "改名后 compose 会认为服务 proxy 不存在,下次 up -d 会再建一个。要长期生效,请改 compose.yaml 里的 container_name。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "iJiLw", + "name": "Preview", + "width": "fill_container", + "fill": "$bg-terminal", + "cornerRadius": "$r-sm", + "layout": "vertical", + "gap": 3, + "padding": [ + 9, + 11 + ], + "children": [ + { + "type": "text", + "id": "yOymz", + "name": "C", + "fill": "$text-terminal", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "POST /containers/web-stack-proxy-1/rename?name=nginx-proxy", + "lineHeight": 1.5, + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + "WAXqz": { + "content": "改名不重启容器" + }, + "Uk4Ly/RQ8pM": { + "enabled": false + }, + "Uk4Ly/P0aUw": { + "content": "取消" + }, + "C08Vw": { + "fill": "$accent" + }, + "C08Vw/RQ8pM": { + "icon": "check", + "fill": "$accent-on" + }, + "C08Vw/P0aUw": { + "content": "重命名" + } + } + }, + { + "id": "Q5NI9", + "type": "ref", + "ref": "obPIB", + "name": "D 重启策略", + "width": "fill_container", + "descendants": { + "ysZdT": { + "icon": "refresh-cw", + "fill": "$accent" + }, + "V5qqj": { + "content": "修改重启策略" + }, + "NUsd4": { + "enabled": false + }, + "slcE7": { + "type": "frame", + "id": "cdW8m", + "name": "Body", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": [ + 16, + 18 + ], + "children": [ + { + "type": "frame", + "id": "AXLzQ", + "name": "LR", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "DVNoE", + "name": "L", + "fill": "$text-secondary", + "content": "策略", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "c7jA9", + "name": "O no", + "width": "fill_container", + "fill": "#00000000", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 9, + "padding": [ + 9, + 11 + ], + "children": [ + { + "type": "ellipse", + "id": "hGDXO", + "name": "Radio", + "fill": "#00000000", + "width": 13, + "height": 13, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "frame", + "id": "rJiA2", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "j97UwU", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "no", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "AEmEI", + "name": "N", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "不自动重启 —— 容器退出后就停在那里。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "kCBNT", + "name": "O on-failure", + "width": "fill_container", + "fill": "#00000000", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 9, + "padding": [ + 9, + 11 + ], + "children": [ + { + "type": "ellipse", + "id": "gQoTG", + "name": "Radio", + "fill": "#00000000", + "width": 13, + "height": 13, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "frame", + "id": "nbC3l", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "J1FUqC", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "on-failure", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "z66FrU", + "name": "N", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "仅失败时重启 —— 退出码非 0 才重启,可限次数。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "xjMgj", + "name": "Sub", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "goQHW", + "name": "L", + "fill": "$text-muted", + "content": "最大重试", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ydHcu", + "name": "In", + "width": 70, + "height": 24, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "d58bR", + "name": "V", + "fill": "$text-muted", + "content": "5", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "x8idI", + "name": "O always", + "width": "fill_container", + "fill": "#00000000", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 9, + "padding": [ + 9, + 11 + ], + "children": [ + { + "type": "ellipse", + "id": "f7rweu", + "name": "Radio", + "fill": "#00000000", + "width": 13, + "height": 13, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "frame", + "id": "p9C8H", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "AoTDO", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "always", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "pqWQz", + "name": "N", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "总是重启 —— daemon 重启后也拉起 —— 手动停过的也会被拉起。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "e2PYEe", + "name": "O unless-stopped", + "width": "fill_container", + "fill": "$accent-dim", + "cornerRadius": "$r-sm", + "stroke": "$accent", + "strokeWidth": 1, + "gap": 9, + "padding": [ + 9, + 11 + ], + "children": [ + { + "type": "ellipse", + "id": "XpzFr", + "name": "Radio", + "innerRadius": 0.45, + "fill": "$accent", + "width": 13, + "height": 13, + "stroke": "$accent", + "strokeWidth": 1 + }, + { + "type": "frame", + "id": "S9NWb", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "TnskC", + "name": "T", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "unless-stopped", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "B7JKxs", + "name": "N", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "除非被手动停止 —— 同 always,但你手动停过的不会被拉起。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "JX0Ni", + "name": "Preview", + "width": "fill_container", + "fill": "$bg-terminal", + "cornerRadius": "$r-sm", + "layout": "vertical", + "gap": 3, + "padding": [ + 9, + 11 + ], + "children": [ + { + "type": "text", + "id": "IDXd9", + "name": "C", + "fill": "$text-terminal", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "POST /containers/nginx-proxy/update RestartPolicy.Name = unless-stopped", + "lineHeight": 1.5, + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + "WAXqz": { + "content": "立即生效,不重启容器" + }, + "Uk4Ly/RQ8pM": { + "enabled": false + }, + "Uk4Ly/P0aUw": { + "content": "取消" + }, + "C08Vw": { + "fill": "$accent" + }, + "C08Vw/RQ8pM": { + "icon": "check", + "fill": "$accent-on" + }, + "C08Vw/P0aUw": { + "content": "保存" + } + } + } + ] + }, + { + "type": "frame", + "id": "XoFMl", + "name": "Row 2", + "width": "fill_container", + "gap": 36, + "children": [ + { + "id": "z5blqK", + "type": "ref", + "ref": "obPIB", + "name": "D 新建卷", + "width": "fill_container", + "descendants": { + "ysZdT": { + "icon": "database", + "fill": "$accent" + }, + "V5qqj": { + "content": "新建卷" + }, + "NUsd4": { + "enabled": false + }, + "slcE7": { + "type": "frame", + "id": "Q6q61u", + "name": "Body", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": [ + 16, + 18 + ], + "children": [ + { + "type": "frame", + "id": "R6o5a", + "name": "LR", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "aBF52", + "name": "L", + "fill": "$text-secondary", + "content": "名称", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "zhXuu", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "rwtzm", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 30, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$accent", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "YC4Tw", + "name": "V", + "fill": "$text-primary", + "content": "pg-backup", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "rectangle", + "id": "U3E7Ot", + "name": "Caret", + "fill": "$accent", + "width": 1.5, + "height": 14 + }, + { + "type": "frame", + "id": "TjqMh", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "VZ3SX", + "name": "LR", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "xN79Q", + "name": "L", + "fill": "$text-secondary", + "content": "驱动", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "jV1Zg", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "BbMTN", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 30, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "xxwq1", + "name": "V", + "fill": "$text-primary", + "content": "local", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "T77bKk", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "h7a0i", + "name": "T", + "width": 12, + "height": 12, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + }, + { + "type": "frame", + "id": "rkK3e", + "name": "LR", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "LrG3B", + "name": "L", + "fill": "$text-secondary", + "content": "驱动选项", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "M5r9x", + "name": "H", + "fill": "$text-muted", + "content": "留空即用默认本地目录 /var/lib/docker/volumes", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Vo9ES", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "yJdbJ", + "name": "KVR", + "width": "fill_container", + "height": 26, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Z1ckt5", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "R1tjW", + "name": "V", + "fill": "$text-primary", + "content": "type", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Kdx2L", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "text", + "id": "ZnahT", + "name": "Eq", + "fill": "$text-muted", + "content": "=", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "FiCa3", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "R7rE02", + "name": "V", + "fill": "$text-primary", + "content": "nfs", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "e3Us1q", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "id": "yowFT", + "type": "ref", + "ref": "yoK0T", + "name": "Rm", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "x", + "width": 12, + "height": 12 + } + } + } + ] + }, + { + "type": "frame", + "id": "E3Vc0", + "name": "KVR", + "width": "fill_container", + "height": 26, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "I520Q", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "tnG4P", + "name": "V", + "fill": "$text-primary", + "content": "o", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "mJPLQ", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "text", + "id": "K5vddS", + "name": "Eq", + "fill": "$text-muted", + "content": "=", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "E4eKs", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Jordx", + "name": "V", + "fill": "$text-primary", + "content": "addr=10.24.8.20,rw,nfsvers=4", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "dNqUA", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "id": "kQek0", + "type": "ref", + "ref": "yoK0T", + "name": "Rm", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "x", + "width": 12, + "height": 12 + } + } + } + ] + }, + { + "type": "frame", + "id": "CG7oh", + "name": "KVR", + "width": "fill_container", + "height": 26, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "sFHmO", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "p3ixq1", + "name": "V", + "fill": "$text-primary", + "content": "device", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "G0fC9g", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "text", + "id": "hEglX", + "name": "Eq", + "fill": "$text-muted", + "content": "=", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "jzCOi", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "JPkqB", + "name": "V", + "fill": "$text-primary", + "content": ":/exports/pg-backup", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "idyJd", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "id": "NyA57", + "type": "ref", + "ref": "yoK0T", + "name": "Rm", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "x", + "width": 12, + "height": 12 + } + } + } + ] + }, + { + "type": "text", + "id": "TWLx0", + "name": "Add", + "fill": "$accent", + "content": "+ 添加选项", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "W8hi2", + "name": "LR", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "v4TeE", + "name": "L", + "fill": "$text-secondary", + "content": "标签", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "vaqg9", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "WQvdr", + "name": "KVR", + "width": "fill_container", + "height": 26, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "BiMXZ", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "g9QHU6", + "name": "V", + "fill": "$text-primary", + "content": "backup.retention", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "OFmXW", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "text", + "id": "uDqZR", + "name": "Eq", + "fill": "$text-muted", + "content": "=", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "LpcFP", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "A1sb1U", + "name": "V", + "fill": "$text-primary", + "content": "30d", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "g9UM1", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "id": "u6uIpv", + "type": "ref", + "ref": "yoK0T", + "name": "Rm", + "width": 22, + "height": 22, + "descendants": { + "XMGqY": { + "icon": "x", + "width": 12, + "height": 12 + } + } + } + ] + }, + { + "type": "frame", + "id": "C3O32", + "name": "Preview", + "width": "fill_container", + "fill": "$bg-terminal", + "cornerRadius": "$r-sm", + "layout": "vertical", + "gap": 3, + "padding": [ + 9, + 11 + ], + "children": [ + { + "type": "text", + "id": "tGrQf", + "name": "C", + "fill": "$text-terminal", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "POST /volumes/create", + "lineHeight": 1.5, + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "R3vyWC", + "name": "N", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "等价于 docker volume create --driver local -o type=nfs … pg-backup", + "lineHeight": 1.5, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + "WAXqz": { + "content": "卷创建后名称不可改" + }, + "Uk4Ly/RQ8pM": { + "enabled": false + }, + "Uk4Ly/P0aUw": { + "content": "取消" + }, + "C08Vw/RQ8pM": { + "icon": "plus", + "fill": "$accent-on" + }, + "C08Vw/P0aUw": { + "content": "新建卷" + } + } + }, + { + "id": "fnuqt", + "type": "ref", + "ref": "obPIB", + "name": "D 新建网络", + "width": "fill_container", + "descendants": { + "ysZdT": { + "icon": "network", + "fill": "$accent" + }, + "V5qqj": { + "content": "新建网络" + }, + "NUsd4": { + "enabled": false + }, + "slcE7": { + "type": "frame", + "id": "i5j42", + "name": "Body", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": [ + 16, + 18 + ], + "children": [ + { + "type": "frame", + "id": "b5K8V", + "name": "LR", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mhESB", + "name": "L", + "fill": "$text-secondary", + "content": "名称", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "PJr52", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "thrvi", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 30, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$accent", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "KAtKn", + "name": "V", + "fill": "$text-primary", + "content": "edge-dmz", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "rectangle", + "id": "CYveF", + "name": "Caret", + "fill": "$accent", + "width": 1.5, + "height": 14 + }, + { + "type": "frame", + "id": "KQcmv", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "AcCoB", + "name": "LR", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "P6eheQ", + "name": "L", + "fill": "$text-secondary", + "content": "驱动", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "pxkEI", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "V4uIG", + "name": "Seg", + "width": "fill_container", + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 2, + "padding": 2, + "children": [ + { + "id": "e94L4", + "type": "ref", + "ref": "MdmRT", + "name": "S bridge", + "width": "fill_container", + "height": 22, + "justifyContent": "center", + "opacity": 1, + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "bridge", + "fontSize": 11, + "fontFamily": "$font-mono", + "fill": "$text-primary" + } + } + }, + { + "id": "PFM2o", + "type": "ref", + "ref": "MdmRT", + "name": "S macvlan", + "width": "fill_container", + "height": 22, + "justifyContent": "center", + "opacity": 1, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "macvlan", + "fontSize": 11, + "fontFamily": "$font-mono", + "fill": "$text-tertiary" + } + } + }, + { + "id": "iFjVh", + "type": "ref", + "ref": "MdmRT", + "name": "S ipvlan", + "width": "fill_container", + "height": 22, + "justifyContent": "center", + "opacity": 1, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "ipvlan", + "fontSize": 11, + "fontFamily": "$font-mono", + "fill": "$text-tertiary" + } + } + }, + { + "id": "j0eav", + "type": "ref", + "ref": "MdmRT", + "name": "S overlay", + "width": "fill_container", + "height": 22, + "justifyContent": "center", + "opacity": 0.35, + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "overlay", + "fontSize": 11, + "fontFamily": "$font-mono", + "fill": "$text-tertiary" + } + } + } + ] + }, + { + "type": "text", + "id": "zdJIJ", + "name": "H", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "overlay 需要 swarm 模式 —— 当前主机 Swarm: inactive,所以直接禁用而不是让你撞一条 docker 的错误。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "I5HfDR", + "name": "Two", + "width": "fill_container", + "gap": 10, + "children": [ + { + "type": "frame", + "id": "czikI", + "name": "F", + "width": "fill_container", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "frame", + "id": "lQo1d", + "name": "LR", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "RyLDU", + "name": "L", + "fill": "$text-secondary", + "content": "子网", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "uCzCW", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "GuGBS", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 30, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "GWd7k", + "name": "V", + "fill": "$text-primary", + "content": "172.28.0.0/16", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "IwkKf", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + } + ] + }, + { + "type": "frame", + "id": "bzfeg", + "name": "F", + "width": "fill_container", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "frame", + "id": "pEImO", + "name": "LR", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "YF8aX", + "name": "L", + "fill": "$text-secondary", + "content": "网关", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "o0Sn2", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "haIs7", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 30, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Lo58o", + "name": "V", + "fill": "$text-primary", + "content": "172.28.0.1", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "lcMKe", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "a6EIZ", + "name": "LR", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ZCaZQ", + "name": "L", + "fill": "$text-secondary", + "content": "运行选项", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "xDlBe", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "Y1jEB", + "name": "T internal", + "width": "fill_container", + "gap": 9, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "LgVfI", + "name": "Sw", + "width": 26, + "height": 15, + "fill": "$bg-active", + "cornerRadius": 8, + "stroke": "$border-strong", + "strokeWidth": 1, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "S5OibB", + "name": "K", + "fill": "$text-muted", + "width": 11, + "height": 11 + } + ] + }, + { + "type": "frame", + "id": "JFzpJ", + "name": "C", + "clip": true, + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "DgNH6", + "name": "T", + "fill": "$text-secondary", + "content": "internal", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "THje0", + "name": "D", + "fill": "$text-muted", + "content": "不给这个网络出外网的能力", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "b2tjTJ", + "name": "T attachable", + "width": "fill_container", + "gap": 9, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "pckAt", + "name": "Sw", + "width": 26, + "height": 15, + "fill": "$accent", + "cornerRadius": 8, + "stroke": "#00000000", + "strokeWidth": 1, + "padding": [ + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "Vm2Jp", + "name": "K", + "fill": "$accent-on", + "width": 11, + "height": 11 + } + ] + }, + { + "type": "frame", + "id": "jFikm", + "name": "C", + "clip": true, + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "OZ0tH", + "name": "T", + "fill": "$text-secondary", + "content": "attachable", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Jq5ku", + "name": "D", + "fill": "$text-muted", + "content": "允许 docker run --network 事后接入", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "z2jv1b", + "name": "T 启用 IPv6", + "width": "fill_container", + "gap": 9, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Zi3BJ", + "name": "Sw", + "width": 26, + "height": 15, + "fill": "$bg-active", + "cornerRadius": 8, + "stroke": "$border-strong", + "strokeWidth": 1, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "hyTZx", + "name": "K", + "fill": "$text-muted", + "width": 11, + "height": 11 + } + ] + }, + { + "type": "frame", + "id": "xkQr3", + "name": "C", + "clip": true, + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "OnqEK", + "name": "T", + "fill": "$text-secondary", + "content": "启用 IPv6", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Nutm5", + "name": "D", + "fill": "$text-muted", + "content": "需要 daemon 开了 ipv6", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "n1K1j", + "name": "Preview", + "width": "fill_container", + "fill": "$bg-terminal", + "cornerRadius": "$r-sm", + "layout": "vertical", + "gap": 3, + "padding": [ + 9, + 11 + ], + "children": [ + { + "type": "text", + "id": "JWL4f", + "name": "C", + "fill": "$text-terminal", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "POST /networks/create", + "lineHeight": 1.5, + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "we6ri", + "name": "N", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "等价于 docker network create -d bridge --subnet 172.28.0.0/16 --gateway 172.28.0.1 --attachable edge-dmz", + "lineHeight": 1.5, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + "WAXqz": { + "content": "子网留空则由 Docker 自动分配" + }, + "Uk4Ly/RQ8pM": { + "enabled": false + }, + "Uk4Ly/P0aUw": { + "content": "取消" + }, + "C08Vw/RQ8pM": { + "icon": "plus", + "fill": "$accent-on" + }, + "C08Vw/P0aUw": { + "content": "新建网络" + } + } + }, + { + "id": "X1dNP", + "type": "ref", + "ref": "obPIB", + "name": "D 接入网络", + "width": "fill_container", + "descendants": { + "ysZdT": { + "icon": "plug", + "fill": "$accent" + }, + "V5qqj": { + "content": "接入容器到网络" + }, + "NUsd4": { + "enabled": false + }, + "slcE7": { + "type": "frame", + "id": "ysMF6", + "name": "Body", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": [ + 16, + 18 + ], + "children": [ + { + "type": "frame", + "id": "RewJx", + "name": "LR", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "vzrFZ", + "name": "L", + "fill": "$text-secondary", + "content": "目标网络", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "ijVUG", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "fHQuw", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 30, + "fill": "$bg-surface", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "l2Xn5", + "name": "V", + "fill": "$text-tertiary", + "content": "web-stack_default · bridge · 172.20.0.0/16", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ZUKpd", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "pSPKE", + "name": "L", + "width": 11, + "height": 11, + "icon": "lock", + "library": "lucide", + "fill": "$text-muted" + } + ] + }, + { + "type": "frame", + "id": "sUyaE", + "name": "LR", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "N0tkdE", + "name": "L", + "fill": "$text-secondary", + "content": "选择容器", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "kEjRH", + "name": "H", + "fill": "$text-muted", + "content": "已选 2 个", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "c1zm5", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "NqOxV", + "name": "Search", + "width": "fill_container", + "height": 28, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "sniwz", + "name": "I", + "width": 12, + "height": 12, + "icon": "search", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "text", + "id": "FM5Zl", + "name": "P", + "fill": "$text-muted", + "content": "过滤未接入的容器…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "D6mrZz", + "name": "List", + "clip": true, + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "NLofe", + "name": "S postgres-main", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "$accent-dim", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "gap": 8, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "wLEnK", + "name": "Cb", + "fill": "$accent", + "width": 12, + "height": 12, + "stroke": "$accent", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "bNn8U", + "name": "N", + "fill": "$text-primary", + "content": "postgres-main", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "aHcx0", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "MedRf", + "name": "St", + "fill": "$text-muted", + "content": "运行中 · db-stack_default", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "RpLom", + "name": "S redis-cache", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "$accent-dim", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "gap": 8, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "FDmOC", + "name": "Cb", + "fill": "$accent", + "width": 12, + "height": 12, + "stroke": "$accent", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "WqLvQ", + "name": "N", + "fill": "$text-primary", + "content": "redis-cache", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "WHwzO", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "iSquR", + "name": "St", + "fill": "$text-muted", + "content": "运行中 · db-stack_default", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "fO7FB", + "name": "S worker-1", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "gap": 8, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "y0AIt", + "name": "Cb", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "enE2d", + "name": "N", + "fill": "$text-primary", + "content": "worker-1", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "vUB2j", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Ak4Zz", + "name": "St", + "fill": "$text-muted", + "content": "运行中 · jobs_default", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "B2i84h", + "name": "S grafana", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "gap": 8, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "N2Z3B", + "name": "Cb", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "rnUev", + "name": "N", + "fill": "$text-primary", + "content": "grafana", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "sq9Nj", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "nxWAX", + "name": "St", + "fill": "$text-muted", + "content": "已停止 · 接入后下次启动生效", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "p5dvr", + "name": "S nginx-proxy", + "opacity": 0.4, + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "gap": 8, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "MDLMM", + "name": "Cb", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "C2DaQ", + "name": "N", + "fill": "$text-primary", + "content": "nginx-proxy", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "SgKrV", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "QJEa5", + "name": "St", + "fill": "$text-muted", + "content": "已在这个网络里", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "gF3yt", + "name": "Two", + "width": "fill_container", + "gap": 10, + "children": [ + { + "type": "frame", + "id": "P0BF7h", + "name": "F", + "width": "fill_container", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "frame", + "id": "y1BvgM", + "name": "LR", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "l6h4B", + "name": "L", + "fill": "$text-secondary", + "content": "网络别名", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "mw82z", + "name": "H", + "fill": "$text-muted", + "content": "逗号分隔", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "PmJb8", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "Kvsgn", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 30, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "W43KN", + "name": "V", + "fill": "$text-primary", + "content": "db, primary", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "FUhzM", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Xqpmg", + "name": "F", + "width": "fill_container", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "frame", + "id": "dOOcQ", + "name": "LR", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "emdxv", + "name": "L", + "fill": "$text-secondary", + "content": "静态 IPv4", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "QqFFu", + "name": "H", + "fill": "$text-muted", + "content": "须落在子网内", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "SJnDY", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "MgJWD", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 30, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "gywbk", + "name": "V", + "fill": "$text-muted", + "content": "自动分配", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "TVHnG", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "FEGaM", + "name": "Preview", + "width": "fill_container", + "fill": "$bg-terminal", + "cornerRadius": "$r-sm", + "layout": "vertical", + "gap": 3, + "padding": [ + 9, + 11 + ], + "children": [ + { + "type": "text", + "id": "rZUcy", + "name": "C", + "fill": "$text-terminal", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "POST /networks/web-stack_default/connect × 2", + "lineHeight": 1.5, + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "DvM4z", + "name": "N", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "等价于 docker network connect --alias db --alias primary web-stack_default postgres-main", + "lineHeight": 1.5, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + "WAXqz": { + "content": "接入后立即生效,不重启容器" + }, + "Uk4Ly/RQ8pM": { + "enabled": false + }, + "Uk4Ly/P0aUw": { + "content": "取消" + }, + "C08Vw/RQ8pM": { + "icon": "plug", + "fill": "$accent-on" + }, + "C08Vw/P0aUw": { + "content": "接入 2 个容器" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "wNul8", + "x": 1059, + "y": 18014, + "name": "19 · 菜单与弹层", + "theme": { + "mode": "dark" + }, + "clip": true, + "width": 1440, + "height": 1335, + "fill": "$bg-page", + "layout": "vertical", + "gap": 16, + "padding": 24, + "children": [ + { + "type": "frame", + "id": "HPxs2", + "name": "Title", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "vdkJZ", + "name": "T", + "fill": "$text-primary", + "content": "菜单与弹层", + "fontFamily": "$font-ui", + "fontSize": 18, + "fontWeight": "600" + }, + { + "type": "text", + "id": "r4hfy", + "name": "S", + "fill": "$text-tertiary", + "content": "轻量选择用弹层(点外面即关、不拦交互),要填表或有后果的才升级成对话框。破坏性动作在菜单里永远单独成组,并且带省略号 —— 省略号代表「还会再问你一次」。", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "uIPzz", + "name": "Row 1", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "R1NZgu", + "name": "C 主机切换器", + "clip": true, + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "rtAsP", + "name": "H", + "width": "fill_container", + "height": 32, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "bi07p", + "name": "T", + "fill": "$text-primary", + "content": "主机切换器", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "v1T1cr", + "name": "S", + "fill": "$text-muted", + "content": "顶栏点开", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "T6qD4", + "name": "B", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": 14, + "children": [ + { + "type": "frame", + "id": "jyiWV", + "name": "Trigger", + "width": "fill_container", + "height": 26, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$accent", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "T4KODa", + "name": "I", + "width": 13, + "height": 13, + "icon": "server", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "Y1SUM", + "name": "H", + "fill": "$text-primary", + "content": "prod-sg-01", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "diFCM", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "tX5dv", + "name": "C", + "width": 12, + "height": 12, + "icon": "chevron-up", + "library": "lucide", + "fill": "$accent" + } + ] + }, + { + "type": "frame", + "id": "OMAhm", + "name": "Popover", + "clip": true, + "width": "fill_container", + "fill": "$bg-elevated", + "cornerRadius": "$r-md", + "stroke": "$border-strong", + "strokeWidth": 1, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000073", + "offset": { + "x": 0, + "y": 8 + }, + "blur": 24 + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "p7HR8", + "name": "Grp", + "width": "fill_container", + "height": 22, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "FhEY8", + "name": "T", + "fill": "$text-muted", + "content": "本机", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "h79zXu", + "name": "Local", + "clip": true, + "width": "fill_container", + "height": 40, + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "p9GUuO", + "name": "I", + "width": 14, + "height": 14, + "icon": "monitor", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "frame", + "id": "J6cIB", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "text", + "id": "z4z6bb", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "本机 Docker", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "uiiM9", + "name": "S", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "//./pipe/docker_engine · 27.2.0", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "id": "jZ9fO", + "type": "ref", + "ref": "x0Pcos", + "name": "Chip", + "height": 16, + "fill": "$bg-active", + "descendants": { + "kvk5k": { + "enabled": false + }, + "ODVko": { + "content": "可用", + "fill": "$text-tertiary", + "fontSize": 9 + } + } + } + ] + }, + { + "type": "rectangle", + "id": "rrgrZ", + "name": "Sep", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "luSca", + "name": "Grp", + "width": "fill_container", + "height": 22, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "N9obM6", + "name": "T", + "fill": "$text-muted", + "content": "已连接的 SSH 会话 (3)", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "ToXGa", + "name": "H prod-sg-01", + "clip": true, + "width": "fill_container", + "height": 40, + "fill": "$accent-dim", + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "uMnj5", + "name": "I", + "width": 14, + "height": 14, + "icon": "server", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "frame", + "id": "wnntY", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "xDOZ4", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "faZ1o", + "name": "T", + "fill": "$text-primary", + "content": "prod-sg-01", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "aDW0c", + "name": "V", + "fill": "$text-muted", + "content": "Engine 27.3.1", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "F4uAK6", + "name": "S", + "fill": "$ok", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "deploy@10.24.8.11 · 通道已建立", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "icon", + "id": "JO0uF", + "name": "K", + "width": 13, + "height": 13, + "icon": "check", + "library": "lucide", + "fill": "$accent" + } + ] + }, + { + "type": "frame", + "id": "TUvz9", + "name": "H staging-fra", + "clip": true, + "width": "fill_container", + "height": 40, + "fill": "#00000000", + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "G8Bmv", + "name": "I", + "width": 14, + "height": 14, + "icon": "server", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "frame", + "id": "LHIbr", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "Gjt3j", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "tlJqO", + "name": "T", + "fill": "$text-primary", + "content": "staging-fra", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Vo5A8", + "name": "V", + "fill": "$text-muted", + "content": "Engine 26.1.4", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "N8SIj3", + "name": "S", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "deploy@10.55.2.4 · 未建立 · 点击连接", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "jGgDJ", + "name": "H build-01", + "opacity": 0.5, + "clip": true, + "width": "fill_container", + "height": 40, + "fill": "#00000000", + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "SX0WC", + "name": "I", + "width": 14, + "height": 14, + "icon": "server", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "frame", + "id": "SE6CK", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "frame", + "id": "dJgIr", + "name": "R", + "width": "fill_container", + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "W40RCS", + "name": "T", + "fill": "$text-primary", + "content": "build-01", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "v5h5R", + "name": "V", + "fill": "$text-muted", + "content": "—", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "aZ7bA", + "name": "S", + "fill": "$danger", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "ci@10.55.9.9 · 找不到 docker.sock", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "OvqyC", + "name": "Sep", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "A5xf6n", + "name": "F", + "width": "fill_container", + "height": 28, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "NIYXM", + "name": "T", + "fill": "$text-muted", + "content": "只列出宿主已连接的会话 —— 面板不发起连接", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "W6zXN", + "name": "C 容器行右键菜单", + "clip": true, + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "w2AM2", + "name": "H", + "width": "fill_container", + "height": 32, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "FsqUz", + "name": "T", + "fill": "$text-primary", + "content": "容器行右键菜单", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "noHQ8", + "name": "S", + "fill": "$text-muted", + "content": "右键 / ⋯ 都是同一份", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "h4yEb", + "name": "B", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": 14, + "children": [ + { + "type": "frame", + "id": "pVWZn", + "name": "Popover", + "clip": true, + "width": 268, + "fill": "$bg-elevated", + "cornerRadius": "$r-md", + "stroke": "$border-strong", + "strokeWidth": 1, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000073", + "offset": { + "x": 0, + "y": 8 + }, + "blur": 24 + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "NgVVw", + "name": "MI 查看日志", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "l68ht", + "name": "I", + "width": 13, + "height": 13, + "icon": "scroll-text", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "wtxTR", + "name": "T", + "fill": "$text-secondary", + "content": "查看日志", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "UpOgr", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "aesd8", + "name": "K", + "fill": "$text-muted", + "content": "Ctrl L", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "x6PKr", + "name": "MI 进入终端", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "$bg-hover", + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "j1ODDN", + "name": "I", + "width": 13, + "height": 13, + "icon": "terminal", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "OYZz5", + "name": "T", + "fill": "$text-secondary", + "content": "进入终端", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "pymgF", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "CZdb1", + "name": "K", + "fill": "$text-muted", + "content": "Ctrl `", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "unOUk", + "name": "MI 浏览文件", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "HTSIg", + "name": "I", + "width": 13, + "height": 13, + "icon": "folder-tree", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "Km77S", + "name": "T", + "fill": "$text-secondary", + "content": "浏览文件", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "GKg5K", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "sf8Ne", + "name": "MI 实时统计", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "oh8Bw", + "name": "I", + "width": 13, + "height": 13, + "icon": "activity", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "akvnS", + "name": "T", + "fill": "$text-secondary", + "content": "实时统计", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "J3KBRv", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "rectangle", + "id": "r4z8U", + "name": "Sep", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "taKDp", + "name": "MI 启动", + "opacity": 0.35, + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "W7LI5m", + "name": "I", + "width": 13, + "height": 13, + "icon": "play", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "yjoh9", + "name": "T", + "fill": "$text-secondary", + "content": "启动", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "W5YOVL", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "aTmgG", + "name": "MI 停止", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "Prw3m", + "name": "I", + "width": 13, + "height": 13, + "icon": "square", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "UYGOO", + "name": "T", + "fill": "$text-secondary", + "content": "停止", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Nuq8s", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "DhkaT", + "name": "K", + "fill": "$text-muted", + "content": "Ctrl .", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "kyLzA", + "name": "MI 重启", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "J1b8h", + "name": "I", + "width": 13, + "height": 13, + "icon": "rotate-cw", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "eKpkx", + "name": "T", + "fill": "$text-secondary", + "content": "重启", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "qpRxh", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "r0PVUl", + "name": "K", + "fill": "$text-muted", + "content": "Ctrl R", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "OFJiy", + "name": "MI 暂停", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "RFuVg", + "name": "I", + "width": 13, + "height": 13, + "icon": "pause", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "Ecbpk", + "name": "T", + "fill": "$text-secondary", + "content": "暂停", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "w5nAQp", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "yqGMk", + "name": "MI 强制杀死…", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "iB5b1", + "name": "I", + "width": 13, + "height": 13, + "icon": "zap", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "text", + "id": "TQ4iq", + "name": "T", + "fill": "$warn", + "content": "强制杀死…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "B2DiNn", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "j6Qjmc", + "name": "C", + "width": 12, + "height": 12, + "icon": "chevron-right", + "library": "lucide", + "fill": "$text-muted" + } + ] + }, + { + "type": "rectangle", + "id": "JDr3a", + "name": "Sep", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "Q0Ybey", + "name": "MI 重命名…", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "D4SYZ6", + "name": "I", + "width": 13, + "height": 13, + "icon": "pencil", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "kI4Fp", + "name": "T", + "fill": "$text-secondary", + "content": "重命名…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "vxAhH", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "E0yY7", + "name": "MI 修改重启策略…", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "liHPD", + "name": "I", + "width": 13, + "height": 13, + "icon": "refresh-cw", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "Zxxcs", + "name": "T", + "fill": "$text-secondary", + "content": "修改重启策略…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "prC7t", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "Szkqn", + "name": "MI 复制容器 ID", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "dqIlV", + "name": "I", + "width": 13, + "height": 13, + "icon": "copy", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "JZE1h", + "name": "T", + "fill": "$text-secondary", + "content": "复制容器 ID", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "fo41O", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "v31kye", + "name": "MI 复制 docker run 命令", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "NwkdX", + "name": "I", + "width": 13, + "height": 13, + "icon": "braces", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "s5X5RQ", + "name": "T", + "fill": "$text-secondary", + "content": "复制 docker run 命令", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "qF6b0", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "rectangle", + "id": "dvUwP", + "name": "Sep", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "jXnxM", + "name": "MI 删除…", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "BMP8U", + "name": "I", + "width": 13, + "height": 13, + "icon": "trash-2", + "library": "lucide", + "fill": "$danger" + }, + { + "type": "text", + "id": "rsceb", + "name": "T", + "fill": "$danger", + "content": "删除…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "hPnPj", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + } + ] + }, + { + "type": "text", + "id": "iwurw", + "name": "N", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "带省略号的项 = 还会再弹一次确认;不带的立即执行。停止/重启不需要确认(可逆),删除、强杀需要。", + "lineHeight": 1.6, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "AC3g2", + "name": "C 溢出菜单 / 筛选弹层", + "clip": true, + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "IIEK4", + "name": "H", + "width": "fill_container", + "height": 32, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "AlzuB", + "name": "T", + "fill": "$text-primary", + "content": "溢出菜单 / 筛选弹层", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "VYyp1", + "name": "B", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": 14, + "children": [ + { + "type": "frame", + "id": "JoF2K", + "name": "Popover", + "clip": true, + "width": 240, + "fill": "$bg-elevated", + "cornerRadius": "$r-md", + "stroke": "$border-strong", + "strokeWidth": 1, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000073", + "offset": { + "x": 0, + "y": 8 + }, + "blur": 24 + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "VpLQO", + "name": "Grp", + "width": "fill_container", + "height": 22, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "YCRkd", + "name": "T", + "fill": "$text-muted", + "content": "镜像 · 更多", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "Y1Vnt", + "name": "MI 导出为 tar…", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "xzzIn", + "name": "I", + "width": 13, + "height": 13, + "icon": "file-archive", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "QUeUO", + "name": "T", + "fill": "$text-secondary", + "content": "导出为 tar…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "TCAPF", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "uYfEP", + "name": "MI 从容器提交为镜像…", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "d2eUp", + "name": "I", + "width": 13, + "height": 13, + "icon": "git-commit-vertical", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "wJUgS", + "name": "T", + "fill": "$text-secondary", + "content": "从容器提交为镜像…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "S2wKct", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "YhXbt", + "name": "MI 查看 inspect JSON", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "KwtsG", + "name": "I", + "width": 13, + "height": 13, + "icon": "braces", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "M7BUb", + "name": "T", + "fill": "$text-secondary", + "content": "查看 inspect JSON", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "bYQMP", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "YAkeT", + "name": "MI 构建历史", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "V1pQry", + "name": "I", + "width": 13, + "height": 13, + "icon": "history", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "k1brDt", + "name": "T", + "fill": "$text-secondary", + "content": "构建历史", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "YWm0h", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "rectangle", + "id": "Ks0sa", + "name": "Sep", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "TvaiF", + "name": "MI 删除镜像…", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "PlzmE", + "name": "I", + "width": 13, + "height": 13, + "icon": "trash-2", + "library": "lucide", + "fill": "$danger" + }, + { + "type": "text", + "id": "AxLb9", + "name": "T", + "fill": "$danger", + "content": "删除镜像…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "bzVaa", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "KVIDg", + "name": "D", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "hXU4S", + "name": "Popover", + "clip": true, + "width": 268, + "fill": "$bg-elevated", + "cornerRadius": "$r-md", + "stroke": "$border-strong", + "strokeWidth": 1, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000073", + "offset": { + "x": 0, + "y": 8 + }, + "blur": 24 + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "NyUrf", + "name": "H", + "width": "fill_container", + "height": 30, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 7, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "m2gCLo", + "name": "T", + "fill": "$text-primary", + "content": "筛选", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "PiP6Y", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "pHs6a", + "name": "A", + "fill": "$accent", + "content": "清除", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "CTXo3", + "name": "Grp", + "width": "fill_container", + "height": 22, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "V2V1BV", + "name": "T", + "fill": "$text-muted", + "content": "状态", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "GMPNJ", + "name": "F 运行中", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "a2xA1r", + "name": "Cb", + "fill": "$accent", + "width": 12, + "height": 12, + "stroke": "$accent", + "strokeWidth": 1 + }, + { + "type": "ellipse", + "id": "SOiGr", + "name": "D", + "fill": "$ok", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "TyzKC", + "name": "T", + "fill": "$text-secondary", + "content": "运行中", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "s5BVs6", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "n6HSc", + "name": "N", + "fill": "$text-muted", + "content": "12", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "vA571", + "name": "F 已停止", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "rDEDW", + "name": "Cb", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "ellipse", + "id": "G9D3E", + "name": "D", + "fill": "$text-muted", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "s8UhK9", + "name": "T", + "fill": "$text-secondary", + "content": "已停止", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "aINeO", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Ix6ZS", + "name": "N", + "fill": "$text-muted", + "content": "5", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "WCrM6", + "name": "F 已暂停", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "oiCNO", + "name": "Cb", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "ellipse", + "id": "yzTED", + "name": "D", + "fill": "$warn", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "jsomt", + "name": "T", + "fill": "$text-secondary", + "content": "已暂停", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "wIuuo", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "e1qkZ", + "name": "N", + "fill": "$text-muted", + "content": "1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "VDvqE", + "name": "F 异常退出", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "wqe9f", + "name": "Cb", + "fill": "$accent", + "width": 12, + "height": 12, + "stroke": "$accent", + "strokeWidth": 1 + }, + { + "type": "ellipse", + "id": "DFdOK", + "name": "D", + "fill": "$danger", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "v6c0o", + "name": "T", + "fill": "$text-secondary", + "content": "异常退出", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "VHZTp", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "EhWGV", + "name": "N", + "fill": "$text-muted", + "content": "1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "cluuS", + "name": "Grp", + "width": "fill_container", + "height": 22, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "e1bA7", + "name": "T", + "fill": "$text-muted", + "content": "COMPOSE 项目", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "ifZep", + "name": "P web-stack", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "gEQft", + "name": "Cb", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "zdCfW", + "name": "T", + "fill": "$text-secondary", + "content": "web-stack", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "wHzh4", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "p54nnZ", + "name": "N", + "fill": "$text-muted", + "content": "3", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "m3Qrw3", + "name": "P db-stack", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "z7FUN", + "name": "Cb", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "F366V", + "name": "T", + "fill": "$text-secondary", + "content": "db-stack", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ecy20", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "OIFBI", + "name": "N", + "fill": "$text-muted", + "content": "2", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "OVJFd", + "name": "P jobs", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "kzlYM", + "name": "Cb", + "fill": "$accent", + "width": 12, + "height": 12, + "stroke": "$accent", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "yTSQs", + "name": "T", + "fill": "$text-secondary", + "content": "jobs", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "q6kMm", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "YvGeP", + "name": "N", + "fill": "$text-muted", + "content": "2", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "DjNxw", + "name": "P (不属于任何项目)", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 9, + "padding": [ + 0, + 11 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "BvXU0", + "name": "Cb", + "fill": "#00000000", + "width": 12, + "height": 12, + "stroke": "$border-strong", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "AgzG9", + "name": "T", + "fill": "$text-secondary", + "content": "(不属于任何项目)", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "SGDB8", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "ANUdr", + "name": "N", + "fill": "$text-muted", + "content": "11", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "G0Wk4", + "name": "Row 2", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "JcUYM", + "name": "C 命令面板", + "clip": true, + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "WEN3B", + "name": "H", + "width": "fill_container", + "height": 32, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "M38mgA", + "name": "T", + "fill": "$text-primary", + "content": "命令面板", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "W2WNdt", + "name": "S", + "fill": "$text-muted", + "content": "Ctrl K · 在面板里,不劫持宿主的命令面板", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "iXdwl", + "name": "B", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": 14, + "children": [ + { + "type": "frame", + "id": "X9uHQv", + "name": "Palette", + "clip": true, + "width": "fill_container", + "fill": "$bg-elevated", + "cornerRadius": "$r-md", + "stroke": "$border-strong", + "strokeWidth": 1, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000080", + "offset": { + "x": 0, + "y": 10 + }, + "blur": 30 + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "obTje", + "name": "Input", + "width": "fill_container", + "height": 44, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 10, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "doPMp", + "name": "I", + "width": 16, + "height": 16, + "icon": "search", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "IH1vy", + "name": "V", + "fill": "$text-primary", + "content": "res", + "fontFamily": "$font-mono", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "rectangle", + "id": "gRl3S", + "name": "Caret", + "fill": "$accent", + "width": 1.5, + "height": 18 + }, + { + "type": "frame", + "id": "byy7D", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "OBxvG", + "name": "H", + "fill": "$text-muted", + "content": "prod-sg-01", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "FMSld", + "name": "G 动作", + "width": "fill_container", + "height": 24, + "fill": "$bg-surface", + "gap": 7, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "HlNFg", + "name": "T", + "fill": "$text-muted", + "content": "动作", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "Ysdsp", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "f3Ydd1", + "name": "N", + "fill": "$text-muted", + "content": "2", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "VXsJY", + "name": "P 重启 ", + "clip": true, + "width": "fill_container", + "height": 34, + "fill": "$accent-dim", + "gap": 10, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "btpL0", + "name": "I", + "width": 14, + "height": 14, + "icon": "rotate-cw", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "vANh7", + "name": "A", + "fill": "$text-primary", + "content": "重启 ", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "YfiVR", + "name": "B", + "fill": "$accent", + "content": "nginx-proxy", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ObW6H", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Q3roqh", + "name": "M", + "fill": "$text-muted", + "content": "运行中 · web-stack", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "L1ZHIn", + "name": "K", + "fill": "$text-muted", + "content": "⏎", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "UfAel", + "name": "P 重启 ", + "clip": true, + "width": "fill_container", + "height": 34, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "nzHym", + "name": "I", + "width": 14, + "height": 14, + "icon": "rotate-cw", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "Oshwr", + "name": "A", + "fill": "$text-primary", + "content": "重启 ", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "u5qEUh", + "name": "B", + "fill": "$accent", + "content": "api-gateway", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "hn12S", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "LzwPW", + "name": "M", + "fill": "$text-muted", + "content": "运行中 · web-stack", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Pc7Y9", + "name": "G 容器", + "width": "fill_container", + "height": 24, + "fill": "$bg-surface", + "gap": 7, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "B01NOw", + "name": "T", + "fill": "$text-muted", + "content": "容器", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "mrJr7", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "w1Fstl", + "name": "N", + "fill": "$text-muted", + "content": "1", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "R0RZK", + "name": "P ", + "clip": true, + "width": "fill_container", + "height": 34, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "p1O8hF", + "name": "I", + "width": 14, + "height": 14, + "icon": "box", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "z8CIh3", + "name": "A", + "fill": "$text-primary", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "R9k5IZ", + "name": "B", + "fill": "$accent", + "content": "redis-cache", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "xEMem", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "t9NCq", + "name": "M", + "fill": "$text-muted", + "content": "运行中 · 12d 6h · 6379", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "N2dMHC", + "name": "G 镜像 / 卷", + "width": "fill_container", + "height": 24, + "fill": "$bg-surface", + "gap": 7, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "LR2B6", + "name": "T", + "fill": "$text-muted", + "content": "镜像 / 卷", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "e6aWg", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "SZcBh", + "name": "N", + "fill": "$text-muted", + "content": "2", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "RCOw2", + "name": "P ", + "clip": true, + "width": "fill_container", + "height": 34, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "PcrNc", + "name": "I", + "width": 14, + "height": 14, + "icon": "database", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "xKCgU", + "name": "A", + "fill": "$text-primary", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "XCars", + "name": "B", + "fill": "$accent", + "content": "pg-data", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "wsRgF", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "to0Xy", + "name": "M", + "fill": "$text-muted", + "content": "卷 · 18.4 GB", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "C7Qi8h", + "name": "P ", + "clip": true, + "width": "fill_container", + "height": 34, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "PX4mW", + "name": "I", + "width": 14, + "height": 14, + "icon": "layers", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "bSztr", + "name": "A", + "fill": "$text-primary", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "z0sWR", + "name": "B", + "fill": "$accent", + "content": "registry:2.8.3", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "UhmMq", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "XrUnk", + "name": "M", + "fill": "$text-muted", + "content": "镜像 · 24.1 MB", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "M0jDN", + "name": "G 面板命令", + "width": "fill_container", + "height": 24, + "fill": "$bg-surface", + "gap": 7, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "s2954", + "name": "T", + "fill": "$text-muted", + "content": "面板命令", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "keMQb", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "tI5cI", + "name": "N", + "fill": "$text-muted", + "content": "2", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "W5OPo5", + "name": "P ", + "clip": true, + "width": "fill_container", + "height": 34, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "f4EU9w", + "name": "I", + "width": 14, + "height": 14, + "icon": "brush-cleaning", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "text", + "id": "O8WeG", + "name": "A", + "fill": "$text-primary", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "NTKQn", + "name": "B", + "fill": "$accent", + "content": "清理未使用的镜像…", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "pHMVA", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "D8JInD", + "name": "M", + "fill": "$text-muted", + "content": "可回收 5.8 GB", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "LzprG", + "name": "P ", + "clip": true, + "width": "fill_container", + "height": 34, + "fill": "#00000000", + "gap": 10, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "Itqef", + "name": "I", + "width": 14, + "height": 14, + "icon": "settings", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "PK1Wt", + "name": "A", + "fill": "$text-primary", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "f9lT0D", + "name": "B", + "fill": "$accent", + "content": "打开设置", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Cfdis", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "V0XhZt", + "name": "F", + "width": "fill_container", + "height": 30, + "fill": "$bg-surface", + "stroke": "$border", + "strokeWidth": { + "top": 1 + }, + "strokeLinejoin": "round", + "gap": 14, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "WVZ4E", + "name": "K", + "fill": "$text-muted", + "content": "↑↓ 选择", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "XpQzS", + "name": "K", + "fill": "$text-muted", + "content": "⏎ 执行", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "QEkoP", + "name": "K", + "fill": "$text-muted", + "content": "Tab 补全", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "GaIH7", + "name": "K", + "fill": "$text-muted", + "content": "Esc 关闭", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "VUgO4", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "bcCHt", + "name": "N", + "fill": "$text-muted", + "content": "破坏性命令带省略号,选中后仍走确认闸门", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "iSl5i", + "name": "C 设置", + "clip": true, + "width": 420, + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "K5ESXN", + "name": "H", + "width": "fill_container", + "height": 32, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "McoqQ", + "name": "T", + "fill": "$text-primary", + "content": "设置", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "aX2Ad", + "name": "S", + "fill": "$text-muted", + "content": "抽屉形态 · 面板右侧滑出", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "QM1BC", + "name": "B", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": 14, + "children": [ + { + "type": "frame", + "id": "QzkB7", + "name": "Panel", + "clip": true, + "width": "fill_container", + "fill": "$bg-elevated", + "cornerRadius": "$r-md", + "stroke": "$border-strong", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "KYNpy", + "name": "H", + "width": "fill_container", + "height": 40, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "YQ45z", + "name": "I", + "width": 15, + "height": 15, + "icon": "settings", + "library": "lucide", + "fill": "$text-secondary" + }, + { + "type": "text", + "id": "rp7gi", + "name": "T", + "fill": "$text-primary", + "content": "设置", + "fontFamily": "$font-ui", + "fontSize": 13, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "c3EuLd", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "Q2Bzsd", + "type": "ref", + "ref": "yoK0T", + "name": "X", + "descendants": { + "XMGqY": { + "icon": "x" + } + } + } + ] + }, + { + "type": "frame", + "id": "t6nvu", + "name": "B", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": [ + 12, + 14 + ], + "children": [ + { + "type": "frame", + "id": "ujcgJ", + "name": "S 连接", + "width": "fill_container", + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Ez2yn", + "name": "T", + "fill": "$text-secondary", + "content": "连接", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "U0A9K", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "PnZhP", + "name": "G", + "fill": "$text-muted", + "content": "按主机记忆", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "YVhtm", + "name": "F DOCKER_HOST", + "width": "fill_container", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "text", + "id": "ehXHX", + "name": "L", + "fill": "$text-secondary", + "content": "DOCKER_HOST", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "sVfLR", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "rIqyp", + "name": "V", + "fill": "$text-muted", + "content": "留空即用远端默认", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "mIIXz", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + } + ] + }, + { + "type": "frame", + "id": "x6iyu", + "name": "F DOCKER_CONTEXT", + "width": "fill_container", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "text", + "id": "DE2lB", + "name": "L", + "fill": "$text-secondary", + "content": "DOCKER_CONTEXT", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "PSOhT", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "s3aiO", + "name": "V", + "fill": "$text-muted", + "content": "default", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "BqgyH", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "BeN51", + "name": "T", + "width": 12, + "height": 12, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "HUunq", + "name": "R 以 sudo -n 执行", + "width": "fill_container", + "gap": 9, + "children": [ + { + "type": "frame", + "id": "rg5sw", + "name": "Sw", + "width": 26, + "height": 15, + "fill": "$bg-active", + "cornerRadius": 8, + "stroke": "$border-strong", + "strokeWidth": 1, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "E65MZC", + "name": "K", + "fill": "$text-muted", + "width": 11, + "height": 11 + } + ] + }, + { + "type": "frame", + "id": "iS36R", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "HNzgM", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "以 sudo -n 执行", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "aFceJ", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "账号不在 docker 组时打开。绝不弹口令提示;遇到要口令的规则会如实报出来。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "Dou5N", + "name": "D", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "u1U2PN", + "name": "S 显示", + "width": "fill_container", + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mcrjK", + "name": "T", + "fill": "$text-secondary", + "content": "显示", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "Ja2VW", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "k59boC", + "name": "G", + "fill": "$text-muted", + "content": "全局", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "nCDHz", + "name": "Density", + "width": "fill_container", + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 2, + "padding": 2, + "children": [ + { + "id": "D8YeF", + "type": "ref", + "ref": "MdmRT", + "name": "D 紧凑 32px", + "width": "fill_container", + "height": 22, + "justifyContent": "center", + "fill": "$bg-active", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "紧凑 32px", + "fontSize": 11, + "fill": "$text-primary" + } + } + }, + { + "id": "C6esqE", + "type": "ref", + "ref": "MdmRT", + "name": "D 中等 40px", + "width": "fill_container", + "height": 22, + "justifyContent": "center", + "fill": "#00000000", + "descendants": { + "aVLQQ": { + "enabled": false + }, + "iscyV": { + "content": "中等 40px", + "fontSize": 11, + "fill": "$text-tertiary" + } + } + } + ] + }, + { + "type": "frame", + "id": "D53hJo", + "name": "R 显示已停止的容器", + "width": "fill_container", + "gap": 9, + "children": [ + { + "type": "frame", + "id": "cbA3P", + "name": "Sw", + "width": 26, + "height": 15, + "fill": "$accent", + "cornerRadius": 8, + "stroke": "#00000000", + "strokeWidth": 1, + "padding": [ + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "T1CRH", + "name": "K", + "fill": "$accent-on", + "width": 11, + "height": 11 + } + ] + }, + { + "type": "frame", + "id": "NnUYo", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "nEZAu", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "显示已停止的容器", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "NtGD1", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "关掉后列表只留运行中的。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "SvwV6", + "name": "R 行内 CPU / 内存 sparkline", + "width": "fill_container", + "gap": 9, + "children": [ + { + "type": "frame", + "id": "r0arf8", + "name": "Sw", + "width": 26, + "height": 15, + "fill": "$accent", + "cornerRadius": 8, + "stroke": "#00000000", + "strokeWidth": 1, + "padding": [ + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "ZTGm8", + "name": "K", + "fill": "$accent-on", + "width": 11, + "height": 11 + } + ] + }, + { + "type": "frame", + "id": "RtflE", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "VFPdI", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "行内 CPU / 内存 sparkline", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "dKZFV", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "关掉可省一点远端开销。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "BGQyi", + "name": "F 日志默认行数", + "width": "fill_container", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "text", + "id": "K16Ug", + "name": "L", + "fill": "$text-secondary", + "content": "日志默认行数", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "z4GxQA", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ONw1H", + "name": "V", + "fill": "$text-muted", + "content": "500 行", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "HKJ7L", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "GmaoF", + "name": "T", + "width": 12, + "height": 12, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "SbZPX", + "name": "D2", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "mAb7h", + "name": "S 行为", + "width": "fill_container", + "height": 22, + "gap": 7, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "uNJQ5", + "name": "T", + "fill": "$text-secondary", + "content": "行为", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "zXNen", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "j0p9Gm", + "name": "R 自动刷新(30s)", + "opacity": 0.45, + "width": "fill_container", + "gap": 9, + "children": [ + { + "type": "frame", + "id": "ibjNC", + "name": "Sw", + "width": 26, + "height": 15, + "fill": "$bg-active", + "cornerRadius": 8, + "stroke": "$border-strong", + "strokeWidth": 1, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "TVFkw", + "name": "K", + "fill": "$text-muted", + "width": 11, + "height": 11 + } + ] + }, + { + "type": "frame", + "id": "wJoKV", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "b8KDp", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "自动刷新(30s)", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "KdE5r", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "事件流已接通,自动刷新自动停用 —— 只在事件流断掉时兜底。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "AJA5L", + "name": "R 破坏性操作需确认", + "opacity": 0.45, + "width": "fill_container", + "gap": 9, + "children": [ + { + "type": "frame", + "id": "k4yMXp", + "name": "Sw", + "width": 26, + "height": 15, + "fill": "$accent", + "cornerRadius": 8, + "stroke": "#00000000", + "strokeWidth": 1, + "padding": [ + 0, + 2 + ], + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "qiCwx", + "name": "K", + "fill": "$accent-on", + "width": 11, + "height": 11 + } + ] + }, + { + "type": "frame", + "id": "FTnX5", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "ZAP8E", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "破坏性操作需确认", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Iaq88", + "name": "D", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "不可关闭。删除、强杀、prune 一律走面板内确认框。", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "ra8fP", + "x": 2619, + "y": 14467, + "name": "16L · 拉取镜像 · 亮色", + "theme": { + "mode": "light" + }, + "clip": true, + "width": 1440, + "height": 703, + "fill": "$bg-page", + "layout": "vertical", + "gap": 16, + "padding": 24, + "children": [ + { + "type": "frame", + "id": "j8tDat", + "name": "Title", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "xc7FY", + "name": "T", + "fill": "$text-primary", + "content": "二级弹窗 · 拉取镜像的三个状态", + "fontFamily": "$font-ui", + "fontSize": 18, + "fontWeight": "600" + }, + { + "type": "text", + "id": "p4VZC", + "name": "S", + "fill": "$text-tertiary", + "content": "同一个对话框原地换态,不弹第二层。进行中可以「转入后台」——任务继续跑,对话框关掉,进度移交给顶栏的任务中心。", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Hzy8t", + "name": "Row", + "width": "fill_container", + "gap": 36, + "children": [ + { + "id": "n7eFN", + "type": "ref", + "ref": "obPIB", + "name": "D 表单", + "width": "fill_container", + "descendants": { + "ysZdT": { + "icon": "arrow-down-to-line", + "fill": "$accent" + }, + "V5qqj": { + "content": "拉取镜像" + }, + "NUsd4": { + "enabled": false + }, + "slcE7": { + "type": "frame", + "id": "XXYhM", + "name": "Body", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": [ + 16, + 18 + ], + "children": [ + { + "type": "text", + "id": "K413q8", + "name": "L", + "fill": "$text-secondary", + "content": "镜像引用", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "ZNUxE", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 30, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$accent", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "UaCrh", + "name": "V", + "fill": "$text-primary", + "content": "ghcr.io/acme/api", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "rectangle", + "id": "De8ob", + "name": "Caret", + "fill": "$accent", + "width": 1.5, + "height": 14 + }, + { + "type": "frame", + "id": "mQ8nH", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "BEvOF", + "name": "Autocomplete", + "clip": true, + "width": "fill_container", + "fill": "$bg-elevated", + "cornerRadius": "$r-sm", + "stroke": "$border-strong", + "strokeWidth": 1, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000066", + "offset": { + "x": 0, + "y": 6 + }, + "blur": 18 + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "u27jza", + "name": "AC", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "$accent-dim", + "gap": 8, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "vx8qL", + "name": "I", + "width": 12, + "height": 12, + "icon": "layers", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "uU9zL", + "name": "T", + "fill": "$text-primary", + "content": "ghcr.io/acme/api", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "PgbYU", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "jZ87t", + "name": "M", + "fill": "$text-muted", + "content": "本地已有 2.8.1 · 2.8.0", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "hMqtj", + "name": "AC", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 8, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "plVSG", + "name": "I", + "width": 12, + "height": 12, + "icon": "layers", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "Edu8G", + "name": "T", + "fill": "$text-secondary", + "content": "ghcr.io/acme/api-worker", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "m1RgJq", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Lfh77", + "name": "M", + "fill": "$text-muted", + "content": "本地已有 2.8.1", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Lg31p", + "name": "AC", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 8, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "p34Zk", + "name": "I", + "width": 12, + "height": 12, + "icon": "cloud", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "IjDXp", + "name": "T", + "fill": "$text-secondary", + "content": "ghcr.io/acme/apigw", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "r36Au", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "he22g", + "name": "M", + "fill": "$text-muted", + "content": "远端 · 未拉取过", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "k3o3Ew", + "name": "AC", + "clip": true, + "width": "fill_container", + "height": 28, + "fill": "#00000000", + "gap": 8, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "JfAYI", + "name": "I", + "width": 12, + "height": 12, + "icon": "cloud", + "library": "lucide", + "fill": "$text-tertiary" + }, + { + "type": "text", + "id": "h9PicP", + "name": "T", + "fill": "$text-secondary", + "content": "docker.io/library/api", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "unb41", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "T1wwr", + "name": "M", + "fill": "$text-muted", + "content": "公共库 · 名称相近", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "eAwuJ", + "name": "Two", + "width": "fill_container", + "gap": 10, + "children": [ + { + "type": "frame", + "id": "x3m1a", + "name": "F 标签", + "width": "fill_container", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "text", + "id": "Z3RNMM", + "name": "L", + "fill": "$text-secondary", + "content": "标签", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "a80jg", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 30, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "lQd5p", + "name": "V", + "fill": "$text-primary", + "content": "2.8.2", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "y68VV", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "kUWUP", + "name": "T", + "width": 12, + "height": 12, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + } + ] + }, + { + "type": "frame", + "id": "RiE6Q", + "name": "F 平台", + "width": "fill_container", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "text", + "id": "Gn2WA", + "name": "L", + "fill": "$text-secondary", + "content": "平台", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "jN9z7", + "name": "In", + "clip": true, + "width": "fill_container", + "height": 30, + "fill": "$bg-input", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 7, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "DmrM9", + "name": "V", + "fill": "$text-primary", + "content": "linux/amd64", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Sv1LM", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "icon", + "id": "BZ5bw", + "name": "T", + "width": 12, + "height": 12, + "icon": "chevron-down", + "library": "lucide", + "fill": "$text-tertiary" + } + ] + } + ] + } + ] + }, + { + "type": "text", + "id": "G6txjX", + "name": "PlatHint", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "平台与远端主机架构一致(amd64),不会拉到跑不起来的镜像。", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "N3wdyd", + "name": "Toggle", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 9, + "padding": [ + 10, + 12 + ], + "children": [ + { + "type": "frame", + "id": "Q6WwN", + "name": "Sw", + "width": 26, + "height": 15, + "fill": "$bg-active", + "cornerRadius": 8, + "stroke": "$border-strong", + "strokeWidth": 1, + "padding": [ + 0, + 2 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "fB6LF", + "name": "K", + "fill": "$text-muted", + "width": 11, + "height": 11 + } + ] + }, + { + "type": "frame", + "id": "ndClb", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "ha0gQ", + "name": "T", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "拉取全部标签 --all-tags", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "yu1S3", + "name": "D", + "fill": "$warn", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "这个仓库有 47 个标签,合计约 8.2 GB。", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "pdDfy", + "name": "Registry", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "o4opWY", + "name": "I", + "width": 13, + "height": 13, + "icon": "shield-check", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "H6nWF", + "name": "T", + "fill": "$ok", + "content": "ghcr.io 已登录", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "bimmr", + "name": "D", + "fill": "$text-muted", + "content": "凭据来自远端 ~/.docker/config.json", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "w8f32", + "name": "Preview", + "width": "fill_container", + "fill": "$bg-terminal", + "cornerRadius": "$r-sm", + "layout": "vertical", + "gap": 3, + "padding": [ + 9, + 11 + ], + "children": [ + { + "type": "text", + "id": "wgTxl", + "name": "C", + "fill": "$text-terminal", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "POST /images/create?fromImage=ghcr.io/acme/api&tag=2.8.2", + "lineHeight": 1.5, + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "d90w6Z", + "name": "N", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "等价于 docker pull ghcr.io/acme/api:2.8.2 --platform linux/amd64", + "lineHeight": 1.5, + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + "WAXqz": { + "content": "Esc 取消 · Enter 开始" + }, + "Uk4Ly/RQ8pM": { + "enabled": false + }, + "Uk4Ly/P0aUw": { + "content": "取消", + "fill": "$text-secondary" + }, + "C08Vw": { + "fill": "$accent" + }, + "C08Vw/RQ8pM": { + "icon": "arrow-down-to-line", + "fill": "$accent-on" + }, + "C08Vw/P0aUw": { + "content": "开始拉取" + } + } + }, + { + "id": "KwWIB", + "type": "ref", + "ref": "obPIB", + "name": "D 进行中", + "width": "fill_container", + "descendants": { + "ysZdT": { + "icon": "arrow-down-to-line", + "fill": "$accent" + }, + "V5qqj": { + "content": "拉取中" + }, + "NUsd4": { + "enabled": true, + "fill": "$accent-dim" + }, + "NUsd4/kvk5k": { + "fill": "$accent" + }, + "NUsd4/ODVko": { + "content": "进行中", + "fill": "$accent" + }, + "slcE7": { + "type": "frame", + "id": "PctTK", + "name": "Body", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": [ + 16, + 18 + ], + "children": [ + { + "type": "text", + "id": "FYbH0", + "name": "Ref", + "fill": "$text-primary", + "content": "ghcr.io/acme/api:2.8.2", + "fontFamily": "$font-mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "fjXaI", + "name": "Overall", + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "gap": 8, + "padding": [ + 12, + 13 + ], + "children": [ + { + "type": "frame", + "id": "pHqZv", + "name": "T", + "width": "fill_container", + "gap": 8, + "alignItems": "end", + "children": [ + { + "type": "text", + "id": "qq74t", + "name": "P", + "fill": "$text-primary", + "content": "42%", + "fontFamily": "$font-ui", + "fontSize": 26, + "fontWeight": "600" + }, + { + "type": "text", + "id": "nY6NF", + "name": "B", + "fill": "$text-tertiary", + "content": "89.6 / 214 MB", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "c3gWiP", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "zOm96", + "name": "R", + "layout": "vertical", + "gap": 2, + "alignItems": "end", + "children": [ + { + "type": "text", + "id": "n67J1n", + "name": "Sp", + "fill": "$accent", + "content": "12.4 MB/s", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "wiqgn", + "name": "Et", + "fill": "$text-muted", + "content": "剩余 ~18s", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "GiCbP", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 8, + "fill": "$bg-active", + "cornerRadius": 4, + "children": [ + { + "type": "rectangle", + "cornerRadius": 4, + "id": "UKhWE", + "name": "Fill", + "fill": "$accent", + "width": "fill_container", + "height": 8 + }, + { + "type": "frame", + "id": "wkf5r", + "name": "Rest", + "width": "fill_container", + "height": 8, + "fill": "#00000000" + } + ] + } + ] + }, + { + "type": "rectangle", + "id": "kOlPy", + "name": "Div", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "frame", + "id": "Mqs6M", + "name": "LayerHead", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "iC3fD", + "name": "T", + "fill": "$text-secondary", + "content": "9 层", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "DNTGt", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "DzVaU", + "name": "M", + "fill": "$text-muted", + "content": "6 层复用 · 2 层进行中 · 1 层等待", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "m2tMs7", + "name": "Collapsed", + "width": "fill_container", + "height": 26, + "fill": "$bg-surface", + "cornerRadius": "$r-sm", + "gap": 8, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "RJV7O", + "name": "C", + "width": 12, + "height": 12, + "icon": "chevron-right", + "library": "lucide", + "fill": "$text-muted" + }, + { + "type": "icon", + "id": "lU2c0", + "name": "I", + "width": 12, + "height": 12, + "icon": "check", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "BsYXp", + "name": "T", + "fill": "$text-tertiary", + "content": "6 层已存在,已折叠", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "BLFsP", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "HUq1U", + "name": "M", + "fill": "$text-muted", + "content": "复用 124 MB", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "bRLGA", + "name": "L d4e8b2f105", + "clip": true, + "width": "fill_container", + "height": 24, + "gap": 9, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "VmkKA", + "name": "Id", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": 74, + "content": "d4e8b2f105", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "SpoJs", + "name": "St", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 70, + "content": "Downloading", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "vQznF", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 5, + "fill": "$bg-active", + "cornerRadius": 3, + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "mak0Y", + "name": "F", + "fill": "$accent", + "width": "fill_container", + "height": 5 + }, + { + "type": "frame", + "id": "KNTjP", + "name": "R", + "width": "fill_container", + "height": 5, + "fill": "#00000000" + } + ] + }, + { + "type": "text", + "id": "Dzqsq", + "name": "P", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 30, + "content": "62%", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "V5rmzn", + "name": "Sz", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 84, + "content": "38.2 / 61.4 MB", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "TZsbF", + "name": "L 6ef3a0b829", + "clip": true, + "width": "fill_container", + "height": 24, + "gap": 9, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Gy3fB", + "name": "Id", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": 74, + "content": "6ef3a0b829", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "uHLGd", + "name": "St", + "fill": "$violet", + "textGrowth": "fixed-width", + "width": 70, + "content": "Extracting", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "FHZO6", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 5, + "fill": "$bg-active", + "cornerRadius": 3, + "children": [ + { + "type": "rectangle", + "cornerRadius": 3, + "id": "WNq6o", + "name": "F", + "fill": "$violet", + "width": "fill_container", + "height": 5 + }, + { + "type": "frame", + "id": "Szc52", + "name": "R", + "width": "fill_container", + "height": 5, + "fill": "#00000000" + } + ] + }, + { + "type": "text", + "id": "OX58y", + "name": "P", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 30, + "content": "28%", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "UiXqK", + "name": "Sz", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 84, + "content": "14.6 MB", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ZtkbK", + "name": "L 0af31b8ce4", + "clip": true, + "width": "fill_container", + "height": 24, + "gap": 9, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "LqDGY", + "name": "Id", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": 74, + "content": "0af31b8ce4", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "qnxMA", + "name": "St", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 70, + "content": "Waiting", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "nUZJI", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 5, + "fill": "$bg-active", + "cornerRadius": 3, + "children": [ + { + "type": "frame", + "id": "Zado5", + "name": "R", + "width": "fill_container", + "height": 5, + "fill": "#00000000" + } + ] + }, + { + "type": "text", + "id": "V1sDN", + "name": "P", + "fill": "$text-secondary", + "textGrowth": "fixed-width", + "width": 30, + "content": "—", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "FOxZf", + "name": "Sz", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 84, + "content": "待定", + "textAlign": "right", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + }, + "WAXqz": { + "content": "关闭对话框不会取消 —— 任务转入顶栏任务中心" + }, + "Uk4Ly/RQ8pM": { + "enabled": true, + "icon": "minimize-2" + }, + "Uk4Ly/P0aUw": { + "content": "转入后台" + }, + "C08Vw": { + "fill": "$danger" + }, + "C08Vw/RQ8pM": { + "icon": "x", + "fill": "$accent-on" + }, + "C08Vw/P0aUw": { + "content": "取消拉取" + } + } + }, + { + "id": "foYRr", + "type": "ref", + "ref": "obPIB", + "name": "D 完成", + "width": "fill_container", + "descendants": { + "ysZdT": { + "icon": "circle-check-big", + "fill": "$ok" + }, + "V5qqj": { + "content": "拉取完成" + }, + "NUsd4": { + "enabled": true, + "fill": "$ok-dim" + }, + "NUsd4/kvk5k": { + "fill": "$ok" + }, + "NUsd4/ODVko": { + "content": "成功", + "fill": "$ok" + }, + "slcE7": { + "type": "frame", + "id": "ilpf8", + "name": "Body", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": [ + 16, + 18 + ], + "children": [ + { + "type": "frame", + "id": "mjjAL", + "name": "Success", + "width": "fill_container", + "fill": "$ok-dim", + "cornerRadius": "$r-md", + "gap": 10, + "padding": [ + 11, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "z6S2W", + "name": "I", + "width": 20, + "height": 20, + "icon": "circle-check-big", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "frame", + "id": "llypk", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "MlUAv", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "ghcr.io/acme/api:2.8.2", + "fontFamily": "$font-mono", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "AyLqe", + "name": "D", + "fill": "$ok", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "用时 24.3s · 新下载 89.6 MB · 复用 124 MB", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "id": "HegM0", + "type": "ref", + "ref": "l3qo4", + "name": "KV 摘要", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "摘要", + "width": 74, + "fontSize": 10 + }, + "psgr6": { + "content": "sha256:3f81a9…c04e", + "fontSize": 10 + } + } + }, + { + "id": "GfbQT", + "type": "ref", + "ref": "l3qo4", + "name": "KV 大小", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "大小", + "width": 74, + "fontSize": 10 + }, + "psgr6": { + "content": "214 MB(9 层)", + "fontSize": 10 + } + } + }, + { + "id": "fCsd5", + "type": "ref", + "ref": "l3qo4", + "name": "KV 平台", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "平台", + "width": 74, + "fontSize": 10 + }, + "psgr6": { + "content": "linux/amd64", + "fontSize": 10 + } + } + }, + { + "id": "fZlbM", + "type": "ref", + "ref": "l3qo4", + "name": "KV 创建于", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "创建于", + "width": 74, + "fontSize": 10 + }, + "psgr6": { + "content": "2026-08-20 22:41 (镜像本身)", + "fontSize": 10 + } + } + }, + { + "id": "zvHmt", + "type": "ref", + "ref": "l3qo4", + "name": "KV 本地标签", + "width": "fill_container", + "descendants": { + "N9bNk": { + "content": "本地标签", + "width": 74, + "fontSize": 10 + }, + "psgr6": { + "content": "2.8.2 · 2.8.1 · 2.8.0", + "fontSize": 10 + } + } + }, + { + "type": "frame", + "id": "Wm5fM", + "name": "Warn", + "width": "fill_container", + "height": 28, + "fill": "$warn-dim", + "cornerRadius": "$r-sm", + "gap": 8, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "y6ex8Q", + "name": "C", + "width": 12, + "height": 12, + "icon": "chevron-right", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "icon", + "id": "d3UDtg", + "name": "I", + "width": 12, + "height": 12, + "icon": "triangle-alert", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "text", + "id": "zN9iU", + "name": "T", + "fill": "$warn", + "content": "1 条 registry 警告", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "r6Qsr", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "Q32K3G", + "name": "M", + "fill": "$warn", + "content": "展开", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "rectangle", + "id": "tPr4I", + "name": "Div", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "text", + "id": "IeHZi", + "name": "NextL", + "fill": "$text-secondary", + "content": "下一步", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "ZBaTI", + "name": "Next", + "width": "fill_container", + "gap": 8, + "children": [ + { + "id": "xJNNX", + "type": "ref", + "ref": "i1OTz", + "name": "N 运行容器…", + "height": 28, + "fill": "$accent-dim", + "stroke": "#00000000", + "descendants": { + "RQ8pM": { + "icon": "play", + "fill": "$accent" + }, + "P0aUw": { + "content": "运行容器…", + "fontSize": 11, + "fill": "$accent" + } + } + }, + { + "id": "E0EYn", + "type": "ref", + "ref": "i1OTz", + "name": "N 打标签", + "height": 28, + "fill": "#00000000", + "stroke": "$border", + "descendants": { + "RQ8pM": { + "icon": "tag", + "fill": "$text-secondary" + }, + "P0aUw": { + "content": "打标签", + "fontSize": 11, + "fill": "$text-secondary" + } + } + }, + { + "id": "kyA11", + "type": "ref", + "ref": "i1OTz", + "name": "N 重建用它的容器", + "height": 28, + "fill": "#00000000", + "stroke": "$border", + "descendants": { + "RQ8pM": { + "icon": "rotate-cw", + "fill": "$text-secondary" + }, + "P0aUw": { + "content": "重建用它的容器", + "fontSize": 11, + "fill": "$text-secondary" + } + } + } + ] + } + ] + }, + "WAXqz": { + "content": "已写入执行记录" + }, + "Uk4Ly/RQ8pM": { + "enabled": true, + "icon": "square-terminal" + }, + "Uk4Ly/P0aUw": { + "content": "查看执行记录" + }, + "C08Vw": { + "fill": "$accent" + }, + "C08Vw/RQ8pM": { + "icon": "check", + "fill": "$accent-on" + }, + "C08Vw/P0aUw": { + "content": "完成" + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "uWSzv", + "x": 2619, + "y": 15387, + "name": "17L · 进度体系 · 亮色", + "theme": { + "mode": "light" + }, + "clip": true, + "width": 1440, + "height": 1149, + "fill": "$bg-page", + "layout": "vertical", + "gap": 16, + "padding": 24, + "children": [ + { + "type": "frame", + "id": "GpgdO", + "name": "Title", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "bjAn2", + "name": "T", + "fill": "$text-primary", + "content": "进度体系", + "fontFamily": "$font-ui", + "fontSize": 18, + "fontWeight": "600" + }, + { + "type": "text", + "id": "OKxfK", + "name": "S", + "fill": "$text-tertiary", + "content": "三个层级:对话框内的详细进度 → 顶栏任务中心的并行任务 → 状态栏与行内的就地反馈。任何一个动作都不允许「按下去没反应」。", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "WNwfM", + "name": "Row 1", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "HpzL7", + "name": "C 顶栏任务中心", + "clip": true, + "width": 430, + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "H9zBg", + "name": "H", + "width": "fill_container", + "height": 32, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "K8cAwS", + "name": "T", + "fill": "$text-primary", + "content": "顶栏任务中心", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "Mjphg", + "name": "S", + "fill": "$text-muted", + "content": "收起态在顶栏,点开是弹层", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "A3io1A", + "name": "B", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": 14, + "children": [ + { + "type": "frame", + "id": "Selfn", + "name": "TopBar Mock", + "width": "fill_container", + "height": 40, + "fill": "$bg-elevated", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "gap": 9, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "eidQS", + "name": "L", + "width": 15, + "height": 15, + "icon": "container", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "ZopuB", + "name": "T", + "fill": "$text-primary", + "content": "Docker", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "He0bv", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "frame", + "id": "QRKcS", + "name": "Task Indicator", + "height": 26, + "fill": "$accent-dim", + "cornerRadius": "$r-sm", + "gap": 7, + "padding": [ + 0, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "SoE21", + "name": "Ring", + "innerRadius": 0.6, + "startAngle": 90, + "sweepAngle": -152, + "fill": "$accent", + "width": 13, + "height": 13 + }, + { + "type": "text", + "id": "b0SWA", + "name": "T", + "fill": "$accent", + "content": "2 个任务", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "bn2jk", + "name": "P", + "fill": "$accent", + "content": "42%", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "id": "ba6Ll", + "type": "ref", + "ref": "x0Pcos", + "name": "Live", + "descendants": { + "ODVko": { + "content": "实时" + } + } + } + ] + }, + { + "type": "text", + "id": "DbiBs", + "name": "Note", + "fill": "$text-muted", + "content": "↓ 点开后的弹层", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "RnagE", + "name": "Popover", + "clip": true, + "width": "fill_container", + "fill": "$bg-elevated", + "cornerRadius": "$r-md", + "stroke": "$border-strong", + "strokeWidth": 1, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000073", + "offset": { + "x": 0, + "y": 8 + }, + "blur": 24 + }, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "fu6Wp", + "name": "H", + "width": "fill_container", + "height": 34, + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 7, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "uSzyo", + "name": "T", + "fill": "$text-primary", + "content": "任务", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "ptbLh", + "name": "C", + "fill": "$text-muted", + "content": "2 进行中 · 3 已完成", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "lxPNm", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "pxQzU", + "name": "A", + "fill": "$accent", + "content": "清空已完成", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "bzTJP", + "name": "T 拉取 ghcr.io/acme/api:2.8.2", + "width": "fill_container", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 6, + "padding": [ + 10, + 12 + ], + "children": [ + { + "type": "frame", + "id": "Xx1Wt", + "name": "H", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "HlhLw", + "name": "I", + "width": 13, + "height": 13, + "icon": "arrow-down-to-line", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "frame", + "id": "d0TCrR", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "text", + "id": "Q7mBkE", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "拉取 ghcr.io/acme/api:2.8.2", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "xmF2T", + "name": "S", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "9 层 · 89.6 / 214 MB · 12.4 MB/s", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "sTyLw", + "name": "R", + "fill": "$accent", + "content": "42%", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "SlBAY", + "type": "ref", + "ref": "yoK0T", + "name": "A", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "x", + "width": 12, + "height": 12 + } + } + } + ] + }, + { + "type": "frame", + "id": "R5ZAQ", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 5, + "fill": "$bg-active", + "cornerRadius": 4, + "children": [ + { + "type": "rectangle", + "cornerRadius": 4, + "id": "Z5NKw", + "name": "F", + "fill": "$accent", + "width": "fill_container", + "height": 5 + }, + { + "type": "frame", + "id": "Rksrj", + "name": "R", + "width": "fill_container", + "height": 5, + "fill": "#00000000" + } + ] + } + ] + }, + { + "type": "frame", + "id": "OzSat", + "name": "T compose up -d · web-stack", + "width": "fill_container", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 6, + "padding": [ + 10, + 12 + ], + "children": [ + { + "type": "frame", + "id": "d4DfF", + "name": "H", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "GhSSo", + "name": "I", + "width": 13, + "height": 13, + "icon": "boxes", + "library": "lucide", + "fill": "$violet" + }, + { + "type": "frame", + "id": "u2Oac", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "text", + "id": "cixoS", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "compose up -d · web-stack", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "mWQDS", + "name": "S", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "已运行 4.2s · 正在等待 nginx-proxy 健康", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "id": "tUP2Q", + "type": "ref", + "ref": "yoK0T", + "name": "A", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "x", + "width": 12, + "height": 12 + } + } + } + ] + }, + { + "type": "frame", + "id": "Prg8M", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 5, + "fill": "$bg-active", + "cornerRadius": 4, + "children": [ + { + "type": "frame", + "id": "kH1Xn", + "name": "A", + "width": "fill_container", + "height": 5, + "fill": "#00000000" + }, + { + "type": "rectangle", + "cornerRadius": 4, + "id": "Sk2P9", + "name": "F", + "fill": "$accent", + "width": "fill_container", + "height": 5 + }, + { + "type": "frame", + "id": "USpC6", + "name": "B", + "width": "fill_container", + "height": 5, + "fill": "#00000000" + } + ] + } + ] + }, + { + "type": "frame", + "id": "pP2Tl", + "name": "T 拉取 redis:7.4-alpine", + "width": "fill_container", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 6, + "padding": [ + 10, + 12 + ], + "children": [ + { + "type": "frame", + "id": "blMBc", + "name": "H", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "E4xAJ", + "name": "I", + "width": 13, + "height": 13, + "icon": "circle-check-big", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "frame", + "id": "UrXxQ", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "text", + "id": "I51G2j", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "拉取 redis:7.4-alpine", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "F27OV", + "name": "S", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "41.8 MB · 用时 6.1s", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "GPGAU", + "name": "R", + "fill": "$ok", + "content": "完成", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "s7i0Jw", + "name": "T 停止 10 个容器", + "width": "fill_container", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 6, + "padding": [ + 10, + 12 + ], + "children": [ + { + "type": "frame", + "id": "zhuzY", + "name": "H", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "Z5R0qz", + "name": "I", + "width": 13, + "height": 13, + "icon": "triangle-alert", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "frame", + "id": "q290i", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "text", + "id": "y0BC1", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "停止 10 个容器", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "GCiJP", + "name": "S", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "成功 8 · 失败 2", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "UZuFB", + "name": "R", + "fill": "$warn", + "content": "部分失败", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "pZDle", + "type": "ref", + "ref": "yoK0T", + "name": "A", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "chevron-right", + "width": 12, + "height": 12 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "L7zzdY", + "name": "T 导出卷 pg-data → tar", + "width": "fill_container", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "gap": 6, + "padding": [ + 10, + 12 + ], + "children": [ + { + "type": "frame", + "id": "UOZfd", + "name": "H", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "TlpcA", + "name": "I", + "width": 13, + "height": 13, + "icon": "circle-check-big", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "frame", + "id": "rG68R", + "name": "C", + "clip": true, + "width": "fill_container", + "layout": "vertical", + "gap": 1, + "children": [ + { + "type": "text", + "id": "kllMr", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "导出卷 pg-data → tar", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "D3zzXz", + "name": "S", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "18.4 GB · 用时 3m 12s", + "fontFamily": "$font-mono", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "jEl2J", + "name": "R", + "fill": "$ok", + "content": "完成", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "DZdLo", + "name": "F", + "width": "fill_container", + "height": 30, + "gap": 7, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "VIO1y", + "name": "T", + "fill": "$text-muted", + "content": "任务只属于当前主机 —— 换会话不会串台", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "Z3Q7o", + "name": "C 批量操作 · 选中条原地变进度条", + "clip": true, + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "HojvX", + "name": "H", + "width": "fill_container", + "height": 32, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "xvC2p", + "name": "T", + "fill": "$text-primary", + "content": "批量操作 · 选中条原地变进度条", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "yfogA", + "name": "S", + "fill": "$text-muted", + "content": "不弹窗、不遮挡列表", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "iEjVM", + "name": "B", + "width": "fill_container", + "layout": "vertical", + "gap": 14, + "padding": 14, + "children": [ + { + "type": "frame", + "id": "h6kCB", + "name": "Step", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "xYfXX", + "name": "N", + "fill": "$text-muted", + "content": "01", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "prysg", + "name": "T", + "fill": "$text-tertiary", + "content": "选中后,选中条出现", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "U8LuA", + "name": "Sel", + "width": "fill_container", + "height": 32, + "fill": "$accent-dim", + "cornerRadius": "$r-sm", + "gap": 7, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "vuI4U", + "name": "I", + "width": 13, + "height": 13, + "icon": "check", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "FKjZw", + "name": "T", + "fill": "$accent", + "content": "已选 10 个容器", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "rectangle", + "id": "t4UQED", + "name": "D", + "fill": "$border-strong", + "width": 1, + "height": 16 + }, + { + "id": "c1eHb", + "type": "ref", + "ref": "i1OTz", + "name": "B 启动", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "play" + }, + "P0aUw": { + "content": "启动", + "fontSize": 11 + } + } + }, + { + "id": "FDwYy", + "type": "ref", + "ref": "i1OTz", + "name": "B 停止", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "square" + }, + "P0aUw": { + "content": "停止", + "fontSize": 11 + } + } + }, + { + "id": "B5aUi", + "type": "ref", + "ref": "i1OTz", + "name": "B 重启", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "rotate-cw" + }, + "P0aUw": { + "content": "重启", + "fontSize": 11 + } + } + }, + { + "id": "k2wURI", + "type": "ref", + "ref": "i1OTz", + "name": "B 删除", + "height": 22, + "stroke": "$danger-dim", + "descendants": { + "RQ8pM": { + "icon": "trash-2", + "fill": "$danger" + }, + "P0aUw": { + "content": "删除", + "fontSize": 11, + "fill": "$danger" + } + } + }, + { + "type": "frame", + "id": "m7s97M", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "S8GDph", + "name": "C", + "fill": "$text-tertiary", + "content": "取消选择", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "b0Mz3m", + "name": "Step", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "jNSdA", + "name": "N", + "fill": "$text-muted", + "content": "02", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ZeL3e", + "name": "T", + "fill": "$text-tertiary", + "content": "按下「停止」——同一条变成进度条,列表不动、选中不丢", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "P6SJe", + "name": "Running", + "clip": true, + "width": "fill_container", + "fill": "$accent-dim", + "cornerRadius": "$r-sm", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "c6aYwG", + "name": "Bar", + "width": "fill_container", + "height": 32, + "gap": 8, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "D8ZSz", + "name": "Ring", + "innerRadius": 0.6, + "startAngle": 90, + "sweepAngle": -216, + "fill": "$accent", + "width": 13, + "height": 13 + }, + { + "type": "text", + "id": "A2LVVH", + "name": "T", + "fill": "$accent", + "content": "正在停止 10 个容器…", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "text", + "id": "vg5nU", + "name": "P", + "fill": "$accent", + "content": "6 / 10", + "fontFamily": "$font-mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "vxDAS", + "name": "Cur", + "fill": "$text-tertiary", + "content": "当前:worker-2 · 等待 SIGTERM(10s 超时)", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "LIEvz", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "xAcNq", + "type": "ref", + "ref": "i1OTz", + "name": "Cancel", + "height": 22, + "descendants": { + "RQ8pM": { + "icon": "x" + }, + "P0aUw": { + "content": "取消剩余", + "fontSize": 11 + } + } + } + ] + }, + { + "type": "frame", + "id": "o4jfz2", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 3, + "fill": "$bg-active", + "children": [ + { + "type": "rectangle", + "id": "m1PFf", + "name": "F", + "fill": "$accent", + "width": "fill_container", + "height": 3 + }, + { + "type": "frame", + "id": "h5Wos", + "name": "R", + "width": "fill_container", + "height": 3, + "fill": "#00000000" + } + ] + } + ] + }, + { + "type": "frame", + "id": "US1Mt", + "name": "Step", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "CtvwK", + "name": "N", + "fill": "$text-muted", + "content": "03", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "GC4ro", + "name": "T", + "fill": "$text-tertiary", + "content": "完成 —— 逐个目标判定,失败的单独列出来,而不是把整批说成「失败」", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "YnZa2", + "name": "Result", + "clip": true, + "width": "fill_container", + "fill": "$warn-dim", + "cornerRadius": "$r-sm", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "mw49v", + "name": "Bar", + "width": "fill_container", + "height": 32, + "gap": 8, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "BIR0J", + "name": "I", + "width": 13, + "height": 13, + "icon": "triangle-alert", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "text", + "id": "R6nDQu", + "name": "T", + "fill": "$warn", + "content": "已停止 8 个,2 个失败", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "iJcRk", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "id": "XZfVU", + "type": "ref", + "ref": "i1OTz", + "name": "Retry", + "height": 22, + "stroke": "$warn", + "descendants": { + "RQ8pM": { + "icon": "rotate-cw", + "fill": "$warn" + }, + "P0aUw": { + "content": "重试失败的 2 个", + "fontSize": 11, + "fill": "$warn" + } + } + }, + { + "id": "w1MPN3", + "type": "ref", + "ref": "yoK0T", + "name": "X", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "x", + "width": 12, + "height": 12 + } + } + } + ] + }, + { + "type": "frame", + "id": "J19jZs", + "name": "Detail", + "width": "fill_container", + "fill": "$bg-surface", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "lSO8m", + "name": "R worker-1", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "h3wuB", + "name": "I", + "width": 12, + "height": 12, + "icon": "circle-check-big", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "vCL7Y", + "name": "N", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": 120, + "content": "worker-1", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Xt28L", + "name": "M", + "fill": "$text-muted", + "content": "已停止 · 用时 0.8s", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "y3YxB5", + "name": "R worker-2", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "t3RRt", + "name": "I", + "width": 12, + "height": 12, + "icon": "circle-check-big", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "YNMZF", + "name": "N", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": 120, + "content": "worker-2", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "iafrq", + "name": "M", + "fill": "$text-muted", + "content": "已停止 · 用时 9.6s(等到超时后 SIGKILL)", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Y8dtN", + "name": "R postgres-main", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "HHJL6", + "name": "I", + "width": 12, + "height": 12, + "icon": "circle-x", + "library": "lucide", + "fill": "$danger" + }, + { + "type": "text", + "id": "fR4Rp", + "name": "N", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": 120, + "content": "postgres-main", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "HHnSM", + "name": "M", + "fill": "$danger", + "content": "conflict: 容器被 api-gateway 通过 depends_on 依赖", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "D1eLj", + "name": "R minio", + "clip": true, + "width": "fill_container", + "height": 26, + "gap": 8, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "C0CBMJ", + "name": "I", + "width": 12, + "height": 12, + "icon": "circle-x", + "library": "lucide", + "fill": "$danger" + }, + { + "type": "text", + "id": "QvKLN", + "name": "N", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": 120, + "content": "minio", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "nEFIv", + "name": "M", + "fill": "$danger", + "content": "container is paused —— 先恢复再停止", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "h7bV4s", + "name": "More", + "fill": "$text-muted", + "content": "+ 6 个成功已折叠", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "YB4A5", + "name": "Row 2", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "OcQQN", + "name": "C 行内进度", + "clip": true, + "width": "fill_container", + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "G8P49", + "name": "H", + "width": "fill_container", + "height": 32, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "E7SOa", + "name": "T", + "fill": "$text-primary", + "content": "行内进度", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "Sng1V", + "name": "S", + "fill": "$text-muted", + "content": "单个目标的动作,反馈就在那一行上", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "R4NRW", + "name": "B", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": 14, + "children": [ + { + "type": "frame", + "id": "l53a6", + "name": "List", + "clip": true, + "width": "fill_container", + "fill": "$bg-elevated", + "cornerRadius": "$r-sm", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "g37tBO", + "name": "W nginx-proxy", + "width": "fill_container", + "fill": "$accent-dim", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "Rq9RU", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 2, + "fill": "$bg-active", + "children": [ + { + "type": "frame", + "id": "d6RJMO", + "name": "A", + "width": "fill_container", + "height": 2, + "fill": "#00000000" + }, + { + "type": "rectangle", + "id": "lvxeV", + "name": "F", + "fill": "$accent", + "width": "fill_container", + "height": 2 + }, + { + "type": "frame", + "id": "oYelW", + "name": "B", + "width": "fill_container", + "height": 2, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "zMp7v", + "name": "R", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 9, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "jG1Px", + "name": "Ring", + "innerRadius": 0.6, + "startAngle": 90, + "sweepAngle": -200, + "fill": "$accent", + "width": 11, + "height": 11 + }, + { + "type": "text", + "id": "S8jxg", + "name": "N", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": 132, + "content": "nginx-proxy", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "zRPo2", + "name": "I", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 186, + "content": "nginx:1.27-alpine", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "I3ZL2E", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "BbiLL", + "name": "St", + "fill": "$accent", + "content": "重启中 · 等待健康检查 6s", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "u0MB7w", + "type": "ref", + "ref": "i1OTz", + "name": "Cancel", + "height": 20, + "descendants": { + "RQ8pM": { + "enabled": false + }, + "P0aUw": { + "content": "取消", + "fontSize": 10 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "EThfs", + "name": "W api-gateway", + "width": "fill_container", + "fill": "#00000000", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "FWkBM", + "name": "R", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 9, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "b3WsuH", + "name": "D", + "fill": "$ok", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "Vbrxl", + "name": "N", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": 132, + "content": "api-gateway", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "nPxGb", + "name": "I", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 186, + "content": "ghcr.io/acme/api:2.8.1", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "KRdIT", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "wjBjg", + "name": "St", + "fill": "$ok", + "content": "运行中 · 3d 4h", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "BWFti", + "type": "ref", + "ref": "yoK0T", + "name": "A", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "square", + "width": 12, + "height": 12 + } + } + }, + { + "id": "d1nNuw", + "type": "ref", + "ref": "yoK0T", + "name": "A", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "rotate-cw", + "width": 12, + "height": 12 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "GYXev", + "name": "W postgres-main", + "width": "fill_container", + "fill": "$accent-dim", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "hrq2K", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 2, + "fill": "$bg-active", + "children": [ + { + "type": "rectangle", + "id": "g5G54T", + "name": "F", + "fill": "$accent", + "width": "fill_container", + "height": 2 + }, + { + "type": "frame", + "id": "J0EA0z", + "name": "R", + "width": "fill_container", + "height": 2, + "fill": "#00000000" + } + ] + }, + { + "type": "frame", + "id": "kz58F", + "name": "R", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 9, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "cyoeG", + "name": "Ring", + "innerRadius": 0.6, + "startAngle": 90, + "sweepAngle": -200, + "fill": "$accent", + "width": 11, + "height": 11 + }, + { + "type": "text", + "id": "f3Ja1t", + "name": "N", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": 132, + "content": "postgres-main", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "btjL3", + "name": "I", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 186, + "content": "postgres:16.4", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "IAoGN", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "QTyYy", + "name": "St", + "fill": "$accent", + "content": "拉取新镜像 42% · 12.4 MB/s", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "WGKhn", + "type": "ref", + "ref": "i1OTz", + "name": "Cancel", + "height": 20, + "descendants": { + "RQ8pM": { + "enabled": false + }, + "P0aUw": { + "content": "取消", + "fontSize": 10 + } + } + } + ] + } + ] + }, + { + "type": "frame", + "id": "FngBX", + "name": "W redis-cache", + "width": "fill_container", + "fill": "#00000000", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "V30jX", + "name": "R", + "clip": true, + "width": "fill_container", + "height": 32, + "gap": 9, + "padding": [ + 0, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "Z9bP3J", + "name": "D", + "fill": "$ok", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "MStc5", + "name": "N", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": 132, + "content": "redis-cache", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "GmusP", + "name": "I", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": 186, + "content": "redis:7.4-alpine", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "EeNSH", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "V02vm", + "name": "St", + "fill": "$ok", + "content": "运行中 · 12d 6h", + "fontFamily": "$font-mono", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "id": "Fbnwu", + "type": "ref", + "ref": "yoK0T", + "name": "A", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "square", + "width": 12, + "height": 12 + } + } + }, + { + "id": "Y4GoRy", + "type": "ref", + "ref": "yoK0T", + "name": "A", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "rotate-cw", + "width": 12, + "height": 12 + } + } + } + ] + } + ] + } + ] + }, + { + "type": "text", + "id": "xQNyb", + "name": "N", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "进行中的行:2px 顶部进度条 + 圆环 + 状态列写清当前在等什么。动作按钮换成「取消」,其余按钮禁用而不是消失 —— 位置不跳。", + "lineHeight": 1.6, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "rectangle", + "id": "fptns", + "name": "Div", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "text", + "id": "ilp4l", + "name": "L", + "fill": "$text-secondary", + "content": "确定型 / 不确定型", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "LoyMD", + "name": "G", + "width": "fill_container", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "frame", + "id": "O4OcB", + "name": "H", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "h6sZi", + "name": "T", + "fill": "$text-primary", + "content": "确定型 —— 知道分母", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "DeiSi", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "X4NycP", + "name": "D", + "fill": "$text-muted", + "content": "镜像层字节数、批量操作的目标个数、文件传输大小", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "MXoX8", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 6, + "fill": "$bg-active", + "cornerRadius": 4, + "children": [ + { + "type": "rectangle", + "cornerRadius": 4, + "id": "LD9jS", + "name": "F", + "fill": "$accent", + "width": "fill_container", + "height": 6 + }, + { + "type": "frame", + "id": "O4NScT", + "name": "R", + "width": "fill_container", + "height": 6, + "fill": "#00000000" + } + ] + } + ] + }, + { + "type": "frame", + "id": "q5jaZf", + "name": "G", + "width": "fill_container", + "layout": "vertical", + "gap": 5, + "children": [ + { + "type": "frame", + "id": "BeMVg", + "name": "H", + "width": "fill_container", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "saPgk", + "name": "T", + "fill": "$text-primary", + "content": "不确定型 —— 不知道要多久", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "p7P8zI", + "name": "S", + "width": "fill_container", + "height": 1, + "fill": "#00000000" + }, + { + "type": "text", + "id": "WzwEk", + "name": "D", + "fill": "$text-muted", + "content": "compose up 等健康检查、docker stop 等 SIGTERM、镜像构建", + "fontFamily": "$font-ui", + "fontSize": 9, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "RNHar", + "name": "Track", + "clip": true, + "width": "fill_container", + "height": 6, + "fill": "$bg-active", + "cornerRadius": 4, + "children": [ + { + "type": "frame", + "id": "C14H3", + "name": "A", + "width": "fill_container", + "height": 6, + "fill": "#00000000" + }, + { + "type": "rectangle", + "cornerRadius": 4, + "id": "Y4uMDZ", + "name": "F", + "fill": "$accent", + "width": "fill_container", + "height": 6 + }, + { + "type": "frame", + "id": "Ngt9h", + "name": "B", + "width": "fill_container", + "height": 6, + "fill": "#00000000" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "Nryf5", + "name": "C 结果反馈", + "clip": true, + "width": 460, + "fill": "$bg-surface", + "cornerRadius": "$r-md", + "stroke": "$border", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "uyuE0", + "name": "H", + "width": "fill_container", + "height": 32, + "fill": "$bg-page", + "stroke": "$border", + "strokeWidth": { + "bottom": 1 + }, + "strokeLinejoin": "round", + "gap": 8, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "YgLxj", + "name": "T", + "fill": "$text-primary", + "content": "结果反馈", + "fontFamily": "$font-ui", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "Vzo44", + "name": "S", + "fill": "$text-muted", + "content": "状态栏常驻 · toast 只在你已经离开现场时用", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "DmcgF", + "name": "B", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": 14, + "children": [ + { + "type": "text", + "id": "NJtUS", + "name": "L1", + "fill": "$text-secondary", + "content": "状态栏 —— 永远说最后一件事的结果", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "VSJMO", + "name": "SB", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "$bg-elevated", + "cornerRadius": 3, + "stroke": "$border", + "strokeWidth": 1, + "gap": 8, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "E8Pk3", + "name": "I", + "width": 11, + "height": 11, + "icon": "circle-check-big", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "text", + "id": "j6hSA", + "name": "T", + "fill": "$text-tertiary", + "content": "已停止 8 个容器 · 用时 11.4s", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "c4Bv4n", + "name": "SB", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "$bg-elevated", + "cornerRadius": 3, + "stroke": "$border", + "strokeWidth": 1, + "gap": 8, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "M5QVT", + "name": "I", + "width": 11, + "height": 11, + "icon": "triangle-alert", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "text", + "id": "Kb7iU", + "name": "T", + "fill": "$warn", + "content": "已停止 8 个,2 个失败:postgres-main 被 api-gateway 依赖", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "a9YGL", + "name": "SB", + "clip": true, + "width": "fill_container", + "height": 24, + "fill": "$bg-elevated", + "cornerRadius": 3, + "stroke": "$border", + "strokeWidth": 1, + "gap": 8, + "padding": [ + 0, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "C2jq63", + "name": "I", + "width": 11, + "height": 11, + "icon": "arrow-down-to-line", + "library": "lucide", + "fill": "$accent" + }, + { + "type": "text", + "id": "tNk1t", + "name": "T", + "fill": "$accent", + "content": "拉取 ghcr.io/acme/api:2.8.2 · 42% · 12.4 MB/s", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "rectangle", + "id": "NnzYf", + "name": "Div", + "fill": "$border", + "width": "fill_container", + "height": 1 + }, + { + "type": "text", + "id": "exSgA", + "name": "L2", + "fill": "$text-secondary", + "content": "Toast —— 贴在面板右下角,不飘在屏幕中央", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "frame", + "id": "NQXof", + "name": "Toast 拉取完成 · ghcr.io/acme/api:2.8.2", + "width": "fill_container", + "fill": "$bg-elevated", + "cornerRadius": "$r-md", + "stroke": "$ok", + "strokeWidth": 1, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000066", + "offset": { + "x": 0, + "y": 6 + }, + "blur": 20 + }, + "gap": 9, + "padding": [ + 11, + 12 + ], + "children": [ + { + "type": "icon", + "id": "wCwGw", + "name": "I", + "width": 15, + "height": 15, + "icon": "circle-check-big", + "library": "lucide", + "fill": "$ok" + }, + { + "type": "frame", + "id": "JcRdy", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "kpvXh", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "拉取完成 · ghcr.io/acme/api:2.8.2", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "EThoS", + "name": "D", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "214 MB · 用时 24.3s · 复用 6 层", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "j2kPMM", + "name": "A", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "T4EY7o", + "name": "A", + "fill": "$accent", + "content": "运行容器…", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + }, + { + "type": "text", + "id": "bDR0p", + "name": "A", + "fill": "$accent", + "content": "查看镜像", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + } + ] + }, + { + "id": "T84tY8", + "type": "ref", + "ref": "yoK0T", + "name": "X", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "x", + "width": 12, + "height": 12 + } + } + } + ] + }, + { + "type": "frame", + "id": "TqbTM", + "name": "Toast 停止 10 个容器 · 2 个失败", + "width": "fill_container", + "fill": "$bg-elevated", + "cornerRadius": "$r-md", + "stroke": "$warn", + "strokeWidth": 1, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000066", + "offset": { + "x": 0, + "y": 6 + }, + "blur": 20 + }, + "gap": 9, + "padding": [ + 11, + 12 + ], + "children": [ + { + "type": "icon", + "id": "xr7si", + "name": "I", + "width": 15, + "height": 15, + "icon": "triangle-alert", + "library": "lucide", + "fill": "$warn" + }, + { + "type": "frame", + "id": "Q3YmmQ", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "iodUE", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "停止 10 个容器 · 2 个失败", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "YQ6Sn", + "name": "D", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "postgres-main:被 api-gateway 依赖\nminio:容器处于暂停态", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ZdmQJ", + "name": "A", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "PMW4W", + "name": "A", + "fill": "$warn", + "content": "查看详情", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + }, + { + "type": "text", + "id": "d40P3B", + "name": "A", + "fill": "$warn", + "content": "重试失败的 2 个", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + } + ] + }, + { + "id": "LefmW", + "type": "ref", + "ref": "yoK0T", + "name": "X", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "x", + "width": 12, + "height": 12 + } + } + } + ] + }, + { + "type": "frame", + "id": "W2lmd", + "name": "Toast compose up 失败 · web-stack", + "width": "fill_container", + "fill": "$bg-elevated", + "cornerRadius": "$r-md", + "stroke": "$danger", + "strokeWidth": 1, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#00000066", + "offset": { + "x": 0, + "y": 6 + }, + "blur": 20 + }, + "gap": 9, + "padding": [ + 11, + 12 + ], + "children": [ + { + "type": "icon", + "id": "goT0M", + "name": "I", + "width": 15, + "height": 15, + "icon": "circle-x", + "library": "lucide", + "fill": "$danger" + }, + { + "type": "frame", + "id": "jybsz", + "name": "C", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "x06Sl7", + "name": "T", + "fill": "$text-primary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "compose up 失败 · web-stack", + "fontFamily": "$font-ui", + "fontSize": 11, + "fontWeight": "600" + }, + { + "type": "text", + "id": "PPmMT", + "name": "D", + "fill": "$text-tertiary", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "service proxy: port 8080 已被 caddy 占用", + "lineHeight": 1.5, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "VTi60", + "name": "A", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "NVRba", + "name": "A", + "fill": "$danger", + "content": "查看执行记录", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + }, + { + "type": "text", + "id": "m80ED", + "name": "A", + "fill": "$danger", + "content": "编辑 compose.yaml", + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "500" + } + ] + } + ] + }, + { + "id": "Mnhr8", + "type": "ref", + "ref": "yoK0T", + "name": "X", + "width": 20, + "height": 20, + "descendants": { + "XMGqY": { + "icon": "x", + "width": 12, + "height": 12 + } + } + } + ] + }, + { + "type": "text", + "id": "jEPuG", + "name": "N", + "fill": "$text-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "规则:动作发起点还在视野里 → 只更新状态栏和那一行;你已经切走了(换了页签、关了对话框)→ 才弹 toast。成功 toast 4s 自动消失,失败的不自动消失。", + "lineHeight": 1.6, + "fontFamily": "$font-ui", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ], + "themes": { + "mode": [ + "dark", + "light" + ] + }, + "variables": { + "bg-page": { + "type": "color", + "value": [ + { + "value": "#0F1113", + "theme": { + "mode": "dark" + } + }, + { + "value": "#F2F4F7", + "theme": { + "mode": "light" + } + } + ] + }, + "bg-surface": { + "type": "color", + "value": [ + { + "value": "#16181C", + "theme": { + "mode": "dark" + } + }, + { + "value": "#FFFFFF", + "theme": { + "mode": "light" + } + } + ] + }, + "bg-elevated": { + "type": "color", + "value": [ + { + "value": "#1B1E23", + "theme": { + "mode": "dark" + } + }, + { + "value": "#FFFFFF", + "theme": { + "mode": "light" + } + } + ] + }, + "bg-rail": { + "type": "color", + "value": [ + { + "value": "#121417", + "theme": { + "mode": "dark" + } + }, + { + "value": "#EAEDF1", + "theme": { + "mode": "light" + } + } + ] + }, + "bg-input": { + "type": "color", + "value": [ + { + "value": "#0F1114", + "theme": { + "mode": "dark" + } + }, + { + "value": "#FFFFFF", + "theme": { + "mode": "light" + } + } + ] + }, + "bg-hover": { + "type": "color", + "value": [ + { + "value": "#1F232A", + "theme": { + "mode": "dark" + } + }, + { + "value": "#EEF1F5", + "theme": { + "mode": "light" + } + } + ] + }, + "bg-active": { + "type": "color", + "value": [ + { + "value": "#252A32", + "theme": { + "mode": "dark" + } + }, + { + "value": "#E1E8F2", + "theme": { + "mode": "light" + } + } + ] + }, + "bg-terminal": { + "type": "color", + "value": [ + { + "value": "#0B0D0F", + "theme": { + "mode": "dark" + } + }, + { + "value": "#1A1D21", + "theme": { + "mode": "light" + } + } + ] + }, + "border": { + "type": "color", + "value": [ + { + "value": "#262A31", + "theme": { + "mode": "dark" + } + }, + { + "value": "#E3E6EB", + "theme": { + "mode": "light" + } + } + ] + }, + "border-strong": { + "type": "color", + "value": [ + { + "value": "#343A44", + "theme": { + "mode": "dark" + } + }, + { + "value": "#C9CFD8", + "theme": { + "mode": "light" + } + } + ] + }, + "text-primary": { + "type": "color", + "value": [ + { + "value": "#E7EAEF", + "theme": { + "mode": "dark" + } + }, + { + "value": "#14171C", + "theme": { + "mode": "light" + } + } + ] + }, + "text-secondary": { + "type": "color", + "value": [ + { + "value": "#A2AAB6", + "theme": { + "mode": "dark" + } + }, + { + "value": "#4B545F", + "theme": { + "mode": "light" + } + } + ] + }, + "text-tertiary": { + "type": "color", + "value": [ + { + "value": "#6E7681", + "theme": { + "mode": "dark" + } + }, + { + "value": "#79838F", + "theme": { + "mode": "light" + } + } + ] + }, + "text-muted": { + "type": "color", + "value": [ + { + "value": "#545C67", + "theme": { + "mode": "dark" + } + }, + { + "value": "#9AA3AE", + "theme": { + "mode": "light" + } + } + ] + }, + "text-terminal": { + "type": "color", + "value": [ + { + "value": "#C9D1D9", + "theme": { + "mode": "dark" + } + }, + { + "value": "#D5DBE2", + "theme": { + "mode": "light" + } + } + ] + }, + "accent": { + "type": "color", + "value": [ + { + "value": "#2496ED", + "theme": { + "mode": "dark" + } + }, + { + "value": "#0B72C4", + "theme": { + "mode": "light" + } + } + ] + }, + "accent-hover": { + "type": "color", + "value": [ + { + "value": "#3AA5F5", + "theme": { + "mode": "dark" + } + }, + { + "value": "#0A65AF", + "theme": { + "mode": "light" + } + } + ] + }, + "accent-dim": { + "type": "color", + "value": [ + { + "value": "#2496ED2E", + "theme": { + "mode": "dark" + } + }, + { + "value": "#0B72C41F", + "theme": { + "mode": "light" + } + } + ] + }, + "accent-on": { + "type": "color", + "value": [ + { + "value": "#FFFFFF", + "theme": { + "mode": "dark" + } + }, + { + "value": "#FFFFFF", + "theme": { + "mode": "light" + } + } + ] + }, + "ok": { + "type": "color", + "value": [ + { + "value": "#3FB950", + "theme": { + "mode": "dark" + } + }, + { + "value": "#1A7F37", + "theme": { + "mode": "light" + } + } + ] + }, + "ok-dim": { + "type": "color", + "value": [ + { + "value": "#3FB9502E", + "theme": { + "mode": "dark" + } + }, + { + "value": "#1A7F3714", + "theme": { + "mode": "light" + } + } + ] + }, + "warn": { + "type": "color", + "value": [ + { + "value": "#D29922", + "theme": { + "mode": "dark" + } + }, + { + "value": "#9A6700", + "theme": { + "mode": "light" + } + } + ] + }, + "warn-dim": { + "type": "color", + "value": [ + { + "value": "#D299222E", + "theme": { + "mode": "dark" + } + }, + { + "value": "#9A670014", + "theme": { + "mode": "light" + } + } + ] + }, + "danger": { + "type": "color", + "value": [ + { + "value": "#F0524B", + "theme": { + "mode": "dark" + } + }, + { + "value": "#CF222E", + "theme": { + "mode": "light" + } + } + ] + }, + "danger-hover": { + "type": "color", + "value": [ + { + "value": "#FF6259", + "theme": { + "mode": "dark" + } + }, + { + "value": "#B71C26", + "theme": { + "mode": "light" + } + } + ] + }, + "danger-dim": { + "type": "color", + "value": [ + { + "value": "#F0524B2E", + "theme": { + "mode": "dark" + } + }, + { + "value": "#CF222E14", + "theme": { + "mode": "light" + } + } + ] + }, + "info": { + "type": "color", + "value": [ + { + "value": "#58A6FF", + "theme": { + "mode": "dark" + } + }, + { + "value": "#0969DA", + "theme": { + "mode": "light" + } + } + ] + }, + "violet": { + "type": "color", + "value": [ + { + "value": "#A371F7", + "theme": { + "mode": "dark" + } + }, + { + "value": "#8250DF", + "theme": { + "mode": "light" + } + } + ] + }, + "violet-dim": { + "type": "color", + "value": [ + { + "value": "#A371F72E", + "theme": { + "mode": "dark" + } + }, + { + "value": "#8250DF14", + "theme": { + "mode": "light" + } + } + ] + }, + "shadow": { + "type": "color", + "value": [ + { + "value": "#00000073", + "theme": { + "mode": "dark" + } + }, + { + "value": "#0F172A1F", + "theme": { + "mode": "light" + } + } + ] + }, + "font-ui": { + "type": "string", + "value": "Inter" + }, + "font-mono": { + "type": "string", + "value": "JetBrains Mono" + }, + "r-sm": { + "type": "number", + "value": 4 + }, + "r-md": { + "type": "number", + "value": 6 + }, + "r-lg": { + "type": "number", + "value": 10 + }, + "row-h": { + "type": "number", + "value": 30 + } + }, + "fileToken": "278a4df1-a537-474c-80af-c69f0df738c5" +} \ No newline at end of file diff --git a/plugins/VelaShell.Plugin.DockerPanel/plan.md b/plugins/VelaShell.Plugin.DockerPanel/plan.md new file mode 100644 index 0000000..4c2ba8b --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/plan.md @@ -0,0 +1,360 @@ +# DockerPanel v0.2 —— 设计稿逐板对照 + +> 2026-08-21。构建 0 错 0 警告,**124/124** 单测通过,`velashell.dockerpanel-0.2.0.vpx` 451 KB。 +> 宿主 `VelaShell.slnx` 同样 0 错 0 警告,275 个插件能力测试通过。 +> +> 对照基准:`VelaShell.Plugin.DockerPanel.pen`,19 个画板 + 5 个亮色变体 + 13 个 reusable 组件。 +> 全部画板已逐节点读过,下面的每一条都对得上具体控件。 +> +> 图例:**✅ 已完成** · **⚠️ 部分完成** · **❌ 未做** + +--- + +## 总览:完成度 + +| 画板 | 完成度 | 一句话 | +| --- | --- | --- | +| 01/02 容器(暗/亮) | ✅ 100% | 列表、详情、全局搜索、右键菜单、pin/最大化、More 菜单全齐 | +| 03 日志(多容器合并流) | ⚠️ 95% | 合并流、来源面板、级别/来源着色、下载、chip 可点掉;缺 JSON 正文高亮 | +| 04 容器文件浏览与编辑 | ⚠️ 90% | diff 视图、属性栏、写入历史、拖放、只看变更齐;缺树形展开与语法高亮 | +| 05 容器终端 | ⚠️ 80% | **真 TTY 已接通**(借宿主仿真器,SDK 1.3);还差多会话标签 | +| 06 镜像 + 运行容器表单 | ✅ 100% | 含「从 .env 导入」 | +| 07 卷 | ⚠️ 90% | 标签、驱动选项、备份、删前先备份都齐;抽屉未页签化 | +| 08 网络 | ⚠️ 90% | 原始 JSON、导出配置、全部摘除、IPAM 已分配都齐;缺拓扑图 | +| 09 Compose | ⚠️ 90% | 五个页签、逐服务动作、新建项目、退出码 chip 全齐;缺 YAML 语法高亮 | +| 10 系统 · 磁盘与回收 | ✅ 100% | 含宿主磁盘总量(借容器跑 df) | +| 11 总览 | ✅ 100% | 六个快捷动作、CPU 峰值、持续高 CPU 告警、趋势时间轴全齐 | +| 12/13 确认闸门 | ✅ 100% | 含「先备份为 tar 再删除」—— 备份失败就不删 | +| 14 状态与边界(6 态) | ✅ 100% | 六态齐,含空列表动作按钮与两条排查命令 | +| 15 设计系统 | ✅ — | **稿子已更正**:映射表改成 Dracula 紫,Docker 蓝只留品牌标 | +| 16 拉取镜像三态 | ✅ 95% | 「查看执行记录」由任务中心承担(见下) | +| 17 进度体系 | ✅ 100% | 批量原地进度、当前在等什么、失败重试、逐条明细全齐 | +| 18 表单弹窗合集 | ✅ 100% | 六个表单全有,另外多做了一个 | +| 19 菜单与弹层 | ⚠️ 95% | 右键菜单、命令面板、全局搜索、日志行数设置齐;缺 compose 项目筛选弹层 | + +--- + +## 1. 逐板明细 + +### 01 / 02 · 容器(暗色 / 亮色) + +✅ 顶栏品牌 + 主机切换器(名称 + socket + 传输方式)· 实时徽章(**事件流断开会转灰**,与设计一致)· 设置入口 +✅ 左侧 56px 图标栏七项 + 底部设置 +✅ 筛选档(全部/运行中/已停止/异常)、页内搜索、拉取镜像、运行容器…、刷新 +✅ 选中条批量:启动/停止/重启/暂停/删除 + 取消选择(实现另有恢复、强杀) +✅ 列:名称(带 compose chip)、镜像、端口、CPU(14 点 sparkline)、内存、运行时长 +✅ 抽屉五页签、快捷动作条、概览全部字段(镜像/摘要/创建/启动/重启策略/平台/命令、端口映射、挂载 + 只读读写标签、网络 + IP、健康检查) +✅ 状态栏:消息 + API 版本 + Engine 版本 + 计数 + 事件流状态 + +✅ **顶栏全局搜索框**「搜索容器、镜像、卷、网络… `Ctrl K`」 +✅ **行右键菜单**(见板 19) +✅ 工具栏的「**新建 Compose**」按钮 +✅ 抽屉头的 **pin(钉住)** 与 **最大化** —— 钉住之后刷新、切页、点别的行都不关它, + 只有"这个容器被删了"和"用户明确点了另一行"两种情况才让位 +✅ 快捷动作条末尾的 **More(⋯)溢出菜单**:复制 ID / 复制 docker run / 提交为镜像 / 改重启策略 / 强杀 + +⚠️ 快捷动作条的「进入终端」在设计里是 accent 色的一等动作;实现把它放在终端页签内部 +⚠️ 端口映射的「编辑」在实现里没有 —— 改端口 Docker 必须**重建容器**, + 一个叫「编辑」的按钮做的却是删了重建,那是误导;要改端口请用「提交为镜像」+「运行容器」 + +### 03 · 日志(多容器合并流) + +✅ 行数档位 100/500/2000/10k、时间戳、换行、仅错误 +✅ 搜索 + 命中计数 + 正则开关、跟随、复制、清屏 +✅ 时间戳独立成列(不混进正文被搜索命中) +✅ **左侧「日志来源」面板** —— 容器列表(含已停止的:"它为什么退出"正是最常看的一份日志)、 + 全选 / 全不选、过滤框、每项状态与颜色条、底部「合并 N 条流」 +✅ **多容器合并流** —— 每个来源一条独立的 `docker logs`,全部汇进同一个待刷队列。 + 合并顺序是**到达顺序**,不按时间戳重排:重排要缓冲、要等,而「跟随」的全部意义就是不等 +✅ **顶部已选来源 chips** +✅ **级别着色** INFO / WARN / ERROR / DEBUG —— 认 `[warn]`、`"level":"error"`、行首裸词三种形态 +✅ **来源着色** —— 六个宿主语义色轮换,按**选中顺序**编号(否则选两个相邻容器会拿到两个几乎一样的颜色) +✅ 工具条的**下载日志**按钮(导的是界面上这些行,含筛选与来源标记,不是重新去 daemon 拉一遍) +✅ 底部:跟随状态、`docker logs -f · N 条流 · 已运行 X`、缓冲字节、行数 + +✅ 顶部来源 chip **可以直接点掉**(走的是勾选那条路,左边面板同步变) + +❌ **JSON 正文语法高亮**(级别已经着色,正文仍是单色) +⚠️ 搜索命中高亮:`LogLineItem.Matched` 字段在,界面是否真的高亮需要再确认 + +> 入口:容器页勾选若干容器 → 选中条的「合并日志」。与设计一致 —— +> 板 03 的 rail 高亮的是**容器**,它是容器页的一种模式,不是独立的导航项。 + +### 04 · 容器文件浏览与编辑 + +✅ 目录浏览、`docker diff` 变更标记(A/C/D)、变更计数 +✅ 文本文件在线编辑、撤销、保存(手打 `save` 确认) +✅ 上传本地文件、下载文件/目录 + +✅ **编辑 / 差异 两个模式切换** + 编辑器里的 **diff 标记(`+` / `−` / `~`)** 与**行号**。 + 差异用 LCS 算,不是逐行对齐 —— 在文件顶部插一行,逐行对齐会把之后的**每一行**都标成改过, + 那样的差异视图等于没有。相邻的一删一增合并成"改过",改三处显示三行而不是六行 +✅ **「只看变更」开关**(目录永远留着,否则会变成走不进子目录的死胡同) +✅ 编辑器底部状态条:行列位置、已修改 N 行、UTF-8、LF/CRLF、语言 +✅ **右侧文件属性栏**:大小/权限/属主/修改时间/相对镜像状态 +✅ 属性栏里的**写回方式说明**、**「保存后执行 nginx -s reload」勾选**(按路径猜命令, + 猜不出来就不显示那个框 —— 给一个会失败的默认命令比不给更糟)、**本面板写入历史**(`+2 −1` 这种摘要) +✅ **拖放上传**(拖进来 = 上传到当前目录;一次只接一个文件) + +❌ **树形展开**(设计是一棵展开到 `/etc/nginx/conf.d` 的树;实现是一级一级进的平铺目录) +❌ **语法高亮**(行号与 diff 标记有了,正文仍是单色) + +> 这两项都要一个带语法着色与虚拟化的编辑器控件。宿主没有,自己写不是这一轮该做的事 —— +> 它们不影响"能不能改文件",只影响"改起来舒不舒服"。 + +### 05 · 容器终端 + +✅ **真 TTY 交互式会话** —— `exec` + TTY,渲染借宿主的终端仿真器(SDK 1.3 新增的 `ITerminalViewApi`)。 + VT 解析、屏幕缓冲、回滚、选区、IME、鼠标上报、键盘编码都是宿主那一套,与 SSH 标签同一个控件、 + 同一份字体与配色。`top` / `vim` / `less` 都能跑 +✅ shell 自动探测(bash → sh → ash;alpine 上不会撞 "no such file or directory")、用户 / 工作目录可切、重新连接 +✅ 工具条:复制屏幕文本、清屏、重连、**power(结束会话)**、在宿主终端里打开 +✅ 尺寸变化实时报给远端(`/exec/{id}/resize`)—— 不报的话 `vim` 会照 80×24 画,画出来是错位的 +✅ 底部:TTY 尺寸、UTF-8、exec id、「命令与输出不写入宿主会话日志」 + +❌ **多会话标签页**(`bash · nginx-proxy`、`sh · redis-cache`、+ 新建)—— 目前一个容器一个会话 +❌ 底部的 shell 版本(`bash 5.2.15`) + +### 06 · 镜像 + 运行容器表单 + +✅ 列、悬空镜像图标、筛选档、拉取/运行/打标签/推送/删除/清理 +✅ 运行容器表单:容器名、端口映射、卷挂载、环境变量(三组键值列表带增删)、网络、重启策略、运行选项(-d / --rm / -t / --privileged)、额外参数、**等效命令实时预览** + 「面板实际调用 POST /containers/create 与 /start」说明 + +✅ 环境变量的「**从 .env 导入**」—— 只认最朴素的 `KEY=VALUE`(含 `export` 前缀与引号剥离), + 跳过空行与注释,**把认不出来的行数报出来**。不做变量插值与多行值: + dotenv 的方言各家不同,猜错一次就等于悄悄改了用户的配置 + +### 07 · 卷 + +✅ 列表(名称/驱动/使用者/大小/创建)、新建、删除、清理未使用 +✅ 浏览卷内文件(借运行中容器,直接落到挂载点)、**备份为 tar** +✅ 概览:驱动、作用域、挂载点、创建于、使用者、危险操作说明 + +✅ 概览补上了 **标签(Labels)** 与 **驱动选项** —— compose 起的卷靠标签才看得出归谁管 +✅ 删卷闸门上的「**先备份为 tar 再删除**」勾选,默认打开;**备份失败就不删** + (勾这个框的意思正是"没有备份就别删") + +❌ **抽屉页签化**:设计是「概览 / 卷内文件 / 备份」三页签,实现是单页 + 跳转到容器页 +❌ 大小缺「· 4,182 个文件」这半句(要在卷里跑一次 `find | wc -l`,代价与收益不成比例) + +### 08 · 网络 + +✅ 列表(名称/驱动/作用域/子网/已接入)、内置网络保护 +✅ 新建、删除、清理、接入容器、摘除容器 +✅ 概览:网络 ID、驱动、作用域、创建于、**internal / attachable / IPv6**、IPAM(子网/网关/IP 范围) + +✅ **原始 JSON**(`InspectNetworkRawAsync` 不再是死方法) +✅ **导出配置** —— 导的是 inspect 原文,文件头写明它含运行态、不是可直接重建的清单 +✅ 已接入容器区的「**全部摘除**」,逐个判定,一个摘不掉不影响其余 +✅ IPAM 的「已分配 N / 65533」—— 按 CIDR 算,减掉网络地址、广播地址与网关; + IPv6 不给(一个 /64 是 1.8×10^19,显示出来没有信息) + +❌ **拓扑页签**(设计有 share-2 图标的拓扑视图)—— 要一个图布局引擎,单独一件事 + +### 09 · Compose + +✅ 项目列表(名称/状态/路径)、按路径打开、服务列表 +✅ up -d / stop / restart / pull / build / down(实现另有 down -v) +✅ YAML 在线编辑与保存(手打确认)、执行输出区、清空输出 + +✅ **五个页签**:compose.yaml / .env / 服务(N) / config 展开 / 合并日志 +✅ **`.env` 编辑与保存**(手打 `save`;提示"已在跑的容器要 up -d 重建才读得到新值") +✅ **`config 展开`** 有了自己的页签 —— 几百行 YAML 不再冲掉执行记录 +✅ **合并日志**交给 `compose logs -f` 去并,而不是面板自己并 N 条 `docker logs` —— + compose 认得项目里有哪些服务,包括面板列表还没刷到的那些 +✅ 服务表的**逐服务行内动作**:日志 / 终端 / 重启 +✅ **新建 Compose 项目** —— 写一个带注释的骨架,**不**顺手 up -d(骨架里的服务是占位的) +✅ 执行记录带**退出码 chip**、`$ 命令` 行、`✔ / ✘` 结果行、复制、stdout/stderr 分色 + +❌ YAML 编辑器的行号 + diff 标记 + 语法高亮(同板 04,要编辑器控件) + +### 10 · 系统 · 磁盘与回收 + +✅ 五张统计卡(容器/镜像/卷/网络/构建缓存) +✅ 磁盘占用表(类型/总数/活跃/占用/可回收) +✅ 六张回收卡,全部带 安全 / 注意 / 危险 / 会丢数据 标签 +✅ 引擎信息八项(Engine/API/OS/架构 CPU/存储驱动/Cgroup/日志驱动/Rootless Swarm) + +✅ 容器可写层(**已修**:`ContainerSummary` 补了 `SizeRw` / `SizeRootFs`;「已停止的容器」回收卡按**已停止那些**的可写层算,不是全部容器) +✅ 堆叠条补上了 **容器可写层** 段 +✅ **宿主磁盘总量**「宿主盘 X 中已用 Y」—— Engine API 不给,借一个运行中的容器跑 `df`, + 查的是 `Docker Root Dir` 那块盘(不是 `/`,它们常常不是同一块)。 + 拿不到就不显示,不报错 —— 它是锦上添花 + +### 11 · 总览 + +✅ 四张卡(运行中容器 / CPU 总占用 / 内存 / 可回收空间)—— 本轮刚接通 +✅ CPU Top 5、实时事件时间线、快捷动作 +✅ 需要关注:不健康、异常退出 + +✅ 「**CPU 持续高于 30%**」告警 —— 要**连续两分钟**在高位才报。 + CPU 尖峰在容器世界里再正常不过,把一次 40% 报成告警只会训练用户忽略这块面板 +✅ 「卷未被使用」告警(本来就有) +✅ 资源趋势的**时间轴刻度** +✅ 快捷动作补齐 6 个:运行容器 / 拉取镜像 / 新建 Compose / 清理空间 / **打开终端** / **导出诊断** +✅ CPU 卡的「N 核 · 峰值 X%」 + +> **导出诊断**刻意**不含**凭据、环境变量与日志正文 —— 它的用途是发给别人, +> 而那几处最容易夹带口令;一份要发出去的文件不该由用户去逐行检查。 + +### 12 / 13 · 确认闸门 + +✅ 两档(一般破坏性 / 会丢数据)、Tier chip、目标主机(名称 + 地址 + socket + 传输) +✅ 将要执行(逐条真请求 + 等价命令)、目标列表、后果逐条、手打确认串 +✅ 「Esc 取消」提示 + +✅ 会丢数据档的「**先备份为 tar 再删除**」勾选,默认打开;只在真备得了的时候才出现 + (一个点了没反应的复选框比没有更糟),备份失败就不执行删除 +✅ 手打框的「还差 N 个字符」实时提示、「Esc 取消 · 回车不会触发」 + +### 14 · 状态与边界(六种状态) + +✅ 01 未选择会话(含「面板不自己发起连接」说明 + 两个动作) +✅ 02 建立通道中(骨架屏保留列宽) +✅ 03 找不到 docker.sock(含原始 errno) +✅ 04 没有权限(含原始报错) +✅ 05 空列表(容器页/镜像页都有) +✅ 06 事件流断开 · 退化 30s + 重连 + 实时徽章转灰 + +✅ 03 态的「**在终端里检查**」——- 把 `ls -l ; systemctl status docker` 送进宿主终端 +✅ 04 态的两条出路:「加进 docker 组」与「试试 sudo -n」(两条都是送命令到终端, + **面板绝不弹口令框** —— 那需要一个它拿不到也不该拿的东西) +✅ 05 态的动作按钮「运行容器…」+「从 compose 起一套」 + +> 排查命令是**送进宿主终端**而不是面板自己跑:这类命令的输出常常要人读、 +> 要接着改着再跑一遍,那是终端的主场;而且它经宿主的授权闸,面板不越过那道门。 + +### 15 · 设计系统 + +⚠️ **这块要改的是稿子,不是代码。** 稿子假设强调色是 Docker 蓝 `#2496ED`(亮色降到 `#0B72C4`), +而宿主真实调色板是 **Dracula(暗)/ Alucard(亮)**,强调色是紫 `#BD93F9`,令牌定义在 +`src/VelaShell.Controls/Themes/VelaTokens.axaml`。实现按真实调色板走,Docker 蓝只留给左上角品牌标。 +15 号板的「设计变量 → `{DynamicResource Vela*}`」映射表需要同步这一点。 + +### 16 · 拉取镜像 · 三态 + +✅ 表单 / 进行中 / 完成 三态原地切换、逐层进度、速度、复用层统计、凭据状态、转入后台、取消 +✅ 「关闭对话框不会取消 —— 任务转入顶栏任务中心」 + +⚠️ 完成态的「查看执行记录」:拉取的逐层进度与结果留在**任务中心**里, + Compose 那边有自己的执行记录面板。没有做一个跨页面的全局"执行记录"—— + 那会与任务中心重复,而两个都记一半比只有一个更难用。 + +### 17 · 进度体系 + +✅ 顶栏任务中心:收起态(N 个任务 + 总进度)、弹层、清空已完成、逐个取消 +✅ 确定型 / 不确定型两类 +✅ 状态栏常驻 + toast(带动作按钮、成功 4s 自动消失、失败不自动消失) +✅ 批量结果逐目标判定(`BatchRunner` 从不把整批说成失败) + +✅ **批量操作原地变进度条** —— 选中条本身变成「正在停止 10 个容器… 6/10 · 当前:worker-2 · + 等待 SIGTERM(10s 超时)」+「取消剩余」。位置不动、列表不动、选中不丢 +✅ 批量完成态的「**重试失败的 N 个**」+ 逐条失败明细,**成功的折叠成一句** + (它们不需要用户做任何事) +✅ 行内进度条接了 `BusyProgress`,值为 0 才退化成不确定型 + +⚠️ 任务中心里部分失败任务的 chevron-right 展开(结果现在直接留在选中条上,优先级下降) +⚠️ 「任务只属于当前主机 —— 换会话不会串台」需要真机验证 + +### 18 · 表单弹窗合集 + +✅ 打标签、重命名容器、修改重启策略、新建卷、新建网络、接入容器到网络 —— 六个全有 +✅ 统一的 C/Dialog 壳:标题栏 → 主机条 → 内容 → 页脚,主机条一行不省 +✅ 额外多做了一个「提交为镜像」 + +### 19 · 菜单与弹层 + +✅ **主机切换器弹层**:本机 + 已连接 SSH 会话,每条带 Engine 版本与状态(通道已建立 / 未建立 · 点击连接 / 找不到 docker.sock)、「只列出宿主已连接的会话」说明 +✅ **设置抽屉**:连接 / 显示 / 行为三节,行高分段、显示已停止、实时统计、自动刷新兜底、破坏性确认不可关闭 + +✅ **容器行右键菜单** —— 查看日志 `Ctrl L`、进入终端、浏览文件、实时统计 / 启动、停止 `Ctrl .`、 + 重启 `Ctrl R`、暂停、恢复、强制杀死… / 重命名…、修改重启策略… / 复制容器 ID、 + **复制 docker run 命令** / 删除…。带省略号的走确认闸门,不带的立即执行; + 不适用的项(停止一个已停的容器)置灰而不是消失 +✅ **镜像行右键菜单** —— 运行、inspect/构建历史、打标签、推送、导出 tar、复制 ID、删除 +✅ **命令面板 `Ctrl K`** —— 分组(动作 / 容器 / 镜像·卷 / 面板命令)、↑↓ 环绕选择、⏎ 执行、 + Tab 补全(补的是去掉省略号的标题)、Esc 关闭、顶部显示当前主机名、 + 脚注写明「破坏性命令带省略号,选中后仍走确认闸门」。**每次打开重新收集**,不缓存 —— + 缓存会让用户对着一个已经不存在的容器按回车 +✅ **顶栏全局搜索框**「搜索容器、镜像、卷、网络… `Ctrl K`」—— 与快捷键同一个入口 + +✅ 设置抽屉的「**日志默认行数**」分段(100 / 500 / 2000 / 全部),带"全部会拉回几十万行"的提醒 +✅ 连接节写清了**为什么没有 DOCKER_HOST / DOCKER_CONTEXT**:面板只经 SSH 隧道连 unix socket, + 不走 TCP,填的是路径不是 URL;而 context 是 CLI 的概念,daemon 不认 +✅ sudo 那条出路挪到了「没有权限」的错误态里(板 14),那才是用户需要它的时刻 + +❌ **筛选弹层** —— 按 **COMPOSE 项目**分组筛选(带各组计数)。 + 状态筛选已经是分段按钮,缺的是项目分组这一半 + +> **`Ctrl+K` 不注册全局热键**,只在面板拿到焦点时生效 —— 宿主自己也有一个 `Ctrl+K`, +> 抢它的键会让用户在别处按不出宿主的命令面板。设计稿那句「在面板里,不劫持宿主的命令面板」 +> 说的就是这件事。 + +--- + +## 2. 超出设计稿的实现 + +这些不在稿子上,是实现时补的,**要不要留下由你定**: + +| 项 | 位置 | 说明 | +| --- | --- | --- | +| **镜像详情抽屉** | 镜像页 | 稿子 06 号板的抽屉位置给了运行容器表单,没画镜像详情。实现做了三页签(概览 / 层历史 / 原文),覆盖了 19 号板溢出菜单里的「inspect JSON」与「构建历史」 | +| **镜像导入 tar**(`docker load`) | 镜像页工具栏 | 稿子只有「导出为 tar」,没有导入 | +| **容器内进程表**(`docker top`) | 容器详情 · 统计页签 | 稿子没画 | +| **卷备份的 tar 流式下载** | 卷详情 | 稿子有「备份为 tar」按钮,但没画备份页签的内容 | +| 容器文件的上传 / 下载按钮 | 容器详情 · 文件页签 | 稿子是拖放区 + 上传图标,实现用了按钮 | +| 恢复(unpause)、强杀入口 | 容器页选中条 | 稿子的选中条只有五个动作 | +| 按主机记住 socket 路径 | 设置 · 连接 | 对应稿子的 DOCKER_HOST,但形态不同 | + +--- + +## 3. bug(已修) + +| # | 位置 | 问题 | 状态 | +| --- | --- | --- | --- | +| 1 | `Ui/Pages/SystemPageViewModel.cs` | 容器可写层恒为 `0`(`Sum(_ => 0L)`,且 `ContainerSummary` 缺 `SizeRw`)。磁盘表「容器」行与「已停止的容器」回收卡都永远显示 `0 B` | ✅ 已修 | +| 2 | `Ui/Pages/ContainersPageView.axaml` | 行内进度条写死 `IsIndeterminate="True"`,`RowBase.BusyProgress` 有值也用不上 | ✅ 已修 | + +## 3b. SDK 1.3(本轮新增) + +真 TTY 不是在插件里重写一个 ANSI 解析器,而是把宿主 `VelaShell.Terminal` 那一套整体出借: + +| 位置 | 内容 | +| --- | --- | +| `plugin-sdk/.../TerminalView/ITerminalViewApi.cs` | `ITerminalViewApi` + `IPluginTerminalView` + `TerminalViewOptions`。SDK 仍是 BCL-only,控件以 `object` 出面(与 `IUiApi` 的内容工厂同一个约定) | +| `IPluginContext.TerminalView` | 新能力挂点;`SdkVersion` 1.2.0 → **1.3.0** | +| `src/VelaShell/Services/Plugins/PluginTerminalViewApi.cs` | 真实现:每次 `Create` 交出一个独立的 `VelaTerminalControl`,外观走宿主自己那条 `ApplyLiveTerminalSettings` | +| `MainWindowViewModel.ApplyTerminalAppearanceToPluginView` | 让插件终端与 SSH 标签共用同一条外观路径,以后加的外观项自动跟着走 | +| `PluginTerminalView.AttachAsync` | 一行接上双工流:读在后台、渲染回 UI 线程、用户按键经 channel **串行**写回(并发写会把一次按键的多个字节劈开,UTF-8 与转义序列都经不起劈) | +| `RemotePluginContext.IsolatedTerminalView` | 隔离进程明确报不可用 —— 活的原生控件嵌不进另一个进程 | +| `FakeTerminalView` / `FakeTerminalViewApi` | 测试替身,记录喂进去什么、插件送回什么 | + +宿主侧 275 测试通过(新增 8 个),SDK 已打包到 `G:\VelaShell\artifacts\nuget`。 +插件的 `minSdkVersion` 提到 `1.3.0`。 + +--- + +## 4. 还剩什么 + +设计稿上的功能已经全部实现。剩下的几项是**刻意留着**的,每一条都有理由: + +| 项 | 板 | 为什么没做 | +| --- | --- | --- | +| 编辑器语法高亮 + 行号(容器文件 / compose YAML) | 04 / 09 | 要一个带语法着色与虚拟化的编辑器控件。宿主没有,自己写是单独一件事。它不影响"能不能改文件",只影响"改起来舒不舒服" | +| 文件树形展开 | 04 | 同上 —— 平铺目录一级一级进是可用的,树只是更快 | +| 日志 JSON 正文高亮 | 03 | 级别已经着色(那是判断"要不要管"的那一层);正文着色收益小得多 | +| 网络拓扑图 | 08 | 要一个图布局引擎。已接入容器的**列表**已经把同样的信息给全了 | +| 终端多会话标签 | 05 | 真 TTY 已经通了,一个容器一个会话覆盖绝大多数用法;多标签是并发便利 | +| 卷抽屉页签化 | 07 | 内容全都有,只是排在一页里而不是三个页签 | +| compose 项目筛选弹层 | 19 | 状态筛选已是分段按钮;按项目筛选可以用搜索框输项目名代替 | +| i18n | — | 你明确延后 | + +**真机验证清单**(这些只有连上真 daemon 才验得了):真 TTY 里跑 `top` / `vim` / `less`、 +多容器合并日志的到达顺序、`docker run` 反推与实际命令的差异、宿主磁盘 `df`(要有运行中的容器)、 +删卷前的备份闸门、任务中心换会话是否串台。 + +--- + +## 5. 未提交 + +插件 v2 重写整体仍未提交;SDK 侧(`G:\VelaShell`)的 1.3 改动同样未提交。 diff --git a/plugins/VelaShell.Plugin.DockerPanel/plugin.json b/plugins/VelaShell.Plugin.DockerPanel/plugin.json new file mode 100644 index 0000000..1ee854e --- /dev/null +++ b/plugins/VelaShell.Plugin.DockerPanel/plugin.json @@ -0,0 +1,35 @@ +{ + "id": "velashell.dockerpanel", + "version": "2.0.0", + "displayName": "Docker Panel", + "description": "远端 Docker 管理面板:容器 / 镜像 / 卷 / 网络 / Compose 一站式管理,含实时统计、日志、容器内文件编辑与内置终端。经既有 SSH 会话直连远端的 /var/run/docker.sock,服务器上不需要暴露 daemon 端口。", + "publisher": "velashell", + "author": "Joe", + "entry": "VelaShell.Plugin.DockerPanel.dll", + "apiLevel": 2, + // 面板走 Docker Engine 的 HTTP API,而这条 API 的载体是一个 unix socket。 + // SDK 1.2 的 IRemoteTunnelApi 才提供到远端 socket 的**裸字节双工流**; + // 1.1 的远程执行只有"整段 UTF-8 解码"与"按 \n 切行"两种文本形态, + // 承载不了分块传输、tar 归档流与 exec 的多路复用帧 —— 那不是慢一点,是数据静默损坏。 + // apiLevel 表达不了这一档(它只在**破坏性**变更时才动),所以钉 minSdkVersion。 + "minSdkVersion": "2.0.0", + "license": "MIT", + "homepage": "https://github.com/joesdu/VelaShell", + // 命令贡献:发现期就进命令面板(**不装载本程序集**),用户按下它才触发下面的惰性激活。 + // 面板本身没有连接配置 —— 它复用用户已经建好的 SSH 会话,因此不需要 protocols/workspaces 页签。 + "contributes": { + "commands": [ + { + "id": "velashell.dockerpanel.open", + "title": "Docker: 打开 Docker 管理面板", + "category": "Docker" + } + ] + }, + "activationEvents": [ + "onCommand:velashell.dockerpanel.open" + ] + // hostMode 必须是默认的 inProcess,两个理由都是硬的: + // · 面板要以停靠标签页的身份进主窗口标签区,而原生控件无法跨进程嵌入; + // · IRemoteTunnelApi 交出去的是一条活的 Stream,隔离进程里明确不可用。 +} diff --git a/tests/VelaShell.Plugin.DockerPanel.Tests/ComposeHostTests.cs b/tests/VelaShell.Plugin.DockerPanel.Tests/ComposeHostTests.cs new file mode 100644 index 0000000..a48f64a --- /dev/null +++ b/tests/VelaShell.Plugin.DockerPanel.Tests/ComposeHostTests.cs @@ -0,0 +1,188 @@ +using VelaShell.Plugin.DockerPanel.Docker; +using VelaShell.PluginSdk.RemoteExec; +using VelaShell.PluginSdk.RemoteFs; + +namespace VelaShell.Plugin.DockerPanel.Tests; + +/// +/// compose 的执行通道。 +/// +/// 这里盯的是**下发到远端 shell 的那一整行**。参数在插件里是按 argv 传的, +/// 拼行只发生在一处;拼错了(比如漏掉开头的 docker、或者带空格的项目名没引起来), +/// 界面上看不出来 —— 命令是在别人的机器上失败的。 +/// +/// +[TestClass] +public sealed class ComposeHostTests +{ + // ProjectDirectory 是从 compose 文件路径推出来的,不能单独给。 + private static ComposeProject Project(string name, string directory) => + new(name, "running(2)", directory + "/compose.yaml"); + + [TestMethod] + public async Task RemoteHost_PrependsDockerAndQuotesOnlyWhatNeedsIt() + { + var exec = new RecordingExec(); + var cli = new ComposeCli(new RemoteComposeHost(exec, RemoteFs, "session-1")); + + await cli.ConfigAsync(Project("shop", "/srv/app"), TestContext.CancellationToken); + + // 开头的 docker 是远端这条通道自己补的 —— 本机那条由进程名承担,argv 里没有它。 + Assert.AreEqual("docker compose -p shop -f /srv/app/compose.yaml --project-directory /srv/app config", + exec.LastCommand); + } + + [TestMethod] + public async Task RemoteHost_QuotesProjectNamesWithSpaces() + { + var exec = new RecordingExec(); + var cli = new ComposeCli(new RemoteComposeHost(exec, RemoteFs, "session-1")); + + await cli.ConfigAsync(Project("my shop", "/srv/my app"), TestContext.CancellationToken); + + Assert.Contains("-p 'my shop'", exec.LastCommand); + Assert.Contains("-f '/srv/my app/compose.yaml'", exec.LastCommand); + Assert.Contains("--project-directory '/srv/my app'", exec.LastCommand); + } + + [TestMethod] + public async Task RemoteHost_ProbesComposeV2() + { + var exec = new RecordingExec(); + var cli = new ComposeCli(new RemoteComposeHost(exec, RemoteFs, "session-1")); + + Assert.IsTrue(await cli.IsAvailableAsync(TestContext.CancellationToken)); + Assert.AreEqual("docker compose version --short", exec.LastCommand); + } + + [TestMethod] + public void LocalHost_IsFlaggedAsLocalSoTheHintCanSayWhereToLook() + { + Assert.IsTrue(new ComposeCli(new LocalComposeHost()).IsLocal); + Assert.IsFalse(new ComposeCli(new RemoteComposeHost(new RecordingExec(), RemoteFs, "s")).IsLocal); + } + + /// + /// 这几个用例只走命令那条路,文件通道压根不参与 —— + /// 与其塞一个二十来个成员的空壳,不如把"没用到"直接写出来。 + /// + private static IRemoteFsApi RemoteFs => null!; + + /// 测试上下文(取消令牌用)。 + public TestContext TestContext { get; set; } = null!; + + private sealed class RecordingExec : IRemoteExecApi + { + public string LastCommand { get; private set; } = ""; + + public Task RunAsync(string sessionId, string command, ExecOptions? options = null, + CancellationToken cancellationToken = default) + { + LastCommand = command; + return Task.FromResult(new ExecResult("") { ExitCode = 0 }); + } + + public Task StreamAsync(string sessionId, string command, ExecStreamOptions? options, + IProgress output, CancellationToken cancellationToken = default) + { + LastCommand = command; + return Task.FromResult(new ExecStreamResult(0, 0)); + } + } +} + +/// +/// 本机那条通道的实地校验。 +/// +/// 这台机器上没有 docker 时整类跳过 —— 它验的是"起进程、收两条流、拿退出码" +/// 这条真实路径,不是可以拿假对象糊过去的东西。 +/// +/// +[TestClass] +public sealed class LocalComposeHostSmokeTests +{ + private static readonly LocalComposeHost Host = new(); + + /// 测试上下文(取消令牌用)。 + public TestContext TestContext { get; set; } = null!; + + [TestMethod] + public async Task LocalHost_RunsDockerComposeAndBringsBackStdout() + { + ExecResult result; + try + { + result = await Host.RunAsync(["compose", "version", "--short"], TimeSpan.FromSeconds(30), + TestContext.CancellationToken); + } + catch (Exception ex) + { + Assert.Inconclusive($"这台机器上起不了 docker:{ex.Message}"); + return; + } + if (result.ExitCode != 0 && LooksLikeNoDaemon(result.Error)) + { + Assert.Inconclusive($"这台机器上 docker 守护进程没起:{result.Error}"); + return; + } + Assert.AreEqual(0, result.ExitCode, result.Error); + // compose v2 的版本号形如 2.29.7 / 5.4.0 —— 只断言"有内容且以数字打头"。 + var version = result.Output.Trim(); + Assert.IsTrue(version.Length > 0 && char.IsAsciiDigit(version[0]), $"版本号不像话:{version}"); + } + + [TestMethod] + public async Task LocalHost_StreamsBothPipesLineByLine() + { + List lines = []; + int exit; + try + { + exit = await Host.StreamAsync(["compose", "ls", "--all", "--format", "json"], + new CollectingProgress(lines), TestContext.CancellationToken); + } + catch (Exception ex) + { + Assert.Inconclusive($"这台机器上起不了 docker:{ex.Message}"); + return; + } + var output = string.Join('\n', lines.Select(l => l.Line)); + if (exit != 0 && LooksLikeNoDaemon(output)) + { + Assert.Inconclusive($"这台机器上 docker 守护进程没起:{output}"); + return; + } + Assert.AreEqual(0, exit, output); + // --format json 至少给一个 JSON 数组,哪怕是空的。 + var joined = string.Concat(lines.Where(l => l.Stream == ExecStream.StandardOutput).Select(l => l.Line)); + Assert.StartsWith("[", joined.TrimStart()); + } + + /// + /// "docker 在,但守护进程没起"。 + /// + /// 与"这台机器压根没装 docker"是两回事,而上面那两处 try/catch 只拦得住后者 —— + /// 没装时进程起不来会抛异常,装了但守护进程没起时 docker compose 正常退出, + /// 只是**退出码非零 + stderr 写着连不上**。于是这两条用例在 CI runner + /// (装了 docker、守护进程不一定起着)与没开 Docker Desktop 的开发机上都会判失败, + /// 而它们本该是 Inconclusive —— 那不是被测代码的问题。 + /// + /// + private static bool LooksLikeNoDaemon(string? text) => + !string.IsNullOrEmpty(text) + && (text.Contains("docker API", StringComparison.OrdinalIgnoreCase) + || text.Contains("daemon", StringComparison.OrdinalIgnoreCase) + || text.Contains("cannot connect", StringComparison.OrdinalIgnoreCase) + || text.Contains("pipe/docker", StringComparison.OrdinalIgnoreCase)); + + private sealed class CollectingProgress(List sink) : IProgress + { + public void Report(ExecOutput value) + { + lock (sink) + { + sink.Add(value); + } + } + } +} diff --git a/tests/VelaShell.Plugin.DockerPanel.Tests/DockerPanelHeadlessApp.cs b/tests/VelaShell.Plugin.DockerPanel.Tests/DockerPanelHeadlessApp.cs new file mode 100644 index 0000000..be1db71 --- /dev/null +++ b/tests/VelaShell.Plugin.DockerPanel.Tests/DockerPanelHeadlessApp.cs @@ -0,0 +1,38 @@ +using Avalonia; +using Avalonia.Headless; +using VelaShell.Plugin.DockerPanel.Tests; + +[assembly: AvaloniaTestApplication(typeof(DockerPanelHeadlessApp))] + +namespace VelaShell.Plugin.DockerPanel.Tests; + +/// +/// 确认闸门那几条测试共用的 headless 宿主。 +/// +/// 为什么需要它。Ui.Post 落状态, +/// 而 Ui.Post 只在**调用方已经在 UI 线程上**时才同步执行,否则排进 +/// Dispatcher.UIThread 的队列。生产路径上闸门总是从 UI 线程打开的,这条成立; +/// 但测试线程不是 UI 线程 —— 那次 Post 会排进一个**没人泵**的队列, +/// 于是 Request 永远不落,断言读到的是闸门关着时的空值。 +/// +/// +/// 为什么以前没露面。没有 TestTimeout 时 MSTest 在当前线程上直接跑测试体, +/// 全程只有一条线程 —— 谁先碰 Dispatcher.UIThread 谁就是"UI 线程", +/// CheckAccess() 于是恒为真,Post 全部同步执行。本仓库的 +/// tests/velashell.runsettings 设了 60 秒单测超时,而带超时的测试体被 MSTest 放到 +/// **线程池线程**上跑 —— 每条测试落在哪条线程不再确定,这条隐式依赖就暴露了: +/// 并入本仓库后 5 次里有 4 次红,而且红的是哪几条全看调度。 +/// +/// +/// 刻意不装任何主题。这几条测试一个控件都不构造,要的只是一条真的在泵的 UI 线程。 +/// (Redis 那边的 headless 宿主装 Fluent,是因为它真的要装载 AXAML 与模板。) +/// +/// +public class DockerPanelHeadlessApp : Application +{ + /// headless 宿主的构建入口(由 Avalonia 的测试基建反射调用)。 + /// 应用构建器。 + public static AppBuilder BuildAvaloniaApp() => + AppBuilder.Configure() + .UseHeadless(new AvaloniaHeadlessPlatformOptions { UseHeadlessDrawing = true }); +} diff --git a/tests/VelaShell.Plugin.DockerPanel.Tests/DockerProtocolTests.cs b/tests/VelaShell.Plugin.DockerPanel.Tests/DockerProtocolTests.cs new file mode 100644 index 0000000..23fdc12 --- /dev/null +++ b/tests/VelaShell.Plugin.DockerPanel.Tests/DockerProtocolTests.cs @@ -0,0 +1,342 @@ +using System.Buffers.Binary; +using System.Text; +using VelaShell.Plugin.DockerPanel.Docker; + +namespace VelaShell.Plugin.DockerPanel.Tests; + +/// +/// 线上协议那一层的测试。 +/// +/// 这些是**改走 HTTP API 之后新出现的失败模式**:8 字节帧头解错会把日志劈成两半、 +/// tar 的校验和算反了 daemon 会说文件损坏、拉取失败时 HTTP 仍然是 200。 +/// 它们都不会在编译期被发现,而且在真机上表现成"偶尔少几行"这类最难查的现象。 +/// +/// +[TestClass] +public class DockerProtocolTests +{ + // ── 多路复用帧 ──────────────────────────────────────────── + + private static byte[] Frame(DockerStreamKind kind, string text) + { + var payload = Encoding.UTF8.GetBytes(text); + var frame = new byte[8 + payload.Length]; + frame[0] = (byte)kind; + BinaryPrimitives.WriteInt32BigEndian(frame.AsSpan(4, 4), payload.Length); + payload.CopyTo(frame, 8); + return frame; + } + + [TestMethod] + public async Task Decoder_SplitsStdoutAndStderrIntoSeparateStreams() + { + byte[] data = [.. Frame(DockerStreamKind.StdOut, "hello\n"), .. Frame(DockerStreamKind.StdErr, "boom\n")]; + List lines = []; + await new DockerFrameDecoder(tty: false, timestamps: false) + .ReadAsync(new MemoryStream(data), lines.Add, CancellationToken.None); + + Assert.HasCount(2, lines); + Assert.AreEqual(DockerStreamKind.StdOut, lines[0].Kind); + Assert.AreEqual("hello", lines[0].Text); + // 标准错误单独一条流:合并进 stdout 会让"命令失败了"和"命令没有输出"长得一模一样。 + Assert.AreEqual(DockerStreamKind.StdErr, lines[1].Kind); + Assert.AreEqual("boom", lines[1].Text); + } + + [TestMethod] + public async Task Decoder_KeepsPayloadIntactWhenItContainsNewlines() + { + // 帧长度字段里出现 0x0A 是常态 —— 按行切分会把一帧劈成两半,这正是不能用 + // 文本行模型承载这条流的原因。 + var data = Frame(DockerStreamKind.StdOut, "a\nb\nc\n"); + List lines = []; + await new DockerFrameDecoder(tty: false, timestamps: false) + .ReadAsync(new MemoryStream(data), lines.Add, CancellationToken.None); + + Assert.AreSequenceEqual(["a", "b", "c"], [.. lines.Select(l => l.Text)]); + } + + [TestMethod] + public async Task Decoder_HoldsBackHalfLinesUntilTheyAreComplete() + { + byte[] data = [.. Frame(DockerStreamKind.StdOut, "par"), .. Frame(DockerStreamKind.StdOut, "tial\n")]; + List lines = []; + await new DockerFrameDecoder(tty: false, timestamps: false) + .ReadAsync(new MemoryStream(data), lines.Add, CancellationToken.None); + + // 半行开头的日志比少几行更难读 —— 跨帧的半行必须攒起来。 + Assert.HasCount(1, lines); + Assert.AreEqual("partial", lines[0].Text); + } + + [TestMethod] + public async Task Decoder_FlushesTheLastLineWithoutTrailingNewline() + { + var data = Frame(DockerStreamKind.StdOut, "no newline at eof"); + List lines = []; + await new DockerFrameDecoder(tty: false, timestamps: false) + .ReadAsync(new MemoryStream(data), lines.Add, CancellationToken.None); + + Assert.HasCount(1, lines); + Assert.AreEqual("no newline at eof", lines[0].Text); + } + + [TestMethod] + public async Task Decoder_SurvivesUtf8CharactersSplitAcrossFrames() + { + // "容" 是三个字节;把它劈在两帧之间,天真的实现会吐出两个 U+FFFD。 + var full = Encoding.UTF8.GetBytes("容器\n"); + var first = Frame(DockerStreamKind.StdOut, ""); + var head = BuildFrame(DockerStreamKind.StdOut, full.AsSpan()[..2]); + var tail = BuildFrame(DockerStreamKind.StdOut, full.AsSpan()[2..]); + List lines = []; + await new DockerFrameDecoder(tty: false, timestamps: false) + .ReadAsync(new MemoryStream([.. head, .. tail]), lines.Add, CancellationToken.None); + + Assert.HasCount(1, lines); + Assert.AreEqual("容器", lines[0].Text); + Assert.IsFalse(lines[0].Text.Contains('�'), "UTF-8 序列被跨帧劈开时不应产生替换字符。"); + Assert.HasCount(8, first); + } + + private static byte[] BuildFrame(DockerStreamKind kind, ReadOnlySpan payload) + { + var frame = new byte[8 + payload.Length]; + frame[0] = (byte)kind; + BinaryPrimitives.WriteInt32BigEndian(frame.AsSpan(4, 4), payload.Length); + payload.CopyTo(frame.AsSpan(8)); + return frame; + } + + [TestMethod] + public async Task Decoder_InTtyModeTreatsEverythingAsRawStdout() + { + // 有 TTY 时没有帧头 —— 按 8 字节头去解会把前八个字符吃掉。 + var data = Encoding.UTF8.GetBytes("raw tty line\n"); + List lines = []; + await new DockerFrameDecoder(tty: true, timestamps: false) + .ReadAsync(new MemoryStream(data), lines.Add, CancellationToken.None); + + Assert.HasCount(1, lines); + Assert.AreEqual("raw tty line", lines[0].Text); + } + + [TestMethod] + public void TrySplitTimestamp_PullsTheRfc3339PrefixOutOfTheLine() + { + Assert.IsTrue(DockerFrameDecoder.TrySplitTimestamp("2026-08-21T09:41:22.118000000Z hello world", out var stamp, out var rest)); + Assert.AreEqual("hello world", rest); + Assert.AreEqual(2026, stamp.Year); + } + + [TestMethod] + public void TrySplitTimestamp_LeavesOrdinaryLinesAlone() + { + // 正文里恰好有空格的普通行不能被当成带时间戳的行切掉一段。 + Assert.IsFalse(DockerFrameDecoder.TrySplitTimestamp("GET /health 200", out _, out var rest)); + Assert.AreEqual("GET /health 200", rest); + } + + // ── tar ─────────────────────────────────────────────────── + + [TestMethod] + public async Task Tar_RoundTripsASingleFile() + { + var content = Encoding.UTF8.GetBytes("server {\n listen 80;\n}\n"); + var archive = TarUtil.CreateSingleFile("default.conf", content); + var entry = + await TarUtil.ReadFirstFileAsync(new MemoryStream(archive), 1 << 20, CancellationToken.None); + + Assert.IsNotNull(entry); + Assert.AreEqual("default.conf", entry.Value.Name); + Assert.AreSequenceEqual(content, entry.Value.Content); + } + + [TestMethod] + public void Tar_HeaderIsBlockAlignedAndEndsWithTwoZeroBlocks() + { + var archive = TarUtil.CreateSingleFile("a.txt", "x"u8); + // 512 头 + 512 补齐的内容 + 两个全零块。长度不对的话 daemon 会说归档被截断。 + Assert.HasCount(512 * 4, archive); + Assert.IsTrue(archive[^1024..].All(b => b == 0)); + } + + [TestMethod] + public void Tar_ChecksumIsComputedOverSpacesInTheChecksumField() + { + var archive = TarUtil.CreateSingleFile("a.txt", "x"u8); + uint expected = 0; + for (var i = 0; i < 512; i++) + { + // 求和时校验和字段要按 8 个空格计 —— 顺序反了 tar 会说文件损坏。 + expected += i is >= 148 and < 156 ? (byte)' ' : archive[i]; + } + uint stored = 0; + for (var i = 148; i < 155; i++) + { + if (archive[i] is >= (byte)'0' and <= (byte)'7') + { + stored = (stored * 8) + (uint)(archive[i] - '0'); + } + } + Assert.AreEqual(expected, stored); + } + + [TestMethod] + public async Task Tar_RefusesFilesLargerThanTheEditingCap() + { + var archive = TarUtil.CreateSingleFile("big.bin", new byte[4096]); + await Assert.ThrowsExactlyAsync(() => + TarUtil.ReadFirstFileAsync(new MemoryStream(archive), 1024, CancellationToken.None)); + } + + [TestMethod] + public async Task Tar_ExtractStreamsTheFileOutWithoutTheEditingCap() + { + // "另存为"这条路不该受在线编辑那个 2 MB 上限管 —— + // 几百兆的日志正是用户最想取下来的东西。 + var payload = new byte[300_000]; + for (var i = 0; i < payload.Length; i++) + { + payload[i] = (byte)(i % 251); + } + var archive = TarUtil.CreateSingleFile("app.log", payload); + var output = new MemoryStream(); + + var written = await TarUtil.ExtractFirstFileAsync(new MemoryStream(archive), output, CancellationToken.None); + + Assert.AreEqual(payload.Length, written); + Assert.AreSequenceEqual(payload, output.ToArray()); + } + + [TestMethod] + public async Task Tar_ExtractReportsZeroWhenTheArchiveHoldsNoRegularFile() + { + // 目录条目走的是另一条路(整包存成 tar),这里返回 0 而不是抛。 + var empty = new byte[1024]; + Assert.AreEqual(0, await TarUtil.ExtractFirstFileAsync(new MemoryStream(empty), new MemoryStream(), + CancellationToken.None)); + } + + [TestMethod] + public async Task Tar_ExtractRefusesToSilentlyTruncateATruncatedStream() + { + var archive = TarUtil.CreateSingleFile("a.txt", new byte[2048]); + // 砍掉后半段:一个静默截断的文件比一句"下载失败"危险得多。 + var truncated = archive[..(512 + 600)]; + + await Assert.ThrowsExactlyAsync(() => + TarUtil.ExtractFirstFileAsync(new MemoryStream(truncated), new MemoryStream(), CancellationToken.None)); + } + + // ── 查询串 ──────────────────────────────────────────────── + + [TestMethod] + public void Query_SkipsNullsAndEscapesValues() + { + var query = DockerClient.Query(("a", "1"), ("skip", null), ("path", "/etc/nginx/conf.d")); + Assert.AreEqual("?a=1&path=%2Fetc%2Fnginx%2Fconf.d", query); + } + + [TestMethod] + public void Filters_GroupsRepeatedKeysIntoOneArray() + { + var filters = DockerClient.Filters(("label", "a=1"), ("label", "b=2")); + Assert.IsNotNull(filters); + Assert.Contains("\"label\"", filters); + Assert.Contains("a=1", filters); + Assert.Contains("b=2", filters); + } + + // ── ls 解析 ─────────────────────────────────────────────── + + [TestMethod] + public void ParseLsLine_ReadsAnOrdinaryFile() + { + var entry = DockerClient.ParseLsLine( + "-rw-r--r-- 1 root root 1842 2026-08-21 09:12 default.conf", "/etc/nginx/conf.d"); + + Assert.IsNotNull(entry); + Assert.AreEqual("default.conf", entry.Value.Name); + Assert.AreEqual("/etc/nginx/conf.d/default.conf", entry.Value.FullPath); + Assert.AreEqual(1842, entry.Value.Size); + Assert.IsFalse(entry.Value.IsDirectory); + } + + [TestMethod] + public void ParseLsLine_KeepsSpacesInsideFileNames() + { + var entry = DockerClient.ParseLsLine( + "-rw-r--r-- 1 root root 10 2026-08-21 09:12 my file.txt", "/tmp"); + + Assert.IsNotNull(entry); + // 按"第八段"取名字会把这个文件截成 "my" —— 必须取第七段之后的全部。 + Assert.AreEqual("my file.txt", entry.Value.Name); + } + + [TestMethod] + public void ParseLsLine_SplitsSymlinkTargets() + { + var entry = DockerClient.ParseLsLine( + "lrwxrwxrwx 1 root root 7 2026-08-21 09:12 sh -> busybox", "/bin"); + + Assert.IsNotNull(entry); + Assert.IsTrue(entry.Value.IsSymlink); + Assert.AreEqual("sh", entry.Value.Name); + Assert.AreEqual("busybox", entry.Value.LinkTarget); + } + + [TestMethod] + public void ParseLsLine_IgnoresTotalsAndDotEntries() + { + Assert.IsNull(DockerClient.ParseLsLine("total 12", "/tmp")); + Assert.IsNull(DockerClient.ParseLsLine("", "/tmp")); + Assert.IsNull(DockerClient.ParseLsLine("drwxr-xr-x 2 root root 4096 2026-08-21 09:12 .", "/tmp")); + } + + [TestMethod] + public void ShellQuote_NeutralisesEmbeddedSingleQuotes() + { + // 列目录是这一层唯一一处把用户输入拼进 shell 的地方,引用错了就是命令注入。 + Assert.AreEqual("'/tmp/it'\\''s here'", Sh.Quote("/tmp/it's here")); + } + + // ── 连不上的分类 ────────────────────────────────────────── + + /// 一条永远连不上的传输,用来把 Unreachable 的分类逼出来。 + private sealed class FailingTransport(string message) : IDockerTransport + { + public string Description => "/var/run/docker.sock (SSH 隧道)"; + + public Task ConnectAsync(CancellationToken cancellationToken = default) => + throw new IOException(message); + } + + private static async Task PingFailureAsync(string transportMessage) + { + await using var client = new DockerClient( + DockerEndpoint.Local("测试"), new FailingTransport(transportMessage)); + return await Assert.ThrowsExactlyAsync(() => client.PingAsync()); + } + + [TestMethod] + public async Task Unreachable_ClassifiesSshChannelOpenFailureAsSocketMissing() + { + // SDK 报的是 ConnectFailed —— 中间没有空格。只匹配 "connect failed" 会漏掉它, + // 于是最常见的一种失败反而落进 Unknown,界面上只剩一个"重试",没有任何出路。 + var ex = await PingFailureAsync( + "Failed to open channel - ConnectFailed - open failed. (docker:80)"); + + Assert.AreEqual(DockerUnreachableReason.SocketMissing, ex.Reason); + } + + [TestMethod] + public async Task Unreachable_PrefersPermissionDeniedOverChannelFailure() + { + // 两者同时出现时按"没权限"报:它是明说的,而"通道开不起来"是笼统的。 + var ex = await PingFailureAsync( + "open failed: connect /var/run/docker.sock: permission denied"); + + Assert.AreEqual(DockerUnreachableReason.PermissionDenied, ex.Reason); + } +} diff --git a/tests/VelaShell.Plugin.DockerPanel.Tests/DotEnvImportTests.cs b/tests/VelaShell.Plugin.DockerPanel.Tests/DotEnvImportTests.cs new file mode 100644 index 0000000..327475c --- /dev/null +++ b/tests/VelaShell.Plugin.DockerPanel.Tests/DotEnvImportTests.cs @@ -0,0 +1,117 @@ +using VelaShell.Plugin.DockerPanel.Ui; + +namespace VelaShell.Plugin.DockerPanel.Tests; + +/// +/// 「从 .env 导入」。 +/// +/// dotenv 的方言各家不同(插值、多行、转义)。面板只认最朴素的那一档, +/// 认不出来的原样跳过并**把跳过的条数报出来** —— 猜错一次就等于悄悄改了用户的配置。 +/// +/// +[TestClass] +public class DotEnvImportTests +{ + private static PairListField Field() => new("环境变量"); + + [TestMethod] + public void ReadsPlainAssignments() + { + var field = Field(); + + (var imported, var skipped) = field.ImportDotEnv("APP_MODE=prod\nPORT=8080"); + + Assert.AreEqual(2, imported); + Assert.AreEqual(0, skipped); + Assert.AreEqual("APP_MODE", field.Rows[0].Key); + Assert.AreEqual("prod", field.Rows[0].Value); + Assert.AreEqual("8080", field.Rows[1].Value); + } + + [TestMethod] + public void SkipsBlanksAndComments() + { + var field = Field(); + + (var imported, var skipped) = field.ImportDotEnv("# 注释\n\nA=1\n \n# 又一条\nB=2"); + + Assert.AreEqual(2, imported); + // 空行与注释不算"跳过" —— 它们本来就不是配置。 + Assert.AreEqual(0, skipped); + } + + [TestMethod] + public void StripsSurroundingQuotes() + { + var field = Field(); + + field.ImportDotEnv(""" + A="hello world" + B='single' + C=no-quotes + """); + + Assert.AreEqual("hello world", field.Rows[0].Value); + Assert.AreEqual("single", field.Rows[1].Value); + Assert.AreEqual("no-quotes", field.Rows[2].Value); + } + + [TestMethod] + public void HandlesExportPrefix() + { + var field = Field(); + + field.ImportDotEnv("export DATABASE_URL=postgres://app@db:5432/shop"); + + Assert.AreEqual("DATABASE_URL", field.Rows[0].Key); + Assert.AreEqual("postgres://app@db:5432/shop", field.Rows[0].Value); + } + + [TestMethod] + public void KeepsEqualsSignsInsideTheValue() + { + var field = Field(); + + // base64 与连接串里满是 = ,只能按**第一个** = 切。 + field.ImportDotEnv("TOKEN=YWJjZA=="); + + Assert.AreEqual("TOKEN", field.Rows[0].Key); + Assert.AreEqual("YWJjZA==", field.Rows[0].Value); + } + + [TestMethod] + public void ReportsLinesItCouldNotUnderstand() + { + var field = Field(); + + (var imported, var skipped) = field.ImportDotEnv("A=1\n这一行没有等号\n=没有键\nB=2"); + + Assert.AreEqual(2, imported); + // 报出来而不是静默丢掉:用户得知道有东西没进来。 + Assert.AreEqual(2, skipped); + } + + [TestMethod] + public void LaterAssignmentsWinJustLikeDotEnvItself() + { + var field = Field(); + + field.ImportDotEnv("A=first\nA=second"); + + Assert.HasCount(1, field.Rows); + Assert.AreEqual("second", field.Rows[0].Value); + } + + [TestMethod] + public void DropsTheEmptyPlaceholderRow() + { + var field = Field(); + field.AddCommand.Execute(null); + + field.ImportDotEnv("A=1"); + + // 导完之后留着一个空占位行只会让"等效命令"多一个空的 -e。 + Assert.HasCount(1, field.Rows); + Assert.AreEqual("A", field.Rows[0].Key); + } +} diff --git a/tests/VelaShell.Plugin.DockerPanel.Tests/InspectionTests.cs b/tests/VelaShell.Plugin.DockerPanel.Tests/InspectionTests.cs new file mode 100644 index 0000000..9d214f5 --- /dev/null +++ b/tests/VelaShell.Plugin.DockerPanel.Tests/InspectionTests.cs @@ -0,0 +1,216 @@ +using VelaShell.Plugin.DockerPanel.Docker; +using VelaShell.Plugin.DockerPanel.Ui; +using VelaShell.Plugin.DockerPanel.Ui.Pages; + +namespace VelaShell.Plugin.DockerPanel.Tests; + +/// +/// 面板"看进去"的那几处翻译:容器里的进程表、镜像的构建历史、可回收空间。 +/// 这三处的共同点是 daemon 给的东西**形状不固定** —— 列由远端的 ps 决定、 +/// 指令带着构建器的脚手架、df 的口径要和 prune 对得上。 +/// 这里钉的就是这些翻译。 +/// +[TestClass] +public class InspectionTests +{ + // ── docker top ──────────────────────────────────────────────── + + [TestMethod] + public void ProcessTable_ReadsGnuPsMinusEfByColumnName() + { + var top = new ContainerTopResult + { + Titles = ["UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"], + Processes = [["root", "1234", "1200", "0", "10:21", "?", "00:00:03", "nginx: master process"]] + }; + + var rows = ProcessTable.Normalize(top); + + Assert.HasCount(1, rows); + Assert.AreEqual("1234", rows[0].Pid); + Assert.AreEqual("root", rows[0].User); + Assert.AreEqual("00:00:03", rows[0].Cpu); + Assert.AreEqual("nginx: master process", rows[0].Command); + } + + [TestMethod] + public void ProcessTable_ReadsBusyboxLayoutWhereColumnsAreInADifferentOrder() + { + // Alpine 的 busybox ps 给的是另一套列。按下标取会把 TIME 显示成用户名 —— + // 这正是这一层存在的理由。 + var top = new ContainerTopResult + { + Titles = ["PID", "USER", "TIME", "COMMAND"], + Processes = [["1", "nobody", "0:00", "/bin/sh -c server"]] + }; + + var rows = ProcessTable.Normalize(top); + + Assert.AreEqual("1", rows[0].Pid); + Assert.AreEqual("nobody", rows[0].User); + Assert.AreEqual("0:00", rows[0].Cpu); + Assert.AreEqual("/bin/sh -c server", rows[0].Command); + } + + [TestMethod] + public void ProcessTable_FallsBackToTheLastColumnWhenTheCommandColumnHasAnUnknownName() + { + var top = new ContainerTopResult + { + Titles = ["PID", "USER", "TIME", "WHATEVER"], + Processes = [["7", "root", "0:01", "/usr/bin/redis-server"]] + }; + + // 命令永远排在末尾 —— 只有它能带空格,ps 没别的地方放它。 + Assert.AreEqual("/usr/bin/redis-server", ProcessTable.Normalize(top)[0].Command); + } + + [TestMethod] + public void ProcessTable_SurvivesRowsShorterThanTheTitleRow() + { + var top = new ContainerTopResult + { + Titles = ["UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"], + Processes = [["root", "1"]] + }; + + var rows = ProcessTable.Normalize(top); + + // 缺列不能抛:一个越界异常会把整张表换成一句报错,而前两列本来是读得到的。 + Assert.AreEqual("1", rows[0].Pid); + Assert.AreEqual("root", rows[0].User); + Assert.AreEqual("", rows[0].Cpu); + } + + [TestMethod] + public void ProcessTable_EmptyResultIsEmptyNotAnError() => + Assert.IsEmpty(ProcessTable.Normalize(null)); + + [TestMethod] + public void ProcessTable_NamesTheCpuColumnAfterWhatTheRemotePsActuallyGave() + { + Assert.AreEqual("TIME", ProcessTable.CpuColumnTitle(new() { Titles = ["PID", "USER", "TIME", "CMD"] })); + Assert.AreEqual("%CPU", ProcessTable.CpuColumnTitle(new() { Titles = ["USER", "PID", "%CPU", "COMMAND"] })); + Assert.AreEqual("CPU", ProcessTable.CpuColumnTitle(new() { Titles = ["PID", "CMD"] })); + } + + // ── 镜像层历史 ──────────────────────────────────────────────── + + [TestMethod] + public void CleanInstruction_StripsTheBuilderScaffoldingOffMetadataLayers() + { + Assert.AreEqual("ENV PATH=/usr/local/bin", + ImageDetailViewModel.CleanInstruction("/bin/sh -c #(nop) ENV PATH=/usr/local/bin")); + Assert.AreEqual("EXPOSE 80", + ImageDetailViewModel.CleanInstruction("/bin/sh -c #(nop) EXPOSE 80")); + } + + [TestMethod] + public void CleanInstruction_PutsRunBackOnRealCommandLayers() + { + // 剥掉 /bin/sh -c 之后只剩一句裸命令,夹在一排 COPY / ENV 中间读不出它是什么。 + Assert.AreEqual("RUN apt-get update && apt-get install -y curl", + ImageDetailViewModel.CleanInstruction("/bin/sh -c apt-get update && apt-get install -y curl")); + } + + [TestMethod] + public void CleanInstruction_LeavesBuildKitStyleEntriesAlone() + { + Assert.AreEqual("RUN /bin/sh -c make build # buildkit", + ImageDetailViewModel.CleanInstruction("RUN /bin/sh -c make build # buildkit")); + Assert.AreEqual("COPY . /app # buildkit", + ImageDetailViewModel.CleanInstruction("COPY . /app # buildkit")); + } + + [TestMethod] + public void CleanInstruction_SaysSomethingWhenTheresNothingToSay() => + Assert.AreEqual("(无记录)", ImageDetailViewModel.CleanInstruction(null)); + + // ── system df ───────────────────────────────────────────────── + + [TestMethod] + public void Reclaimable_CountsEveryUnusedImageNotJustTheDanglingOnes() + { + var usage = new DiskUsage + { + Images = + [ + new() { Id = "sha256:a", Size = 100, Containers = 1, RepoTags = ["app:1"] }, + new() { Id = "sha256:b", Size = 250, Containers = 0, RepoTags = ["old:1"] }, + new() { Id = "sha256:c", Size = 50, Containers = 0, RepoTags = [":"] } + ] + }; + + var reclaim = DiskMath.Reclaimable(usage); + + // 只算悬空的那 50 会把可回收量报少五倍 —— prune -a 会把 old:1 也删掉。 + Assert.AreEqual(300, reclaim.Images); + Assert.AreEqual(2, reclaim.UnusedImages); + } + + [TestMethod] + public void Reclaimable_CountsOnlyVolumesNothingRefersTo() + { + var usage = new DiskUsage + { + Volumes = + [ + new() { Name = "used", UsageData = new() { Size = 900, RefCount = 2 } }, + new() { Name = "orphan", UsageData = new() { Size = 400, RefCount = 0 } } + ] + }; + + var reclaim = DiskMath.Reclaimable(usage); + + Assert.AreEqual(400, reclaim.Volumes); + Assert.AreEqual(1, reclaim.UnusedVolumes); + } + + [TestMethod] + public void Reclaimable_SkipsBuildCacheThatIsStillInUse() + { + var usage = new DiskUsage + { + BuildCache = + [ + new() { ID = "x", Size = 1000, InUse = true }, + new() { ID = "y", Size = 700, InUse = false } + ] + }; + + Assert.AreEqual(700, DiskMath.Reclaimable(usage).BuildCache); + } + + [TestMethod] + public void Reclaimable_TotalIsTheSumOfThreeAndSurvivesAnEmptyReport() + { + var usage = new DiskUsage + { + Images = [new() { Id = "sha256:b", Size = 250, Containers = 0 }], + Volumes = [new() { Name = "orphan", UsageData = new() { Size = 400, RefCount = 0 } }], + BuildCache = [new() { ID = "y", Size = 700, InUse = false }] + }; + + Assert.AreEqual(1350, DiskMath.Reclaimable(usage).Total); + // daemon 在全新机器上把这三段都给 null,不能因此抛。 + Assert.AreEqual(0, DiskMath.Reclaimable(new DiskUsage()).Total); + Assert.AreEqual(0, DiskMath.Reclaimable(null).Total); + } + + [TestMethod] + public void Reclaimable_SaysNothingToCleanWhenThereIsNothing() => + Assert.AreEqual("没有可回收的空间", DiskMath.Reclaimable(new DiskUsage()).Describe()); + + // ── 时间口径 ────────────────────────────────────────────────── + + [TestMethod] + public void AgoFromIso_HandlesTheRfc3339FormThatImageInspectUses() + { + var text = Humanize.AgoFromIso(DateTimeOffset.UtcNow.AddHours(-3).ToString("O")); + + Assert.EndsWith("前", text); + // 列表接口给 unix 秒、inspect 给 RFC3339,两条路要落到同一句话上。 + Assert.AreEqual("—", Humanize.AgoFromIso("not a timestamp")); + Assert.AreEqual("—", Humanize.AgoFromIso(null)); + } +} diff --git a/tests/VelaShell.Plugin.DockerPanel.Tests/LineDiffTests.cs b/tests/VelaShell.Plugin.DockerPanel.Tests/LineDiffTests.cs new file mode 100644 index 0000000..6294eff --- /dev/null +++ b/tests/VelaShell.Plugin.DockerPanel.Tests/LineDiffTests.cs @@ -0,0 +1,121 @@ +using VelaShell.Plugin.DockerPanel.Docker; + +namespace VelaShell.Plugin.DockerPanel.Tests; + +/// +/// 按行比对。 +/// +/// 这块的价值全在"改一行只显示一行" —— 一个把整个文件都标成改过的差异视图, +/// 和没有差异视图是一样的,而且更浪费用户的时间。 +/// +/// +[TestClass] +public class LineDiffTests +{ + private static string Render(IReadOnlyList lines) => + string.Join("\n", lines.Select(l => l.Marker switch + { + DiffMarker.Added => "+" + l.Text, + DiffMarker.Removed => "-" + l.Text, + DiffMarker.Changed => "~" + l.Text, + _ => " " + l.Text + })); + + [TestMethod] + public void InsertingALineAtTheTopDoesNotMarkEverythingElse() + { + // 逐行对齐会把之后的每一行都标成改过 —— 那正是不能逐行对齐的理由。 + var diff = LineDiff.Compute("a\nb\nc", "new\na\nb\nc"); + + Assert.AreEqual("+new\n a\n b\n c", Render(diff)); + Assert.AreEqual(1, LineDiff.CountChanged(diff)); + } + + [TestMethod] + public void ReplacingALineShowsOneChangeNotTwo() + { + var diff = LineDiff.Compute("listen 80;", "listen 8080;"); + + // 人看到的是"一行改了",不是"删一行加一行"。 + Assert.AreEqual("~listen 8080;", Render(diff)); + Assert.AreEqual(1, LineDiff.CountChanged(diff)); + } + + [TestMethod] + public void KeepsOldAndNewLineNumbers() + { + var diff = LineDiff.Compute("a\nb", "a\nx\nb"); + + var added = diff.Single(l => l.Marker == DiffMarker.Added); + Assert.AreEqual(0, added.OldNumber); + Assert.AreEqual(2, added.NewNumber); + var last = diff[^1]; + // 原文第 2 行在新文里是第 3 行 —— 两侧行号都要留着,差异视图才对得上原文件。 + Assert.AreEqual(2, last.OldNumber); + Assert.AreEqual(3, last.NewNumber); + } + + [TestMethod] + public void DeletionsAreMarkedAndCounted() + { + var diff = LineDiff.Compute("a\ngone\nb", "a\nb"); + + Assert.AreEqual(" a\n-gone\n b", Render(diff)); + Assert.AreEqual(1, LineDiff.CountChanged(diff)); + } + + [TestMethod] + public void IdenticalTextHasNoChanges() + { + var diff = LineDiff.Compute("a\nb\nc", "a\nb\nc"); + + Assert.AreEqual(0, LineDiff.CountChanged(diff)); + Assert.HasCount(3, diff); + } + + [TestMethod] + public void NormalisesLineEndingsSoCrlfIsNotAWholeFileChange() + { + // 编辑器保存成 LF、原文件是 CRLF 时,不做归一化会把整个文件报成改过。 + var diff = LineDiff.Compute("a\r\nb\r\nc", "a\nb\nc"); + + Assert.AreEqual(0, LineDiff.CountChanged(diff)); + } + + [TestMethod] + public void HandlesEmptySides() + { + Assert.IsEmpty(LineDiff.Compute("", "")); + Assert.AreEqual(2, LineDiff.Compute("", "a\nb").Count(l => l.Marker == DiffMarker.Added)); + Assert.AreEqual(2, LineDiff.Compute("a\nb", "").Count(l => l.Marker == DiffMarker.Removed)); + } + + [TestMethod] + public void RealisticConfigEditShowsOnlyTheEditedRegion() + { + const string before = """ + upstream api_backend { + least_conn; + keepalive 32; + } + server { + listen 80; + } + """; + const string after = """ + upstream api_backend { + least_conn; + server api-gateway:8000 max_fails=3; + keepalive 32; + } + server { + listen 80; + } + """; + + var diff = LineDiff.Compute(before, after); + + Assert.AreEqual(1, LineDiff.CountChanged(diff)); + Assert.AreEqual(DiffMarker.Added, diff.Single(l => l.Marker != DiffMarker.None).Marker); + } +} diff --git a/tests/VelaShell.Plugin.DockerPanel.Tests/LogLevelTests.cs b/tests/VelaShell.Plugin.DockerPanel.Tests/LogLevelTests.cs new file mode 100644 index 0000000..9504ade --- /dev/null +++ b/tests/VelaShell.Plugin.DockerPanel.Tests/LogLevelTests.cs @@ -0,0 +1,83 @@ +using VelaShell.Plugin.DockerPanel.Ui; + +namespace VelaShell.Plugin.DockerPanel.Tests; + +/// +/// 日志级别识别。 +/// +/// 这块的风险不是"认不出来"(认不出来就不着色,没人受伤),而是**误伤** —— +/// 把一条成功日志染成红色,用户会去查一个不存在的故障。所以这里的重点是 +/// 各种"正文里出现 error 字样但级别不是 error"的行必须保持中性。 +/// +/// +[TestClass] +public class LogLevelTests +{ + [TestMethod] + public void ReadsBracketedLevelsLikeNginx() + { + Assert.AreEqual(LogLevel.Warn, + LogLevels.Detect("[warn] 29#29: *18421 upstream server temporarily disabled")); + Assert.AreEqual(LogLevel.Error, + LogLevels.Detect("[error] 29#29: *18421 connect() failed (111: Connection refused)")); + } + + [TestMethod] + public void ReadsStructuredJsonLevels() + { + Assert.AreEqual(LogLevel.Error, + LogLevels.Detect("""{"level":"error","msg":"upstream unavailable","target":"payments-svc:8443"}""")); + Assert.AreEqual(LogLevel.Info, + LogLevels.Detect("""{"level":"info","msg":"order confirmed","order":"ord_8812"}""")); + // 有些库用 lvl / severity。 + Assert.AreEqual(LogLevel.Warn, LogLevels.Detect("""{"lvl":"warn","msg":"pool near limit"}""")); + Assert.AreEqual(LogLevel.Debug, LogLevels.Detect("""{"severity":"DEBUG","msg":"cache hit"}""")); + } + + [TestMethod] + public void ReadsPlainLeadingLevels() + { + Assert.AreEqual(LogLevel.Error, LogLevels.Detect("ERROR 2026-08-21 09:41:14 something broke")); + Assert.AreEqual(LogLevel.Info, LogLevels.Detect("INFO scheduled job started")); + Assert.AreEqual(LogLevel.Debug, LogLevels.Detect("TRACE entering handler")); + Assert.AreEqual(LogLevel.Error, LogLevels.Detect("FATAL cannot bind port")); + } + + [TestMethod] + public void DoesNotDyeASuccessLineRedJustBecauseItMentionsErrors() + { + // 这是这一层存在的全部理由:一条 200 的访问日志里出现 "error" 这个词, + // 把它染成红色会让人去查一个不存在的故障。 + Assert.AreEqual(LogLevel.None, + LogLevels.Detect("""172.20.0.1 - - "GET /api/v1/errors?page=2 HTTP/1.1" 200 4821 rt=0.043""")); + Assert.AreEqual(LogLevel.None, + LogLevels.Detect("""{"msg":"error budget remaining","pct":99.4,"window":"30d"}""")); + } + + [TestMethod] + public void OnlyLooksAtTheHeadOfTheLine() + { + // 一行很长的正文,末尾才出现 ERROR —— 那多半是被引用的内容,不是这一行的级别。 + var line = new string('x', 200) + " ERROR"; + + Assert.AreEqual(LogLevel.None, LogLevels.Detect(line)); + } + + [TestMethod] + public void UnknownAndEmptyStayNeutral() + { + Assert.AreEqual(LogLevel.None, LogLevels.Detect("")); + Assert.AreEqual(LogLevel.None, LogLevels.Detect("Listening on 0.0.0.0:8080")); + Assert.AreEqual("", LogLevels.Label(LogLevel.None)); + } + + [TestMethod] + public void LabelsAndTonesLineUp() + { + Assert.AreEqual("ERROR", LogLevels.Label(LogLevel.Error)); + Assert.AreEqual(RowTone.Danger, LogLevels.Tone(LogLevel.Error)); + Assert.AreEqual(RowTone.Warn, LogLevels.Tone(LogLevel.Warn)); + // DEBUG 走"闲置"那一档:它比正文更不重要,不该抢注意力。 + Assert.AreEqual(RowTone.Idle, LogLevels.Tone(LogLevel.Debug)); + } +} diff --git a/tests/VelaShell.Plugin.DockerPanel.Tests/MergedLogTests.cs b/tests/VelaShell.Plugin.DockerPanel.Tests/MergedLogTests.cs new file mode 100644 index 0000000..e4e9dc2 --- /dev/null +++ b/tests/VelaShell.Plugin.DockerPanel.Tests/MergedLogTests.cs @@ -0,0 +1,76 @@ +using VelaShell.Plugin.DockerPanel.Ui; +using VelaShell.Plugin.DockerPanel.Ui.Pages; + +namespace VelaShell.Plugin.DockerPanel.Tests; + +/// +/// 合并日志的分行。 +/// +/// 这里盯的是"别把日志本身切坏"。合并日志的服务名着色是个锦上添花的东西, +/// 而切错前缀会让正文少掉一截 —— 用户看到的日志与远端真实吐出来的不一样, +/// 那比没有颜色严重得多。 +/// +/// +[TestClass] +public sealed class MergedLogTests +{ + private static bool Known(string name) => name is "web" or "easilynet-mongo-1"; + + [TestMethod] + public void Split_TakesTheComposePrefixAndTheSpaceAfterTheBar() + { + (var source, var body) = MergedLog.Split("web | listening on :8080", Known); + + Assert.AreEqual("web", source); + Assert.AreEqual("listening on :8080", body); + } + + [TestMethod] + public void Split_KeepsBarsInsideTheBody() + { + // 只切第一根竖线 —— 正文里的竖线是正文的一部分。 + (var source, var body) = MergedLog.Split("web | a | b | c", Known); + + Assert.AreEqual("web", source); + Assert.AreEqual("a | b | c", body); + } + + [TestMethod] + public void Split_LeavesTimestampedFormatsAlone() + { + // 这是一行**没有** compose 前缀、自己带竖线的日志。把 2026-08-23 当成服务名的话, + // 用户看到的正文就少了日期,而且会多出一个莫名其妙的"服务"。 + (var source, var body) = MergedLog.Split("2026-08-23 | INFO | ok", Known); + + Assert.AreEqual("", source); + Assert.AreEqual("2026-08-23 | INFO | ok", body); + } + + [TestMethod] + public void Split_LeavesComposesOwnChatterAlone() + { + (var source, var body) = MergedLog.Split("Attaching to web, db", Known); + + Assert.AreEqual("", source); + Assert.AreEqual("Attaching to web, db", body); + } + + [TestMethod] + public void Split_AcceptsContainerNamesToo() + { + // compose 的前缀有时是服务名、有时是容器名,取决于版本与 container_name。 + (var source, var body) = MergedLog.Split("easilynet-mongo-1 | waiting for connections", Known); + + Assert.AreEqual("easilynet-mongo-1", source); + Assert.AreEqual("waiting for connections", body); + } + + [TestMethod] + public void OutputLine_ReadsTheLevelFromTheBodyAndCallsStderrAnError() + { + Assert.AreEqual(LogLevel.Warn, new OutputLine("00:00", "WARN slow query", false, false).Level); + Assert.AreEqual(LogLevel.None, new OutputLine("00:00", "starting up", false, false).Level); + // 正文里没有 ERROR 字样,但它走的是 stderr —— 那件事本身就是信息。 + Assert.AreEqual(RowTone.Danger, new OutputLine("00:00", "starting up", true, false).Tone); + } +} diff --git a/tests/VelaShell.Plugin.DockerPanel.Tests/ModelTests.cs b/tests/VelaShell.Plugin.DockerPanel.Tests/ModelTests.cs new file mode 100644 index 0000000..886f87e --- /dev/null +++ b/tests/VelaShell.Plugin.DockerPanel.Tests/ModelTests.cs @@ -0,0 +1,222 @@ +using System.Text.Json; +using VelaShell.Plugin.DockerPanel.Docker; +using VelaShell.Plugin.DockerPanel.Ui; + +namespace VelaShell.Plugin.DockerPanel.Tests; + +/// +/// DTO 与显示口径。这里钉的是"面板显示的数字要和用户在 docker 命令行里看到的对得上"。 +/// +[TestClass] +public class ModelTests +{ + [TestMethod] + public void Stats_CpuPercentMatchesDockerStatsFormula() + { + var sample = JsonSerializer.Deserialize(""" + { + "cpu_stats": { "cpu_usage": { "total_usage": 2000 }, "system_cpu_usage": 20000, "online_cpus": 4 }, + "precpu_stats": { "cpu_usage": { "total_usage": 1000 }, "system_cpu_usage": 10000, "online_cpus": 4 } + } + """, DockerJson.Options); + + Assert.IsNotNull(sample); + // (1000 / 10000) * 4 * 100 = 40% + Assert.AreEqual(40, sample.CpuPercent, 0.001); + } + + [TestMethod] + public void Stats_FirstFrameHasNoPreviousSampleAndReportsZero() + { + var sample = JsonSerializer.Deserialize(""" + { "cpu_stats": { "cpu_usage": { "total_usage": 2000 }, "system_cpu_usage": 20000 } } + """, DockerJson.Options); + + // 第一帧的 precpu 是空的 —— 编一个数出来就是一个假的尖峰。 + Assert.AreEqual(0, sample!.CpuPercent); + } + + [TestMethod] + public void Stats_MemoryExcludesPageCache() + { + var sample = JsonSerializer.Deserialize(""" + { "memory_stats": { "usage": 1000, "limit": 4000, "stats": { "inactive_file": 400 } } } + """, DockerJson.Options); + + // 不扣缓存的话,一个只是读过大文件的容器会显示成"内存快满了"。 + Assert.AreEqual(600UL, sample!.MemoryUsed); + } + + [TestMethod] + public void Stats_FallsBackToCgroupV1CacheField() + { + var sample = JsonSerializer.Deserialize(""" + { "memory_stats": { "usage": 1000, "limit": 4000, "stats": { "cache": 250 } } } + """, DockerJson.Options); + + Assert.AreEqual(750UL, sample!.MemoryUsed); + } + + [TestMethod] + public void ContainerSummary_StripsTheLeadingSlashFromNames() + { + var summary = JsonSerializer.Deserialize(""" + { "Id": "abc123", "Names": ["/nginx-proxy"], "State": "running" } + """, DockerJson.Options); + + Assert.AreEqual("nginx-proxy", summary!.Name); + } + + [TestMethod] + public void ContainerSummary_ReadsComposeLabels() + { + var summary = JsonSerializer.Deserialize(""" + { + "Id": "abc", + "Labels": { "com.docker.compose.project": "web-stack", "com.docker.compose.service": "proxy" } + } + """, DockerJson.Options); + + Assert.AreEqual("web-stack", summary!.ComposeProject); + Assert.AreEqual("proxy", summary.ComposeService); + } + + [TestMethod] + public void ImageSummary_DetectsDanglingImages() + { + var dangling = JsonSerializer.Deserialize(""" + { "Id": "sha256:aaa", "RepoTags": [":"] } + """, DockerJson.Options); + var tagged = JsonSerializer.Deserialize(""" + { "Id": "sha256:bbb", "RepoTags": ["nginx:1.27-alpine"] } + """, DockerJson.Options); + + Assert.IsTrue(dangling!.IsDangling); + Assert.IsFalse(tagged!.IsDangling); + Assert.AreEqual("aaa", dangling.ShortId); + } + + [TestMethod] + public void NetworkSummary_KnowsThePredefinedNetworksCannotBeDeleted() + { + foreach (var name in new[] { "bridge", "host", "none" }) + { + Assert.IsTrue(new NetworkSummary { Name = name }.IsPredefined, name); + } + Assert.IsFalse(new NetworkSummary { Name = "web-stack_default" }.IsPredefined); + } + + [TestMethod] + public void DockerJson_ToleratesNumbersEncodedAsStrings() + { + // compose 与几条统计流会把数字写成字符串;严格模式下这会整条反序列化失败。 + var port = JsonSerializer.Deserialize(""" + { "PrivatePort": "80", "PublicPort": "8080", "Type": "tcp" } + """, DockerJson.Options); + + Assert.AreEqual(80, port!.PrivatePort); + Assert.AreEqual(8080, port.PublicPort); + } + + [TestMethod] + public void Ports_DeduplicatesTheIPv4AndIPv6BindingsOfOnePort() + { + var text = Humanize.Ports([ + new() { IP = "0.0.0.0", PrivatePort = 80, PublicPort = 8080, Type = "tcp" }, + new() { IP = "::", PrivatePort = 80, PublicPort = 8080, Type = "tcp" } + ]); + + // daemon 会给两条,但界面上它们是同一件事。 + Assert.AreEqual("8080→80", text); + } + + [TestMethod] + public void Ports_ShowsUnpublishedPortsSeparately() + { + var text = Humanize.Ports([new() { PrivatePort = 8000, PublicPort = 0, Type = "tcp" }]); + Assert.AreEqual("8000/tcp", text); + } + + [TestMethod] + public void Bytes_UsesTheSameDecimalUnitsAsDocker() + { + // 面板显示的 "214 MB" 要和用户在 docker images 里看到的对得上。 + Assert.AreEqual("999 B", Humanize.Bytes(999)); + Assert.AreEqual("1 KB", Humanize.Bytes(1000)); + Assert.AreEqual("214 MB", Humanize.Bytes(214_000_000)); + Assert.AreEqual("—", Humanize.Bytes(-1)); + } + + [TestMethod] + public void Duration_KeepsTwoUnitsAtMost() + { + Assert.AreEqual("3d 4h", Humanize.Duration(TimeSpan.FromHours(76))); + Assert.AreEqual("6h 12m", Humanize.Duration(TimeSpan.FromMinutes(372))); + Assert.AreEqual("48s", Humanize.Duration(TimeSpan.FromSeconds(48))); + } + + [TestMethod] + public void ShortId_HandlesBothPrefixedAndBareIds() + { + Assert.AreEqual("9c4f2e1b7a11", Humanize.ShortId("sha256:9c4f2e1b7a1122334455")); + Assert.AreEqual("a3f2c81b9d4e", Humanize.ShortId("a3f2c81b9d4e5566")); + } + + [TestMethod] + public void RegistryAuth_ResolvesTheRegistryTheSameWayDockerDoes() + { + Assert.AreEqual(RegistryAuthProvider.DockerHub, RegistryAuthProvider.ResolveRegistry("nginx")); + Assert.AreEqual(RegistryAuthProvider.DockerHub, RegistryAuthProvider.ResolveRegistry("library/nginx")); + Assert.AreEqual("ghcr.io", RegistryAuthProvider.ResolveRegistry("ghcr.io/acme/api")); + Assert.AreEqual("registry:5000", RegistryAuthProvider.ResolveRegistry("registry:5000/acme/api")); + Assert.AreEqual("localhost", RegistryAuthProvider.ResolveRegistry("localhost/acme/api")); + } + + [TestMethod] + public void ComposeProject_DerivesTheProjectDirectoryFromTheComposeFile() + { + var project = new ComposeProject("web-stack", "running(3)", "/srv/stacks/web-stack/compose.yaml"); + + Assert.AreEqual("/srv/stacks/web-stack", project.ProjectDirectory); + Assert.AreEqual(3, project.RunningCount); + } + + [TestMethod] + public void DockerEndpoint_LocalPicksThePlatformDefaultSocket() + { + var local = DockerEndpoint.Local("本机 Docker"); + + Assert.AreEqual(DockerEndpointKind.Local, local.Kind); + Assert.AreEqual(OperatingSystem.IsWindows() ? DockerEndpoint.DefaultWindowsPipe : DockerEndpoint.DefaultUnixSocket, + local.SocketPath); + } + + [TestMethod] + public void DockerEndpoint_RemoteDefaultsToTheStandardSocket() + { + var remote = DockerEndpoint.Remote("session-1", "prod-sg-01", "deploy@10.24.8.11"); + + Assert.AreEqual(DockerEndpoint.DefaultUnixSocket, remote.SocketPath); + } + + [TestMethod] + public void ContainerRow_ClassifiesStateIntoTheRightTone() + { + Assert.AreEqual(RowTone.Ok, Row("running", "Up 3 days").Tone); + Assert.AreEqual(RowTone.Danger, Row("running", "Up 3 days (unhealthy)").Tone); + Assert.AreEqual(RowTone.Warn, Row("paused", "Up 2 days (Paused)").Tone); + Assert.AreEqual(RowTone.Idle, Row("exited", "Exited (0) 5 hours ago").Tone); + Assert.AreEqual(RowTone.Danger, Row("exited", "Exited (1) 2 hours ago").Tone); + Assert.AreEqual(RowTone.Busy, Row("restarting", "Restarting (1) 3 seconds ago").Tone); + } + + [TestMethod] + public void ContainerRow_ReadsUptimeOutOfTheDaemonStatusText() + { + Assert.AreEqual("3 days", Row("running", "Up 3 days (healthy)").Uptime); + Assert.AreEqual("退出 (1) 2 hours ago", Row("exited", "Exited (1) 2 hours ago").Uptime); + } + + private static ContainerRow Row(string state, string status) => + new(new() { Id = "abc123def456", Names = ["/x"], State = state, Status = status }); +} diff --git a/tests/VelaShell.Plugin.DockerPanel.Tests/PaletteAndRunCommandTests.cs b/tests/VelaShell.Plugin.DockerPanel.Tests/PaletteAndRunCommandTests.cs new file mode 100644 index 0000000..e596f39 --- /dev/null +++ b/tests/VelaShell.Plugin.DockerPanel.Tests/PaletteAndRunCommandTests.cs @@ -0,0 +1,241 @@ +using VelaShell.Plugin.DockerPanel.Docker; +using VelaShell.Plugin.DockerPanel.Ui; + +namespace VelaShell.Plugin.DockerPanel.Tests; + +/// +/// 命令面板的筛选/排序,以及从 inspect 反推 docker run。 +/// +/// 后者是近似的,所以这里钉的不是"字节级还原",而是那几条**会让人踩坑**的取舍: +/// 镜像自带的环境变量要减掉、停掉的容器也要能推出端口、命名卷不能漏。 +/// +/// +[TestClass] +public class PaletteAndRunCommandTests +{ + // ── 命令面板 ────────────────────────────────────────────────── + + private static CommandPalette Palette(params PaletteEntry[] entries) => new(() => entries); + + private static PaletteEntry Entry(string group, string title, string detail = "") => + new(group, title, detail, "Icon.info", RowTone.Idle, false, () => Task.CompletedTask); + + [TestMethod] + public void OpenCollectsFreshEntriesEveryTime() + { + var collected = 0; + var palette = new CommandPalette(() => + { + collected++; + return [Entry("容器", "nginx")]; + }); + + palette.Open("prod-sg-01"); + palette.CloseCommand.Execute(null); + palette.Open("prod-sg-01"); + + // 不缓存:容器列表随时在变,缓存会让用户对着一个已经不存在的容器按回车。 + Assert.AreEqual(2, collected); + Assert.AreEqual("prod-sg-01", palette.HostName); + } + + [TestMethod] + public void FirstItemOfEachGroupCarriesTheGroupHeader() + { + var palette = Palette( + Entry("动作", "重启 nginx-proxy"), + Entry("动作", "重启 api-gateway"), + Entry("容器", "redis-cache")); + palette.Open("h"); + + Assert.IsTrue(palette.Items[0].ShowGroup); + Assert.AreEqual(2, palette.Items[0].GroupCount); + Assert.IsFalse(palette.Items[1].ShowGroup); + Assert.IsTrue(palette.Items[2].ShowGroup); + Assert.AreEqual(1, palette.Items[2].GroupCount); + } + + [TestMethod] + public void TitleMatchesOutrankDetailMatches() + { + var palette = Palette( + Entry("镜像 / 卷", "pg-data", "卷 · 名字里没有那个词"), + Entry("动作", "restart nginx", "运行中"), + Entry("容器", "some-container", "restart policy: always")); + palette.Open("h"); + + palette.Query = "restart"; + + // 标题前缀命中排最前;只在小字里命中的排最后。 + Assert.AreEqual("restart nginx", palette.Items[0].Title); + Assert.AreEqual("some-container", palette.Items[^1].Title); + } + + [TestMethod] + public void ArrowKeysWrapAround() + { + var palette = Palette(Entry("g", "a"), Entry("g", "b"), Entry("g", "c")); + palette.Open("h"); + + Assert.IsTrue(palette.Items[0].Active); + palette.Move(-1); + // 到顶再往上回到最后一条,比停在那里不动更符合预期。 + Assert.IsTrue(palette.Items[2].Active); + palette.Move(1); + Assert.IsTrue(palette.Items[0].Active); + } + + [TestMethod] + public void TabCompletesWithoutTheEllipsis() + { + var palette = Palette(Entry("面板命令", "清理未使用的镜像…")); + palette.Open("h"); + + palette.Complete(); + + // 省略号代表"还会再问你一次",补进搜索框只会让下一次匹配失败。 + Assert.AreEqual("清理未使用的镜像", palette.Query); + } + + [TestMethod] + public void EscapeClosesWithoutRunningAnything() + { + var ran = false; + var palette = new CommandPalette(() => + [ + new("g", "t", "", "Icon.info", RowTone.Idle, false, () => + { + ran = true; + return Task.CompletedTask; + }) + ]); + palette.Open("h"); + + palette.CloseCommand.Execute(null); + + Assert.IsFalse(palette.IsOpen); + Assert.IsFalse(ran); + } + + [TestMethod] + public void EmptyResultIsReportedSoTheViewCanSaySomething() + { + var palette = Palette(Entry("容器", "nginx")); + palette.Open("h"); + + palette.Query = "没有这个东西"; + + // 一片空白不如一句"没有匹配的命令"。 + Assert.IsTrue(palette.IsEmpty); + Assert.HasCount(0, palette.Items); + } + + // ── docker run 反推 ─────────────────────────────────────────── + + [TestMethod] + public void SubtractsTheImageOwnEnvironment() + { + string[] container = ["PATH=/usr/local/sbin:/usr/bin", "NGINX_VERSION=1.27", "APP_MODE=prod"]; + string[] image = ["PATH=/usr/local/sbin:/usr/bin", "NGINX_VERSION=1.27"]; + + string[] user = [.. RunCommandBuilder.UserEnv(container, image)]; + + // 不减的话命令会拖着镜像里那十几条 PATH / LANG —— 它们不是用户写的。 + Assert.AreSequenceEqual(["APP_MODE=prod"], user); + } + + [TestMethod] + public void KeepsEverythingWhenTheImageIsGone() + { + string[] container = ["A=1", "B=2"]; + + // 镜像被删了但容器还在跑很常见 —— 那就不减,多几条环境变量而已。 + Assert.AreSequenceEqual(container, [.. RunCommandBuilder.UserEnv(container, null)]); + } + + [TestMethod] + public void BuildsFromDeclaredConfigSoStoppedContainersStillWork() + { + var inspect = new ContainerInspect + { + Id = "abc", + Name = "/nginx-proxy", + Config = new() + { + Image = "nginx:1.27-alpine", + Env = ["APP_MODE=prod"], + WorkingDir = "/etc/nginx" + }, + HostConfig = new() + { + RestartPolicy = new() { Name = "unless-stopped" }, + NetworkMode = "web-stack_default", + PortBindings = new() + { + ["80/tcp"] = [new() { HostPort = "8080" }], + ["443/tcp"] = [new() { HostIp = "127.0.0.1", HostPort = "8443" }] + }, + Binds = ["/srv/nginx/conf.d:/etc/nginx/conf.d:ro"] + }, + Mounts = [new() { Type = "volume", Name = "nginx-cache", Destination = "/var/cache/nginx", RW = true }] + }; + + var command = RunCommandBuilder.Build(inspect, null); + + Assert.Contains("--name 'nginx-proxy'", command); + // 端口取自 PortBindings 而不是运行态的 Ports:容器停了 Ports 就是空的, + // 而"这个停掉的容器当初怎么起的"恰恰是最需要这条命令的时候。 + // /tcp 是 -p 的默认协议,不写出来。 + Assert.Contains("-p '8080:80'", command); + Assert.Contains("-p '127.0.0.1:8443:443'", command); + Assert.Contains("-v '/srv/nginx/conf.d:/etc/nginx/conf.d:ro'", command); + // 命名卷走 Mounts,Binds 里没有 —— 漏掉它重建出来的容器会丢数据。 + Assert.Contains("-v 'nginx-cache:/var/cache/nginx'", command); + Assert.Contains("-e 'APP_MODE=prod'", command); + Assert.Contains("--restart unless-stopped", command); + Assert.Contains("--network 'web-stack_default'", command); + Assert.Contains("-w '/etc/nginx'", command); + Assert.EndsWith("'nginx:1.27-alpine'", command); + } + + [TestMethod] + public void KeepsNonDefaultProtocols() + { + var inspect = new ContainerInspect + { + Config = new() { Image = "coredns/coredns:1.11" }, + HostConfig = new() + { + PortBindings = new() { ["53/udp"] = [new() { HostPort = "53" }] } + } + }; + + // udp 不写就变成了 tcp,那是一个跑不起来的 DNS。 + Assert.Contains("-p '53:53/udp'", RunCommandBuilder.Build(inspect, null)); + } + + [TestMethod] + public void SkipsDefaultsThatWouldBeNoise() + { + var inspect = new ContainerInspect + { + Config = new() { Image = "redis:7.4" }, + HostConfig = new() { RestartPolicy = new() { Name = "no" }, NetworkMode = "default" } + }; + + var command = RunCommandBuilder.Build(inspect, null); + + // --restart no 与 --network default 就是不写时的行为,写出来只是噪音。 + Assert.IsFalse(command.Contains("--restart", StringComparison.Ordinal)); + Assert.IsFalse(command.Contains("--network", StringComparison.Ordinal)); + Assert.AreEqual("docker run -d 'redis:7.4'", command); + } + + [TestMethod] + public void CarriesACaveatBecauseTheReconstructionIsApproximate() + { + // Engine 不保存原始命令行,只保存生效后的配置 —— 产物必须说明这一点, + // 而不是假装它可以照抄执行。 + Assert.Contains("近似", RunCommandBuilder.Caveat); + } +} diff --git a/tests/VelaShell.Plugin.DockerPanel.Tests/PanelLogicTests.cs b/tests/VelaShell.Plugin.DockerPanel.Tests/PanelLogicTests.cs new file mode 100644 index 0000000..da8736f --- /dev/null +++ b/tests/VelaShell.Plugin.DockerPanel.Tests/PanelLogicTests.cs @@ -0,0 +1,395 @@ +using Avalonia.Headless; +using VelaShell.Plugin.DockerPanel.Docker; +using VelaShell.Plugin.DockerPanel.Ui; +using VelaShell.Plugin.DockerPanel.Ui.Pages; + +namespace VelaShell.Plugin.DockerPanel.Tests; + +/// +/// 面板逻辑:批量判定、进度聚合、行合并、确认闸门、表单校验。 +/// +/// 这一层没有界面,但界面上最要紧的那几句话("成功 8、失败 2 —— 谁失败了、为什么") +/// 全是它算出来的。 +/// +/// +[TestClass] +public class PanelLogicTests +{ + // 确认闸门那几条必须在一条真的在泵的 UI 线程上跑,理由见 DockerPanelHeadlessApp。 + private static HeadlessUnitTestSession _session = null!; + + [ClassInitialize] + public static void InitSession(TestContext _) => + _session = HeadlessUnitTestSession.GetOrStartForAssembly(typeof(PanelLogicTests).Assembly); + + /// + /// 在 headless UI 线程上跑一段测试体。lambda 必须带返回值 —— + /// 没有 Func<Task> 重载, + /// 写成无返回值会拿到一个从未被等待的 Task<Task>:测试体跑到第一个 await 就 + /// “通过”了,后面的断言失败全部丢失(Redis 面板测试记的是同一条)。 + /// + private static void OnUi(Func body) => + _session.Dispatch(async () => + { + await body(); + return true; + }, CancellationToken.None).GetAwaiter().GetResult(); + + // ── 批量 ────────────────────────────────────────────────── + + [TestMethod] + public async Task Batch_JudgesEachTargetSeparatelyAndKeepsGoing() + { + var result = await BatchRunner.RunAsync( + [("a", "worker-1"), ("b", "postgres-main"), ("c", "worker-2")], + (target, _) => target == "b" + ? throw new DockerApiException(System.Net.HttpStatusCode.Conflict, + "conflict: container is being used by api-gateway") + : Task.CompletedTask, cancellationToken: TestContext.CancellationToken); + + // 一个目标失败不能把后面的目标一起拖下水 —— 界面要报"成功 2、失败 1", + // 而不是把整批说成"操作失败"。 + Assert.AreEqual(2, result.SucceededCount); + Assert.AreEqual(1, result.FailedCount); + Assert.AreEqual("postgres-main", result.Failures.Single().Target); + Assert.Contains("api-gateway", result.Failures.Single().Failure!); + } + + [TestMethod] + public async Task Batch_StopsAfterTheConnectionDies() + { + var attempts = 0; + var result = await BatchRunner.RunAsync( + [("a", "one"), ("b", "two"), ("c", "three")], + (_, _) => + { + attempts++; + throw new DockerUnreachableException("连接断了", DockerUnreachableReason.SessionUnavailable); + }, cancellationToken: TestContext.CancellationToken); + + // 连接没了就没必要继续戳后面的目标 —— 它们只会拿到同一条错误。 + Assert.AreEqual(1, attempts); + Assert.AreEqual(3, result.FailedCount); + Assert.Contains("没有执行", result.Outcomes[2].Failure!); + } + + [TestMethod] + public async Task Batch_ReportsProgressForEveryTarget() + { + List<(int Done, int Total)> progress = []; + await BatchRunner.RunAsync( + [("a", "one"), ("b", "two")], + (_, _) => Task.CompletedTask, + (done, total, _) => progress.Add((done, total)), TestContext.CancellationToken); + + Assert.AreSequenceEqual([(0, 2), (1, 2), (2, 2)], progress); + } + + // ── 拉取进度聚合 ────────────────────────────────────────── + + [TestMethod] + public void PullAggregator_TracksBytesAcrossLayers() + { + var aggregator = new PullAggregator(); + aggregator.Accept(new() { Id = "a", Status = "Downloading", ProgressDetail = new() { Current = 50, Total = 100 } }); + aggregator.Accept(new() { Id = "b", Status = "Downloading", ProgressDetail = new() { Current = 25, Total = 100 } }); + + Assert.AreEqual(2, aggregator.LayerCount); + Assert.AreEqual(75, aggregator.CurrentBytes); + Assert.AreEqual(200, aggregator.TotalBytes); + Assert.AreEqual(0.375, aggregator.Progress, 0.001); + } + + [TestMethod] + public void PullAggregator_DoesNotRegressWhenALayerCompletes() + { + var aggregator = new PullAggregator(); + aggregator.Accept(new() { Id = "a", Status = "Downloading", ProgressDetail = new() { Current = 80, Total = 100 } }); + // "Pull complete" 之后不再有字节明细;沿用上次的 total 并补满, + // 否则进度条会在最后一刻往回跳。 + aggregator.Accept(new() { Id = "a", Status = "Pull complete" }); + + Assert.AreEqual(100, aggregator.CurrentBytes); + Assert.AreEqual(1, aggregator.Progress, 0.001); + } + + [TestMethod] + public void PullAggregator_FallsBackToLayerCountWhenEverythingIsCached() + { + var aggregator = new PullAggregator(); + aggregator.Accept(new() { Id = "a", Status = "Already exists" }); + aggregator.Accept(new() { Id = "b", Status = "Already exists" }); + + // 一次全命中缓存的拉取一个字节都不报;按字节算会显示成 0% 然后直接跳到完成。 + Assert.AreEqual(2, aggregator.ReusedLayers); + Assert.AreEqual(1, aggregator.Progress, 0.001); + } + + [TestMethod] + public void PullAggregator_IgnoresFramesWithoutALayerId() + { + var aggregator = new PullAggregator(); + aggregator.Accept(new() { Status = "Pulling from library/nginx" }); + aggregator.Accept(new() { Status = "Digest: sha256:abc" }); + + Assert.AreEqual(0, aggregator.LayerCount); + } + + // ── 行合并 ──────────────────────────────────────────────── + + private sealed class Item(string id) : ObservableObject + { + public string Id { get; } = id; + public int Version { get; set; } + } + + [TestMethod] + public void Merge_KeepsExistingInstancesSoSelectionSurvivesARefresh() + { + var collection = new KeyedCollection(i => i.Id); + var a = new Item("a"); + var b = new Item("b"); + collection.Merge([a, b], (_, _) => { }); + var keptA = collection[0]; + + collection.Merge([new Item("a"), new Item("b")], (current, incoming) => current.Version++); + + // 简单地 Clear + AddRange 会把选中态、滚动位置与展开状态全部清掉, + // 而这个面板每秒都可能因为一条事件而刷新。 + Assert.AreSame(keptA, collection[0]); + Assert.AreEqual(1, collection[0].Version); + } + + [TestMethod] + public void Merge_AddsRemovesAndReorders() + { + var collection = new KeyedCollection(i => i.Id); + collection.Merge([new Item("a"), new Item("b"), new Item("c")], (_, _) => { }); + + collection.Merge([new Item("c"), new Item("a"), new Item("d")], (_, _) => { }); + + Assert.AreSequenceEqual(["c", "a", "d"], [.. collection.Select(i => i.Id)]); + } + + // ── 确认闸门 ────────────────────────────────────────────── + + private static ConfirmRequest DataLossRequest() => new() + { + Title = "删除卷 pg-data?", + HostName = "prod-sg-01", + ConfirmLabel = "永久删除卷", + Tier = ConfirmTier.DataLoss, + ConfirmWord = "delete" + }; + + [TestMethod] + public void Gate_DataLossTierStaysLockedUntilTheWordIsTypedExactly() => OnUi(() => + { + var gate = new ConfirmGate(); + _ = gate.AskAsync(DataLossRequest()); + + gate.TypedWord = "delet"; + Assert.IsFalse(gate.CanConfirm); + Assert.Contains("还差 1", gate.RemainingHint); + + gate.TypedWord = "DELETE"; + // 大小写不同就是不同 —— 这道闸门的全部意义就是"必须精确地打对"。 + Assert.IsFalse(gate.CanConfirm); + + gate.TypedWord = "delete"; + Assert.IsTrue(gate.CanConfirm); + return Task.CompletedTask; + }); + + [TestMethod] + public void Gate_DestructiveTierNeedsNoTypedWord() => OnUi(() => + { + var gate = new ConfirmGate(); + _ = gate.AskAsync(new() + { + Title = "删除 2 个容器?", + HostName = "prod-sg-01", + ConfirmLabel = "删除" + }); + + Assert.IsTrue(gate.CanConfirm); + Assert.IsFalse(gate.IsDataLoss); + return Task.CompletedTask; + }); + + [TestMethod] + public void Gate_ConfirmAndCancelResolveTheWaiter() => OnUi(async () => + { + var gate = new ConfirmGate(); + var pending = gate.AskAsync(DataLossRequest()); + gate.TypedWord = "delete"; + gate.ConfirmCommand.Execute(null); + Assert.IsTrue(await pending); + + var second = gate.AskAsync(DataLossRequest()); + gate.CancelCommand.Execute(null); + Assert.IsFalse(await second); + }); + + [TestMethod] + public void Gate_RefusesASecondRequestWhileOneIsOpen() => OnUi(async () => + { + var gate = new ConfirmGate(); + var first = gate.AskAsync(DataLossRequest()); + + // 两层确认框叠在一起,用户不可能说清自己在确认哪一个。 + Assert.IsFalse(await gate.AskAsync(DataLossRequest())); + gate.CancelCommand.Execute(null); + Assert.IsFalse(await first); + }); + + // ── 表单校验 ────────────────────────────────────────────── + + [TestMethod] + public void RenameForm_RejectsIllegalNamesAndNoOpRenames() + { + var form = new RenameContainerForm("nginx-proxy", "web-stack"); + Assert.IsFalse(form.Validate(), "改成同一个名字不该放行。"); + + SetText(form, "新名称", "-bad-start"); + Assert.IsFalse(form.Validate()); + + SetText(form, "新名称", "nginx-proxy-2"); + Assert.IsTrue(form.Validate()); + Assert.Contains("web-stack", form.ComposeWarning); + } + + [TestMethod] + public void RunContainerForm_RejectsTheRmPlusRestartPolicyCombination() + { + var form = new RunContainerForm("nginx:1.27-alpine", "", ["bridge"]); + SetToggle(form, "退出即删除 --rm", true); + + // Docker 自己会拒掉这个组合,但等它拒不如现在就说清楚。 + Assert.IsFalse(form.Validate()); + Assert.Contains("互斥", form.FormError!); + } + + [TestMethod] + public void RunContainerForm_ValidatesPortsAndMountTargets() + { + var form = new RunContainerForm("nginx:1.27-alpine", "", ["bridge"]); + form.Ports.Rows.Add(new("99999", "80")); + Assert.IsFalse(form.Validate()); + + form.Ports.Rows.Clear(); + form.Volumes.Rows.Add(new("/srv/data", "relative/path")); + Assert.IsFalse(form.Validate()); + + form.Volumes.Rows.Clear(); + form.Ports.Rows.Add(new("8080", "80")); + form.Volumes.Rows.Add(new("/srv/data", "/data")); + Assert.IsTrue(form.Validate()); + } + + [TestMethod] + public void RunContainerForm_BuildsACreateRequestThatMatchesTheForm() + { + var form = new RunContainerForm("nginx:1.27-alpine", "", ["web-stack_default"]); + form.Ports.Rows.Add(new("8081", "80")); + form.Volumes.Rows.Add(new("/srv/conf", "/etc/nginx/conf.d")); + form.Env.Rows.Add(new("KEY", "value")); + + var request = form.ToRequest(); + + Assert.AreEqual("nginx:1.27-alpine", request.Image); + Assert.AreEqual("8081", request.HostConfig!.PortBindings!["80/tcp"][0].HostPort); + Assert.AreSequenceEqual(["/srv/conf:/etc/nginx/conf.d"], [.. request.HostConfig.Binds!]); + Assert.AreSequenceEqual(["KEY=value"], [.. request.Env!]); + Assert.Contains("-p 8081:80", form.CommandNote); + } + + [TestMethod] + public void SplitArguments_RespectsQuotes() + { + Assert.AreSequenceEqual(["nginx", "-g", "daemon off;"], RunContainerForm.SplitArguments("nginx -g 'daemon off;'")); + } + + [TestMethod] + public void CreateNetworkForm_RequiresASubnetWhenAGatewayIsGiven() + { + var form = new CreateNetworkForm(swarmActive: false); + SetText(form, "名称", "edge-dmz"); + SetText(form, "网关", "172.28.0.1"); + + // 只给网关不给子网,Docker 不知道它属于哪一段。 + Assert.IsFalse(form.Validate()); + + SetText(form, "子网", "172.28.0.0/16"); + Assert.IsTrue(form.Validate()); + } + + [TestMethod] + public void CreateNetworkForm_DisablesOverlayWhenSwarmIsInactive() + { + var form = new CreateNetworkForm(swarmActive: false); + var driver = form.Fields.OfType().Single(f => f.Label == "驱动"); + var overlay = driver.Options.Single(o => o.Value == "overlay"); + + // 直接置灰而不是让用户去撞一条 daemon 的错误。 + Assert.IsFalse(overlay.Enabled); + Assert.Contains("swarm", overlay.DisabledReason); + } + + [TestMethod] + public void ConnectNetworkForm_RefusesOneAliasForManyContainers() + { + var form = new ConnectNetworkForm("web-stack_default", + [ + ("a", "one", "运行中", true, ""), + ("b", "two", "运行中", true, "") + ]); + foreach (var item in form.Containers.Items) + { + item.Selected = true; + } + SetText(form, "网络别名", "db"); + + // 同一个别名指向多个容器,DNS 解析结果就成了随机的。 + Assert.IsFalse(form.Validate()); + } + + [TestMethod] + public void OpenComposeForm_DerivesTheProjectNameFromTheDirectory() + { + var form = new OpenComposeForm(isLocal: false); + SetText(form, "compose 文件路径", "/srv/stacks/web-stack/compose.yaml"); + + Assert.IsTrue(form.Validate()); + Assert.AreEqual("web-stack", form.ProjectName); + } + + [TestMethod] + public void OpenComposeForm_TakesWindowsPathsOnLocalEndpoints() + { + // 本机端点上 compose ls 报回来的就是这种路径 —— 拦掉它等于本机端点用不了这个入口。 + var form = new OpenComposeForm(isLocal: true); + SetText(form, "compose 文件路径", @"D:\stacks\web-stack\compose.yaml"); + + Assert.IsTrue(form.Validate()); + Assert.AreEqual("web-stack", form.ProjectName); + } + + [TestMethod] + public void OpenComposeForm_RejectsRelativePaths() + { + var form = new OpenComposeForm(isLocal: false); + SetText(form, "compose 文件路径", "stacks/web/compose.yaml"); + + // 相对路径会以登录目录为基准 —— 一个安静地打开错项目的 bug。 + Assert.IsFalse(form.Validate()); + } + + private static void SetText(PanelForm form, string label, string value) => + form.Fields.OfType().Single(f => f.Label == label).Value = value; + + private static void SetToggle(PanelForm form, string label, bool value) => + form.Fields.OfType().Single(f => f.Label == label).Value = value; + + public TestContext TestContext { get; set; } +} diff --git a/tests/VelaShell.Plugin.DockerPanel.Tests/SocketProbeTests.cs b/tests/VelaShell.Plugin.DockerPanel.Tests/SocketProbeTests.cs new file mode 100644 index 0000000..6def540 --- /dev/null +++ b/tests/VelaShell.Plugin.DockerPanel.Tests/SocketProbeTests.cs @@ -0,0 +1,109 @@ +using VelaShell.Plugin.DockerPanel.Docker; +using VelaShell.PluginSdk.RemoteExec; + +namespace VelaShell.Plugin.DockerPanel.Tests; + +/// +/// 连不上时的自动诊断。 +/// +/// 这里盯的是"分得出是哪一种"。sshd 打不开通道只回一句笼统的失败,面板要是分不出 +/// "没这个文件"与"你没权限",界面就只能说"自己去终端看看" —— 而这两件事 +/// 要用户做的完全不一样。 +/// +/// +[TestClass] +public sealed class SocketProbeTests +{ + private static readonly DockerEndpoint Remote = + DockerEndpoint.Remote("session-1", "EQ12-FnOS", "joes@192.168.16.5"); + + /// 测试上下文(取消令牌用)。 + public TestContext TestContext { get; set; } = null!; + + [TestMethod] + public async Task Probe_ReadsBackTheAccountAndTheGroupByName() + { + // 真机上的形态:socket 属于 root:docker,而登录账号只在 Users / Administrators 里。 + var exec = new StubExec("DENIED|docker|joes|Users Administrators\n"); + + var result = await SocketProbe.RunAsync(exec, Remote, TestContext.CancellationToken); + + Assert.AreEqual(SocketProbeKind.PermissionDenied, result.Kind); + // 名字要是具体的 —— 界面要把它们直接写进"账号 joes 不在 docker 组里"这句话。 + Assert.AreEqual("joes", result.Account); + Assert.AreEqual("docker", result.Group); + Assert.AreEqual("Users Administrators", result.Groups); + } + + [TestMethod] + public async Task Probe_SeparatesMissingFromDenied() + { + Assert.AreEqual(SocketProbeKind.Missing, + (await SocketProbe.RunAsync(new StubExec("MISSING\n"), Remote, + TestContext.CancellationToken)).Kind); + Assert.AreEqual(SocketProbeKind.Ready, + (await SocketProbe.RunAsync(new StubExec("OK\n"), Remote, + TestContext.CancellationToken)).Kind); + } + + [TestMethod] + public async Task Probe_IgnoresLoginBannersAndReadsTheLastLine() + { + // 登录 shell 会往 stdout 上打欢迎语、fortune、rc 文件的输出 —— 结论在最后一行。 + var exec = new StubExec("Welcome to EQ12-FnOS\nLast login: Sun Aug 23\nOK\n"); + + Assert.AreEqual(SocketProbeKind.Ready, + (await SocketProbe.RunAsync(exec, Remote, TestContext.CancellationToken)).Kind); + } + + [TestMethod] + public async Task Probe_StaysQuietWhenItCannotTell() + { + // 探不出来就是探不出来。硬凑一个答案会把用户引去做一件没用的事, + // 那比"连不上,原因不明"更糟。 + Assert.AreEqual(SocketProbeKind.Unknown, + (await SocketProbe.RunAsync(new StubExec("sh: stat: not found\n"), Remote, + TestContext.CancellationToken)).Kind); + Assert.AreEqual(SocketProbeKind.Unknown, + (await SocketProbe.RunAsync(new ThrowingExec(), Remote, + TestContext.CancellationToken)).Kind); + } + + [TestMethod] + public async Task Probe_AsksAboutTheConfiguredPathNotADefault() + { + var exec = new StubExec("OK\n"); + var custom = DockerEndpoint.Remote("s", "n", "d", "/run/user/1000/docker.sock"); + + await SocketProbe.RunAsync(exec, custom, TestContext.CancellationToken); + + Assert.Contains("/run/user/1000/docker.sock", exec.LastCommand); + } + + private sealed class StubExec(string output) : IRemoteExecApi + { + public string LastCommand { get; private set; } = ""; + + public Task RunAsync(string sessionId, string command, ExecOptions? options = null, + CancellationToken cancellationToken = default) + { + LastCommand = command; + return Task.FromResult(new ExecResult(output) { ExitCode = 0 }); + } + + public Task StreamAsync(string sessionId, string command, ExecStreamOptions? options, + IProgress output, CancellationToken cancellationToken = default) => + Task.FromResult(new ExecStreamResult(0, 0)); + } + + private sealed class ThrowingExec : IRemoteExecApi + { + public Task RunAsync(string sessionId, string command, ExecOptions? options = null, + CancellationToken cancellationToken = default) => + throw new InvalidOperationException("会话没了"); + + public Task StreamAsync(string sessionId, string command, ExecStreamOptions? options, + IProgress output, CancellationToken cancellationToken = default) => + throw new InvalidOperationException("会话没了"); + } +} diff --git a/tests/VelaShell.Plugin.DockerPanel.Tests/SubnetCapacityTests.cs b/tests/VelaShell.Plugin.DockerPanel.Tests/SubnetCapacityTests.cs new file mode 100644 index 0000000..36ec713 --- /dev/null +++ b/tests/VelaShell.Plugin.DockerPanel.Tests/SubnetCapacityTests.cs @@ -0,0 +1,88 @@ +using VelaShell.Plugin.DockerPanel.Ui.Pages; + +namespace VelaShell.Plugin.DockerPanel.Tests; + +/// +/// 子网容量。 +/// +/// 「已分配 3 / 65533」这一行的价值是**提前**看见子网快满 —— 到了才发现就太晚了, +/// 那时候新容器已经起不来。所以数字必须与 docker 实际能分出去的一致,不能是理论总数。 +/// +/// +[TestClass] +public class SubnetCapacityTests +{ + [TestMethod] + public void SubtractsNetworkBroadcastAndGateway() + { + // /16 有 65536 个地址,减掉网络地址、广播地址与 docker 占的 .1 网关 = 65533, + // 与设计稿那一行对得上。 + Assert.AreEqual(65533L, NetworksPageViewModel.SubnetCapacity("172.20.0.0/16")); + Assert.AreEqual(253L, NetworksPageViewModel.SubnetCapacity("192.168.10.0/24")); + } + + [TestMethod] + public void TinySubnetsDoNotGoNegative() + { + // /31 与 /32 在点对点场景里合法,但给不出可用地址 —— 报 0,不报负数。 + Assert.AreEqual(0L, NetworksPageViewModel.SubnetCapacity("10.0.0.0/31")); + Assert.AreEqual(0L, NetworksPageViewModel.SubnetCapacity("10.0.0.1/32")); + } + + [TestMethod] + public void SkipsIpv6BecauseTheNumberWouldBeMeaningless() + { + // 一个 /64 是 1.8×10^19 个地址。显示出来除了占地方没有任何信息。 + Assert.IsNull(NetworksPageViewModel.SubnetCapacity("fd00::/64")); + } + + [TestMethod] + public void UnparseableInputIsSilentlySkipped() + { + Assert.IsNull(NetworksPageViewModel.SubnetCapacity(null)); + Assert.IsNull(NetworksPageViewModel.SubnetCapacity("")); + Assert.IsNull(NetworksPageViewModel.SubnetCapacity("(自动)")); + Assert.IsNull(NetworksPageViewModel.SubnetCapacity("172.20.0.0/abc")); + Assert.IsNull(NetworksPageViewModel.SubnetCapacity("172.20.0.0/99")); + } +} + +/// +/// df 输出的解析。 +/// +/// 「宿主盘 94 GB 中已用 38 GB」是决定要不要清理的那个数,而 Engine API 不给它 —— +/// 只能借一个容器跑 df。各家 df 的表头与列宽都不一样,所以按**位置**取, +/// 不按表头名。 +/// +/// +[TestClass] +public class DfParsingTests +{ + [TestMethod] + public void ReadsTotalAndUsedFromTheDataRow() + { + const string output = """ + Filesystem 1-blocks Used Available Capacity Mounted on + /dev/sda1 100931731456 41231731456 54700000000 43% /var/lib/docker + """; + + (var total, var used) = SystemPageViewModel.ParseDf(output)!.Value; + + Assert.AreEqual(100931731456L, total); + Assert.AreEqual(41231731456L, used); + } + + [TestMethod] + public void SkipsTheHeaderRow() + { + // 表头里的 "1-blocks" 不是数字,不能被当成总量。 + Assert.IsNull(SystemPageViewModel.ParseDf("Filesystem 1-blocks Used Available Capacity Mounted on")); + } + + [TestMethod] + public void ReturnsNullOnGarbage() + { + Assert.IsNull(SystemPageViewModel.ParseDf("")); + Assert.IsNull(SystemPageViewModel.ParseDf("df: /nope: No such file or directory")); + } +} diff --git a/tests/VelaShell.Plugin.DockerPanel.Tests/VelaShell.Plugin.DockerPanel.Tests.csproj b/tests/VelaShell.Plugin.DockerPanel.Tests/VelaShell.Plugin.DockerPanel.Tests.csproj new file mode 100644 index 0000000..df3e5f6 --- /dev/null +++ b/tests/VelaShell.Plugin.DockerPanel.Tests/VelaShell.Plugin.DockerPanel.Tests.csproj @@ -0,0 +1,29 @@ + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + +