feat: new ToolStarter component — unified workspace-first tool starting
- New ToolStarter component: workspace context, fetches real tool types, auto-fetches config profiles per tool type, shows SSH key status - Backend: add repo_ssh_key_id to workspace list responses - WorkspacesPage: uses ToolStarter in modal instead of StartToolModal - WorkspaceDetailPage ToolsTab: uses ToolStarter in modal - Removed old inline StartToolModal from workspace-detail.tsx - Styles: .tool-starter-context, .context-row, .ssh-key-status Quality gates: ruff clean, tsc --noEmit clean, pytest workspaces API (9 passed, 1 skipped)
This commit is contained in:
@@ -38,6 +38,7 @@ async def list_all_workspaces(
|
|||||||
Workspace,
|
Workspace,
|
||||||
GitRepository.name.label("repo_name"),
|
GitRepository.name.label("repo_name"),
|
||||||
GitRepository.project_id,
|
GitRepository.project_id,
|
||||||
|
GitRepository.ssh_key_id.label("repo_ssh_key_id"),
|
||||||
instance_count.label("instance_count"),
|
instance_count.label("instance_count"),
|
||||||
)
|
)
|
||||||
.join(GitRepository, Workspace.repo_id == GitRepository.id)
|
.join(GitRepository, Workspace.repo_id == GitRepository.id)
|
||||||
@@ -52,6 +53,7 @@ async def list_all_workspaces(
|
|||||||
"name": ws.name,
|
"name": ws.name,
|
||||||
"repo_id": str(ws.repo_id),
|
"repo_id": str(ws.repo_id),
|
||||||
"repo_name": repo_name or "",
|
"repo_name": repo_name or "",
|
||||||
|
"repo_ssh_key_id": str(ssh_key_id) if ssh_key_id else None,
|
||||||
"project_id": str(project_id) if project_id else "",
|
"project_id": str(project_id) if project_id else "",
|
||||||
"project_name": "",
|
"project_name": "",
|
||||||
"user_id": str(ws.user_id),
|
"user_id": str(ws.user_id),
|
||||||
@@ -63,7 +65,7 @@ async def list_all_workspaces(
|
|||||||
"updated_at": ws.updated_at.isoformat() if ws.updated_at else None,
|
"updated_at": ws.updated_at.isoformat() if ws.updated_at else None,
|
||||||
"instance_count": count or 0,
|
"instance_count": count or 0,
|
||||||
}
|
}
|
||||||
for ws, repo_name, project_id, count in rows
|
for ws, repo_name, project_id, ssh_key_id, count in rows
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -190,6 +192,7 @@ async def list_workspaces(
|
|||||||
"name": ws.name,
|
"name": ws.name,
|
||||||
"repo_id": str(ws.repo_id),
|
"repo_id": str(ws.repo_id),
|
||||||
"repo_name": repo.name,
|
"repo_name": repo.name,
|
||||||
|
"repo_ssh_key_id": str(repo.ssh_key_id) if repo.ssh_key_id else None,
|
||||||
"project_id": str(repo.project_id) if repo.project_id else "",
|
"project_id": str(repo.project_id) if repo.project_id else "",
|
||||||
"project_name": repo.project.name if repo.project else "",
|
"project_name": repo.project.name if repo.project else "",
|
||||||
"user_id": str(ws.user_id),
|
"user_id": str(ws.user_id),
|
||||||
|
|||||||
@@ -0,0 +1,268 @@
|
|||||||
|
/** Unified tool starter — workspace-first, fetches real tool types and config profiles. */
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
import { Icon } from "./icon";
|
||||||
|
import { listToolTypes, type ToolType } from "../api/tool_types";
|
||||||
|
import { listConfigProfiles, type ConfigProfile } from "../api/config_profiles";
|
||||||
|
import { listSSHKeys, type SSHKey } from "../api/ssh_keys";
|
||||||
|
import type { Workspace } from "../types/workspace";
|
||||||
|
import type { ToolInstance } from "../api/sessions";
|
||||||
|
|
||||||
|
export interface ToolStarterProps {
|
||||||
|
workspace: Workspace;
|
||||||
|
onStarted: (instance: ToolInstance) => void;
|
||||||
|
onCancel?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ToolStarter({ workspace, onStarted, onCancel }: ToolStarterProps) {
|
||||||
|
const [toolTypes, setToolTypes] = useState<ToolType[]>([]);
|
||||||
|
const [toolTypesLoading, setToolTypesLoading] = useState(true);
|
||||||
|
const [toolTypesError, setToolTypesError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [selectedToolTypeId, setSelectedToolTypeId] = useState("");
|
||||||
|
|
||||||
|
const [profiles, setProfiles] = useState<ConfigProfile[]>([]);
|
||||||
|
const [profilesLoading, setProfilesLoading] = useState(false);
|
||||||
|
const [selectedProfileId, setSelectedProfileId] = useState("");
|
||||||
|
|
||||||
|
const [sshKeys, setSshKeys] = useState<SSHKey[]>([]);
|
||||||
|
const [sshKeysLoading, setSshKeysLoading] = useState(true);
|
||||||
|
|
||||||
|
const [starting, setStarting] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Fetch tool types on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const load = async () => {
|
||||||
|
try {
|
||||||
|
const data = await listToolTypes();
|
||||||
|
setToolTypes(data);
|
||||||
|
} catch (err) {
|
||||||
|
setToolTypesError(
|
||||||
|
err instanceof Error ? err.message : "Failed to load tool types",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setToolTypesLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
void load();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Fetch config profiles when tool type changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedToolTypeId) {
|
||||||
|
setProfiles([]);
|
||||||
|
setSelectedProfileId("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const load = async () => {
|
||||||
|
setProfilesLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await listConfigProfiles(
|
||||||
|
workspace.project_id,
|
||||||
|
selectedToolTypeId,
|
||||||
|
);
|
||||||
|
setProfiles(data);
|
||||||
|
// Auto-select default profile if available
|
||||||
|
const defaultProfile = data.find((p) => p.is_default);
|
||||||
|
if (defaultProfile) {
|
||||||
|
setSelectedProfileId(defaultProfile.id);
|
||||||
|
} else {
|
||||||
|
setSelectedProfileId("");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setProfiles([]);
|
||||||
|
} finally {
|
||||||
|
setProfilesLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
void load();
|
||||||
|
}, [selectedToolTypeId, workspace.project_id]);
|
||||||
|
|
||||||
|
// Fetch SSH keys on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const load = async () => {
|
||||||
|
try {
|
||||||
|
const data = await listSSHKeys();
|
||||||
|
setSshKeys(data);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
setSshKeysLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
void load();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const repoHasSshKey = !!workspace.repo_ssh_key_id;
|
||||||
|
const repoSshKey = sshKeys.find((k) => k.id === workspace.repo_ssh_key_id);
|
||||||
|
|
||||||
|
const handleStart = useCallback(async () => {
|
||||||
|
if (!selectedToolTypeId) {
|
||||||
|
setError("Please select a tool type");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStarting(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const { createInstance, startInstance } = await import(
|
||||||
|
"../api/sessions"
|
||||||
|
);
|
||||||
|
const instance = await createInstance(
|
||||||
|
workspace.project_id,
|
||||||
|
workspace.repo_id,
|
||||||
|
selectedToolTypeId,
|
||||||
|
workspace.name,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
selectedProfileId || undefined,
|
||||||
|
[],
|
||||||
|
workspace.id,
|
||||||
|
);
|
||||||
|
await startInstance(
|
||||||
|
workspace.project_id,
|
||||||
|
workspace.repo_id,
|
||||||
|
instance.id,
|
||||||
|
selectedProfileId || undefined,
|
||||||
|
);
|
||||||
|
onStarted(instance);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to start tool");
|
||||||
|
} finally {
|
||||||
|
setStarting(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
selectedToolTypeId,
|
||||||
|
selectedProfileId,
|
||||||
|
workspace,
|
||||||
|
onStarted,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tool-starter">
|
||||||
|
{/* Context header — read-only workspace info */}
|
||||||
|
<div className="tool-starter-context">
|
||||||
|
<div className="context-row">
|
||||||
|
<span className="context-label">Project</span>
|
||||||
|
<span className="context-value">{workspace.project_name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="context-row">
|
||||||
|
<span className="context-label">Repository</span>
|
||||||
|
<span className="context-value">{workspace.repo_name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="context-row">
|
||||||
|
<span className="context-label">Workspace</span>
|
||||||
|
<span className="context-value">{workspace.name}</span>
|
||||||
|
<span className="branch-badge">
|
||||||
|
<Icon name="branch" size="sm" /> {workspace.branch}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tool Type */}
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="tool-type">Tool Type</label>
|
||||||
|
<select
|
||||||
|
id="tool-type"
|
||||||
|
value={selectedToolTypeId}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSelectedToolTypeId(e.target.value);
|
||||||
|
setError(null);
|
||||||
|
}}
|
||||||
|
disabled={toolTypesLoading || starting}
|
||||||
|
>
|
||||||
|
<option value="">Select a tool...</option>
|
||||||
|
{toolTypes.map((tt) => (
|
||||||
|
<option key={tt.id} value={tt.id}>
|
||||||
|
{tt.display_name}
|
||||||
|
{tt.category && ` (${tt.category})`}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{toolTypesLoading && (
|
||||||
|
<span className="muted">Loading tools...</span>
|
||||||
|
)}
|
||||||
|
{toolTypesError && (
|
||||||
|
<span className="error-text">{toolTypesError}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Config Profile */}
|
||||||
|
{selectedToolTypeId && (
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="config-profile">Config Profile</label>
|
||||||
|
<select
|
||||||
|
id="config-profile"
|
||||||
|
value={selectedProfileId}
|
||||||
|
onChange={(e) => setSelectedProfileId(e.target.value)}
|
||||||
|
disabled={profilesLoading || starting}
|
||||||
|
>
|
||||||
|
<option value="">Default (no profile)</option>
|
||||||
|
{profiles.map((p) => (
|
||||||
|
<option key={p.id} value={p.id}>
|
||||||
|
{p.name}
|
||||||
|
{p.is_default && " (default)"}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{profilesLoading && (
|
||||||
|
<span className="muted">Loading profiles...</span>
|
||||||
|
)}
|
||||||
|
{profiles.length === 0 && !profilesLoading && (
|
||||||
|
<span className="muted">No custom profiles for this tool.</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* SSH Key Status */}
|
||||||
|
<div className="form-group ssh-key-status">
|
||||||
|
<label>SSH Key</label>
|
||||||
|
{sshKeysLoading ? (
|
||||||
|
<span className="muted">Checking...</span>
|
||||||
|
) : repoHasSshKey ? (
|
||||||
|
<span className="success-text">
|
||||||
|
<Icon name="success" size="sm" />{" "}
|
||||||
|
{repoSshKey?.name || "SSH key assigned"}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="warning-text">
|
||||||
|
<Icon name="warning" size="sm" />{" "}
|
||||||
|
No SSH key assigned to repository
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <p className="form-error">{error}</p>}
|
||||||
|
|
||||||
|
<div className="form-actions">
|
||||||
|
{onCancel && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={onCancel}
|
||||||
|
disabled={starting}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleStart}
|
||||||
|
disabled={!selectedToolTypeId || toolTypesLoading || starting}
|
||||||
|
>
|
||||||
|
{starting ? (
|
||||||
|
<>
|
||||||
|
<Icon name="loading" size="sm" /> Starting...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icon name="play" size="sm" /> Start Tool
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,9 +7,8 @@ import { useWorkspaces } from "../hooks/use-workspaces";
|
|||||||
import { useWorkspaceFiles } from "../hooks/use-workspace-files";
|
import { useWorkspaceFiles } from "../hooks/use-workspace-files";
|
||||||
import { useWorkspaceGit } from "../hooks/use-workspace-git";
|
import { useWorkspaceGit } from "../hooks/use-workspace-git";
|
||||||
import { useWorkspaceInstances } from "../hooks/use-workspace-instances";
|
import { useWorkspaceInstances } from "../hooks/use-workspace-instances";
|
||||||
import { useStartTool } from "../hooks/use-start-tool";
|
|
||||||
import { useMobileViewport } from "../hooks/use-mobile-viewport";
|
import { useMobileViewport } from "../hooks/use-mobile-viewport";
|
||||||
import { StartToolModal } from "../components/start-tool-modal";
|
import { ToolStarter } from "../components/tool-starter";
|
||||||
import type { FileEntry } from "../api/workspace-files";
|
import type { FileEntry } from "../api/workspace-files";
|
||||||
import type { Workspace } from "../types/workspace";
|
import type { Workspace } from "../types/workspace";
|
||||||
|
|
||||||
@@ -314,7 +313,6 @@ function GitTab({ workspaceId }: { workspaceId: string }) {
|
|||||||
|
|
||||||
function ToolsTab({ workspace }: { workspace: Workspace }) {
|
function ToolsTab({ workspace }: { workspace: Workspace }) {
|
||||||
const { instances, loading, refresh } = useWorkspaceInstances(workspace.id);
|
const { instances, loading, refresh } = useWorkspaceInstances(workspace.id);
|
||||||
const { startTool, starting } = useStartTool();
|
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -328,9 +326,8 @@ function ToolsTab({ workspace }: { workspace: Workspace }) {
|
|||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
onClick={() => setShowModal(true)}
|
onClick={() => setShowModal(true)}
|
||||||
disabled={starting}
|
|
||||||
>
|
>
|
||||||
{starting ? "Starting..." : "Start Tool"}
|
Start Tool
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -358,29 +355,25 @@ function ToolsTab({ workspace }: { workspace: Workspace }) {
|
|||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
onClick={() => setShowModal(true)}
|
onClick={() => setShowModal(true)}
|
||||||
disabled={starting}
|
|
||||||
>
|
>
|
||||||
{starting ? "Starting..." : "Start Another Tool"}
|
Start Another Tool
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{showModal && (
|
{showModal && (
|
||||||
<StartToolModal
|
<div className="modal-overlay" onClick={() => setShowModal(false)}>
|
||||||
workspace={workspace}
|
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||||
onClose={() => setShowModal(false)}
|
<h3>Start Tool</h3>
|
||||||
onStart={async (toolTypeId, configProfileId) => {
|
<ToolStarter
|
||||||
const instance = await startTool(
|
workspace={workspace}
|
||||||
workspace,
|
onStarted={() => {
|
||||||
toolTypeId,
|
setShowModal(false);
|
||||||
undefined,
|
void refresh();
|
||||||
configProfileId,
|
}}
|
||||||
);
|
onCancel={() => setShowModal(false)}
|
||||||
if (instance) {
|
/>
|
||||||
setShowModal(false);
|
</div>
|
||||||
await refresh();
|
</div>
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ import { useState } from "react";
|
|||||||
import { Icon } from "../components/icon";
|
import { Icon } from "../components/icon";
|
||||||
import { useWorkspaces } from "../hooks/use-workspaces";
|
import { useWorkspaces } from "../hooks/use-workspaces";
|
||||||
import { useWorkspaceActions } from "../hooks/use-workspace-actions";
|
import { useWorkspaceActions } from "../hooks/use-workspace-actions";
|
||||||
import { useStartTool } from "../hooks/use-start-tool";
|
|
||||||
import { WorkspaceCard } from "../components/workspace-card";
|
import { WorkspaceCard } from "../components/workspace-card";
|
||||||
import { WorkspaceCreateForm } from "../components/workspace-create-form";
|
import { WorkspaceCreateForm } from "../components/workspace-create-form";
|
||||||
import { StartToolModal } from "../components/start-tool-modal";
|
import { ToolStarter } from "../components/tool-starter";
|
||||||
import type { Workspace } from "../types/workspace";
|
import type { Workspace } from "../types/workspace";
|
||||||
|
|
||||||
export function WorkspacesPage() {
|
export function WorkspacesPage() {
|
||||||
@@ -16,8 +15,6 @@ export function WorkspacesPage() {
|
|||||||
|
|
||||||
const { workspaces, loading, error, refresh } = useWorkspaces();
|
const { workspaces, loading, error, refresh } = useWorkspaces();
|
||||||
const actions = useWorkspaceActions();
|
const actions = useWorkspaceActions();
|
||||||
const { startTool } = useStartTool();
|
|
||||||
|
|
||||||
const handleDelete = async (workspace: Workspace) => {
|
const handleDelete = async (workspace: Workspace) => {
|
||||||
await actions.delete(workspace, refresh);
|
await actions.delete(workspace, refresh);
|
||||||
};
|
};
|
||||||
@@ -31,23 +28,6 @@ export function WorkspacesPage() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStartTool = async (
|
|
||||||
toolTypeId: string,
|
|
||||||
configProfileId?: string,
|
|
||||||
) => {
|
|
||||||
if (!startWorkspace) return;
|
|
||||||
const instance = await startTool(
|
|
||||||
startWorkspace,
|
|
||||||
toolTypeId,
|
|
||||||
undefined,
|
|
||||||
configProfileId,
|
|
||||||
);
|
|
||||||
if (instance) {
|
|
||||||
setStartWorkspace(null);
|
|
||||||
await refresh();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page workspaces-page">
|
<div className="page workspaces-page">
|
||||||
<header className="page-header">
|
<header className="page-header">
|
||||||
@@ -109,11 +89,19 @@ export function WorkspacesPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{startWorkspace && (
|
{startWorkspace && (
|
||||||
<StartToolModal
|
<div className="modal-overlay" onClick={() => setStartWorkspace(null)}>
|
||||||
workspace={startWorkspace}
|
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||||
onClose={() => setStartWorkspace(null)}
|
<h3>Start Tool</h3>
|
||||||
onStart={handleStartTool}
|
<ToolStarter
|
||||||
/>
|
workspace={startWorkspace}
|
||||||
|
onStarted={() => {
|
||||||
|
setStartWorkspace(null);
|
||||||
|
void refresh();
|
||||||
|
}}
|
||||||
|
onCancel={() => setStartWorkspace(null)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5503,3 +5503,51 @@ a:active,
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-top: var(--space-2);
|
margin-top: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Tool Starter ─── */
|
||||||
|
.tool-starter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-starter-context {
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: var(--space-3);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-label {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--muted);
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-value {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ssh-key-status {
|
||||||
|
padding: var(--space-2);
|
||||||
|
background: var(--bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ssh-key-status .success-text,
|
||||||
|
.ssh-key-status .warning-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export interface Workspace {
|
|||||||
name: string;
|
name: string;
|
||||||
repo_id: string;
|
repo_id: string;
|
||||||
repo_name: string;
|
repo_name: string;
|
||||||
|
repo_ssh_key_id: string | null;
|
||||||
project_id: string;
|
project_id: string;
|
||||||
project_name: string;
|
project_name: string;
|
||||||
user_id: string;
|
user_id: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user