diff --git a/apps/web/src/api/client.ts b/apps/web/src/api/client.ts index 04a6d7b..e6fe487 100644 --- a/apps/web/src/api/client.ts +++ b/apps/web/src/api/client.ts @@ -1,4 +1,4 @@ -import type { Config, ConfigCreate, Project, ProjectCreate, ProjectUpdate, Secret, SecretCreate, ToolDefinition, ToolInstance, User } from '../types/api.ts' +import type { Config, ConfigCreate, Project, ProjectCreate, ProjectUpdate, Repository, RepositoryConnection, RepositoryConnectionCreate, RepositoryCreate, Secret, SecretCreate, ToolDefinition, ToolInstance, User } from '../types/api.ts' const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000' @@ -195,4 +195,54 @@ export const api = { method: 'DELETE', }) }, + + getRepository: async (projectId: string, repoId: string): Promise => { + const response = await fetchWithAuth(`/projects/${projectId}/repositories/${repoId}`) + return response.json() + }, + + getRepositories: async (projectId: string): Promise => { + const response = await fetchWithAuth(`/projects/${projectId}/repositories`) + return response.json() + }, + + createRepository: async (projectId: string, data: RepositoryCreate): Promise => { + const response = await fetchWithAuth(`/projects/${projectId}/repositories`, { + method: 'POST', + body: JSON.stringify(data), + }) + return response.json() + }, + + deleteRepository: async (projectId: string, repoId: string): Promise => { + await fetchWithAuth(`/projects/${projectId}/repositories/${repoId}`, { + method: 'DELETE', + }) + }, + + getRepositoryConnections: async (projectId: string): Promise => { + const response = await fetchWithAuth(`/projects/${projectId}/repository-connections`) + return response.json() + }, + + createRepositoryConnection: async (projectId: string, data: RepositoryConnectionCreate): Promise => { + const response = await fetchWithAuth(`/projects/${projectId}/repository-connections`, { + method: 'POST', + body: JSON.stringify(data), + }) + return response.json() + }, + + deleteRepositoryConnection: async (projectId: string, connectionId: string): Promise => { + await fetchWithAuth(`/projects/${projectId}/repository-connections/${connectionId}`, { + method: 'DELETE', + }) + }, + + generateSshKey: async (projectId: string, connectionId: string): Promise<{ public_key: string }> => { + const response = await fetchWithAuth(`/projects/${projectId}/repository-connections/${connectionId}/ssh-key`, { + method: 'POST', + }) + return response.json() + }, } diff --git a/apps/web/src/pages/RepositoryDetailPage.tsx b/apps/web/src/pages/RepositoryDetailPage.tsx new file mode 100644 index 0000000..c8622d9 --- /dev/null +++ b/apps/web/src/pages/RepositoryDetailPage.tsx @@ -0,0 +1,193 @@ +import { useState } from 'react' +import { useNavigate, useParams } from 'react-router-dom' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { api } from '../api/client.ts' + +export default function RepositoryDetailPage() { + const { projectId, repoId } = useParams<{ projectId: string; repoId: string }>() + const navigate = useNavigate() + const queryClient = useQueryClient() + const [providerKind, setProviderKind] = useState('github') + const [credentialPayload, setCredentialPayload] = useState('') + const [showSshKey, setShowSshKey] = useState(false) + + const { data: repository } = useQuery({ + queryKey: ['repository', projectId, repoId], + queryFn: () => api.getRepository(projectId!, repoId!), + enabled: !!projectId && !!repoId, + }) + + const { data: connections } = useQuery({ + queryKey: ['repository-connections', projectId], + queryFn: () => api.getRepositoryConnections(projectId!), + enabled: !!projectId, + }) + + const createConnectionMutation = useMutation({ + mutationFn: (data: { + repository_id: string + provider_kind: string + credential_kind: string + credential_payload: string + }) => api.createRepositoryConnection(projectId!, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['repository-connections', projectId] }) + setCredentialPayload('') + }, + }) + + const deleteConnectionMutation = useMutation({ + mutationFn: (connectionId: string) => + api.deleteRepositoryConnection(projectId!, connectionId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['repository-connections', projectId] }) + }, + }) + + const generateSshKeyMutation = useMutation({ + mutationFn: (connectionId: string) => + api.generateSshKey(projectId!, connectionId), + onSuccess: () => { + setShowSshKey(true) + }, + }) + + const handleCreateConnection = (e: React.FormEvent) => { + e.preventDefault() + createConnectionMutation.mutate({ + repository_id: repoId!, + provider_kind: providerKind, + credential_kind: 'access_token', + credential_payload: credentialPayload, + }) + } + + const repoConnections = connections?.filter( + (conn) => conn.repository_id === repoId + ) + + return ( +
+
+

{repository?.name || 'Repository'}

+ +
+ + {repository && ( +
+
+
+ +

{repository.git_url}

+
+
+ +

{repository.provider_type}

+
+
+ +

{repository.default_branch}

+
+
+
+ )} + +
+

