Compare commits

...

3 Commits

Author SHA1 Message Date
Paul Payne
1d2f0b7891 Instance-namespace various endpoints and services. 2025-10-14 21:05:53 +00:00
Paul Payne
5260373fee Fix dashboard token button. 2025-10-14 18:54:23 +00:00
Paul Payne
684f29ba4f Lint fixes. 2025-10-14 07:32:13 +00:00
9 changed files with 96 additions and 69 deletions

View File

@@ -17,7 +17,7 @@ export function Advanced() {
const { instanceId } = useParams<{ instanceId: string }>();
const [copied, setCopied] = useState(false);
const { data: instance } = useInstance(instanceId || '');
const { data: dashboardToken, isLoading: tokenLoading } = useDashboardToken();
const { data: dashboardToken, isLoading: tokenLoading } = useDashboardToken(instanceId || '');
const [upstreamValue, setUpstreamValue] = useState("https://mywildcloud.org");
const [editingUpstream, setEditingUpstream] = useState(false);

View File

@@ -5,6 +5,7 @@ import { Loader2, CheckCircle, AlertCircle, XCircle, Clock } from 'lucide-react'
import { useOperation } from '../../hooks/useOperations';
interface OperationProgressProps {
instanceName: string;
operationId: string;
onComplete?: () => void;
onError?: (error: string) => void;
@@ -12,12 +13,13 @@ interface OperationProgressProps {
}
export function OperationProgress({
instanceName,
operationId,
onComplete,
onError,
showDetails = true
}: OperationProgressProps) {
const { operation, error, isLoading, cancel, isCancelling } = useOperation(operationId);
const { operation, error, isLoading, cancel, isCancelling } = useOperation(instanceName, operationId);
// Handle operation completion
if (operation?.status === 'completed' && onComplete) {

View File

@@ -12,19 +12,19 @@ export function useOperations(instanceName: string | null | undefined) {
});
}
export function useOperation(operationId: string | null | undefined) {
export function useOperation(instanceName: string | null | undefined, operationId: string | null | undefined) {
const [operation, setOperation] = useState<Operation | null>(null);
const [error, setError] = useState<Error | null>(null);
const queryClient = useQueryClient();
useEffect(() => {
if (!operationId) return;
if (!instanceName || !operationId) return;
// Fetch initial state
operationsApi.get(operationId).then(setOperation).catch(setError);
operationsApi.get(instanceName, operationId).then(setOperation).catch(setError);
// Set up SSE stream
const eventSource = operationsApi.createStream(operationId);
const eventSource = operationsApi.createStream(instanceName, operationId);
eventSource.onmessage = (event) => {
try {
@@ -54,14 +54,14 @@ export function useOperation(operationId: string | null | undefined) {
return () => {
eventSource.close();
};
}, [operationId, queryClient]);
}, [instanceName, operationId, queryClient]);
const cancelMutation = useMutation({
mutationFn: () => {
if (!operation?.instance_name) {
throw new Error('Cannot cancel operation: instance name not available');
if (!instanceName || !operationId) {
throw new Error('Cannot cancel operation: instance name or operation ID not available');
}
return operationsApi.cancel(operationId!, operation.instance_name);
return operationsApi.cancel(instanceName, operationId);
},
onSuccess: () => {
// Operation state will be updated via SSE

View File

@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useParams } from 'react-router';
import { UtilityCard, CopyableValue } from '../../components/UtilityCard';
import { Button } from '../../components/ui/button';
import {
@@ -18,18 +19,25 @@ import {
} from '../../services/api/hooks/useUtilities';
export function UtilitiesPage() {
const { instanceId } = useParams<{ instanceId: string }>();
const [secretToCopy, setSecretToCopy] = useState('');
const [targetInstance, setTargetInstance] = useState('');
const [sourceNamespace, setSourceNamespace] = useState('');
const [destinationNamespace, setDestinationNamespace] = useState('');
const dashboardToken = useDashboardToken();
const versions = useClusterVersions();
const nodeIPs = useNodeIPs();
const controlPlaneIP = useControlPlaneIP();
const dashboardToken = useDashboardToken(instanceId || '');
const versions = useClusterVersions(instanceId || '');
const nodeIPs = useNodeIPs(instanceId || '');
const controlPlaneIP = useControlPlaneIP(instanceId || '');
const copySecret = useCopySecret();
const handleCopySecret = () => {
if (secretToCopy && targetInstance) {
copySecret.mutate({ secret: secretToCopy, targetInstance });
if (secretToCopy && sourceNamespace && destinationNamespace && instanceId) {
copySecret.mutate({
instanceName: instanceId,
secret: secretToCopy,
sourceNamespace,
destinationNamespace
});
}
};
@@ -130,7 +138,7 @@ export function UtilitiesPage() {
{/* Secret Copy Utility */}
<UtilityCard
title="Copy Secret"
description="Copy a secret between namespaces or instances"
description="Copy a secret between namespaces"
icon={<Copy className="h-5 w-5 text-primary" />}
>
<div className="space-y-4">
@@ -146,19 +154,31 @@ export function UtilitiesPage() {
</div>
<div>
<label className="text-sm font-medium mb-2 block">
Target Instance/Namespace
Source Namespace
</label>
<input
type="text"
placeholder="e.g., default"
value={sourceNamespace}
onChange={(e) => setSourceNamespace(e.target.value)}
className="w-full px-3 py-2 border rounded-lg bg-background"
/>
</div>
<div>
<label className="text-sm font-medium mb-2 block">
Destination Namespace
</label>
<input
type="text"
placeholder="e.g., production"
value={targetInstance}
onChange={(e) => setTargetInstance(e.target.value)}
value={destinationNamespace}
onChange={(e) => setDestinationNamespace(e.target.value)}
className="w-full px-3 py-2 border rounded-lg bg-background"
/>
</div>
<Button
onClick={handleCopySecret}
disabled={!secretToCopy || !targetInstance || copySecret.isPending}
disabled={!secretToCopy || !sourceNamespace || !destinationNamespace || copySecret.isPending}
className="w-full"
>
{copySecret.isPending ? 'Copying...' : 'Copy Secret'}

View File

@@ -26,11 +26,11 @@ export const useOperations = (instanceName: string, filter?: 'running' | 'comple
});
};
export const useOperation = (operationId: string) => {
export const useOperation = (instanceName: string, operationId: string) => {
return useQuery<Operation>({
queryKey: ['operation', operationId],
queryFn: () => operationsApi.get(operationId),
enabled: !!operationId,
queryKey: ['operation', instanceName, operationId],
queryFn: () => operationsApi.get(instanceName, operationId),
enabled: !!instanceName && !!operationId,
refetchInterval: (query) => {
// Stop polling if operation is completed, failed, or cancelled
const status = query.state.data?.status;
@@ -47,12 +47,12 @@ export const useCancelOperation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ operationId, instanceName }: { operationId: string; instanceName: string }) =>
operationsApi.cancel(operationId, instanceName),
onSuccess: (_, { operationId }) => {
mutationFn: ({ instanceName, operationId }: { instanceName: string; operationId: string }) =>
operationsApi.cancel(instanceName, operationId),
onSuccess: (_, { instanceName, operationId }) => {
// Invalidate operation queries to refresh data
queryClient.invalidateQueries({ queryKey: ['operation', operationId] });
queryClient.invalidateQueries({ queryKey: ['operations'] });
queryClient.invalidateQueries({ queryKey: ['operation', instanceName, operationId] });
queryClient.invalidateQueries({ queryKey: ['operations', instanceName] });
},
});
};

View File

@@ -1,35 +1,39 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { utilitiesApi } from '../utilities';
export function useDashboardToken() {
export function useDashboardToken(instanceName: string) {
return useQuery({
queryKey: ['utilities', 'dashboard', 'token'],
queryFn: utilitiesApi.getDashboardToken,
staleTime: 5 * 60 * 1000, // 5 minutes
queryKey: ['instances', instanceName, 'utilities', 'dashboard', 'token'],
queryFn: () => utilitiesApi.getDashboardToken(instanceName),
staleTime: 30 * 60 * 1000, // 30 minutes
enabled: !!instanceName,
});
}
export function useClusterVersions() {
export function useClusterVersions(instanceName: string) {
return useQuery({
queryKey: ['utilities', 'version'],
queryFn: utilitiesApi.getVersion,
queryKey: ['instances', instanceName, 'utilities', 'version'],
queryFn: () => utilitiesApi.getVersion(instanceName),
staleTime: 10 * 60 * 1000, // 10 minutes
enabled: !!instanceName,
});
}
export function useNodeIPs() {
export function useNodeIPs(instanceName: string) {
return useQuery({
queryKey: ['utilities', 'nodes', 'ips'],
queryFn: utilitiesApi.getNodeIPs,
queryKey: ['instances', instanceName, 'utilities', 'nodes', 'ips'],
queryFn: () => utilitiesApi.getNodeIPs(instanceName),
staleTime: 30 * 1000, // 30 seconds
enabled: !!instanceName,
});
}
export function useControlPlaneIP() {
export function useControlPlaneIP(instanceName: string) {
return useQuery({
queryKey: ['utilities', 'controlplane', 'ip'],
queryFn: utilitiesApi.getControlPlaneIP,
queryKey: ['instances', instanceName, 'utilities', 'controlplane', 'ip'],
queryFn: () => utilitiesApi.getControlPlaneIP(instanceName),
staleTime: 60 * 1000, // 1 minute
enabled: !!instanceName,
});
}
@@ -37,8 +41,12 @@ export function useCopySecret() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ secret, targetInstance }: { secret: string; targetInstance: string }) =>
utilitiesApi.copySecret(secret, targetInstance),
mutationFn: ({ instanceName, secret, sourceNamespace, destinationNamespace }: {
instanceName: string;
secret: string;
sourceNamespace: string;
destinationNamespace: string;
}) => utilitiesApi.copySecret(instanceName, secret, sourceNamespace, destinationNamespace),
onSuccess: () => {
// Invalidate secrets queries
queryClient.invalidateQueries({ queryKey: ['secrets'] });

View File

@@ -6,18 +6,17 @@ export const operationsApi = {
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 get(instanceName: string, operationId: string): Promise<Operation> {
return apiClient.get(`/api/v1/instances/${instanceName}/operations/${operationId}`);
},
async cancel(operationId: string, instanceName: string): Promise<{ message: string }> {
return apiClient.post(`/api/v1/operations/${operationId}/cancel?instance=${instanceName}`);
async cancel(instanceName: string, operationId: string): Promise<{ message: string }> {
return apiClient.post(`/api/v1/instances/${instanceName}/operations/${operationId}/cancel`);
},
// SSE stream for operation updates
createStream(operationId: string): EventSource {
createStream(instanceName: string, operationId: string): EventSource {
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5055';
return new EventSource(`${baseUrl}/api/v1/operations/${operationId}/stream`);
return new EventSource(`${baseUrl}/api/v1/instances/${instanceName}/operations/${operationId}/stream`);
},
};

View File

@@ -2,7 +2,6 @@ import { apiClient } from './client';
import type {
ServiceListResponse,
Service,
ServiceStatus,
DetailedServiceStatus,
ServiceManifest,
ServiceInstallRequest,

View File

@@ -11,32 +11,31 @@ export interface VersionResponse {
}
export const utilitiesApi = {
async health(): Promise<HealthResponse> {
return apiClient.get('/api/v1/utilities/health');
},
async instanceHealth(instanceName: string): Promise<HealthResponse> {
async health(instanceName: string): Promise<HealthResponse> {
return apiClient.get(`/api/v1/instances/${instanceName}/utilities/health`);
},
async getDashboardToken(): Promise<{ token: string }> {
const response = await apiClient.get<{ data: { token: string }; success: boolean }>('/api/v1/utilities/dashboard/token');
async getDashboardToken(instanceName: string): Promise<{ token: string }> {
const response = await apiClient.get<{ data: { token: string }; success: boolean }>(`/api/v1/instances/${instanceName}/utilities/dashboard/token`);
return response.data;
},
async getNodeIPs(): Promise<{ ips: string[] }> {
return apiClient.get('/api/v1/utilities/nodes/ips');
async getNodeIPs(instanceName: string): Promise<{ ips: string[] }> {
return apiClient.get(`/api/v1/instances/${instanceName}/utilities/nodes/ips`);
},
async getControlPlaneIP(): Promise<{ ip: string }> {
return apiClient.get('/api/v1/utilities/controlplane/ip');
async getControlPlaneIP(instanceName: string): Promise<{ ip: string }> {
return apiClient.get(`/api/v1/instances/${instanceName}/utilities/controlplane/ip`);
},
async copySecret(secret: string, targetInstance: string): Promise<{ message: string }> {
return apiClient.post(`/api/v1/utilities/secrets/${secret}/copy`, { target: targetInstance });
async copySecret(instanceName: string, secret: string, sourceNamespace: string, destinationNamespace: string): Promise<{ message: string }> {
return apiClient.post(`/api/v1/instances/${instanceName}/utilities/secrets/${secret}/copy`, {
source_namespace: sourceNamespace,
destination_namespace: destinationNamespace
});
},
async getVersion(): Promise<VersionResponse> {
return apiClient.get('/api/v1/utilities/version');
async getVersion(instanceName: string): Promise<VersionResponse> {
return apiClient.get(`/api/v1/instances/${instanceName}/utilities/version`);
},
};