@linxin666/dsh-ssh 0.1.20 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # node scripts/verify-docs.mjs --write <dir>
5
- README.md: 0ec42622377937f15e508d4b28a95688f511f5e5
6
- README.zh.md: 8a5de44f200457cef7d01c94162b8bc3a5af1c5e
5
+ README.md: 4ea116d416ac311ffde1f1052d9857e46b9c4849
6
+ README.zh.md: d5bec45d8054b999d24832a4a8d103269b2c816d
package/README.md CHANGED
@@ -8,7 +8,7 @@ Built on the capability list of [badseal/ssh-skill](https://github.com/badseal/s
8
8
 
9
9
  | Capability | Description |
10
10
  | --- | --- |
11
- | Host management | CRUD, search, connection test; config stored in `~/.dsh/dsh-ssh.json`; supports key / password auth, passphrase keys, ProxyJump jump hosts (multi-level) |
11
+ | Host management | CRUD, search, connection test; collapsible grouping by environment / tags with per-group batch test; config stored in `~/.dsh/dsh-ssh.json`; supports key / password auth, passphrase keys, ProxyJump jump hosts (multi-level) |
12
12
  | Config import | One-click parse of a standard `~/.ssh/config` (Host/HostName/User/Port/IdentityFile/ProxyJump etc.); existing aliases are skipped |
13
13
  | Persistent connection pool | Reuses a long-lived connection per host (opposite of the ssh-skill daemon), automatically disconnects after 30 minutes idle, auto-reconnects on disconnect (up to 3 times) |
14
14
  | Command execution | exec with a timeout (default 60s), stdout/stderr separated, output truncation guard (2MB) |
package/README.zh.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  | 能力 | 说明 |
10
10
  | --- | --- |
11
- | 主机管理 | 增删改查、搜索、连接测试;配置存 `~/.dsh/dsh-ssh.json`;支持密钥 / 密码认证、passphrase 密钥、ProxyJump 跳板机(多级) |
11
+ | 主机管理 | 增删改查、搜索、连接测试;支持按环境 / 标签分组折叠与组内批量测试;配置存 `~/.dsh/dsh-ssh.json`;支持密钥 / 密码认证、passphrase 密钥、ProxyJump 跳板机(多级) |
12
12
  | 配置导入 | 一键解析标准 `~/.ssh/config`(Host/HostName/User/Port/IdentityFile/ProxyJump 等),已有别名自动跳过 |
13
13
  | 持久连接池 | 每台主机复用长连接(对应 ssh-skill 的守护进程),空闲 30 分钟自动断开,断线自动重连(最多 3 次) |
14
14
  | 命令执行 | exec 带超时(默认 60s),stdout/stderr 分离,输出截断保护(2MB) |
package/lib/client.js CHANGED
@@ -323,6 +323,14 @@ window.__ModuleLoader__.load({
323
323
  "hosts.add": "新增主机",
324
324
  "hosts.import": "导入 ~/.ssh/config",
325
325
  "hosts.search": "搜索别名 / 描述 / 标签…",
326
+ "hosts.groupBy.label": "分组方式",
327
+ "hosts.groupBy.none": "不分组",
328
+ "hosts.groupBy.environment": "按环境分组",
329
+ "hosts.groupBy.tags": "按标签分组",
330
+ "hosts.group.ungrouped": "未设置环境",
331
+ "hosts.group.noTags": "无标签",
332
+ "hosts.group.count": "{count} 台",
333
+ "hosts.group.testAll": "测试全部",
326
334
  "hosts.col.alias": "别名",
327
335
  "hosts.col.host": "主机",
328
336
  "hosts.col.user": "用户",
@@ -452,6 +460,14 @@ window.__ModuleLoader__.load({
452
460
  "hosts.add": "Add host",
453
461
  "hosts.import": "Import ~/.ssh/config",
454
462
  "hosts.search": "Search alias / description / tags…",
463
+ "hosts.groupBy.label": "Group by",
464
+ "hosts.groupBy.none": "No grouping",
465
+ "hosts.groupBy.environment": "Group by environment",
466
+ "hosts.groupBy.tags": "Group by tags",
467
+ "hosts.group.ungrouped": "No environment",
468
+ "hosts.group.noTags": "No tags",
469
+ "hosts.group.count": "{count} hosts",
470
+ "hosts.group.testAll": "Test all",
455
471
  "hosts.col.alias": "Alias",
456
472
  "hosts.col.host": "Host",
457
473
  "hosts.col.user": "User",
@@ -595,7 +611,7 @@ window.__ModuleLoader__.load({
595
611
  }
596
612
  //#endregion
597
613
  //#region \0dsh-css:packages/dsh-ssh/src/client/panel/panel.module.css.mjs
598
- const css = "[data-pane=conversation],[class*=centerCol]{position:relative}[data-dsh-ssh-view]{z-index:60;background:var(--dsw-alias-bg-base);display:none;position:absolute;inset:0}html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [data-dsh-ssh-view]{display:block}html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [data-pane=conversation]>:not([data-dsh-ssh-view]),html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [class*=centerCol]>:not([data-dsh-ssh-view]){display:none!important}.mL8Uca_entry{width:100%;height:32px;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:8px;align-items:center;gap:8px;padding:0 12px;font-size:13px;display:flex}.mL8Uca_entry:hover{background:var(--dsw-specific-sidebar-nav-item-hover);color:var(--dsw-alias-label-primary)}.mL8Uca_entry[data-active]{background:var(--dsw-specific-sidebar-nav-item-active);color:var(--dsw-alias-label-primary);font-weight:600}.mL8Uca_entryIcon{flex:none;justify-content:center;align-items:center;display:inline-flex}.mL8Uca_entryLabel{text-overflow:ellipsis;overflow:hidden}[data-dsh-frame][data-sidebar-collapsed] .mL8Uca_entry{justify-content:center;width:100%;padding:0}[data-dsh-frame][data-sidebar-collapsed] .mL8Uca_entryLabel{display:none}.mL8Uca_view{overflow:hidden}.mL8Uca_panel{background:var(--dsw-alias-bg-base);min-width:0;height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);flex-direction:column;gap:10px;padding:14px 16px 16px;display:flex}.mL8Uca_panelHeader{flex:none;align-items:center;gap:10px;display:flex}.mL8Uca_panelTitle{color:var(--dsw-alias-label-primary);white-space:nowrap;flex:1;margin:0;font-size:16px;font-weight:700}.mL8Uca_backButton{align-items:center;gap:4px;display:inline-flex}.mL8Uca_tabBar{border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;gap:2px;display:flex}.mL8Uca_tab{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-bottom:2px solid #0000;border-radius:6px 6px 0 0;padding:7px 14px;font-size:13px}.mL8Uca_tab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.mL8Uca_tab[data-active]{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-business-primary);font-weight:600}.mL8Uca_panelContent{flex-direction:column;flex:1;min-height:0;display:flex;overflow:hidden}.mL8Uca_tabBody{flex-direction:column;flex:1;gap:10px;min-height:0;display:flex;overflow-y:auto}.mL8Uca_fillBody{flex-direction:column;flex:1;gap:10px;min-height:0;display:flex;overflow:hidden}.mL8Uca_toolbar,.mL8Uca_controls{flex-wrap:wrap;flex:none;align-items:center;gap:8px;display:flex}.mL8Uca_controls .mL8Uca_input{flex:0 260px;min-width:150px}.mL8Uca_toolbarSpacer{flex:1}.mL8Uca_search{min-width:120px;color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:none;flex:0 260px;padding:6px 10px;font-size:13px}.mL8Uca_search::placeholder{color:var(--dsw-alias-label-tertiary)}.mL8Uca_tableWrap{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex:1;min-height:0;overflow:auto}.mL8Uca_table{border-collapse:collapse;width:100%;font-size:12.5px}.mL8Uca_table th{z-index:1;text-align:left;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);border-bottom:1px solid var(--dsw-alias-border-l1);white-space:nowrap;padding:8px 10px;font-weight:600;position:sticky;top:0}.mL8Uca_table td{border-bottom:1px solid var(--dsw-alias-separator-primary);vertical-align:top;padding:7px 10px}.mL8Uca_table tbody tr:last-child td{border-bottom:none}.mL8Uca_table tbody tr:hover td{background:var(--dsw-alias-interactive-bg-hover)}.mL8Uca_mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.mL8Uca_cellMuted{color:var(--dsw-alias-label-tertiary)}.mL8Uca_actions{white-space:nowrap;align-items:center;gap:8px;display:flex}.mL8Uca_inlineTest{color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;font-size:11.5px;display:inline-flex}.mL8Uca_inlineTest[data-status=ok]{color:var(--dsw-alias-state-success-primary)}.mL8Uca_inlineTest[data-status=fail]{color:var(--dsw-alias-state-error-primary)}.mL8Uca_badge{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;line-height:1.6;display:inline-block}.mL8Uca_badge[data-kind=key]{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}.mL8Uca_badge[data-kind=password]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}.mL8Uca_badge[data-status=ok]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.mL8Uca_badge[data-status=fail]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.mL8Uca_badge[data-status=timeout]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}.mL8Uca_primaryButton{color:var(--dsw-alias-label-primary-foreground);background:var(--dsw-alias-button-info-fill);cursor:pointer;white-space:nowrap;border:none;border-radius:8px;padding:6px 14px;font-size:13px;font-weight:600}.mL8Uca_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-info-hover)}.mL8Uca_primaryButton:disabled{opacity:.5;cursor:default}.mL8Uca_ghostButton{color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;white-space:nowrap;background:0 0;border-radius:8px;padding:5px 12px;font-size:12px}.mL8Uca_ghostButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.mL8Uca_ghostButton:disabled{opacity:.45;cursor:default}.mL8Uca_iconButton{width:26px;height:26px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:0;font-size:13px;display:inline-flex}.mL8Uca_iconButton:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.mL8Uca_linkButton{color:var(--dsw-alias-state-business-primary);cursor:pointer;white-space:nowrap;background:0 0;border:none;padding:0;font-size:12px}.mL8Uca_linkButton:hover:not(:disabled){text-decoration:underline}.mL8Uca_linkButton:disabled{opacity:.45;cursor:default}.mL8Uca_linkButton[data-danger]{color:var(--dsw-alias-state-error-primary)}.mL8Uca_spinner{border:2px solid var(--dsw-alias-state-business-primary);vertical-align:-1px;border-top-color:#0000;border-radius:50%;flex:none;width:11px;height:11px;animation:.8s linear infinite mL8Uca_dshSshSpin;display:inline-block}@keyframes mL8Uca_dshSshSpin{to{transform:rotate(360deg)}}.mL8Uca_banner{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;border-radius:8px;padding:8px 12px;font-size:12.5px;line-height:1.5}.mL8Uca_banner[data-kind=ok]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.mL8Uca_banner[data-kind=error]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.mL8Uca_banner[data-kind=info]{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}.mL8Uca_empty,.mL8Uca_loading{text-align:center;color:var(--dsw-alias-label-tertiary);padding:28px 12px;font-size:12.5px}.mL8Uca_modalBackdrop{z-index:40;background:var(--dsw-alias-bg-mask-1);justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.mL8Uca_modal{background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);width:min(560px,100vw - 48px);max-height:calc(100vh - 96px);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);border-radius:14px;flex-direction:column;gap:12px;padding:18px;display:flex;overflow-y:auto}.mL8Uca_modalTitle{margin:0;font-size:15px;font-weight:700}.mL8Uca_modalFooter{justify-content:flex-end;gap:10px;margin-top:4px;display:flex}.mL8Uca_formError{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px}.mL8Uca_hint{color:var(--dsw-alias-label-tertiary);font-size:11.5px}.mL8Uca_field{flex-direction:column;gap:5px;display:flex}.mL8Uca_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}.mL8Uca_input{color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);resize:vertical;border-radius:8px;outline:none;padding:7px 10px;font-family:inherit;font-size:13px}.mL8Uca_input:focus{border-color:var(--dsw-alias-state-business-primary)}.mL8Uca_input::placeholder{color:var(--dsw-alias-label-tertiary)}.mL8Uca_input:disabled{opacity:.55}.mL8Uca_formRow{grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px 12px;display:grid}.mL8Uca_radioRow{align-items:center;gap:16px;display:flex}.mL8Uca_radioLabel{color:var(--dsw-alias-label-primary);cursor:pointer;align-items:center;gap:6px;font-size:13px;display:inline-flex}.mL8Uca_termBody{flex-direction:column;flex:1;gap:8px;min-height:0;display:flex;overflow:hidden}.mL8Uca_termWrap{border:1px solid var(--dsw-alias-border-l1);background:#0b0e14;border-radius:10px;flex:1;min-height:0;position:relative;overflow:hidden}.mL8Uca_termContainer{padding:8px 10px;position:absolute;inset:0}.mL8Uca_termPlaceholder{z-index:2;text-align:center;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-base);justify-content:center;align-items:center;padding:0 24px;font-size:12.5px;display:flex;position:absolute;inset:0}.mL8Uca_hiddenFile{display:none}.mL8Uca_browsePanel{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex-direction:column;flex:none;display:flex;overflow:hidden}.mL8Uca_browseHeader{background:var(--dsw-alias-bg-layer-2);border-bottom:1px solid var(--dsw-alias-border-l1);align-items:center;gap:8px;padding:8px 10px;display:flex}.mL8Uca_browsePath{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-secondary);flex:1;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;overflow:hidden}.mL8Uca_browseList{max-height:240px;overflow-y:auto}.mL8Uca_dirRow{text-align:left;width:100%;font:inherit;color:var(--dsw-alias-label-primary);border:none;border-bottom:1px solid var(--dsw-alias-separator-primary);cursor:pointer;background:0 0;grid-template-columns:minmax(0,1fr) auto auto;gap:12px;padding:5px 10px;font-size:12.5px;display:grid}.mL8Uca_dirRow:hover{background:var(--dsw-alias-interactive-bg-hover)}.mL8Uca_dirRow[data-type=dir] .mL8Uca_dirName{color:var(--dsw-alias-state-business-primary)}.mL8Uca_dirRow[data-up]{color:var(--dsw-alias-label-secondary)}.mL8Uca_dirName{text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow:hidden}.mL8Uca_dirType{color:var(--dsw-alias-label-tertiary);white-space:nowrap}.mL8Uca_dirSize{color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-align:right}.mL8Uca_transferBlock{flex-direction:column;flex:none;gap:6px;display:flex}.mL8Uca_progressMeta{color:var(--dsw-alias-label-secondary);flex-wrap:wrap;gap:12px;font-size:12px;display:flex}.mL8Uca_progressTrack{background:var(--dsw-alias-interactive-bg-hover);border-radius:999px;height:8px;overflow:hidden}.mL8Uca_progressBar{background:var(--dsw-alias-state-business-primary);border-radius:999px;height:100%;transition:width .12s linear}.mL8Uca_tunnelList{flex-direction:column;flex:none;gap:8px;display:flex}.mL8Uca_tunnelRow{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;align-items:center;gap:10px;padding:8px 12px;display:flex}.mL8Uca_tunnelRow[data-state=forwarding]{border-color:var(--dsw-alias-state-success-primary)}.mL8Uca_tunnelRow[data-state=connecting]{border-color:var(--dsw-alias-state-warn-primary)}.mL8Uca_tunnelRow[data-state=failed]{border-color:var(--dsw-alias-state-error-primary)}.mL8Uca_tunnelLabel{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12.5px;overflow:hidden}.mL8Uca_formCard{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex-direction:column;flex:none;gap:10px;padding:12px;display:flex}.mL8Uca_clusterForm{flex-direction:column;flex:none;gap:8px;display:flex}.mL8Uca_clusterFilters{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px;display:grid}.mL8Uca_commandInput{min-height:96px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.mL8Uca_cellDetails summary{cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px}.mL8Uca_cellPre{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);white-space:pre-wrap;word-break:break-all;border-radius:6px;max-height:180px;margin:4px 0 0;padding:6px 8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px;overflow:auto}";
614
+ const css = "[data-pane=conversation],[class*=centerCol]{position:relative}[data-dsh-ssh-view]{z-index:60;background:var(--dsw-alias-bg-base);display:none;position:absolute;inset:0}html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [data-dsh-ssh-view]{display:block}html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [data-pane=conversation]>:not([data-dsh-ssh-view]),html[data-dsh-ssh-active]:not([data-dsh-taskboard-active]) [class*=centerCol]>:not([data-dsh-ssh-view]){display:none!important}.mL8Uca_entry{width:100%;height:32px;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:8px;align-items:center;gap:8px;padding:0 12px;font-size:13px;display:flex}.mL8Uca_entry:hover{background:var(--dsw-specific-sidebar-nav-item-hover);color:var(--dsw-alias-label-primary)}.mL8Uca_entry[data-active]{background:var(--dsw-specific-sidebar-nav-item-active);color:var(--dsw-alias-label-primary);font-weight:600}.mL8Uca_entryIcon{flex:none;justify-content:center;align-items:center;display:inline-flex}.mL8Uca_entryLabel{text-overflow:ellipsis;overflow:hidden}[data-dsh-frame][data-sidebar-collapsed] .mL8Uca_entry{justify-content:center;width:100%;padding:0}[data-dsh-frame][data-sidebar-collapsed] .mL8Uca_entryLabel{display:none}.mL8Uca_view{overflow:hidden}.mL8Uca_panel{background:var(--dsw-alias-bg-base);min-width:0;height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);flex-direction:column;gap:10px;padding:14px 16px 16px;display:flex}.mL8Uca_panelHeader{flex:none;align-items:center;gap:10px;display:flex}.mL8Uca_panelTitle{color:var(--dsw-alias-label-primary);white-space:nowrap;flex:1;margin:0;font-size:16px;font-weight:700}.mL8Uca_backButton{align-items:center;gap:4px;display:inline-flex}.mL8Uca_tabBar{border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;gap:2px;display:flex}.mL8Uca_tab{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-bottom:2px solid #0000;border-radius:6px 6px 0 0;padding:7px 14px;font-size:13px}.mL8Uca_tab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.mL8Uca_tab[data-active]{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-business-primary);font-weight:600}.mL8Uca_panelContent{flex-direction:column;flex:1;min-height:0;display:flex;overflow:hidden}.mL8Uca_tabBody{flex-direction:column;flex:1;gap:10px;min-height:0;display:flex;overflow-y:auto}.mL8Uca_fillBody{flex-direction:column;flex:1;gap:10px;min-height:0;display:flex;overflow:hidden}.mL8Uca_toolbar,.mL8Uca_controls{flex-wrap:wrap;flex:none;align-items:center;gap:8px;display:flex}.mL8Uca_controls .mL8Uca_input{flex:0 260px;min-width:150px}.mL8Uca_toolbarSpacer{flex:1}.mL8Uca_search{min-width:120px;color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:none;flex:0 260px;padding:6px 10px;font-size:13px}.mL8Uca_search::placeholder{color:var(--dsw-alias-label-tertiary)}.mL8Uca_groupBySelect{color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:none;padding:6px 8px;font-size:13px}.mL8Uca_groupSection+.mL8Uca_groupSection{border-top:1px solid var(--dsw-alias-border-l1)}.mL8Uca_groupHeader{background:var(--dsw-alias-bg-base,var(--dsw-specific-input-major));align-items:center;gap:8px;padding:4px 8px;display:flex;position:sticky;top:0}.mL8Uca_groupToggle{min-width:0;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;background:0 0;border:none;border-radius:6px;flex:1;align-items:center;gap:6px;padding:4px 6px;font-size:12.5px;font-weight:500;display:flex}.mL8Uca_groupToggle:hover{background:var(--dsw-alias-interactive-bg-hover)}.mL8Uca_groupChevron{border-right:1.5px solid var(--dsw-alias-label-tertiary);border-bottom:1.5px solid var(--dsw-alias-label-tertiary);flex:none;width:7px;height:7px;margin-bottom:2px;transition:transform .12s;transform:rotate(45deg)}.mL8Uca_groupChevron[data-collapsed]{margin-bottom:0;transform:rotate(-45deg)}.mL8Uca_groupName{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.mL8Uca_groupCount{color:var(--dsw-alias-label-tertiary);flex:none;font-weight:400}.mL8Uca_tableWrap{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex:1;min-height:0;overflow:auto}.mL8Uca_table{border-collapse:collapse;width:100%;font-size:12.5px}.mL8Uca_table th{z-index:1;text-align:left;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);border-bottom:1px solid var(--dsw-alias-border-l1);white-space:nowrap;padding:8px 10px;font-weight:600;position:sticky;top:0}.mL8Uca_table td{border-bottom:1px solid var(--dsw-alias-separator-primary);vertical-align:top;padding:7px 10px}.mL8Uca_table tbody tr:last-child td{border-bottom:none}.mL8Uca_table tbody tr:hover td{background:var(--dsw-alias-interactive-bg-hover)}.mL8Uca_mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.mL8Uca_cellMuted{color:var(--dsw-alias-label-tertiary)}.mL8Uca_actions{white-space:nowrap;align-items:center;gap:8px;display:flex}.mL8Uca_inlineTest{color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;font-size:11.5px;display:inline-flex}.mL8Uca_inlineTest[data-status=ok]{color:var(--dsw-alias-state-success-primary)}.mL8Uca_inlineTest[data-status=fail]{color:var(--dsw-alias-state-error-primary)}.mL8Uca_badge{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;line-height:1.6;display:inline-block}.mL8Uca_badge[data-kind=key]{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}.mL8Uca_badge[data-kind=password]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}.mL8Uca_badge[data-status=ok]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.mL8Uca_badge[data-status=fail]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.mL8Uca_badge[data-status=timeout]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}.mL8Uca_primaryButton{color:var(--dsw-alias-label-primary-foreground);background:var(--dsw-alias-button-info-fill);cursor:pointer;white-space:nowrap;border:none;border-radius:8px;padding:6px 14px;font-size:13px;font-weight:600}.mL8Uca_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-info-hover)}.mL8Uca_primaryButton:disabled{opacity:.5;cursor:default}.mL8Uca_ghostButton{color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;white-space:nowrap;background:0 0;border-radius:8px;padding:5px 12px;font-size:12px}.mL8Uca_ghostButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.mL8Uca_ghostButton:disabled{opacity:.45;cursor:default}.mL8Uca_iconButton{width:26px;height:26px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:0;font-size:13px;display:inline-flex}.mL8Uca_iconButton:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.mL8Uca_linkButton{color:var(--dsw-alias-state-business-primary);cursor:pointer;white-space:nowrap;background:0 0;border:none;padding:0;font-size:12px}.mL8Uca_linkButton:hover:not(:disabled){text-decoration:underline}.mL8Uca_linkButton:disabled{opacity:.45;cursor:default}.mL8Uca_linkButton[data-danger]{color:var(--dsw-alias-state-error-primary)}.mL8Uca_spinner{border:2px solid var(--dsw-alias-state-business-primary);vertical-align:-1px;border-top-color:#0000;border-radius:50%;flex:none;width:11px;height:11px;animation:.8s linear infinite mL8Uca_dshSshSpin;display:inline-block}@keyframes mL8Uca_dshSshSpin{to{transform:rotate(360deg)}}.mL8Uca_banner{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;border-radius:8px;padding:8px 12px;font-size:12.5px;line-height:1.5}.mL8Uca_banner[data-kind=ok]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.mL8Uca_banner[data-kind=error]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.mL8Uca_banner[data-kind=info]{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}.mL8Uca_empty,.mL8Uca_loading{text-align:center;color:var(--dsw-alias-label-tertiary);padding:28px 12px;font-size:12.5px}.mL8Uca_modalBackdrop{z-index:40;background:var(--dsw-alias-bg-mask-1);justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.mL8Uca_modal{background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);width:min(560px,100vw - 48px);max-height:calc(100vh - 96px);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);border-radius:14px;flex-direction:column;gap:12px;padding:18px;display:flex;overflow-y:auto}.mL8Uca_modalTitle{margin:0;font-size:15px;font-weight:700}.mL8Uca_modalFooter{justify-content:flex-end;gap:10px;margin-top:4px;display:flex}.mL8Uca_formError{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px}.mL8Uca_hint{color:var(--dsw-alias-label-tertiary);font-size:11.5px}.mL8Uca_field{flex-direction:column;gap:5px;display:flex}.mL8Uca_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}.mL8Uca_input{color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);resize:vertical;border-radius:8px;outline:none;padding:7px 10px;font-family:inherit;font-size:13px}.mL8Uca_input:focus{border-color:var(--dsw-alias-state-business-primary)}.mL8Uca_input::placeholder{color:var(--dsw-alias-label-tertiary)}.mL8Uca_input:disabled{opacity:.55}.mL8Uca_formRow{grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px 12px;display:grid}.mL8Uca_radioRow{align-items:center;gap:16px;display:flex}.mL8Uca_radioLabel{color:var(--dsw-alias-label-primary);cursor:pointer;align-items:center;gap:6px;font-size:13px;display:inline-flex}.mL8Uca_termBody{flex-direction:column;flex:1;gap:8px;min-height:0;display:flex;overflow:hidden}.mL8Uca_termWrap{border:1px solid var(--dsw-alias-border-l1);background:#0b0e14;border-radius:10px;flex:1;min-height:0;position:relative;overflow:hidden}.mL8Uca_termContainer{padding:8px 10px;position:absolute;inset:0}.mL8Uca_termPlaceholder{z-index:2;text-align:center;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-base);justify-content:center;align-items:center;padding:0 24px;font-size:12.5px;display:flex;position:absolute;inset:0}.mL8Uca_hiddenFile{display:none}.mL8Uca_browsePanel{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex-direction:column;flex:none;display:flex;overflow:hidden}.mL8Uca_browseHeader{background:var(--dsw-alias-bg-layer-2);border-bottom:1px solid var(--dsw-alias-border-l1);align-items:center;gap:8px;padding:8px 10px;display:flex}.mL8Uca_browsePath{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-secondary);flex:1;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;overflow:hidden}.mL8Uca_browseList{max-height:240px;overflow-y:auto}.mL8Uca_dirRow{text-align:left;width:100%;font:inherit;color:var(--dsw-alias-label-primary);border:none;border-bottom:1px solid var(--dsw-alias-separator-primary);cursor:pointer;background:0 0;grid-template-columns:minmax(0,1fr) auto auto;gap:12px;padding:5px 10px;font-size:12.5px;display:grid}.mL8Uca_dirRow:hover{background:var(--dsw-alias-interactive-bg-hover)}.mL8Uca_dirRow[data-type=dir] .mL8Uca_dirName{color:var(--dsw-alias-state-business-primary)}.mL8Uca_dirRow[data-up]{color:var(--dsw-alias-label-secondary)}.mL8Uca_dirName{text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow:hidden}.mL8Uca_dirType{color:var(--dsw-alias-label-tertiary);white-space:nowrap}.mL8Uca_dirSize{color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-align:right}.mL8Uca_transferBlock{flex-direction:column;flex:none;gap:6px;display:flex}.mL8Uca_progressMeta{color:var(--dsw-alias-label-secondary);flex-wrap:wrap;gap:12px;font-size:12px;display:flex}.mL8Uca_progressTrack{background:var(--dsw-alias-interactive-bg-hover);border-radius:999px;height:8px;overflow:hidden}.mL8Uca_progressBar{background:var(--dsw-alias-state-business-primary);border-radius:999px;height:100%;transition:width .12s linear}.mL8Uca_tunnelList{flex-direction:column;flex:none;gap:8px;display:flex}.mL8Uca_tunnelRow{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;align-items:center;gap:10px;padding:8px 12px;display:flex}.mL8Uca_tunnelRow[data-state=forwarding]{border-color:var(--dsw-alias-state-success-primary)}.mL8Uca_tunnelRow[data-state=connecting]{border-color:var(--dsw-alias-state-warn-primary)}.mL8Uca_tunnelRow[data-state=failed]{border-color:var(--dsw-alias-state-error-primary)}.mL8Uca_tunnelLabel{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12.5px;overflow:hidden}.mL8Uca_formCard{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex-direction:column;flex:none;gap:10px;padding:12px;display:flex}.mL8Uca_clusterForm{flex-direction:column;flex:none;gap:8px;display:flex}.mL8Uca_clusterFilters{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px;display:grid}.mL8Uca_commandInput{min-height:96px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.mL8Uca_cellDetails summary{cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px}.mL8Uca_cellPre{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);white-space:pre-wrap;word-break:break-all;border-radius:6px;max-height:180px;margin:4px 0 0;padding:6px 8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px;overflow:auto}";
599
615
  const tagId = "@linxin666/dsh-ssh/panel.module.css";
600
616
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
601
617
  const tag = document.createElement("style");
@@ -636,6 +652,13 @@ window.__ModuleLoader__.load({
636
652
  "formError": "mL8Uca_formError",
637
653
  "formRow": "mL8Uca_formRow",
638
654
  "ghostButton": "mL8Uca_ghostButton",
655
+ "groupBySelect": "mL8Uca_groupBySelect",
656
+ "groupChevron": "mL8Uca_groupChevron",
657
+ "groupCount": "mL8Uca_groupCount",
658
+ "groupHeader": "mL8Uca_groupHeader",
659
+ "groupName": "mL8Uca_groupName",
660
+ "groupSection": "mL8Uca_groupSection",
661
+ "groupToggle": "mL8Uca_groupToggle",
639
662
  "hiddenFile": "mL8Uca_hiddenFile",
640
663
  "hint": "mL8Uca_hint",
641
664
  "iconButton": "mL8Uca_iconButton",
@@ -1238,6 +1261,31 @@ window.__ModuleLoader__.load({
1238
1261
  * add/edit/delete/test actions, ~/.ssh/config import, and a connect action
1239
1262
  * that hands the alias to the terminal tab via onConnect.
1240
1263
  */
1264
+ /**
1265
+ * Bucket hosts into collapsible groups (#379). Grouping by tags places a
1266
+ * multi-tag host in every one of its tag groups (folder view); hosts without
1267
+ * the grouping key land in the '' bucket, which always sorts last. Groups
1268
+ * sort alphabetically; host order inside a group follows the API listing.
1269
+ */
1270
+ function groupHosts(hosts, groupBy) {
1271
+ if (groupBy === "none") return [{
1272
+ key: "",
1273
+ hosts
1274
+ }];
1275
+ const buckets = /* @__PURE__ */ new Map();
1276
+ const push = (key, host) => {
1277
+ const bucket = buckets.get(key);
1278
+ if (bucket === void 0) buckets.set(key, [host]);
1279
+ else bucket.push(host);
1280
+ };
1281
+ for (const host of hosts) if (groupBy === "environment") push(host.environment ?? "", host);
1282
+ else if (host.tags.length === 0) push("", host);
1283
+ else for (const tag of host.tags) push(tag, host);
1284
+ return [...buckets.entries()].sort(([a], [b]) => a === "" ? 1 : b === "" ? -1 : a.localeCompare(b)).map(([key, group]) => ({
1285
+ key,
1286
+ hosts: group
1287
+ }));
1288
+ }
1241
1289
  /** The hosts table plus its toolbar and dialogs. */
1242
1290
  function HostsTab({ api, onConnect }) {
1243
1291
  const [hosts, setHosts] = (0, react.useState)(null);
@@ -1248,6 +1296,9 @@ window.__ModuleLoader__.load({
1248
1296
  const [importing, setImporting] = (0, react.useState)(false);
1249
1297
  const [notice, setNotice] = (0, react.useState)(null);
1250
1298
  const [dialog, setDialog] = (0, react.useState)(null);
1299
+ const [groupBy, setGroupBy] = (0, react.useState)("none");
1300
+ const [collapsed, setCollapsed] = (0, react.useState)({});
1301
+ const [testingGroup, setTestingGroup] = (0, react.useState)(null);
1251
1302
  const seqRef = (0, react.useRef)(0);
1252
1303
  const load = (0, react.useCallback)(async (query) => {
1253
1304
  const seq = ++seqRef.current;
@@ -1302,6 +1353,14 @@ window.__ModuleLoader__.load({
1302
1353
  setError(errorMessage(cause));
1303
1354
  }
1304
1355
  };
1356
+ const testGroup = async (group) => {
1357
+ setTestingGroup(group.key);
1358
+ try {
1359
+ await Promise.all(group.hosts.map((host) => runTest(host.alias)));
1360
+ } finally {
1361
+ setTestingGroup(null);
1362
+ }
1363
+ };
1305
1364
  const importConfig = async () => {
1306
1365
  setImporting(true);
1307
1366
  try {
@@ -1318,6 +1377,106 @@ window.__ModuleLoader__.load({
1318
1377
  setImporting(false);
1319
1378
  }
1320
1379
  };
1380
+ const renderHostRow = (host) => {
1381
+ const test = testResults[host.alias];
1382
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
1383
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1384
+ className: panel_module_css_default.mono,
1385
+ children: host.alias
1386
+ }),
1387
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", {
1388
+ className: panel_module_css_default.mono,
1389
+ children: [
1390
+ host.host,
1391
+ ":",
1392
+ host.port
1393
+ ]
1394
+ }),
1395
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: host.user }),
1396
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1397
+ className: panel_module_css_default.badge,
1398
+ "data-kind": host.auth,
1399
+ children: host.auth === "key" ? tt("form.auth.key") : tt("form.auth.password")
1400
+ }) }),
1401
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1402
+ className: panel_module_css_default.cellMuted,
1403
+ children: host.environment ?? ""
1404
+ }),
1405
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1406
+ className: panel_module_css_default.cellMuted,
1407
+ children: host.tags.join(", ")
1408
+ }),
1409
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1410
+ className: panel_module_css_default.cellMuted,
1411
+ children: host.description ?? ""
1412
+ }),
1413
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1414
+ className: panel_module_css_default.actions,
1415
+ children: [
1416
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1417
+ type: "button",
1418
+ className: panel_module_css_default.linkButton,
1419
+ disabled: testingAlias === host.alias,
1420
+ onClick: () => {
1421
+ runTest(host.alias);
1422
+ },
1423
+ children: testingAlias === host.alias ? tt("hosts.testing") : tt("hosts.test")
1424
+ }),
1425
+ testingAlias === host.alias && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1426
+ className: panel_module_css_default.spinner,
1427
+ "aria-hidden": "true"
1428
+ }),
1429
+ test !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1430
+ className: panel_module_css_default.inlineTest,
1431
+ "data-status": test.ok ? "ok" : "fail",
1432
+ children: test.ok ? tt("hosts.testOk", { latency: test.latencyMs ?? 0 }) : tt("hosts.testFail", { error: test.error ?? "" })
1433
+ }),
1434
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1435
+ type: "button",
1436
+ className: panel_module_css_default.linkButton,
1437
+ onClick: () => {
1438
+ setDialog({
1439
+ mode: "edit",
1440
+ host
1441
+ });
1442
+ },
1443
+ children: tt("hosts.edit")
1444
+ }),
1445
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1446
+ type: "button",
1447
+ className: panel_module_css_default.linkButton,
1448
+ "data-danger": true,
1449
+ onClick: () => {
1450
+ deleteHost(host.alias);
1451
+ },
1452
+ children: tt("hosts.delete")
1453
+ }),
1454
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1455
+ type: "button",
1456
+ className: panel_module_css_default.ghostButton,
1457
+ onClick: () => {
1458
+ onConnect(host.alias);
1459
+ },
1460
+ children: tt("hosts.connected")
1461
+ })
1462
+ ]
1463
+ }) })
1464
+ ] }, host.alias);
1465
+ };
1466
+ const renderHostTable = (rows) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
1467
+ className: panel_module_css_default.table,
1468
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
1469
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.alias") }),
1470
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.host") }),
1471
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.user") }),
1472
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.auth") }),
1473
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.environment") }),
1474
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.tags") }),
1475
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.description") }),
1476
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.actions") })
1477
+ ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: rows.map(renderHostRow) })]
1478
+ });
1479
+ const groups = hosts === null ? [] : groupHosts(hosts, groupBy);
1321
1480
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1322
1481
  className: panel_module_css_default.fillBody,
