diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db008a0..a4eae66 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,11 +113,17 @@ jobs: # 共享程序集绝不能落进插件目录 —— 落了就说明 SDK 包的 exclude=Runtime 链路断了。 # 这条在插件侧再验一遍(工具链仓库的 CI 只在模板生成的空工程上验过), # 因为真实插件才有第三方依赖,才可能把 Avalonia 从别的包里拖回来。 + # + # MicroCom.Runtime 单列一条:它是 Avalonia 的依赖,但**不以 Avalonia 打头** —— + # 既躲过装载器的共享前缀判定(插件会加载自己那一份),也躲过上面那条按名字前缀的体检。 + # 直接引 Avalonia.* 包的插件(DockerPanel 引了 Avalonia.AvaloniaEdit)一旦漏写 + # ExcludeAssets="runtime",它就会静悄悄落进插件目录 —— 2026-09-11 并 DockerPanel 时 + # 实测过这条路:构建 0 错 0 警告,包也照常打出来,只有肉眼逐个文件看才发现。 - name: Assert no shared assemblies leaked shell: pwsh run: | $leaked = Get-ChildItem artifacts/plugins-bundle -Recurse -Filter '*.dll' | - Where-Object { $_.Name -like 'Avalonia*' -or $_.Name -eq 'VelaShell.PluginSdk.dll' } + Where-Object { $_.Name -like 'Avalonia*' -or $_.Name -eq 'VelaShell.PluginSdk.dll' -or $_.Name -eq 'MicroCom.Runtime.dll' } if ($leaked) { throw "Shared assemblies leaked into the plugin bundle: $(($leaked | ForEach-Object FullName) -join ', ')" } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fb77a9a..c9526df 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -148,7 +148,7 @@ jobs: shell: pwsh run: | $leaked = Get-ChildItem artifacts/plugins-bundle -Recurse -Filter '*.dll' | - Where-Object { $_.Name -like 'Avalonia*' -or $_.Name -eq 'VelaShell.PluginSdk.dll' } + Where-Object { $_.Name -like 'Avalonia*' -or $_.Name -eq 'VelaShell.PluginSdk.dll' -or $_.Name -eq 'MicroCom.Runtime.dll' } if ($leaked) { throw "Shared assemblies leaked into the plugin bundle: $(($leaked | ForEach-Object FullName) -join ', ')" } diff --git a/AGENTS.md b/AGENTS.md index 82a95d2..e8cc5b3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,7 +39,7 @@ VelaShell 生态的**全部文档**集中在一个仓库: ## 三、本仓库:velashell-plugins(第一方插件) -Redis / S3 / Telnet / 串口等第一方插件,以 Release 资产 `velashell-plugins-<版本>.zip` 交付。 +Redis / S3 / Telnet / 串口 / Docker 面板等第一方插件,以 Release 资产 `.vpx`(每插件一份,已签名)交付。 ### 构建与打包 @@ -65,6 +65,13 @@ S3 见 [`zh/host/S3协议插件化设计.md`](https://github.com/VelaShellLabs/v Telnet / 串口见 [`zh/host/Telnet与串口可行性调研.md`](https://github.com/VelaShellLabs/velashell-docs/blob/main/zh/host/Telnet与串口可行性调研.md)。 **改了这些插件的设计取舍,要回写对应那篇。** +Docker 面板是个例外:它从独立仓库 `VelaShell.Plugin.DockerPanel` 并进来(2026-09-11), +设计取证留在插件目录自己的 [`README.md`](plugins/VelaShell.Plugin.DockerPanel/README.md) 与 +[`plan.md`](plugins/VelaShell.Plugin.DockerPanel/plan.md)(后者是对着 +`VelaShell.Plugin.DockerPanel.pen` 的 19 个画板逐条核对的完成度快照)。 +这三份是上面第二节说的那类**例外**:它们服务的是"在这个目录里改这个插件", +跟着代码走比搬进 velashell-docs 更有用。 + ### 视觉对齐宿主 插件面板的几何与配色取自宿主的 diff --git a/Directory.Build.targets b/Directory.Build.targets index 9097b36..02d4341 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -34,4 +34,26 @@ Code="VELAP1000" Text="Avalonia 版本与 SDK 锁的不一致:VelaShell.PluginSdk $(VelaSdkPackageVersion) 锁的是 $(VelaSdkPinnedAvaloniaVersion),而本仓库 Directory.Packages.props 里的 Avalonia 是 $(_VelaAvaloniaPin)。把 Directory.Packages.props 里三个 Avalonia* 都改成 $(VelaSdkPinnedAvaloniaVersion)。" /> + + + + + <_VelaAvaloniaEditPin>@(PackageVersion->WithMetadataValue('Identity','Avalonia.AvaloniaEdit')->'%(Version)') + + + diff --git a/Directory.Packages.props b/Directory.Packages.props index 349aac8..8c8b425 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,9 +17,9 @@ - + - + + + + + - + + + + + + + + + + + + + + + + + + + + ^\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/plugins/VelaShell.Plugin.Redis/README.md b/plugins/VelaShell.Plugin.Redis/README.md index e22c7af..0eeb7d7 100644 --- a/plugins/VelaShell.Plugin.Redis/README.md +++ b/plugins/VelaShell.Plugin.Redis/README.md @@ -45,6 +45,12 @@ Redis / Valkey 的完整界面客户端。以**工作台连接类型**(`contribu | 左右分栏 | `GridSplitter` 直接改相邻两列 —— 键名长短因项目而异,固定宽度总有一半人嫌窄一半嫌宽 | | 抽屉上边缘 | **不用 `GridSplitter`**:它只认紧邻的两个行定义,而抽屉与主体之间隔着状态条和页签条两行固定高度,拖出来的会是那两行变形。这里让抽屉那一行是 `Auto`、主体是 `*`,拖拽条是个 `Thumb`,拖动只改 `DrawerHeight` 一个数,主体自动让位。上界按**当前面板高度**算(给主体留 220px),而不是写死 | +会话标签页上的图标是 **Redis 自己的品牌标**(`RedisIcon.cs`,实心、视框 1030,经 +`WorkspaceDescriptor.Icon` 交给宿主)。lucide 里没有 Redis,而拿「数据库」之类的近似字形凑, +换来的是一排标签里认不出哪个是它 —— 图标那一格答的正是"这是哪一种连接"。 +路径数据是原始 SVG 三段 path 的顺序拼接,`RedisIconTests` 把它解析一遍钉住: +宿主那边解析失败是**静默**退回通用插头,不会有任何报错。 + 版式(三百多行样式)单放在 `Ui/RedisPanelStyles.axaml`:面板视图必须先有一条活连接才建得起来, 而样式表是个独立的 avares 资源,于是「样式写坏了没有」可以脱离 Redis 单独验(`RedisPanelStylesTests`)。 diff --git a/plugins/VelaShell.Plugin.Redis/RedisIcon.cs b/plugins/VelaShell.Plugin.Redis/RedisIcon.cs new file mode 100644 index 0000000..96d895a --- /dev/null +++ b/plugins/VelaShell.Plugin.Redis/RedisIcon.cs @@ -0,0 +1,47 @@ +using VelaShell.PluginSdk; + +namespace VelaShell.Plugin.Redis; + +/// +/// Redis 会话标签页上的那个品牌图标。 +/// +/// +/// +/// 为什么是品牌标而不是 lucide 的某个字形。lucide 里没有 Redis。拿「数据库」之类的 +/// 近似字形凑,换来的是一排标签里认不出哪个是 Redis —— 而标签页图标要答的正是"这是哪一种连接"。 +/// +/// +/// 实心、视框 1030,两件都必须报给宿主(见 )。视框不报的话 +/// 宿主按 lucide 的 24 缩放,等于把它放大四十多倍,屏幕上什么都看不见;而用描边去画一个实心 +/// 图形,得到的是它的轮廓线,一团糊。宿主按 min(宽,高)/视框 等比缩放并居中, +/// 原图 1030×1024 那 6 个单位的高差因此只是底部半个像素的留白。 +/// +/// +/// 单独一个文件而不是塞进 :两千多字符的路径数据横在注册与探测 +/// 逻辑中间,读代码的人要翻很久才能翻过去(AI 插件的 AiIcon 同此)。 +/// +/// +internal static class RedisIcon +{ + /// 交给宿主的那份图标:工作台描述符填的就是它。 + public static readonly PluginIcon Tab = PluginIcon.Filled(PathData, ViewBoxSize); + + /// 视框边长(原始 SVG 是 viewBox="0 0 1030 1024")。 + public const double ViewBoxSize = 1030d; + + /// + /// 路径数据:原始 SVG 的三段 path 按原顺序拼接。 + /// + /// 前两段末尾导出工具留下的空移动指令 m0 0 去掉了 —— 画不出任何东西, + /// 却让解析器多收一个空图形。三段的绕向本就相反,顶面那几处镂空在两种填充规则下 + /// 都是洞,不必额外声明 F1。 + /// + /// + public const string PathData = + // 最下面那一层圆柱 + "M584.282353 813.176471c-60.235294 30.117647-96.376471 30.117647-138.541177 6.023529-48.188235-24.094118-445.741176-186.729412-445.741176-186.729412v120.470588c0 12.047059 12.047059 24.094118 42.164706 36.141177 54.211765 24.094118 355.388235 144.564706 403.57647 168.658823 48.188235 24.094118 78.305882 24.094118 138.541177-6.023529 60.235294-30.117647 343.341176-144.564706 397.552941-174.682353 30.117647-12.047059 42.164706-24.094118 42.164706-36.141176v-120.470589s-379.482353 162.635294-439.717647 192.752942z" + + // 中间那一层 + " M584.282353 614.4c-60.235294 30.117647-96.376471 30.117647-138.541177 6.023529-48.188235-24.094118-445.741176-186.729412-445.741176-186.729411v120.470588c0 12.047059 12.047059 24.094118 42.164706 36.141176 54.211765 24.094118 355.388235 144.564706 403.57647 168.658824 48.188235 24.094118 78.305882 24.094118 138.541177-6.02353 60.235294-30.117647 343.341176-144.564706 397.552941-174.682352 30.117647-12.047059 42.164706-24.094118 42.164706-36.141177v-120.470588s-379.482353 162.635294-439.717647 192.752941z" + + // 顶面,以及它上面那几处镂空(星标、圆环、折角) + " M1024 240.941176c0-12.047059-12.047059-18.070588-42.164706-30.117647-54.211765-18.070588-331.294118-132.517647-385.505882-150.588235-54.211765-18.070588-78.305882-18.070588-138.541177 6.02353-60.235294 24.094118-361.411765 138.541176-415.623529 162.635294-24.094118 12.047059-42.164706 18.070588-36.141177 30.117647v120.470588s397.552941 168.658824 445.741177 186.729412c48.188235 24.094118 78.305882 24.094118 138.541176-6.02353 60.235294-30.117647 439.717647-198.776471 439.717647-198.77647l-6.023529-120.470589z m-542.117647-96.37647l-24.094118-42.164706 72.282353 30.117647 66.258824-24.094118-18.070588 42.164706 66.258823 24.094118-90.352941 12.047059-18.070588 48.188235-30.117647-54.211765-102.4-12.047058 78.305882-24.094118z m-307.2 108.423529c0-24.094118 54.211765-48.188235 126.494118-48.188235 72.282353 0 126.494118 24.094118 126.494117 48.188235s-54.211765 48.188235-126.494117 48.188236c-66.258824 0-126.494118-18.070588-126.494118-48.188236z m355.388235 156.611765l-162.635294-66.258824 234.917647-36.141176-72.282353 102.4z m204.8-102.4l-138.541176-54.211765 156.611764-60.235294 138.541177 54.211765-156.611765 60.235294z"; +} diff --git a/plugins/VelaShell.Plugin.Redis/RedisPlugin.cs b/plugins/VelaShell.Plugin.Redis/RedisPlugin.cs index 2d2396f..2a24154 100644 --- a/plugins/VelaShell.Plugin.Redis/RedisPlugin.cs +++ b/plugins/VelaShell.Plugin.Redis/RedisPlugin.cs @@ -151,6 +151,9 @@ private static WorkspaceDescriptor Describe(string pluginId, Loc loc) => { Id = pluginId, DisplayName = "Redis", + // 会话标签页上的图标。不填的话宿主画通用插头 —— 同时开着 Redis、S3、串口三种 + // 插件会话时,三条标签顶着同一个插头,图标那一格等于白占。路径数据见 RedisIcon。 + Icon = RedisIcon.Tab, DefaultPort = 6379, HostLabel = loc["Redis_Host"], HostPlaceholder = "127.0.0.1", diff --git a/plugins/VelaShell.Plugin.S3/README.md b/plugins/VelaShell.Plugin.S3/README.md index 73ffda2..87ade37 100644 --- a/plugins/VelaShell.Plugin.S3/README.md +++ b/plugins/VelaShell.Plugin.S3/README.md @@ -12,6 +12,7 @@ S3 兼容对象存储的官方插件:Amazon S3、MinIO、Ceph RGW、阿里云 OS | 文件管理器 | 实现 SDK 的 `IProtocolFileSystem`,把平的键空间翻译成目录树;宿主的双栏浏览、传输队列、限速、拖放、冲突策略**零改动**复用 | | 桶管理器 | 概览 + 对象版本 + 未完成分片上传 + **22 项桶配置**(表单 / JSON 双形态编辑器) | | 对象检视器 | 详情 / 标签 / 权限 / 保留与合法保留 / 存储与加密 / S3 Select / 预签名 URL | +| 标签页图标 | lucide `cloud`(`S3Icon`),会话标签页与两扇工具窗共用。**不用存储那一类字形** —— 宿主给 SFTP / FTP 的标签画的正是 `hard-drive`,而三者并排躺在同一条标签条上 | S3 协议的 116 个操作里,除 4 个客户端无法或不应调用的(见设计文档 §九)之外全部可达。 @@ -24,6 +25,7 @@ S3 协议的 116 个操作里,除 4 个客户端无法或不应调用的(见设 | `S3ManagementService.cs` | 文件管理器之外的近百个操作;与文件系统共用同一条会话与同一个客户端 | | `S3ObjectPath.cs` | 路径 ↔「桶 + 键」的双向翻译 —— 整个后端都架在这层映射上 | | `S3Interop.cs` | AWSSDK 异常 → `VelaS3*` 异常族。**AWSSDK 的类型不越过插件边界** | +| `S3Icon.cs` | 标签页/标题栏图标的路径数据(lucide `cloud`) | | `S3ConfigKind.cs` | 22 项桶配置的枚举与元数据表;桶管理器的导航由它驱动 | | `Ui/` | 两个面板(不引 ReactiveUI,理由见设计文档 §5.3) | | `ThrottledStream.cs` | 宿主同名实现的副本 —— 限速上限仍由宿主统一给出 | diff --git a/plugins/VelaShell.Plugin.S3/S3ActionHandler.cs b/plugins/VelaShell.Plugin.S3/S3ActionHandler.cs index 3e6786f..396b718 100644 --- a/plugins/VelaShell.Plugin.S3/S3ActionHandler.cs +++ b/plugins/VelaShell.Plugin.S3/S3ActionHandler.cs @@ -100,6 +100,10 @@ private async Task OpenPanelAsync(string token, string title, int width, int hei new PanelOptions { Title = title, + // 标题栏上的图标:与会话标签页同一个实例 —— 桶管理器与对象检视器是同一个插件 + // 开的两扇工具窗,认的是「这扇窗属于哪个插件」。宿主若还不给窗口画图标, + // 这一行也只是被忽略,不会出错。 + Icon = S3Icon.Cloud, // 独立窗口而不是停靠标签页:这两扇是「对着某个对象/桶做一串操作」的工具窗, // 用户通常要一边看文件列表一边改配置,占掉一个标签位反而碍事。 DisplayMode = PanelDisplayMode.Window, diff --git a/plugins/VelaShell.Plugin.S3/S3Icon.cs b/plugins/VelaShell.Plugin.S3/S3Icon.cs new file mode 100644 index 0000000..a12c764 --- /dev/null +++ b/plugins/VelaShell.Plugin.S3/S3Icon.cs @@ -0,0 +1,29 @@ +using VelaShell.PluginSdk; + +namespace VelaShell.Plugin.S3; + +/// +/// S3 的那个图标:会话标签页、桶管理器与对象检视器两扇工具窗,三处共用同一个实例。 +/// +/// +/// +/// 三处共用是刻意的 —— 标签条与标题栏上认的都是「这一扇属于哪个插件」,分开画只会让 +/// 同一个插件的三个界面看着像三个来路。 +/// +/// +/// 为什么是云而不是存储那一类字形。宿主给 SFTP / FTP 的标签画的正是 hard-drive, +/// 而 S3 的标签与它们并排躺在同一条标签条上(三者都是双栏文件浏览器)。再拿硬盘、机箱 +/// 一类的字形画 S3,一排标签就只能靠读文字来分了。云答的是「文件在网那头的对象存储里」, +/// 这正是它与前两者的区别 —— 至于自建的 MinIO / Ceph 算不算“云”,对象存储这套模型本就 +/// 是云存储带起来的,用户的心智模型在这一边。 +/// +/// 描边、视框 24:lucide 那套字形的通用规格,报成实心会把它填成一团色块。 +/// +internal static class S3Icon +{ + /// 交给宿主的那份图标(lucide cloud)。 + public static readonly PluginIcon Cloud = PluginIcon.Stroked(PathData); + + /// lucide cloud 的路径数据。 + public const string PathData = "M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z"; +} diff --git a/plugins/VelaShell.Plugin.S3/S3Plugin.cs b/plugins/VelaShell.Plugin.S3/S3Plugin.cs index df3267d..b30b9ce 100644 --- a/plugins/VelaShell.Plugin.S3/S3Plugin.cs +++ b/plugins/VelaShell.Plugin.S3/S3Plugin.cs @@ -79,6 +79,9 @@ private static ProtocolDescriptor BuildDescriptor(IPluginContext context) { Id = context.PluginId, DisplayName = "S3", + // 会话标签页上的图标。不填的话宿主画通用插头 —— 而 S3 的标签与 SFTP / FTP 的 + // 并排躺在同一条标签条上,那一格不说清是哪种连接就等于白占。取舍见 S3Icon。 + Icon = S3Icon.Cloud, DefaultPort = S3Settings.DefaultPort, HostLabel = loc["S3_Endpoint"], HostPlaceholder = "s3.amazonaws.com", diff --git a/plugins/VelaShell.Plugin.Serial/README.md b/plugins/VelaShell.Plugin.Serial/README.md index 9ee2d78..6f99307 100644 --- a/plugins/VelaShell.Plugin.Serial/README.md +++ b/plugins/VelaShell.Plugin.Serial/README.md @@ -13,6 +13,11 @@ Break 与 DTR/RTS)。 | 激活时机 | `onProtocol:velashell.serial` —— 用户点到串口页签才装载 | | 依赖 | `System.IO.Ports`(MIT) | | 最低 SDK | 1.5.0(用到 `NoEndpoint` / `DynamicChoice` / `HostKind`) | +| 标签页图标 | lucide `usb-c-port`(`ProtocolDescriptor.Icon`,描边、视框 24) | + +图标与 Telnet 插件的 `ethernet-port` 成对:一个串口一个网口,答的都是"插的是哪一种口"。 +终端字形(`square-terminal`)归宿主的 SSH 标签,再拿它画串口就分不出来了。 +不自报图标则是所有插件共用的通用插头。 ## 连接表单 diff --git a/plugins/VelaShell.Plugin.Serial/SerialPlugin.cs b/plugins/VelaShell.Plugin.Serial/SerialPlugin.cs index ddc0174..807c213 100644 --- a/plugins/VelaShell.Plugin.Serial/SerialPlugin.cs +++ b/plugins/VelaShell.Plugin.Serial/SerialPlugin.cs @@ -127,6 +127,11 @@ private static ProtocolDescriptor BuildDescriptor(IPluginContext context) { Id = context.PluginId, DisplayName = loc["Serial_DisplayName"], + // 会话标签页上的图标:lucide 的 usb-c-port,描边、视框 24(那套字形的通用规格)。 + // 认的是「插的是哪一种口」—— 与 Telnet 的以太网口图标正好成对,而终端字形 + // (square-terminal)已经被宿主的 SSH 标签占着,再拿它画串口就分不出来了。 + // 不填则是通用插头,与另外几个插件的标签撞在一起。 + Icon = PluginIcon.Stroked("M6 12h12 M6 8h12a4 4 0 0 1 0 8H6a4 4 0 0 1 0-8Z"), // NoEndpoint:收起"端口"那一栏(串口的目标不是 host:port)。 // NoCredentials:串口没有协议级凭据 —— 登录发生在带内(设备自己打印 login:)。 Features = ProtocolFeatures.NoEndpoint | ProtocolFeatures.NoCredentials, diff --git a/plugins/VelaShell.Plugin.Telnet/README.md b/plugins/VelaShell.Plugin.Telnet/README.md index 7783444..cebaaa7 100644 --- a/plugins/VelaShell.Plugin.Telnet/README.md +++ b/plugins/VelaShell.Plugin.Telnet/README.md @@ -12,6 +12,11 @@ VT 引擎、回滚、搜索、会话日志、会话录制、ZMODEM 全部是宿 | 装载模式 | 进程内(协议能力的硬性要求) | | 激活时机 | `onProtocol:velashell.telnet` —— 用户点到 Telnet 页签才装载 | | 依赖 | 无(只用 `TcpClient` + SDK) | +| 标签页图标 | lucide `ethernet-port`(`ProtocolDescriptor.Icon`,描边、视框 24) | + +图标**刻意不用终端字形**:宿主给 SSH 标签画的就是 `square-terminal`,而标签上的图标只有 +12–16px,两个终端框并排等于没分。以太网口答的是"这条终端从哪儿接进去",与串口插件的 +`usb-c-port` 成对 —— 一个网口一个串口,一眼分得开。不自报图标则是所有插件共用的通用插头。 ## 为什么自己实现协议 diff --git a/plugins/VelaShell.Plugin.Telnet/TelnetPlugin.cs b/plugins/VelaShell.Plugin.Telnet/TelnetPlugin.cs index 6f08d04..7bd8fe8 100644 --- a/plugins/VelaShell.Plugin.Telnet/TelnetPlugin.cs +++ b/plugins/VelaShell.Plugin.Telnet/TelnetPlugin.cs @@ -69,6 +69,12 @@ private static ProtocolDescriptor BuildDescriptor(IPluginContext context) { Id = context.PluginId, DisplayName = "Telnet", + // 会话标签页上的图标:lucide 的 ethernet-port,描边、视框 24。 + // **刻意不用终端字形**:宿主的 SSH 标签画的就是 square-terminal,而标签上的图标 + // 只有 12–16px —— 两个终端框摆在一起等于没分。以太网口答的是「这条终端从哪儿接进去」, + // 与串口的 usb-c-port 成对:一个网口,一个串口,一眼分得开。 + Icon = PluginIcon.Stroked( + "M15 20l3-3h2a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h2l3 3Z M6 8v2 M10 8v2 M14 8v2 M18 8v2"), DefaultPort = 23, HostLabel = loc["Telnet_Host"], HostPlaceholder = loc["Telnet_HostPlaceholder"], 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 + + + + + + + + + + + + + + + + + + + + diff --git a/tests/VelaShell.Plugin.Redis.Tests/RedisIconTests.cs b/tests/VelaShell.Plugin.Redis.Tests/RedisIconTests.cs new file mode 100644 index 0000000..09e6216 --- /dev/null +++ b/tests/VelaShell.Plugin.Redis.Tests/RedisIconTests.cs @@ -0,0 +1,63 @@ +using Avalonia; +using Avalonia.Headless; +using Avalonia.Media; + +namespace VelaShell.Plugin.Redis.Tests; + +/// +/// 标签页图标那串路径数据:必须真的解析得出几何,而且撑满它自报的视框。 +/// +/// 这一层守的是一种**不会报错的失败**:宿主拿到插件给的路径是 Geometry.Parse 一把, +/// 解析不出来就当作没给、退回通用插头 —— 于是路径里打错一个字符,编译过、测试过、 +/// 装载过,只是 Redis 标签上悄悄换成了插头。没人盯着标签条看就发现不了。 +/// +/// +/// 与 同一条口径:不需要 127.0.0.1:6379, +/// 因此在任何机器上都**无条件跑得起来**。 +/// +/// +/// ⚠️ 这里只验得了"解析得出、位置对",验不了"顶面那几处镂空还是不是洞" —— +/// headless 宿主(UseHeadlessDrawing)的 FillContains 给的结果与真实几何对不上 +/// (整行整行地报 true,连视框外的点也报)。镂空那件事是另一条路核的:三段 path 的绕向相反, +/// 因此 evenodd 与 nonzero 两种填充规则画出来一模一样,不必在路径上声明 F0 / F1。 +/// +/// +[TestClass] +[TestCategory("Plugins")] +public sealed class RedisIconTests +{ + private static HeadlessUnitTestSession _session = null!; + + [ClassInitialize] + public static void Init(TestContext _) => + // 几何要平台后端才建得起来(走的是渲染接口),所以这组也得进 headless 会话。 + _session = HeadlessUnitTestSession.GetOrStartForAssembly(typeof(RedisIconTests).Assembly); + + private static void OnUi(Action body) => + _session.Dispatch(() => + { + body(); + return true; + }, CancellationToken.None).GetAwaiter().GetResult(); + + /// 路径解析得出几何,撑满自报的视框,且不越界。 + [TestMethod] + public void PathData_ParsesAndFillsItsOwnViewBox() => + OnUi(() => + { + // 解析不出来会抛(宿主那边则是吞掉、退回通用插头)—— 这一行本身就是一半的验收。 + Rect bounds = Geometry.Parse(RedisIcon.PathData).Bounds; + + // 三段 path 都拼上了:横向正好撑满 1030,纵向 940 上下(原图上下各留一点白)。 + // 漏掉一段不会解析失败,只会让图形缺一层 —— 而缺了圆柱的 Redis 标画出来仍是个图标, + // 只是不再是那个标,这种错肉眼之外没有别的地方会红。 + Assert.AreEqual(RedisIcon.ViewBoxSize, bounds.Width, 1d, $"实际 {bounds}"); + Assert.AreEqual(940d, bounds.Height, 2d, $"实际 {bounds}"); + // 落在视框内:宿主按 min(宽,高)/视框 缩放后**不裁剪**,越界的部分会画到相邻标签上。 + // 容 1 个单位(0.1%)的导出误差 —— 原图右边界就恰好探出 0.024。 + Assert.IsGreaterThanOrEqualTo(-1d, bounds.X, $"实际 {bounds}"); + Assert.IsGreaterThanOrEqualTo(0d, bounds.Y, $"实际 {bounds}"); + Assert.IsLessThanOrEqualTo(RedisIcon.ViewBoxSize + 1d, bounds.Right, $"实际 {bounds}"); + Assert.IsLessThanOrEqualTo(RedisIcon.ViewBoxSize, bounds.Bottom, $"实际 {bounds}"); + }); +} diff --git a/tests/VelaShell.Plugin.Redis.Tests/RedisPluginActivationTests.cs b/tests/VelaShell.Plugin.Redis.Tests/RedisPluginActivationTests.cs index ae7d21c..00ab5fd 100644 --- a/tests/VelaShell.Plugin.Redis.Tests/RedisPluginActivationTests.cs +++ b/tests/VelaShell.Plugin.Redis.Tests/RedisPluginActivationTests.cs @@ -1,3 +1,4 @@ +using VelaShell.PluginSdk; using VelaShell.PluginSdk.Protocols; using VelaShell.PluginSdk.Testing; using VelaShell.PluginSdk.Workspaces; @@ -61,6 +62,24 @@ public async Task Activate_PointsThumbprintKeyAtARealHiddenField() Assert.IsTrue(field.IsHidden); } + [TestMethod] + public async Task Activate_GivesTheSessionTabTheRedisBrandMark() + { + using TestPluginContext context = NewContext(); + var plugin = new RedisPlugin(); + + await plugin.ActivateAsync(context, CancellationToken.None); + + PluginIcon? icon = context.RecordingWorkspaces.Registered.Single().Icon; + Assert.IsNotNull(icon, "不自报图标,标签上就是所有插件共用的那个通用插头。"); + // 两位一起报才画得对:实心图形被 2px 圆头画笔描边会变成一圈轮廓线, + // 而视框留在 lucide 的 24 则等于把这张 1030 的图放大四十多倍,屏幕上什么也看不见。 + Assert.IsTrue(icon.IsFilled, "品牌标是实心的。"); + Assert.AreEqual(1030d, icon.ViewBoxSize, "原始 SVG 的 viewBox 是 0 0 1030 1024。"); + // 路径本身在 RedisIconTests 里解析一遍:那才是"画出来是不是那个标"的验收。 + Assert.AreEqual(RedisIcon.PathData, icon.PathData); + } + [TestMethod] public async Task Activate_LocalizesLabels() { diff --git a/tests/VelaShell.Plugin.S3.Tests/S3PluginActivationTests.cs b/tests/VelaShell.Plugin.S3.Tests/S3PluginActivationTests.cs new file mode 100644 index 0000000..845e8e6 --- /dev/null +++ b/tests/VelaShell.Plugin.S3.Tests/S3PluginActivationTests.cs @@ -0,0 +1,58 @@ +using VelaShell.PluginSdk; +using VelaShell.PluginSdk.Protocols; +using VelaShell.PluginSdk.Testing; + +namespace VelaShell.Plugin.S3.Tests; + +/// +/// 插件激活:注册的协议描述本身就是产品的一部分 —— 它决定连接对话框长什么样、 +/// 会话标签页画什么图标。这里按 SDK 的测试替身把它取出来验。 +/// +[TestClass] +public sealed class S3PluginActivationTests +{ + private static TestPluginContext NewContext(string locale = "zh-Hans") + { + var context = new TestPluginContext { PluginId = "velashell.s3" }; + context.HostInfo.Locale = locale; + context.RecordingProtocols.PluginId = "velashell.s3"; + return context; + } + + private static async Task ActivateAsync(TestPluginContext context) + { + var plugin = new S3Plugin(); + await plugin.ActivateAsync(context, CancellationToken.None); + return context.RecordingProtocols.Registered.Single(); + } + + [TestMethod] + public async Task Activate_RegistersAFileProtocolUnderThePluginId() + { + using TestPluginContext context = NewContext(); + + ProtocolDescriptor descriptor = await ActivateAsync(context); + + Assert.AreEqual("velashell.s3", descriptor.Id); + Assert.AreEqual("S3", descriptor.DisplayName); + // 文件协议,不是终端协议:标签里是双栏浏览器,宿主据此路由。 + Assert.IsNotNull(context.RecordingProtocols.GetFileSystem("velashell.s3")); + } + + [TestMethod] + public async Task Activate_GivesTheSessionTabTheCloudGlyph() + { + using TestPluginContext context = NewContext(); + + ProtocolDescriptor descriptor = await ActivateAsync(context); + + PluginIcon? icon = descriptor.Icon; + Assert.IsNotNull(icon, "不自报图标,标签上就是所有插件共用的那个通用插头。"); + // lucide 那套的规格:描边、视框 24。报成实心会把这个描边字形填成一团色块。 + Assert.IsFalse(icon.IsFilled); + Assert.AreEqual(24d, icon.ViewBoxSize); + // 刻意不是存储那一类字形:宿主给 SFTP / FTP 画的正是 hard-drive,而这三种标签并排 + // 躺在同一条标签条上。这一条写成断言,免得日后有人"顺手统一"成硬盘图标。 + Assert.AreEqual("M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z", icon.PathData); + } +} diff --git a/tests/VelaShell.Plugin.Serial.Tests/SerialPluginActivationTests.cs b/tests/VelaShell.Plugin.Serial.Tests/SerialPluginActivationTests.cs index 8fe65c5..165f57b 100644 --- a/tests/VelaShell.Plugin.Serial.Tests/SerialPluginActivationTests.cs +++ b/tests/VelaShell.Plugin.Serial.Tests/SerialPluginActivationTests.cs @@ -1,3 +1,4 @@ +using VelaShell.PluginSdk; using VelaShell.PluginSdk.Protocols; using VelaShell.PluginSdk.Testing; @@ -80,6 +81,21 @@ public async Task Activate_LetsTheUserTypeANonStandardBaudRate() Assert.IsFalse(baud.IsAdvanced, "波特率是'连不连得上'的参数,不该收进高级选项"); } + [TestMethod] + public async Task Activate_GivesTheSessionTabTheUsbCPortGlyph() + { + using TestPluginContext context = NewContext(); + + ProtocolDescriptor descriptor = await ActivateAsync(context); + + PluginIcon? icon = descriptor.Icon; + Assert.IsNotNull(icon, "不自报图标,标签上就是所有插件共用的那个通用插头。"); + // lucide 那套的规格:描边、视框 24。报成实心会把这个描边字形填成一团色块。 + Assert.IsFalse(icon.IsFilled); + Assert.AreEqual(24d, icon.ViewBoxSize); + Assert.AreEqual("M6 12h12 M6 8h12a4 4 0 0 1 0 8H6a4 4 0 0 1 0-8Z", icon.PathData); + } + [TestMethod] public async Task Activate_KeepsTheConnectOrNotFieldsOutOfTheAdvancedSection() { diff --git a/tests/VelaShell.Plugin.Telnet.Tests/TelnetPluginActivationTests.cs b/tests/VelaShell.Plugin.Telnet.Tests/TelnetPluginActivationTests.cs new file mode 100644 index 0000000..ae8935b --- /dev/null +++ b/tests/VelaShell.Plugin.Telnet.Tests/TelnetPluginActivationTests.cs @@ -0,0 +1,59 @@ +using VelaShell.PluginSdk; +using VelaShell.PluginSdk.Protocols; +using VelaShell.PluginSdk.Testing; + +namespace VelaShell.Plugin.Telnet.Tests; + +/// +/// 插件激活:注册的协议描述本身就是产品的一部分 —— 它决定连接对话框长什么样、 +/// 会话标签页画什么图标。这里按 SDK 的测试替身把它取出来验。 +/// +[TestClass] +public sealed class TelnetPluginActivationTests +{ + private static TestPluginContext NewContext(string locale = "zh-Hans") + { + var context = new TestPluginContext { PluginId = "velashell.telnet" }; + context.HostInfo.Locale = locale; + context.RecordingProtocols.PluginId = "velashell.telnet"; + return context; + } + + private static async Task ActivateAsync(TestPluginContext context) + { + var plugin = new TelnetPlugin(); + await plugin.ActivateAsync(context, CancellationToken.None); + return context.RecordingProtocols.Registered.Single(); + } + + [TestMethod] + public async Task Activate_RegistersATerminalProtocolUnderThePluginId() + { + using TestPluginContext context = NewContext(); + + ProtocolDescriptor descriptor = await ActivateAsync(context); + + Assert.AreEqual("velashell.telnet", descriptor.Id); + Assert.AreEqual(23, descriptor.DefaultPort); + Assert.IsNotNull(context.RecordingProtocols.GetTerminal("velashell.telnet")); + } + + [TestMethod] + public async Task Activate_GivesTheSessionTabTheEthernetPortGlyph() + { + using TestPluginContext context = NewContext(); + + ProtocolDescriptor descriptor = await ActivateAsync(context); + + PluginIcon? icon = descriptor.Icon; + Assert.IsNotNull(icon, "不自报图标,标签上就是所有插件共用的那个通用插头。"); + // lucide 那套的规格:描边、视框 24。报成实心会把这个描边字形填成一团色块。 + Assert.IsFalse(icon.IsFilled); + Assert.AreEqual(24d, icon.ViewBoxSize); + // 刻意不是终端字形:宿主的 SSH 标签画的就是 square-terminal,而标签上只有 12–16px, + // 两个终端框摆在一起等于没分。这一条写成断言,免得日后有人"顺手统一"成终端图标。 + Assert.AreEqual( + "M15 20l3-3h2a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h2l3 3Z M6 8v2 M10 8v2 M14 8v2 M18 8v2", + icon.PathData); + } +}