cicy-desktop 2.1.95 → 2.1.96

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.
@@ -1537,7 +1537,12 @@ function DockerInstallDrawerHost() {
1537
1537
  // Desktop and runs it (主人指令), streaming progress through the drawer above.
1538
1538
  function DockerCard({ dockerTeam, onOpen, onRefresh }) {
1539
1539
  const [status, setStatus] = useState(null);
1540
- const [busy, setBusy] = useState(false);
1540
+ const [busy, setBusy] = useState(""); // "" | bootstrap | restart | stop | upgrade
1541
+ const [menuOpen, setMenuOpen] = useState(false);
1542
+ const [menuPos, setMenuPos] = useState({ top: 0, left: 0 });
1543
+ const kebabRef = useRef(null);
1544
+ const menuRef = useRef(null);
1545
+ const MENU_W = 184;
1541
1546
  const DOCKER_BLUE = "#2496ed";
1542
1547
 
1543
1548
  const checkStatus = useCallback(async () => {
@@ -1547,8 +1552,31 @@ function DockerCard({ dockerTeam, onOpen, onRefresh }) {
1547
1552
 
1548
1553
  useEffect(() => { checkStatus(); }, [checkStatus]);
1549
1554
 
1555
+ // Close the ⋯ menu on outside-click / Esc (mirrors LocalTeamCard).
1556
+ useEffect(() => {
1557
+ if (!menuOpen) return;
1558
+ const onDoc = (e) => {
1559
+ if (kebabRef.current?.contains(e.target) || menuRef.current?.contains(e.target)) return;
1560
+ setMenuOpen(false);
1561
+ };
1562
+ const onKey = (e) => { if (e.key === "Escape") setMenuOpen(false); };
1563
+ document.addEventListener("mousedown", onDoc);
1564
+ document.addEventListener("keydown", onKey);
1565
+ return () => { document.removeEventListener("mousedown", onDoc); document.removeEventListener("keydown", onKey); };
1566
+ }, [menuOpen]);
1567
+
1568
+ const toggleMenu = () => {
1569
+ if (!menuOpen && kebabRef.current) {
1570
+ const r = kebabRef.current.getBoundingClientRect();
1571
+ const left = Math.max(8, Math.min(r.right - MENU_W, window.innerWidth - MENU_W - 8));
1572
+ setMenuPos({ top: Math.round(r.bottom + 4), left: Math.round(left) });
1573
+ }
1574
+ setMenuOpen((v) => !v);
1575
+ };
1576
+
1577
+ // Install / start: streams through the drawer modal (logs + progress + retry).
1550
1578
  const runBootstrap = useCallback(async () => {
1551
- setBusy(true);
1579
+ setBusy("bootstrap");
1552
1580
  dockerDrawer.open({ onRetry: runBootstrap });
1553
1581
  const unsub = window.cicy?.docker?.onAppProgress?.((ev) => dockerDrawer.push(ev));
1554
1582
  try {
@@ -1559,11 +1587,41 @@ function DockerCard({ dockerTeam, onOpen, onRefresh }) {
1559
1587
  dockerDrawer.finish({ ok: false, message: e.message });
1560
1588
  } finally {
1561
1589
  try { unsub && unsub(); } catch {}
1562
- setBusy(false);
1563
- checkStatus();
1590
+ setBusy(""); checkStatus();
1591
+ }
1592
+ }, [checkStatus, onRefresh]);
1593
+
1594
+ // Upgrade: re-pull the R2 image + recreate the container — also through the
1595
+ // drawer so the user sees the pull/import/restart log (主人: 升级要能看日志).
1596
+ const runUpgrade = useCallback(async () => {
1597
+ setMenuOpen(false); setBusy("upgrade");
1598
+ dockerDrawer.open({ onRetry: runUpgrade });
1599
+ const unsub = window.cicy?.docker?.onAppProgress?.((ev) => dockerDrawer.push(ev));
1600
+ try {
1601
+ const r = await window.cicy?.docker?.appUpgrade?.();
1602
+ dockerDrawer.finish({ ok: !!r?.ok, message: r?.ok ? tr("docker.upgraded", "已升级到最新") : (r?.error || tr("docker.upgradeFailed", "升级失败")) });
1603
+ if (r?.ok) onRefresh?.();
1604
+ } catch (e) {
1605
+ dockerDrawer.finish({ ok: false, message: e.message });
1606
+ } finally {
1607
+ try { unsub && unsub(); } catch {}
1608
+ setBusy(""); checkStatus();
1564
1609
  }
1565
1610
  }, [checkStatus, onRefresh]);
1566
1611
 
1612
+ // Restart / stop: quick lifecycle ops with a toast (no full drawer needed).
1613
+ const runOp = useCallback(async (op, fn, okMsg) => {
1614
+ setMenuOpen(false); setBusy(op);
1615
+ toast.show({ id: "docker-op", message: tr(`docker.${op}ing`, op === "restart" ? "重启中…" : "停止中…"), status: "running" });
1616
+ try {
1617
+ const r = await fn();
1618
+ if (r?.ok) toast.show({ id: "docker-op", message: okMsg, status: "done", ttl: 2500 });
1619
+ else toast.show({ id: "docker-op", message: (r?.error || tr("docker.opFailed", "操作失败")), status: "error", ttl: 6000 });
1620
+ } catch (e) {
1621
+ toast.show({ id: "docker-op", message: e.message, status: "error", ttl: 6000 });
1622
+ } finally { setBusy(""); checkStatus(); }
1623
+ }, [checkStatus]);
1624
+
1567
1625
  // Render only on Windows. window.cicy.platform is sync, so we can decide
1568
1626
  // immediately without waiting on the async appStatus probe.
1569
1627
  const platform = window.cicy?.platform || status?.platform;
@@ -1572,13 +1630,14 @@ function DockerCard({ dockerTeam, onOpen, onRefresh }) {
1572
1630
  const running = !!status?.running || dockerTeam?.status === "running";
1573
1631
  const installed = !!status?.installed;
1574
1632
  const tone = running ? "ok" : installed ? "warn" : "off";
1633
+ const isBusy = !!busy;
1575
1634
  const stateText = running
1576
1635
  ? tr("docker.running", "运行中 · :8009")
1577
1636
  : installed
1578
1637
  ? tr("docker.notRunning", "未启动 · 点「启动」")
1579
1638
  : tr("docker.notInstalled", "Docker Desktop 未安装");
1580
1639
 
1581
- const ctaLabel = busy
1640
+ const ctaLabel = isBusy
1582
1641
  ? tr("docker.working", "处理中…")
1583
1642
  : running
1584
1643
  ? tr("localTeams.open", "打开")
@@ -1587,11 +1646,14 @@ function DockerCard({ dockerTeam, onOpen, onRefresh }) {
1587
1646
  : tr("docker.install", "下载安装");
1588
1647
 
1589
1648
  const onCta = () => {
1590
- if (busy) return;
1649
+ if (isBusy) return;
1591
1650
  if (running) { onOpen?.(dockerTeam?.id); return; }
1592
1651
  runBootstrap();
1593
1652
  };
1594
1653
 
1654
+ // The ⋯ menu (重启 / 停止 / 升级) is only meaningful once Docker is installed.
1655
+ const showMenu = installed;
1656
+
1595
1657
  return (
1596
1658
  <div data-id="DockerCard" className={`bcard bcard--docker${running ? " bcard--online" : ""}`}>
1597
1659
  <div className="bcard__accent" style={{ background: DOCKER_BLUE }} />
@@ -1602,6 +1664,44 @@ function DockerCard({ dockerTeam, onOpen, onRefresh }) {
1602
1664
  <path d="M21.81 10.25c-.06-.05-.67-.51-1.95-.51-.34 0-.68.03-1.01.09-.25-1.69-1.64-2.51-1.7-2.55l-.34-.2-.22.32a4.5 4.5 0 0 0-.59 1.4c-.23.94-.09 1.83.39 2.59-.58.32-1.51.4-1.7.41H2.62a.61.61 0 0 0-.61.61 9.32 9.32 0 0 0 .57 3.35 4.9 4.9 0 0 0 1.95 2.53c.92.52 2.42.82 4.12.82.77 0 1.54-.07 2.3-.21a9.6 9.6 0 0 0 3-1.09 8.3 8.3 0 0 0 2.05-1.68c.98-1.11 1.56-2.35 1.99-3.45h.17c1.36 0 2.2-.55 2.66-1l.13-.16zM4.7 11.33h1.78a.16.16 0 0 0 .16-.16V9.58a.16.16 0 0 0-.16-.16H4.7a.16.16 0 0 0-.16.16v1.59c0 .09.07.16.16.16m2.46 0h1.78a.16.16 0 0 0 .16-.16V9.58a.16.16 0 0 0-.16-.16H7.16a.16.16 0 0 0-.16.16v1.59c0 .09.07.16.16.16m2.5 0h1.78a.16.16 0 0 0 .16-.16V9.58a.16.16 0 0 0-.16-.16H9.66a.16.16 0 0 0-.16.16v1.59c0 .09.07.16.16.16m2.47 0h1.78a.16.16 0 0 0 .16-.16V9.58a.16.16 0 0 0-.16-.16h-1.78a.16.16 0 0 0-.16.16v1.59c0 .09.07.16.16.16M7.16 9.06h1.78a.16.16 0 0 0 .16-.16V7.31a.16.16 0 0 0-.16-.16H7.16a.16.16 0 0 0-.16.16v1.59c0 .09.07.16.16.16m2.5 0h1.78a.16.16 0 0 0 .16-.16V7.31a.16.16 0 0 0-.16-.16H9.66a.16.16 0 0 0-.16.16v1.59c0 .09.07.16.16.16m2.47 0h1.78a.16.16 0 0 0 .16-.16V7.31a.16.16 0 0 0-.16-.16h-1.78a.16.16 0 0 0-.16.16v1.59c0 .09.07.16.16.16" />
1603
1665
  </svg>
1604
1666
  </div>
1667
+ {showMenu && (
1668
+ <div className="bcard__menuwrap" onClick={(e) => e.stopPropagation()}>
1669
+ <button
1670
+ type="button"
1671
+ ref={kebabRef}
1672
+ data-id="DockerCard-menu-btn"
1673
+ className="bcard__kebab"
1674
+ title={tr("docker.manage", "管理 Docker cicy-code")}
1675
+ disabled={isBusy}
1676
+ onClick={toggleMenu}
1677
+ >
1678
+ {isBusy ? <Spinner /> : <KebabIcon />}
1679
+ </button>
1680
+ {menuOpen && createPortal(
1681
+ <div className="bcard__menu bcard__menu--portal" data-id="DockerCard-menu" role="menu"
1682
+ ref={menuRef}
1683
+ style={{ position: "fixed", top: menuPos.top, left: menuPos.left, width: MENU_W }}
1684
+ onClick={(e) => e.stopPropagation()}>
1685
+ {running && (
1686
+ <button type="button" data-id="DockerCard-restart" className="bcard__menu-item"
1687
+ onClick={() => runOp("restart", () => window.cicy.docker.appRestart(), tr("docker.restarted", "已重启"))}>
1688
+ {tr("docker.restart", "重启")}
1689
+ </button>
1690
+ )}
1691
+ <button type="button" data-id="DockerCard-upgrade" className="bcard__menu-item is-accent" onClick={runUpgrade}>
1692
+ {tr("docker.upgrade", "升级(拉取最新镜像)")}
1693
+ </button>
1694
+ {running && (
1695
+ <button type="button" data-id="DockerCard-stop" className="bcard__menu-item is-danger"
1696
+ onClick={() => runOp("stop", () => window.cicy.docker.appStop(), tr("docker.stopped", "已停止"))}>
1697
+ {tr("docker.stop", "停止")}
1698
+ </button>
1699
+ )}
1700
+ </div>,
1701
+ document.body
1702
+ )}
1703
+ </div>
1704
+ )}
1605
1705
  </div>
1606
1706
  <div className="bcard__body">
1607
1707
  <h3 className="bcard__name">{tr("docker.title", "Docker cicy-code")}</h3>
@@ -1612,11 +1712,11 @@ function DockerCard({ dockerTeam, onOpen, onRefresh }) {
1612
1712
  type="button"
1613
1713
  className="bcard__cta"
1614
1714
  data-id="DockerCard-cta"
1615
- disabled={busy}
1715
+ disabled={isBusy}
1616
1716
  onClick={onCta}
1617
1717
  style={!running ? { background: DOCKER_BLUE, color: "white" } : undefined}
1618
1718
  >
1619
- {busy ? <Spinner /> : <ArrowIcon />}
1719
+ {isBusy ? <Spinner /> : <ArrowIcon />}
1620
1720
  <span>{ctaLabel}</span>
1621
1721
  </button>
1622
1722
  </div>