refactor: centralize types and extract seed data (Task 1.1)
- Create types/ directory with canonical domain type definitions - session.ts, tool-instance.ts, tool-type.ts, git-repository.ts - config-folder.ts, tool-config.ts, project.ts, user.ts, api-response.ts - Move inline types from api modules to types/ with backward-compatible re-exports - Update all consumers (pages, components, state) to import from types/ - Extract seed_builtin_tool_types from main.py to seeds/builtin_tool_types.py - Ensure Session, ToolInstance, ToolType, GitRepository defined exactly once Quality gates: tsc (pass), eslint (pass), Python syntax (pass) Refs: repo-restructure Task 1.1
This commit is contained in:
@@ -24,7 +24,7 @@ export const getConfigFolder = async (id: string): Promise<ConfigFolder> => {
|
||||
};
|
||||
|
||||
export const createConfigFolder = async (
|
||||
data: CreateConfigFolderRequest
|
||||
data: CreateConfigFolderRequest,
|
||||
): Promise<ConfigFolder> => {
|
||||
const response = await apiClient.post<ConfigFolder>("/config-folders", data);
|
||||
return response.data;
|
||||
@@ -32,11 +32,11 @@ export const createConfigFolder = async (
|
||||
|
||||
export const updateConfigFolder = async (
|
||||
id: string,
|
||||
data: UpdateConfigFolderRequest
|
||||
data: UpdateConfigFolderRequest,
|
||||
): Promise<ConfigFolder> => {
|
||||
const response = await apiClient.put<ConfigFolder>(
|
||||
`/config-folders/${id}`,
|
||||
data
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
@@ -48,11 +48,11 @@ export const deleteConfigFolder = async (id: string): Promise<void> => {
|
||||
export const addProjectOverride = async (
|
||||
id: string,
|
||||
projectId: string,
|
||||
data: ProjectOverrideRequest
|
||||
data: ProjectOverrideRequest,
|
||||
): Promise<ConfigFolder> => {
|
||||
const response = await apiClient.post<ConfigFolder>(
|
||||
`/config-folders/${id}/overrides/${projectId}`,
|
||||
data
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
@@ -60,18 +60,18 @@ export const addProjectOverride = async (
|
||||
export const updateProjectOverride = async (
|
||||
id: string,
|
||||
projectId: string,
|
||||
data: ProjectOverrideRequest
|
||||
data: ProjectOverrideRequest,
|
||||
): Promise<ConfigFolder> => {
|
||||
const response = await apiClient.put<ConfigFolder>(
|
||||
`/config-folders/${id}/overrides/${projectId}`,
|
||||
data
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const deleteProjectOverride = async (
|
||||
id: string,
|
||||
projectId: string
|
||||
projectId: string,
|
||||
): Promise<void> => {
|
||||
await apiClient.delete(`/config-folders/${id}/overrides/${projectId}`);
|
||||
};
|
||||
|
||||
@@ -23,24 +23,34 @@ export type {
|
||||
} from "../types/git-repository";
|
||||
|
||||
export async function parseGitUrl(url: string): Promise<URLParseResult> {
|
||||
const response = await apiClient.post("/projects/repositories/parse-url", { url });
|
||||
const response = await apiClient.post("/projects/repositories/parse-url", {
|
||||
url,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function listRepositories(projectId: string): Promise<GitRepository[]> {
|
||||
export async function listRepositories(
|
||||
projectId: string,
|
||||
): Promise<GitRepository[]> {
|
||||
const response = await apiClient.get(`/projects/${projectId}/repositories`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function createRepository(
|
||||
projectId: string,
|
||||
data: GitRepositoryCreate
|
||||
data: GitRepositoryCreate,
|
||||
): Promise<GitRepository> {
|
||||
const response = await apiClient.post(`/projects/${projectId}/repositories`, data);
|
||||
const response = await apiClient.post(
|
||||
`/projects/${projectId}/repositories`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deleteRepository(projectId: string, repoId: string): Promise<void> {
|
||||
export async function deleteRepository(
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
): Promise<void> {
|
||||
await apiClient.delete(`/projects/${projectId}/repositories/${repoId}`);
|
||||
}
|
||||
|
||||
@@ -48,34 +58,36 @@ export async function getRepositoryHistory(
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
branch?: string,
|
||||
limit?: number
|
||||
limit?: number,
|
||||
): Promise<CommitHistoryResponse> {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (branch) searchParams.set("branch", branch);
|
||||
if (limit) searchParams.set("limit", String(limit));
|
||||
const queryString = searchParams.toString();
|
||||
const params = queryString ? `?${queryString}` : "";
|
||||
const response = await apiClient.get(`/projects/${projectId}/repositories/${repoId}/history${params}`);
|
||||
const response = await apiClient.get(
|
||||
`/projects/${projectId}/repositories/${repoId}/history${params}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getCommitDetail(
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
commitHash: string
|
||||
commitHash: string,
|
||||
): Promise<CommitDetail> {
|
||||
const response = await apiClient.get(
|
||||
`/projects/${projectId}/repositories/${repoId}/commits/${commitHash}`
|
||||
`/projects/${projectId}/repositories/${repoId}/commits/${commitHash}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getRepositoryStatus(
|
||||
projectId: string,
|
||||
repoId: string
|
||||
repoId: string,
|
||||
): Promise<GitStatus> {
|
||||
const response = await apiClient.get(
|
||||
`/projects/${projectId}/repositories/${repoId}/status`
|
||||
`/projects/${projectId}/repositories/${repoId}/status`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
@@ -84,11 +96,11 @@ export async function createBranch(
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
name: string,
|
||||
baseBranch: string = "HEAD"
|
||||
baseBranch: string = "HEAD",
|
||||
): Promise<{ message: string; branch: string }> {
|
||||
const response = await apiClient.post(
|
||||
`/projects/${projectId}/repositories/${repoId}/branches`,
|
||||
{ name, base_branch: baseBranch }
|
||||
{ name, base_branch: baseBranch },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
@@ -97,10 +109,10 @@ export async function deleteBranch(
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
branchName: string,
|
||||
force: boolean = false
|
||||
force: boolean = false,
|
||||
): Promise<{ message: string }> {
|
||||
const response = await apiClient.delete(
|
||||
`/projects/${projectId}/repositories/${repoId}/branches/${branchName}?force=${force}`
|
||||
`/projects/${projectId}/repositories/${repoId}/branches/${branchName}?force=${force}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
@@ -108,11 +120,11 @@ export async function deleteBranch(
|
||||
export async function checkoutBranch(
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
branch: string
|
||||
branch: string,
|
||||
): Promise<{ message: string; branch: string }> {
|
||||
const response = await apiClient.post(
|
||||
`/projects/${projectId}/repositories/${repoId}/checkout`,
|
||||
{ branch }
|
||||
{ branch },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
@@ -121,21 +133,21 @@ export async function commitChanges(
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
message: string,
|
||||
files?: string[]
|
||||
files?: string[],
|
||||
): Promise<CommitResponse> {
|
||||
const response = await apiClient.post(
|
||||
`/projects/${projectId}/repositories/${repoId}/commit`,
|
||||
{ message, files }
|
||||
{ message, files },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function fetchRepository(
|
||||
projectId: string,
|
||||
repoId: string
|
||||
repoId: string,
|
||||
): Promise<{ message: string }> {
|
||||
const response = await apiClient.post(
|
||||
`/projects/${projectId}/repositories/${repoId}/fetch`
|
||||
`/projects/${projectId}/repositories/${repoId}/fetch`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
@@ -143,11 +155,11 @@ export async function fetchRepository(
|
||||
export async function pullRepository(
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
branch?: string
|
||||
branch?: string,
|
||||
): Promise<{ message: string }> {
|
||||
const params = branch ? `?branch=${branch}` : "";
|
||||
const response = await apiClient.post(
|
||||
`/projects/${projectId}/repositories/${repoId}/pull${params}`
|
||||
`/projects/${projectId}/repositories/${repoId}/pull${params}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
@@ -155,11 +167,11 @@ export async function pullRepository(
|
||||
export async function pushRepository(
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
branch?: string
|
||||
branch?: string,
|
||||
): Promise<{ message: string }> {
|
||||
const params = branch ? `?branch=${branch}` : "";
|
||||
const response = await apiClient.post(
|
||||
`/projects/${projectId}/repositories/${repoId}/push${params}`
|
||||
`/projects/${projectId}/repositories/${repoId}/push${params}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
@@ -169,11 +181,11 @@ export async function mergeBranches(
|
||||
repoId: string,
|
||||
sourceBranch: string,
|
||||
targetBranch?: string,
|
||||
message?: string
|
||||
message?: string,
|
||||
): Promise<MergeResponse> {
|
||||
const response = await apiClient.post(
|
||||
`/projects/${projectId}/repositories/${repoId}/merge`,
|
||||
{ source_branch: sourceBranch, target_branch: targetBranch, message }
|
||||
{ source_branch: sourceBranch, target_branch: targetBranch, message },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ export type { ToolInstance } from "../types/tool-instance";
|
||||
|
||||
export async function listInstances(
|
||||
projectId: string,
|
||||
repoId: string
|
||||
repoId: string,
|
||||
): Promise<ToolInstance[]> {
|
||||
const response = await apiClient.get(
|
||||
`/projects/${projectId}/repositories/${repoId}/instances`
|
||||
`/projects/${projectId}/repositories/${repoId}/instances`,
|
||||
);
|
||||
return response.data.instances;
|
||||
}
|
||||
@@ -19,14 +19,14 @@ export async function createInstance(
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
toolTypeId: string,
|
||||
displayName?: string
|
||||
displayName?: string,
|
||||
): Promise<ToolInstance> {
|
||||
const response = await apiClient.post(
|
||||
`/projects/${projectId}/repositories/${repoId}/instances`,
|
||||
{
|
||||
tool_type_id: toolTypeId,
|
||||
display_name: displayName,
|
||||
}
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
@@ -34,10 +34,10 @@ export async function createInstance(
|
||||
export async function startInstance(
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
instanceId: string
|
||||
instanceId: string,
|
||||
): Promise<{ status: string; url?: string }> {
|
||||
const response = await apiClient.post(
|
||||
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/start`
|
||||
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/start`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
@@ -45,10 +45,10 @@ export async function startInstance(
|
||||
export async function stopInstance(
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
instanceId: string
|
||||
instanceId: string,
|
||||
): Promise<{ status: string }> {
|
||||
const response = await apiClient.post(
|
||||
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/stop`
|
||||
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/stop`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
@@ -56,10 +56,10 @@ export async function stopInstance(
|
||||
export async function restartInstance(
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
instanceId: string
|
||||
instanceId: string,
|
||||
): Promise<{ status: string; url?: string }> {
|
||||
const response = await apiClient.post(
|
||||
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/restart`
|
||||
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/restart`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
@@ -67,10 +67,10 @@ export async function restartInstance(
|
||||
export async function deleteInstance(
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
instanceId: string
|
||||
instanceId: string,
|
||||
): Promise<void> {
|
||||
await apiClient.delete(
|
||||
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}`
|
||||
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,10 +82,10 @@ export async function getUserSessions(): Promise<Session[]> {
|
||||
export async function checkInstanceHealth(
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
instanceId: string
|
||||
instanceId: string,
|
||||
): Promise<{ healthy: boolean; status_code: number | null; error?: string }> {
|
||||
const response = await apiClient.get(
|
||||
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/health`
|
||||
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/health`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
@@ -93,10 +93,10 @@ export async function checkInstanceHealth(
|
||||
export async function recreateInstanceTunnel(
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
instanceId: string
|
||||
instanceId: string,
|
||||
): Promise<{ status: string; url?: string }> {
|
||||
const response = await apiClient.post(
|
||||
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/recreate-tunnel`
|
||||
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/recreate-tunnel`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -5,32 +5,35 @@ export type { ToolConfig, CreateToolConfigRequest } from "../types/tool-config";
|
||||
|
||||
export const listToolConfigs = async (
|
||||
tool_type_id?: string,
|
||||
project_id?: string
|
||||
project_id?: string,
|
||||
): Promise<ToolConfig[]> => {
|
||||
const params = new URLSearchParams();
|
||||
if (tool_type_id) params.append("tool_type_id", tool_type_id);
|
||||
if (project_id) params.append("project_id", project_id);
|
||||
|
||||
const response = await apiClient.get<{ configs: ToolConfig[] }>(
|
||||
`/tool-configs?${params.toString()}`
|
||||
`/tool-configs?${params.toString()}`,
|
||||
);
|
||||
return response.data.configs;
|
||||
};
|
||||
|
||||
export const createToolConfig = async (
|
||||
data: CreateToolConfigRequest
|
||||
data: CreateToolConfigRequest,
|
||||
): Promise<ToolConfig> => {
|
||||
const response = await apiClient.post<{ configs: ToolConfig[] }>("/tool-configs", data);
|
||||
const response = await apiClient.post<{ configs: ToolConfig[] }>(
|
||||
"/tool-configs",
|
||||
data,
|
||||
);
|
||||
return response.data.configs[0];
|
||||
};
|
||||
|
||||
export const updateToolConfig = async (
|
||||
id: string,
|
||||
data: CreateToolConfigRequest
|
||||
data: CreateToolConfigRequest,
|
||||
): Promise<ToolConfig> => {
|
||||
const response = await apiClient.put<{ configs: ToolConfig[] }>(
|
||||
`/tool-configs/${id}`,
|
||||
data
|
||||
data,
|
||||
);
|
||||
return response.data.configs[0];
|
||||
};
|
||||
@@ -40,10 +43,10 @@ export const deleteToolConfig = async (id: string): Promise<void> => {
|
||||
};
|
||||
|
||||
export const getToolConfigDefaults = async (
|
||||
toolTypeId: string
|
||||
toolTypeId: string,
|
||||
): Promise<ToolConfig> => {
|
||||
const response = await apiClient.get<ToolConfig>(
|
||||
`/tool-configs/defaults/${toolTypeId}`
|
||||
`/tool-configs/defaults/${toolTypeId}`,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { apiClient } from "./client";
|
||||
import type { ToolType, CreateToolTypeRequest, UpdateToolTypeRequest } from "../types/tool-type";
|
||||
import type {
|
||||
ToolType,
|
||||
CreateToolTypeRequest,
|
||||
UpdateToolTypeRequest,
|
||||
} from "../types/tool-type";
|
||||
|
||||
export type { ReadinessProbe, ToolType, CreateToolTypeRequest, UpdateToolTypeRequest } from "../types/tool-type";
|
||||
export type {
|
||||
ReadinessProbe,
|
||||
ToolType,
|
||||
CreateToolTypeRequest,
|
||||
UpdateToolTypeRequest,
|
||||
} from "../types/tool-type";
|
||||
|
||||
export const listToolTypes = async (): Promise<ToolType[]> => {
|
||||
const response = await apiClient.get<ToolType[]>("/tool-types");
|
||||
@@ -13,12 +22,17 @@ export const getToolType = async (id: string): Promise<ToolType> => {
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const createToolType = async (data: CreateToolTypeRequest): Promise<ToolType> => {
|
||||
export const createToolType = async (
|
||||
data: CreateToolTypeRequest,
|
||||
): Promise<ToolType> => {
|
||||
const response = await apiClient.post<ToolType>("/tool-types", data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateToolType = async (id: string, data: UpdateToolTypeRequest): Promise<ToolType> => {
|
||||
export const updateToolType = async (
|
||||
id: string,
|
||||
data: UpdateToolTypeRequest,
|
||||
): Promise<ToolType> => {
|
||||
const response = await apiClient.put<ToolType>(`/tool-types/${id}`, data);
|
||||
return response.data;
|
||||
};
|
||||
@@ -27,7 +41,11 @@ export const deleteToolType = async (id: string): Promise<void> => {
|
||||
await apiClient.delete(`/tool-types/${id}`);
|
||||
};
|
||||
|
||||
export const validateToolType = async (id: string): Promise<{ valid: boolean; errors?: string[] }> => {
|
||||
const response = await apiClient.get<{ valid: boolean; errors?: string[] }>(`/tool-types/${id}/validate`);
|
||||
export const validateToolType = async (
|
||||
id: string,
|
||||
): Promise<{ valid: boolean; errors?: string[] }> => {
|
||||
const response = await apiClient.get<{ valid: boolean; errors?: string[] }>(
|
||||
`/tool-types/${id}/validate`,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -13,14 +13,15 @@ const NAV_ITEMS: { to: string; label: string; icon: IconName }[] = [
|
||||
{ to: "/", label: "Home", icon: "dashboard" },
|
||||
{ to: "/projects", label: "Projects", icon: "projects" },
|
||||
{ to: "/tool-workshop", label: "Tool Workshop", icon: "settings" },
|
||||
{ to: "/settings", label: "Settings", icon: "settings" }
|
||||
{ to: "/settings", label: "Settings", icon: "settings" },
|
||||
];
|
||||
|
||||
const ACTIVE_STATUSES = ["running", "building", "pending"];
|
||||
|
||||
const SessionItem = ({ session }: { session: Session }) => {
|
||||
const isRunning = session.status === "running";
|
||||
const displayName = session.display_name || session.tool_type_name || "Unnamed Session";
|
||||
const displayName =
|
||||
session.display_name || session.tool_type_name || "Unnamed Session";
|
||||
|
||||
return (
|
||||
<a
|
||||
@@ -87,12 +88,16 @@ export const AppShell = () => {
|
||||
<aside className="shell-nav" aria-label="Primary navigation">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const isHome = item.to === "/";
|
||||
const activeCount = sessions.filter((s) => s.status === "running").length;
|
||||
const activeCount = sessions.filter(
|
||||
(s) => s.status === "running",
|
||||
).length;
|
||||
return (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={({ isActive }) => (isActive ? "nav-item nav-item-active" : "nav-item")}
|
||||
className={({ isActive }) =>
|
||||
isActive ? "nav-item nav-item-active" : "nav-item"
|
||||
}
|
||||
end={item.to === "/"}
|
||||
>
|
||||
<Icon name={item.icon} size="sm" />
|
||||
@@ -104,7 +109,8 @@ export const AppShell = () => {
|
||||
);
|
||||
})}
|
||||
|
||||
{sessions.filter((s) => ACTIVE_STATUSES.includes(s.status)).length > 0 && (
|
||||
{sessions.filter((s) => ACTIVE_STATUSES.includes(s.status)).length >
|
||||
0 && (
|
||||
<>
|
||||
<div className="nav-divider" />
|
||||
<div className="nav-section-title">Live sessions</div>
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
stopInstance,
|
||||
} from "../api/sessions";
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000";
|
||||
const API_BASE_URL =
|
||||
import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000";
|
||||
|
||||
interface InstanceListProps {
|
||||
projectId: string;
|
||||
@@ -22,7 +23,11 @@ interface InstanceListProps {
|
||||
toolTypes: ToolType[];
|
||||
}
|
||||
|
||||
export const InstanceList = ({ projectId, repoId, toolTypes }: InstanceListProps) => {
|
||||
export const InstanceList = ({
|
||||
projectId,
|
||||
repoId,
|
||||
toolTypes,
|
||||
}: InstanceListProps) => {
|
||||
const navigate = useNavigate();
|
||||
const [instances, setInstances] = useState<ToolInstance[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -35,7 +40,9 @@ export const InstanceList = ({ projectId, repoId, toolTypes }: InstanceListProps
|
||||
const [stopConfirmId, setStopConfirmId] = useState<string | null>(null);
|
||||
|
||||
// Health check state
|
||||
const [healthStatus, setHealthStatus] = useState<Record<string, { healthy: boolean; lastCheck: number }>>({});
|
||||
const [healthStatus, setHealthStatus] = useState<
|
||||
Record<string, { healthy: boolean; lastCheck: number }>
|
||||
>({});
|
||||
|
||||
const loadInstances = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -55,21 +62,27 @@ export const InstanceList = ({ projectId, repoId, toolTypes }: InstanceListProps
|
||||
|
||||
// Health check polling
|
||||
useEffect(() => {
|
||||
const runningInstances = instances.filter(i => i.status === "running" && i.url?.startsWith("http"));
|
||||
const runningInstances = instances.filter(
|
||||
(i) => i.status === "running" && i.url?.startsWith("http"),
|
||||
);
|
||||
if (runningInstances.length === 0) return;
|
||||
|
||||
const checkHealth = async () => {
|
||||
for (const instance of runningInstances) {
|
||||
try {
|
||||
const health = await checkInstanceHealth(projectId, repoId, instance.id);
|
||||
setHealthStatus(prev => ({
|
||||
const health = await checkInstanceHealth(
|
||||
projectId,
|
||||
repoId,
|
||||
instance.id,
|
||||
);
|
||||
setHealthStatus((prev) => ({
|
||||
...prev,
|
||||
[instance.id]: { healthy: health.healthy, lastCheck: Date.now() }
|
||||
[instance.id]: { healthy: health.healthy, lastCheck: Date.now() },
|
||||
}));
|
||||
} catch {
|
||||
setHealthStatus(prev => ({
|
||||
setHealthStatus((prev) => ({
|
||||
...prev,
|
||||
[instance.id]: { healthy: false, lastCheck: Date.now() }
|
||||
[instance.id]: { healthy: false, lastCheck: Date.now() },
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -87,7 +100,12 @@ export const InstanceList = ({ projectId, repoId, toolTypes }: InstanceListProps
|
||||
if (!selectedToolType) return;
|
||||
setError(null);
|
||||
try {
|
||||
await createInstance(projectId, repoId, selectedToolType, displayName || undefined);
|
||||
await createInstance(
|
||||
projectId,
|
||||
repoId,
|
||||
selectedToolType,
|
||||
displayName || undefined,
|
||||
);
|
||||
setShowCreate(false);
|
||||
setSelectedToolType("");
|
||||
setDisplayName("");
|
||||
@@ -130,7 +148,7 @@ export const InstanceList = ({ projectId, repoId, toolTypes }: InstanceListProps
|
||||
try {
|
||||
await deleteInstance(projectId, repoId, instanceId);
|
||||
// Update state immediately instead of reloading
|
||||
setInstances(prev => prev.filter(i => i.id !== instanceId));
|
||||
setInstances((prev) => prev.filter((i) => i.id !== instanceId));
|
||||
} catch {
|
||||
setError("Failed to delete instance");
|
||||
}
|
||||
@@ -181,9 +199,7 @@ export const InstanceList = ({ projectId, repoId, toolTypes }: InstanceListProps
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="error-message">{error}</div>
|
||||
)}
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
|
||||
{loading ? (
|
||||
<p className="muted">Loading instances...</p>
|
||||
@@ -194,7 +210,11 @@ export const InstanceList = ({ projectId, repoId, toolTypes }: InstanceListProps
|
||||
{instances.map((instance) => (
|
||||
<div key={instance.id} className="instance-card">
|
||||
<div className="instance-info">
|
||||
<div className="instance-name">{instance.display_name || instance.tool_type_name || "Unnamed Instance"}</div>
|
||||
<div className="instance-name">
|
||||
{instance.display_name ||
|
||||
instance.tool_type_name ||
|
||||
"Unnamed Instance"}
|
||||
</div>
|
||||
<div className="instance-meta">
|
||||
<span
|
||||
className="status-dot"
|
||||
@@ -210,10 +230,16 @@ export const InstanceList = ({ projectId, repoId, toolTypes }: InstanceListProps
|
||||
</div>
|
||||
</div>
|
||||
<div className="instance-actions">
|
||||
{instance.status === "running" && instance.url && instance.tool_type_interfaces.includes("web") && (
|
||||
{instance.status === "running" &&
|
||||
instance.url &&
|
||||
instance.tool_type_interfaces.includes("web") && (
|
||||
<>
|
||||
<a
|
||||
href={instance.url.startsWith("http") ? instance.url : `${API_BASE_URL}${instance.url}`}
|
||||
href={
|
||||
instance.url.startsWith("http")
|
||||
? instance.url
|
||||
: `${API_BASE_URL}${instance.url}`
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="secondary-button small"
|
||||
@@ -234,10 +260,13 @@ export const InstanceList = ({ projectId, repoId, toolTypes }: InstanceListProps
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{instance.status === "running" && instance.tool_type_interfaces.includes("terminal") && (
|
||||
{instance.status === "running" &&
|
||||
instance.tool_type_interfaces.includes("terminal") && (
|
||||
<button
|
||||
className="secondary-button small"
|
||||
onClick={() => navigate(`/instances/${instance.id}/terminal`)}
|
||||
onClick={() =>
|
||||
navigate(`/instances/${instance.id}/terminal`)
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="terminal" size="sm" />
|
||||
|
||||
@@ -36,7 +36,8 @@ export const RepositoriesSettingsTab: React.FC = () => {
|
||||
|
||||
const handleDelete = async (repoId: string) => {
|
||||
if (!projectId) return;
|
||||
if (!window.confirm("Are you sure you want to delete this repository?")) return;
|
||||
if (!window.confirm("Are you sure you want to delete this repository?"))
|
||||
return;
|
||||
try {
|
||||
await deleteRepository(projectId, repoId);
|
||||
setRepositories((current) => current.filter((r) => r.id !== repoId));
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import type { GitRepositoryCreate, URLParseResult } from "../types/git-repository";
|
||||
import type {
|
||||
GitRepositoryCreate,
|
||||
URLParseResult,
|
||||
} from "../types/git-repository";
|
||||
import { createRepository, parseGitUrl } from "../api/git_repositories";
|
||||
import { Icon } from "./icon";
|
||||
|
||||
type CreateMode = "clone" | "blank";
|
||||
type UrlValidationStatus = "idle" | "validating" | "valid" | "needs-parsing" | "invalid";
|
||||
type UrlValidationStatus =
|
||||
| "idle"
|
||||
| "validating"
|
||||
| "valid"
|
||||
| "needs-parsing"
|
||||
| "invalid";
|
||||
|
||||
interface RepositoryCreateDialogProps {
|
||||
projectId: string;
|
||||
@@ -15,7 +23,13 @@ interface RepositoryCreateDialogProps {
|
||||
onCreated: () => Promise<void> | void;
|
||||
}
|
||||
|
||||
export const RepositoryCreateDialog = ({ projectId, open, title, onClose, onCreated }: RepositoryCreateDialogProps) => {
|
||||
export const RepositoryCreateDialog = ({
|
||||
projectId,
|
||||
open,
|
||||
title,
|
||||
onClose,
|
||||
onCreated,
|
||||
}: RepositoryCreateDialogProps) => {
|
||||
const [createMode, setCreateMode] = useState<CreateMode>("clone");
|
||||
const [formName, setFormName] = useState("");
|
||||
const [owner, setOwner] = useState("");
|
||||
@@ -129,7 +143,9 @@ export const RepositoryCreateDialog = ({ projectId, open, title, onClose, onCrea
|
||||
} catch (error: unknown) {
|
||||
const response = error as { response?: { data?: { detail?: string } } };
|
||||
const detail = response.response?.data?.detail;
|
||||
setFormError(typeof detail === "string" ? detail : "Failed to create repository");
|
||||
setFormError(
|
||||
typeof detail === "string" ? detail : "Failed to create repository",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -161,7 +177,8 @@ export const RepositoryCreateDialog = ({ projectId, open, title, onClose, onCrea
|
||||
<div className="dialog">
|
||||
<h3>{title}</h3>
|
||||
<p className="muted">
|
||||
Clone an existing repository from git.commumedia.org, or create a blank bare repo here.
|
||||
Clone an existing repository from git.commumedia.org, or create a
|
||||
blank bare repo here.
|
||||
</p>
|
||||
<form onSubmit={handleSubmit} className="stack">
|
||||
<div className="form-field">
|
||||
@@ -213,7 +230,10 @@ export const RepositoryCreateDialog = ({ projectId, open, title, onClose, onCrea
|
||||
placeholder="repo-name"
|
||||
/>
|
||||
</label>
|
||||
<p className="muted">SSH target: git@git.commumedia.org:{owner || "owner"}/{repoName || "repo"}.git</p>
|
||||
<p className="muted">
|
||||
SSH target: git@git.commumedia.org:{owner || "owner"}/
|
||||
{repoName || "repo"}.git
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button small"
|
||||
@@ -234,20 +254,26 @@ export const RepositoryCreateDialog = ({ projectId, open, title, onClose, onCrea
|
||||
className={getUrlInputClass()}
|
||||
/>
|
||||
{urlValidation.status === "validating" && (
|
||||
<span className="validation-status validating">Validating...</span>
|
||||
<span className="validation-status validating">
|
||||
Validating...
|
||||
</span>
|
||||
)}
|
||||
{urlValidation.status === "valid" && (
|
||||
<span className="validation-status valid">
|
||||
<Icon name="success" size="sm" /> Valid git URL
|
||||
</span>
|
||||
)}
|
||||
{urlValidation.status === "needs-parsing" && urlValidation.result && (
|
||||
{urlValidation.status === "needs-parsing" &&
|
||||
urlValidation.result && (
|
||||
<div className="url-suggestion">
|
||||
<span className="validation-status warning">
|
||||
<Icon name="warning" size="sm" /> This looks like a browser URL
|
||||
<Icon name="warning" size="sm" /> This looks like a
|
||||
browser URL
|
||||
</span>
|
||||
<div className="suggestion-actions">
|
||||
<span className="suggested-url">Suggested: {urlValidation.result.base_url}</span>
|
||||
<span className="suggested-url">
|
||||
Suggested: {urlValidation.result.base_url}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button small"
|
||||
@@ -278,13 +304,19 @@ export const RepositoryCreateDialog = ({ projectId, open, title, onClose, onCrea
|
||||
</div>
|
||||
)}
|
||||
<div className="dialog-actions">
|
||||
<button className="secondary-button" onClick={handleClose} type="button">
|
||||
<button
|
||||
className="secondary-button"
|
||||
onClick={handleClose}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="cancel" size="sm" />
|
||||
Cancel
|
||||
</button>
|
||||
<button className="primary-button" type="submit">
|
||||
<Icon name="add" size="sm" />
|
||||
{createMode === "clone" ? "Clone Repository" : "Create Blank Repository"}
|
||||
{createMode === "clone"
|
||||
? "Clone Repository"
|
||||
: "Create Blank Repository"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -2,7 +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 } from "../api/sessions";
|
||||
import {
|
||||
createInstance,
|
||||
getUserSessions,
|
||||
startInstance,
|
||||
stopInstance,
|
||||
deleteInstance,
|
||||
recreateInstanceTunnel,
|
||||
} from "../api/sessions";
|
||||
import { listProjects } from "../api/projects";
|
||||
import { listRepositories } from "../api/git_repositories";
|
||||
import { listToolTypes } from "../api/tool_types";
|
||||
@@ -35,14 +42,17 @@ export const HomePage = () => {
|
||||
const [selectedRepo, setSelectedRepo] = useState("");
|
||||
const [selectedToolType, setSelectedToolType] = useState("");
|
||||
const [displayName, setDisplayName] = useState("");
|
||||
const [saveState, setSaveState] = useState<"idle" | "saving" | "error">("idle");
|
||||
const [saveState, setSaveState] = useState<"idle" | "saving" | "error">(
|
||||
"idle",
|
||||
);
|
||||
const [actionBusy, setActionBusy] = useState<string | null>(null);
|
||||
const safeSessions = Array.isArray(sessions) ? sessions : [];
|
||||
|
||||
const loadHome = useCallback(async () => {
|
||||
setStatus("loading");
|
||||
try {
|
||||
const [dashboard, sessionData, projectData, toolTypeData] = await Promise.all([
|
||||
const [dashboard, sessionData, projectData, toolTypeData] =
|
||||
await Promise.all([
|
||||
getDashboardSummary(),
|
||||
getUserSessions(),
|
||||
listProjects(),
|
||||
@@ -81,13 +91,19 @@ export const HomePage = () => {
|
||||
}, [selectedProject]);
|
||||
|
||||
const activeSessions = useMemo(
|
||||
() => safeSessions.filter((session) => ["running", "building", "pending"].includes(session.status)),
|
||||
[safeSessions]
|
||||
() =>
|
||||
safeSessions.filter((session) =>
|
||||
["running", "building", "pending"].includes(session.status),
|
||||
),
|
||||
[safeSessions],
|
||||
);
|
||||
|
||||
const recentSessions = useMemo(
|
||||
() => safeSessions.filter((session) => ["stopped", "error"].includes(session.status)).slice(0, 5),
|
||||
[safeSessions]
|
||||
() =>
|
||||
safeSessions
|
||||
.filter((session) => ["stopped", "error"].includes(session.status))
|
||||
.slice(0, 5),
|
||||
[safeSessions],
|
||||
);
|
||||
|
||||
const handleCreate = async (event: React.FormEvent) => {
|
||||
@@ -96,7 +112,12 @@ export const HomePage = () => {
|
||||
|
||||
setSaveState("saving");
|
||||
try {
|
||||
const instance = await createInstance(selectedProject, selectedRepo, selectedToolType, displayName || undefined);
|
||||
const instance = await createInstance(
|
||||
selectedProject,
|
||||
selectedRepo,
|
||||
selectedToolType,
|
||||
displayName || undefined,
|
||||
);
|
||||
await startInstance(selectedProject, selectedRepo, instance.id);
|
||||
await updateUserConfig({ last_session_id: instance.id });
|
||||
setDisplayName("");
|
||||
@@ -135,7 +156,11 @@ export const HomePage = () => {
|
||||
const handleDelete = async (session: SessionView) => {
|
||||
setActionBusy(session.id);
|
||||
try {
|
||||
await deleteInstance(session.project_id, session.repository_id, session.id);
|
||||
await deleteInstance(
|
||||
session.project_id,
|
||||
session.repository_id,
|
||||
session.id,
|
||||
);
|
||||
await loadHome();
|
||||
} finally {
|
||||
setActionBusy(null);
|
||||
@@ -145,7 +170,11 @@ export const HomePage = () => {
|
||||
const handleRecreateTunnel = async (session: SessionView) => {
|
||||
setActionBusy(session.id);
|
||||
try {
|
||||
await recreateInstanceTunnel(session.project_id, session.repository_id, session.id);
|
||||
await recreateInstanceTunnel(
|
||||
session.project_id,
|
||||
session.repository_id,
|
||||
session.id,
|
||||
);
|
||||
await loadHome();
|
||||
} finally {
|
||||
setActionBusy(null);
|
||||
@@ -158,11 +187,26 @@ export const HomePage = () => {
|
||||
<div className="stack-sm">
|
||||
<p className="eyebrow">Workspace overview</p>
|
||||
<h1>Home</h1>
|
||||
<p className="muted">Open sessions, available projects, and the fastest path back into work.</p>
|
||||
<p className="muted">
|
||||
Open sessions, available projects, and the fastest path back into
|
||||
work.
|
||||
</p>
|
||||
</div>
|
||||
<div className="home-hero-actions">
|
||||
<button className="primary-button" type="button" onClick={() => navigate("/projects")}>New Project</button>
|
||||
<button className="secondary-button" type="button" onClick={() => navigate("/settings")}>Settings</button>
|
||||
<button
|
||||
className="primary-button"
|
||||
type="button"
|
||||
onClick={() => navigate("/projects")}
|
||||
>
|
||||
New Project
|
||||
</button>
|
||||
<button
|
||||
className="secondary-button"
|
||||
type="button"
|
||||
onClick={() => navigate("/settings")}
|
||||
>
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -171,7 +215,11 @@ export const HomePage = () => {
|
||||
{status === "error" && (
|
||||
<div className="card stack">
|
||||
<p>Unable to load your workspace overview.</p>
|
||||
<button className="secondary-button" type="button" onClick={() => void loadHome()}>
|
||||
<button
|
||||
className="secondary-button"
|
||||
type="button"
|
||||
onClick={() => void loadHome()}
|
||||
>
|
||||
<Icon name="refresh" size="sm" />
|
||||
Retry
|
||||
</button>
|
||||
@@ -211,25 +259,48 @@ export const HomePage = () => {
|
||||
<div className="stack-sm">
|
||||
<div className="row row-tight">
|
||||
<h3>{session.display_name}</h3>
|
||||
<span className={`status-badge ${session.status}`}>{session.status}</span>
|
||||
<span className={`status-badge ${session.status}`}>
|
||||
{session.status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="muted">{session.project_name} · {session.repository_name}</p>
|
||||
<p className="muted">
|
||||
{session.project_name} · {session.repository_name}
|
||||
</p>
|
||||
<p className="muted">{session.tool_type_name}</p>
|
||||
</div>
|
||||
<div className="session-actions">
|
||||
<button className="secondary-button small" type="button" onClick={() => handleOpen(session)}>
|
||||
<button
|
||||
className="secondary-button small"
|
||||
type="button"
|
||||
onClick={() => handleOpen(session)}
|
||||
>
|
||||
<Icon name="external" size="sm" />
|
||||
Open
|
||||
</button>
|
||||
<button className="ghost-button small" type="button" onClick={() => void handleRecreateTunnel(session)} disabled={actionBusy === session.id}>
|
||||
<button
|
||||
className="ghost-button small"
|
||||
type="button"
|
||||
onClick={() => void handleRecreateTunnel(session)}
|
||||
disabled={actionBusy === session.id}
|
||||
>
|
||||
<Icon name="refresh" size="sm" />
|
||||
Tunnel
|
||||
</button>
|
||||
<button className="ghost-button small" type="button" onClick={() => void handleStop(session)} disabled={actionBusy === session.id}>
|
||||
<button
|
||||
className="ghost-button small"
|
||||
type="button"
|
||||
onClick={() => void handleStop(session)}
|
||||
disabled={actionBusy === session.id}
|
||||
>
|
||||
<Icon name="stop" size="sm" />
|
||||
Stop
|
||||
</button>
|
||||
<button className="ghost-button small danger-text" type="button" onClick={() => void handleDelete(session)} disabled={actionBusy === session.id}>
|
||||
<button
|
||||
className="ghost-button small danger-text"
|
||||
type="button"
|
||||
onClick={() => void handleDelete(session)}
|
||||
disabled={actionBusy === session.id}
|
||||
>
|
||||
<Icon name="delete" size="sm" />
|
||||
Delete
|
||||
</button>
|
||||
@@ -246,19 +317,34 @@ export const HomePage = () => {
|
||||
<p className="eyebrow">Available projects</p>
|
||||
<h2>{projects.length}</h2>
|
||||
</div>
|
||||
<button className="secondary-button" type="button" onClick={() => navigate("/projects")}>View all</button>
|
||||
<button
|
||||
className="secondary-button"
|
||||
type="button"
|
||||
onClick={() => navigate("/projects")}
|
||||
>
|
||||
View all
|
||||
</button>
|
||||
</div>
|
||||
{projects.length === 0 ? (
|
||||
<p className="muted">No projects yet.</p>
|
||||
) : (
|
||||
<div className="home-project-grid">
|
||||
{projects.map((project) => (
|
||||
<article className="card project-card home-project-card" key={project.id}>
|
||||
<article
|
||||
className="card project-card home-project-card"
|
||||
key={project.id}
|
||||
>
|
||||
<div className="stack-sm">
|
||||
<h3>{project.name}</h3>
|
||||
{project.description && <p className="muted">{project.description}</p>}
|
||||
{project.description && (
|
||||
<p className="muted">{project.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<button className="ghost-button small" type="button" onClick={() => navigate(`/projects/${project.id}`)}>
|
||||
<button
|
||||
className="ghost-button small"
|
||||
type="button"
|
||||
onClick={() => navigate(`/projects/${project.id}`)}
|
||||
>
|
||||
Open Workspace
|
||||
</button>
|
||||
</article>
|
||||
@@ -278,35 +364,81 @@ export const HomePage = () => {
|
||||
<div className="form-row">
|
||||
<label className="form-field">
|
||||
Project
|
||||
<select value={selectedProject} onChange={(event) => { setSelectedProject(event.target.value); setSelectedRepo(""); }}>
|
||||
<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>)}
|
||||
{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}>
|
||||
<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>)}
|
||||
{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)}>
|
||||
<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>)}
|
||||
{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" />
|
||||
<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
|
||||
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>}
|
||||
{saveState === "error" && (
|
||||
<span className="error-text">Failed to create session</span>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
@@ -323,10 +455,20 @@ export const HomePage = () => {
|
||||
{recentSessions.map((session) => (
|
||||
<article className="recent-session-item" key={session.id}>
|
||||
<div className="recent-session-info">
|
||||
<span className="recent-session-name">{session.display_name}</span>
|
||||
<span className="muted">{session.project_name} · {session.tool_type_name}</span>
|
||||
<span className="recent-session-name">
|
||||
{session.display_name}
|
||||
</span>
|
||||
<span className="muted">
|
||||
{session.project_name} · {session.tool_type_name}
|
||||
</span>
|
||||
</div>
|
||||
<button className="ghost-button small" type="button" onClick={() => handleOpen(session)}>Open</button>
|
||||
<button
|
||||
className="ghost-button small"
|
||||
type="button"
|
||||
onClick={() => handleOpen(session)}
|
||||
>
|
||||
Open
|
||||
</button>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,7 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import type { GitRepository } from "../types/git-repository";
|
||||
import {
|
||||
deleteRepository,
|
||||
listRepositories,
|
||||
} from "../api/git_repositories";
|
||||
import { deleteRepository, listRepositories } from "../api/git_repositories";
|
||||
import { Icon } from "../components/icon";
|
||||
import { RepositoryCreateDialog } from "../components/repository-create-dialog";
|
||||
|
||||
@@ -53,7 +50,11 @@ export const GitRepositoriesPage = () => {
|
||||
<section className="stack">
|
||||
<div className="page-header">
|
||||
<h1>Repositories</h1>
|
||||
<button className="primary-button" onClick={() => setShowCreate(true)} type="button">
|
||||
<button
|
||||
className="primary-button"
|
||||
onClick={() => setShowCreate(true)}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="add" size="sm" />
|
||||
New Repository
|
||||
</button>
|
||||
@@ -64,14 +65,22 @@ export const GitRepositoriesPage = () => {
|
||||
{status === "error" && (
|
||||
<div className="card stack">
|
||||
<p>Failed to load repositories</p>
|
||||
<button className="secondary-button" onClick={() => void loadRepositories()} type="button">
|
||||
<button
|
||||
className="secondary-button"
|
||||
onClick={() => void loadRepositories()}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="refresh" size="sm" />
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isEmpty && <p className="muted">No repositories yet. Create your first repository above.</p>}
|
||||
{isEmpty && (
|
||||
<p className="muted">
|
||||
No repositories yet. Create your first repository above.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{status === "ready" && repositories.length > 0 && (
|
||||
<div className="repository-list">
|
||||
@@ -87,7 +96,11 @@ export const GitRepositoriesPage = () => {
|
||||
<div className="repository-actions">
|
||||
<button
|
||||
className="secondary-button small"
|
||||
onClick={() => navigate(`/projects/${projectId}/repositories/${repo.id}/history`)}
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`/projects/${projectId}/repositories/${repo.id}/history`,
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
History
|
||||
|
||||
@@ -48,7 +48,7 @@ export const RepoWorkspace = () => {
|
||||
const [project, setProject] = useState<Project | null>(null);
|
||||
const [repositories, setRepositories] = useState<GitRepository[]>([]);
|
||||
const [selectedRepoId, setSelectedRepoId] = useState<string | null>(
|
||||
searchParams.get("repo")
|
||||
searchParams.get("repo"),
|
||||
);
|
||||
const [branches, setBranches] = useState<string[]>([]);
|
||||
const [currentBranch, setCurrentBranch] = useState<string>("main");
|
||||
@@ -95,9 +95,11 @@ export const RepoWorkspace = () => {
|
||||
if (!projectId || !selectedRepoId) return;
|
||||
try {
|
||||
const response = await apiClient.get(
|
||||
`/projects/${projectId}/repositories/${selectedRepoId}/branches`
|
||||
`/projects/${projectId}/repositories/${selectedRepoId}/branches`,
|
||||
);
|
||||
const branchList = response.data.branches.map(
|
||||
(b: { name: string }) => b.name,
|
||||
);
|
||||
const branchList = response.data.branches.map((b: { name: string }) => b.name);
|
||||
setBranches(branchList);
|
||||
const defaultBranch = response.data.default_branch;
|
||||
if (defaultBranch) {
|
||||
@@ -152,15 +154,10 @@ export const RepoWorkspace = () => {
|
||||
return (
|
||||
<section className="repo-workspace">
|
||||
{project && (
|
||||
<WorkspaceHeader
|
||||
project={project}
|
||||
currentRepo={selectedRepo || null}
|
||||
/>
|
||||
<WorkspaceHeader project={project} currentRepo={selectedRepo || null} />
|
||||
)}
|
||||
|
||||
{status === "loading" && (
|
||||
<p className="muted">Loading repositories...</p>
|
||||
)}
|
||||
{status === "loading" && <p className="muted">Loading repositories...</p>}
|
||||
|
||||
{status === "error" && (
|
||||
<div className="card stack">
|
||||
@@ -245,7 +242,9 @@ export const RepoWorkspace = () => {
|
||||
untracked={gitStatus.untracked}
|
||||
onCommit={() => {
|
||||
void loadGitStatus();
|
||||
window.dispatchEvent(new CustomEvent("refresh-file-tree"));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("refresh-file-tree"),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -301,7 +300,7 @@ const FileBrowser = ({
|
||||
branch,
|
||||
path,
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
setEntries(response.data.entries || []);
|
||||
} catch {
|
||||
@@ -361,7 +360,11 @@ const FileBrowser = ({
|
||||
return (
|
||||
<div className="file-tree">
|
||||
{path && (
|
||||
<button className="tree-entry tree-up" onClick={navigateUp} type="button">
|
||||
<button
|
||||
className="tree-entry tree-up"
|
||||
onClick={navigateUp}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="folder" size="sm" /> ..
|
||||
</button>
|
||||
)}
|
||||
@@ -369,7 +372,8 @@ const FileBrowser = ({
|
||||
<p className="muted">No files in this repository yet.</p>
|
||||
)}
|
||||
{entries.map((entry) => {
|
||||
const fileStatus = entry.type === "file" ? getFileStatus(entry.path) : null;
|
||||
const fileStatus =
|
||||
entry.type === "file" ? getFileStatus(entry.path) : null;
|
||||
return (
|
||||
<button
|
||||
key={entry.path}
|
||||
@@ -377,7 +381,11 @@ const FileBrowser = ({
|
||||
onClick={() => handleEntryClick(entry)}
|
||||
type="button"
|
||||
>
|
||||
<Icon name={entry.type === "directory" ? "folder" : "file"} size="sm" /> {entry.name}
|
||||
<Icon
|
||||
name={entry.type === "directory" ? "folder" : "file"}
|
||||
size="sm"
|
||||
/>{" "}
|
||||
{entry.name}
|
||||
{fileStatus && (
|
||||
<span className={`file-status-indicator ${fileStatus}`}>
|
||||
{fileStatus === "modified" && "M"}
|
||||
|
||||
@@ -42,7 +42,12 @@ export const SessionsPage = () => {
|
||||
|
||||
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
|
||||
const [stopConfirmId, setStopConfirmId] = useState<string | null>(null);
|
||||
const [tunnelHealth, setTunnelHealth] = useState<Record<string, { healthy: boolean; status_code: number | null; error?: string }>>({});
|
||||
const [tunnelHealth, setTunnelHealth] = useState<
|
||||
Record<
|
||||
string,
|
||||
{ healthy: boolean; status_code: number | null; error?: string }
|
||||
>
|
||||
>({});
|
||||
const [recreatingId, setRecreatingId] = useState<string | null>(null);
|
||||
|
||||
const loadSessions = useCallback(async () => {
|
||||
@@ -92,14 +97,14 @@ export const SessionsPage = () => {
|
||||
useEffect(() => {
|
||||
const checkHealth = async () => {
|
||||
const runningSessions = sessions.filter(
|
||||
(s) => s.status === "running" && s.url
|
||||
(s) => s.status === "running" && s.url,
|
||||
);
|
||||
for (const session of runningSessions) {
|
||||
try {
|
||||
const health = await checkInstanceHealth(
|
||||
session.project_id,
|
||||
session.repository_id,
|
||||
session.id
|
||||
session.id,
|
||||
);
|
||||
setTunnelHealth((prev) => ({
|
||||
...prev,
|
||||
@@ -108,7 +113,11 @@ export const SessionsPage = () => {
|
||||
} catch {
|
||||
setTunnelHealth((prev) => ({
|
||||
...prev,
|
||||
[session.id]: { healthy: false, status_code: null, error: "check failed" },
|
||||
[session.id]: {
|
||||
healthy: false,
|
||||
status_code: null,
|
||||
error: "check failed",
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -137,18 +146,24 @@ export const SessionsPage = () => {
|
||||
}, [selectedProject]);
|
||||
|
||||
const activeSessions = useMemo(
|
||||
() => sessions.filter((s) => ["running", "building", "pending"].includes(s.status)),
|
||||
[sessions]
|
||||
() =>
|
||||
sessions.filter((s) =>
|
||||
["running", "building", "pending"].includes(s.status),
|
||||
),
|
||||
[sessions],
|
||||
);
|
||||
|
||||
const recentSessions = useMemo(
|
||||
() => sessions.filter((s) => ["stopped", "error"].includes(s.status)).slice(0, 5),
|
||||
[sessions]
|
||||
() =>
|
||||
sessions
|
||||
.filter((s) => ["stopped", "error"].includes(s.status))
|
||||
.slice(0, 5),
|
||||
[sessions],
|
||||
);
|
||||
|
||||
const lastSession = useMemo(
|
||||
() => sessions.find((s) => s.id === lastSessionId) ?? null,
|
||||
[sessions, lastSessionId]
|
||||
[sessions, lastSessionId],
|
||||
);
|
||||
|
||||
const handleCreate = async (e: React.FormEvent) => {
|
||||
@@ -166,7 +181,7 @@ export const SessionsPage = () => {
|
||||
selectedProject,
|
||||
selectedRepo,
|
||||
selectedToolType,
|
||||
displayName || undefined
|
||||
displayName || undefined,
|
||||
);
|
||||
|
||||
// Auto-start the instance
|
||||
@@ -185,7 +200,11 @@ export const SessionsPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleStop = async (sessionId: string, projectId: string, repoId: string) => {
|
||||
const handleStop = async (
|
||||
sessionId: string,
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
) => {
|
||||
try {
|
||||
await stopInstance(projectId, repoId, sessionId);
|
||||
setStopConfirmId(null);
|
||||
@@ -195,7 +214,11 @@ export const SessionsPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (sessionId: string, projectId: string, repoId: string) => {
|
||||
const handleDelete = async (
|
||||
sessionId: string,
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
) => {
|
||||
try {
|
||||
await deleteInstance(projectId, repoId, sessionId);
|
||||
setDeleteConfirmId(null);
|
||||
@@ -212,7 +235,7 @@ export const SessionsPage = () => {
|
||||
await recreateInstanceTunnel(
|
||||
session.project_id,
|
||||
session.repository_id,
|
||||
session.id
|
||||
session.id,
|
||||
);
|
||||
// Refresh sessions to get new URL
|
||||
await loadSessions();
|
||||
@@ -225,7 +248,7 @@ export const SessionsPage = () => {
|
||||
|
||||
const handleOpen = (session: Session) => {
|
||||
if (session.url) {
|
||||
window.open(session.url, '_blank', 'noopener,noreferrer');
|
||||
window.open(session.url, "_blank", "noopener,noreferrer");
|
||||
} else if (session.tool_type_interfaces?.includes("terminal")) {
|
||||
navigate(`/instances/${session.id}/terminal`);
|
||||
} else {
|
||||
@@ -253,7 +276,11 @@ export const SessionsPage = () => {
|
||||
{status === "error" && (
|
||||
<div className="card stack">
|
||||
<p>Failed to load sessions</p>
|
||||
<button className="secondary-button" onClick={() => void loadSessions()} type="button">
|
||||
<button
|
||||
className="secondary-button"
|
||||
onClick={() => void loadSessions()}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="refresh" size="sm" />
|
||||
Retry
|
||||
</button>
|
||||
@@ -268,18 +295,29 @@ export const SessionsPage = () => {
|
||||
<h2>Last Session</h2>
|
||||
<div className="card last-session-card">
|
||||
<div className="last-session-info">
|
||||
<h3>{lastSession.display_name || lastSession.tool_type_name || "Unnamed Session"}</h3>
|
||||
<h3>
|
||||
{lastSession.display_name ||
|
||||
lastSession.tool_type_name ||
|
||||
"Unnamed Session"}
|
||||
</h3>
|
||||
<p className="muted">
|
||||
{lastSession.tool_type_name} · {lastSession.project_name} · {lastSession.repository_name}
|
||||
{lastSession.tool_type_name} · {lastSession.project_name} ·{" "}
|
||||
{lastSession.repository_name}
|
||||
</p>
|
||||
{lastSession.url && (
|
||||
<p className="session-url">
|
||||
<a href={lastSession.url} target="_blank" rel="noopener noreferrer">
|
||||
<a
|
||||
href={lastSession.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{lastSession.url}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
<span className={`status-badge ${lastSession.status}`}>{lastSession.status}</span>
|
||||
<span className={`status-badge ${lastSession.status}`}>
|
||||
{lastSession.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="last-session-actions">
|
||||
{lastSession.url ? (
|
||||
@@ -293,7 +331,11 @@ export const SessionsPage = () => {
|
||||
Open
|
||||
</a>
|
||||
) : (
|
||||
<button className="primary-button" onClick={handleResumeLast} type="button">
|
||||
<button
|
||||
className="primary-button"
|
||||
onClick={handleResumeLast}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="play" size="sm" />
|
||||
Resume
|
||||
</button>
|
||||
@@ -318,20 +360,33 @@ export const SessionsPage = () => {
|
||||
{activeSessions.map((session) => (
|
||||
<div className="card session-card" key={session.id}>
|
||||
<div className="session-info">
|
||||
<h4>{session.display_name || session.tool_type_name || "Unnamed Session"}</h4>
|
||||
<h4>
|
||||
{session.display_name ||
|
||||
session.tool_type_name ||
|
||||
"Unnamed Session"}
|
||||
</h4>
|
||||
<p className="muted">
|
||||
{session.tool_type_name} · {session.project_name}
|
||||
</p>
|
||||
{session.url && (
|
||||
<p className="session-url">
|
||||
<a href={session.url} target="_blank" rel="noopener noreferrer">
|
||||
<a
|
||||
href={session.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{session.url}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
<span className={`status-badge ${session.status}`}>{session.status}</span>
|
||||
{tunnelHealth[session.id] && !tunnelHealth[session.id].healthy && (
|
||||
<span className="status-badge error">tunnel error</span>
|
||||
<span className={`status-badge ${session.status}`}>
|
||||
{session.status}
|
||||
</span>
|
||||
{tunnelHealth[session.id] &&
|
||||
!tunnelHealth[session.id].healthy && (
|
||||
<span className="status-badge error">
|
||||
tunnel error
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="session-actions">
|
||||
@@ -355,7 +410,8 @@ export const SessionsPage = () => {
|
||||
Open
|
||||
</button>
|
||||
)}
|
||||
{tunnelHealth[session.id] && !tunnelHealth[session.id].healthy && (
|
||||
{tunnelHealth[session.id] &&
|
||||
!tunnelHealth[session.id].healthy && (
|
||||
<button
|
||||
className="secondary-button small"
|
||||
onClick={() => void handleRecreateTunnel(session)}
|
||||
@@ -363,7 +419,9 @@ export const SessionsPage = () => {
|
||||
disabled={recreatingId === session.id}
|
||||
>
|
||||
<Icon name="refresh" size="sm" />
|
||||
{recreatingId === session.id ? "Recreating..." : "Recreate Tunnel"}
|
||||
{recreatingId === session.id
|
||||
? "Recreating..."
|
||||
: "Recreate Tunnel"}
|
||||
</button>
|
||||
)}
|
||||
{stopConfirmId === session.id ? (
|
||||
@@ -375,7 +433,7 @@ export const SessionsPage = () => {
|
||||
void handleStop(
|
||||
session.id,
|
||||
session.project_id,
|
||||
session.repository_id
|
||||
session.repository_id,
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
@@ -408,7 +466,7 @@ export const SessionsPage = () => {
|
||||
void handleDelete(
|
||||
session.id,
|
||||
session.project_id,
|
||||
session.repository_id
|
||||
session.repository_id,
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
@@ -447,7 +505,11 @@ export const SessionsPage = () => {
|
||||
{recentSessions.map((session) => (
|
||||
<div className="recent-session-item" key={session.id}>
|
||||
<div className="recent-session-info">
|
||||
<span className="recent-session-name">{session.display_name || session.tool_type_name || "Unnamed Session"}</span>
|
||||
<span className="recent-session-name">
|
||||
{session.display_name ||
|
||||
session.tool_type_name ||
|
||||
"Unnamed Session"}
|
||||
</span>
|
||||
<span className="muted">
|
||||
{session.tool_type_name} · {session.project_name}
|
||||
</span>
|
||||
@@ -479,7 +541,7 @@ export const SessionsPage = () => {
|
||||
void handleDelete(
|
||||
session.id,
|
||||
session.project_id,
|
||||
session.repository_id
|
||||
session.repository_id,
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
@@ -513,7 +575,10 @@ export const SessionsPage = () => {
|
||||
{/* Create Session */}
|
||||
<div className="create-session-section">
|
||||
<h2>Create New Session</h2>
|
||||
<form onSubmit={handleCreate} className="card stack create-session-form">
|
||||
<form
|
||||
onSubmit={handleCreate}
|
||||
className="card stack create-session-form"
|
||||
>
|
||||
<div className="form-row">
|
||||
<label className="form-field">
|
||||
Project
|
||||
|
||||
@@ -28,7 +28,9 @@ export const ToolConfigsPage = () => {
|
||||
config_type: "env",
|
||||
file_path: "",
|
||||
});
|
||||
const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle");
|
||||
const [saveStatus, setSaveStatus] = useState<
|
||||
"idle" | "saving" | "saved" | "error"
|
||||
>("idle");
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
try {
|
||||
@@ -60,7 +62,8 @@ export const ToolConfigsPage = () => {
|
||||
key: formData.key,
|
||||
value: formData.value,
|
||||
config_type: formData.config_type,
|
||||
file_path: formData.config_type === "file" ? formData.file_path : undefined,
|
||||
file_path:
|
||||
formData.config_type === "file" ? formData.file_path : undefined,
|
||||
};
|
||||
|
||||
if (editingConfig) {
|
||||
@@ -102,7 +105,7 @@ export const ToolConfigsPage = () => {
|
||||
};
|
||||
|
||||
const filteredConfigs = configs.filter(
|
||||
(c) => c.tool_type_id === selectedToolType
|
||||
(c) => c.tool_type_id === selectedToolType,
|
||||
);
|
||||
|
||||
const selectedTool = toolTypes.find((t) => t.id === selectedToolType);
|
||||
@@ -126,7 +129,11 @@ export const ToolConfigsPage = () => {
|
||||
</div>
|
||||
<div className="card stack">
|
||||
<p>Failed to load configurations</p>
|
||||
<button className="secondary-button" onClick={() => void loadData()} type="button">
|
||||
<button
|
||||
className="secondary-button"
|
||||
onClick={() => void loadData()}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="refresh" size="sm" />
|
||||
Retry
|
||||
</button>
|
||||
@@ -142,7 +149,11 @@ export const ToolConfigsPage = () => {
|
||||
<p className="eyebrow">Settings</p>
|
||||
<h1>Tool Configurations</h1>
|
||||
</div>
|
||||
<button className="secondary-button" type="button" onClick={() => navigate("/settings")}>
|
||||
<button
|
||||
className="secondary-button"
|
||||
type="button"
|
||||
onClick={() => navigate("/settings")}
|
||||
>
|
||||
Back to settings
|
||||
</button>
|
||||
<p className="muted">
|
||||
@@ -171,21 +182,30 @@ export const ToolConfigsPage = () => {
|
||||
</select>
|
||||
{selectedTool && (
|
||||
<p className="muted" style={{ marginTop: "0.5rem" }}>
|
||||
Category: {selectedTool.category} · Interfaces: {selectedTool.interfaces?.join(", ")}
|
||||
Category: {selectedTool.category} · Interfaces:{" "}
|
||||
{selectedTool.interfaces?.join(", ")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Config List */}
|
||||
<div className="card stack">
|
||||
<div className="row" style={{ justifyContent: "space-between", alignItems: "center" }}>
|
||||
<div
|
||||
className="row"
|
||||
style={{ justifyContent: "space-between", alignItems: "center" }}
|
||||
>
|
||||
<h2>Configuration Variables</h2>
|
||||
<button
|
||||
className="primary-button small"
|
||||
onClick={() => {
|
||||
setShowForm(true);
|
||||
setEditingConfig(null);
|
||||
setFormData({ key: "", value: "", config_type: "env", file_path: "" });
|
||||
setFormData({
|
||||
key: "",
|
||||
value: "",
|
||||
config_type: "env",
|
||||
file_path: "",
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
@@ -210,14 +230,20 @@ export const ToolConfigsPage = () => {
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div className="row" style={{ gap: "0.5rem", alignItems: "center" }}>
|
||||
<div
|
||||
className="row"
|
||||
style={{ gap: "0.5rem", alignItems: "center" }}
|
||||
>
|
||||
<code style={{ fontWeight: 600 }}>{config.key}</code>
|
||||
<span
|
||||
className="badge"
|
||||
style={{
|
||||
fontSize: "0.7rem",
|
||||
textTransform: "uppercase",
|
||||
background: config.config_type === "env" ? "var(--color-info)" : "var(--color-warning)",
|
||||
background:
|
||||
config.config_type === "env"
|
||||
? "var(--color-info)"
|
||||
: "var(--color-warning)",
|
||||
color: "white",
|
||||
padding: "0.125rem 0.5rem",
|
||||
borderRadius: "9999px",
|
||||
@@ -226,7 +252,10 @@ export const ToolConfigsPage = () => {
|
||||
{config.config_type}
|
||||
</span>
|
||||
</div>
|
||||
<p className="muted" style={{ marginTop: "0.25rem", fontSize: "0.875rem" }}>
|
||||
<p
|
||||
className="muted"
|
||||
style={{ marginTop: "0.25rem", fontSize: "0.875rem" }}
|
||||
>
|
||||
{config.config_type === "file" && config.file_path
|
||||
? `File: ${config.file_path}`
|
||||
: "Environment variable"}
|
||||
@@ -265,7 +294,9 @@ export const ToolConfigsPage = () => {
|
||||
id="config-key"
|
||||
type="text"
|
||||
value={formData.key}
|
||||
onChange={(e) => setFormData({ ...formData, key: e.target.value })}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, key: e.target.value })
|
||||
}
|
||||
placeholder="e.g., OPENAI_API_KEY"
|
||||
className="form-input"
|
||||
required
|
||||
@@ -309,7 +340,9 @@ export const ToolConfigsPage = () => {
|
||||
<textarea
|
||||
id="config-value"
|
||||
value={formData.value}
|
||||
onChange={(e) => setFormData({ ...formData, value: e.target.value })}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, value: e.target.value })
|
||||
}
|
||||
placeholder={
|
||||
formData.config_type === "env"
|
||||
? "Enter value..."
|
||||
@@ -321,7 +354,10 @@ export const ToolConfigsPage = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="row" style={{ gap: "0.5rem", justifyContent: "flex-end" }}>
|
||||
<div
|
||||
className="row"
|
||||
style={{ gap: "0.5rem", justifyContent: "flex-end" }}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import type { ToolType, CreateToolTypeRequest, UpdateToolTypeRequest } from "../types/tool-type";
|
||||
import type {
|
||||
ToolType,
|
||||
CreateToolTypeRequest,
|
||||
UpdateToolTypeRequest,
|
||||
} from "../types/tool-type";
|
||||
import {
|
||||
createToolType,
|
||||
deleteToolType,
|
||||
@@ -128,7 +132,8 @@ export const ToolTypesPage = () => {
|
||||
await loadToolTypes();
|
||||
} catch (err) {
|
||||
const axiosError = err as { response?: { data?: { detail?: string } } };
|
||||
const detail = axiosError?.response?.data?.detail || "Failed to save tool type";
|
||||
const detail =
|
||||
axiosError?.response?.data?.detail || "Failed to save tool type";
|
||||
setFormError(detail);
|
||||
}
|
||||
};
|
||||
@@ -171,7 +176,13 @@ export const ToolTypesPage = () => {
|
||||
<h1>Tool Types</h1>
|
||||
</div>
|
||||
<div className="row">
|
||||
<button className="secondary-button" type="button" onClick={() => navigate("/settings")}>Back to settings</button>
|
||||
<button
|
||||
className="secondary-button"
|
||||
type="button"
|
||||
onClick={() => navigate("/settings")}
|
||||
>
|
||||
Back to settings
|
||||
</button>
|
||||
<button onClick={openCreate}>
|
||||
<Icon name="add" size="sm" />
|
||||
Create Tool Type
|
||||
@@ -189,18 +200,25 @@ export const ToolTypesPage = () => {
|
||||
<h3>{toolType.display_name}</h3>
|
||||
{toolType.is_builtin && <span className="badge">Built-in</span>}
|
||||
</div>
|
||||
<p className="text-secondary">{toolType.description || "No description"}</p>
|
||||
<p className="text-secondary">
|
||||
{toolType.description || "No description"}
|
||||
</p>
|
||||
<div className="tool-type-meta">
|
||||
<span>Port: {toolType.default_port || "N/A"}</span>
|
||||
{toolType.interfaces?.length > 0 && (
|
||||
<span>Interfaces: {toolType.interfaces.join(", ")}</span>
|
||||
)}
|
||||
{toolType.category && <span>Category: {toolType.category}</span>}
|
||||
{toolType.category && (
|
||||
<span>Category: {toolType.category}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="card-actions">
|
||||
{!toolType.is_builtin && (
|
||||
<>
|
||||
<button onClick={() => openEdit(toolType)} className="button-secondary">
|
||||
<button
|
||||
onClick={() => openEdit(toolType)}
|
||||
className="button-secondary"
|
||||
>
|
||||
<Icon name="edit" size="sm" />
|
||||
Edit
|
||||
</button>
|
||||
@@ -220,7 +238,10 @@ export const ToolTypesPage = () => {
|
||||
<div className="dialog">
|
||||
<p>Delete tool type "{toolType.display_name}"?</p>
|
||||
<div className="dialog-actions">
|
||||
<button onClick={() => handleDelete(toolType.id)} className="button-danger">
|
||||
<button
|
||||
onClick={() => handleDelete(toolType.id)}
|
||||
className="button-danger"
|
||||
>
|
||||
<Icon name="delete" size="sm" />
|
||||
Delete
|
||||
</button>
|
||||
@@ -240,7 +261,9 @@ export const ToolTypesPage = () => {
|
||||
{dialogMode !== "none" && (
|
||||
<div className="dialog-overlay">
|
||||
<div className="dialog">
|
||||
<h2>{dialogMode === "create" ? "Create Tool Type" : "Edit Tool Type"}</h2>
|
||||
<h2>
|
||||
{dialogMode === "create" ? "Create Tool Type" : "Edit Tool Type"}
|
||||
</h2>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label>Name (unique identifier)</label>
|
||||
@@ -294,7 +317,9 @@ export const ToolTypesPage = () => {
|
||||
if (e.target.checked) {
|
||||
setFormInterfaces([...formInterfaces, "web"]);
|
||||
} else {
|
||||
setFormInterfaces(formInterfaces.filter((i) => i !== "web"));
|
||||
setFormInterfaces(
|
||||
formInterfaces.filter((i) => i !== "web"),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -308,7 +333,9 @@ export const ToolTypesPage = () => {
|
||||
if (e.target.checked) {
|
||||
setFormInterfaces([...formInterfaces, "terminal"]);
|
||||
} else {
|
||||
setFormInterfaces(formInterfaces.filter((i) => i !== "terminal"));
|
||||
setFormInterfaces(
|
||||
formInterfaces.filter((i) => i !== "terminal"),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -364,7 +391,11 @@ export const ToolTypesPage = () => {
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button type="button" onClick={closeDialog} className="button-secondary">
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeDialog}
|
||||
className="button-secondary"
|
||||
>
|
||||
<Icon name="cancel" size="sm" />
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { createContext, useCallback, useContext, useState, type ReactNode } from "react";
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import type { Session } from "../types/session";
|
||||
|
||||
export type { Session } from "../types/session";
|
||||
@@ -8,7 +14,9 @@ interface SessionsContextType {
|
||||
setAllSessions: (sessions: Session[]) => void;
|
||||
}
|
||||
|
||||
const SessionsContext = createContext<SessionsContextType | undefined>(undefined);
|
||||
const SessionsContext = createContext<SessionsContextType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export const SessionsProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [sessions, setSessions] = useState<Session[]>([]);
|
||||
|
||||
@@ -5,7 +5,10 @@ export interface ConfigFolder {
|
||||
description: string | null;
|
||||
mount_path: string;
|
||||
files: Record<string, string>;
|
||||
project_overrides: Record<string, { mount_path?: string; files?: Record<string, string> }> | null;
|
||||
project_overrides: Record<
|
||||
string,
|
||||
{ mount_path?: string; files?: Record<string, string> }
|
||||
> | null;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
export type { ApiResponse, PaginatedResponse } from "./api-response";
|
||||
export type { ConfigFolder, CreateConfigFolderRequest, UpdateConfigFolderRequest, ProjectOverrideRequest } from "./config-folder";
|
||||
export type {
|
||||
ConfigFolder,
|
||||
CreateConfigFolderRequest,
|
||||
UpdateConfigFolderRequest,
|
||||
ProjectOverrideRequest,
|
||||
} from "./config-folder";
|
||||
export type {
|
||||
CommitDetail,
|
||||
CommitHistoryEntry,
|
||||
|
||||
Reference in New Issue
Block a user