mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
Template literals such as `Remove data source '${name}'?` reached props and
calls that look text up exactly (dialog messages, labels, aria-labels, menu
items, action titles), so they always showed in English. Some notification
titles also spliced lowercase English words into a sentence.
- `tx` also works as a tag: tx`Remove data source '${name}'?` looks up
"Remove data source '{name}'?" by its literal parts, fills in the values
(translated when the catalog has them) and remembers the result, so the
primitives that pass strings through `tx` translate it again after a locale
change or once the catalog has loaded (actions registered at startup).
- 269 template literals at exact-lookup sites now use the tag; text that
spliced words or plural suffixes is written as one sentence per variant
(safe delete, Docker delete/prune, rebase, conflicts, VCS "No … in
progress", database copy errors, inspection descriptions, ...).
- zh: 280 new entries; stale and duplicate-shape entries removed.
- textCatalog guard: untagged template literals at exact-lookup sites fail
(with an allowlist for code-like text), tagged text is checked against the
catalog by shape, case-changing values and `c ? "a" : "b"` suffixes inside
translated templates fail, and a catalog may hold one entry per template shape.
Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
207 lines
9.0 KiB
TypeScript
207 lines
9.0 KiB
TypeScript
/** What the Services toolbar, context menu and `docker.*` actions do with a node of a connection. */
|
|
import { tx, txTemplate } from "@/platform/i18n";
|
|
import type { PruneTarget } from "@/platform/ipc";
|
|
import { executeAction } from "@/platform/actions";
|
|
import type { MenuItem } from "@/ui";
|
|
import { confirm, confirmChecked } from "@/workbench/messages";
|
|
import { promptText } from "./dialogs";
|
|
import { isRunning, nodeContainers, nodeLabel, shortId, type DockerTarget, type ServiceNode } from "./model";
|
|
import { composeOp, connectionState, containerOp, prune, pullImage, refresh, removeObjects, useServices } from "./store";
|
|
|
|
export function canStart(t: DockerTarget | null): boolean {
|
|
return !!t && nodeContainers(t.node).some((c) => !isRunning(c));
|
|
}
|
|
export function canStop(t: DockerTarget | null): boolean {
|
|
return !!t && nodeContainers(t.node).some(isRunning);
|
|
}
|
|
export function canRestart(t: DockerTarget | null): boolean {
|
|
return !!t && (t.node.kind === "container" || t.node.kind === "compose");
|
|
}
|
|
export function canRemove(t: DockerTarget | null): boolean {
|
|
return !!t && ["container", "compose", "image", "volume", "network"].includes(t.node.kind);
|
|
}
|
|
|
|
export async function start(t: DockerTarget | null) {
|
|
if (!t) return;
|
|
if (t.node.kind === "compose") return composeAction(t.conn, t.node, "start");
|
|
const ids = nodeContainers(t.node).filter((c) => !isRunning(c)).map((c) => c.id);
|
|
if (ids.length) await containerOp(t.conn, ids, "start");
|
|
}
|
|
|
|
export async function stop(t: DockerTarget | null) {
|
|
if (!t) return;
|
|
if (t.node.kind === "compose") return composeAction(t.conn, t.node, "stop");
|
|
const ids = nodeContainers(t.node).filter(isRunning).map((c) => c.id);
|
|
if (ids.length) await containerOp(t.conn, ids, "stop");
|
|
}
|
|
|
|
export async function restart(t: DockerTarget | null) {
|
|
if (!t) return;
|
|
if (t.node.kind === "compose") return composeAction(t.conn, t.node, "restart");
|
|
const ids = nodeContainers(t.node).map((c) => c.id);
|
|
if (ids.length) await containerOp(t.conn, ids, "restart");
|
|
}
|
|
|
|
function composeAction(conn: string, n: Extract<ServiceNode, { kind: "compose" }>, op: "start" | "stop" | "restart" | "down") {
|
|
const first = n.containers[0];
|
|
return composeOp(conn, n.project, first?.composeFiles ?? [], first?.composeWorkingDir ?? null, op).then(() => undefined);
|
|
}
|
|
|
|
function deleteQuestion(kind: "container" | "image" | "volume" | "network", label: string, running: boolean) {
|
|
switch (kind) {
|
|
case "container":
|
|
return {
|
|
title: "Delete Container",
|
|
message: running
|
|
? tx`Delete container '${label}'? It is running and will be stopped.`
|
|
: tx`Delete container '${label}'?`,
|
|
};
|
|
case "image":
|
|
return { title: "Delete Image", message: tx`Delete image '${label}'?` };
|
|
case "volume":
|
|
return { title: "Delete Volume", message: tx`Delete volume '${label}'?` };
|
|
case "network":
|
|
return { title: "Delete Network", message: tx`Delete network '${label}'?` };
|
|
}
|
|
}
|
|
|
|
export async function remove(t: DockerTarget | null) {
|
|
if (!t || !canRemove(t)) return;
|
|
const n = t.node;
|
|
const label = nodeLabel(n);
|
|
if (n.kind === "compose") {
|
|
const ok = await confirm({
|
|
title: "Compose Down",
|
|
message: tx`Stop and remove the containers and networks of '${label}'?`,
|
|
ok: "Down",
|
|
});
|
|
if (ok) await composeAction(t.conn, n, "down");
|
|
return;
|
|
}
|
|
if (n.kind !== "container" && n.kind !== "image" && n.kind !== "volume" && n.kind !== "network") return;
|
|
const what = n.kind;
|
|
const running = n.kind === "container" && isRunning(n.container);
|
|
const answer = await confirmChecked({
|
|
...deleteQuestion(what, label, running),
|
|
ok: "Delete",
|
|
checkbox: n.kind === "image" ? "Force (also untag / remove when used by stopped containers)" : undefined,
|
|
});
|
|
if (!answer) return;
|
|
const id = n.kind === "container" ? n.container.id : n.kind === "image" ? n.image.id : n.kind === "volume" ? n.volume.name : n.network.id;
|
|
await removeObjects(t.conn, what, [id], running || answer.checked);
|
|
}
|
|
|
|
/** Opens a terminal tab with a shell in the container (bash when present, else sh). */
|
|
export function execShell(t: DockerTarget | null) {
|
|
const c = t?.node.kind === "container" ? t.node.container : null;
|
|
if (!t || !c || !isRunning(c)) return;
|
|
const snap = connectionState(t.conn).snapshot;
|
|
void executeAction("terminal.runCommand", {
|
|
program: snap?.executable ?? "docker",
|
|
args: [...(snap?.globalArgs ?? []), "exec", "-it", c.id, "sh", "-c", "command -v bash >/dev/null 2>&1 && exec bash || exec sh"],
|
|
name: c.name,
|
|
});
|
|
}
|
|
|
|
export async function pullPrompt(conn: string) {
|
|
const ref = await promptText("Pull Image", "Image to pull:", "e.g. postgres:16 or ghcr.io/org/app:latest");
|
|
if (ref) await pullImage(conn, ref);
|
|
}
|
|
|
|
const PRUNE: Array<[PruneTarget, string, string]> = [
|
|
["containers", "Stopped Containers", "Remove all stopped containers?"],
|
|
["images", "Dangling Images", "Remove all dangling (untagged) images?"],
|
|
["allImages", "Unused Images", "Remove all images not used by a container?"],
|
|
["volumes", "Unused Volumes", "Remove all anonymous volumes not used by a container? Their data is lost."],
|
|
["networks", "Unused Networks", "Remove all networks not used by a container?"],
|
|
["system", "Everything Unused (System Prune)", "Remove stopped containers, unused networks, dangling images and the build cache?"],
|
|
];
|
|
|
|
export async function prunePrompt(conn: string, target: PruneTarget) {
|
|
const entry = PRUNE.find(([t]) => t === target);
|
|
if (!entry) return;
|
|
const ok = await confirm({ title: txTemplate(`Prune ${entry[1]}`), message: entry[2], ok: "Prune" });
|
|
if (ok) await prune(conn, target);
|
|
}
|
|
|
|
function pruneLabel(target: PruneTarget): string {
|
|
switch (target) {
|
|
case "containers":
|
|
return tx("Prune Unused Containers…");
|
|
case "images":
|
|
return tx("Prune Dangling Images…");
|
|
case "allImages":
|
|
return tx("Prune Unused Images…");
|
|
case "volumes":
|
|
return tx("Prune Unused Volumes…");
|
|
case "networks":
|
|
return tx("Prune Unused Networks…");
|
|
case "system":
|
|
return tx("Prune Everything Unused…");
|
|
}
|
|
}
|
|
|
|
export function pruneMenu(conn: string): MenuItem[] {
|
|
return PRUNE.map(([target, label]) => ({ label, run: () => void prunePrompt(conn, target) }));
|
|
}
|
|
|
|
const copy = (text: string) => void navigator.clipboard?.writeText(text);
|
|
|
|
export function showDetail(tab: "log" | "inspect") {
|
|
useServices.setState({ detailTab: tab });
|
|
}
|
|
|
|
/** Context menu of a node of a connection's subtree. */
|
|
export function nodeMenu(t: DockerTarget): MenuItem[] {
|
|
const { conn, node: n } = t;
|
|
const items: MenuItem[] = [];
|
|
if (n.kind === "container" || n.kind === "compose") {
|
|
items.push(
|
|
{ label: "Start", icon: "run", disabled: !canStart(t), run: () => void start(t) },
|
|
{ label: "Stop", icon: "stop", disabled: !canStop(t), run: () => void stop(t) },
|
|
{ label: "Restart", icon: "rerun", run: () => void restart(t) },
|
|
);
|
|
}
|
|
if (n.kind === "container") {
|
|
const running = isRunning(n.container);
|
|
items.push(
|
|
n.container.state === "paused"
|
|
? { label: "Unpause", icon: "resume", run: () => void containerOp(conn, [n.container.id], "unpause") }
|
|
: { label: "Pause", icon: "pause", disabled: !running, run: () => void containerOp(conn, [n.container.id], "pause") },
|
|
{ kind: "separator" },
|
|
{ label: "Show Log", icon: "file-log", run: () => showDetail("log") },
|
|
{ label: "Inspect", icon: "info", run: () => showDetail("inspect") },
|
|
{ label: "Create Terminal", icon: "terminal", disabled: !running, run: () => execShell(t) },
|
|
{ kind: "separator" },
|
|
{ label: "Copy Container ID", icon: "copy", run: () => copy(n.container.id) },
|
|
{ label: "Copy Image Name", run: () => copy(n.container.image) },
|
|
);
|
|
}
|
|
if (n.kind === "image" || n.kind === "volume" || n.kind === "network") {
|
|
items.push({ label: "Inspect", icon: "info", run: () => showDetail("inspect") });
|
|
if (n.kind === "image")
|
|
items.push(
|
|
{ label: "Pull Again", icon: "git-pull", disabled: n.image.repository === "<none>", run: () => void pullImage(conn, nodeLabel(n)) },
|
|
{ label: "Copy Image ID", icon: "copy", run: () => copy(shortId(n.image.id)) },
|
|
);
|
|
}
|
|
if (n.kind === "group" && n.group === "images") items.push({ label: "Pull Image…", icon: "git-pull", run: () => void pullPrompt(conn) });
|
|
if (n.kind === "group") {
|
|
const target: PruneTarget = n.group === "containers" ? "containers" : n.group === "images" ? "images" : n.group === "volumes" ? "volumes" : "networks";
|
|
items.push({
|
|
label: pruneLabel(target),
|
|
run: () => void prunePrompt(conn, target),
|
|
});
|
|
}
|
|
if (n.kind === "docker")
|
|
items.push(
|
|
{ label: "Refresh", icon: "refresh", run: () => void refresh(conn) },
|
|
{ label: "Pull Image…", icon: "git-pull", run: () => void pullPrompt(conn) },
|
|
{ label: "Prune", submenu: pruneMenu(conn) },
|
|
);
|
|
if (canRemove(t)) {
|
|
items.push({ kind: "separator" }, { label: n.kind === "compose" ? "Compose Down" : "Delete…", icon: "trash", danger: true, run: () => void remove(t) });
|
|
}
|
|
return items;
|
|
}
|