1323
1482
  children: [
@@ -1333,6 +1492,28 @@ window.__ModuleLoader__.load({
1333
1492
  setSearch(event.target.value);
1334
1493
  }
1335
1494
  }),
1495
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1496
+ className: panel_module_css_default.groupBySelect,
1497
+ "aria-label": tt("hosts.groupBy.label"),
1498
+ value: groupBy,
1499
+ onChange: (event) => {
1500
+ setGroupBy(event.target.value);
1501
+ },
1502
+ children: [
1503
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1504
+ value: "none",
1505
+ children: tt("hosts.groupBy.none")
1506
+ }),
1507
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1508
+ value: "environment",
1509
+ children: tt("hosts.groupBy.environment")
1510
+ }),
1511
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1512
+ value: "tags",
1513
+ children: tt("hosts.groupBy.tags")
1514
+ })
1515
+ ]
1516
+ }),
1336
1517
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: panel_module_css_default.toolbarSpacer }),
1337
1518
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1338
1519
  type: "button",
@@ -1371,105 +1552,55 @@ window.__ModuleLoader__.load({
1371
1552
  className: panel_module_css_default.empty,
1372
1553
  children: tt("hosts.empty")
1373
1554
  }),
1374
- hosts !== null && hosts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1555
+ hosts !== null && hosts.length > 0 && groupBy === "none" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1375
1556
  className: panel_module_css_default.tableWrap,
