WIP: frontend clone_mode removal in create form and API
- Remove clone_mode/branch/new_branch from createInstance API helper - Remove clone mode UI and branch fields from CreateSessionForm Remaining: wire workspace_id in form/tool-starter, remove session-card badge, tests
This commit is contained in:
@@ -17,6 +17,7 @@ __pycache__/
|
|||||||
*.so
|
*.so
|
||||||
.python-version
|
.python-version
|
||||||
.venv/
|
.venv/
|
||||||
|
.venv-test/
|
||||||
venv/
|
venv/
|
||||||
env/
|
env/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
@@ -56,3 +57,4 @@ Thumbs.db
|
|||||||
.pi-lens/
|
.pi-lens/
|
||||||
minerv3/
|
minerv3/
|
||||||
.cache/
|
.cache/
|
||||||
|
openspec-audit-report.md
|
||||||
|
|||||||
@@ -2,13 +2,16 @@
|
|||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.auth.dependencies import get_current_user_id, get_db_session
|
from src.auth.dependencies import get_current_user_id, get_db_session
|
||||||
|
from src.models import GitRepository
|
||||||
from src.models import ToolInstance
|
from src.models import ToolInstance
|
||||||
from src.models import Workspace
|
from src.models import Workspace
|
||||||
|
from src.schemas.tool import CreateInstanceRequest, CreateWorkspaceInstanceRequest
|
||||||
|
from src.services.tool.instance_service import create_tool_instance
|
||||||
|
|
||||||
router = APIRouter(prefix="/workspaces/{workspace_id}/instances")
|
router = APIRouter(prefix="/workspaces/{workspace_id}/instances")
|
||||||
|
|
||||||
@@ -30,6 +33,63 @@ async def _get_workspace(
|
|||||||
return workspace
|
return workspace
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/",
|
||||||
|
summary="Create instance from workspace",
|
||||||
|
description="Create a new tool instance mounted on this workspace.",
|
||||||
|
status_code=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
async def create_workspace_instance(
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
|
data: CreateWorkspaceInstanceRequest,
|
||||||
|
user_id: uuid.UUID = Depends(get_current_user_id),
|
||||||
|
session: AsyncSession = Depends(get_db_session),
|
||||||
|
) -> dict:
|
||||||
|
"""Create a tool instance directly on a workspace."""
|
||||||
|
workspace = await _get_workspace(session, workspace_id, user_id)
|
||||||
|
|
||||||
|
repo = await session.get(GitRepository, workspace.repo_id)
|
||||||
|
if repo is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Repository not found")
|
||||||
|
if repo.project_id is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Repository is not associated with a project",
|
||||||
|
)
|
||||||
|
|
||||||
|
request = CreateInstanceRequest(
|
||||||
|
tool_type_id=data.tool_type_id,
|
||||||
|
display_name=data.display_name,
|
||||||
|
workspace_id=str(workspace.id),
|
||||||
|
config_profile_id=data.config_profile_id,
|
||||||
|
ssh_key_ids=data.ssh_key_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
instance = await create_tool_instance(
|
||||||
|
session, user_id, repo.project_id, repo.id, request
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
|
||||||
|
except RuntimeError as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": str(instance.id),
|
||||||
|
"name": instance.name,
|
||||||
|
"display_name": instance.display_name,
|
||||||
|
"tool_type_id": str(instance.tool_type_id),
|
||||||
|
"status": instance.status,
|
||||||
|
"workspace_id": str(instance.workspace_id) if instance.workspace_id else None,
|
||||||
|
"selected_config_profile_id": str(instance.selected_config_profile_id)
|
||||||
|
if instance.selected_config_profile_id
|
||||||
|
else None,
|
||||||
|
"created_at": instance.created_at.isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
@router.get("/")
|
||||||
async def list_workspace_instances(
|
async def list_workspace_instances(
|
||||||
workspace_id: uuid.UUID,
|
workspace_id: uuid.UUID,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
"""Tool schemas module."""
|
"""Tool schemas module."""
|
||||||
|
|
||||||
from src.schemas.tool.tool_instance import CreateInstanceRequest, StartInstanceRequest
|
from src.schemas.tool.tool_instance import (
|
||||||
|
CreateInstanceRequest,
|
||||||
|
CreateWorkspaceInstanceRequest,
|
||||||
|
StartInstanceRequest,
|
||||||
|
)
|
||||||
from src.schemas.tool.tool_type import (
|
from src.schemas.tool.tool_type import (
|
||||||
ToolTypeCreate,
|
ToolTypeCreate,
|
||||||
ToolTypeResponse,
|
ToolTypeResponse,
|
||||||
@@ -10,6 +14,7 @@ from src.schemas.tool.tool_type import (
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"CreateInstanceRequest",
|
"CreateInstanceRequest",
|
||||||
|
"CreateWorkspaceInstanceRequest",
|
||||||
"StartInstanceRequest",
|
"StartInstanceRequest",
|
||||||
"ToolTypeCreate",
|
"ToolTypeCreate",
|
||||||
"ToolTypeResponse",
|
"ToolTypeResponse",
|
||||||
|
|||||||
@@ -31,8 +31,6 @@ export interface Session {
|
|||||||
url: string | null;
|
url: string | null;
|
||||||
container_status?: string;
|
container_status?: string;
|
||||||
probe_status?: string;
|
probe_status?: string;
|
||||||
clone_mode?: string;
|
|
||||||
branch?: string | null;
|
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,12 +49,9 @@ export async function createInstance(
|
|||||||
repoId: string,
|
repoId: string,
|
||||||
toolTypeId: string,
|
toolTypeId: string,
|
||||||
displayName?: string,
|
displayName?: string,
|
||||||
cloneMode?: string,
|
workspaceId?: string,
|
||||||
branch?: string,
|
|
||||||
newBranch?: string,
|
|
||||||
configProfileId?: string,
|
configProfileId?: string,
|
||||||
sshKeyIds?: string[],
|
sshKeyIds?: string[],
|
||||||
workspaceId?: string,
|
|
||||||
): Promise<ToolInstance> {
|
): Promise<ToolInstance> {
|
||||||
const response = await apiClient.post(
|
const response = await apiClient.post(
|
||||||
`/projects/${projectId}/repositories/${repoId}/instances`,
|
`/projects/${projectId}/repositories/${repoId}/instances`,
|
||||||
@@ -64,9 +59,6 @@ export async function createInstance(
|
|||||||
tool_type_id: toolTypeId,
|
tool_type_id: toolTypeId,
|
||||||
display_name: displayName,
|
display_name: displayName,
|
||||||
workspace_id: workspaceId || undefined,
|
workspace_id: workspaceId || undefined,
|
||||||
clone_mode: cloneMode || "mount",
|
|
||||||
branch: branch || undefined,
|
|
||||||
new_branch: newBranch || undefined,
|
|
||||||
config_profile_id: configProfileId,
|
config_profile_id: configProfileId,
|
||||||
ssh_key_ids: sshKeyIds || [],
|
ssh_key_ids: sshKeyIds || [],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,11 +6,7 @@ import {
|
|||||||
type ToolInstance,
|
type ToolInstance,
|
||||||
} from "../../../api/sessions";
|
} from "../../../api/sessions";
|
||||||
import type { Project } from "../../../types";
|
import type { Project } from "../../../types";
|
||||||
import {
|
import type { GitRepository } from "../../../api/git-repositories";
|
||||||
listRepositoryBranches,
|
|
||||||
type GitRepository,
|
|
||||||
type Branch,
|
|
||||||
} from "../../../api/git-repositories";
|
|
||||||
import type { ToolType } from "../../../api/tool-types";
|
import type { ToolType } from "../../../api/tool-types";
|
||||||
import { listSSHKeys, type SSHKey } from "../../../api/ssh-keys";
|
import { listSSHKeys, type SSHKey } from "../../../api/ssh-keys";
|
||||||
import {
|
import {
|
||||||
@@ -26,7 +22,6 @@ interface CreateSessionFormProps {
|
|||||||
fixedRepoId?: string;
|
fixedRepoId?: string;
|
||||||
projectName?: string;
|
projectName?: string;
|
||||||
repoName?: string;
|
repoName?: string;
|
||||||
showCloneMode?: boolean;
|
|
||||||
showFixedFields?: boolean;
|
showFixedFields?: boolean;
|
||||||
onProjectChange?: (projectId: string) => void;
|
onProjectChange?: (projectId: string) => void;
|
||||||
onSuccess?: (instance: ToolInstance) => void;
|
onSuccess?: (instance: ToolInstance) => void;
|
||||||
@@ -43,7 +38,6 @@ export const CreateSessionForm = ({
|
|||||||
fixedRepoId,
|
fixedRepoId,
|
||||||
projectName,
|
projectName,
|
||||||
repoName,
|
repoName,
|
||||||
showCloneMode = true,
|
|
||||||
showFixedFields = true,
|
showFixedFields = true,
|
||||||
onProjectChange,
|
onProjectChange,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
@@ -55,19 +49,11 @@ export const CreateSessionForm = ({
|
|||||||
const [selectedRepo, setSelectedRepo] = useState(fixedRepoId || "");
|
const [selectedRepo, setSelectedRepo] = useState(fixedRepoId || "");
|
||||||
const [selectedToolType, setSelectedToolType] = useState("");
|
const [selectedToolType, setSelectedToolType] = useState("");
|
||||||
const [displayName, setDisplayName] = useState("");
|
const [displayName, setDisplayName] = useState("");
|
||||||
const [cloneMode, setCloneMode] = useState<"mount" | "clone">("mount");
|
|
||||||
const [branch, setBranch] = useState("main");
|
|
||||||
const [sshKeys, setSshKeys] = useState<SSHKey[]>([]);
|
const [sshKeys, setSshKeys] = useState<SSHKey[]>([]);
|
||||||
const [configProfiles, setConfigProfiles] = useState<ConfigProfile[]>([]);
|
const [configProfiles, setConfigProfiles] = useState<ConfigProfile[]>([]);
|
||||||
const [selectedConfigProfile, setSelectedConfigProfile] = useState("");
|
const [selectedConfigProfile, setSelectedConfigProfile] = useState("");
|
||||||
const [selectedSshKeyIds, setSelectedSshKeyIds] = useState<string[]>([]);
|
const [selectedSshKeyIds, setSelectedSshKeyIds] = useState<string[]>([]);
|
||||||
|
|
||||||
const [branches, setBranches] = useState<Branch[]>([]);
|
|
||||||
const [isLoadingBranches, setIsLoadingBranches] = useState(false);
|
|
||||||
const [isCreatingNewBranch, setIsCreatingNewBranch] = useState(false);
|
|
||||||
const [newBranchName, setNewBranchName] = useState("");
|
|
||||||
const [baseBranch, setBaseBranch] = useState("");
|
|
||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -108,31 +94,6 @@ export const CreateSessionForm = ({
|
|||||||
void loadProfiles();
|
void loadProfiles();
|
||||||
}, [selectedToolType, selectedProject, fixedProjectId]);
|
}, [selectedToolType, selectedProject, fixedProjectId]);
|
||||||
|
|
||||||
// Load branches when selected repo changes
|
|
||||||
useEffect(() => {
|
|
||||||
const projectId = fixedProjectId || selectedProject;
|
|
||||||
if (!selectedRepo || !projectId || !showCloneMode) {
|
|
||||||
setBranches([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const loadBranches = async () => {
|
|
||||||
setIsLoadingBranches(true);
|
|
||||||
try {
|
|
||||||
const response = await listRepositoryBranches(projectId, selectedRepo);
|
|
||||||
setBranches(response.branches);
|
|
||||||
if (response.default_branch) {
|
|
||||||
setBranch(response.default_branch);
|
|
||||||
setBaseBranch(response.default_branch);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
} finally {
|
|
||||||
setIsLoadingBranches(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
void loadBranches();
|
|
||||||
}, [selectedRepo, selectedProject, fixedProjectId, showCloneMode]);
|
|
||||||
|
|
||||||
// Filter repositories by selected project
|
// Filter repositories by selected project
|
||||||
const availableRepos = selectedProject
|
const availableRepos = selectedProject
|
||||||
? repositories.filter((r) => r.project_id === selectedProject)
|
? repositories.filter((r) => r.project_id === selectedProject)
|
||||||
@@ -143,12 +104,6 @@ export const CreateSessionForm = ({
|
|||||||
if (!fixedRepoId) setSelectedRepo("");
|
if (!fixedRepoId) setSelectedRepo("");
|
||||||
setSelectedToolType("");
|
setSelectedToolType("");
|
||||||
setDisplayName("");
|
setDisplayName("");
|
||||||
setCloneMode("mount");
|
|
||||||
setBranch("main");
|
|
||||||
setIsCreatingNewBranch(false);
|
|
||||||
setNewBranchName("");
|
|
||||||
setBaseBranch("");
|
|
||||||
setBranches([]);
|
|
||||||
setSelectedSshKeyIds([]);
|
setSelectedSshKeyIds([]);
|
||||||
setSelectedConfigProfile("");
|
setSelectedConfigProfile("");
|
||||||
};
|
};
|
||||||
@@ -165,14 +120,6 @@ export const CreateSessionForm = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showCloneMode && cloneMode === "clone") {
|
|
||||||
const repo = repositories.find((r) => r.id === repoId);
|
|
||||||
if (!repo?.ssh_key_id) {
|
|
||||||
setError("Repository must have an SSH key assigned for clone mode");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -181,15 +128,7 @@ export const CreateSessionForm = ({
|
|||||||
repoId,
|
repoId,
|
||||||
selectedToolType,
|
selectedToolType,
|
||||||
displayName || undefined,
|
displayName || undefined,
|
||||||
showCloneMode ? cloneMode : undefined,
|
undefined,
|
||||||
showCloneMode && cloneMode === "clone"
|
|
||||||
? isCreatingNewBranch
|
|
||||||
? baseBranch
|
|
||||||
: branch
|
|
||||||
: undefined,
|
|
||||||
showCloneMode && cloneMode === "clone" && isCreatingNewBranch
|
|
||||||
? newBranchName
|
|
||||||
: undefined,
|
|
||||||
selectedConfigProfile || undefined,
|
selectedConfigProfile || undefined,
|
||||||
selectedSshKeyIds.length > 0 ? selectedSshKeyIds : undefined,
|
selectedSshKeyIds.length > 0 ? selectedSshKeyIds : undefined,
|
||||||
);
|
);
|
||||||
@@ -240,8 +179,6 @@ export const CreateSessionForm = ({
|
|||||||
setSelectedProject(value);
|
setSelectedProject(value);
|
||||||
setSelectedRepo("");
|
setSelectedRepo("");
|
||||||
setSelectedToolType("");
|
setSelectedToolType("");
|
||||||
setCloneMode("mount");
|
|
||||||
setIsCreatingNewBranch(false);
|
|
||||||
onProjectChange?.(value);
|
onProjectChange?.(value);
|
||||||
}}
|
}}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
@@ -277,8 +214,6 @@ export const CreateSessionForm = ({
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSelectedRepo(e.target.value);
|
setSelectedRepo(e.target.value);
|
||||||
setSelectedToolType("");
|
setSelectedToolType("");
|
||||||
setCloneMode("mount");
|
|
||||||
setIsCreatingNewBranch(false);
|
|
||||||
}}
|
}}
|
||||||
disabled={!hasProject || isSubmitting}
|
disabled={!hasProject || isSubmitting}
|
||||||
>
|
>
|
||||||
@@ -301,8 +236,6 @@ export const CreateSessionForm = ({
|
|||||||
value={selectedToolType}
|
value={selectedToolType}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSelectedToolType(e.target.value);
|
setSelectedToolType(e.target.value);
|
||||||
setCloneMode("mount");
|
|
||||||
setIsCreatingNewBranch(false);
|
|
||||||
}}
|
}}
|
||||||
disabled={!hasRepo || isSubmitting}
|
disabled={!hasRepo || isSubmitting}
|
||||||
>
|
>
|
||||||
@@ -382,134 +315,6 @@ export const CreateSessionForm = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Clone Mode & Branch */}
|
|
||||||
{showCloneMode && hasToolType && (
|
|
||||||
<div className="form-field">
|
|
||||||
<label>Repository Access</label>
|
|
||||||
<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");
|
|
||||||
setIsCreatingNewBranch(false);
|
|
||||||
}}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
/>
|
|
||||||
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");
|
|
||||||
setIsCreatingNewBranch(false);
|
|
||||||
}}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
/>
|
|
||||||
Clone fresh copy
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{cloneMode === "clone" && (
|
|
||||||
<>
|
|
||||||
<label className="form-field">
|
|
||||||
Branch
|
|
||||||
{isLoadingBranches ? (
|
|
||||||
<span className="muted">Loading branches...</span>
|
|
||||||
) : (
|
|
||||||
<select
|
|
||||||
value={isCreatingNewBranch ? "__new__" : branch}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "__new__") {
|
|
||||||
setIsCreatingNewBranch(true);
|
|
||||||
setNewBranchName("");
|
|
||||||
} else {
|
|
||||||
setIsCreatingNewBranch(false);
|
|
||||||
setBranch(value);
|
|
||||||
setBaseBranch(value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
{branches.map((b) => (
|
|
||||||
<option key={b.name} value={b.name}>
|
|
||||||
{b.name} {b.is_default ? "(default)" : ""}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
<option value="__new__">Create new branch...</option>
|
|
||||||
</select>
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{isCreatingNewBranch && (
|
|
||||||
<>
|
|
||||||
<label className="form-field">
|
|
||||||
New Branch Name
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={newBranchName}
|
|
||||||
onChange={(e) => setNewBranchName(e.target.value)}
|
|
||||||
placeholder="feature/my-new-branch"
|
|
||||||
required
|
|
||||||
disabled={isSubmitting}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="form-field">
|
|
||||||
Base Branch
|
|
||||||
<select
|
|
||||||
value={baseBranch}
|
|
||||||
onChange={(e) => setBaseBranch(e.target.value)}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
{branches.map((b) => (
|
|
||||||
<option key={b.name} value={b.name}>
|
|
||||||
{b.name} {b.is_default ? "(default)" : ""}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Display Name */}
|
{/* Display Name */}
|
||||||
{hasToolType && (
|
{hasToolType && (
|
||||||
<div className="form-field">
|
<div className="form-field">
|
||||||
|
|||||||
Reference in New Issue
Block a user