feat: add API client with auth middleware and React Query hooks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
46
ui/src/api/client.ts
Normal file
46
ui/src/api/client.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useAuthStore } from '../auth/auth-store';
|
||||
|
||||
const API_BASE = '/api';
|
||||
|
||||
async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
const token = useAuthStore.getState().accessToken;
|
||||
const headers: Record<string, string> = {
|
||||
...(options.headers as Record<string, string> || {}),
|
||||
};
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
if (!headers['Content-Type'] && !(options.body instanceof FormData)) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE}${path}`, { ...options, headers });
|
||||
|
||||
if (response.status === 401) {
|
||||
useAuthStore.getState().logout();
|
||||
window.location.href = '/login';
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`API error ${response.status}: ${text}`);
|
||||
}
|
||||
|
||||
if (response.status === 204) return undefined as T;
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export const api = {
|
||||
get: <T>(path: string) => apiFetch<T>(path),
|
||||
post: <T>(path: string, body?: unknown) =>
|
||||
apiFetch<T>(path, {
|
||||
method: 'POST',
|
||||
body: body instanceof FormData ? body : JSON.stringify(body),
|
||||
}),
|
||||
patch: <T>(path: string, body: unknown) =>
|
||||
apiFetch<T>(path, { method: 'PATCH', body: JSON.stringify(body) }),
|
||||
put: <T>(path: string, body: FormData) =>
|
||||
apiFetch<T>(path, { method: 'PUT', body }),
|
||||
delete: <T>(path: string) => apiFetch<T>(path, { method: 'DELETE' }),
|
||||
};
|
||||
185
ui/src/api/hooks.ts
Normal file
185
ui/src/api/hooks.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { api } from './client';
|
||||
import type {
|
||||
TenantResponse, EnvironmentResponse, AppResponse,
|
||||
DeploymentResponse, LicenseResponse, AgentStatusResponse,
|
||||
ObservabilityStatusResponse, LogEntry,
|
||||
} from '../types/api';
|
||||
|
||||
// Tenant
|
||||
export function useTenant(tenantId: string) {
|
||||
return useQuery({
|
||||
queryKey: ['tenant', tenantId],
|
||||
queryFn: () => api.get<TenantResponse>(`/tenants/${tenantId}`),
|
||||
enabled: !!tenantId,
|
||||
});
|
||||
}
|
||||
|
||||
// License
|
||||
export function useLicense(tenantId: string) {
|
||||
return useQuery({
|
||||
queryKey: ['license', tenantId],
|
||||
queryFn: () => api.get<LicenseResponse>(`/tenants/${tenantId}/license`),
|
||||
enabled: !!tenantId,
|
||||
});
|
||||
}
|
||||
|
||||
// Environments
|
||||
export function useEnvironments(tenantId: string) {
|
||||
return useQuery({
|
||||
queryKey: ['environments', tenantId],
|
||||
queryFn: () => api.get<EnvironmentResponse[]>(`/tenants/${tenantId}/environments`),
|
||||
enabled: !!tenantId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateEnvironment(tenantId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: { slug: string; displayName: string }) =>
|
||||
api.post<EnvironmentResponse>(`/tenants/${tenantId}/environments`, data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['environments', tenantId] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateEnvironment(tenantId: string, envId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: { displayName: string }) =>
|
||||
api.patch<EnvironmentResponse>(`/tenants/${tenantId}/environments/${envId}`, data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['environments', tenantId] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteEnvironment(tenantId: string, envId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: () => api.delete(`/tenants/${tenantId}/environments/${envId}`),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['environments', tenantId] }),
|
||||
});
|
||||
}
|
||||
|
||||
// Apps
|
||||
export function useApps(environmentId: string) {
|
||||
return useQuery({
|
||||
queryKey: ['apps', environmentId],
|
||||
queryFn: () => api.get<AppResponse[]>(`/environments/${environmentId}/apps`),
|
||||
enabled: !!environmentId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useApp(environmentId: string, appId: string) {
|
||||
return useQuery({
|
||||
queryKey: ['app', appId],
|
||||
queryFn: () => api.get<AppResponse>(`/environments/${environmentId}/apps/${appId}`),
|
||||
enabled: !!appId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateApp(environmentId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (formData: FormData) =>
|
||||
api.post<AppResponse>(`/environments/${environmentId}/apps`, formData),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['apps', environmentId] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteApp(environmentId: string, appId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: () => api.delete(`/environments/${environmentId}/apps/${appId}`),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['apps', environmentId] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateRouting(environmentId: string, appId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: { exposedPort: number | null }) =>
|
||||
api.patch<AppResponse>(`/environments/${environmentId}/apps/${appId}/routing`, data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['app', appId] }),
|
||||
});
|
||||
}
|
||||
|
||||
// Deployments
|
||||
export function useDeploy(appId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: () => api.post<DeploymentResponse>(`/apps/${appId}/deploy`),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['deployments', appId] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeployments(appId: string) {
|
||||
return useQuery({
|
||||
queryKey: ['deployments', appId],
|
||||
queryFn: () => api.get<DeploymentResponse[]>(`/apps/${appId}/deployments`),
|
||||
enabled: !!appId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeployment(appId: string, deploymentId: string) {
|
||||
return useQuery({
|
||||
queryKey: ['deployment', deploymentId],
|
||||
queryFn: () => api.get<DeploymentResponse>(`/apps/${appId}/deployments/${deploymentId}`),
|
||||
enabled: !!deploymentId,
|
||||
refetchInterval: (query) => {
|
||||
const status = query.state.data?.observedStatus;
|
||||
return status === 'BUILDING' || status === 'STARTING' ? 3000 : false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useStop(appId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: () => api.post<DeploymentResponse>(`/apps/${appId}/stop`),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['deployments', appId] });
|
||||
qc.invalidateQueries({ queryKey: ['app'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useRestart(appId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: () => api.post<DeploymentResponse>(`/apps/${appId}/restart`),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['deployments', appId] }),
|
||||
});
|
||||
}
|
||||
|
||||
// Observability
|
||||
export function useAgentStatus(appId: string) {
|
||||
return useQuery({
|
||||
queryKey: ['agent-status', appId],
|
||||
queryFn: () => api.get<AgentStatusResponse>(`/apps/${appId}/agent-status`),
|
||||
enabled: !!appId,
|
||||
refetchInterval: 15_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useObservabilityStatus(appId: string) {
|
||||
return useQuery({
|
||||
queryKey: ['observability-status', appId],
|
||||
queryFn: () => api.get<ObservabilityStatusResponse>(`/apps/${appId}/observability-status`),
|
||||
enabled: !!appId,
|
||||
refetchInterval: 30_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useLogs(appId: string, params?: { since?: string; limit?: number; stream?: string }) {
|
||||
return useQuery({
|
||||
queryKey: ['logs', appId, params],
|
||||
queryFn: () => {
|
||||
const qs = new URLSearchParams();
|
||||
if (params?.since) qs.set('since', params.since);
|
||||
if (params?.limit) qs.set('limit', String(params.limit));
|
||||
if (params?.stream) qs.set('stream', params.stream);
|
||||
const query = qs.toString();
|
||||
return api.get<LogEntry[]>(`/apps/${appId}/logs${query ? `?${query}` : ''}`);
|
||||
},
|
||||
enabled: !!appId,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user