fix: use saved SSH keys for task runners
This commit is contained in:
@@ -34,6 +34,7 @@ import {
|
|||||||
useTestServiceInstance,
|
useTestServiceInstance,
|
||||||
} from "../hooks/useServices";
|
} from "../hooks/useServices";
|
||||||
import { useServiceTypes } from "../hooks/useServices";
|
import { useServiceTypes } from "../hooks/useServices";
|
||||||
|
import { useSSHKeys } from "../hooks/useSettings";
|
||||||
import {
|
import {
|
||||||
useDashboards,
|
useDashboards,
|
||||||
useDeleteDashboard,
|
useDeleteDashboard,
|
||||||
@@ -45,6 +46,7 @@ import type {
|
|||||||
ServiceInstanceInput,
|
ServiceInstanceInput,
|
||||||
ServiceTestResult,
|
ServiceTestResult,
|
||||||
ServiceTypeInfo,
|
ServiceTypeInfo,
|
||||||
|
SSHKey,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { SectionCard } from "../components/SectionCard";
|
import { SectionCard } from "../components/SectionCard";
|
||||||
import { ConfirmDialog } from "../components/ConfirmDialog";
|
import { ConfirmDialog } from "../components/ConfirmDialog";
|
||||||
@@ -92,10 +94,12 @@ function ServiceConfigFields({
|
|||||||
type,
|
type,
|
||||||
config,
|
config,
|
||||||
onChange,
|
onChange,
|
||||||
|
sshKeys,
|
||||||
}: {
|
}: {
|
||||||
type: ServiceTypeInfo;
|
type: ServiceTypeInfo;
|
||||||
config: Record<string, unknown>;
|
config: Record<string, unknown>;
|
||||||
onChange: (config: Record<string, unknown>) => void;
|
onChange: (config: Record<string, unknown>) => void;
|
||||||
|
sshKeys: SSHKey[];
|
||||||
}) {
|
}) {
|
||||||
const properties =
|
const properties =
|
||||||
(
|
(
|
||||||
@@ -109,6 +113,7 @@ function ServiceConfigFields({
|
|||||||
// Multi-line resizable textarea for fields that hold complex values (opt-in
|
// Multi-line resizable textarea for fields that hold complex values (opt-in
|
||||||
// via `format: "textarea"`, or well-known multi-line keys).
|
// via `format: "textarea"`, or well-known multi-line keys).
|
||||||
const TEXTAREA_KEYS = new Set(["notes", "command"]);
|
const TEXTAREA_KEYS = new Set(["notes", "command"]);
|
||||||
|
const noSSHKey = "__no_ssh_key__";
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{Object.entries(properties).map(([key, schema]) => {
|
{Object.entries(properties).map(([key, schema]) => {
|
||||||
@@ -118,11 +123,37 @@ function ServiceConfigFields({
|
|||||||
return (
|
return (
|
||||||
<Field
|
<Field
|
||||||
key={key}
|
key={key}
|
||||||
label={key}
|
label={
|
||||||
|
type.service_type === "ssh_tasks" && key === "ssh_key_id"
|
||||||
|
? "SSH key"
|
||||||
|
: key
|
||||||
|
}
|
||||||
htmlFor={`cfg-${key}`}
|
htmlFor={`cfg-${key}`}
|
||||||
helper={schema.description}
|
helper={schema.description}
|
||||||
>
|
>
|
||||||
{isTextarea ? (
|
{type.service_type === "ssh_tasks" && key === "ssh_key_id" ? (
|
||||||
|
<Select
|
||||||
|
value={String(config[key] ?? "") || noSSHKey}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onChange({
|
||||||
|
...config,
|
||||||
|
[key]: value === noSSHKey ? "" : value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger id={`cfg-${key}`} className="w-full">
|
||||||
|
<SelectValue placeholder="Select an SSH key" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={noSSHKey}>No key selected</SelectItem>
|
||||||
|
{sshKeys.map((sshKey) => (
|
||||||
|
<SelectItem key={sshKey.id} value={sshKey.id}>
|
||||||
|
{sshKey.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
) : isTextarea ? (
|
||||||
<Textarea
|
<Textarea
|
||||||
id={`cfg-${key}`}
|
id={`cfg-${key}`}
|
||||||
rows={6}
|
rows={6}
|
||||||
@@ -195,6 +226,7 @@ export function CreateServiceDialog({
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { data: types = [] } = useServiceTypes();
|
const { data: types = [] } = useServiceTypes();
|
||||||
|
const { data: sshKeys = [] } = useSSHKeys();
|
||||||
const saveService = useSaveServiceInstance();
|
const saveService = useSaveServiceInstance();
|
||||||
const testService = useTestServiceInstance();
|
const testService = useTestServiceInstance();
|
||||||
const [draft, setDraft] = useState<CreateDraft | null>(null);
|
const [draft, setDraft] = useState<CreateDraft | null>(null);
|
||||||
@@ -306,6 +338,7 @@ export function CreateServiceDialog({
|
|||||||
<ServiceConfigFields
|
<ServiceConfigFields
|
||||||
type={selectedType}
|
type={selectedType}
|
||||||
config={draft.config}
|
config={draft.config}
|
||||||
|
sshKeys={sshKeys}
|
||||||
onChange={(config) => setDraft({ ...draft, config })}
|
onChange={(config) => setDraft({ ...draft, config })}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -1477,6 +1477,7 @@ function ServiceConfigEditor({
|
|||||||
const saveService = useSaveServiceInstance();
|
const saveService = useSaveServiceInstance();
|
||||||
const deleteService = useDeleteServiceInstance();
|
const deleteService = useDeleteServiceInstance();
|
||||||
const testService = useTestServiceInstance();
|
const testService = useTestServiceInstance();
|
||||||
|
const { data: sshKeys = [] } = useSSHKeys();
|
||||||
const [name, setName] = useState(instance.name);
|
const [name, setName] = useState(instance.name);
|
||||||
const [enabled, setEnabled] = useState(instance.enabled);
|
const [enabled, setEnabled] = useState(instance.enabled);
|
||||||
const [draftConfig, setDraftConfig] = useState<Record<string, unknown>>({
|
const [draftConfig, setDraftConfig] = useState<Record<string, unknown>>({
|
||||||
@@ -1598,10 +1599,41 @@ function ServiceConfigEditor({
|
|||||||
return (
|
return (
|
||||||
<FormField
|
<FormField
|
||||||
key={key}
|
key={key}
|
||||||
label={key}
|
label={
|
||||||
|
instance.service_type === "ssh_tasks" && key === "ssh_key_id"
|
||||||
|
? "SSH key"
|
||||||
|
: key
|
||||||
|
}
|
||||||
htmlFor={`svc-cfg-${instance.id}-${key}`}
|
htmlFor={`svc-cfg-${instance.id}-${key}`}
|
||||||
helperText={schema.description}
|
helperText={schema.description}
|
||||||
>
|
>
|
||||||
|
{instance.service_type === "ssh_tasks" &&
|
||||||
|
key === "ssh_key_id" ? (
|
||||||
|
<Select
|
||||||
|
value={String(draftConfig[key] ?? "") || NONE}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setDraftConfig({
|
||||||
|
...draftConfig,
|
||||||
|
[key]: value === NONE ? "" : value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id={`svc-cfg-${instance.id}-${key}`}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Select an SSH key" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={NONE}>No key selected</SelectItem>
|
||||||
|
{sshKeys.map((sshKey) => (
|
||||||
|
<SelectItem key={sshKey.id} value={sshKey.id}>
|
||||||
|
{sshKey.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
<Input
|
<Input
|
||||||
id={`svc-cfg-${instance.id}-${key}`}
|
id={`svc-cfg-${instance.id}-${key}`}
|
||||||
type={isNumber ? "number" : "text"}
|
type={isNumber ? "number" : "text"}
|
||||||
@@ -1617,6 +1649,7 @@ function ServiceConfigEditor({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</FormField>
|
</FormField>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const saveServiceMutate = vi.fn().mockResolvedValue({});
|
|||||||
// tab, so stub them out to keep the render focused on the Services editor.
|
// tab, so stub them out to keep the render focused on the Services editor.
|
||||||
vi.mock("../../hooks/useSettings", () => ({
|
vi.mock("../../hooks/useSettings", () => ({
|
||||||
useMonitoringSettings: () => ({ data: [] }),
|
useMonitoringSettings: () => ({ data: [] }),
|
||||||
useSSHKeys: () => ({ data: [] }),
|
useSSHKeys: () => ({ data: [{ id: "key-1", name: "Production key" }] }),
|
||||||
useSaveMonitoringMachine: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
useSaveMonitoringMachine: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
||||||
useDeleteMonitoringMachine: () => ({ mutate: vi.fn() }),
|
useDeleteMonitoringMachine: () => ({ mutate: vi.fn() }),
|
||||||
useTestMonitoringMachineSSH: () => ({
|
useTestMonitoringMachineSSH: () => ({
|
||||||
@@ -75,6 +75,22 @@ vi.mock("../../hooks/useServices", () => ({
|
|||||||
],
|
],
|
||||||
widget_kinds: [],
|
widget_kinds: [],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
service_type: "ssh_tasks",
|
||||||
|
name: "SSH task runner",
|
||||||
|
description: "Run saved tasks over SSH",
|
||||||
|
config_schema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
host: { type: "string" },
|
||||||
|
port: { type: "integer" },
|
||||||
|
username: { type: "string" },
|
||||||
|
ssh_key_id: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
secret_fields: [],
|
||||||
|
widget_kinds: [],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
useServiceInstances: () => ({
|
useServiceInstances: () => ({
|
||||||
@@ -156,6 +172,15 @@ describe("Settings > Services editor", () => {
|
|||||||
|
|
||||||
await userEvent.click(screen.getByRole("button", { name: "Add service" }));
|
await userEvent.click(screen.getByRole("button", { name: "Add service" }));
|
||||||
expect(screen.getByText("New service")).toBeInTheDocument();
|
expect(screen.getByText("New service")).toBeInTheDocument();
|
||||||
|
await userEvent.click(
|
||||||
|
screen.getByRole("button", { name: "SSH task runner" }),
|
||||||
|
);
|
||||||
|
const sshKeySelect = screen.getByRole("combobox", { name: "SSH key" });
|
||||||
|
await userEvent.click(sshKeySelect);
|
||||||
|
expect(
|
||||||
|
screen.getByRole("option", { name: "Production key" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
await userEvent.keyboard("{Escape}");
|
||||||
await userEvent.keyboard("{Escape}");
|
await userEvent.keyboard("{Escape}");
|
||||||
|
|
||||||
await userEvent.click(screen.getByRole("tab", { name: "Dashboards" }));
|
await userEvent.click(screen.getByRole("tab", { name: "Dashboards" }));
|
||||||
|
|||||||
Reference in New Issue
Block a user