1376
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
1377
- className: panel_module_css_default.table,
1378
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
1379
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.alias") }),
1380
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.host") }),
1381
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.user") }),
1382
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.auth") }),
1383
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.environment") }),
1384
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.tags") }),
1385
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.description") }),
1386
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("hosts.col.actions") })
1387
- ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: hosts.map((host) => {
1388
- const test = testResults[host.alias];
1389
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
1390
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1391
- className: panel_module_css_default.mono,
1392
- children: host.alias
1393
- }),
1394
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", {
1395
- className: panel_module_css_default.mono,
1396
- children: [
1397
- host.host,
1398
- ":",
1399
- host.port
1400
- ]
1401
- }),
1402
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: host.user }),
1403
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1404
- className: panel_module_css_default.badge,
1405
- "data-kind": host.auth,
1406
- children: host.auth === "key" ? tt("form.auth.key") : tt("form.auth.password")
1407
- }) }),
1408
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1409
- className: panel_module_css_default.cellMuted,
1410
- children: host.environment ?? ""
1411
- }),
1412
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1413
- className: panel_module_css_default.cellMuted,
1414
- children: host.tags.join(", ")
1415
- }),
1416
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1417
- className: panel_module_css_default.cellMuted,
1418
- children: host.description ?? ""
1419
- }),
1420
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1421
- className: panel_module_css_default.actions,
1557
+ children: renderHostTable(hosts)
1558
+ }),
1559
+ hosts !== null && hosts.length > 0 && groupBy !== "none" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1560
+ className: panel_module_css_default.tableWrap,
1561
+ children: groups.map((group) => {
1562
+ const isCollapsed = collapsed[group.key] === true;
1563
+ const label = group.key === "" ? groupBy === "tags" ? tt("hosts.group.noTags") : tt("hosts.group.ungrouped") : group.key;
1564
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1565
+ className: panel_module_css_default.groupSection,
1566
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1567
+ className: panel_module_css_default.groupHeader,
1568
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1569
+ type: "button",
1570
+ className: panel_module_css_default.groupToggle,
1571
+ "aria-expanded": !isCollapsed,
1572
+ onClick: () => {
1573
+ setCollapsed((prev) => ({
1574
+ ...prev,
1575
+ [group.key]: !isCollapsed
1576
+ }));
1577
+ },
1422
1578
  children: [
1423
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1424
- type: "button",
1425
- className: panel_module_css_default.linkButton,
1426
- disabled: testingAlias === host.alias,
1427
- onClick: () => {
1428
- runTest(host.alias);
1429
- },
1430
- children: testingAlias === host.alias ? tt("hosts.testing") : tt("hosts.test")
1431
- }),
1432
- testingAlias === host.alias && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1433
- className: panel_module_css_default.spinner,
1579
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1580
+ className: panel_module_css_default.groupChevron,
1581
+ "data-collapsed": isCollapsed || void 0,
1434
1582
  "aria-hidden": "true"
1435
1583
  }),
