First swing.
This commit is contained in:
54
src/services/api/apps.ts
Normal file
54
src/services/api/apps.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { apiClient } from './client';
|
||||
import type {
|
||||
AppListResponse,
|
||||
App,
|
||||
AppAddRequest,
|
||||
AppAddResponse,
|
||||
AppStatus,
|
||||
OperationResponse,
|
||||
} from './types';
|
||||
|
||||
export const appsApi = {
|
||||
// Available apps (from catalog)
|
||||
async listAvailable(): Promise<AppListResponse> {
|
||||
return apiClient.get('/api/v1/apps');
|
||||
},
|
||||
|
||||
async getAvailable(appName: string): Promise<App> {
|
||||
return apiClient.get(`/api/v1/apps/${appName}`);
|
||||
},
|
||||
|
||||
// Deployed apps (instance-specific)
|
||||
async listDeployed(instanceName: string): Promise<AppListResponse> {
|
||||
return apiClient.get(`/api/v1/instances/${instanceName}/apps`);
|
||||
},
|
||||
|
||||
async add(instanceName: string, app: AppAddRequest): Promise<AppAddResponse> {
|
||||
return apiClient.post(`/api/v1/instances/${instanceName}/apps`, app);
|
||||
},
|
||||
|
||||
async deploy(instanceName: string, appName: string): Promise<OperationResponse> {
|
||||
return apiClient.post(`/api/v1/instances/${instanceName}/apps/${appName}/deploy`);
|
||||
},
|
||||
|
||||
async delete(instanceName: string, appName: string): Promise<OperationResponse> {
|
||||
return apiClient.delete(`/api/v1/instances/${instanceName}/apps/${appName}`);
|
||||
},
|
||||
|
||||
async getStatus(instanceName: string, appName: string): Promise<AppStatus> {
|
||||
return apiClient.get(`/api/v1/instances/${instanceName}/apps/${appName}/status`);
|
||||
},
|
||||
|
||||
// Backup operations
|
||||
async backup(instanceName: string, appName: string): Promise<OperationResponse> {
|
||||
return apiClient.post(`/api/v1/instances/${instanceName}/apps/${appName}/backup`);
|
||||
},
|
||||
|
||||
async listBackups(instanceName: string, appName: string): Promise<{ backups: Array<{ id: string; timestamp: string; size?: string }> }> {
|
||||
return apiClient.get(`/api/v1/instances/${instanceName}/apps/${appName}/backup`);
|
||||
},
|
||||
|
||||
async restore(instanceName: string, appName: string, backupId: string): Promise<OperationResponse> {
|
||||
return apiClient.post(`/api/v1/instances/${instanceName}/apps/${appName}/restore`, { backup_id: backupId });
|
||||
},
|
||||
};
|
||||
122
src/services/api/client.ts
Normal file
122
src/services/api/client.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public statusCode: number,
|
||||
public details?: unknown
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ApiClient {
|
||||
constructor(private baseUrl: string = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5055') {}
|
||||
|
||||
private async request<T>(
|
||||
endpoint: string,
|
||||
options?: RequestInit
|
||||
): Promise<T> {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new ApiError(
|
||||
errorData.error || `HTTP ${response.status}: ${response.statusText}`,
|
||||
response.status,
|
||||
errorData
|
||||
);
|
||||
}
|
||||
|
||||
// Handle 204 No Content
|
||||
if (response.status === 204) {
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
throw error;
|
||||
}
|
||||
throw new ApiError(
|
||||
error instanceof Error ? error.message : 'Network error',
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async get<T>(endpoint: string): Promise<T> {
|
||||
return this.request<T>(endpoint, { method: 'GET' });
|
||||
}
|
||||
|
||||
async post<T>(endpoint: string, data?: unknown): Promise<T> {
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'POST',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async put<T>(endpoint: string, data?: unknown): Promise<T> {
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'PUT',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async patch<T>(endpoint: string, data?: unknown): Promise<T> {
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'PATCH',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async delete<T>(endpoint: string): Promise<T> {
|
||||
return this.request<T>(endpoint, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
async getText(endpoint: string): Promise<string> {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new ApiError(
|
||||
errorData.error || `HTTP ${response.status}: ${response.statusText}`,
|
||||
response.status,
|
||||
errorData
|
||||
);
|
||||
}
|
||||
|
||||
return response.text();
|
||||
}
|
||||
|
||||
async putText(endpoint: string, text: string): Promise<{ message?: string; [key: string]: unknown }> {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
body: text,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new ApiError(
|
||||
errorData.error || `HTTP ${response.status}: ${response.statusText}`,
|
||||
response.status,
|
||||
errorData
|
||||
);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
|
||||
export const apiClient = new ApiClient();
|
||||
47
src/services/api/cluster.ts
Normal file
47
src/services/api/cluster.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { apiClient } from './client';
|
||||
import type {
|
||||
ClusterConfig,
|
||||
ClusterStatus,
|
||||
ClusterHealthResponse,
|
||||
KubeconfigResponse,
|
||||
TalosconfigResponse,
|
||||
OperationResponse,
|
||||
} from './types';
|
||||
|
||||
export const clusterApi = {
|
||||
async generateConfig(instanceName: string, config: ClusterConfig): Promise<OperationResponse> {
|
||||
return apiClient.post(`/api/v1/instances/${instanceName}/cluster/config/generate`, config);
|
||||
},
|
||||
|
||||
async bootstrap(instanceName: string, node: string): Promise<OperationResponse> {
|
||||
return apiClient.post<OperationResponse>(`/api/v1/instances/${instanceName}/cluster/bootstrap`, { node });
|
||||
},
|
||||
|
||||
async configureEndpoints(instanceName: string, includeNodes = false): Promise<OperationResponse> {
|
||||
return apiClient.post(`/api/v1/instances/${instanceName}/cluster/endpoints`, { include_nodes: includeNodes });
|
||||
},
|
||||
|
||||
async getStatus(instanceName: string): Promise<ClusterStatus> {
|
||||
return apiClient.get(`/api/v1/instances/${instanceName}/cluster/status`);
|
||||
},
|
||||
|
||||
async getHealth(instanceName: string): Promise<ClusterHealthResponse> {
|
||||
return apiClient.get(`/api/v1/instances/${instanceName}/cluster/health`);
|
||||
},
|
||||
|
||||
async getKubeconfig(instanceName: string): Promise<KubeconfigResponse> {
|
||||
return apiClient.get(`/api/v1/instances/${instanceName}/cluster/kubeconfig`);
|
||||
},
|
||||
|
||||
async generateKubeconfig(instanceName: string): Promise<OperationResponse> {
|
||||
return apiClient.post(`/api/v1/instances/${instanceName}/cluster/kubeconfig/generate`);
|
||||
},
|
||||
|
||||
async getTalosconfig(instanceName: string): Promise<TalosconfigResponse> {
|
||||
return apiClient.get(`/api/v1/instances/${instanceName}/cluster/talosconfig`);
|
||||
},
|
||||
|
||||
async reset(instanceName: string, confirm: boolean): Promise<OperationResponse> {
|
||||
return apiClient.post<OperationResponse>(`/api/v1/instances/${instanceName}/cluster/reset`, { confirm });
|
||||
},
|
||||
};
|
||||
12
src/services/api/context.ts
Normal file
12
src/services/api/context.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { apiClient } from './client';
|
||||
import type { ContextResponse, SetContextResponse } from './types';
|
||||
|
||||
export const contextApi = {
|
||||
async get(): Promise<ContextResponse> {
|
||||
return apiClient.get('/api/v1/context');
|
||||
},
|
||||
|
||||
async set(context: string): Promise<SetContextResponse> {
|
||||
return apiClient.post<SetContextResponse>('/api/v1/context', { context });
|
||||
},
|
||||
};
|
||||
28
src/services/api/dnsmasq.ts
Normal file
28
src/services/api/dnsmasq.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { apiClient } from './client';
|
||||
|
||||
export interface DnsmasqStatus {
|
||||
running: boolean;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export const dnsmasqApi = {
|
||||
async getStatus(): Promise<DnsmasqStatus> {
|
||||
return apiClient.get('/api/v1/dnsmasq/status');
|
||||
},
|
||||
|
||||
async getConfig(): Promise<string> {
|
||||
return apiClient.getText('/api/v1/dnsmasq/config');
|
||||
},
|
||||
|
||||
async restart(): Promise<{ message: string }> {
|
||||
return apiClient.post('/api/v1/dnsmasq/restart');
|
||||
},
|
||||
|
||||
async generate(): Promise<{ message: string }> {
|
||||
return apiClient.post('/api/v1/dnsmasq/generate');
|
||||
},
|
||||
|
||||
async update(): Promise<{ message: string }> {
|
||||
return apiClient.post('/api/v1/dnsmasq/update');
|
||||
},
|
||||
};
|
||||
33
src/services/api/hooks/useCluster.ts
Normal file
33
src/services/api/hooks/useCluster.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { clusterApi, nodesApi } from '..';
|
||||
import type { ClusterHealthResponse, ClusterStatus, NodeListResponse } from '../types';
|
||||
|
||||
export const useClusterHealth = (instanceName: string) => {
|
||||
return useQuery<ClusterHealthResponse>({
|
||||
queryKey: ['cluster-health', instanceName],
|
||||
queryFn: () => clusterApi.getHealth(instanceName),
|
||||
enabled: !!instanceName,
|
||||
refetchInterval: 10000, // Auto-refresh every 10 seconds
|
||||
staleTime: 5000,
|
||||
});
|
||||
};
|
||||
|
||||
export const useClusterStatus = (instanceName: string) => {
|
||||
return useQuery<ClusterStatus>({
|
||||
queryKey: ['cluster-status', instanceName],
|
||||
queryFn: () => clusterApi.getStatus(instanceName),
|
||||
enabled: !!instanceName,
|
||||
refetchInterval: 10000, // Auto-refresh every 10 seconds
|
||||
staleTime: 5000,
|
||||
});
|
||||
};
|
||||
|
||||
export const useClusterNodes = (instanceName: string) => {
|
||||
return useQuery<NodeListResponse>({
|
||||
queryKey: ['cluster-nodes', instanceName],
|
||||
queryFn: () => nodesApi.list(instanceName),
|
||||
enabled: !!instanceName,
|
||||
refetchInterval: 10000, // Auto-refresh every 10 seconds
|
||||
staleTime: 5000,
|
||||
});
|
||||
};
|
||||
40
src/services/api/hooks/useInstance.ts
Normal file
40
src/services/api/hooks/useInstance.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { instancesApi, operationsApi, clusterApi } from '..';
|
||||
import type { GetInstanceResponse, OperationListResponse, ClusterHealthResponse } from '../types';
|
||||
|
||||
export const useInstance = (name: string) => {
|
||||
return useQuery<GetInstanceResponse>({
|
||||
queryKey: ['instance', name],
|
||||
queryFn: () => instancesApi.get(name),
|
||||
enabled: !!name,
|
||||
staleTime: 30000, // 30 seconds
|
||||
});
|
||||
};
|
||||
|
||||
export const useInstanceOperations = (instanceName: string, limit?: number) => {
|
||||
return useQuery<OperationListResponse>({
|
||||
queryKey: ['instance-operations', instanceName],
|
||||
queryFn: async () => {
|
||||
const response = await operationsApi.list(instanceName);
|
||||
if (limit) {
|
||||
return {
|
||||
operations: response.operations.slice(0, limit)
|
||||
};
|
||||
}
|
||||
return response;
|
||||
},
|
||||
enabled: !!instanceName,
|
||||
refetchInterval: 3000, // Poll every 3 seconds
|
||||
staleTime: 1000,
|
||||
});
|
||||
};
|
||||
|
||||
export const useInstanceClusterHealth = (instanceName: string) => {
|
||||
return useQuery<ClusterHealthResponse>({
|
||||
queryKey: ['instance-cluster-health', instanceName],
|
||||
queryFn: () => clusterApi.getHealth(instanceName),
|
||||
enabled: !!instanceName,
|
||||
refetchInterval: 10000, // Refresh every 10 seconds
|
||||
staleTime: 5000,
|
||||
});
|
||||
};
|
||||
58
src/services/api/hooks/useOperations.ts
Normal file
58
src/services/api/hooks/useOperations.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { operationsApi } from '../operations';
|
||||
import type { OperationListResponse, Operation } from '../types';
|
||||
|
||||
export const useOperations = (instanceName: string, filter?: 'running' | 'completed' | 'failed') => {
|
||||
return useQuery<OperationListResponse>({
|
||||
queryKey: ['operations', instanceName, filter],
|
||||
queryFn: async () => {
|
||||
const response = await operationsApi.list(instanceName);
|
||||
|
||||
if (filter) {
|
||||
const filtered = response.operations.filter(op => {
|
||||
if (filter === 'running') return op.status === 'running' || op.status === 'pending';
|
||||
if (filter === 'completed') return op.status === 'completed';
|
||||
if (filter === 'failed') return op.status === 'failed';
|
||||
return true;
|
||||
});
|
||||
return { operations: filtered };
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
enabled: !!instanceName,
|
||||
refetchInterval: 3000, // Poll every 3 seconds for real-time updates
|
||||
staleTime: 1000,
|
||||
});
|
||||
};
|
||||
|
||||
export const useOperation = (operationId: string) => {
|
||||
return useQuery<Operation>({
|
||||
queryKey: ['operation', operationId],
|
||||
queryFn: () => operationsApi.get(operationId),
|
||||
enabled: !!operationId,
|
||||
refetchInterval: (query) => {
|
||||
// Stop polling if operation is completed, failed, or cancelled
|
||||
const status = query.state.data?.status;
|
||||
if (status === 'completed' || status === 'failed' || status === 'cancelled') {
|
||||
return false;
|
||||
}
|
||||
return 2000; // Poll every 2 seconds while running
|
||||
},
|
||||
staleTime: 1000,
|
||||
});
|
||||
};
|
||||
|
||||
export const useCancelOperation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ operationId, instanceName }: { operationId: string; instanceName: string }) =>
|
||||
operationsApi.cancel(operationId, instanceName),
|
||||
onSuccess: (_, { operationId }) => {
|
||||
// Invalidate operation queries to refresh data
|
||||
queryClient.invalidateQueries({ queryKey: ['operation', operationId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['operations'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
57
src/services/api/hooks/usePxeAssets.ts
Normal file
57
src/services/api/hooks/usePxeAssets.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { pxeApi } from '../pxe';
|
||||
import type { DownloadAssetRequest, PxeAssetType } from '../types';
|
||||
|
||||
export function usePxeAssets(instanceName: string | null | undefined) {
|
||||
return useQuery({
|
||||
queryKey: ['instances', instanceName, 'pxe', 'assets'],
|
||||
queryFn: () => pxeApi.listAssets(instanceName!),
|
||||
enabled: !!instanceName,
|
||||
refetchInterval: 5000, // Poll every 5 seconds to track download status
|
||||
});
|
||||
}
|
||||
|
||||
export function usePxeAsset(
|
||||
instanceName: string | null | undefined,
|
||||
assetType: PxeAssetType | null | undefined
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: ['instances', instanceName, 'pxe', 'assets', assetType],
|
||||
queryFn: () => pxeApi.getAsset(instanceName!, assetType!),
|
||||
enabled: !!instanceName && !!assetType,
|
||||
});
|
||||
}
|
||||
|
||||
export function useDownloadPxeAsset() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
instanceName,
|
||||
request,
|
||||
}: {
|
||||
instanceName: string;
|
||||
request: DownloadAssetRequest;
|
||||
}) => pxeApi.downloadAsset(instanceName, request),
|
||||
onSuccess: (_data, variables) => {
|
||||
// Invalidate assets list to show downloading status
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['instances', variables.instanceName, 'pxe', 'assets'],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeletePxeAsset() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ instanceName, type }: { instanceName: string; type: PxeAssetType }) =>
|
||||
pxeApi.deleteAsset(instanceName, type),
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['instances', variables.instanceName, 'pxe', 'assets'],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
47
src/services/api/hooks/useUtilities.ts
Normal file
47
src/services/api/hooks/useUtilities.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { utilitiesApi } from '../utilities';
|
||||
|
||||
export function useDashboardToken() {
|
||||
return useQuery({
|
||||
queryKey: ['utilities', 'dashboard', 'token'],
|
||||
queryFn: utilitiesApi.getDashboardToken,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
export function useClusterVersions() {
|
||||
return useQuery({
|
||||
queryKey: ['utilities', 'version'],
|
||||
queryFn: utilitiesApi.getVersion,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
});
|
||||
}
|
||||
|
||||
export function useNodeIPs() {
|
||||
return useQuery({
|
||||
queryKey: ['utilities', 'nodes', 'ips'],
|
||||
queryFn: utilitiesApi.getNodeIPs,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
});
|
||||
}
|
||||
|
||||
export function useControlPlaneIP() {
|
||||
return useQuery({
|
||||
queryKey: ['utilities', 'controlplane', 'ip'],
|
||||
queryFn: utilitiesApi.getControlPlaneIP,
|
||||
staleTime: 60 * 1000, // 1 minute
|
||||
});
|
||||
}
|
||||
|
||||
export function useCopySecret() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ secret, targetInstance }: { secret: string; targetInstance: string }) =>
|
||||
utilitiesApi.copySecret(secret, targetInstance),
|
||||
onSuccess: () => {
|
||||
// Invalidate secrets queries
|
||||
queryClient.invalidateQueries({ queryKey: ['secrets'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
19
src/services/api/index.ts
Normal file
19
src/services/api/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export { apiClient, ApiError } from './client';
|
||||
export * from './types';
|
||||
export { instancesApi } from './instances';
|
||||
export { contextApi } from './context';
|
||||
export { nodesApi } from './nodes';
|
||||
export { clusterApi } from './cluster';
|
||||
export { appsApi } from './apps';
|
||||
export { servicesApi } from './services';
|
||||
export { operationsApi } from './operations';
|
||||
export { dnsmasqApi } from './dnsmasq';
|
||||
export { utilitiesApi } from './utilities';
|
||||
export { pxeApi } from './pxe';
|
||||
|
||||
// React Query hooks
|
||||
export { useInstance, useInstanceOperations, useInstanceClusterHealth } from './hooks/useInstance';
|
||||
export { useOperations, useOperation, useCancelOperation } from './hooks/useOperations';
|
||||
export { useClusterHealth, useClusterStatus, useClusterNodes } from './hooks/useCluster';
|
||||
export { useDashboardToken, useClusterVersions, useNodeIPs, useControlPlaneIP, useCopySecret } from './hooks/useUtilities';
|
||||
export { usePxeAssets, useDownloadPxeAsset, useDeletePxeAsset } from './hooks/usePxeAssets';
|
||||
49
src/services/api/instances.ts
Normal file
49
src/services/api/instances.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { apiClient } from './client';
|
||||
import type {
|
||||
InstanceListResponse,
|
||||
CreateInstanceRequest,
|
||||
CreateInstanceResponse,
|
||||
DeleteInstanceResponse,
|
||||
GetInstanceResponse,
|
||||
} from './types';
|
||||
|
||||
export const instancesApi = {
|
||||
async list(): Promise<InstanceListResponse> {
|
||||
return apiClient.get('/api/v1/instances');
|
||||
},
|
||||
|
||||
async get(name: string): Promise<GetInstanceResponse> {
|
||||
return apiClient.get(`/api/v1/instances/${name}`);
|
||||
},
|
||||
|
||||
async create(data: CreateInstanceRequest): Promise<CreateInstanceResponse> {
|
||||
return apiClient.post('/api/v1/instances', data);
|
||||
},
|
||||
|
||||
async delete(name: string): Promise<DeleteInstanceResponse> {
|
||||
return apiClient.delete(`/api/v1/instances/${name}`);
|
||||
},
|
||||
|
||||
// Config management
|
||||
async getConfig(instanceName: string): Promise<Record<string, unknown>> {
|
||||
return apiClient.get(`/api/v1/instances/${instanceName}/config`);
|
||||
},
|
||||
|
||||
async updateConfig(instanceName: string, config: Record<string, unknown>): Promise<{ message: string }> {
|
||||
return apiClient.put(`/api/v1/instances/${instanceName}/config`, config);
|
||||
},
|
||||
|
||||
async batchUpdateConfig(instanceName: string, updates: Array<{path: string; value: unknown}>): Promise<{ message: string; updated?: number }> {
|
||||
return apiClient.patch(`/api/v1/instances/${instanceName}/config`, { updates });
|
||||
},
|
||||
|
||||
// Secrets management
|
||||
async getSecrets(instanceName: string, raw = false): Promise<Record<string, unknown>> {
|
||||
const query = raw ? '?raw=true' : '';
|
||||
return apiClient.get(`/api/v1/instances/${instanceName}/secrets${query}`);
|
||||
},
|
||||
|
||||
async updateSecrets(instanceName: string, secrets: Record<string, unknown>): Promise<{ message: string }> {
|
||||
return apiClient.put(`/api/v1/instances/${instanceName}/secrets`, secrets);
|
||||
},
|
||||
};
|
||||
57
src/services/api/nodes.ts
Normal file
57
src/services/api/nodes.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { apiClient } from './client';
|
||||
import type {
|
||||
NodeListResponse,
|
||||
NodeAddRequest,
|
||||
NodeUpdateRequest,
|
||||
Node,
|
||||
HardwareInfo,
|
||||
DiscoveryStatus,
|
||||
OperationResponse,
|
||||
} from './types';
|
||||
|
||||
export const nodesApi = {
|
||||
async list(instanceName: string): Promise<NodeListResponse> {
|
||||
return apiClient.get(`/api/v1/instances/${instanceName}/nodes`);
|
||||
},
|
||||
|
||||
async get(instanceName: string, nodeName: string): Promise<Node> {
|
||||
return apiClient.get(`/api/v1/instances/${instanceName}/nodes/${nodeName}`);
|
||||
},
|
||||
|
||||
async add(instanceName: string, node: NodeAddRequest): Promise<OperationResponse> {
|
||||
return apiClient.post(`/api/v1/instances/${instanceName}/nodes`, node);
|
||||
},
|
||||
|
||||
async update(instanceName: string, nodeName: string, updates: NodeUpdateRequest): Promise<OperationResponse> {
|
||||
return apiClient.put(`/api/v1/instances/${instanceName}/nodes/${nodeName}`, updates);
|
||||
},
|
||||
|
||||
async delete(instanceName: string, nodeName: string): Promise<OperationResponse> {
|
||||
return apiClient.delete(`/api/v1/instances/${instanceName}/nodes/${nodeName}`);
|
||||
},
|
||||
|
||||
async apply(instanceName: string, nodeName: string): Promise<OperationResponse> {
|
||||
return apiClient.post(`/api/v1/instances/${instanceName}/nodes/${nodeName}/apply`);
|
||||
},
|
||||
|
||||
// Discovery
|
||||
async discover(instanceName: string, subnet: string): Promise<OperationResponse> {
|
||||
return apiClient.post(`/api/v1/instances/${instanceName}/nodes/discover`, { subnet });
|
||||
},
|
||||
|
||||
async detect(instanceName: string): Promise<OperationResponse> {
|
||||
return apiClient.post(`/api/v1/instances/${instanceName}/nodes/detect`);
|
||||
},
|
||||
|
||||
async discoveryStatus(instanceName: string): Promise<DiscoveryStatus> {
|
||||
return apiClient.get(`/api/v1/instances/${instanceName}/discovery`);
|
||||
},
|
||||
|
||||
async getHardware(instanceName: string, ip: string): Promise<HardwareInfo> {
|
||||
return apiClient.get(`/api/v1/instances/${instanceName}/nodes/hardware/${ip}`);
|
||||
},
|
||||
|
||||
async fetchTemplates(instanceName: string): Promise<OperationResponse> {
|
||||
return apiClient.post(`/api/v1/instances/${instanceName}/nodes/fetch-templates`);
|
||||
},
|
||||
};
|
||||
23
src/services/api/operations.ts
Normal file
23
src/services/api/operations.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { apiClient } from './client';
|
||||
import type { Operation, OperationListResponse } from './types';
|
||||
|
||||
export const operationsApi = {
|
||||
async list(instanceName: string): Promise<OperationListResponse> {
|
||||
return apiClient.get(`/api/v1/instances/${instanceName}/operations`);
|
||||
},
|
||||
|
||||
async get(operationId: string, instanceName?: string): Promise<Operation> {
|
||||
const params = instanceName ? `?instance=${instanceName}` : '';
|
||||
return apiClient.get(`/api/v1/operations/${operationId}${params}`);
|
||||
},
|
||||
|
||||
async cancel(operationId: string, instanceName: string): Promise<{ message: string }> {
|
||||
return apiClient.post(`/api/v1/operations/${operationId}/cancel?instance=${instanceName}`);
|
||||
},
|
||||
|
||||
// SSE stream for operation updates
|
||||
createStream(operationId: string): EventSource {
|
||||
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5055';
|
||||
return new EventSource(`${baseUrl}/api/v1/operations/${operationId}/stream`);
|
||||
},
|
||||
};
|
||||
29
src/services/api/pxe.ts
Normal file
29
src/services/api/pxe.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { apiClient } from './client';
|
||||
import type {
|
||||
PxeAssetsResponse,
|
||||
PxeAsset,
|
||||
DownloadAssetRequest,
|
||||
OperationResponse,
|
||||
PxeAssetType,
|
||||
} from './types';
|
||||
|
||||
export const pxeApi = {
|
||||
async listAssets(instanceName: string): Promise<PxeAssetsResponse> {
|
||||
return apiClient.get(`/api/v1/instances/${instanceName}/pxe/assets`);
|
||||
},
|
||||
|
||||
async getAsset(instanceName: string, type: PxeAssetType): Promise<PxeAsset> {
|
||||
return apiClient.get(`/api/v1/instances/${instanceName}/pxe/assets/${type}`);
|
||||
},
|
||||
|
||||
async downloadAsset(
|
||||
instanceName: string,
|
||||
request: DownloadAssetRequest
|
||||
): Promise<OperationResponse> {
|
||||
return apiClient.post(`/api/v1/instances/${instanceName}/pxe/assets/download`, request);
|
||||
},
|
||||
|
||||
async deleteAsset(instanceName: string, type: PxeAssetType): Promise<{ message: string }> {
|
||||
return apiClient.delete(`/api/v1/instances/${instanceName}/pxe/assets/${type}`);
|
||||
},
|
||||
};
|
||||
62
src/services/api/services.ts
Normal file
62
src/services/api/services.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { apiClient } from './client';
|
||||
import type {
|
||||
ServiceListResponse,
|
||||
Service,
|
||||
ServiceStatus,
|
||||
ServiceManifest,
|
||||
ServiceInstallRequest,
|
||||
OperationResponse,
|
||||
} from './types';
|
||||
|
||||
export const servicesApi = {
|
||||
// Instance services
|
||||
async list(instanceName: string): Promise<ServiceListResponse> {
|
||||
return apiClient.get(`/api/v1/instances/${instanceName}/services`);
|
||||
},
|
||||
|
||||
async get(instanceName: string, serviceName: string): Promise<Service> {
|
||||
return apiClient.get(`/api/v1/instances/${instanceName}/services/${serviceName}`);
|
||||
},
|
||||
|
||||
async install(instanceName: string, service: ServiceInstallRequest): Promise<OperationResponse> {
|
||||
return apiClient.post(`/api/v1/instances/${instanceName}/services`, service);
|
||||
},
|
||||
|
||||
async installAll(instanceName: string): Promise<OperationResponse> {
|
||||
return apiClient.post(`/api/v1/instances/${instanceName}/services/install-all`);
|
||||
},
|
||||
|
||||
async delete(instanceName: string, serviceName: string): Promise<OperationResponse> {
|
||||
return apiClient.delete(`/api/v1/instances/${instanceName}/services/${serviceName}`);
|
||||
},
|
||||
|
||||
async getStatus(instanceName: string, serviceName: string): Promise<ServiceStatus> {
|
||||
return apiClient.get(`/api/v1/instances/${instanceName}/services/${serviceName}/status`);
|
||||
},
|
||||
|
||||
async getConfig(instanceName: string, serviceName: string): Promise<Record<string, unknown>> {
|
||||
return apiClient.get(`/api/v1/instances/${instanceName}/services/${serviceName}/config`);
|
||||
},
|
||||
|
||||
// Service lifecycle
|
||||
async fetch(instanceName: string, serviceName: string): Promise<OperationResponse> {
|
||||
return apiClient.post(`/api/v1/instances/${instanceName}/services/${serviceName}/fetch`);
|
||||
},
|
||||
|
||||
async compile(instanceName: string, serviceName: string): Promise<OperationResponse> {
|
||||
return apiClient.post(`/api/v1/instances/${instanceName}/services/${serviceName}/compile`);
|
||||
},
|
||||
|
||||
async deploy(instanceName: string, serviceName: string): Promise<OperationResponse> {
|
||||
return apiClient.post(`/api/v1/instances/${instanceName}/services/${serviceName}/deploy`);
|
||||
},
|
||||
|
||||
// Global service info (not instance-specific)
|
||||
async getManifest(serviceName: string): Promise<ServiceManifest> {
|
||||
return apiClient.get(`/api/v1/services/${serviceName}/manifest`);
|
||||
},
|
||||
|
||||
async getGlobalConfig(serviceName: string): Promise<Record<string, unknown>> {
|
||||
return apiClient.get(`/api/v1/services/${serviceName}/config`);
|
||||
},
|
||||
};
|
||||
53
src/services/api/types/app.ts
Normal file
53
src/services/api/types/app.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
export interface App {
|
||||
name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
category?: string;
|
||||
icon?: string;
|
||||
requires?: AppRequirement[];
|
||||
defaultConfig?: Record<string, unknown>;
|
||||
requiredSecrets?: string[];
|
||||
dependencies?: string[];
|
||||
config?: Record<string, string>;
|
||||
status?: AppStatus;
|
||||
}
|
||||
|
||||
export interface AppRequirement {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface DeployedApp {
|
||||
name: string;
|
||||
status: 'added' | 'deployed';
|
||||
version?: string;
|
||||
namespace?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface AppStatus {
|
||||
status: 'available' | 'added' | 'deploying' | 'deployed' | 'running' | 'error' | 'stopped';
|
||||
message?: string;
|
||||
namespace?: string;
|
||||
replicas?: number;
|
||||
resources?: AppResources;
|
||||
}
|
||||
|
||||
export interface AppResources {
|
||||
cpu?: string;
|
||||
memory?: string;
|
||||
storage?: string;
|
||||
}
|
||||
|
||||
export interface AppListResponse {
|
||||
apps: App[];
|
||||
}
|
||||
|
||||
export interface AppAddRequest {
|
||||
name: string;
|
||||
config?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface AppAddResponse {
|
||||
message: string;
|
||||
app: string;
|
||||
}
|
||||
45
src/services/api/types/cluster.ts
Normal file
45
src/services/api/types/cluster.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export interface ClusterConfig {
|
||||
clusterName: string;
|
||||
vip: string;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export interface ClusterStatus {
|
||||
ready: boolean;
|
||||
nodes: number;
|
||||
controlPlaneNodes: number;
|
||||
workerNodes: number;
|
||||
kubernetesVersion?: string;
|
||||
talosVersion?: string;
|
||||
}
|
||||
|
||||
export interface HealthCheck {
|
||||
name: string;
|
||||
status: 'passing' | 'warning' | 'failing';
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ClusterHealthResponse {
|
||||
status: 'healthy' | 'degraded' | 'unhealthy';
|
||||
checks: HealthCheck[];
|
||||
}
|
||||
|
||||
export interface KubeconfigResponse {
|
||||
kubeconfig: string;
|
||||
}
|
||||
|
||||
export interface TalosconfigResponse {
|
||||
talosconfig: string;
|
||||
}
|
||||
|
||||
export interface ClusterBootstrapRequest {
|
||||
node: string;
|
||||
}
|
||||
|
||||
export interface ClusterEndpointsRequest {
|
||||
include_nodes?: boolean;
|
||||
}
|
||||
|
||||
export interface ClusterResetRequest {
|
||||
confirm: boolean;
|
||||
}
|
||||
17
src/services/api/types/config.ts
Normal file
17
src/services/api/types/config.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export interface ConfigUpdate {
|
||||
path: string;
|
||||
value: unknown;
|
||||
}
|
||||
|
||||
export interface ConfigUpdateBatchRequest {
|
||||
updates: ConfigUpdate[];
|
||||
}
|
||||
|
||||
export interface ConfigUpdateResponse {
|
||||
message: string;
|
||||
updated?: number;
|
||||
}
|
||||
|
||||
export interface SecretsResponse {
|
||||
[key: string]: string;
|
||||
}
|
||||
12
src/services/api/types/context.ts
Normal file
12
src/services/api/types/context.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export interface ContextResponse {
|
||||
context: string | null;
|
||||
}
|
||||
|
||||
export interface SetContextRequest {
|
||||
context: string;
|
||||
}
|
||||
|
||||
export interface SetContextResponse {
|
||||
context: string;
|
||||
message: string;
|
||||
}
|
||||
9
src/services/api/types/index.ts
Normal file
9
src/services/api/types/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export * from './instance';
|
||||
export * from './context';
|
||||
export * from './operation';
|
||||
export * from './config';
|
||||
export * from './node';
|
||||
export * from './cluster';
|
||||
export * from './app';
|
||||
export * from './service';
|
||||
export * from './pxe';
|
||||
27
src/services/api/types/instance.ts
Normal file
27
src/services/api/types/instance.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export interface Instance {
|
||||
name: string;
|
||||
config: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface InstanceListResponse {
|
||||
instances: string[];
|
||||
}
|
||||
|
||||
export interface CreateInstanceRequest {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface CreateInstanceResponse {
|
||||
name: string;
|
||||
message: string;
|
||||
warning?: string;
|
||||
}
|
||||
|
||||
export interface DeleteInstanceResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface GetInstanceResponse {
|
||||
name: string;
|
||||
config: Record<string, unknown>;
|
||||
}
|
||||
58
src/services/api/types/node.ts
Normal file
58
src/services/api/types/node.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
export interface Node {
|
||||
hostname: string;
|
||||
target_ip: string;
|
||||
role: 'controlplane' | 'worker';
|
||||
current_ip?: string;
|
||||
interface?: string;
|
||||
disk?: string;
|
||||
version?: string;
|
||||
schematic_id?: string;
|
||||
// Backend state flags for deriving status
|
||||
maintenance?: boolean;
|
||||
configured?: boolean;
|
||||
applied?: boolean;
|
||||
// Optional fields (not yet returned by API)
|
||||
hardware?: HardwareInfo;
|
||||
talosVersion?: string;
|
||||
kubernetesVersion?: string;
|
||||
}
|
||||
|
||||
export interface HardwareInfo {
|
||||
cpu?: string;
|
||||
memory?: string;
|
||||
disk?: string;
|
||||
manufacturer?: string;
|
||||
model?: string;
|
||||
}
|
||||
|
||||
export interface DiscoveredNode {
|
||||
ip: string;
|
||||
hostname?: string;
|
||||
maintenance_mode?: boolean;
|
||||
version?: string;
|
||||
interface?: string;
|
||||
disks?: string[];
|
||||
}
|
||||
|
||||
export interface DiscoveryStatus {
|
||||
active: boolean;
|
||||
started_at?: string;
|
||||
nodes_found?: DiscoveredNode[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface NodeListResponse {
|
||||
nodes: Node[];
|
||||
}
|
||||
|
||||
export interface NodeAddRequest {
|
||||
hostname: string;
|
||||
target_ip: string;
|
||||
role: 'controlplane' | 'worker';
|
||||
disk?: string;
|
||||
}
|
||||
|
||||
export interface NodeUpdateRequest {
|
||||
role?: 'controlplane' | 'worker';
|
||||
config?: Record<string, unknown>;
|
||||
}
|
||||
21
src/services/api/types/operation.ts
Normal file
21
src/services/api/types/operation.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export interface Operation {
|
||||
id: string;
|
||||
instance_name: string;
|
||||
type: string;
|
||||
target: string;
|
||||
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
|
||||
message: string;
|
||||
progress: number;
|
||||
started: string;
|
||||
completed?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface OperationListResponse {
|
||||
operations: Operation[];
|
||||
}
|
||||
|
||||
export interface OperationResponse {
|
||||
operation_id: string;
|
||||
message: string;
|
||||
}
|
||||
27
src/services/api/types/pxe.ts
Normal file
27
src/services/api/types/pxe.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export type PxeAssetType = 'kernel' | 'initramfs' | 'iso';
|
||||
|
||||
export type PxeAssetStatus = 'available' | 'missing' | 'downloading' | 'error';
|
||||
|
||||
export interface PxeAsset {
|
||||
type: PxeAssetType;
|
||||
status: PxeAssetStatus;
|
||||
version?: string;
|
||||
size?: string;
|
||||
path?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface PxeAssetsResponse {
|
||||
assets: PxeAsset[];
|
||||
}
|
||||
|
||||
export interface DownloadAssetRequest {
|
||||
type: PxeAssetType;
|
||||
version?: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface OperationResponse {
|
||||
operation_id: string;
|
||||
message: string;
|
||||
}
|
||||
29
src/services/api/types/service.ts
Normal file
29
src/services/api/types/service.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export interface Service {
|
||||
name: string;
|
||||
description: string;
|
||||
version?: string;
|
||||
status?: ServiceStatus;
|
||||
deployed?: boolean;
|
||||
}
|
||||
|
||||
export interface ServiceStatus {
|
||||
status: 'available' | 'deploying' | 'running' | 'error' | 'stopped';
|
||||
message?: string;
|
||||
namespace?: string;
|
||||
ready?: boolean;
|
||||
}
|
||||
|
||||
export interface ServiceListResponse {
|
||||
services: Service[];
|
||||
}
|
||||
|
||||
export interface ServiceManifest {
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
config: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ServiceInstallRequest {
|
||||
name: string;
|
||||
}
|
||||
41
src/services/api/utilities.ts
Normal file
41
src/services/api/utilities.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { apiClient } from './client';
|
||||
|
||||
export interface HealthResponse {
|
||||
status: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface VersionResponse {
|
||||
version: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export const utilitiesApi = {
|
||||
async health(): Promise<HealthResponse> {
|
||||
return apiClient.get('/api/v1/utilities/health');
|
||||
},
|
||||
|
||||
async instanceHealth(instanceName: string): Promise<HealthResponse> {
|
||||
return apiClient.get(`/api/v1/instances/${instanceName}/utilities/health`);
|
||||
},
|
||||
|
||||
async getDashboardToken(): Promise<{ token: string }> {
|
||||
return apiClient.get('/api/v1/utilities/dashboard/token');
|
||||
},
|
||||
|
||||
async getNodeIPs(): Promise<{ ips: string[] }> {
|
||||
return apiClient.get('/api/v1/utilities/nodes/ips');
|
||||
},
|
||||
|
||||
async getControlPlaneIP(): Promise<{ ip: string }> {
|
||||
return apiClient.get('/api/v1/utilities/controlplane/ip');
|
||||
},
|
||||
|
||||
async copySecret(secret: string, targetInstance: string): Promise<{ message: string }> {
|
||||
return apiClient.post(`/api/v1/utilities/secrets/${secret}/copy`, { target: targetInstance });
|
||||
},
|
||||
|
||||
async getVersion(): Promise<VersionResponse> {
|
||||
return apiClient.get('/api/v1/utilities/version');
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user