feat: instance-level SSH key selection for container mounting
- Revert mistaken ssh_key_id from ConfigProfile (model, API, resolver, frontend)
- Add ssh_key_ids JSON column to tool_instances via migration
- Update create_instance to accept and store ssh_key_ids
- Update start_instance to mount selected SSH keys to {home_dir}/.ssh
- Update list_instances to return ssh_key_ids
- Frontend CreateSessionForm: multi-select SSH key checkboxes
- Frontend instance-list: SSH key selector for start/restart actions
- Maintain separate SSH key dirs per key to avoid conflicts
Quality gates: pytest (231 passed, 6 pre-existing), tsc --noEmit clean
This commit is contained in:
@@ -12,7 +12,6 @@ export interface ConfigProfile {
|
||||
mounts: ConfigProfileMount[];
|
||||
git_mounts: GitMount[];
|
||||
files: Record<string, string>;
|
||||
ssh_key_id: string | null;
|
||||
is_default: boolean;
|
||||
includes: ConfigProfileInclude[];
|
||||
created_at: string;
|
||||
@@ -53,7 +52,6 @@ export interface ResolvedProfile {
|
||||
mounts: ResolvedMount[];
|
||||
git_mounts: GitMount[];
|
||||
files: Record<string, string>;
|
||||
ssh_key_id: string | null;
|
||||
overrides: {
|
||||
env_vars: Record<string, string>;
|
||||
runtime_hints: Record<string, string>;
|
||||
@@ -80,7 +78,6 @@ export interface CreateConfigProfileRequest {
|
||||
mounts?: ConfigProfileMount[];
|
||||
git_mounts?: GitMount[];
|
||||
files?: Record<string, string>;
|
||||
ssh_key_id?: string;
|
||||
is_default?: boolean;
|
||||
}
|
||||
|
||||
@@ -94,7 +91,6 @@ export interface UpdateConfigProfileRequest {
|
||||
mounts?: ConfigProfileMount[];
|
||||
git_mounts?: GitMount[];
|
||||
files?: Record<string, string>;
|
||||
ssh_key_id?: string;
|
||||
is_default?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface ToolInstance {
|
||||
url: string | null;
|
||||
port: number | null;
|
||||
selected_config_profile_id: string | null;
|
||||
ssh_key_ids: string[];
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
@@ -52,7 +53,8 @@ export async function createInstance(
|
||||
cloneMode?: string,
|
||||
branch?: string,
|
||||
newBranch?: string,
|
||||
configProfileId?: string
|
||||
configProfileId?: string,
|
||||
sshKeyIds?: string[]
|
||||
): Promise<ToolInstance> {
|
||||
const response = await apiClient.post(
|
||||
`/projects/${projectId}/repositories/${repoId}/instances`,
|
||||
@@ -63,6 +65,7 @@ export async function createInstance(
|
||||
branch: branch || undefined,
|
||||
new_branch: newBranch || undefined,
|
||||
config_profile_id: configProfileId,
|
||||
ssh_key_ids: sshKeyIds || [],
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
@@ -73,12 +76,13 @@ export async function startInstance(
|
||||
repoId: string,
|
||||
instanceId: string,
|
||||
configProfileId?: string,
|
||||
sshKeyIds?: string[],
|
||||
retries = 2
|
||||
): Promise<{ status: string; url?: string }> {
|
||||
try {
|
||||
const response = await apiClient.post(
|
||||
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/start`,
|
||||
{ config_profile_id: configProfileId }
|
||||
{ config_profile_id: configProfileId, ssh_key_ids: sshKeyIds || [] }
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
@@ -86,7 +90,7 @@ export async function startInstance(
|
||||
const axiosError = error as AxiosError;
|
||||
if (retries > 0 && !axiosError.response) {
|
||||
await new Promise((r) => setTimeout(r, 1500));
|
||||
return startInstance(projectId, repoId, instanceId, configProfileId, retries - 1);
|
||||
return startInstance(projectId, repoId, instanceId, configProfileId, sshKeyIds, retries - 1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -108,12 +112,13 @@ export async function restartInstance(
|
||||
repoId: string,
|
||||
instanceId: string,
|
||||
configProfileId?: string,
|
||||
sshKeyIds?: string[],
|
||||
retries = 2
|
||||
): Promise<{ status: string; url?: string }> {
|
||||
try {
|
||||
const response = await apiClient.post(
|
||||
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/restart`,
|
||||
{ config_profile_id: configProfileId }
|
||||
{ config_profile_id: configProfileId, ssh_key_ids: sshKeyIds || [] }
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
@@ -121,7 +126,7 @@ export async function restartInstance(
|
||||
const axiosError = error as AxiosError;
|
||||
if (retries > 0 && !axiosError.response) {
|
||||
await new Promise((r) => setTimeout(r, 1500));
|
||||
return restartInstance(projectId, repoId, instanceId, configProfileId, retries - 1);
|
||||
return restartInstance(projectId, repoId, instanceId, configProfileId, sshKeyIds, retries - 1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ export const CreateSessionForm = ({
|
||||
const [sshKeys, setSshKeys] = useState<SSHKey[]>([]);
|
||||
const [configProfiles, setConfigProfiles] = useState<ConfigProfile[]>([]);
|
||||
const [selectedConfigProfile, setSelectedConfigProfile] = useState("");
|
||||
const [selectedSshKeyIds, setSelectedSshKeyIds] = useState<string[]>([]);
|
||||
|
||||
const [branches, setBranches] = useState<Branch[]>([]);
|
||||
const [isLoadingBranches, setIsLoadingBranches] = useState(false);
|
||||
@@ -60,9 +61,8 @@ export const CreateSessionForm = ({
|
||||
const [progress, setProgress] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Load SSH keys when clone mode is shown
|
||||
// Load SSH keys
|
||||
useEffect(() => {
|
||||
if (!showCloneMode) return;
|
||||
const loadKeys = async () => {
|
||||
try {
|
||||
const keys = await listSSHKeys();
|
||||
@@ -72,7 +72,7 @@ export const CreateSessionForm = ({
|
||||
}
|
||||
};
|
||||
void loadKeys();
|
||||
}, [showCloneMode]);
|
||||
}, []);
|
||||
|
||||
// Load config profiles when tool type is selected
|
||||
useEffect(() => {
|
||||
@@ -166,7 +166,8 @@ export const CreateSessionForm = ({
|
||||
showCloneMode && cloneMode === "clone" && isCreatingNewBranch
|
||||
? newBranchName
|
||||
: undefined,
|
||||
selectedConfigProfile || undefined
|
||||
selectedConfigProfile || undefined,
|
||||
selectedSshKeyIds.length > 0 ? selectedSshKeyIds : undefined
|
||||
);
|
||||
|
||||
setProgress("Starting container...");
|
||||
@@ -182,7 +183,8 @@ export const CreateSessionForm = ({
|
||||
setIsCreatingNewBranch(false);
|
||||
setNewBranchName("");
|
||||
setBaseBranch("");
|
||||
setBranches([]);
|
||||
setBranches([]);
|
||||
setSelectedSshKeyIds([]);
|
||||
setStatus("idle");
|
||||
|
||||
onSuccess?.(instance);
|
||||
@@ -344,8 +346,54 @@ export const CreateSessionForm = ({
|
||||
</label>
|
||||
)}
|
||||
|
||||
{/* Step 5: Clone Mode & Branch */}
|
||||
{showCloneMode && hasToolType && renderStep("Repository Access", 5, true, false,
|
||||
{/* Step 5: SSH Keys */}
|
||||
{hasToolType && renderStep("SSH Keys (optional)", 5, true, false,
|
||||
<div className="form-field">
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.5rem" }}>
|
||||
{sshKeys.length === 0 && (
|
||||
<span className="muted">No SSH keys configured.</span>
|
||||
)}
|
||||
{sshKeys.map((key) => (
|
||||
<label
|
||||
key={key.id}
|
||||
className="checkbox-label"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.25rem",
|
||||
padding: "0.375rem 0.75rem",
|
||||
background: "var(--panel)",
|
||||
borderRadius: "0.375rem",
|
||||
border: "1px solid var(--border)",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedSshKeyIds.includes(key.id)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedSshKeyIds((prev) => [...prev, key.id]);
|
||||
} else {
|
||||
setSelectedSshKeyIds((prev) =>
|
||||
prev.filter((id) => id !== key.id)
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
{key.name}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className="hint" style={{ marginTop: "0.5rem" }}>
|
||||
Selected keys will be mounted into the container at ~/.ssh
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 6: Clone Mode & Branch */}
|
||||
{showCloneMode && hasToolType && renderStep("Repository Access", 6, true, false,
|
||||
<div className="form-row">
|
||||
<label className="form-field">
|
||||
<div className="radio-group">
|
||||
@@ -468,8 +516,8 @@ export const CreateSessionForm = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 6: Display Name */}
|
||||
{hasToolType && renderStep("Display Name (optional)", 6, true, !!displayName,
|
||||
{/* Step 7: Display Name */}
|
||||
{hasToolType && renderStep("Display Name (optional)", 7, true, !!displayName,
|
||||
<label className="form-field">
|
||||
<input
|
||||
type="text"
|
||||
|
||||
@@ -6,7 +6,6 @@ import type { GitMount, GitMountMapping } from "../api/config_profiles";
|
||||
interface GitMountEditorProps {
|
||||
mounts: GitMount[];
|
||||
onChange: (mounts: GitMount[]) => void;
|
||||
defaultSshKeyId?: string;
|
||||
}
|
||||
|
||||
function normalizeMount(mount: GitMount): GitMount {
|
||||
@@ -34,7 +33,10 @@ function normalizeMounts(mounts: GitMount[]): GitMount[] {
|
||||
return mounts.map(normalizeMount);
|
||||
}
|
||||
|
||||
export const GitMountEditor = ({ mounts, onChange, defaultSshKeyId }: GitMountEditorProps) => {
|
||||
export const GitMountEditor = ({
|
||||
mounts,
|
||||
onChange,
|
||||
}: GitMountEditorProps) => {
|
||||
const [normalizedMounts, setNormalizedMounts] = useState<GitMount[]>(() =>
|
||||
normalizeMounts(mounts),
|
||||
);
|
||||
@@ -93,7 +95,6 @@ export const GitMountEditor = ({ mounts, onChange, defaultSshKeyId }: GitMountEd
|
||||
mount={mount}
|
||||
onSave={(updated) => handleUpdate(index, updated)}
|
||||
onCancel={() => setEditingIndex(null)}
|
||||
defaultSshKeyId={defaultSshKeyId}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
@@ -185,7 +186,6 @@ export const GitMountEditor = ({ mounts, onChange, defaultSshKeyId }: GitMountEd
|
||||
}}
|
||||
onSave={handleAdd}
|
||||
onCancel={() => setIsAdding(false)}
|
||||
defaultSshKeyId={defaultSshKeyId}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@@ -206,7 +206,6 @@ interface GitMountFormProps {
|
||||
mount: GitMount;
|
||||
onSave: (mount: GitMount) => void;
|
||||
onCancel: () => void;
|
||||
defaultSshKeyId?: string;
|
||||
}
|
||||
|
||||
type ValidationState =
|
||||
@@ -216,7 +215,11 @@ type ValidationState =
|
||||
| { status: "suggestion"; suggestedUrl: string; message: string }
|
||||
| { status: "invalid"; message: string };
|
||||
|
||||
const GitMountForm = ({ mount, onSave, onCancel, defaultSshKeyId }: GitMountFormProps) => {
|
||||
const GitMountForm = ({
|
||||
mount,
|
||||
onSave,
|
||||
onCancel,
|
||||
}: GitMountFormProps) => {
|
||||
const [remoteUrl, setRemoteUrl] = useState(mount.remote_url);
|
||||
const [branch, setBranch] = useState(mount.branch || "");
|
||||
const [mappings, setMappings] = useState<GitMountMapping[]>(
|
||||
@@ -245,10 +248,7 @@ const GitMountForm = ({ mount, onSave, onCancel, defaultSshKeyId }: GitMountForm
|
||||
return next;
|
||||
});
|
||||
try {
|
||||
const result = await validateGitUrl(
|
||||
remoteUrl.trim(),
|
||||
defaultSshKeyId,
|
||||
);
|
||||
const result = await validateGitUrl(remoteUrl.trim());
|
||||
if (result.valid && result.branches) {
|
||||
setValidation({
|
||||
status: "valid",
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import type { ToolType } from "../api/tool_types";
|
||||
import { CreateSessionForm } from "./create-session-form";
|
||||
import { listConfigProfiles, type ConfigProfile } from "../api/config_profiles";
|
||||
import { listSSHKeys, type SSHKey } from "../api/ssh_keys";
|
||||
import { useEventContext } from "../state/events";
|
||||
|
||||
const API_BASE_URL =
|
||||
@@ -47,6 +48,10 @@ export const InstanceList = ({
|
||||
string | null
|
||||
>(null);
|
||||
const [selectedProfileForAction, setSelectedProfileForAction] = useState("");
|
||||
const [selectedSshKeyIdsForAction, setSelectedSshKeyIdsForAction] = useState<
|
||||
string[]
|
||||
>([]);
|
||||
const [sshKeys, setSshKeys] = useState<SSHKey[]>([]);
|
||||
|
||||
// Per-instance busy state for actions
|
||||
const [busyInstanceId, setBusyInstanceId] = useState<string | null>(null);
|
||||
@@ -105,8 +110,12 @@ export const InstanceList = ({
|
||||
const loadConfigProfiles = useCallback(
|
||||
async (toolTypeId: string) => {
|
||||
try {
|
||||
const profiles = await listConfigProfiles(projectId, toolTypeId);
|
||||
const [profiles, keys] = await Promise.all([
|
||||
listConfigProfiles(projectId, toolTypeId),
|
||||
listSSHKeys(),
|
||||
]);
|
||||
setConfigProfiles(profiles);
|
||||
setSshKeys(keys);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -114,12 +123,23 @@ export const InstanceList = ({
|
||||
[projectId],
|
||||
);
|
||||
|
||||
const handleStart = async (instanceId: string, configProfileId?: string) => {
|
||||
const handleStart = async (
|
||||
instanceId: string,
|
||||
configProfileId?: string,
|
||||
sshKeyIds?: string[],
|
||||
) => {
|
||||
setBusyInstanceId(instanceId);
|
||||
try {
|
||||
await startInstance(projectId, repoId, instanceId, configProfileId);
|
||||
await startInstance(
|
||||
projectId,
|
||||
repoId,
|
||||
instanceId,
|
||||
configProfileId,
|
||||
sshKeyIds,
|
||||
);
|
||||
setProfileSelectInstanceId(null);
|
||||
setSelectedProfileForAction("");
|
||||
setSelectedSshKeyIdsForAction([]);
|
||||
await loadInstances();
|
||||
} catch {
|
||||
setError("Failed to start instance");
|
||||
@@ -144,12 +164,20 @@ export const InstanceList = ({
|
||||
const handleRestart = async (
|
||||
instanceId: string,
|
||||
configProfileId?: string,
|
||||
sshKeyIds?: string[],
|
||||
) => {
|
||||
setBusyInstanceId(instanceId);
|
||||
try {
|
||||
await restartInstance(projectId, repoId, instanceId, configProfileId);
|
||||
await restartInstance(
|
||||
projectId,
|
||||
repoId,
|
||||
instanceId,
|
||||
configProfileId,
|
||||
sshKeyIds,
|
||||
);
|
||||
setProfileSelectInstanceId(null);
|
||||
setSelectedProfileForAction("");
|
||||
setSelectedSshKeyIdsForAction([]);
|
||||
await loadInstances();
|
||||
} catch {
|
||||
setError("Failed to restart instance");
|
||||
@@ -279,69 +307,120 @@ export const InstanceList = ({
|
||||
)}
|
||||
{instance.status !== "running" && (
|
||||
<>
|
||||
{profileSelectInstanceId === instance.id ? (
|
||||
<div className="inline-profile-select">
|
||||
<select
|
||||
value={selectedProfileForAction}
|
||||
onChange={(e) =>
|
||||
setSelectedProfileForAction(e.target.value)
|
||||
}
|
||||
>
|
||||
<option value="">Default (none)</option>
|
||||
{configProfiles.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="primary-button small"
|
||||
onClick={() =>
|
||||
void handleStart(
|
||||
instance.id,
|
||||
selectedProfileForAction || undefined,
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
disabled={busyInstanceId === instance.id}
|
||||
>
|
||||
<Icon name="play" size="sm" />
|
||||
Start
|
||||
</button>
|
||||
<button
|
||||
className="ghost-button small"
|
||||
onClick={() => {
|
||||
setProfileSelectInstanceId(null);
|
||||
setSelectedProfileForAction("");
|
||||
{profileSelectInstanceId === instance.id ? (
|
||||
<div className="inline-profile-select">
|
||||
<select
|
||||
value={selectedProfileForAction}
|
||||
onChange={(e) =>
|
||||
setSelectedProfileForAction(e.target.value)
|
||||
}
|
||||
>
|
||||
<option value="">Default (none)</option>
|
||||
{configProfiles.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: "0.25rem",
|
||||
marginTop: "0.25rem",
|
||||
}}
|
||||
>
|
||||
{sshKeys.map((key) => (
|
||||
<label
|
||||
key={key.id}
|
||||
className="checkbox-label"
|
||||
style={{
|
||||
fontSize: "0.75rem",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.25rem",
|
||||
}}
|
||||
type="button"
|
||||
disabled={busyInstanceId === instance.id}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className="secondary-button small"
|
||||
onClick={() => {
|
||||
const toolType = toolTypes.find(
|
||||
(t) => t.id === instance.tool_type_id,
|
||||
);
|
||||
if (toolType) {
|
||||
void loadConfigProfiles(toolType.id);
|
||||
}
|
||||
setProfileSelectInstanceId(instance.id);
|
||||
setSelectedProfileForAction(
|
||||
instance.selected_config_profile_id || "",
|
||||
);
|
||||
}}
|
||||
type="button"
|
||||
disabled={busyInstanceId === instance.id}
|
||||
>
|
||||
<Icon name="play" size="sm" />
|
||||
Start
|
||||
</button>
|
||||
)}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedSshKeyIdsForAction.includes(
|
||||
key.id,
|
||||
)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedSshKeyIdsForAction(
|
||||
(prev) => [...prev, key.id],
|
||||
);
|
||||
} else {
|
||||
setSelectedSshKeyIdsForAction(
|
||||
(prev) =>
|
||||
prev.filter(
|
||||
(id) =>
|
||||
id !== key.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{key.name}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
className="primary-button small"
|
||||
onClick={() =>
|
||||
void handleStart(
|
||||
instance.id,
|
||||
selectedProfileForAction || undefined,
|
||||
selectedSshKeyIdsForAction.length > 0
|
||||
? selectedSshKeyIdsForAction
|
||||
: undefined,
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
disabled={busyInstanceId === instance.id}
|
||||
>
|
||||
<Icon name="play" size="sm" />
|
||||
Start
|
||||
</button>
|
||||
<button
|
||||
className="ghost-button small"
|
||||
onClick={() => {
|
||||
setProfileSelectInstanceId(null);
|
||||
setSelectedProfileForAction("");
|
||||
setSelectedSshKeyIdsForAction([]);
|
||||
}}
|
||||
type="button"
|
||||
disabled={busyInstanceId === instance.id}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className="secondary-button small"
|
||||
onClick={() => {
|
||||
const toolType = toolTypes.find(
|
||||
(t) => t.id === instance.tool_type_id,
|
||||
);
|
||||
if (toolType) {
|
||||
void loadConfigProfiles(toolType.id);
|
||||
}
|
||||
setProfileSelectInstanceId(instance.id);
|
||||
setSelectedProfileForAction(
|
||||
instance.selected_config_profile_id || "",
|
||||
);
|
||||
setSelectedSshKeyIdsForAction(
|
||||
instance.ssh_key_ids || [],
|
||||
);
|
||||
}}
|
||||
type="button"
|
||||
disabled={busyInstanceId === instance.id}
|
||||
>
|
||||
<Icon name="play" size="sm" />
|
||||
Start
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{instance.status === "running" && (
|
||||
@@ -376,68 +455,119 @@ export const InstanceList = ({
|
||||
<Icon name="stop" size="sm" />
|
||||
</button>
|
||||
)}
|
||||
{profileSelectInstanceId === instance.id ? (
|
||||
<div className="inline-profile-select">
|
||||
<select
|
||||
value={selectedProfileForAction}
|
||||
onChange={(e) =>
|
||||
setSelectedProfileForAction(e.target.value)
|
||||
}
|
||||
>
|
||||
<option value="">Default (none)</option>
|
||||
{configProfiles.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="primary-button small"
|
||||
onClick={() =>
|
||||
void handleRestart(
|
||||
instance.id,
|
||||
selectedProfileForAction || undefined,
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
disabled={busyInstanceId === instance.id}
|
||||
>
|
||||
<Icon name="refresh" size="sm" />
|
||||
Restart
|
||||
</button>
|
||||
<button
|
||||
className="ghost-button small"
|
||||
onClick={() => {
|
||||
setProfileSelectInstanceId(null);
|
||||
setSelectedProfileForAction("");
|
||||
{profileSelectInstanceId === instance.id ? (
|
||||
<div className="inline-profile-select">
|
||||
<select
|
||||
value={selectedProfileForAction}
|
||||
onChange={(e) =>
|
||||
setSelectedProfileForAction(e.target.value)
|
||||
}
|
||||
>
|
||||
<option value="">Default (none)</option>
|
||||
{configProfiles.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: "0.25rem",
|
||||
marginTop: "0.25rem",
|
||||
}}
|
||||
>
|
||||
{sshKeys.map((key) => (
|
||||
<label
|
||||
key={key.id}
|
||||
className="checkbox-label"
|
||||
style={{
|
||||
fontSize: "0.75rem",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.25rem",
|
||||
}}
|
||||
type="button"
|
||||
disabled={busyInstanceId === instance.id}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className="ghost-button small"
|
||||
onClick={() => {
|
||||
const toolType = toolTypes.find(
|
||||
(t) => t.id === instance.tool_type_id,
|
||||
);
|
||||
if (toolType) {
|
||||
void loadConfigProfiles(toolType.id);
|
||||
}
|
||||
setProfileSelectInstanceId(instance.id);
|
||||
setSelectedProfileForAction(
|
||||
instance.selected_config_profile_id || "",
|
||||
);
|
||||
}}
|
||||
type="button"
|
||||
disabled={busyInstanceId === instance.id}
|
||||
>
|
||||
<Icon name="refresh" size="sm" />
|
||||
</button>
|
||||
)}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedSshKeyIdsForAction.includes(
|
||||
key.id,
|
||||
)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedSshKeyIdsForAction(
|
||||
(prev) => [...prev, key.id],
|
||||
);
|
||||
} else {
|
||||
setSelectedSshKeyIdsForAction(
|
||||
(prev) =>
|
||||
prev.filter(
|
||||
(id) =>
|
||||
id !== key.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{key.name}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
className="primary-button small"
|
||||
onClick={() =>
|
||||
void handleRestart(
|
||||
instance.id,
|
||||
selectedProfileForAction || undefined,
|
||||
selectedSshKeyIdsForAction.length > 0
|
||||
? selectedSshKeyIdsForAction
|
||||
: undefined,
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
disabled={busyInstanceId === instance.id}
|
||||
>
|
||||
<Icon name="refresh" size="sm" />
|
||||
Restart
|
||||
</button>
|
||||
<button
|
||||
className="ghost-button small"
|
||||
onClick={() => {
|
||||
setProfileSelectInstanceId(null);
|
||||
setSelectedProfileForAction("");
|
||||
setSelectedSshKeyIdsForAction([]);
|
||||
}}
|
||||
type="button"
|
||||
disabled={busyInstanceId === instance.id}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className="ghost-button small"
|
||||
onClick={() => {
|
||||
const toolType = toolTypes.find(
|
||||
(t) => t.id === instance.tool_type_id,
|
||||
);
|
||||
if (toolType) {
|
||||
void loadConfigProfiles(toolType.id);
|
||||
}
|
||||
setProfileSelectInstanceId(instance.id);
|
||||
setSelectedProfileForAction(
|
||||
instance.selected_config_profile_id || "",
|
||||
);
|
||||
setSelectedSshKeyIdsForAction(
|
||||
instance.ssh_key_ids || [],
|
||||
);
|
||||
}}
|
||||
type="button"
|
||||
disabled={busyInstanceId === instance.id}
|
||||
>
|
||||
<Icon name="refresh" size="sm" />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
|
||||
+1591
-1302
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user