feat(tasks): unify saved tasks on ssh_tasks services

- Add shared task_runner.run_saved_task helper used by routers/tasks.py and
  widgets/sources.py SshTaskWidgetSource.
- Saved tasks now target ssh_tasks service instances via default_service_id;
  the legacy default_machine_id and saved_task_runs are removed.
- Actions page lists ssh_tasks services for default and run-time selection.
- Update types, API client, hooks, tests, docs, and changelog.

Backend tests: 222 passed. Frontend lint/build/test: clean (71 passed).
This commit is contained in:
Developer
2026-06-23 13:55:13 +00:00
parent d7ad933b2a
commit 50eb76a10d
14 changed files with 371 additions and 410 deletions
+40 -28
View File
@@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Actions } from "../Actions";
import type { MonitoringMachine, SavedTask } from "../../types";
import type { SavedTask, ServiceInstance } from "../../types";
const saveTaskMutate = vi.fn().mockResolvedValue({
id: "t1",
@@ -10,17 +10,16 @@ const saveTaskMutate = vi.fn().mockResolvedValue({
task_type: "shell",
content: "",
enabled: true,
default_machine_id: "",
default_service_id: "",
notes: "",
});
const deleteTaskMutate = vi.fn();
const runTaskMutate = vi.fn().mockResolvedValue({});
let machines: MonitoringMachine[] = [];
let sshServices: ServiceInstance[] = [];
let tasks: SavedTask[] = [];
vi.mock("../../hooks/useSettings", () => ({
useMonitoringSettings: () => ({ data: machines }),
useTasks: () => ({ data: tasks }),
useSaveTask: () => ({ mutateAsync: saveTaskMutate, isPending: false }),
useDeleteTask: () => ({ mutate: deleteTaskMutate }),
@@ -28,29 +27,22 @@ vi.mock("../../hooks/useSettings", () => ({
useTaskRuns: () => ({ data: { items: [] } }),
}));
function machine(
overrides: Partial<MonitoringMachine> = {},
): MonitoringMachine {
vi.mock("../../hooks/useServices", () => ({
useServiceInstances: () => ({ data: sshServices }),
}));
function sshService(overrides: Partial<ServiceInstance> = {}): ServiceInstance {
return {
id: "m1",
name: "This machine",
mode: "local",
id: "s1",
service_type: "ssh_tasks",
name: "Box",
config: { host: "box", username: "u" },
secrets_set: {},
enabled: true,
services: ["monitoring", "files"],
host: "",
port: 22,
username: "",
key_directory: "",
key_name: "",
ssh_key_id: "",
ssh_private_key_set: false,
ssh_private_key_passphrase_set: false,
password_set: false,
media_root: "",
path_prefix: "",
notes: "",
created_at: 0,
updated_at: 0,
...overrides,
} as MonitoringMachine;
} as ServiceInstance;
}
function task(overrides: Partial<SavedTask> = {}): SavedTask {
@@ -60,7 +52,7 @@ function task(overrides: Partial<SavedTask> = {}): SavedTask {
task_type: "shell",
content: "systemctl restart foo",
enabled: true,
default_machine_id: "",
default_service_id: "",
notes: "",
created_at: 0,
updated_at: 0,
@@ -72,7 +64,7 @@ beforeEach(() => {
saveTaskMutate.mockClear();
deleteTaskMutate.mockClear();
runTaskMutate.mockClear();
machines = [];
sshServices = [];
tasks = [];
});
@@ -97,10 +89,11 @@ describe("Actions", () => {
const saved = saveTaskMutate.mock.calls[0][0];
expect(saved.name).toBe("Restart svc");
expect(saved.task_type).toBe("shell");
expect(saved.default_service_id).toBe("");
});
it("disables the Run button until a run machine is selected", async () => {
machines = [machine()];
it("disables the Run button until a run service is selected", async () => {
sshServices = [sshService()];
tasks = [task()];
render(<Actions />);
@@ -110,4 +103,23 @@ describe("Actions", () => {
const runButton = screen.getByRole("button", { name: "Run action" });
expect(runButton).toBeDisabled();
});
it("runs a task on the selected SSH task service", async () => {
sshServices = [sshService()];
tasks = [task()];
render(<Actions />);
await userEvent.click(screen.getByRole("tab", { name: "Restart svc" }));
await userEvent.click(
screen.getByRole("combobox", { name: "Run on SSH task service" }),
);
await userEvent.click(screen.getByRole("option", { name: "Box" }));
await userEvent.click(screen.getByRole("button", { name: "Run action" }));
expect(runTaskMutate).toHaveBeenCalledTimes(1);
expect(runTaskMutate).toHaveBeenCalledWith({
taskId: "t1",
serviceId: "s1",
});
});
});