673 lines
20 KiB
TypeScript
673 lines
20 KiB
TypeScript
const DEFAULT_API_BASE_URL = 'https://api.xcloudify.tech/api';
|
|
|
|
// Read API base URL from env with safe normalization and fallback
|
|
const RAW_API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string | undefined;
|
|
const normalizeBaseUrl = (url: string) => url ? url.replace(/\/+$/, '') : '';
|
|
export const API_BASE_URL = normalizeBaseUrl(RAW_API_BASE_URL || DEFAULT_API_BASE_URL);
|
|
|
|
export interface XCloudifyWorkload {
|
|
cloudflare_tunnel: any;
|
|
containers: Array<{
|
|
container_id: string;
|
|
container_name: string;
|
|
is_sidecar?: boolean;
|
|
launch_params: {
|
|
container_name: string;
|
|
docker_image: string;
|
|
networks: string;
|
|
ports: Array<{
|
|
external: number;
|
|
internal: number;
|
|
use_dns: boolean;
|
|
}>;
|
|
};
|
|
status: string;
|
|
}>;
|
|
nscontroller_workload_id: string;
|
|
pod_id: string;
|
|
pod_name: string;
|
|
created_at?: string;
|
|
port_forwardings: Array<{
|
|
container_id: string;
|
|
container_name: string;
|
|
dns_record_hostname: string | null;
|
|
dns_record_id: string | null;
|
|
external_port: number;
|
|
internal_port: number;
|
|
external_ip_address: string;
|
|
internal_ip_address: string;
|
|
protocol: string;
|
|
}>;
|
|
region_id: string;
|
|
vdc_id: string;
|
|
virtual_data_center_id: string;
|
|
workload_host_id: string;
|
|
}
|
|
|
|
export interface ApiPod {
|
|
id: string;
|
|
name: string;
|
|
status: string;
|
|
created?: string;
|
|
namespace?: string;
|
|
node?: string;
|
|
ip?: string;
|
|
total_cpu?: number;
|
|
total_memory?: string;
|
|
virtual_data_center_id?: string;
|
|
containers: Array<{
|
|
id: string;
|
|
name: string;
|
|
image: string;
|
|
status: string;
|
|
type: string;
|
|
restarts?: number;
|
|
created?: string;
|
|
cpu_usage?: number;
|
|
memory_usage?: string;
|
|
sidecar_type?: string;
|
|
port_forwarding?: Array<{
|
|
internal: number;
|
|
external: number;
|
|
protocol: string;
|
|
use_dns?: boolean;
|
|
dns_name?: string;
|
|
external_ip_address?: string;
|
|
status?: string;
|
|
}>;
|
|
}>;
|
|
createdAt?: string;
|
|
updatedAt?: string;
|
|
}
|
|
|
|
export interface ApiResponse<T> {
|
|
code: number;
|
|
data: T;
|
|
message: string;
|
|
request_id: string;
|
|
success: boolean;
|
|
version: number;
|
|
}
|
|
|
|
export interface VirtualDataCenter {
|
|
id: string;
|
|
name: string;
|
|
description: string | null;
|
|
status: string | null;
|
|
project_id: string;
|
|
region_id: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
created_by: string;
|
|
deleted: boolean;
|
|
deleted_at: string | null;
|
|
visible: boolean;
|
|
}
|
|
|
|
// Transform XCloudify workload data to our Pod format
|
|
export function transformWorkloadToPod(workload: XCloudifyWorkload): ApiPod {
|
|
// Map port forwardings by container_id for easy lookup
|
|
const portForwardingMap = workload.port_forwardings.reduce((acc, pf) => {
|
|
if (!acc[pf.container_id]) {
|
|
acc[pf.container_id] = [];
|
|
}
|
|
acc[pf.container_id].push({
|
|
internal: pf.internal_port,
|
|
external: pf.external_port,
|
|
protocol: pf.protocol,
|
|
use_dns: !!pf.dns_record_hostname,
|
|
dns_name: pf.dns_record_hostname || undefined,
|
|
external_ip_address: pf.external_ip_address,
|
|
status: "active"
|
|
});
|
|
return acc;
|
|
}, {} as Record<string, any[]>);
|
|
|
|
return {
|
|
id: workload.pod_id,
|
|
name: workload.pod_name,
|
|
status: workload.containers.every(c => c.status === "running") ? "Running" : "Pending",
|
|
created: workload.created_at,
|
|
ip: workload.port_forwardings[0]?.external_ip_address,
|
|
virtual_data_center_id: workload.virtual_data_center_id,
|
|
containers: workload.containers.map(container => ({
|
|
id: container.container_id,
|
|
name: container.container_name,
|
|
image: container.launch_params.docker_image,
|
|
status: container.status,
|
|
type: container.is_sidecar ? "sidecar" : "user",
|
|
port_forwarding: portForwardingMap[container.container_id] || []
|
|
}))
|
|
};
|
|
}
|
|
|
|
class XCloudifyApiError extends Error {
|
|
constructor(message: string, public status?: number) {
|
|
super(message);
|
|
this.name = 'XCloudifyApiError';
|
|
}
|
|
}
|
|
|
|
// Global callback for API monitoring
|
|
let apiMonitorCallback: ((call: any) => void) | null = null;
|
|
|
|
export function setApiMonitorCallback(callback: (call: any) => void) {
|
|
apiMonitorCallback = callback;
|
|
}
|
|
|
|
export async function apiRequest<T>(endpoint: string, options: RequestInit & { responseType?: 'json' | 'blob' } = {}): Promise<T> {
|
|
const url = endpoint.startsWith('http')
|
|
? endpoint
|
|
: `${API_BASE_URL}${endpoint.startsWith('/') ? '' : '/'}${endpoint}`;
|
|
const startTime = Date.now();
|
|
|
|
const callData = {
|
|
method: options.method || 'GET',
|
|
url,
|
|
requestHeaders: options.headers,
|
|
requestBody: options.body ? JSON.parse(options.body as string) : undefined,
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...options.headers,
|
|
},
|
|
...options,
|
|
});
|
|
|
|
const duration = Date.now() - startTime;
|
|
let responseBody;
|
|
|
|
try {
|
|
responseBody = options.responseType === 'blob' ? await response.blob() : await response.json();
|
|
} catch (parseError) {
|
|
// If response body parsing fails, capture the raw text
|
|
responseBody = await response.text();
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const error = `API request failed: ${response.statusText}`;
|
|
// Always log failed requests
|
|
if (apiMonitorCallback) {
|
|
apiMonitorCallback({
|
|
...callData,
|
|
responseStatus: response.status,
|
|
responseHeaders: Object.fromEntries(response.headers.entries()),
|
|
responseBody,
|
|
duration,
|
|
error,
|
|
});
|
|
}
|
|
throw new XCloudifyApiError(error, response.status);
|
|
}
|
|
|
|
// Monitor successful API calls
|
|
if (apiMonitorCallback) {
|
|
apiMonitorCallback({
|
|
...callData,
|
|
responseStatus: response.status,
|
|
responseHeaders: Object.fromEntries(response.headers.entries()),
|
|
responseBody,
|
|
duration,
|
|
});
|
|
}
|
|
|
|
return responseBody;
|
|
} catch (error) {
|
|
const duration = Date.now() - startTime;
|
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
|
|
// Only log network errors, not already logged XCloudifyApiError instances
|
|
if (apiMonitorCallback && !(error instanceof XCloudifyApiError)) {
|
|
apiMonitorCallback({
|
|
...callData,
|
|
responseStatus: undefined,
|
|
duration,
|
|
error: errorMessage,
|
|
});
|
|
}
|
|
|
|
if (error instanceof XCloudifyApiError) {
|
|
throw error;
|
|
}
|
|
throw new XCloudifyApiError(`Network error: ${errorMessage}`);
|
|
}
|
|
}
|
|
|
|
export const xcloudifyApi = {
|
|
// Fetch all pods
|
|
async getPods(): Promise<ApiPod[]> {
|
|
const response = await apiRequest<ApiResponse<XCloudifyWorkload[]>>('/workloads/pods');
|
|
// Transform the workloads array to pods
|
|
return response.data.map(transformWorkloadToPod);
|
|
},
|
|
|
|
// Create a new container
|
|
async createContainer(containerData: any): Promise<any> {
|
|
return apiRequest('/workloads/containers', {
|
|
method: 'POST',
|
|
body: JSON.stringify(containerData),
|
|
});
|
|
},
|
|
|
|
// Get container details
|
|
async getContainer(containerId: string): Promise<any> {
|
|
const response = await apiRequest<ApiResponse<any>>(`/workloads/containers/${containerId}`);
|
|
return response.data;
|
|
},
|
|
|
|
// Container lifecycle actions (start, stop, restart)
|
|
async containerLifecycleAction(containerId: string, action: 'start' | 'stop' | 'restart'): Promise<any> {
|
|
return apiRequest(`/workloads/containers/${containerId}/lifecycle/${action}`, {
|
|
method: 'POST',
|
|
});
|
|
},
|
|
|
|
// Delete container
|
|
async deleteContainer(containerId: string): Promise<any> {
|
|
return apiRequest(`/workloads/containers/${containerId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
},
|
|
|
|
// Delete pod
|
|
async deletePod(podId: string): Promise<any> {
|
|
return apiRequest(`/workloads/pods/${podId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
},
|
|
|
|
// Get workload hosts
|
|
async getWorkloadHosts(regionId?: string): Promise<any[]> {
|
|
const url = regionId ? `/workload_hosts?region_id=${regionId}` : '/workload_hosts';
|
|
const response = await apiRequest<ApiResponse<any[]>>(url);
|
|
return response.data || [];
|
|
},
|
|
|
|
// Get workload host details
|
|
async getWorkloadHost(hostId: string): Promise<any> {
|
|
return apiRequest(`/workload_hosts/${hostId}`);
|
|
},
|
|
|
|
// Update workload host
|
|
async updateWorkloadHost(hostId: string, data: any): Promise<any> {
|
|
return apiRequest(`/workload_hosts/${hostId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
|
|
// Delete workload host
|
|
async deleteWorkloadHost(hostId: string): Promise<any> {
|
|
return apiRequest(`/workload_hosts/${hostId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
},
|
|
|
|
// Get audit logs for a container
|
|
async getContainerAuditLogs(containerId: string, params?: {
|
|
limit?: number;
|
|
offset?: number;
|
|
action?: string;
|
|
}): Promise<any> {
|
|
const queryParams = new URLSearchParams();
|
|
if (params?.limit) queryParams.append('limit', params.limit.toString());
|
|
if (params?.offset) queryParams.append('offset', params.offset.toString());
|
|
if (params?.action) queryParams.append('action', params.action);
|
|
|
|
const url = `/audit/container/${containerId}${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
|
return apiRequest(url);
|
|
},
|
|
|
|
// Get audit action types
|
|
async getAuditActionTypes(): Promise<any> {
|
|
return apiRequest('/audit/actions');
|
|
},
|
|
|
|
// Get audit logs for a pod
|
|
async getPodAuditLogs(podId: string, params?: {
|
|
limit?: number;
|
|
offset?: number;
|
|
action?: string;
|
|
}): Promise<any> {
|
|
const queryParams = new URLSearchParams();
|
|
if (params?.limit) queryParams.append('limit', params.limit.toString());
|
|
if (params?.offset) queryParams.append('offset', params.offset.toString());
|
|
if (params?.action) queryParams.append('action', params.action);
|
|
|
|
const url = `/audit/pod/${podId}${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
|
return apiRequest(url);
|
|
},
|
|
|
|
// Get labels for a workload host
|
|
async getLabelsForHost(hostId: string): Promise<any[]> {
|
|
const response = await apiRequest<ApiResponse<any[]>>(`/workload_hosts/${hostId}/labels`);
|
|
return response.data || [];
|
|
},
|
|
|
|
// Add label to workload host
|
|
async addLabel(hostId: string, key: string, value: string, adminOnlyView = false, adminOnlySet = false): Promise<any> {
|
|
return apiRequest(`/workload_hosts/${hostId}/labels`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
label_key: key,
|
|
label_value: value,
|
|
admin_only_view: adminOnlyView,
|
|
admin_only_set: adminOnlySet,
|
|
}),
|
|
});
|
|
},
|
|
|
|
// Remove label from workload host
|
|
async removeLabel(hostId: string, key: string): Promise<any> {
|
|
return apiRequest(`/workload_hosts/${hostId}/labels`, {
|
|
method: 'DELETE',
|
|
body: JSON.stringify({
|
|
label_key: key,
|
|
}),
|
|
});
|
|
},
|
|
|
|
// Get OVS bridges for host
|
|
async getOVSBridgesForHost(hostId: string): Promise<any[]> {
|
|
return apiRequest(`/workload_hosts/${hostId}/ovs_bridges`);
|
|
},
|
|
|
|
// Region management endpoints
|
|
async getRegions(): Promise<any[]> {
|
|
const response = await apiRequest<ApiResponse<any[]>>('/regions');
|
|
return response.data || [];
|
|
},
|
|
|
|
async getRegion(regionId: string): Promise<any> {
|
|
return apiRequest(`/regions/${regionId}`);
|
|
},
|
|
|
|
async createRegion(data: any): Promise<any> {
|
|
return apiRequest('/regions', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
|
|
async updateRegion(regionId: string, data: any): Promise<any> {
|
|
return apiRequest(`/regions/${regionId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
|
|
async deleteRegion(regionId: string): Promise<any> {
|
|
return apiRequest(`/regions/${regionId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
},
|
|
|
|
// Virtual Data Center management endpoints
|
|
async getVirtualDataCenters(): Promise<VirtualDataCenter[]> {
|
|
const response = await apiRequest<ApiResponse<VirtualDataCenter[]>>('/virtual_data_centers');
|
|
return response.data;
|
|
},
|
|
|
|
async getVirtualDataCenter(vdcId: string): Promise<any> {
|
|
return apiRequest(`/virtual_data_centers/${vdcId}`);
|
|
},
|
|
|
|
async createVirtualDataCenter(data: any): Promise<any> {
|
|
return apiRequest('/virtual_data_centers', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
|
|
async updateVirtualDataCenter(vdcId: string, data: any): Promise<any> {
|
|
return apiRequest(`/virtual_data_centers/${vdcId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
|
|
async deleteVirtualDataCenter(vdcId: string): Promise<any> {
|
|
return apiRequest(`/virtual_data_centers/${vdcId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
},
|
|
|
|
// Project management endpoints (needed for VDC creation)
|
|
async getProjects(): Promise<any[]> {
|
|
const response = await apiRequest<ApiResponse<any[]>>('/projects');
|
|
return response.data || [];
|
|
},
|
|
|
|
// Image management endpoints
|
|
async getImages(): Promise<any[]> {
|
|
const response = await apiRequest<{data: any[]}>('/images');
|
|
return response.data || [];
|
|
},
|
|
|
|
async getImage(imageId: string): Promise<any> {
|
|
return apiRequest(`/images/${imageId}`);
|
|
},
|
|
|
|
async createImage(data: any): Promise<any> {
|
|
return apiRequest('/images', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
|
|
async updateImage(imageId: string, data: any): Promise<any> {
|
|
return apiRequest(`/images/${imageId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
|
|
async deleteImage(imageId: string): Promise<any> {
|
|
return apiRequest(`/images/${imageId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
},
|
|
|
|
// Project management endpoints
|
|
async getProject(projectId: string): Promise<any> {
|
|
return apiRequest(`/projects/${projectId}`);
|
|
},
|
|
|
|
async createProject(data: any): Promise<any> {
|
|
return apiRequest('/projects', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
|
|
async updateProject(projectId: string, data: any): Promise<any> {
|
|
return apiRequest(`/projects/${projectId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
|
|
async deleteProject(projectId: string): Promise<any> {
|
|
return apiRequest(`/projects/${projectId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
},
|
|
|
|
// Universe management endpoints (needed for project creation)
|
|
async getUniverses(): Promise<any[]> {
|
|
const response = await apiRequest<ApiResponse<any[]>>('/universes');
|
|
return response.data || [];
|
|
},
|
|
|
|
// Get workloads for a specific workload host
|
|
async getWorkloadsForHost(workloadHostId: string): Promise<any[]> {
|
|
const response = await apiRequest<ApiResponse<any[]>>(`/workload_hosts/${workloadHostId}/active_workloads`);
|
|
return response.data || [];
|
|
},
|
|
|
|
// Virtual Machine management endpoints
|
|
async getVirtualMachines(): Promise<any[]> {
|
|
try {
|
|
const response = await apiRequest('/workloads/virtual_machines');
|
|
// Ensure we always return an array
|
|
return Array.isArray(response) ? response : [];
|
|
} catch (error) {
|
|
console.error('Error fetching virtual machines:', error);
|
|
return [];
|
|
}
|
|
},
|
|
|
|
async getVirtualMachine(vmId: string): Promise<any> {
|
|
return apiRequest(`/workloads/virtual_machines/${vmId}`);
|
|
},
|
|
|
|
async createVirtualMachine(data: any): Promise<any> {
|
|
return apiRequest('/workloads/virtual_machines', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
|
|
async deleteVirtualMachine(vmId: string): Promise<any> {
|
|
return apiRequest(`/workloads/virtual_machines/${vmId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
},
|
|
|
|
async updateVirtualMachine(vmId: string, data: any): Promise<any> {
|
|
return apiRequest(`/workloads/virtual_machines/status_update/${vmId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
|
|
// Network management endpoints
|
|
async getNetworks(): Promise<any[]> {
|
|
return apiRequest('/networks');
|
|
},
|
|
|
|
// Get networks by VDC
|
|
async getNetworksByVDC(vdcId: string): Promise<any[]> {
|
|
const response = await apiRequest<ApiResponse<any[]>>(`/networks?virtual_datacenter_id=${vdcId}`);
|
|
return response.data || [];
|
|
},
|
|
|
|
// Create network
|
|
async createNetwork(data: any): Promise<any> {
|
|
return apiRequest('/networks', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
|
|
// Volume management
|
|
getVolumes(params?: { vdc_id?: string; type?: string; boot?: boolean }): Promise<any> {
|
|
const searchParams = new URLSearchParams();
|
|
if (params?.vdc_id) searchParams.append('vdc_id', params.vdc_id);
|
|
if (params?.type) searchParams.append('type', params.type);
|
|
if (params?.boot !== undefined) searchParams.append('boot', params.boot.toString());
|
|
|
|
const queryString = searchParams.toString();
|
|
return apiRequest<any>(`/volumes${queryString ? `?${queryString}` : ''}`);
|
|
},
|
|
|
|
// Get volumes for a specific virtual data center
|
|
getVolumesForVirtualDataCenter(vdcId: string): Promise<any> {
|
|
return apiRequest<any>(`/virtual_data_centers/${vdcId}/volumes`);
|
|
},
|
|
|
|
createVolume(volumeData: any): Promise<any> {
|
|
return apiRequest<any>('/volumes', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(volumeData),
|
|
});
|
|
},
|
|
|
|
getVolume(volumeId: string): Promise<any> {
|
|
return apiRequest<any>(`/volumes/${volumeId}`);
|
|
},
|
|
|
|
updateVolume(volumeId: string, volumeData: any): Promise<any> {
|
|
return apiRequest<any>(`/volumes/${volumeId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(volumeData),
|
|
});
|
|
},
|
|
|
|
deleteVolume(volumeId: string): Promise<any> {
|
|
return apiRequest<any>(`/volumes/${volumeId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
},
|
|
|
|
attachVolumeToWorkload(volumeId: string, workloadId: string): Promise<any> {
|
|
return apiRequest<any>(`/volumes/${volumeId}/attach`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ workload_id: workloadId }),
|
|
});
|
|
},
|
|
|
|
detachVolumeFromWorkload(volumeId: string, workloadId: string): Promise<any> {
|
|
return apiRequest<any>(`/volumes/${volumeId}/detach/${workloadId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
},
|
|
|
|
getVolumesForWorkload(workloadId: string): Promise<any> {
|
|
return apiRequest<any>(`/workloads/${workloadId}/volumes`);
|
|
},
|
|
|
|
getWorkloadsForVolume(volumeId: string): Promise<any> {
|
|
return apiRequest<any>(`/volumes/${volumeId}/workloads`);
|
|
},
|
|
|
|
getVirtualDataCentersByProject(projectId: string): Promise<VirtualDataCenter[]> {
|
|
return apiRequest<ApiResponse<VirtualDataCenter[]>>(`/virtual_data_centers?project_id=${projectId}`).then(response => response.data || []);
|
|
},
|
|
|
|
// Available ports for workload hosts
|
|
getAvailablePortsForHost(hostId: string): Promise<any[]> {
|
|
return apiRequest<ApiResponse<any[]>>(`/workload_hosts/${hostId}/available_ports`).then(response => response.data || []);
|
|
},
|
|
|
|
addAvailablePortsForHost(hostId: string, portData: { start_port: number; end_port: number; protocol: string }): Promise<any> {
|
|
return apiRequest<any>(`/workload_hosts/${hostId}/available_ports`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(portData)
|
|
});
|
|
},
|
|
|
|
removeAvailablePortsForHost(hostId: string, portId: string): Promise<any> {
|
|
return apiRequest<any>(`/workload_hosts/${hostId}/available_ports/${portId}`, {
|
|
method: 'DELETE'
|
|
});
|
|
},
|
|
|
|
getVirtualDataCentersByRegion(regionId: string): Promise<VirtualDataCenter[]> {
|
|
return apiRequest<{ data: VirtualDataCenter[] }>(`/virtual_data_centers?region_id=${regionId}`)
|
|
.then(response => response.data);
|
|
},
|
|
|
|
deleteNetwork(networkId: string): Promise<any> {
|
|
return apiRequest(`/api/networks/${networkId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
},
|
|
};
|
|
|