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