mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
feat(run): mark running configurations in the chooser, with Rerun and Stop (#1118)
A configuration with a running or debugged process gets a green dot on its icon in the Run/Debug configuration popup; its row offers Rerun and Stop in place of Run and Debug. Rerun closes the popup, Stop keeps it open. Refs #997
This commit is contained in:
@@ -5,9 +5,11 @@ import { shortcutLabel } from "@/platform/keymap";
|
||||
import { HighlightedText, Icon, IconButton, List, Popup, SearchField, fuzzyMatch, listNavigate, T } from "@/ui";
|
||||
import type { RunConfig } from "@/platform/ipc";
|
||||
import { useWorkflowSettings } from "@/platform/workflowSettings";
|
||||
import { useDebugRunSessions } from "@/workbench/contributions";
|
||||
import { commandLine, configIcon, groupConfigs } from "./configs";
|
||||
import { targetName } from "./remote";
|
||||
import { canDeleteConfig, CURRENT_FILE, deleteConfig, saveTemporary, selectConfig, startRun, useRun } from "./store";
|
||||
import { selectedTarget } from "./sessionTargets";
|
||||
import styles from "./Run.module.css";
|
||||
import { tx } from "@/platform/i18n";
|
||||
|
||||
@@ -79,6 +81,8 @@ function ConfigChooser() {
|
||||
const selectedId = useRun((s) => s.selectedId);
|
||||
const temporary = useRun((s) => s.temporary);
|
||||
const runTargets = useWorkflowSettings((s) => s.runTargets);
|
||||
const sessions = useRun((s) => s.sessions);
|
||||
const debugSessions = useDebugRunSessions((s) => s.sessions);
|
||||
const [query, setQuery] = useState("");
|
||||
const rows = useMemo(
|
||||
() =>
|
||||
@@ -272,13 +276,16 @@ function ConfigChooser() {
|
||||
}
|
||||
if (r.type !== "config") return null;
|
||||
const c = r.config;
|
||||
const running = selectedTarget(c.id, sessions, debugSessions);
|
||||
return (
|
||||
<span className={styles.chooserRow} title={commandLine(c)}>
|
||||
<Icon
|
||||
name={configIcon(c)}
|
||||
size={13}
|
||||
className={clsx(styles.chooserIcon, temporary.includes(c.id) && styles.chooserTemporary)}
|
||||
/>
|
||||
<span className={clsx(styles.chooserIcon, running && styles.chooserRunning)}>
|
||||
<Icon
|
||||
name={configIcon(c)}
|
||||
size={13}
|
||||
className={clsx(temporary.includes(c.id) && styles.chooserTemporary)}
|
||||
/>
|
||||
</span>
|
||||
{numbers.size > 0 && <span className={styles.chooserMnemonic}>{numbers.get(r.key) ?? ""}</span>}
|
||||
<span className={clsx(styles.chooserName, c.id === selectedId && styles.chooserSelected)}>
|
||||
{r.indices ? <HighlightedText text={c.name} indices={r.indices} /> : c.name}
|
||||
@@ -299,7 +306,35 @@ function ConfigChooser() {
|
||||
<T>temporary</T>
|
||||
</span>
|
||||
)}
|
||||
{isActive && (
|
||||
{isActive && running && (
|
||||
<span className={styles.chooserActions}>
|
||||
<IconButton
|
||||
icon={running.debug ? "restart-debug-filled" : "rerun-filled"}
|
||||
size="small"
|
||||
iconSize={11}
|
||||
title={tx`Rerun '${c.name}'`}
|
||||
tabIndex={-1}
|
||||
disabled={!running.rerun}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
close();
|
||||
running.rerun?.();
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
icon="stop-filled"
|
||||
size="small"
|
||||
iconSize={11}
|
||||
title={tx`Stop '${c.name}'`}
|
||||
tabIndex={-1}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
running.stop();
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
{isActive && !running && (
|
||||
<span className={styles.chooserActions}>
|
||||
<IconButton
|
||||
icon="run"
|
||||
|
||||
@@ -331,6 +331,22 @@
|
||||
flex: none;
|
||||
color: var(--fg-subtle);
|
||||
}
|
||||
/* A running configuration: a dot on its icon. */
|
||||
.chooserRunning {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
.chooserRunning::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: -2px;
|
||||
bottom: -1px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--success);
|
||||
box-shadow: 0 0 0 1px var(--bg-popup);
|
||||
}
|
||||
.chooserName {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
|
||||
vi.mock("@/platform/ipc", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("@/platform/ipc")>()),
|
||||
isTauri: () => false,
|
||||
}));
|
||||
|
||||
import type { RunConfig } from "@/platform/ipc";
|
||||
import { useDebugRunSessions } from "@/workbench/contributions";
|
||||
import { ConfigChooserOverlay } from "./ConfigChooser";
|
||||
import { resetRuns, useRun, type RunSession } from "./store";
|
||||
|
||||
const config = (id: string, name: string): RunConfig => ({
|
||||
id,
|
||||
name,
|
||||
kind: "npm",
|
||||
command: "run",
|
||||
args: [],
|
||||
cwd: null,
|
||||
env: {},
|
||||
detected: true,
|
||||
isTest: false,
|
||||
debugAdapter: null,
|
||||
});
|
||||
const server = config("npm:server", "server");
|
||||
const worker = config("npm:worker", "worker");
|
||||
const idle = config("npm:idle", "idle");
|
||||
|
||||
const rerun = vi.fn();
|
||||
const stop = vi.fn();
|
||||
beforeEach(() => {
|
||||
resetRuns();
|
||||
const session = { id: "s1", config: server, runId: null, status: "running", startedAt: 1 } as RunSession;
|
||||
useRun.setState({
|
||||
configs: [server, worker, idle],
|
||||
loaded: true,
|
||||
selectedId: server.id,
|
||||
sessions: [session],
|
||||
chooserOpen: true,
|
||||
});
|
||||
useDebugRunSessions.setState({
|
||||
sessions: [
|
||||
{ id: 7, configId: worker.id, name: "worker", startedAt: 2, paused: false, rerun, stop, resume: () => {} },
|
||||
],
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
useDebugRunSessions.setState({ sessions: [] });
|
||||
rerun.mockReset();
|
||||
stop.mockReset();
|
||||
});
|
||||
|
||||
const row = (name: string) => screen.getByRole("option", { name: new RegExp(`^${name}`) });
|
||||
|
||||
it("marks running and debugged configurations", () => {
|
||||
render(<ConfigChooserOverlay />);
|
||||
const marked = (name: string) => !!row(name).querySelector("[class*='chooserRunning']");
|
||||
expect([marked("server"), marked("worker"), marked("idle")]).toEqual([true, true, false]);
|
||||
});
|
||||
|
||||
it("offers Rerun and Stop for a running configuration instead of Run and Debug", () => {
|
||||
render(<ConfigChooserOverlay />);
|
||||
fireEvent.pointerMove(row("worker"));
|
||||
expect(screen.queryByRole("button", { name: "Run 'worker'" })).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Stop 'worker'" }));
|
||||
expect(stop).toHaveBeenCalledOnce();
|
||||
expect(useRun.getState().chooserOpen).toBe(true);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Rerun 'worker'" }));
|
||||
expect(rerun).toHaveBeenCalledOnce();
|
||||
expect(useRun.getState().chooserOpen).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps Run and Debug for a configuration that isn't running", () => {
|
||||
render(<ConfigChooserOverlay />);
|
||||
fireEvent.pointerMove(row("idle"));
|
||||
expect(screen.getByRole("button", { name: "Run 'idle'" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Stop 'idle'" })).not.toBeInTheDocument();
|
||||
});
|
||||
Reference in New Issue
Block a user