Instance-namespace various endpoints and services.
This commit is contained in:
@@ -10,14 +10,14 @@ import {
|
||||
import { ConfigEditor } from "./ConfigEditor";
|
||||
import { Button, Input, Label } from "./ui";
|
||||
import { Check, Edit2, HelpCircle, X, ExternalLink, Copy } from "lucide-react";
|
||||
import { useInstanceDashboardToken } from "../services/api/hooks/useUtilities";
|
||||
import { useDashboardToken } from "../services/api/hooks/useUtilities";
|
||||
import { useInstance } from "../services/api";
|
||||
|
||||
export function Advanced() {
|
||||
const { instanceId } = useParams<{ instanceId: string }>();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const { data: instance } = useInstance(instanceId || '');
|
||||
const { data: dashboardToken, isLoading: tokenLoading } = useInstanceDashboardToken(instanceId || '');
|
||||
const { data: dashboardToken, isLoading: tokenLoading } = useDashboardToken(instanceId || '');
|
||||
|
||||
const [upstreamValue, setUpstreamValue] = useState("https://mywildcloud.org");
|
||||
const [editingUpstream, setEditingUpstream] = useState(false);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,44 +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
|
||||
});
|
||||
}
|
||||
|
||||
export function useInstanceDashboardToken(instanceId: string) {
|
||||
return useQuery({
|
||||
queryKey: ['instances', instanceId, 'utilities', 'dashboard', 'token'],
|
||||
queryFn: () => utilitiesApi.getInstanceDashboardToken(instanceId),
|
||||
queryKey: ['instances', instanceName, 'utilities', 'dashboard', 'token'],
|
||||
queryFn: () => utilitiesApi.getDashboardToken(instanceName),
|
||||
staleTime: 30 * 60 * 1000, // 30 minutes
|
||||
enabled: !!instanceId,
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,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'] });
|
||||
|
||||
@@ -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`);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -11,37 +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');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getInstanceDashboardToken(instanceName: string): Promise<{ token: string }> {
|
||||
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`);
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user