refactor: unify session creation form into CreateSessionForm component

This commit is contained in:
Fusion
2026-05-23 16:26:35 +02:00
parent cd4eba9803
commit 2e9ca52cdb
4 changed files with 327 additions and 349 deletions
+13 -58
View File
@@ -2,13 +2,14 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { getDashboardSummary, type DashboardSummary } from "../api/dashboard";
import { createInstance, getUserSessions, startInstance, stopInstance, deleteInstance, recreateInstanceTunnel, checkInstanceHealth, type Session as SessionApi, type InstanceHealth } from "../api/sessions";
import { getUserSessions, startInstance, stopInstance, deleteInstance, recreateInstanceTunnel, checkInstanceHealth, type Session as SessionApi, type InstanceHealth } from "../api/sessions";
import { listProjects } from "../api/projects";
import { listRepositories, type GitRepository } from "../api/git_repositories";
import { listToolTypes, type ToolType } from "../api/tool_types";
import { updateUserConfig } from "../api/settings";
import type { Project } from "../types";
import { Icon } from "../components/icon";
import { CreateSessionForm } from "../components/create-session-form";
type HomeStatus = "loading" | "ready" | "error";
@@ -29,10 +30,6 @@ export const HomePage = () => {
const [repositories, setRepositories] = useState<GitRepository[]>([]);
const [toolTypes, setToolTypes] = useState<ToolType[]>([]);
const [selectedProject, setSelectedProject] = useState("");
const [selectedRepo, setSelectedRepo] = useState("");
const [selectedToolType, setSelectedToolType] = useState("");
const [displayName, setDisplayName] = useState("");
const [saveState, setSaveState] = useState<"idle" | "saving" | "error">("idle");
const [actionBusy, setActionBusy] = useState<string | null>(null);
const [stopConfirmId, setStopConfirmId] = useState<string | null>(null);
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
@@ -133,24 +130,10 @@ export const HomePage = () => {
[safeSessions]
);
const handleCreate = async (event: React.FormEvent) => {
event.preventDefault();
if (!selectedProject || !selectedRepo || !selectedToolType) return;
setSaveState("saving");
try {
const instance = await createInstance(selectedProject, selectedRepo, selectedToolType, displayName || undefined);
await startInstance(selectedProject, selectedRepo, instance.id);
await updateUserConfig({ last_session_id: instance.id });
setDisplayName("");
setSelectedProject("");
setSelectedRepo("");
setSelectedToolType("");
setSaveState("idle");
await loadHome();
} catch {
setSaveState("error");
}
const handleCreateSuccess = async (instance: { id: string }) => {
await updateUserConfig({ last_session_id: instance.id });
setSelectedProject("");
await loadHome();
};
const handleOpen = (session: SessionView) => {
@@ -358,41 +341,13 @@ export const HomePage = () => {
<h2>Start a session</h2>
</div>
</div>
<form className="stack create-session-form" onSubmit={handleCreate}>
<div className="form-row">
<label className="form-field">
Project
<select value={selectedProject} onChange={(event) => { setSelectedProject(event.target.value); setSelectedRepo(""); }}>
<option value="">Select project...</option>
{projects.map((project) => <option key={project.id} value={project.id}>{project.name}</option>)}
</select>
</label>
<label className="form-field">
Repository
<select value={selectedRepo} onChange={(event) => setSelectedRepo(event.target.value)} disabled={!selectedProject}>
<option value="">Select repository...</option>
{repositories.map((repo) => <option key={repo.id} value={repo.id}>{repo.name}</option>)}
</select>
</label>
<label className="form-field">
Tool type
<select value={selectedToolType} onChange={(event) => setSelectedToolType(event.target.value)}>
<option value="">Select tool...</option>
{toolTypes.map((tool) => <option key={tool.id} value={tool.id}>{tool.display_name}</option>)}
</select>
</label>
</div>
<label className="form-field">
Display name
<input type="text" value={displayName} onChange={(event) => setDisplayName(event.target.value)} placeholder="My Development Environment" />
</label>
<div className="form-actions">
<button className="primary-button" type="submit" disabled={saveState === "saving"}>
{saveState === "saving" ? <><Icon name="loading" size="sm" /> Creating...</> : <><Icon name="add" size="sm" /> Create Session</>}
</button>
{saveState === "error" && <span className="error-text">Failed to create session</span>}
</div>
</form>
<CreateSessionForm
projects={projects}
repositories={repositories}
toolTypes={toolTypes}
onProjectChange={(projectId) => setSelectedProject(projectId)}
onSuccess={handleCreateSuccess}
/>
</section>
{recentSessions.length > 0 && (
+17 -234
View File
@@ -14,13 +14,11 @@ import {
recreateInstanceTunnel,
} from "../api/sessions";
import { listToolTypes, type ToolType } from "../api/tool_types";
import { createInstance } from "../api/sessions";
import { getUserConfig, updateUserConfig } from "../api/settings";
import { listSSHKeys, type SSHKey } from "../api/ssh_keys";
import { Icon } from "../components/icon";
import { CreateSessionForm } from "../components/create-session-form";
type SessionsStatus = "loading" | "ready" | "error";
type CreateStatus = "idle" | "creating" | "error";
export const SessionsPage = () => {
const navigate = useNavigate();
@@ -31,18 +29,7 @@ export const SessionsPage = () => {
const [projects, setProjects] = useState<Project[]>([]);
const [repositories, setRepositories] = useState<GitRepository[]>([]);
const [toolTypes, setToolTypes] = useState<ToolType[]>([]);
const [selectedProject, setSelectedProject] = useState<string>("");
const [selectedRepo, setSelectedRepo] = useState<string>("");
const [selectedToolType, setSelectedToolType] = useState<string>("");
const [displayName, setDisplayName] = useState("");
const [createStatus, setCreateStatus] = useState<CreateStatus>("idle");
const [createError, setCreateError] = useState<string | null>(null);
const [createProgress, setCreateProgress] = useState("");
const [cloneMode, setCloneMode] = useState<"mount" | "clone">("mount");
const [branch, setBranch] = useState("main");
const [sshKeys, setSshKeys] = useState<SSHKey[]>([]);
const [dirtyDeleteSession, setDirtyDeleteSession] = useState<Session | null>(null);
const [dirtyDeleteFiles, setDirtyDeleteFiles] = useState<string[]>([]);
@@ -107,17 +94,7 @@ export const SessionsPage = () => {
void loadToolTypes();
}, []);
useEffect(() => {
const loadSshKeys = async () => {
try {
const data = await listSSHKeys();
setSshKeys(data);
} catch {
// ignore
}
};
void loadSshKeys();
}, []);
// Poll health every 30 seconds for active instances
useEffect(() => {
@@ -191,58 +168,10 @@ export const SessionsPage = () => {
[sessions, lastSessionId]
);
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
setCreateError(null);
if (!selectedProject || !selectedRepo || !selectedToolType) {
setCreateError("Project, repository, and tool type are required");
return;
}
if (cloneMode === "clone") {
const repo = repositories.find((r) => r.id === selectedRepo);
if (!repo?.ssh_key_id) {
setCreateError("Repository must have an SSH key assigned for clone mode");
return;
}
}
setCreateStatus("creating");
setCreateProgress("Creating instance...");
try {
const instance = await createInstance(
selectedProject,
selectedRepo,
selectedToolType,
displayName || undefined,
cloneMode,
cloneMode === "clone" ? branch : undefined
);
setCreateProgress("Starting container...");
// Auto-start the instance
await startInstance(selectedProject, selectedRepo, instance.id);
await updateUserConfig({ last_session_id: instance.id });
setCreateStatus("idle");
setCreateProgress("");
setSelectedProject("");
setSelectedRepo("");
setSelectedToolType("");
setDisplayName("");
setCloneMode("mount");
setBranch("main");
await loadSessions();
} catch (error) {
setCreateStatus("error");
setCreateProgress("");
const axiosError = error as { response?: { data?: { detail?: string } } };
const message = axiosError.response?.data?.detail;
setCreateError(
typeof message === "string" ? message : "Failed to create session"
);
}
const handleCreateSuccess = async (instance: { id: string }) => {
await updateUserConfig({ last_session_id: instance.id });
setSelectedProject("");
await loadSessions();
};
const handleStop = async (sessionId: string, projectId: string, repoId: string) => {
@@ -630,164 +559,18 @@ export const SessionsPage = () => {
)}
{/* Create Session */}
<div className={`create-session-section ${createStatus === "creating" ? "dimmed" : ""}`}>
{createStatus === "creating" && (
<div className="loading-overlay">
<div className="loading-content">
<Icon name="loading" size="lg" />
<p>{createProgress || "Creating session..."}</p>
</div>
</div>
)}
<div className="create-session-section">
<h2>Create New Session</h2>
<form onSubmit={handleCreate} className="card stack create-session-form">
<div className="form-row">
<label className="form-field">
Project
<select
value={selectedProject}
onChange={(e) => {
setSelectedProject(e.target.value);
setSelectedRepo("");
}}
>
<option value="">Select project...</option>
{projects.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
</label>
<label className="form-field">
Repository
<select
value={selectedRepo}
onChange={(e) => setSelectedRepo(e.target.value)}
disabled={!selectedProject}
>
<option value="">Select repository...</option>
{repositories.map((r) => (
<option key={r.id} value={r.id}>
{r.name}
</option>
))}
</select>
</label>
<label className="form-field">
Tool Type
<select
value={selectedToolType}
onChange={(e) => setSelectedToolType(e.target.value)}
>
<option value="">Select tool...</option>
{toolTypes.map((t) => (
<option key={t.id} value={t.id}>
{t.display_name}
</option>
))}
</select>
</label>
</div>
<div className="form-row">
<label className="form-field">
Repository Access
<div className="radio-group">
<label className="radio-label">
<input
type="radio"
name="cloneMode"
value="mount"
checked={cloneMode === "mount"}
onChange={(e) => setCloneMode(e.target.value as "mount" | "clone")}
/>
Mount (live sync)
</label>
<label className="radio-label">
<input
type="radio"
name="cloneMode"
value="clone"
checked={cloneMode === "clone"}
onChange={(e) => setCloneMode(e.target.value as "mount" | "clone")}
/>
Clone fresh copy
</label>
</div>
</label>
{cloneMode === "clone" && (
<>
<label className="form-field">
Branch
<input
type="text"
value={branch}
onChange={(e) => setBranch(e.target.value)}
placeholder="main"
/>
</label>
{selectedRepo && (
<div className="form-field ssh-key-info">
{(() => {
const repo = repositories.find((r) => r.id === selectedRepo);
if (!repo) return null;
if (repo.ssh_key_id) {
const key = sshKeys.find((k) => k.id === repo.ssh_key_id);
return (
<span className="success-text">
SSH key: {key?.name || "Assigned"}
</span>
);
}
return (
<span className="warning-text">
No SSH key assigned to this repository. Clone mode requires an SSH key.
</span>
);
})()}
</div>
)}
</>
)}
</div>
<label className="form-field">
Display Name (optional)
<input
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="My Development Environment"
/>
</label>
{createError && <p className="error-text">{createError}</p>}
<div className="form-actions">
<button
className="primary-button"
type="submit"
disabled={createStatus === "creating"}
>
{createStatus === "creating" ? (
<>
<Icon name="loading" size="sm" />
Creating...
</>
) : (
<>
<Icon name="add" size="sm" />
Create Session
</>
)}
</button>
</div>
</form>
<CreateSessionForm
projects={projects}
repositories={repositories}
toolTypes={toolTypes}
showCloneMode={true}
onProjectChange={(projectId) => {
setSelectedProject(projectId);
}}
onSuccess={handleCreateSuccess}
/>
</div>
{/* Dirty Delete Confirmation Modal */}