Create Connection

+
+
+ + +
+
+ + setCredentialPayload(e.target.value)} + placeholder="ghp_xxxxxxxxxxxx" + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" + required + /> +
+ +
+
+ +
+

Connections

+ {repoConnections?.length === 0 ? ( +

No connections yet.

+ ) : ( +
+ {repoConnections?.map((conn) => ( +
+
+

{conn.provider_kind}

+ + {conn.connection_status} + +
+
+ + +
+
+ ))} +
+ )} +
+ + {showSshKey && generateSshKeyMutation.data && ( +
+

SSH Public Key

+
+            {generateSshKeyMutation.data.public_key}
+          
+

+ Add this key to your repository's deploy keys. +

+
+ )} +
+ ) +} diff --git a/apps/web/src/pages/RepositoryListPage.tsx b/apps/web/src/pages/RepositoryListPage.tsx new file mode 100644 index 0000000..61b1d0d --- /dev/null +++ b/apps/web/src/pages/RepositoryListPage.tsx @@ -0,0 +1,129 @@ +import { useState } from 'react' +import { useNavigate, useParams } from 'react-router-dom' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { api } from '../api/client.ts' + +export default function RepositoryListPage() { + const { projectId } = useParams<{ projectId: string }>() + const navigate = useNavigate() + const queryClient = useQueryClient() + const [name, setName] = useState('') + const [gitUrl, setGitUrl] = useState('') + const [providerType, setProviderType] = useState('github') + + const { data: repositories, isLoading } = useQuery({ + queryKey: ['repositories', projectId], + queryFn: () => api.getRepositories(projectId!), + enabled: !!projectId, + }) + + const createMutation = useMutation({ + mutationFn: (data: { name: string; git_url: string; provider_type: string }) => + api.createRepository(projectId!, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['repositories', projectId] }) + setName('') + setGitUrl('') + }, + }) + + const deleteMutation = useMutation({ + mutationFn: (repoId: string) => api.deleteRepository(projectId!, repoId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['repositories', projectId] }) + }, + }) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + createMutation.mutate({ name, git_url: gitUrl, provider_type: providerType }) + } + + if (isLoading) return
Loading repositories...
+ + return ( +
+

Repositories

+ +
+

Add Repository

+
+ + setName(e.target.value)} + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" + required + /> +
+
+ + setGitUrl(e.target.value)} + placeholder="https://github.com/user/repo.git" + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" + required + /> +
+
+ + +
+ +
+ +
+
+

Repository List

+ {repositories?.length === 0 ? ( +

No repositories yet.

+ ) : ( +
+ {repositories?.map((repo) => ( +
navigate(`/projects/${projectId}/repositories/${repo.id}`)} + > +
+

{repo.name}

+

{repo.git_url}

+ + {repo.provider_type} + +
+ +
+ ))} +
+ )} +
+
+
+ ) +} diff --git a/apps/web/src/router.tsx b/apps/web/src/router.tsx index 1c7d235..5fcbe63 100644 --- a/apps/web/src/router.tsx +++ b/apps/web/src/router.tsx @@ -12,7 +12,8 @@ import ToolsPage from './pages/ToolsPage' import ToolSpawnPage from './pages/ToolSpawnPage' import ToolInstanceDetailPage from './pages/ToolInstanceDetailPage' import SettingsPage from './pages/SettingsPage' -import RepositoriesPage from './pages/RepositoriesPage' +import RepositoryListPage from './pages/RepositoryListPage' +import RepositoryDetailPage from './pages/RepositoryDetailPage' import ConfigListPage from './pages/ConfigListPage' import SecretListPage from './pages/SecretListPage' @@ -37,7 +38,8 @@ export const router = createBrowserRouter([ { path: '/tools/spawn', element: }, { path: '/projects/:projectId/instances/:instanceId', element: }, { path: '/settings', element: }, - { path: '/repositories', element: }, + { path: '/repositories', element: }, + { path: '/projects/:projectId/repositories/:repoId', element: }, { path: '/projects/:id/configs', element: }, { path: '/projects/:id/secrets', element: }, ], diff --git a/apps/web/src/types/api.ts b/apps/web/src/types/api.ts index d817403..3b37b14 100644 --- a/apps/web/src/types/api.ts +++ b/apps/web/src/types/api.ts @@ -85,3 +85,36 @@ export interface SecretCreate { scope_type: string scope_id: string } + +export interface Repository { + id: string + name: string + git_url: string + provider_type: string + default_branch: string + project_id: string +} + +export interface RepositoryCreate { + name: string + git_url: string + provider_type?: string + default_branch?: string +} + +export interface RepositoryConnection { + id: string + project_id: string + repository_id: string | null + provider_kind: string + credential_id: string | null + connection_status: string + default_branch: string | null +} + +export interface RepositoryConnectionCreate { + repository_id: string + provider_kind: string + credential_kind: string + credential_payload: string +}