Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { beforeEach, describe, expect, it, mock } from "bun:test";
import type { AlertOptions } from "@superset/ui/atoms/Alert";

let suppressed = false;
const suppress = mock(() => {
suppressed = true;
});

mock.module("renderer/stores/terminal-close-confirm/store", () => ({
useTerminalCloseConfirmStore: {
getState: () => ({ suppressed, suppress }),
},
}));

const { confirmClosePorts } = await import("./confirmClosePorts");

describe("confirmClosePorts", () => {
beforeEach(() => {
suppressed = false;
suppress.mockClear();
});

it("confirms a single port with running-process copy", async () => {
let options: AlertOptions | undefined;
const showAlert = mock((nextOptions: AlertOptions) => {
options = nextOptions;
return true;
});

const confirmation = confirmClosePorts(1, showAlert);

expect(options?.title).toBe("This port is still in use");
expect(options?.description).toBe(
"Closing this port will end the process using it.",
);
expect(options?.actions[0]?.label).toBe("Close port");

await options?.actions[0]?.onClick?.({ checkboxChecked: false });
expect(await confirmation).toBe(true);
});

it("uses plural copy and resolves false when canceled", async () => {
let options: AlertOptions | undefined;
const showAlert = (nextOptions: AlertOptions) => {
options = nextOptions;
return true;
};

const confirmation = confirmClosePorts(2, showAlert);

expect(options?.title).toBe("These ports are still in use");
expect(options?.description).toBe(
"Closing these ports will end the processes using them.",
);
expect(options?.actions[0]?.label).toBe("Close ports");

await options?.actions[1]?.onClick?.({ checkboxChecked: false });
expect(await confirmation).toBe(false);
});

it("resolves false when the dialog is dismissed", async () => {
let options: AlertOptions | undefined;
const confirmation = confirmClosePorts(1, (nextOptions) => {
options = nextOptions;
return true;
});

options?.onDismiss?.();

expect(await confirmation).toBe(false);
});

it("persists the shared running-process suppression preference", async () => {
let options: AlertOptions | undefined;
const showAlert = mock((nextOptions: AlertOptions) => {
options = nextOptions;
return true;
});
const confirmation = confirmClosePorts(1, showAlert);

await options?.actions[0]?.onClick?.({
checkboxChecked: true,
});
expect(await confirmation).toBe(true);
expect(suppressed).toBe(true);
expect(suppress).toHaveBeenCalledTimes(1);

expect(await confirmClosePorts(1, showAlert)).toBe(true);
expect(showAlert).toHaveBeenCalledTimes(1);
});

it("fails open when the alert layer is unavailable", async () => {
expect(await confirmClosePorts(1, () => false)).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { type AlertOptions, alert } from "@superset/ui/atoms/Alert";
import { useTerminalCloseConfirmStore } from "renderer/stores/terminal-close-confirm/store";

type ShowAlert = (options: AlertOptions) => boolean;

/**
* Confirm before terminating the process or processes that own detected ports.
* This shares the running-process suppression preference with terminal close
* confirmations so "Don't ask again" behaves consistently.
*/
export function confirmClosePorts(
portCount: number,
showAlert: ShowAlert = alert,
): Promise<boolean> {
if (portCount === 0 || useTerminalCloseConfirmStore.getState().suppressed) {
return Promise.resolve(true);
}

const isSinglePort = portCount === 1;

return new Promise<boolean>((resolve) => {
const shown = showAlert({
title: isSinglePort
? "This port is still in use"
: "These ports are still in use",
description: isSinglePort
? "Closing this port will end the process using it."
: "Closing these ports will end the processes using them.",
checkbox: { label: "Don't ask again" },
onDismiss: () => resolve(false),
actions: [
{
label: isSinglePort ? "Close port" : "Close ports",
variant: "destructive",
onClick: ({ checkboxChecked }) => {
if (checkboxChecked) {
useTerminalCloseConfirmStore.getState().suppress();
}
resolve(true);
},
},
{ label: "Cancel", variant: "ghost", onClick: () => resolve(false) },
],
});

// Match terminal-close behavior: never leave the action hanging if the
// global alert layer is unavailable.
if (!shown) resolve(true);
});
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { toast } from "@superset/ui/sonner";
import { type QueryKey, useQueryClient } from "@tanstack/react-query";
import { useCallback, useState } from "react";
import { confirmClosePorts } from "./confirmClosePorts";
import {
killPortTarget,
type LocalPortKill,
Expand Down Expand Up @@ -36,7 +37,9 @@ export function usePortKillActions<TPort extends PortKillTarget>({
}, [queryClient, refreshQueryKey]);

const killPort = useCallback(
async (port: TPort): Promise<PortKillResult> => {
async (port: TPort): Promise<PortKillResult | undefined> => {
if (!(await confirmClosePorts(1))) return;

setPendingCount((count) => count + 1);
try {
const result = await killPortTarget(port, localKill);
Expand All @@ -57,6 +60,7 @@ export function usePortKillActions<TPort extends PortKillTarget>({
const killPorts = useCallback(
async (ports: TPort[]): Promise<PortKillResult[]> => {
if (ports.length === 0) return [];
if (!(await confirmClosePorts(ports.length))) return [];

setPendingCount((count) => count + 1);
try {
Expand Down
Loading