1436
- test !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1437
- className: panel_module_css_default.inlineTest,
1438
- "data-status": test.ok ? "ok" : "fail",
1439
- children: test.ok ? tt("hosts.testOk", { latency: test.latencyMs ?? 0 }) : tt("hosts.testFail", { error: test.error ?? "" })
1584
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1585
+ className: panel_module_css_default.groupName,
1586
+ children: label
1440
1587
  }),
1441
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1442
- type: "button",
1443
- className: panel_module_css_default.linkButton,
1444
- onClick: () => {
1445
- setDialog({
1446
- mode: "edit",
1447
- host
1448
- });
1449
- },
1450
- children: tt("hosts.edit")
1451
- }),
1452
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1453
- type: "button",
1454
- className: panel_module_css_default.linkButton,
1455
- "data-danger": true,
1456
- onClick: () => {
1457
- deleteHost(host.alias);
1458
- },
1459
- children: tt("hosts.delete")
1460
- }),
1461
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1462
- type: "button",
1463
- className: panel_module_css_default.ghostButton,
1464
- onClick: () => {
1465
- onConnect(host.alias);
1466
- },
1467
- children: tt("hosts.connected")
1588
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1589
+ className: panel_module_css_default.groupCount,
1590
+ children: tt("hosts.group.count", { count: group.hosts.length })
1468
1591
  })
1469
1592
  ]
1470
- }) })
1471
- ] }, host.alias);
1472
- }) })]
1593
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1594
+ type: "button",
1595
+ className: panel_module_css_default.linkButton,
1596
+ disabled: testingGroup === group.key,
1597
+ onClick: () => {
1598
+ testGroup(group);
1599
+ },
1600
+ children: testingGroup === group.key ? tt("hosts.testing") : tt("hosts.group.testAll")
1601
+ })]
1602
+ }), !isCollapsed && renderHostTable(group.hosts)]
1603
+ }, group.key);
1473
1604
  })
1474
1605
  }),
1475
1606
  dialog !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostFormDialog, {