diff --git a/src/app.tsx b/src/app.tsx index 466e7d0..6b32187 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,13 +1,13 @@ -import { useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { App, ConfigProvider, Select } from 'antd' import { PageContainer, ProLayout } from '@ant-design/pro-components' import { NavLink, useLocation } from 'react-router-dom' import { LayoutSettings } from '@/layout-config' import { NamespaceProvider, useNamespace } from '@/common/context' -import { ResourceType } from '@/clients/ts/types/types' -import { useListResources } from './hooks/use-resource' +import { Namespace, watchNamespaces } from './clients/namespace' import AppRouter from '@/router' import styles from '@/styles/app.module.less' +import useUnmount from './hooks/use-unmount' export default () => { const location = useLocation() @@ -16,7 +16,19 @@ export default () => { const [collapsed, setCollapsed] = useState(false) - const { resources: namespaces } = useListResources(ResourceType.NAMESPACE) + const [namespaces, setNamespaces] = useState() + + const abortCtrl = useRef() + + useEffect(() => { + abortCtrl.current?.abort() + abortCtrl.current = new AbortController() + watchNamespaces(setNamespaces, abortCtrl.current.signal, undefined, undefined, undefined) + }, []) + + useUnmount(() => { + abortCtrl.current?.abort() + }) return ( { onChange={(value) => setNamespace(value)} options={[ { value: '', label: '全部工作空间' }, - ...namespaces.map((ns: any) => ({ + ...(namespaces ?? []).map((ns: any) => ({ value: ns.metadata.name, label: ns.metadata.name })) diff --git a/src/clients/clients.ts b/src/clients/clients.ts index 4d80b93..426ba37 100644 --- a/src/clients/clients.ts +++ b/src/clients/clients.ts @@ -2,22 +2,26 @@ import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport" import { ResourceWatchManagementClient } from "@/clients/ts/management/resource/v1alpha1/watch.client" import { VirtualMachineManagementClient } from "@/clients/ts/management/virtualmachine/v1alpha1/virtualmachine.client" import { ResourceManagementClient } from "@/clients/ts/management/resource/v1alpha1/resource.client" -import { ResourceType, NamespaceName } from "@/clients/ts/types/types" -import { extractNamespaceAndName, namespaceNameKey } from "@/utils/k8s" +import { ResourceType } from "@/clients/ts/types/types" import { VirtualMachinePowerStateRequest_PowerState } from "@/clients/ts/management/virtualmachine/v1alpha1/virtualmachine" -import { ListOptions } from "./ts/management/resource/v1alpha1/resource" -import { virtualMachine } from "@/utils/parse-summary" export const transport = new GrpcWebFetchTransport({ baseUrl: window.location.origin }) -export const defaultTimeout = 1500 +export const defaultTimeout = 0 export const resourceClient = new ResourceManagementClient(transport) export const virtualMachineClient = new VirtualMachineManagementClient(transport) export const resourceWatchClient = new ResourceWatchManagementClient(transport) +// export interface metadata { +// name: string +// namespace: string +// labels?: Record +// annotations?: Record +// } + export const getResourceName = (type: ResourceType) => { return ResourceType[type] } @@ -26,185 +30,185 @@ export const getPowerStateName = (state: VirtualMachinePowerStateRequest_PowerSt return VirtualMachinePowerStateRequest_PowerState[state] } -export class Clients { - private static instance: Clients - - readonly watch: ResourceWatchManagementClient - readonly resource: ResourceManagementClient - readonly virtualmachine: VirtualMachineManagementClient - - private constructor() { - const transport = new GrpcWebFetchTransport({ - baseUrl: window.location.origin - }) - - this.watch = new ResourceWatchManagementClient(transport) - this.resource = new ResourceManagementClient(transport) - this.virtualmachine = new VirtualMachineManagementClient(transport) - } - - public static getInstance(): Clients { - if (!Clients.instance) { - Clients.instance = new Clients() - } - return Clients.instance - } - - public batchDeleteResources = async (resourceType: ResourceType, resources: any[]): Promise => { - const completed: NamespaceName[] = [] - const failed: NamespaceName[] = [] - - await Promise.all(resources.map(async (crd) => { - const namespaceName = extractNamespaceAndName(crd) - try { - await this.resource.delete({ namespaceName: namespaceName, resourceType: resourceType }).response - completed.push(namespaceName) - } catch (err: any) { - failed.push(namespaceName) - } - return - })) - - if (failed.length > 0) { - Promise.reject(new Error(`Failed to delete ${getResourceName(resourceType)} ${failed.map(namespaceNameKey).join(", ")}`)) - } - } - - public createResource = async (resourceType: ResourceType, crd: any): Promise => { - const namespaceName = extractNamespaceAndName(crd) - const data = JSON.stringify(crd) - return new Promise((resolve, reject) => { - const call = this.resource.create({ - resourceType: resourceType, - data: data - }) - - call.then((result) => { - resolve(JSON.parse(result.response.data)) - }) - call.response.catch((err: Error) => { - reject(new Error(`Failed to create ${getResourceName(resourceType)} ${namespaceNameKey(namespaceName)}: ${err.message}`)) - }) - }) - } - - public deleteResource = async (resourceType: ResourceType, namespaceName: NamespaceName): Promise => { - return new Promise((resolve, reject) => { - const call = this.resource.delete({ - resourceType: resourceType, - namespaceName: namespaceName - }) - - call.then(() => { - resolve() - }) - call.response.catch((err: Error) => { - reject(new Error(`Failed to delete ${getResourceName(resourceType)} ${namespaceNameKey(namespaceName)}: ${err.message}`)) - }) - }) - } - - public updateResource = async (resourceType: ResourceType, crd: any): Promise => { - const namespaceName = extractNamespaceAndName(crd) - const data = JSON.stringify(crd) - return new Promise((resolve, reject) => { - const call = this.resource.update({ - resourceType: resourceType, - data: data - }) - - call.then((result) => { - resolve(JSON.parse(result.response.data)) - }) - call.response.catch((err: Error) => { - reject(new Error(`Failed to update ${getResourceName(resourceType)} ${namespaceNameKey(namespaceName)}: ${err.message}`)) - }) - }) - } - - public getResource = async (resourceType: ResourceType, namespaceName: NamespaceName): Promise => { - return new Promise((resolve, reject) => { - const call = this.resource.get({ - resourceType: resourceType, - namespaceName: namespaceName - }) - call.then((result) => { - resolve(JSON.parse(result.response.data)) - }) - call.response.catch((err: Error) => { - reject(new Error(`Failed to get ${getResourceName(resourceType)} ${namespaceNameKey(namespaceName)}: ${err.message}`)) - }) - }) - } - - public listResources = async (resourceType: ResourceType, opts?: ListOptions): Promise => { - return new Promise((resolve, reject) => { - const call = this.resource.list({ - resourceType: resourceType, - options: ListOptions.create(opts) - }) - call.then((result) => { - let items: any[] = [] - result.response.items.forEach((item: any) => { - items.push(JSON.parse(item)) - }) - resolve(items) - }) - call.response.catch((err: Error) => { - reject(new Error(`Failed to list ${getResourceName(resourceType)}: ${err.message}`)) - }) - }) - } - - public batchManageVirtualMachinePowerState = async (vms: any[], state: VirtualMachinePowerStateRequest_PowerState): Promise => { - const completed: NamespaceName[] = [] - const failed: NamespaceName[] = [] - - await Promise.all(vms.map(async (vm) => { - const namespaceName = extractNamespaceAndName(vm) - const isRunning = virtualMachine(vm)?.status.printableStatus as string === "Running" - - if (state === VirtualMachinePowerStateRequest_PowerState.OFF && !isRunning) { - return - } - if (state === VirtualMachinePowerStateRequest_PowerState.ON && isRunning) { - return - } - if (state === VirtualMachinePowerStateRequest_PowerState.REBOOT && !isRunning) { - return - } - - try { - await this.virtualmachine.virtualMachinePowerState({ - namespaceName: namespaceName, powerState: state - }).response - completed.push(namespaceName) - } catch (err: any) { - console.log(err) - failed.push(namespaceName) - } - return - })) - - if (failed.length > 0) { - return Promise.reject(new Error(`Failed to updated power state ${getResourceName(ResourceType.VIRTUAL_MACHINE)}: ${failed.map((vm) => namespaceNameKey(vm)).join(", ")}`)) - } - } - - public manageVirtualMachinePowerState = (namespaceName: NamespaceName, state: VirtualMachinePowerStateRequest_PowerState): Promise => { - return new Promise((resolve, reject) => { - const call = this.virtualmachine.virtualMachinePowerState({ - namespaceName: namespaceName, - powerState: state - }) - call.response.then(() => { - resolve() - }) - call.response.catch((err: Error) => { - reject(new Error(`Failed to update power state for ${namespaceNameKey(namespaceName)}: ${err.message}`)) - }) - }) - } -} - -export const clients = Clients.getInstance() +// export class Clients { +// private static instance: Clients + +// readonly watch: ResourceWatchManagementClient +// readonly resource: ResourceManagementClient +// readonly virtualmachine: VirtualMachineManagementClient + +// private constructor() { +// const transport = new GrpcWebFetchTransport({ +// baseUrl: window.location.origin +// }) + +// this.watch = new ResourceWatchManagementClient(transport) +// this.resource = new ResourceManagementClient(transport) +// this.virtualmachine = new VirtualMachineManagementClient(transport) +// } + +// public static getInstance(): Clients { +// if (!Clients.instance) { +// Clients.instance = new Clients() +// } +// return Clients.instance +// } + +// public batchDeleteResources = async (resourceType: ResourceType, resources: any[]): Promise => { +// const completed: NamespaceName[] = [] +// const failed: NamespaceName[] = [] + +// await Promise.all(resources.map(async (crd) => { +// const namespaceName = extractNamespaceAndName(crd) +// try { +// await this.resource.delete({ namespaceName: namespaceName, resourceType: resourceType }).response +// completed.push(namespaceName) +// } catch (err: any) { +// failed.push(namespaceName) +// } +// return +// })) + +// if (failed.length > 0) { +// Promise.reject(new Error(`Failed to delete ${getResourceName(resourceType)} ${failed.map(namespaceNameKey).join(", ")}`)) +// } +// } + +// public createResource = async (resourceType: ResourceType, crd: any): Promise => { +// const namespaceName = extractNamespaceAndName(crd) +// const data = JSON.stringify(crd) +// return new Promise((resolve, reject) => { +// const call = this.resource.create({ +// resourceType: resourceType, +// data: data +// }) + +// call.then((result) => { +// resolve(JSON.parse(result.response.data)) +// }) +// call.response.catch((err: Error) => { +// reject(new Error(`Failed to create ${getResourceName(resourceType)} ${namespaceNameKey(namespaceName)}: ${err.message}`)) +// }) +// }) +// } + +// public deleteResource = async (resourceType: ResourceType, namespaceName: NamespaceName): Promise => { +// return new Promise((resolve, reject) => { +// const call = this.resource.delete({ +// resourceType: resourceType, +// namespaceName: namespaceName +// }) + +// call.then(() => { +// resolve() +// }) +// call.response.catch((err: Error) => { +// reject(new Error(`Failed to delete ${getResourceName(resourceType)} ${namespaceNameKey(namespaceName)}: ${err.message}`)) +// }) +// }) +// } + +// public updateResource = async (resourceType: ResourceType, crd: any): Promise => { +// const namespaceName = extractNamespaceAndName(crd) +// const data = JSON.stringify(crd) +// return new Promise((resolve, reject) => { +// const call = this.resource.update({ +// resourceType: resourceType, +// data: data +// }) + +// call.then((result) => { +// resolve(JSON.parse(result.response.data)) +// }) +// call.response.catch((err: Error) => { +// reject(new Error(`Failed to update ${getResourceName(resourceType)} ${namespaceNameKey(namespaceName)}: ${err.message}`)) +// }) +// }) +// } + +// public getResource = async (resourceType: ResourceType, namespaceName: NamespaceName): Promise => { +// return new Promise((resolve, reject) => { +// const call = this.resource.get({ +// resourceType: resourceType, +// namespaceName: namespaceName +// }) +// call.then((result) => { +// resolve(JSON.parse(result.response.data)) +// }) +// call.response.catch((err: Error) => { +// reject(new Error(`Failed to get ${getResourceName(resourceType)} ${namespaceNameKey(namespaceName)}: ${err.message}`)) +// }) +// }) +// } + +// public listResources = async (resourceType: ResourceType, opts?: ListOptions): Promise => { +// return new Promise((resolve, reject) => { +// const call = this.resource.list({ +// resourceType: resourceType, +// options: ListOptions.create(opts) +// }) +// call.then((result) => { +// let items: any[] = [] +// result.response.items.forEach((item: any) => { +// items.push(JSON.parse(item)) +// }) +// resolve(items) +// }) +// call.response.catch((err: Error) => { +// reject(new Error(`Failed to list ${getResourceName(resourceType)}: ${err.message}`)) +// }) +// }) +// } + +// public batchManageVirtualMachinePowerState = async (vms: any[], state: VirtualMachinePowerStateRequest_PowerState): Promise => { +// const completed: NamespaceName[] = [] +// const failed: NamespaceName[] = [] + +// await Promise.all(vms.map(async (vm) => { +// const namespaceName = extractNamespaceAndName(vm) +// const isRunning = virtualMachine(vm)?.status.printableStatus as string === "Running" + +// if (state === VirtualMachinePowerStateRequest_PowerState.OFF && !isRunning) { +// return +// } +// if (state === VirtualMachinePowerStateRequest_PowerState.ON && isRunning) { +// return +// } +// if (state === VirtualMachinePowerStateRequest_PowerState.REBOOT && !isRunning) { +// return +// } + +// try { +// await this.virtualmachine.virtualMachinePowerState({ +// namespaceName: namespaceName, powerState: state +// }).response +// completed.push(namespaceName) +// } catch (err: any) { +// console.log(err) +// failed.push(namespaceName) +// } +// return +// })) + +// if (failed.length > 0) { +// return Promise.reject(new Error(`Failed to updated power state ${getResourceName(ResourceType.VIRTUAL_MACHINE)}: ${failed.map((vm) => namespaceNameKey(vm)).join(", ")}`)) +// } +// } + +// public manageVirtualMachinePowerState = (namespaceName: NamespaceName, state: VirtualMachinePowerStateRequest_PowerState): Promise => { +// return new Promise((resolve, reject) => { +// const call = this.virtualmachine.virtualMachinePowerState({ +// namespaceName: namespaceName, +// powerState: state +// }) +// call.response.then(() => { +// resolve() +// }) +// call.response.catch((err: Error) => { +// reject(new Error(`Failed to update power state for ${namespaceNameKey(namespaceName)}: ${err.message}`)) +// }) +// }) +// } +// } + +// export const clients = Clients.getInstance() diff --git a/src/clients/data-volume.ts b/src/clients/data-volume.ts index 68860e7..d26e959 100644 --- a/src/clients/data-volume.ts +++ b/src/clients/data-volume.ts @@ -4,122 +4,333 @@ import { components } from "./ts/openapi/openapi-schema" import { NamespaceName, ResourceType } from "./ts/types/types" import { EventType, WatchOptions } from "./ts/management/resource/v1alpha1/watch" import { namespaceNameKey } from "@/utils/k8s" -import { isAbortedError } from "@/utils/utils" +import { getErrorMessage, isAbortedError } from "@/utils/utils" import { VirtualMachine } from "./virtual-machine" +import { NotificationInstance } from "antd/lib/notification/interface" export type DataVolume = components["schemas"]["v1beta1DataVolume"] -export const getDataVolume = async (ns: NamespaceName): Promise => { - return new Promise((resolve, reject) => { - const call = resourceClient.get({ +// export const getDataVolume = async (ns: NamespaceName): Promise => { +// return new Promise((resolve, reject) => { +// const call = resourceClient.get({ +// resourceType: ResourceType.DATA_VOLUME, +// namespaceName: ns +// }) +// call.then((result) => { +// resolve(JSON.parse(result.response.data) as DataVolume) +// }) +// call.response.catch((err: Error) => { +// reject(new Error(`Failed to get data volume [Namespace: ${ns.namespace}, Name: ${ns.name}]: ${err.message}`)) +// }) +// }) +// } + +export const getDataVolume = async (ns: NamespaceName, setDataVolume?: React.Dispatch>, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const result = await resourceClient.get({ resourceType: ResourceType.DATA_VOLUME, namespaceName: ns }) - call.then((result) => { - resolve(JSON.parse(result.response.data) as DataVolume) - }) - call.response.catch((err: Error) => { - reject(new Error(`Failed to get data volume [Namespace: ${ns.namespace}, Name: ${ns.name}]: ${err.message}`)) - }) - }) + const output = JSON.parse(result.response.data) as DataVolume + setDataVolume?.(output) + return output + } catch (err) { + notification?.error({ message: `Failed to get DataVolume [Namespace: ${ns.namespace}, Name: ${ns.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } } -export const listDataVolumes = async (opts?: ListOptions): Promise => { - return new Promise((resolve, reject) => { - const call = resourceClient.list({ +// export const listDataVolumes = async (opts?: ListOptions): Promise => { +// return new Promise((resolve, reject) => { +// const call = resourceClient.list({ +// resourceType: ResourceType.DATA_VOLUME, +// options: ListOptions.create(opts) +// }) +// call.then((result) => { +// let items: DataVolume[] = [] +// result.response.items.forEach((item: string) => { +// items.push(JSON.parse(item) as DataVolume) +// }) +// resolve(items) +// }) +// call.response.catch((err: Error) => { +// reject(new Error(`Failed to list data volume: ${err.message}`)) +// }) +// }) +// } + +export const listDataVolumes = async (setDataVolumes?: React.Dispatch>, opts?: ListOptions, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + try { + const result = await resourceClient.list({ resourceType: ResourceType.DATA_VOLUME, options: ListOptions.create(opts) }) - call.then((result) => { - let items: DataVolume[] = [] - result.response.items.forEach((item: string) => { - items.push(JSON.parse(item) as DataVolume) - }) - resolve(items) - }) - call.response.catch((err: Error) => { - reject(new Error(`Failed to list data volume: ${err.message}`)) + let items: DataVolume[] = [] + result.response.items.forEach((item: string) => { + items.push(JSON.parse(item) as DataVolume) }) - }) + setDataVolumes?.(items) + return items + } catch (err) { + notification?.error({ message: `Failed to list DataVolumes`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } } -export const watchDataVolumes = (setDataVolumes: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal, opts?: WatchOptions): Promise => { - return new Promise((resolve, reject) => { - setLoading(true) +// export const watchDataVolumes = (setDataVolumes: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal, opts?: WatchOptions): Promise => { +// return new Promise((resolve, reject) => { +// setLoading(true) - const map = new Map() +// const map = new Map() - const call = resourceWatchClient.watch({ - resourceType: ResourceType.DATA_VOLUME, - options: WatchOptions.create(opts) - }, { abort: abortSignal }) +// const call = resourceWatchClient.watch({ +// resourceType: ResourceType.DATA_VOLUME, +// options: WatchOptions.create(opts) +// }, { abort: abortSignal }) + +// let timeoutId: NodeJS.Timeout | null = null +// const updateVirtualMachineSummarys = () => { +// if (map.size === 0 && timeoutId === null) { +// timeoutId = setTimeout(() => { +// const items = Array.from(map.values()) +// setDataVolumes(items.length > 0 ? items : undefined) +// timeoutId = null +// }, defaultTimeout) +// } else { +// const items = Array.from(map.values()) +// setDataVolumes(items.length > 0 ? items : undefined) +// if (timeoutId !== null) { +// clearTimeout(timeoutId) +// timeoutId = null +// } +// } +// } - let timeoutId: NodeJS.Timeout | null = null - const updateVirtualMachineSummarys = () => { - if (map.size === 0 && timeoutId === null) { - timeoutId = setTimeout(() => { +// call.responses.onMessage((response) => { +// switch (response.eventType) { +// case EventType.READY: { +// setLoading(false) +// break +// } +// case EventType.ADDED: +// case EventType.MODIFIED: { +// response.items.forEach((data) => { +// const vm = JSON.parse(data) as DataVolume +// map.set(namespaceNameKey(vm), vm) +// }) +// break +// } +// case EventType.DELETED: { +// response.items.forEach((data) => { +// const vm = JSON.parse(data) as DataVolume +// map.delete(namespaceNameKey(vm)) +// }) +// break +// } +// } +// updateVirtualMachineSummarys() +// }) + +// call.responses.onError((err: Error) => { +// setLoading(false) +// if (isAbortedError(err)) { +// resolve() +// } else { +// reject(new Error(`Error in watch stream for DataVolume: ${err.message}`)) +// } +// }) + +// call.responses.onComplete(() => { +// setLoading(false) +// resolve() +// }) +// }) +// } + +export const watchDataVolumes = async (setDataVolumes: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal, opts?: WatchOptions, notification?: NotificationInstance): Promise => { + setLoading(true) + try { + await new Promise((resolve, reject) => { + const map = new Map() + + const call = resourceWatchClient.watch({ + resourceType: ResourceType.DATA_VOLUME, + options: WatchOptions.create(opts) + }, { abort: abortSignal }) + + let timeoutId: NodeJS.Timeout | null = null + const updateVirtualMachineSummarys = () => { + if (map.size === 0 && timeoutId === null) { + timeoutId = setTimeout(() => { + const items = Array.from(map.values()) + setDataVolumes(items.length > 0 ? items : undefined) + timeoutId = null + }, defaultTimeout) + } else { const items = Array.from(map.values()) setDataVolumes(items.length > 0 ? items : undefined) - timeoutId = null - }, defaultTimeout) - } else { - const items = Array.from(map.values()) - setDataVolumes(items.length > 0 ? items : undefined) - if (timeoutId !== null) { - clearTimeout(timeoutId) - timeoutId = null + if (timeoutId !== null) { + clearTimeout(timeoutId) + timeoutId = null + } } } - } - call.responses.onMessage((response) => { - switch (response.eventType) { - case EventType.READY: { - setLoading(false) - break - } - case EventType.ADDED: - case EventType.MODIFIED: { - response.items.forEach((data) => { - const vm = JSON.parse(data) as DataVolume - map.set(namespaceNameKey(vm), vm) - }) - break + call.responses.onMessage((response) => { + switch (response.eventType) { + case EventType.READY: { + resolve() + break + } + case EventType.ADDED: + case EventType.MODIFIED: { + response.items.forEach((data) => { + const vm = JSON.parse(data) as DataVolume + map.set(namespaceNameKey(vm), vm) + }) + break + } + case EventType.DELETED: { + response.items.forEach((data) => { + const vm = JSON.parse(data) as DataVolume + map.delete(namespaceNameKey(vm)) + }) + break + } } - case EventType.DELETED: { - response.items.forEach((data) => { - const vm = JSON.parse(data) as DataVolume - map.delete(namespaceNameKey(vm)) - }) - break + updateVirtualMachineSummarys() + }) + + call.responses.onError((err: Error) => { + if (isAbortedError(err)) { + resolve() + } else { + reject(new Error(`Error in watch stream for DataVolume: ${err.message}`)) } - } - updateVirtualMachineSummarys() - }) + }) - call.responses.onError((err: Error) => { - setLoading(false) - if (isAbortedError(err)) { + call.responses.onComplete(() => { resolve() - } else { - reject(new Error(`Error in watch stream for DataVolume: ${err.message}`)) - } + }) }) + } catch (err) { + notification?.error({ message: `Failed to watch DataVolumes`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading(false) + } +} + +export const watchDataVolume = async (ns: NamespaceName, setDataVolume: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal, notification?: NotificationInstance): Promise => { + setLoading(true) + try { + await new Promise((resolve, reject) => { + const call = resourceWatchClient.watch({ + resourceType: ResourceType.DATA_VOLUME, + options: WatchOptions.create({ + fieldSelectorGroup: { + operator: "&&", + fieldSelectors: [ + { fieldPath: "metadata.namespace", operator: "=", values: [ns.namespace] }, + { fieldPath: "metadata.name", operator: "=", values: [ns.name] } + ] + } + }) + }, { abort: abortSignal }) + + call.responses.onMessage((response) => { + switch (response.eventType) { + case EventType.READY: { + resolve() + break + } + case EventType.ADDED: + case EventType.MODIFIED: { + if (response.items.length === 0) { + return + } + setDataVolume(JSON.parse(response.items[0]) as DataVolume) + break + } + case EventType.DELETED: { + setDataVolume(undefined) + break + } + } + }) + + call.responses.onError((err: Error) => { + if (!isAbortedError(err)) { + reject(new Error(`Error in watch stream for DataVolume: ${err.message}`)) + } else { + resolve() + } + }) - call.responses.onComplete(() => { - setLoading(false) - resolve() + call.responses.onComplete(() => { + resolve() + }) }) - }) + } catch (err) { + notification?.error({ message: `Failed to watch data volume`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading(false) + } } -export const getRootDisk = (setLoading: React.Dispatch>, vm: VirtualMachine): Promise => { - return new Promise((resolve, reject) => { +// export const getRootDisk = (setLoading: React.Dispatch>, vm: VirtualMachine): Promise => { +// return new Promise((resolve, reject) => { +// const disks = vm.spec?.template?.spec?.domain?.devices?.disks +// if (!disks || disks.length === 0) { +// setLoading(false) +// reject(new Error(`Failed to get root disk for VirtualMachine [Namespace: ${vm.metadata!.namespace}, Name: ${vm.metadata!.name}]: No disks found`)) +// return +// } + +// let disk = disks.find((disk: any) => { +// return disk.bootOrder === 1 +// }) +// if (!disk) { +// disk = disks[0] +// } + +// const volumes = vm.spec?.template?.spec?.volumes +// if (!volumes || volumes.length === 0) { +// setLoading(false) +// reject(new Error(`Failed to get root disk for VirtualMachine [Namespace: ${vm.metadata!.namespace}, Name: ${vm.metadata!.name}]: No volumes found`)) +// return +// } + +// const vol = volumes.find((vol: any) => { +// return vol.name == disk.name +// }) +// if (!vol) { +// setLoading(false) +// reject(new Error(`Failed to get root disk for VirtualMachine [Namespace: ${vm.metadata!.namespace}, Name: ${vm.metadata!.name}]: No volume found`)) +// return +// } +// getDataVolume({ namespace: vm.metadata!.namespace, name: vol.dataVolume!.name }).then(dv => { +// resolve(dv) +// }).catch(err => { +// reject(new Error(`Failed to get root disk for VirtualMachine [Namespace: ${vm.metadata!.namespace}, Name: ${vm.metadata!.name}]: ${err.message}`)) +// }).finally(() => { +// setLoading(false) +// }) +// }) +// } + +export const getRootDisk = async (vm: VirtualMachine, setDataVolume?: React.Dispatch>, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(false) + try { const disks = vm.spec?.template?.spec?.domain?.devices?.disks if (!disks || disks.length === 0) { - setLoading(false) - reject(new Error(`Failed to get root disk for VirtualMachine [Namespace: ${vm.metadata!.namespace}, Name: ${vm.metadata!.name}]: No disks found`)) - return + throw new Error(`Failed to get root disk for VirtualMachine [Namespace: ${vm.metadata!.namespace}, Name: ${vm.metadata!.name}]: No disks found`) } let disk = disks.find((disk: any) => { @@ -131,25 +342,109 @@ export const getRootDisk = (setLoading: React.Dispatch { return vol.name == disk.name }) if (!vol) { - setLoading(false) - reject(new Error(`Failed to get root disk for VirtualMachine [Namespace: ${vm.metadata!.namespace}, Name: ${vm.metadata!.name}]: No volume found`)) - return + throw new Error(`Failed to get root disk for VirtualMachine [Namespace: ${vm.metadata!.namespace}, Name: ${vm.metadata!.name}]: No volume found`) } - getDataVolume({ namespace: vm.metadata!.namespace, name: vol.dataVolume!.name }).then(dv => { - resolve(dv) - }).catch(err => { - reject(new Error(`Failed to get root disk for VirtualMachine [Namespace: ${vm.metadata!.namespace}, Name: ${vm.metadata!.name}]: ${err.message}`)) - }).finally(() => { - setLoading(false) + + const result = await resourceClient.get({ + resourceType: ResourceType.DATA_VOLUME, + namespaceName: { namespace: vm.metadata!.namespace, name: vol.dataVolume!.name } + }) + const output = JSON.parse(result.response.data) as DataVolume + setDataVolume?.(output) + return output + } catch (err) { + notification?.error({ message: `Failed to get root disk for VirtualMachine [Namespace: ${vm.metadata!.namespace}, Name: ${vm.metadata!.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} + + +export const deleteDataVolumes = async (dvs: DataVolume[], setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const completed: DataVolume[] = [] + const failed: { dv: DataVolume; error: any }[] = [] + + await Promise.all(dvs.map(async (dv) => { + const namespace = dv.metadata!.namespace + const name = dv.metadata!.name + + try { + await resourceClient.delete({ namespaceName: { namespace, name }, resourceType: ResourceType.DATA_VOLUME }).response + completed.push(dv) + } catch (err: any) { + failed.push({ dv, error: err }) + } + })) + + if (failed.length > 0) { + const errorMessages = failed.map(({ dv, error }) => `Namespace: ${dv.metadata?.namespace ?? "unknown"}, Name: ${dv.metadata?.name ?? "unknown"}, Error: ${error.message}`).join("\n") + throw new Error(`Failed to delete the following data volumes:\n${errorMessages}`) + } + } catch (err) { + notification?.error({ message: `Failed to delete data volumes:`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} + +export const deleteDataVolume = async (ns: NamespaceName, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + await resourceClient.delete({ + resourceType: ResourceType.DATA_VOLUME, + namespaceName: ns + }) + } catch (err) { + notification?.error({ message: `Failed to delete data volume [Namespace: ${ns.namespace}, Name: ${ns.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} + +export const updateDataVolume = async (dv: DataVolume, setDataVolume?: React.Dispatch>, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const result = await resourceClient.update({ + resourceType: ResourceType.DATA_VOLUME, + data: JSON.stringify(dv) + }) + const temp = JSON.parse(result.response.data) as DataVolume + setDataVolume?.(temp) + return temp + } catch (err) { + notification?.error({ message: `Failed to update data volume [Namespace: ${dv.metadata!.namespace}, Name: ${dv.metadata!.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} + +export const createDataVolume = async (dv: DataVolume, setDataVolume?: React.Dispatch>, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const result = await resourceClient.create({ + resourceType: ResourceType.DATA_VOLUME, + data: JSON.stringify(dv) }) - }) + const output = JSON.parse(result.response.data) as DataVolume + setDataVolume?.(output) + return output + } catch (err) { + notification?.error({ message: `Failed to create data volume [Namespace: ${dv.metadata!.namespace}, Name: ${dv.metadata!.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } } diff --git a/src/clients/event.ts b/src/clients/event.ts index a4e5d6d..c980b86 100644 --- a/src/clients/event.ts +++ b/src/clients/event.ts @@ -2,99 +2,199 @@ import { defaultTimeout, resourceWatchClient } from "./clients" import { NamespaceName, ResourceType } from "./ts/types/types" import { EventType, WatchOptions } from "./ts/management/resource/v1alpha1/watch" import { namespaceNameKey } from "@/utils/k8s" -import { isAbortedError } from "@/utils/utils" +import { getErrorMessage, isAbortedError } from "@/utils/utils" +import { NotificationInstance } from "antd/lib/notification/interface" -export const watchVirtualMachineEvents = (ns: NamespaceName, setEvents: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal): Promise => { - return new Promise((resolve, reject) => { - setLoading(true) +// export const watchVirtualMachineEvents = (ns: NamespaceName, setEvents: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal): Promise => { +// return new Promise((resolve, reject) => { +// setLoading(true) - const map = new Map() +// const map = new Map() - const call = resourceWatchClient.watch({ - resourceType: ResourceType.EVENT, - options: WatchOptions.create({ - fieldSelectorGroup: { - operator: "&&", - fieldSelectors: [ - { - fieldPath: "involvedObject.namespace", - operator: "=", - values: [ns.namespace] - }, - { - fieldPath: "involvedObject.kind", - operator: "~=", - values: ["VirtualMachine", "VirtualMachineInstance", "Pod"] - }, - { - fieldPath: "involvedObject.name", - operator: "=", - values: [ns.name] - } - ] - } - }) - }, { abort: abortSignal }) +// const call = resourceWatchClient.watch({ +// resourceType: ResourceType.EVENT, +// options: WatchOptions.create({ +// fieldSelectorGroup: { +// operator: "&&", +// fieldSelectors: [ +// { +// fieldPath: "involvedObject.namespace", +// operator: "=", +// values: [ns.namespace] +// }, +// { +// fieldPath: "involvedObject.kind", +// operator: "~=", +// values: ["VirtualMachine", "VirtualMachineInstance", "Pod"] +// }, +// { +// fieldPath: "involvedObject.name", +// operator: "=", +// values: [ns.name] +// } +// ] +// } +// }) +// }, { abort: abortSignal }) + +// let timeoutId: NodeJS.Timeout | null = null +// const updateEvents = () => { +// if (map.size === 0 && timeoutId === null) { +// timeoutId = setTimeout(() => { +// const items = Array.from(map.values()).sort((a: any, b: any) => { +// return new Date(b.lastTimestamp).getTime() - new Date(a.lastTimestamp).getTime() +// }) +// setEvents(items.length > 0 ? items : undefined) +// timeoutId = null +// }, defaultTimeout) +// } else { +// const items = Array.from(map.values()).sort((a: any, b: any) => { +// return new Date(b.lastTimestamp).getTime() - new Date(a.lastTimestamp).getTime() +// }) +// setEvents(items.length > 0 ? items : undefined) +// if (timeoutId !== null) { +// clearTimeout(timeoutId) +// timeoutId = null +// } +// } +// } + +// call.responses.onMessage((response) => { +// switch (response.eventType) { +// case EventType.READY: { +// setLoading(false) +// break +// } +// case EventType.ADDED: +// case EventType.MODIFIED: { +// response.items.forEach((data) => { +// const e = JSON.parse(data) +// map.set(namespaceNameKey(e), e) +// }) +// break +// } +// case EventType.DELETED: { +// response.items.forEach((data) => { +// const vm = JSON.parse(data) +// map.delete(namespaceNameKey(vm)) +// }) +// break +// } +// } +// updateEvents() +// }) + +// call.responses.onError((err: Error) => { +// setLoading(false) +// if (isAbortedError(err)) { +// resolve() +// } else { +// reject(new Error(`Error in watch stream for Event: ${err.message}`)) +// } +// }) + +// call.responses.onComplete(() => { +// setLoading(false) +// resolve() +// }) +// }) +// } - let timeoutId: NodeJS.Timeout | null = null - const updateEvents = () => { - if (map.size === 0 && timeoutId === null) { - timeoutId = setTimeout(() => { +export const watchVirtualMachineEvents = async (ns: NamespaceName, setEvents: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal, notification?: NotificationInstance): Promise => { + setLoading(true) + try { + await new Promise((resolve, reject) => { + const map = new Map() + + const call = resourceWatchClient.watch({ + resourceType: ResourceType.EVENT, + options: WatchOptions.create({ + fieldSelectorGroup: { + operator: "&&", + fieldSelectors: [ + { + fieldPath: "involvedObject.namespace", + operator: "=", + values: [ns.namespace] + }, + { + fieldPath: "involvedObject.kind", + operator: "~=", + values: ["VirtualMachine", "VirtualMachineInstance", "Pod"] + }, + { + fieldPath: "involvedObject.name", + operator: "=", + values: [ns.name] + } + ] + } + }) + }, { abort: abortSignal }) + + let timeoutId: NodeJS.Timeout | null = null + const updateEvents = () => { + if (map.size === 0 && timeoutId === null) { + timeoutId = setTimeout(() => { + const items = Array.from(map.values()).sort((a: any, b: any) => { + return new Date(b.lastTimestamp).getTime() - new Date(a.lastTimestamp).getTime() + }) + setEvents(items.length > 0 ? items : undefined) + timeoutId = null + }, defaultTimeout) + } else { const items = Array.from(map.values()).sort((a: any, b: any) => { return new Date(b.lastTimestamp).getTime() - new Date(a.lastTimestamp).getTime() }) setEvents(items.length > 0 ? items : undefined) - timeoutId = null - }, defaultTimeout) - } else { - const items = Array.from(map.values()).sort((a: any, b: any) => { - return new Date(b.lastTimestamp).getTime() - new Date(a.lastTimestamp).getTime() - }) - setEvents(items.length > 0 ? items : undefined) - if (timeoutId !== null) { - clearTimeout(timeoutId) - timeoutId = null + if (timeoutId !== null) { + clearTimeout(timeoutId) + timeoutId = null + } } } - } - call.responses.onMessage((response) => { - switch (response.eventType) { - case EventType.READY: { - setLoading(false) - break - } - case EventType.ADDED: - case EventType.MODIFIED: { - response.items.forEach((data) => { - const e = JSON.parse(data) - map.set(namespaceNameKey(e), e) - }) - break + call.responses.onMessage((response) => { + switch (response.eventType) { + case EventType.READY: { + resolve() + break + } + case EventType.ADDED: + case EventType.MODIFIED: { + response.items.forEach((data) => { + const e = JSON.parse(data) + map.set(namespaceNameKey(e), e) + }) + break + } + case EventType.DELETED: { + response.items.forEach((data) => { + const vm = JSON.parse(data) + map.delete(namespaceNameKey(vm)) + }) + break + } } - case EventType.DELETED: { - response.items.forEach((data) => { - const vm = JSON.parse(data) - map.delete(namespaceNameKey(vm)) - }) - break + updateEvents() + }) + + call.responses.onError((err: Error) => { + if (isAbortedError(err)) { + resolve() + } else { + reject(new Error(`Error in watch stream for Event: ${err.message}`)) } - } - updateEvents() - }) + }) - call.responses.onError((err: Error) => { - setLoading(false) - if (isAbortedError(err)) { + call.responses.onComplete(() => { resolve() - } else { - reject(new Error(`Error in watch stream for Event: ${err.message}`)) - } - }) - - call.responses.onComplete(() => { - setLoading(false) - resolve() + }) }) - }) + } catch (err) { + notification?.error({ message: `Failed to watch events [Namespace: ${ns.namespace}, Name: ${ns.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading(false) + } } diff --git a/src/clients/ippool.ts b/src/clients/ippool.ts index 144bc6b..8372531 100644 --- a/src/clients/ippool.ts +++ b/src/clients/ippool.ts @@ -1,3 +1,246 @@ +import { NotificationInstance } from "antd/lib/notification/interface" +import { ListOptions } from "./ts/management/resource/v1alpha1/resource" import { components } from "./ts/openapi/openapi-schema" +import { defaultTimeout, resourceClient, resourceWatchClient } from "./clients" +import { NamespaceName, ResourceType } from "./ts/types/types" +import { getErrorMessage, isAbortedError } from "@/utils/utils" +import { EventType, WatchOptions } from "./ts/management/resource/v1alpha1/watch" +import { namespaceNameKey } from "@/utils/k8s" export type IPPool = components["schemas"]["v1IPPool"] + +export const listIPPools = async (setIPPools?: React.Dispatch>, opts?: ListOptions, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const result = await resourceClient.list({ + resourceType: ResourceType.IPPOOL, + options: ListOptions.create(opts) + }) + let items: IPPool[] = [] + result.response.items.forEach((item: string) => { + items.push(JSON.parse(item) as IPPool) + }) + setIPPools?.(items.length > 0 ? items : undefined) + return items + } catch (err) { + notification?.error({ message: `Failed to list ippool`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} + +export const deleteIPPools = async (ippools: IPPool[], setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const completed: IPPool[] = [] + const failed: { ippool: IPPool; error: any }[] = [] + + await Promise.all(ippools.map(async (ippool) => { + const namespace = ippool.metadata!.namespace + const name = ippool.metadata!.name + + try { + await resourceClient.delete({ + namespaceName: { namespace, name }, + resourceType: ResourceType.IPPOOL + }).response + completed.push(ippool) + } catch (err: any) { + failed.push({ ippool, error: err }) + } + })) + + if (failed.length > 0) { + const errorMessages = failed.map(({ ippool, error }) => `Namespace: ${ippool.metadata!.namespace ?? "unknown"}, Name: ${ippool.metadata!.name ?? "unknown"}, Error: ${error.message}`).join("\n") + throw new Error(`Failed to delete the following ippools:\n${errorMessages}`) + } + } catch (err) { + notification?.error({ message: `Failed to delete ippools:`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} + +export const deleteIPPool = async (ns: NamespaceName, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + await resourceClient.delete({ + resourceType: ResourceType.IPPOOL, + namespaceName: ns + }) + } catch (err) { + notification?.error({ message: `Failed to delete ippool [Namespace: ${ns.namespace}, Name: ${ns.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} + +export const watchIPPools = async (setIPPools: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal, opts?: WatchOptions, notification?: NotificationInstance): Promise => { + setLoading(true) + try { + await new Promise((resolve, reject) => { + const map = new Map() + + const call = resourceWatchClient.watch({ + resourceType: ResourceType.IPPOOL, + options: WatchOptions.create(opts) + }, { abort: abortSignal }) + + let timeoutId: NodeJS.Timeout | null = null + const updateIPPools = () => { + if (map.size === 0 && timeoutId === null) { + timeoutId = setTimeout(() => { + const items = Array.from(map.values()) + setIPPools(items.length > 0 ? items : undefined) + timeoutId = null + }, defaultTimeout) + } else { + const items = Array.from(map.values()) + setIPPools(items.length > 0 ? items : undefined) + if (timeoutId !== null) { + clearTimeout(timeoutId) + timeoutId = null + } + } + } + + call.responses.onMessage((response) => { + switch (response.eventType) { + case EventType.READY: { + resolve() + break + } + case EventType.ADDED: + case EventType.MODIFIED: { + response.items.forEach((data) => { + const ippool = JSON.parse(data) as IPPool + map.set(namespaceNameKey(ippool), ippool) + }) + break + } + case EventType.DELETED: { + response.items.forEach((data) => { + const ippool = JSON.parse(data) as IPPool + map.delete(namespaceNameKey(ippool)) + }) + break + } + } + updateIPPools() + }) + + call.responses.onError((err: Error) => { + if (isAbortedError(err)) { + resolve() + } else { + reject(new Error(`Error in watch stream for IPPool: ${err.message}`)) + } + }) + + call.responses.onComplete(() => { + resolve() + }) + }) + } catch (err) { + notification?.error({ message: `Failed to watch IPPool`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading(false) + } +} + +export const watchIPPool = async (ns: NamespaceName, setIPPool: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal, notification?: NotificationInstance): Promise => { + setLoading(true) + try { + await new Promise((resolve, reject) => { + const call = resourceWatchClient.watch({ + resourceType: ResourceType.IPPOOL, + options: WatchOptions.create({ + fieldSelectorGroup: { + operator: "&&", + fieldSelectors: [ + { fieldPath: "metadata.name", operator: "=", values: [ns.name] } + ] + } + }) + }, { abort: abortSignal }) + + call.responses.onMessage((response) => { + switch (response.eventType) { + case EventType.READY: { + resolve() + break + } + case EventType.ADDED: + case EventType.MODIFIED: { + if (response.items.length === 0) { + return + } + setIPPool(JSON.parse(response.items[0]) as IPPool) + break + } + case EventType.DELETED: { + setIPPool(undefined) + break + } + } + }) + + call.responses.onError((err: Error) => { + if (!isAbortedError(err)) { + reject(new Error(`Error in watch stream for IPPool: ${err.message}`)) + } else { + resolve() + } + }) + + call.responses.onComplete(() => { + resolve() + }) + }) + } catch (err) { + notification?.error({ message: `Failed to watch IPPool`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading(false) + } +} + +export const createIPPool = async (ippool: IPPool, setIPPool?: React.Dispatch>, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const result = await resourceClient.create({ + resourceType: ResourceType.IPPOOL, + data: JSON.stringify(ippool) + }) + const output = JSON.parse(result.response.data) as IPPool + setIPPool?.(output) + return output + } catch (err) { + notification?.error({ message: `Failed to create ip pool [Namespace: ${ippool.metadata!.namespace}, Name: ${ippool.metadata!.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} + +export const updateIPPool = async (ippool: IPPool, setIPPool?: React.Dispatch>, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const result = await resourceClient.update({ + resourceType: ResourceType.IPPOOL, + data: JSON.stringify(ippool) + }) + const temp = JSON.parse(result.response.data) as IPPool + setIPPool?.(temp) + return temp + } catch (err) { + notification?.error({ message: `Failed to update ip pool [Name: ${ippool.metadata!.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} diff --git a/src/clients/multus.ts b/src/clients/multus.ts new file mode 100644 index 0000000..bcdcd5f --- /dev/null +++ b/src/clients/multus.ts @@ -0,0 +1,280 @@ +import { NotificationInstance } from "antd/lib/notification/interface" +import { defaultTimeout, resourceClient, resourceWatchClient } from "./clients" +import { components } from "./ts/openapi/openapi-schema" +import { NamespaceName, ResourceType } from "./ts/types/types" +import { getErrorMessage, isAbortedError } from "@/utils/utils" +import { ListOptions } from "./ts/management/resource/v1alpha1/resource" +import { EventType, WatchOptions } from "./ts/management/resource/v1alpha1/watch" +import { namespaceNameKey } from "@/utils/k8s" + +export type Multus = components["schemas"]["v1NetworkAttachmentDefinition"] + +// export const getMultus = async (ns: NamespaceName): Promise => { +// return new Promise((resolve, reject) => { +// const call = resourceClient.get({ +// resourceType: ResourceType.MULTUS, +// namespaceName: ns +// }) +// call.then((result) => { +// resolve(JSON.parse(result.response.data) as Multus) +// }) +// call.response.catch((err: Error) => { +// reject(new Error(`Failed to get data NetworkAttachmentDefinition [Namespace: ${ns.namespace}, Name: ${ns.name}]: ${err.message}`)) +// }) +// }) +// } + +export const getMultus = async (ns: NamespaceName, setMultus?: React.Dispatch>, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const result = await resourceClient.get({ + resourceType: ResourceType.MULTUS, + namespaceName: ns + }) + const output = JSON.parse(result.response.data) as Multus + setMultus?.(output) + return output + } catch (err) { + notification?.error({ message: `Failed to get data NetworkAttachmentDefinition [Namespace: ${ns.namespace}, Name: ${ns.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} + +export const listMultus = async (setMultus?: React.Dispatch>, opts?: ListOptions, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const result = await resourceClient.list({ + resourceType: ResourceType.MULTUS, + options: ListOptions.create(opts) + }) + let items: Multus[] = [] + result.response.items.forEach((item: string) => { + items.push(JSON.parse(item) as Multus) + }) + setMultus?.(items.length > 0 ? items : undefined) + return items + } catch (err) { + notification?.error({ message: `Failed to list multus`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} + +export const deleteMultuses = async (multuses: Multus[], setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const completed: Multus[] = [] + const failed: { multus: Multus; error: any }[] = [] + + await Promise.all(multuses.map(async (multus) => { + const namespace = multus.metadata!.namespace + const name = multus.metadata!.name + + try { + await resourceClient.delete({ + namespaceName: { namespace, name }, + resourceType: ResourceType.SUBNET + }).response + completed.push(multus) + } catch (err: any) { + failed.push({ multus, error: err }) + } + })) + + if (failed.length > 0) { + const errorMessages = failed.map(({ multus, error }) => `Namespace: ${multus.metadata!.namespace ?? "unknown"}, Name: ${multus.metadata!.name ?? "unknown"}, Error: ${error.message}`).join("\n") + throw new Error(`Failed to delete the following multuses:\n${errorMessages}`) + } + } catch (err) { + notification?.error({ message: `Failed to delete multuses:`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} + +export const deleteMultus = async (ns: NamespaceName, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + await resourceClient.delete({ + resourceType: ResourceType.MULTUS, + namespaceName: ns + }) + } catch (err) { + notification?.error({ message: `Failed to delete multus [Namespace: ${ns.namespace}, Name: ${ns.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} + +export const watchMultuses = async (setMultuses: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal, opts?: WatchOptions, notification?: NotificationInstance): Promise => { + setLoading(true) + try { + await new Promise((resolve, reject) => { + const map = new Map() + + const call = resourceWatchClient.watch({ + resourceType: ResourceType.MULTUS, + options: WatchOptions.create(opts) + }, { abort: abortSignal }) + + let timeoutId: NodeJS.Timeout | null = null + const updateMultuses = () => { + if (map.size === 0 && timeoutId === null) { + timeoutId = setTimeout(() => { + const items = Array.from(map.values()) + setMultuses(items.length > 0 ? items : undefined) + timeoutId = null + }, defaultTimeout) + } else { + const items = Array.from(map.values()) + setMultuses(items.length > 0 ? items : undefined) + if (timeoutId !== null) { + clearTimeout(timeoutId) + timeoutId = null + } + } + } + + call.responses.onMessage((response) => { + switch (response.eventType) { + case EventType.READY: { + resolve() + break + } + case EventType.ADDED: + case EventType.MODIFIED: { + response.items.forEach((data) => { + const multus = JSON.parse(data) as Multus + map.set(namespaceNameKey(multus), multus) + }) + break + } + case EventType.DELETED: { + response.items.forEach((data) => { + const multus = JSON.parse(data) as Multus + map.delete(namespaceNameKey(multus)) + }) + break + } + } + updateMultuses() + }) + + call.responses.onError((err: Error) => { + if (isAbortedError(err)) { + resolve() + } else { + reject(new Error(`Error in watch stream for Multus: ${err.message}`)) + } + }) + + call.responses.onComplete(() => { + resolve() + }) + }) + } catch (err) { + notification?.error({ message: `Failed to watch Multus`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading(false) + } +} + +export const watchMultus = async (ns: NamespaceName, setMultus: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal, notification?: NotificationInstance): Promise => { + setLoading(true) + try { + await new Promise((resolve, reject) => { + const call = resourceWatchClient.watch({ + resourceType: ResourceType.MULTUS, + options: WatchOptions.create({ + fieldSelectorGroup: { + operator: "&&", + fieldSelectors: [ + { fieldPath: "metadata.name", operator: "=", values: [ns.name] }, + { fieldPath: "metadata.namespace", operator: "=", values: [ns.namespace] } + ] + } + }) + }, { abort: abortSignal }) + + call.responses.onMessage((response) => { + switch (response.eventType) { + case EventType.READY: { + resolve() + break + } + case EventType.ADDED: + case EventType.MODIFIED: { + if (response.items.length === 0) { + return + } + setMultus(JSON.parse(response.items[0]) as Multus) + break + } + case EventType.DELETED: { + setMultus(undefined) + break + } + } + }) + + call.responses.onError((err: Error) => { + if (!isAbortedError(err)) { + reject(new Error(`Error in watch stream for Multus: ${err.message}`)) + } else { + resolve() + } + }) + + call.responses.onComplete(() => { + resolve() + }) + }) + } catch (err) { + notification?.error({ message: `Failed to watch Multus`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading(false) + } +} + +export const createMultus = async (multus: Multus, setMultus?: React.Dispatch>, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const result = await resourceClient.create({ + resourceType: ResourceType.MULTUS, + data: JSON.stringify(multus) + }) + const output = JSON.parse(result.response.data) as Multus + setMultus?.(output) + return output + } catch (err) { + notification?.error({ message: `Failed to create multus [Namespace: ${multus.metadata!.namespace}, Name: ${multus.metadata!.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} + +export const updateMultus = async (multus: Multus, setMultus?: React.Dispatch>, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const result = await resourceClient.update({ + resourceType: ResourceType.MULTUS, + data: JSON.stringify(multus) + }) + const temp = JSON.parse(result.response.data) as Multus + setMultus?.(temp) + return temp + } catch (err) { + notification?.error({ message: `Failed to update multus [Namespace: ${multus.metadata!.namespace}, Name: ${multus.metadata!.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} diff --git a/src/clients/namespace.ts b/src/clients/namespace.ts new file mode 100644 index 0000000..9afcf55 --- /dev/null +++ b/src/clients/namespace.ts @@ -0,0 +1,86 @@ +import { NotificationInstance } from "antd/lib/notification/interface" +import { defaultTimeout, resourceWatchClient } from "./clients" +import { ResourceType } from "./ts/types/types" +import { getErrorMessage, isAbortedError } from "@/utils/utils" +import { EventType, WatchOptions } from "./ts/management/resource/v1alpha1/watch" +import { namespaceNameKey } from "@/utils/k8s" + +export interface Namespace { + metadata: { + name: string + } +} + +export const watchNamespaces = async (setNamespaces: React.Dispatch>, abortSignal: AbortSignal, opts?: WatchOptions, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + await new Promise((resolve, reject) => { + const map = new Map() + + const call = resourceWatchClient.watch({ + resourceType: ResourceType.NAMESPACE, + options: WatchOptions.create(opts) + }, { abort: abortSignal }) + + let timeoutId: NodeJS.Timeout | null = null + const updateNamespaces = () => { + if (map.size === 0 && timeoutId === null) { + timeoutId = setTimeout(() => { + const items = Array.from(map.values()) + setNamespaces(items.length > 0 ? items : undefined) + timeoutId = null + }, defaultTimeout) + } else { + const items = Array.from(map.values()) + setNamespaces(items.length > 0 ? items : undefined) + if (timeoutId !== null) { + clearTimeout(timeoutId) + timeoutId = null + } + } + } + + call.responses.onMessage((response) => { + switch (response.eventType) { + case EventType.READY: { + resolve() + break + } + case EventType.ADDED: + case EventType.MODIFIED: { + response.items.forEach((data) => { + const ns = JSON.parse(data) as Namespace + map.set(namespaceNameKey(ns), ns) + }) + break + } + case EventType.DELETED: { + response.items.forEach((data) => { + const ns = JSON.parse(data) as Namespace + map.delete(namespaceNameKey(ns)) + }) + break + } + } + updateNamespaces() + }) + + call.responses.onError((err: Error) => { + if (isAbortedError(err)) { + resolve() + } else { + reject(new Error(`Error in watch stream for Namespace: ${err.message}`)) + } + }) + + call.responses.onComplete(() => { + resolve() + }) + }) + } catch (err) { + notification?.error({ message: `Failed to watch Namespace`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} diff --git a/src/clients/network-attachment-definition.ts b/src/clients/network-attachment-definition.ts deleted file mode 100644 index 5104948..0000000 --- a/src/clients/network-attachment-definition.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { components } from "./ts/openapi/openapi-schema" - -export type NetworkAttachmentDefinition = components["schemas"]["v1NetworkAttachmentDefinition"] diff --git a/src/clients/storage-class.ts b/src/clients/storage-class.ts new file mode 100644 index 0000000..769e88e --- /dev/null +++ b/src/clients/storage-class.ts @@ -0,0 +1,33 @@ +import { NotificationInstance } from "antd/lib/notification/interface" +import { ListOptions } from "./ts/management/resource/v1alpha1/resource" +import { resourceClient } from "./clients" +import { ResourceType } from "./ts/types/types" +import { getErrorMessage } from "@/utils/utils" + +export interface StorageClass { + metadata: { + name: string + namespace: string + } +} + +export const listStorageClass = async (setStorageClass?: React.Dispatch>, opts?: ListOptions, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const result = await resourceClient.list({ + resourceType: ResourceType.STORAGE_CLASS, + options: ListOptions.create(opts) + }) + let items: StorageClass[] = [] + result.response.items.forEach((item: string) => { + items.push(JSON.parse(item) as StorageClass) + }) + setStorageClass?.(items.length > 0 ? items : undefined) + return items + } catch (err) { + notification?.error({ message: `Failed to list storage class`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} diff --git a/src/clients/subnet.ts b/src/clients/subnet.ts index 8953697..8e61955 100644 --- a/src/clients/subnet.ts +++ b/src/clients/subnet.ts @@ -1,38 +1,191 @@ -import { namespaceNameKey } from "@/utils/k8s" -import { resourceClient } from "./clients" +import { namespaceNameKey, parseNamespaceNameKey } from "@/utils/k8s" +import { defaultTimeout, resourceClient, resourceWatchClient } from "./clients" import { ListOptions } from "./ts/management/resource/v1alpha1/resource" import { components } from "./ts/openapi/openapi-schema" -import { FieldSelector, ResourceType } from "./ts/types/types" +import { FieldSelector, NamespaceName, ResourceType } from "./ts/types/types" import { VirtualMachineSummary } from "./virtual-machine-summary" import { IP } from "./ip" import { defaultNetworkAnno } from "@/pages/compute/machine/virtualmachine" -import { generateKubeovnNetworkAnnon } from "@/utils/utils" +import { generateKubeovnNetworkAnnon, getErrorMessage, isAbortedError } from "@/utils/utils" +import { NotificationInstance } from "antd/lib/notification/interface" +import { Multus } from "./multus" +import { IPPool } from "./ippool" +import { EventType, WatchOptions } from "./ts/management/resource/v1alpha1/watch" export type Subnet = components["schemas"]["v1Subnet"] -export type VirtualMachineNetworkDataSourceType = { - name: string +export type VirtualMachineNetworkType = { + name?: string default: boolean network: string interface: string - multus: string - vpc: string - subnet: string - ippool: string - ipAddress: string - macAddress: string + multus?: NamespaceName | Multus + vpc?: string + subnet?: string | Subnet + ippool?: string | IPPool + ipAddress?: string + macAddress?: string } -export const listSubnetsForVirtualMachine = async (summary: VirtualMachineSummary): Promise => { - return new Promise(async (resolve, reject) => { +// export const getSubnet = async (ns: NamespaceName): Promise => { +// return new Promise((resolve, reject) => { +// const call = resourceClient.get({ +// resourceType: ResourceType.SUBNET, +// namespaceName: ns +// }) +// call.then((result) => { +// resolve(JSON.parse(result.response.data) as Subnet) +// }) +// call.response.catch((err: Error) => { +// reject(new Error(`Failed to get data Subnet [Name: ${ns.name}]: ${err.message}`)) +// }) +// }) +// } + +export const getSubnet = async (ns: NamespaceName, setSubnet?: React.Dispatch>, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const result = await resourceClient.get({ + resourceType: ResourceType.SUBNET, + namespaceName: ns + }) + const output = JSON.parse(result.response.data) as Subnet + setSubnet?.(output) + return output + } catch (err) { + notification?.error({ message: `Failed to get subnet [Name: ${ns.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} + +export const listSubnets = async (setMultus: React.Dispatch>, opts?: ListOptions, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const result = await resourceClient.list({ + resourceType: ResourceType.SUBNET, + options: ListOptions.create(opts) + }) + let items: Subnet[] = [] + result.response.items.forEach((item: string) => { + items.push(JSON.parse(item) as Subnet) + }) + setMultus(items.length > 0 ? items : undefined) + return items + } catch (err) { + notification?.error({ message: `Failed to list subnet`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} + +// export const listVirtualMachineNetwork = async (summary: VirtualMachineSummary): Promise => { +// return new Promise(async (resolve, reject) => { +// const interfaces = summary.status?.virtualMachine?.spec?.template?.spec?.domain?.devices?.interfaces +// if (!interfaces) { +// resolve([]) +// return +// } + +// const interfacesMap = new Map( +// interfaces.map((item: any) => [item.name, item]) +// ) + +// const ips = summary.status?.network?.ips +// const ipsMap = new Map( +// ips?.map((item) => { +// const arr = (item.metadata!.name as string).split(".") +// if (arr && arr.length >= 3) { +// return [`${arr[1]}/${arr[2]}`, item] +// } +// return [namespaceNameKey(item), item as IP] +// }) +// ) + +// const subnetSelectors: FieldSelector[] = [] +// ipsMap.forEach(ip => { +// const subnet = ip.spec?.subnet +// if (subnet) { +// subnetSelectors.push({ fieldPath: "metadata.name", operator: "=", values: [subnet] }) +// } +// }) + +// const subnetMap = new Map() + +// try { +// const response = await resourceClient.list({ +// resourceType: ResourceType.SUBNET, +// options: ListOptions.create({ fieldSelectorGroup: { operator: "||", fieldSelectors: subnetSelectors } }) +// }) +// response.response.items.forEach((item) => { +// const subnet = JSON.parse(item) as Subnet +// subnetMap.set(subnet.metadata!.name!, subnet) +// }) +// } catch (err: any) { +// return reject(new Error(`Failed to list subnets: ${err.message}`)) +// } + +// const networks = summary.status?.virtualMachine?.spec?.template?.spec?.networks || [] +// const data: (VirtualMachineNetworkType | null)[] = await Promise.all(networks.map(async (item) => { +// const inter = interfacesMap.get(item.name) +// if (!inter) { +// return null +// } + +// let ipobj: IP | undefined = undefined +// let ippoolName: string = "" +// let multus = item.multus?.networkName || summary.status?.virtualMachine?.spec?.template?.metadata?.annotations?.[defaultNetworkAnno] +// if (multus) { +// const temp = ipsMap.get(multus) +// if (temp) { +// ipobj = temp +// } +// const annoValue = summary.status?.virtualMachine?.spec?.template?.metadata?.annotations?.[generateKubeovnNetworkAnnon(multus, "ip_pool")] +// if (annoValue) { +// ippoolName = annoValue +// } +// } + +// let subnet: Subnet | undefined = undefined +// if (ipobj) { +// const name = ipobj.spec?.subnet +// if (name) { +// subnet = subnetMap.get(name) +// } +// } + +// const ds: VirtualMachineNetworkType = { +// name: item.name, +// default: item.pod ? true : item.multus?.default ? true : false, +// network: item.multus ? "multus" : "pod", +// interface: inter.bridge ? "bridge" : inter.masquerade ? "masquerade" : inter.sriov ? "sriov" : inter.slirp ? "slirp" : "", +// multus: multus || "", +// vpc: subnet?.spec?.vpc || "", +// subnet: ipobj?.spec?.subnet || "", +// ippool: ippoolName, +// ipAddress: ipobj?.spec?.ipAddress || "", +// macAddress: ipobj?.spec?.macAddress || "" +// } +// return ds +// })) + +// return resolve(data.filter((item): item is VirtualMachineNetworkType => item !== null)) +// }) +// } + +export const listVirtualMachineNetwork = async (summary: VirtualMachineSummary, setVirtualMachineNetwork?: React.Dispatch>, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { const interfaces = summary.status?.virtualMachine?.spec?.template?.spec?.domain?.devices?.interfaces if (!interfaces) { - resolve([]) - return + setVirtualMachineNetwork?.(undefined) + return [] } const interfacesMap = new Map( - interfaces.map((item: any) => [item.name, item]) + interfaces.map((item) => [item.name, item]) ) const ips = summary.status?.network?.ips @@ -66,11 +219,11 @@ export const listSubnetsForVirtualMachine = async (summary: VirtualMachineSummar subnetMap.set(subnet.metadata!.name!, subnet) }) } catch (err: any) { - return reject(new Error(`Failed to list subnets: ${err.message}`)) + throw new Error(`Failed to list subnets: ${err.message}`) } const networks = summary.status?.virtualMachine?.spec?.template?.spec?.networks || [] - const data: (VirtualMachineNetworkDataSourceType | null)[] = await Promise.all(networks.map(async (item) => { + const data: (VirtualMachineNetworkType | null)[] = await Promise.all(networks.map(async (item) => { const inter = interfacesMap.get(item.name) if (!inter) { return null @@ -78,13 +231,13 @@ export const listSubnetsForVirtualMachine = async (summary: VirtualMachineSummar let ipobj: IP | undefined = undefined let ippoolName: string = "" - let multus = item.multus?.networkName || summary.status?.virtualMachine?.spec?.template?.metadata?.annotations?.[defaultNetworkAnno] - if (multus) { - const temp = ipsMap.get(multus) + let multusNsKey = item.multus?.networkName || summary.status?.virtualMachine?.spec?.template?.metadata?.annotations?.[defaultNetworkAnno] + if (multusNsKey) { + const temp = ipsMap.get(multusNsKey) if (temp) { ipobj = temp } - const annoValue = summary.status?.virtualMachine?.spec?.template?.metadata?.annotations?.[generateKubeovnNetworkAnnon(multus, "ip_pool")] + const annoValue = summary.status?.virtualMachine?.spec?.template?.metadata?.annotations?.[generateKubeovnNetworkAnnon(multusNsKey, "ip_pool")] if (annoValue) { ippoolName = annoValue } @@ -98,21 +251,241 @@ export const listSubnetsForVirtualMachine = async (summary: VirtualMachineSummar } } - const ds: VirtualMachineNetworkDataSourceType = { + const ds: VirtualMachineNetworkType = { name: item.name, default: item.pod ? true : item.multus?.default ? true : false, network: item.multus ? "multus" : "pod", interface: inter.bridge ? "bridge" : inter.masquerade ? "masquerade" : inter.sriov ? "sriov" : inter.slirp ? "slirp" : "", - multus: multus || "", - vpc: subnet?.spec?.vpc || "", - subnet: ipobj?.spec?.subnet || "", + multus: parseNamespaceNameKey(multusNsKey), + vpc: subnet?.spec?.vpc, + subnet: ipobj?.spec?.subnet, ippool: ippoolName, - ipAddress: ipobj?.spec?.ipAddress || "", - macAddress: ipobj?.spec?.macAddress || "" + ipAddress: ipobj?.spec?.ipAddress, + macAddress: ipobj?.spec?.macAddress } return ds })) + const output = data.filter((item): item is VirtualMachineNetworkType => item !== null) + setVirtualMachineNetwork?.(output) + return output + } catch (err) { + notification?.error({ message: `Failed to list virtual machine network`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} + +export const watchSubnets = async (setSubnets: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal, opts?: WatchOptions, notification?: NotificationInstance): Promise => { + setLoading(true) + try { + await new Promise((resolve, reject) => { + const map = new Map() + + const call = resourceWatchClient.watch({ + resourceType: ResourceType.SUBNET, + options: WatchOptions.create(opts) + }, { abort: abortSignal }) + + let timeoutId: NodeJS.Timeout | null = null + const updateSubnets = () => { + if (map.size === 0 && timeoutId === null) { + timeoutId = setTimeout(() => { + const items = Array.from(map.values()) + setSubnets(items.length > 0 ? items : undefined) + timeoutId = null + }, defaultTimeout) + } else { + const items = Array.from(map.values()) + setSubnets(items.length > 0 ? items : undefined) + if (timeoutId !== null) { + clearTimeout(timeoutId) + timeoutId = null + } + } + } + + call.responses.onMessage((response) => { + switch (response.eventType) { + case EventType.READY: { + resolve() + break + } + case EventType.ADDED: + case EventType.MODIFIED: { + response.items.forEach((data) => { + const subnet = JSON.parse(data) as Subnet + map.set(namespaceNameKey(subnet), subnet) + }) + break + } + case EventType.DELETED: { + response.items.forEach((data) => { + const subnet = JSON.parse(data) as Subnet + map.delete(namespaceNameKey(subnet)) + }) + break + } + } + updateSubnets() + }) + + call.responses.onError((err: Error) => { + if (isAbortedError(err)) { + resolve() + } else { + reject(new Error(`Error in watch stream for Subnet: ${err.message}`)) + } + }) - return resolve(data.filter((item): item is VirtualMachineNetworkDataSourceType => item !== null)) - }) + call.responses.onComplete(() => { + resolve() + }) + }) + } catch (err) { + notification?.error({ message: `Failed to watch Subnet`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading(false) + } +} + +export const watchSubnet = async (ns: NamespaceName, setSubnet: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal, notification?: NotificationInstance): Promise => { + setLoading(true) + try { + await new Promise((resolve, reject) => { + const call = resourceWatchClient.watch({ + resourceType: ResourceType.SUBNET, + options: WatchOptions.create({ + fieldSelectorGroup: { + operator: "&&", + fieldSelectors: [ + { fieldPath: "metadata.name", operator: "=", values: [ns.name] } + ] + } + }) + }, { abort: abortSignal }) + + call.responses.onMessage((response) => { + switch (response.eventType) { + case EventType.READY: { + resolve() + break + } + case EventType.ADDED: + case EventType.MODIFIED: { + if (response.items.length === 0) { + return + } + setSubnet(JSON.parse(response.items[0]) as Subnet) + break + } + case EventType.DELETED: { + setSubnet(undefined) + break + } + } + }) + + call.responses.onError((err: Error) => { + if (!isAbortedError(err)) { + reject(new Error(`Error in watch stream for IPPool: ${err.message}`)) + } else { + resolve() + } + }) + + call.responses.onComplete(() => { + resolve() + }) + }) + } catch (err) { + notification?.error({ message: `Failed to watch IPPool`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading(false) + } +} + +export const deleteSubnets = async (subnets: Subnet[], setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const completed: Subnet[] = [] + const failed: { subnet: Subnet; error: any }[] = [] + + await Promise.all(subnets.map(async (subnet) => { + const name = subnet.metadata!.name! + + try { + await resourceClient.delete({ + namespaceName: { namespace: "", name: name }, + resourceType: ResourceType.SUBNET + }).response + completed.push(subnet) + } catch (err: any) { + failed.push({ subnet, error: err }) + } + })) + + if (failed.length > 0) { + const errorMessages = failed.map(({ subnet, error }) => `Name: ${subnet.metadata!.name ?? "unknown"}, Error: ${error.message}`).join("\n") + throw new Error(`Failed to delete the following subnets:\n${errorMessages}`) + } + } catch (err) { + notification?.error({ message: `Failed to delete subnets:`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} + +export const deleteSubnet = async (ns: NamespaceName, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + await resourceClient.delete({ + resourceType: ResourceType.SUBNET, + namespaceName: ns + }) + } catch (err) { + notification?.error({ message: `Failed to delete subnet [Name: ${ns.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} + +export const createSubnet = async (subnet: Subnet, setSubnet?: React.Dispatch>, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const result = await resourceClient.create({ + resourceType: ResourceType.SUBNET, + data: JSON.stringify(subnet) + }) + const output = JSON.parse(result.response.data) as Subnet + setSubnet?.(output) + return output + } catch (err) { + notification?.error({ message: `Failed to create subnet [Name: ${subnet.metadata!.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} + +export const updateSubnet = async (subnet: Subnet, setSubnet?: React.Dispatch>, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const result = await resourceClient.update({ + resourceType: ResourceType.SUBNET, + data: JSON.stringify(subnet) + }) + const temp = JSON.parse(result.response.data) as Subnet + setSubnet?.(temp) + return temp + } catch (err) { + notification?.error({ message: `Failed to update subnet [Name: ${subnet.metadata!.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } } diff --git a/src/clients/ts/openapi/openapi-schema.d.ts b/src/clients/ts/openapi/openapi-schema.d.ts index 07f88af..05a6c60 100644 --- a/src/clients/ts/openapi/openapi-schema.d.ts +++ b/src/clients/ts/openapi/openapi-schema.d.ts @@ -13,7 +13,7 @@ export interface components { apiVersion?: string; /** @description Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ kind?: string; - metadata?: Record; + metadata?: Record; /** @description DataVolumeSpec defines the DataVolume type specification */ spec: { /** @description Checkpoints is a list of DataVolumeCheckpoints, representing stages in a multistage import. */ @@ -290,6 +290,7 @@ export interface components { }; }; v1IPPool: { + metadata?: Record; spec?: { subnet: string; namespaces?: string[]; @@ -315,6 +316,7 @@ export interface components { }; }; v1IP: { + metadata?: Record; spec?: { podName?: string; namespace?: string; @@ -337,7 +339,7 @@ export interface components { apiVersion?: string; /** @description Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ kind?: string; - metadata?: Record; + metadata?: Record; /** @description NetworkAttachmentDefinition spec defines the desired state of a network attachment */ spec?: { /** @description NetworkAttachmentDefinition config is a JSON-formatted CNI configuration */ @@ -345,9 +347,7 @@ export interface components { }; }; v1Subnet: { - metadata?: { - name?: string; - }; + metadata?: Record; status?: { v4availableIPs?: number; v4usingIPs?: number; @@ -459,11 +459,11 @@ export interface components { * In CamelCase. * More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ kind?: string; - metadata?: Record; + metadata?: Record; spec?: Record; status?: { dataVolumes?: { - metadata?: Record; + metadata?: Record; /** @description DataVolumeSpec defines the DataVolume type specification */ spec?: { /** @description Checkpoints is a list of DataVolumeCheckpoints, representing stages in a multistage import. */ @@ -835,7 +835,7 @@ export interface components { }[]; network?: { ips?: { - metadata?: Record; + metadata?: Record; spec?: { attachIps: string[]; attachMacs: string[]; @@ -854,7 +854,7 @@ export interface components { }[]; }; virtualMachine?: { - metadata?: Record; + metadata?: Record; /** @description VirtualMachineSpec describes how the proper VirtualMachine * should look like */ spec?: { @@ -872,7 +872,7 @@ export interface components { * In CamelCase. * More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ kind?: string; - metadata?: Record | null; + metadata?: Record | null; /** @description DataVolumeSpec contains the DataVolume specification. */ spec: { /** @description Checkpoints is a list of DataVolumeCheckpoints, representing stages in a multistage import. */ @@ -1270,7 +1270,7 @@ export interface components { running?: boolean; /** @description Template is the direct specification of VirtualMachineInstance */ template: { - metadata?: Record | null; + metadata?: Record | null; /** @description VirtualMachineInstance Spec contains the VirtualMachineInstance specification. */ spec?: { /** @description Specifies a set of public keys to inject into the vm guest */ @@ -3421,7 +3421,7 @@ export interface components { }; }; virtualMachineInstance?: { - metadata?: Record; + metadata?: Record; /** @description Spec *kubevirtcorev1.VirtualMachineInstanceSpec `json:"spec,omitempty"` */ status?: { /** @@ -3836,7 +3836,7 @@ export interface components { apiVersion?: string; /** @description Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ kind?: string; - metadata?: Record; + metadata?: Record; /** @description VirtualMachineInstance Spec contains the VirtualMachineInstance specification. */ spec: { /** @description Specifies a set of public keys to inject into the vm guest */ @@ -5381,7 +5381,7 @@ export interface components { apiVersion?: string; /** @description Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ kind?: string; - metadata?: Record; + metadata?: Record; /** @description VirtualMachineInstance Spec contains the VirtualMachineInstance specification. */ spec: { /** @description Specifies a set of public keys to inject into the vm guest */ @@ -6926,7 +6926,7 @@ export interface components { apiVersion?: string; /** @description Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ kind?: string; - metadata?: Record; + metadata?: Record; /** @description Spec contains the specification of VirtualMachineInstance created */ spec: { /** @description dataVolumeTemplates is a list of dataVolumes that the VirtualMachineInstance template can reference. DataVolumes in this list are dynamically created for the VirtualMachine and are tied to the VirtualMachine's life-cycle. */ @@ -6935,7 +6935,7 @@ export interface components { apiVersion?: string; /** @description Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ kind?: string; - metadata?: Record | null; + metadata?: Record | null; /** @description DataVolumeSpec contains the DataVolume specification. */ spec: { /** @description Checkpoints is a list of DataVolumeCheckpoints, representing stages in a multistage import. */ @@ -7238,7 +7238,7 @@ export interface components { running?: boolean; /** @description Template is the direct specification of VirtualMachineInstance */ template: { - metadata?: Record | null; + metadata?: Record | null; /** @description VirtualMachineInstance Spec contains the VirtualMachineInstance specification. */ spec?: { /** @description Specifies a set of public keys to inject into the vm guest */ @@ -8653,7 +8653,7 @@ export interface components { apiVersion?: string; /** @description Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ kind?: string; - metadata?: Record; + metadata?: Record; /** @description Spec contains the specification of VirtualMachineInstance created */ spec: { /** @description dataVolumeTemplates is a list of dataVolumes that the VirtualMachineInstance template can reference. DataVolumes in this list are dynamically created for the VirtualMachine and are tied to the VirtualMachine's life-cycle. */ @@ -8662,7 +8662,7 @@ export interface components { apiVersion?: string; /** @description Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ kind?: string; - metadata?: Record | null; + metadata?: Record | null; /** @description DataVolumeSpec contains the DataVolume specification. */ spec: { /** @description Checkpoints is a list of DataVolumeCheckpoints, representing stages in a multistage import. */ @@ -8965,7 +8965,7 @@ export interface components { running?: boolean; /** @description Template is the direct specification of VirtualMachineInstance */ template: { - metadata?: Record | null; + metadata?: Record | null; /** @description VirtualMachineInstance Spec contains the VirtualMachineInstance specification. */ spec?: { /** @description Specifies a set of public keys to inject into the vm guest */ @@ -10375,6 +10375,7 @@ export interface components { }; }; v1Vpc: { + metadata?: Record; spec?: { defaultSubnet?: string; enableExternal?: boolean; diff --git a/src/clients/virtual-machine-summary.ts b/src/clients/virtual-machine-summary.ts index 367c7ee..ca090be 100644 --- a/src/clients/virtual-machine-summary.ts +++ b/src/clients/virtual-machine-summary.ts @@ -1,167 +1,339 @@ -import { isAbortedError } from "@/utils/utils" +import { getErrorMessage, isAbortedError, resourceSort } from "@/utils/utils" import { defaultTimeout, resourceClient, resourceWatchClient } from "./clients" import { ListOptions } from "./ts/management/resource/v1alpha1/resource" import { EventType, WatchOptions } from "./ts/management/resource/v1alpha1/watch" import { components } from "./ts/openapi/openapi-schema" import { NamespaceName, ResourceType } from "./ts/types/types" import { namespaceNameKey } from "@/utils/k8s" +import { NotificationInstance } from "antd/lib/notification/interface" export type VirtualMachineSummary = components["schemas"]["v1alpha1VirtualMachineSummary"] -export const getVirtualMachineSummary = async (ns: NamespaceName): Promise => { - return new Promise((resolve, reject) => { - const call = resourceClient.get({ +// export const getVirtualMachineSummary = async (ns: NamespaceName): Promise => { +// return new Promise((resolve, reject) => { +// const call = resourceClient.get({ +// resourceType: ResourceType.VIRTUAL_MACHINE_SUMMARY, +// namespaceName: ns +// }) +// call.then((result) => { +// resolve(JSON.parse(result.response.data) as VirtualMachineSummary) +// }) +// call.response.catch((err: Error) => { +// reject(new Error(`Failed to get virtual machine summary [Namespace: ${ns.namespace}, Name: ${ns.name}]: ${err.message}`)) +// }) +// }) +// } + +export const getVirtualMachineSummary = async (ns: NamespaceName, setVirtualMachineSummary?: React.Dispatch>, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const result = await resourceClient.get({ resourceType: ResourceType.VIRTUAL_MACHINE_SUMMARY, namespaceName: ns }) - call.then((result) => { - resolve(JSON.parse(result.response.data) as VirtualMachineSummary) - }) - call.response.catch((err: Error) => { - reject(new Error(`Failed to get virtual machine summary [Namespace: ${ns.namespace}, Name: ${ns.name}]: ${err.message}`)) - }) - }) + const output = JSON.parse(result.response.data) as VirtualMachineSummary + setVirtualMachineSummary?.(output) + return output + } catch (err) { + notification?.error({ message: `Failed to get virtual machine summary [Namespace: ${ns.namespace}, Name: ${ns.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } } -export const listVirtualMachineSummarys = async (opts?: ListOptions): Promise => { - return new Promise((resolve, reject) => { - const call = resourceClient.list({ +// export const listVirtualMachineSummarys = async (opts?: ListOptions): Promise => { +// return new Promise((resolve, reject) => { +// const call = resourceClient.list({ +// resourceType: ResourceType.VIRTUAL_MACHINE, +// options: ListOptions.create(opts) +// }) +// call.then((result) => { +// let items: VirtualMachineSummary[] = [] +// result.response.items.forEach((item: string) => { +// items.push(JSON.parse(item) as VirtualMachineSummary) +// }) +// resolve(items) +// }) +// call.response.catch((err: Error) => { +// reject(new Error(`Failed to list virtual machine: ${err.message}`)) +// }) +// }) +// } + +export const listVirtualMachineSummarys = async (setVirtualMachineSummarys?: React.Dispatch>, opts?: ListOptions, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const result = await resourceClient.list({ resourceType: ResourceType.VIRTUAL_MACHINE, options: ListOptions.create(opts) }) - call.then((result) => { - let items: VirtualMachineSummary[] = [] - result.response.items.forEach((item: string) => { - items.push(JSON.parse(item) as VirtualMachineSummary) - }) - resolve(items) - }) - call.response.catch((err: Error) => { - reject(new Error(`Failed to list virtual machine: ${err.message}`)) + let items: VirtualMachineSummary[] = [] + result.response.items.forEach((item: string) => { + items.push(JSON.parse(item) as VirtualMachineSummary) }) - }) + setVirtualMachineSummarys?.(items.length > 0 ? items : undefined) + return items + } catch (err) { + notification?.error({ message: `Failed to list virtual machine`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } } -export const watchVirtualMachineSummarys = (setVirtualMachineSummarys: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal, opts?: WatchOptions): Promise => { - return new Promise((resolve, reject) => { - setLoading(true) +// export const watchVirtualMachineSummarys = (setVirtualMachineSummarys: React.Dispatch>, setLoading: React.Dispatch>, abortCtrl: AbortController, opts?: WatchOptions): Promise => { +// return new Promise((resolve, reject) => { +// setLoading(true) - const map = new Map() +// const map = new Map() - const call = resourceWatchClient.watch({ - resourceType: ResourceType.VIRTUAL_MACHINE_SUMMARY, - options: WatchOptions.create(opts) - }, { abort: abortSignal }) - - let timeoutId: NodeJS.Timeout | null = null - const updateVirtualMachineSummarys = () => { - if (map.size === 0 && timeoutId === null) { - timeoutId = setTimeout(() => { - const items = Array.from(map.values()) +// const call = resourceWatchClient.watch({ +// resourceType: ResourceType.VIRTUAL_MACHINE_SUMMARY, +// options: WatchOptions.create(opts) +// }, { abort: abortCtrl.signal }) + +// let timeoutId: NodeJS.Timeout | null = null +// const updateVirtualMachineSummarys = () => { +// if (map.size === 0 && timeoutId === null) { +// timeoutId = setTimeout(() => { +// const items = Array.from(map.values()) +// setVirtualMachineSummarys(items.length > 0 ? items : undefined) +// timeoutId = null +// }, defaultTimeout) +// } else { +// const items = Array.from(map.values()) +// setVirtualMachineSummarys(items.length > 0 ? items : undefined) +// if (timeoutId !== null) { +// clearTimeout(timeoutId) +// timeoutId = null +// } +// } +// } + +// call.responses.onMessage((response) => { +// switch (response.eventType) { +// case EventType.READY: { +// setLoading(false) +// break +// } +// case EventType.ADDED: +// case EventType.MODIFIED: { +// response.items.forEach((data) => { +// const vm = JSON.parse(data) as VirtualMachineSummary +// map.set(namespaceNameKey(vm), vm) +// }) +// break +// } +// case EventType.DELETED: { +// response.items.forEach((data) => { +// const vm = JSON.parse(data) as VirtualMachineSummary +// map.delete(namespaceNameKey(vm)) +// }) +// break +// } +// } +// updateVirtualMachineSummarys() +// }) + +// call.responses.onError((err: Error) => { +// setLoading(false) +// if (isAbortedError(err)) { +// resolve() +// } else { +// reject(new Error(`Error in watch stream for VirtualMachineSummary: ${err.message}`)) +// } +// }) + +// call.responses.onComplete(() => { +// setLoading(false) +// resolve() +// }) +// }) +// } + +export const watchVirtualMachineSummarys = async (setVirtualMachineSummarys: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal, opts?: WatchOptions, notification?: NotificationInstance): Promise => { + setLoading(true) + try { + await new Promise((resolve, reject) => { + const map = new Map() + + const call = resourceWatchClient.watch({ + resourceType: ResourceType.VIRTUAL_MACHINE_SUMMARY, + options: WatchOptions.create(opts) + }, { abort: abortSignal }) + + let timeoutId: NodeJS.Timeout | null = null + const updateVirtualMachineSummarys = () => { + if (map.size === 0 && timeoutId === null) { + timeoutId = setTimeout(() => { + const items = resourceSort(Array.from(map.values())) + setVirtualMachineSummarys(items.length > 0 ? items : undefined) + timeoutId = null + }, defaultTimeout) + } else { + const items = resourceSort(Array.from(map.values())) setVirtualMachineSummarys(items.length > 0 ? items : undefined) - timeoutId = null - }, defaultTimeout) - } else { - const items = Array.from(map.values()) - setVirtualMachineSummarys(items.length > 0 ? items : undefined) - if (timeoutId !== null) { - clearTimeout(timeoutId) - timeoutId = null + if (timeoutId !== null) { + clearTimeout(timeoutId) + timeoutId = null + } } } - } - call.responses.onMessage((response) => { - switch (response.eventType) { - case EventType.READY: { - setLoading(false) - break - } - case EventType.ADDED: - case EventType.MODIFIED: { - response.items.forEach((data) => { - const vm = JSON.parse(data) as VirtualMachineSummary - map.set(namespaceNameKey(vm), vm) - }) - break + call.responses.onMessage((response) => { + switch (response.eventType) { + case EventType.READY: { + resolve() + break + } + case EventType.ADDED: + case EventType.MODIFIED: { + response.items.forEach((data) => { + const vm = JSON.parse(data) as VirtualMachineSummary + map.set(namespaceNameKey(vm), vm) + }) + break + } + case EventType.DELETED: { + response.items.forEach((data) => { + const vm = JSON.parse(data) as VirtualMachineSummary + map.delete(namespaceNameKey(vm)) + }) + break + } } - case EventType.DELETED: { - response.items.forEach((data) => { - const vm = JSON.parse(data) as VirtualMachineSummary - map.delete(namespaceNameKey(vm)) - }) - break + updateVirtualMachineSummarys() + }) + + call.responses.onError((err: Error) => { + if (!isAbortedError(err)) { + reject(new Error(`Error in watch stream for VirtualMachineSummary: ${err.message}`)) + } else { + resolve() } - } - updateVirtualMachineSummarys() - }) + }) - call.responses.onError((err: Error) => { - setLoading(false) - if (isAbortedError(err)) { + call.responses.onComplete(() => { resolve() - } else { - reject(new Error(`Error in watch stream for VirtualMachineSummary: ${err.message}`)) - } - }) - - call.responses.onComplete(() => { - setLoading(false) - resolve() + }) }) - }) + } catch (err) { + notification?.error({ message: "Error in watch stream for VirtualMachineSummary", description: getErrorMessage(err) }) + throw err + } finally { + setLoading(false) + } } -export const watchVirtualMachineSummary = (ns: NamespaceName, setVirtualMachineSummary: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal): Promise => { - return new Promise((resolve, reject) => { - setLoading(true) +// export const watchVirtualMachineSummary = (ns: NamespaceName, setVirtualMachineSummary: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal): Promise => { +// return new Promise((resolve, reject) => { +// setLoading(true) - const call = resourceWatchClient.watch({ - resourceType: ResourceType.VIRTUAL_MACHINE_SUMMARY, - options: WatchOptions.create({ - fieldSelectorGroup: { - operator: "&&", - fieldSelectors: [ - { fieldPath: "metadata.namespace", operator: "=", values: [ns.namespace] }, - { fieldPath: "metadata.name", operator: "=", values: [ns.name] } - ] - } - }) - }, { abort: abortSignal }) +// const call = resourceWatchClient.watch({ +// resourceType: ResourceType.VIRTUAL_MACHINE_SUMMARY, +// options: WatchOptions.create({ +// fieldSelectorGroup: { +// operator: "&&", +// fieldSelectors: [ +// { fieldPath: "metadata.namespace", operator: "=", values: [ns.namespace] }, +// { fieldPath: "metadata.name", operator: "=", values: [ns.name] } +// ] +// } +// }) +// }, { abort: abortSignal }) - call.responses.onMessage((response) => { - switch (response.eventType) { - case EventType.READY: { - setLoading(false) - break - } - case EventType.ADDED: - case EventType.MODIFIED: { - if (response.items.length === 0) { - return +// call.responses.onMessage((response) => { +// switch (response.eventType) { +// case EventType.READY: { +// setLoading(false) +// break +// } +// case EventType.ADDED: +// case EventType.MODIFIED: { +// if (response.items.length === 0) { +// return +// } +// setVirtualMachineSummary(JSON.parse(response.items[0]) as VirtualMachineSummary) +// break +// } +// case EventType.DELETED: { +// setVirtualMachineSummary(undefined) +// break +// } +// } +// }) + +// call.responses.onError((err: Error) => { +// setLoading(false) +// if (isAbortedError(err)) { +// resolve() +// } else { +// reject(new Error(`Error in watch stream for VirtualMachineSummary: ${err.message}`)) +// } +// }) + +// call.responses.onComplete(() => { +// setLoading(false) +// resolve() +// }) +// }) +// } + +export const watchVirtualMachineSummary = async (ns: NamespaceName, setVirtualMachineSummary: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal, notification?: NotificationInstance): Promise => { + setLoading(true) + try { + await new Promise((resolve, reject) => { + const call = resourceWatchClient.watch({ + resourceType: ResourceType.VIRTUAL_MACHINE_SUMMARY, + options: WatchOptions.create({ + fieldSelectorGroup: { + operator: "&&", + fieldSelectors: [ + { fieldPath: "metadata.namespace", operator: "=", values: [ns.namespace] }, + { fieldPath: "metadata.name", operator: "=", values: [ns.name] } + ] + } + }) + }, { abort: abortSignal }) + + call.responses.onMessage((response) => { + switch (response.eventType) { + case EventType.READY: { + resolve() + break + } + case EventType.ADDED: + case EventType.MODIFIED: { + if (response.items.length === 0) { + return + } + setVirtualMachineSummary(JSON.parse(response.items[0]) as VirtualMachineSummary) + break + } + case EventType.DELETED: { + setVirtualMachineSummary(undefined) + break } - setVirtualMachineSummary(JSON.parse(response.items[0]) as VirtualMachineSummary) - break } - case EventType.DELETED: { - setVirtualMachineSummary(undefined) - break + }) + + call.responses.onError((err: Error) => { + if (!isAbortedError(err)) { + reject(new Error(`Error in watch stream for VirtualMachineSummary: ${err.message}`)) + } else { + resolve() } - } - }) + }) - call.responses.onError((err: Error) => { - setLoading(false) - if (isAbortedError(err)) { + call.responses.onComplete(() => { resolve() - } else { - reject(new Error(`Error in watch stream for VirtualMachineSummary: ${err.message}`)) - } - }) - - call.responses.onComplete(() => { - setLoading(false) - resolve() + }) }) - }) + } catch (err) { + notification?.error({ message: "Error in watch stream for VirtualMachineSummary", description: getErrorMessage(err) }) + throw err + } finally { + setLoading(false) + } } diff --git a/src/clients/virtual-machine.ts b/src/clients/virtual-machine.ts index b3df81f..536e345 100644 --- a/src/clients/virtual-machine.ts +++ b/src/clients/virtual-machine.ts @@ -1,4 +1,4 @@ -import { isAbortedError } from "@/utils/utils" +import { getErrorMessage, isAbortedError } from "@/utils/utils" import { defaultTimeout, resourceClient, resourceWatchClient, virtualMachineClient } from "./clients" import { ListOptions } from "./ts/management/resource/v1alpha1/resource" import { EventType, WatchOptions } from "./ts/management/resource/v1alpha1/watch" @@ -6,293 +6,585 @@ import { VirtualMachinePowerStateRequest_PowerState } from "./ts/management/virt import { components } from "./ts/openapi/openapi-schema" import { NamespaceName, ResourceType } from "./ts/types/types" import { namespaceNameKey } from "@/utils/k8s" +import { NotificationInstance } from "antd/lib/notification/interface" export type VirtualMachine = components["schemas"]["v1VirtualMachine"] -export const createVirtualMachine = async (vm: VirtualMachine): Promise => { - return new Promise((resolve, reject) => { - const call = resourceClient.create({ +// export const createVirtualMachine = async (vm: VirtualMachine): Promise => { +// return new Promise((resolve, reject) => { +// const call = resourceClient.create({ +// resourceType: ResourceType.VIRTUAL_MACHINE, +// data: JSON.stringify(vm) +// }) + +// call.then((result) => { +// resolve(JSON.parse(result.response.data) as VirtualMachine) +// }) +// call.response.catch((err: Error) => { +// reject(new Error(`Failed to create virtual machine [Namespace: ${vm.metadata?.namespace}, Name: ${vm.metadata?.name}]: ${err.message}`)) +// }) +// }) +// } + +export const createVirtualMachine = async (vm: VirtualMachine, setVirtualMachine?: React.Dispatch>, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const result = await resourceClient.create({ resourceType: ResourceType.VIRTUAL_MACHINE, data: JSON.stringify(vm) }) - - call.then((result) => { - resolve(JSON.parse(result.response.data) as VirtualMachine) - }) - call.response.catch((err: Error) => { - reject(new Error(`Failed to create virtual machine [Namespace: ${vm.metadata?.namespace}, Name: ${vm.metadata?.name}]: ${err.message}`)) - }) - }) + const output = JSON.parse(result.response.data) as VirtualMachine + setVirtualMachine?.(output) + return output + } catch (err) { + notification?.error({ message: `Failed to create virtual machine [Namespace: ${vm.metadata!.namespace}, Name: ${vm.metadata!.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } } -export const deleteVirtualMachines = async (vms: VirtualMachine[]): Promise => { - const completed: VirtualMachine[] = [] - const failed: { vm: VirtualMachine; error: any }[] = [] +// export const deleteVirtualMachines = async (vms: VirtualMachine[]): Promise => { +// const completed: VirtualMachine[] = [] +// const failed: { vm: VirtualMachine; error: any }[] = [] + +// await Promise.all(vms.map(async (vm) => { +// const namespace = vm.metadata?.namespace +// const name = vm.metadata?.name +// if (!namespace || !name) { +// return +// } +// try { +// await resourceClient.delete({ namespaceName: { namespace, name }, resourceType: ResourceType.VIRTUAL_MACHINE }).response +// completed.push(vm) +// } catch (err: any) { +// failed.push({ vm, error: err }) +// } +// })) + +// if (failed.length > 0) { +// const errorMessages = failed.map(({ vm, error }) => `Namespace: ${vm.metadata?.namespace ?? "unknown"}, Name: ${vm.metadata?.name ?? "unknown"}, Error: ${error.message}`).join("\n") +// Promise.reject(new Error(`Failed to delete the following virtual machines:\n${errorMessages}`)) +// } +// } + +export const deleteVirtualMachines = async (vms: VirtualMachine[], setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const completed: VirtualMachine[] = [] + const failed: { vm: VirtualMachine; error: any }[] = [] + + await Promise.all(vms.map(async (vm) => { + const namespace = vm.metadata!.namespace + const name = vm.metadata!.name + + try { + await resourceClient.delete({ namespaceName: { namespace, name }, resourceType: ResourceType.VIRTUAL_MACHINE }).response + completed.push(vm) + } catch (err: any) { + failed.push({ vm, error: err }) + } + })) - await Promise.all(vms.map(async (vm) => { - const namespace = vm.metadata?.namespace - const name = vm.metadata?.name - if (!namespace || !name) { - return + if (failed.length > 0) { + const errorMessages = failed.map(({ vm, error }) => `Namespace: ${vm.metadata?.namespace ?? "unknown"}, Name: ${vm.metadata?.name ?? "unknown"}, Error: ${error.message}`).join("\n") + throw new Error(`Failed to delete the following virtual machines:\n${errorMessages}`) } - try { - await resourceClient.delete({ namespaceName: { namespace, name }, resourceType: ResourceType.VIRTUAL_MACHINE }).response - completed.push(vm) - } catch (err: any) { - failed.push({ vm, error: err }) - } - })) - - if (failed.length > 0) { - const errorMessages = failed.map(({ vm, error }) => `Namespace: ${vm.metadata?.namespace ?? "unknown"}, Name: ${vm.metadata?.name ?? "unknown"}, Error: ${error.message}`).join("\n") - Promise.reject(new Error(`Failed to delete the following virtual machines:\n${errorMessages}`)) + } catch (err) { + notification?.error({ message: `Failed to delete virtual machines:`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) } } -export const deleteVirtualMachine = async (ns: NamespaceName): Promise => { - return new Promise((resolve, reject) => { - const call = resourceClient.delete({ +export const deleteVirtualMachine = async (ns: NamespaceName, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + await resourceClient.delete({ resourceType: ResourceType.VIRTUAL_MACHINE, namespaceName: ns }) - - call.then(() => { - resolve() - }) - call.response.catch((err: Error) => { - reject(new Error(`Failed to delete virtual machine [Namespace: ${ns.namespace}, Name: ${ns.name}]: ${err.message}`)) - }) - }) + } catch (err) { + notification?.error({ message: `Failed to delete virtual machine [Namespace: ${ns.namespace}, Name: ${ns.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } } -export const getVirtualMachine = async (ns: NamespaceName): Promise => { - return new Promise((resolve, reject) => { - const call = resourceClient.get({ +// export const getVirtualMachine = async (ns: NamespaceName): Promise => { +// return new Promise((resolve, reject) => { +// const call = resourceClient.get({ +// resourceType: ResourceType.VIRTUAL_MACHINE, +// namespaceName: ns +// }) +// call.then((result) => { +// resolve(JSON.parse(result.response.data) as VirtualMachine) +// }) +// call.response.catch((err: Error) => { +// reject(new Error(`Failed to get virtual machine [Namespace: ${ns.namespace}, Name: ${ns.name}]: ${err.message}`)) +// }) +// }) +// } + +export const getVirtualMachine = async (ns: NamespaceName, setVirtualMachine?: React.Dispatch>, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const call = await resourceClient.get({ resourceType: ResourceType.VIRTUAL_MACHINE, namespaceName: ns }) - call.then((result) => { - resolve(JSON.parse(result.response.data) as VirtualMachine) - }) - call.response.catch((err: Error) => { - reject(new Error(`Failed to get virtual machine [Namespace: ${ns.namespace}, Name: ${ns.name}]: ${err.message}`)) - }) - }) -} - -export const updateVirtualMachine = async (vm: VirtualMachine): Promise => { - const namespace = vm.metadata?.namespace - const name = vm.metadata?.name - if (!namespace || !name) { + const vm = JSON.parse(call.response.data) as VirtualMachine + setVirtualMachine?.(vm) return vm + } catch (err) { + notification?.error({ message: `Failed to get virtual machine [Namespace: ${ns.namespace}, Name: ${ns.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) } +} - return new Promise((resolve, reject) => { - const call = resourceClient.update({ +// export const updateVirtualMachine = async (vm: VirtualMachine): Promise => { +// const namespace = vm.metadata?.namespace +// const name = vm.metadata?.name +// if (!namespace || !name) { +// return vm +// } + +// return new Promise((resolve, reject) => { +// const call = resourceClient.update({ +// resourceType: ResourceType.VIRTUAL_MACHINE, +// data: JSON.stringify(vm) +// }) + +// call.then((result) => { +// resolve(JSON.parse(result.response.data)) +// }) +// call.response.catch((err: Error) => { +// reject(new Error(`Failed to update virtual machine [Namespace: ${namespace}, Name: ${name}]: ${err.message}`)) +// }) +// }) +// } + +export const updateVirtualMachine = async (vm: VirtualMachine, setVirtualMachine?: React.Dispatch>, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const result = await resourceClient.update({ resourceType: ResourceType.VIRTUAL_MACHINE, data: JSON.stringify(vm) }) - - call.then((result) => { - resolve(JSON.parse(result.response.data)) - }) - call.response.catch((err: Error) => { - reject(new Error(`Failed to update virtual machine [Namespace: ${namespace}, Name: ${name}]: ${err.message}`)) - }) - }) + const temp = JSON.parse(result.response.data) as VirtualMachine + setVirtualMachine?.(temp) + return temp + } catch (err) { + notification?.error({ message: `Failed to update virtual machine [Namespace: ${vm.metadata!.namespace}, Name: ${vm.metadata!.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } } -export const listVirtualMachines = async (opts?: ListOptions): Promise => { - return new Promise((resolve, reject) => { - const call = resourceClient.list({ +// export const listVirtualMachines = async (opts?: ListOptions): Promise => { +// return new Promise((resolve, reject) => { +// const call = resourceClient.list({ +// resourceType: ResourceType.VIRTUAL_MACHINE, +// options: ListOptions.create(opts) +// }) +// call.then((result) => { +// let items: VirtualMachine[] = [] +// result.response.items.forEach((item: string) => { +// items.push(JSON.parse(item) as VirtualMachine) +// }) +// resolve(items) +// }) +// call.response.catch((err: Error) => { +// reject(new Error(`Failed to list virtual machine: ${err.message}`)) +// }) +// }) +// } + +export const listVirtualMachines = async (setVirtualMachines: React.Dispatch>, opts?: ListOptions, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const result = await resourceClient.list({ resourceType: ResourceType.VIRTUAL_MACHINE, options: ListOptions.create(opts) }) - call.then((result) => { - let items: VirtualMachine[] = [] - result.response.items.forEach((item: string) => { - items.push(JSON.parse(item) as VirtualMachine) - }) - resolve(items) - }) - call.response.catch((err: Error) => { - reject(new Error(`Failed to list virtual machine: ${err.message}`)) + let items: VirtualMachine[] = [] + result.response.items.forEach((item: string) => { + items.push(JSON.parse(item) as VirtualMachine) }) - }) + setVirtualMachines(items) + return items + } catch (err) { + notification?.error({ message: `Failed to list virtual machines`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } } -export const manageVirtualMachinePowerState = (ns: NamespaceName, state: VirtualMachinePowerStateRequest_PowerState): Promise => { - return new Promise((resolve, reject) => { - const call = virtualMachineClient.virtualMachinePowerState({ +// export const manageVirtualMachinePowerState = (ns: NamespaceName, state: VirtualMachinePowerStateRequest_PowerState): Promise => { +// return new Promise((resolve, reject) => { +// const call = virtualMachineClient.virtualMachinePowerState({ +// namespaceName: ns, +// powerState: state +// }) +// call.response.then(() => { +// resolve() +// }) +// call.response.catch((err: Error) => { +// reject(new Error(`Failed to change power state of virtual machine [Namespace: ${ns.namespace}, Name: ${ns.name}] to [State: ${state}]: ${err.message}`)) +// }) +// }) +// } + +export const manageVirtualMachinePowerState = async (ns: NamespaceName, state: VirtualMachinePowerStateRequest_PowerState, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + await virtualMachineClient.virtualMachinePowerState({ namespaceName: ns, powerState: state }) - call.response.then(() => { - resolve() - }) - call.response.catch((err: Error) => { - reject(new Error(`Failed to change power state of virtual machine [Namespace: ${ns.namespace}, Name: ${ns.name}] to [State: ${state}]: ${err.message}`)) - }) - }) + } catch (err) { + notification?.error({ message: `Failed to change power state of virtual machine [Namespace: ${ns.namespace}, Name: ${ns.name}] to [State: ${state}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } } -export const manageVirtualMachinesPowerState = async (vms: VirtualMachine[], state: VirtualMachinePowerStateRequest_PowerState): Promise => { - const completed: VirtualMachine[] = [] - const failed: { vm: VirtualMachine; error: any }[] = [] - - await Promise.all(vms.map(async (vm) => { - const namespace = vm.metadata?.namespace - const name = vm.metadata?.name - if (!namespace || !name) { - return - } - - const isRunning = vm.status?.printableStatus as string === "Running" - - if (state === VirtualMachinePowerStateRequest_PowerState.OFF && !isRunning) { - return - } - if (state === VirtualMachinePowerStateRequest_PowerState.ON && isRunning) { - return - } - if (state === VirtualMachinePowerStateRequest_PowerState.REBOOT && !isRunning) { - return - } +// export const manageVirtualMachinesPowerState = async (vms: VirtualMachine[], state: VirtualMachinePowerStateRequest_PowerState): Promise => { +// const completed: VirtualMachine[] = [] +// const failed: { vm: VirtualMachine; error: any }[] = [] + +// await Promise.all(vms.map(async (vm) => { +// const namespace = vm.metadata?.namespace +// const name = vm.metadata?.name +// if (!namespace || !name) { +// return +// } + +// const isRunning = vm.status?.printableStatus as string === "Running" + +// if (state === VirtualMachinePowerStateRequest_PowerState.OFF && !isRunning) { +// return +// } +// if (state === VirtualMachinePowerStateRequest_PowerState.ON && isRunning) { +// return +// } +// if (state === VirtualMachinePowerStateRequest_PowerState.REBOOT && !isRunning) { +// return +// } + +// try { +// await virtualMachineClient.virtualMachinePowerState({ namespaceName: { namespace, name }, powerState: state }).response +// completed.push(vm) +// } catch (err: any) { +// failed.push({ vm, error: err }) +// } +// })) + +// if (failed.length > 0) { +// const errorMessages = failed.map(({ vm, error }) => `Namespace: ${vm.metadata?.namespace ?? "unknown"}, Name: ${vm.metadata?.name ?? "unknown"}, Error: ${error.message}`).join("\n") +// Promise.reject(new Error(`Failed to change power state the following virtual machines:\n${errorMessages}`)) +// } +// } + +export const manageVirtualMachinesPowerState = async (vms: VirtualMachine[], state: VirtualMachinePowerStateRequest_PowerState, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const completed: VirtualMachine[] = [] + const failed: { vm: VirtualMachine; error: any }[] = [] + + await Promise.all(vms.map(async (vm) => { + const namespace = vm.metadata!.namespace + const name = vm.metadata!.name + + const isRunning = vm.status?.printableStatus as string === "Running" + + if (state === VirtualMachinePowerStateRequest_PowerState.OFF && !isRunning) { + return + } + if (state === VirtualMachinePowerStateRequest_PowerState.ON && isRunning) { + return + } + if (state === VirtualMachinePowerStateRequest_PowerState.REBOOT && !isRunning) { + return + } + try { + await virtualMachineClient.virtualMachinePowerState({ namespaceName: { namespace, name }, powerState: state }).response + completed.push(vm) + } catch (err: any) { + failed.push({ vm, error: err }) + } + })) - try { - await virtualMachineClient.virtualMachinePowerState({ namespaceName: { namespace, name }, powerState: state }).response - completed.push(vm) - } catch (err: any) { - failed.push({ vm, error: err }) + if (failed.length > 0) { + const errorMessages = failed.map(({ vm, error }) => `Namespace: ${vm.metadata?.namespace ?? "unknown"}, Name: ${vm.metadata?.name ?? "unknown"}, Error: ${error.message}`).join("\n") + throw new Error(`Failed to change power state the following virtual machines:\n${errorMessages}`) } - })) - - if (failed.length > 0) { - const errorMessages = failed.map(({ vm, error }) => `Namespace: ${vm.metadata?.namespace ?? "unknown"}, Name: ${vm.metadata?.name ?? "unknown"}, Error: ${error.message}`).join("\n") - Promise.reject(new Error(`Failed to change power state the following virtual machines:\n${errorMessages}`)) + } catch (err) { + notification?.error({ message: `Failed to change power state of virtual machine to [State: ${state}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) } } -export const watchVirtualMachines = (setVirtualMachine: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal, opts?: WatchOptions): Promise => { - return new Promise((resolve, reject) => { - setLoading(true) - - const map = new Map() - - const call = resourceWatchClient.watch({ - resourceType: ResourceType.VIRTUAL_MACHINE, - options: WatchOptions.create(opts) - }, { abort: abortSignal }) - - let timeoutId: NodeJS.Timeout | null = null - const updateVirtualMachine = () => { - if (map.size === 0 && timeoutId === null) { - timeoutId = setTimeout(() => { +// export const watchVirtualMachines = (setVirtualMachine: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal, opts?: WatchOptions): Promise => { +// return new Promise((resolve, reject) => { +// setLoading(true) + +// const map = new Map() + +// const call = resourceWatchClient.watch({ +// resourceType: ResourceType.VIRTUAL_MACHINE, +// options: WatchOptions.create(opts) +// }, { abort: abortSignal }) + +// let timeoutId: NodeJS.Timeout | null = null +// const updateVirtualMachine = () => { +// if (map.size === 0 && timeoutId === null) { +// timeoutId = setTimeout(() => { +// const items = Array.from(map.values()) +// setVirtualMachine(items.length > 0 ? items : undefined) +// timeoutId = null +// }, defaultTimeout) +// } else { +// const items = Array.from(map.values()) +// setVirtualMachine(items.length > 0 ? items : undefined) +// if (timeoutId !== null) { +// clearTimeout(timeoutId) +// timeoutId = null +// } +// } +// } + +// call.responses.onMessage((response) => { +// switch (response.eventType) { +// case EventType.READY: { +// setLoading(false) +// break +// } +// case EventType.ADDED: +// case EventType.MODIFIED: { +// response.items.forEach((data) => { +// const vm = JSON.parse(data) as VirtualMachine +// map.set(namespaceNameKey(vm), vm) +// }) +// break +// } +// case EventType.DELETED: { +// response.items.forEach((data) => { +// const vm = JSON.parse(data) as VirtualMachine +// map.delete(namespaceNameKey(vm)) +// }) +// break +// } +// } +// updateVirtualMachine() +// }) + +// call.responses.onError((err: Error) => { +// setLoading(false) +// if (isAbortedError(err)) { +// resolve() +// } else { +// reject(new Error(`Error in watch stream for VirtualMachine: ${err.message}`)) +// } +// }) + +// call.responses.onComplete(() => { +// setLoading(false) +// resolve() +// }) +// }) +// } + +export const watchVirtualMachines = async (setVirtualMachine: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal, opts?: WatchOptions, notification?: NotificationInstance): Promise => { + setLoading(true) + try { + await new Promise((resolve, reject) => { + const map = new Map() + + const call = resourceWatchClient.watch({ + resourceType: ResourceType.VIRTUAL_MACHINE, + options: WatchOptions.create(opts) + }, { abort: abortSignal }) + + let timeoutId: NodeJS.Timeout | null = null + const updateVirtualMachine = () => { + if (map.size === 0 && timeoutId === null) { + timeoutId = setTimeout(() => { + const items = Array.from(map.values()) + setVirtualMachine(items.length > 0 ? items : undefined) + timeoutId = null + }, defaultTimeout) + } else { const items = Array.from(map.values()) setVirtualMachine(items.length > 0 ? items : undefined) - timeoutId = null - }, defaultTimeout) - } else { - const items = Array.from(map.values()) - setVirtualMachine(items.length > 0 ? items : undefined) - if (timeoutId !== null) { - clearTimeout(timeoutId) - timeoutId = null + if (timeoutId !== null) { + clearTimeout(timeoutId) + timeoutId = null + } } } - } - call.responses.onMessage((response) => { - switch (response.eventType) { - case EventType.READY: { - setLoading(false) - break - } - case EventType.ADDED: - case EventType.MODIFIED: { - response.items.forEach((data) => { - const vm = JSON.parse(data) as VirtualMachine - map.set(namespaceNameKey(vm), vm) - }) - break - } - case EventType.DELETED: { - response.items.forEach((data) => { - const vm = JSON.parse(data) as VirtualMachine - map.delete(namespaceNameKey(vm)) - }) - break + call.responses.onMessage((response) => { + switch (response.eventType) { + case EventType.READY: { + resolve() + break + } + case EventType.ADDED: + case EventType.MODIFIED: { + response.items.forEach((data) => { + const vm = JSON.parse(data) as VirtualMachine + map.set(namespaceNameKey(vm), vm) + }) + break + } + case EventType.DELETED: { + response.items.forEach((data) => { + const vm = JSON.parse(data) as VirtualMachine + map.delete(namespaceNameKey(vm)) + }) + break + } } - } - updateVirtualMachine() - }) + updateVirtualMachine() + }) - call.responses.onError((err: Error) => { - setLoading(false) - if (isAbortedError(err)) { + call.responses.onError((err: Error) => { + if (!isAbortedError(err)) { + reject(new Error(`Error in watch stream for VirtualMachine: ${err.message}`)) + } else { + resolve() + } + }) + call.responses.onComplete(() => { resolve() - } else { - reject(new Error(`Error in watch stream for VirtualMachine: ${err.message}`)) - } - }) - - call.responses.onComplete(() => { - setLoading(false) - resolve() + }) }) - }) + } catch (err) { + notification?.error({ message: `Failed to watch virtual machines`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading(false) + } } -export const watchVirtualMachine = (ns: NamespaceName, setVirtualMachine: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal): Promise => { - return new Promise((resolve, reject) => { - setLoading(true) - - const call = resourceWatchClient.watch({ - resourceType: ResourceType.VIRTUAL_MACHINE, - options: WatchOptions.create({ - fieldSelectorGroup: { - operator: "&&", - fieldSelectors: [ - { fieldPath: "metadata.namespace", operator: "=", values: [ns.namespace] }, - { fieldPath: "metadata.name", operator: "=", values: [ns.name] } - ] +// export const watchVirtualMachine = (ns: NamespaceName, setVirtualMachine: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal): Promise => { +// return new Promise((resolve, reject) => { +// setLoading(true) + +// const call = resourceWatchClient.watch({ +// resourceType: ResourceType.VIRTUAL_MACHINE, +// options: WatchOptions.create({ +// fieldSelectorGroup: { +// operator: "&&", +// fieldSelectors: [ +// { fieldPath: "metadata.namespace", operator: "=", values: [ns.namespace] }, +// { fieldPath: "metadata.name", operator: "=", values: [ns.name] } +// ] +// } +// }) +// }, { abort: abortSignal }) + +// call.responses.onMessage((response) => { +// switch (response.eventType) { +// case EventType.READY: { +// setLoading(false) +// break +// } +// case EventType.ADDED: +// case EventType.MODIFIED: { +// if (response.items.length === 0) { +// return +// } +// setVirtualMachine(JSON.parse(response.items[0]) as VirtualMachine) +// break +// } +// case EventType.DELETED: { +// setVirtualMachine(undefined) +// break +// } +// } +// }) + +// call.responses.onError((err: Error) => { +// setLoading(false) +// if (isAbortedError(err)) { +// resolve() +// } else { +// reject(new Error(`Error in watch stream for VirtualMachine: ${err.message}`)) +// } +// }) + +// call.responses.onComplete(() => { +// setLoading(false) +// resolve() +// }) +// }) +// } + +export const watchVirtualMachine = async (ns: NamespaceName, setVirtualMachine: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal, notification?: NotificationInstance): Promise => { + setLoading(true) + try { + await new Promise((resolve, reject) => { + const call = resourceWatchClient.watch({ + resourceType: ResourceType.VIRTUAL_MACHINE, + options: WatchOptions.create({ + fieldSelectorGroup: { + operator: "&&", + fieldSelectors: [ + { fieldPath: "metadata.namespace", operator: "=", values: [ns.namespace] }, + { fieldPath: "metadata.name", operator: "=", values: [ns.name] } + ] + } + }) + }, { abort: abortSignal }) + + call.responses.onMessage((response) => { + switch (response.eventType) { + case EventType.READY: { + resolve() + break + } + case EventType.ADDED: + case EventType.MODIFIED: { + if (response.items.length === 0) { + return + } + setVirtualMachine(JSON.parse(response.items[0]) as VirtualMachine) + break + } + case EventType.DELETED: { + setVirtualMachine(undefined) + break + } } }) - }, { abort: abortSignal }) - call.responses.onMessage((response) => { - switch (response.eventType) { - case EventType.READY: { - setLoading(false) - break - } - case EventType.ADDED: - case EventType.MODIFIED: { - if (response.items.length === 0) { - return - } - setVirtualMachine(JSON.parse(response.items[0]) as VirtualMachine) - break + call.responses.onError((err: Error) => { + if (!isAbortedError(err)) { + reject(new Error(`Error in watch stream for VirtualMachine: ${err.message}`)) + } else { + resolve() } - case EventType.DELETED: { - setVirtualMachine(undefined) - break - } - } - }) + }) - call.responses.onError((err: Error) => { - setLoading(false) - if (isAbortedError(err)) { + call.responses.onComplete(() => { resolve() - } else { - reject(new Error(`Error in watch stream for VirtualMachine: ${err.message}`)) - } - }) - - call.responses.onComplete(() => { - setLoading(false) - resolve() + }) }) - }) + } catch (err) { + notification?.error({ message: `Failed to watch virtual machine`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading(false) + } } diff --git a/src/clients/vpc.ts b/src/clients/vpc.ts index d82f19a..f447a4b 100644 --- a/src/clients/vpc.ts +++ b/src/clients/vpc.ts @@ -1,3 +1,246 @@ +import { NotificationInstance } from "antd/lib/notification/interface" +import { ListOptions } from "./ts/management/resource/v1alpha1/resource" import { components } from "./ts/openapi/openapi-schema" +import { defaultTimeout, resourceClient, resourceWatchClient } from "./clients" +import { NamespaceName, ResourceType } from "./ts/types/types" +import { getErrorMessage, isAbortedError } from "@/utils/utils" +import { EventType, WatchOptions } from "./ts/management/resource/v1alpha1/watch" +import { namespaceNameKey } from "@/utils/k8s" export type VPC = components["schemas"]["v1Vpc"] + +export const listVPCs = async (setVPCs?: React.Dispatch>, opts?: ListOptions, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const result = await resourceClient.list({ + resourceType: ResourceType.VPC, + options: ListOptions.create(opts) + }) + let items: VPC[] = [] + result.response.items.forEach((item: string) => { + items.push(JSON.parse(item) as VPC) + }) + setVPCs?.(items.length > 0 ? items : undefined) + return items + } catch (err) { + notification?.error({ message: `Failed to list VPC`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} + +export const deleteVPCs = async (vpcs: VPC[], setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const completed: VPC[] = [] + const failed: { vpc: VPC; error: any }[] = [] + + await Promise.all(vpcs.map(async (vpc) => { + const namespace = vpc.metadata!.namespace + const name = vpc.metadata!.name + + try { + await resourceClient.delete({ + namespaceName: { namespace, name }, + resourceType: ResourceType.VPC + }).response + completed.push(vpc) + } catch (err: any) { + failed.push({ vpc, error: err }) + } + })) + + if (failed.length > 0) { + const errorMessages = failed.map(({ vpc, error }) => `Name: ${vpc.metadata!.name ?? "unknown"}, Error: ${error.message}`).join("\n") + throw new Error(`Failed to delete the following VPCs:\n${errorMessages}`) + } + } catch (err) { + notification?.error({ message: `Failed to delete VPCs:`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} + +export const deleteVPC = async (ns: NamespaceName, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + await resourceClient.delete({ + resourceType: ResourceType.VPC, + namespaceName: ns + }) + } catch (err) { + notification?.error({ message: `Failed to delete VPC [Name: ${ns.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} + +export const watchVPCs = async (setVPCs: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal, opts?: WatchOptions, notification?: NotificationInstance): Promise => { + setLoading(true) + try { + await new Promise((resolve, reject) => { + const map = new Map() + + const call = resourceWatchClient.watch({ + resourceType: ResourceType.VPC, + options: WatchOptions.create(opts) + }, { abort: abortSignal }) + + let timeoutId: NodeJS.Timeout | null = null + const updateVPCs = () => { + if (map.size === 0 && timeoutId === null) { + timeoutId = setTimeout(() => { + const items = Array.from(map.values()) + setVPCs(items.length > 0 ? items : undefined) + timeoutId = null + }, defaultTimeout) + } else { + const items = Array.from(map.values()) + setVPCs(items.length > 0 ? items : undefined) + if (timeoutId !== null) { + clearTimeout(timeoutId) + timeoutId = null + } + } + } + + call.responses.onMessage((response) => { + switch (response.eventType) { + case EventType.READY: { + resolve() + break + } + case EventType.ADDED: + case EventType.MODIFIED: { + response.items.forEach((data) => { + const vpc = JSON.parse(data) as VPC + map.set(namespaceNameKey(vpc), vpc) + }) + break + } + case EventType.DELETED: { + response.items.forEach((data) => { + const vpc = JSON.parse(data) as VPC + map.delete(namespaceNameKey(vpc)) + }) + break + } + } + updateVPCs() + }) + + call.responses.onError((err: Error) => { + if (isAbortedError(err)) { + resolve() + } else { + reject(new Error(`Error in watch stream for VPC: ${err.message}`)) + } + }) + + call.responses.onComplete(() => { + resolve() + }) + }) + } catch (err) { + notification?.error({ message: `Failed to watch VPC`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading(false) + } +} + +export const watchVPC = async (ns: NamespaceName, setVPC: React.Dispatch>, setLoading: React.Dispatch>, abortSignal: AbortSignal, notification?: NotificationInstance): Promise => { + setLoading(true) + try { + await new Promise((resolve, reject) => { + const call = resourceWatchClient.watch({ + resourceType: ResourceType.VPC, + options: WatchOptions.create({ + fieldSelectorGroup: { + operator: "&&", + fieldSelectors: [ + { fieldPath: "metadata.name", operator: "=", values: [ns.name] } + ] + } + }) + }, { abort: abortSignal }) + + call.responses.onMessage((response) => { + switch (response.eventType) { + case EventType.READY: { + resolve() + break + } + case EventType.ADDED: + case EventType.MODIFIED: { + if (response.items.length === 0) { + return + } + setVPC(JSON.parse(response.items[0]) as VPC) + break + } + case EventType.DELETED: { + setVPC(undefined) + break + } + } + }) + + call.responses.onError((err: Error) => { + if (!isAbortedError(err)) { + reject(new Error(`Error in watch stream for VPC: ${err.message}`)) + } else { + resolve() + } + }) + + call.responses.onComplete(() => { + resolve() + }) + }) + } catch (err) { + notification?.error({ message: `Failed to watch VPC`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading(false) + } +} + +export const createVPC = async (vpc: VPC, setVPC?: React.Dispatch>, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const result = await resourceClient.create({ + resourceType: ResourceType.VPC, + data: JSON.stringify(vpc) + }) + const output = JSON.parse(result.response.data) as VPC + setVPC?.(output) + return output + } catch (err) { + notification?.error({ message: `Failed to create VPC [Name: ${vpc.metadata!.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} + +export const updateVPC = async (vpc: VPC, setVPC?: React.Dispatch>, setLoading?: React.Dispatch>, notification?: NotificationInstance): Promise => { + setLoading?.(true) + try { + const result = await resourceClient.update({ + resourceType: ResourceType.VPC, + data: JSON.stringify(vpc) + }) + const temp = JSON.parse(result.response.data) as VPC + setVPC?.(temp) + return temp + } catch (err) { + notification?.error({ message: `Failed to update VPC [Name: ${vpc.metadata!.name}]`, description: getErrorMessage(err) }) + throw err + } finally { + setLoading?.(false) + } +} diff --git a/src/components/custom-table/index.tsx b/src/components/custom-table/index.tsx index f658737..c826826 100644 --- a/src/components/custom-table/index.tsx +++ b/src/components/custom-table/index.tsx @@ -12,15 +12,15 @@ import tableStyles from '@/common/styles/table.module.less' import commonStyles from '@/common/styles/common.module.less' interface CustomTableProps> extends Partial>> { + key: string searchItems?: SearchItem[] | undefined - storageKey?: string loading?: boolean defaultFieldSelectors?: FieldSelector[] onSelectRows?: (rows: T[]) => void updateWatchOptions?: React.Dispatch> } -export const CustomTable = >({ searchItems, onSelectRows, storageKey, loading, defaultFieldSelectors, updateWatchOptions, ...proTableProps }: CustomTableProps) => { +export const CustomTable = >({ key, searchItems, onSelectRows, loading, defaultFieldSelectors, updateWatchOptions, ...proTableProps }: CustomTableProps) => { const { namespace } = useNamespace() const searchRef = useRef() @@ -40,7 +40,7 @@ export const CustomTable = >({ searchItems, onSele }, [namespace]) useEffect(() => { - const inputElement = document.querySelector('input[name="search"]') + const inputElement = document.querySelector(`input[name="search-${key}"]`) if (inputElement) { searchRef.current = inputElement as HTMLInputElement } @@ -52,9 +52,7 @@ export const CustomTable = >({ searchItems, onSele rowKey={(crd) => namespaceNameKey(crd)} rowSelection={{ onChange: (_, selectedRows) => { - if (onSelectRows) { - onSelectRows(selectedRows) - } + onSelectRows?.(selectedRows) }, }} tableAlertRender={({ selectedRowKeys, onCleanSelected }) => { @@ -70,7 +68,7 @@ export const CustomTable = >({ searchItems, onSele search={false} loading={{ spinning: loading, delay: 500, indicator: }} columnsState={{ - persistenceKey: storageKey, + persistenceKey: key, persistenceType: 'localStorage', onChange: (obj) => setScroll(calcScroll(obj)) }} @@ -107,7 +105,7 @@ export const CustomTable = >({ searchItems, onSele fullScreen: true, density: false, search: { - name: "search", + name: `search-${key}`, autoComplete: "off", allowClear: true, style: { width: 300 }, diff --git a/src/hooks/use-resource.ts b/src/hooks/use-resource.ts index ea62fbe..06d1486 100644 --- a/src/hooks/use-resource.ts +++ b/src/hooks/use-resource.ts @@ -1,161 +1,161 @@ -import { useCallback, useEffect, useRef, useState } from "react" -import { clients, getResourceName } from "@/clients/clients" -import { ResourceType } from "@/clients/ts/types/types" -import { isAbortedError } from "@/utils/utils" -import { EventType, WatchOptions } from "@/clients/ts/management/resource/v1alpha1/watch" -import { namespaceNameKey } from "@/utils/k8s" -import { App } from "antd" -import { useNamespaceFromURL } from "./use-query-params-from-url" -import { ListOptions } from "@/clients/ts/management/resource/v1alpha1/resource" -import useUnmount from "./use-unmount" - -export const useListResources = (resourceType: ResourceType, opts?: ListOptions) => { - const { notification } = App.useApp() - - const [resources, setResources] = useState([]) - const [error, setError] = useState(null) - - const fetchData = useCallback(async () => { - const call = clients.listResources(resourceType, ListOptions.create(opts)) - call.then(crds => { - setResources(crds) - setError(null) - }).catch(err => { - notification.error({ message: "Data fetching failed", description: err.message }) - setError(err.message) - }) - }, [opts]) - - useEffect(() => { - fetchData() - }, [fetchData]) - - return { resources, error } -} - -export const useWatchResources = (resourceType: ResourceType, opts?: WatchOptions, pause?: boolean) => { - console.log("Watching resources", getResourceName(resourceType), opts) - const abortCtrl = useRef(new AbortController()) - const initResourceRef = useRef(false) - const { notification } = App.useApp() - - const [loading, setLoading] = useState(true) - const [resources, setResources] = useState>(new Map()) - - const fetchData = useCallback(async () => { - console.log("Fetching data", getResourceName(resourceType), opts) - setLoading(true) - initResourceRef.current = true - abortCtrl.current.abort() - abortCtrl.current = new AbortController() - - const call = clients.watch.watch({ - resourceType: resourceType, - options: WatchOptions.create(opts), - }, { abort: abortCtrl.current.signal }) - - call.responses.onMessage((response) => { - switch (response.eventType) { - case EventType.ADDED: - case EventType.MODIFIED: { - if (response.items.length === 0 && initResourceRef.current && resources.size > 0) { - setResources(new Map()) - } - response.items.forEach((data) => { - const crd = JSON.parse(data) - - // const s = crd as V1alpha1VirtualMachineSummary - // console.log(s?.status?.dataVolumes?.[0]?.spec?.pvc?.resources?.limits, "==========") - // console.log(s?.status?.dataVolumes?.[0]?.spec?.pvc?.resources?.requests, "==========") - // console.log(s, "==========") - - // const summary = crd as components["schemas"]["v1alpha1VirtualMachineSummary"] - // console.log(summary?.status?.dataVolumes?.[0]?.spec?.pvc?.resources?.requests, "==========") - - setResources((prevResources) => { - const updatedResources = initResourceRef.current ? new Map() : new Map(prevResources) - initResourceRef.current = false - updatedResources.set(namespaceNameKey(crd), crd) - return updatedResources - }) - }) - break - } - case EventType.DELETED: { - response.items.forEach((data) => { - const crd = JSON.parse(data) - setResources((prevResources) => { - const updatedResources = new Map(prevResources) - updatedResources.delete(namespaceNameKey(crd)) - return updatedResources - }) - }) - break - } - } - setLoading(false) - }) - - call.responses.onError((err: Error) => { - setLoading(false) - if (isAbortedError(err)) { - return - } - notification.error({ - message: "Failed to watcher resources", - description: err.message - }) - }) - }, [opts]) - - useEffect(() => { - if (pause) { - return - } - fetchData() - }, [fetchData, pause]) - - useUnmount(() => { - console.log("Unmounting watcher", getResourceName(resourceType)) - setLoading(false) - abortCtrl.current?.abort() - }) - - return { resources, loading } -} - -export const useWatchResourceInNamespaceName = (resourceType: ResourceType) => { - const [resource, setResource] = useState(null) - const namespaceName = useNamespaceFromURL() - - const selector = { - fieldSelectorGroup: { - operator: "&&", - fieldSelectors: [ - { - fieldPath: 'metadata.name', - operator: '=', - values: [namespaceName.name] - } - ] - } - } - if (namespaceName.namespace.length > 0) { - selector.fieldSelectorGroup.fieldSelectors.unshift({ - fieldPath: 'metadata.namespace', - operator: '=', - values: [namespaceName.namespace] - }) - } - - const opts = useRef(WatchOptions.create(selector)) - - const { resources, loading } = useWatchResources(resourceType, opts.current) - - useEffect(() => { - const resourceData = resources.get(namespaceNameKey(namespaceName)) - setResource(resourceData) - }, [resources]) - - return { resource, loading } -} +// import { useCallback, useEffect, useRef, useState } from "react" +// import { getResourceName } from "@/clients/clients" +// import { ResourceType } from "@/clients/ts/types/types" +// import { isAbortedError } from "@/utils/utils" +// import { EventType, WatchOptions } from "@/clients/ts/management/resource/v1alpha1/watch" +// import { namespaceNameKey } from "@/utils/k8s" +// import { App } from "antd" +// import { useNamespaceFromURL } from "./use-query-params-from-url" +// import { ListOptions } from "@/clients/ts/management/resource/v1alpha1/resource" +// import useUnmount from "./use-unmount" + +// export const useListResources = (resourceType: ResourceType, opts?: ListOptions) => { +// const { notification } = App.useApp() + +// const [resources, setResources] = useState([]) +// const [error, setError] = useState(null) + +// const fetchData = useCallback(async () => { +// const call = clients.listResources(resourceType, ListOptions.create(opts)) +// call.then(crds => { +// setResources(crds) +// setError(null) +// }).catch(err => { +// notification.error({ message: "Data fetching failed", description: err.message }) +// setError(err.message) +// }) +// }, [opts]) + +// useEffect(() => { +// fetchData() +// }, [fetchData]) + +// return { resources, error } +// } + +// export const useWatchResources = (resourceType: ResourceType, opts?: WatchOptions, pause?: boolean) => { +// console.log("Watching resources", getResourceName(resourceType), opts) +// const abortCtrl = useRef(new AbortController()) +// const initResourceRef = useRef(false) +// const { notification } = App.useApp() + +// const [loading, setLoading] = useState(true) +// const [resources, setResources] = useState>(new Map()) + +// const fetchData = useCallback(async () => { +// console.log("Fetching data", getResourceName(resourceType), opts) +// setLoading(true) +// initResourceRef.current = true +// abortCtrl.current.abort() +// abortCtrl.current = new AbortController() + +// const call = clients.watch.watch({ +// resourceType: resourceType, +// options: WatchOptions.create(opts), +// }, { abort: abortCtrl.current.signal }) + +// call.responses.onMessage((response) => { +// switch (response.eventType) { +// case EventType.ADDED: +// case EventType.MODIFIED: { +// if (response.items.length === 0 && initResourceRef.current && resources.size > 0) { +// setResources(new Map()) +// } +// response.items.forEach((data) => { +// const crd = JSON.parse(data) + +// // const s = crd as V1alpha1VirtualMachineSummary +// // console.log(s?.status?.dataVolumes?.[0]?.spec?.pvc?.resources?.limits, "==========") +// // console.log(s?.status?.dataVolumes?.[0]?.spec?.pvc?.resources?.requests, "==========") +// // console.log(s, "==========") + +// // const summary = crd as components["schemas"]["v1alpha1VirtualMachineSummary"] +// // console.log(summary?.status?.dataVolumes?.[0]?.spec?.pvc?.resources?.requests, "==========") + +// setResources((prevResources) => { +// const updatedResources = initResourceRef.current ? new Map() : new Map(prevResources) +// initResourceRef.current = false +// updatedResources.set(namespaceNameKey(crd), crd) +// return updatedResources +// }) +// }) +// break +// } +// case EventType.DELETED: { +// response.items.forEach((data) => { +// const crd = JSON.parse(data) +// setResources((prevResources) => { +// const updatedResources = new Map(prevResources) +// updatedResources.delete(namespaceNameKey(crd)) +// return updatedResources +// }) +// }) +// break +// } +// } +// setLoading(false) +// }) + +// call.responses.onError((err: Error) => { +// setLoading(false) +// if (isAbortedError(err)) { +// return +// } +// notification.error({ +// message: "Failed to watcher resources", +// description: err.message +// }) +// }) +// }, [opts]) + +// useEffect(() => { +// if (pause) { +// return +// } +// fetchData() +// }, [fetchData, pause]) + +// useUnmount(() => { +// console.log("Unmounting watcher", getResourceName(resourceType)) +// setLoading(false) +// abortCtrl.current?.abort() +// }) + +// return { resources, loading } +// } + +// export const useWatchResourceInNamespaceName = (resourceType: ResourceType) => { +// const [resource, setResource] = useState(null) +// const namespaceName = useNamespaceFromURL() + +// const selector = { +// fieldSelectorGroup: { +// operator: "&&", +// fieldSelectors: [ +// { +// fieldPath: 'metadata.name', +// operator: '=', +// values: [namespaceName.name] +// } +// ] +// } +// } +// if (namespaceName.namespace.length > 0) { +// selector.fieldSelectorGroup.fieldSelectors.unshift({ +// fieldPath: 'metadata.namespace', +// operator: '=', +// values: [namespaceName.namespace] +// }) +// } + +// const opts = useRef(WatchOptions.create(selector)) + +// const { resources, loading } = useWatchResources(resourceType, opts.current) + +// useEffect(() => { +// const resourceData = resources.get(namespaceNameKey(namespaceName)) +// setResource(resourceData) +// }, [resources]) + +// return { resource, loading } +// } diff --git a/src/pages/compute/machine/components/data-disk-drawer/index.tsx b/src/pages/compute/machine/components/data-disk-drawer/index.tsx index 207d080..1ad00c2 100644 --- a/src/pages/compute/machine/components/data-disk-drawer/index.tsx +++ b/src/pages/compute/machine/components/data-disk-drawer/index.tsx @@ -2,14 +2,13 @@ import { App, Button, Drawer, Flex, Popover, Space, Tag } from 'antd' import { useEffect, useRef, useState } from 'react' import { instances as annotations } from "@/clients/ts/annotation/annotations.gen" import { instances as labels } from "@/clients/ts/label/labels.gen" -import { FieldSelector, ResourceType } from '@/clients/ts/types/types' +import { FieldSelector } from '@/clients/ts/types/types' import { getNamespaceFieldSelector, replaceDots } from '@/utils/search' import { useNamespaceFromURL } from '@/hooks/use-query-params-from-url' import { CustomTable, SearchItem } from '@/components/custom-table' -import { filterNullish, getErrorMessage } from '@/utils/utils' +import { filterNullish } from '@/utils/utils' import { WatchOptions } from '@/clients/ts/management/resource/v1alpha1/watch' import { DataVolume, watchDataVolumes } from '@/clients/data-volume' -import { getResourceName } from '@/clients/clients' import type { ProColumns } from '@ant-design/pro-components' import DataVolumeStatus from '@/components/datavolume-status' import useUnmount from '@/hooks/use-unmount' @@ -57,18 +56,15 @@ export const DataDiskDrawer: React.FC = ({ open, current, o const abortCtrl = useRef() useEffect(() => { + if (!open) { + return + } abortCtrl.current?.abort() abortCtrl.current = new AbortController() - watchDataVolumes(setDataVolumes, setLoading, abortCtrl.current.signal, opts).catch(err => { - notification.error({ - message: getResourceName(ResourceType.DATA_VOLUME), - description: getErrorMessage(err) - }) - }) - }, [opts]) + watchDataVolumes(setDataVolumes, setLoading, abortCtrl.current.signal, opts, notification) + }, [open, opts]) useUnmount(() => { - console.log("Unmounting watcher", getResourceName(ResourceType.DATA_VOLUME)) abortCtrl.current?.abort() }) @@ -77,11 +73,11 @@ export const DataDiskDrawer: React.FC = ({ open, current, o }, [namespaceName.namespace]) const handleCheckboxProps = (dv: DataVolume) => { - const binding = dv.metadata!.annotations?.[annotations.VinkDatavolumeOwner.name] - if (!binding || (binding as string).length == 0) { + const owner = dv.metadata!.annotations?.[annotations.VinkDatavolumeOwner.name] + if (!owner || (owner as string).length == 0) { return { disabled: false } } - const parse = JSON.parse(binding) + const parse = JSON.parse(owner) return { disabled: parse && parse.length > 0 } } @@ -176,7 +172,7 @@ export const DataDiskDrawer: React.FC = ({ open, current, o > loading={loading} - storageKey="data-disk-drawer-table-columns" + key="data-disk-drawer-table-columns" defaultFieldSelectors={defaultFieldSelectors} searchItems={searchItems} columns={columns} diff --git a/src/pages/compute/machine/components/management/index.tsx b/src/pages/compute/machine/components/management/index.tsx index c551bc2..9081564 100644 --- a/src/pages/compute/machine/components/management/index.tsx +++ b/src/pages/compute/machine/components/management/index.tsx @@ -1,17 +1,18 @@ import { VirtualMachinePowerStateRequest_PowerState } from '@/clients/ts/management/virtualmachine/v1alpha1/virtualmachine' import { NamespaceName, ResourceType } from '@/clients/ts/types/types' -import { getErrorMessage, openConsole } from '@/utils/utils' +import { openConsole } from '@/utils/utils' import { App, Button, Dropdown, MenuProps, Modal, Spin } from 'antd' import { EllipsisOutlined } from '@ant-design/icons' import { useState } from 'react' import { DataDiskDrawer } from '../data-disk-drawer' import { Link } from 'react-router-dom' import { NetworkDrawer } from '../network-drawer' -import { NetworkConfig, updateDataDisks, updateNetwork } from '../../virtualmachine' -import { extractNamespaceAndName, namespaceNameKey } from '@/utils/k8s' +import { generateDataDisks, generateNetwork } from '../../virtualmachine' +import { namespaceNameKey } from '@/utils/k8s' import { LoadingOutlined } from '@ant-design/icons' -import { clients, getPowerStateName, getResourceName } from '@/clients/clients' -import { deleteVirtualMachine, getVirtualMachine, manageVirtualMachinePowerState, VirtualMachine } from '@/clients/virtual-machine' +import { deleteVirtualMachine, getVirtualMachine, manageVirtualMachinePowerState, updateVirtualMachine, VirtualMachine } from '@/clients/virtual-machine' +import { DataVolume } from '@/clients/data-volume' +import { VirtualMachineNetworkType } from '@/clients/subnet' interface Props { namespace: NamespaceName @@ -32,77 +33,46 @@ const VirtualMachineManagement: React.FC = ({ namespace, type }) => { content = - + {/* */} } > @@ -165,7 +169,7 @@ export const NetworkDrawer: React.FC = ({ open, networkConfig, onC labelAlign="left" colon={false} formRef={formRef} - onReset={reset} + // onReset={reset} submitter={false} > = ({ open, networkConfig, onC label="Multus CR" name="multusCR" placeholder="选择 Multus CR" - initialValue={networkConfig?.multus} + initialValue={networkConfig?.multus ? namespaceNameKey(networkConfig.multus) : undefined} fieldProps={{ allowClear: false, showSearch: true, - onDropdownVisibleChange: (open) => { - if (!open) return - fetchMultusData() - } + onDropdownVisibleChange: handleMultusDropdownVisibleChange }} onChange={handleSelectMultus} options={ - multus.map((m: any) => ({ value: namespaceNameKey(m), label: namespaceNameKey(m) })) + multus?.map((m: any) => ({ value: namespaceNameKey(m), label: namespaceNameKey(m) })) } rules={[{ required: true, @@ -231,14 +232,11 @@ export const NetworkDrawer: React.FC = ({ open, networkConfig, onC fieldProps={{ allowClear: false, showSearch: true, - onDropdownVisibleChange: (open) => { - if (!open) return - fetchSubnetsData() - } + onDropdownVisibleChange: handleSubnetDropdownVisibleChange }} onChange={handleSelectSubnet} options={ - subnets.map((s: any) => ({ value: s.metadata.name, label: s.metadata.name })) + subnets?.map((s: any) => ({ value: s.metadata.name, label: s.metadata.name })) } rules={[{ required: true, @@ -254,14 +252,11 @@ export const NetworkDrawer: React.FC = ({ open, networkConfig, onC fieldProps={{ allowClear: false, showSearch: true, - onDropdownVisibleChange: (open) => { - if (!open) return - fetchIPPoolsData() - } + onDropdownVisibleChange: handleIPPoolDropdownVisibleChange }} onChange={handleSelectIPPool} options={ - ippools.map((p: any) => ({ value: p.metadata?.name, label: p.metadata?.name })) + ippools?.map((p: any) => ({ value: p.metadata?.name, label: p.metadata?.name })) } /> diff --git a/src/pages/compute/machine/components/root-disk-drawer/index.tsx b/src/pages/compute/machine/components/root-disk-drawer/index.tsx index f510a48..2da837c 100644 --- a/src/pages/compute/machine/components/root-disk-drawer/index.tsx +++ b/src/pages/compute/machine/components/root-disk-drawer/index.tsx @@ -2,13 +2,12 @@ import { App, Button, Drawer, Flex, Space } from 'antd' import { useEffect, useRef, useState } from 'react' import { namespaceNameKey } from '@/utils/k8s' import { instances as labels } from "@/clients/ts/label/labels.gen" -import { FieldSelector, ResourceType } from '@/clients/ts/types/types' +import { FieldSelector } from '@/clients/ts/types/types' import { replaceDots } from '@/utils/search' import { CustomTable, SearchItem } from '@/components/custom-table' -import { filterNullish, getErrorMessage } from '@/utils/utils' +import { filterNullish } from '@/utils/utils' import { WatchOptions } from '@/clients/ts/management/resource/v1alpha1/watch' import { DataVolume, watchDataVolumes } from '@/clients/data-volume' -import { getResourceName } from '@/clients/clients' import type { ProColumns } from '@ant-design/pro-components' import OperatingSystem from '@/components/operating-system' import DataVolumeStatus from '@/components/datavolume-status' @@ -23,6 +22,28 @@ interface RootDiskDrawerProps { const dvTypeSelector: FieldSelector = { fieldPath: `metadata.labels.${replaceDots(labels.VinkDatavolumeType.name)}`, operator: "=", values: ["image"] } +const searchItems: SearchItem[] = [ + { fieldPath: "metadata.name", name: "Name", operator: "*=" }, + { + fieldPath: "status.phase", name: "Status", + items: [ + { inputValue: "Succeeded", values: ["Succeeded"], operator: '=' }, + { inputValue: "Failed", values: ["Failed"], operator: '=' }, + { inputValue: 'Provisioning', values: ["ImportInProgress", "CloneInProgress", "CloneFromSnapshotSourceInProgress", "SmartClonePVCInProgress", "CSICloneInProgress", "ExpansionInProgress", "NamespaceTransferInProgress", "PrepClaimInProgress", "RebindInProgress"], operator: '~=' }, + ] + }, + { + fieldPath: `metadata.labels.${replaceDots("vink.kubevm.io/virtualmachine.os")}`, name: "OS", + items: [ + { inputValue: "Ubuntu", values: ["Ubuntu"], operator: '=' }, + { inputValue: "CentOS", values: ["CentOS"], operator: '=' }, + { inputValue: "Debian", values: ["Debian"], operator: '=' }, + { inputValue: "Linux", values: ["Linux", "Ubuntu", "CentOS", "Debian"], operator: '~=' }, + { inputValue: "Windows", values: ["Windows"], operator: '=' }, + ] + } +] + export const RootDiskDrawer: React.FC = ({ open, current, onCanel, onConfirm }) => { const { notification } = App.useApp() @@ -41,22 +62,19 @@ export const RootDiskDrawer: React.FC = ({ open, current, o const abortCtrl = useRef() useEffect(() => { + if (!open) { + return + } abortCtrl.current?.abort() abortCtrl.current = new AbortController() - watchDataVolumes(setDataVolumes, setLoading, abortCtrl.current.signal, opts).catch(err => { - notification.error({ - message: getResourceName(ResourceType.DATA_VOLUME), - description: getErrorMessage(err) - }) - }) - }, [opts]) + watchDataVolumes(setDataVolumes, setLoading, abortCtrl.current.signal, opts, notification) + }, [open, opts]) useUnmount(() => { - console.log("Unmounting watcher", getResourceName(ResourceType.DATA_VOLUME)) abortCtrl.current?.abort() }) - const columns: ProColumns[] = [ + const columns: ProColumns[] = [ { title: '名称', key: 'name', @@ -79,7 +97,7 @@ export const RootDiskDrawer: React.FC = ({ open, current, o title: '容量', key: 'capacity', ellipsis: true, - render: (_, dv) => dv.spec?.pvc?.resources?.requests?.storage + render: (_, dv) => dv.spec.pvc?.resources?.requests?.storage } ] @@ -107,7 +125,7 @@ export const RootDiskDrawer: React.FC = ({ open, current, o > = ({ open, current, o ) } - -const searchItems: SearchItem[] = [ - { fieldPath: "metadata.name", name: "Name", operator: "*=" }, - { - fieldPath: "status.phase", name: "Status", - items: [ - { inputValue: "Succeeded", values: ["Succeeded"], operator: '=' }, - { inputValue: "Failed", values: ["Failed"], operator: '=' }, - { inputValue: 'Provisioning', values: ["ImportInProgress", "CloneInProgress", "CloneFromSnapshotSourceInProgress", "SmartClonePVCInProgress", "CSICloneInProgress", "ExpansionInProgress", "NamespaceTransferInProgress", "PrepClaimInProgress", "RebindInProgress"], operator: '~=' }, - ] - }, - { - fieldPath: `metadata.labels.${replaceDots("vink.kubevm.io/virtualmachine.os")}`, name: "OS", - items: [ - { inputValue: "Ubuntu", values: ["Ubuntu"], operator: '=' }, - { inputValue: "CentOS", values: ["CentOS"], operator: '=' }, - { inputValue: "Debian", values: ["Debian"], operator: '=' }, - { inputValue: "Linux", values: ["Linux", "Ubuntu", "CentOS", "Debian"], operator: '~=' }, - { inputValue: "Windows", values: ["Windows"], operator: '=' }, - ] - }, -] diff --git a/src/pages/compute/machine/create/index.tsx b/src/pages/compute/machine/create/index.tsx index 4805c34..63cb00f 100644 --- a/src/pages/compute/machine/create/index.tsx +++ b/src/pages/compute/machine/create/index.tsx @@ -1,19 +1,20 @@ -import { FooterToolbar, ProCard, ProForm, ProFormItem, ProFormSelect, ProFormText } from '@ant-design/pro-components' +import { FooterToolbar, ProCard, ProForm, ProFormItem, ProFormText } from '@ant-design/pro-components' import { App, Button, Divider, Flex, InputNumber, Space, Table, TableProps } from 'antd' import { useEffect, useRef, useState } from 'react' -import { formatMemory, namespaceNameKey } from '@/utils/k8s' -import { capacity, classNames, getErrorMessage } from '@/utils/utils' +import { namespaceNameKey, parseMemoryValue } from '@/utils/k8s' +import { classNames } from '@/utils/utils' import { DataDiskDrawer } from '@/pages/compute/machine/components/data-disk-drawer' import { useNavigate } from 'react-router-dom' import { useNamespace } from '@/common/context' import { yaml as langYaml } from "@codemirror/lang-yaml" -import { ResourceType } from '@/clients/ts/types/types' -import { clients, getResourceName } from '@/clients/clients' import { PlusOutlined } from '@ant-design/icons' -import { NetworkConfig, newVirtualMachine } from '../virtualmachine' +import { newVirtualMachine } from '../virtualmachine' import { NetworkDrawer } from '../components/network-drawer' -import { defaultCloudInit } from '../virtualmachine/template' +import { defaultCloudInit } from '../virtualmachine' import { RootDiskDrawer } from '../components/root-disk-drawer' +import { DataVolume } from '@/clients/data-volume' +import { createVirtualMachine } from '@/clients/virtual-machine' +import { VirtualMachineNetworkType } from '@/clients/subnet' import type { ProFormInstance } from '@ant-design/pro-components' import OperatingSystem from '@/components/operating-system' import styles from '@/pages/compute/machine/create/styles/index.module.less' @@ -22,96 +23,171 @@ import codeMirrorStyles from "@/common/styles/code-mirror.module.less" import formStyles from "@/common/styles/form.module.less" import commonStyles from "@/common/styles/common.module.less" +type formValues = { + name: string + namespace: string + cpu: number + memory: number + rootDisk: DataVolume + rootDiskCapacity: number + dataDisks: DataVolume[] + networks: VirtualMachineNetworkType[] + cloudInit: string +} + export default () => { const { notification } = App.useApp() const { namespace } = useNamespace() - const formRef = useRef() + const formRef = useRef>() const navigate = useNavigate() const [openDrawer, setOpenDrawer] = useState({ rootDisk: false, network: false, dataDisk: false }) useEffect(() => { - formRef.current?.setFieldValue("namespace", namespace) + formRef.current?.setFieldsValue({ namespace: namespace }) if (namespace) { formRef.current?.validateFields(["namespace"]) } }, [namespace]) useEffect(() => { - return formRef.current?.setFieldValue("cloudInit", defaultCloudInit) + return formRef.current?.setFieldsValue({ cloudInit: defaultCloudInit }) }, [defaultCloudInit]) - const dataDiskcolumns = getDataDiskColumns(formRef) - const networkColumns = getNetworkColumns(formRef) - - const [inputCpuValue, setInputCpuValue] = useState() - const [inputMemValue, setInputMemValue] = useState() + const handleSubmit = async () => { + if (!formRef.current) { + throw new Error("formRef is not initialized") + } - const [_, setReset] = useState(false) + await formRef.current.validateFields() + const fields = formRef.current.getFieldsValue() - const createOptions = (count: number, multiplier: number, unit: string) => { - return Array.from({ length: count }, (_, i) => ({ - label: `${(i + 1) * multiplier} ${unit}`, - value: (i + 1) * multiplier - })) - } + const ns = { name: fields.name, namespace: fields.namespace } + const cpuMem = { cpu: fields.cpu, memory: fields.memory } + const rootDisk = { image: fields.rootDisk, capacity: fields.rootDiskCapacity } + const instance = newVirtualMachine(ns, cpuMem, rootDisk, fields.dataDisks || [], fields.networks, fields.cloudInit) - const [cpuOptions, setCpuOptions] = useState(createOptions(32, 2, "Core")) - const [memOptions, setMemOptions] = useState(createOptions(32, 2, "Gi")) + await createVirtualMachine(instance, undefined, undefined, notification) - const handleAddNewOption = ( - inputValue: number | undefined, - options: { label: string; value: number }[], - setOptions: React.Dispatch>, - fieldName: "cpu" | "memory" - ) => { - if (!inputValue || !formRef.current) { - return - } - const unit = fieldName === "cpu" ? "Core" : "Gi" - if (!options.some(opt => opt.value === inputValue)) { - const newOption = { label: `${inputValue} ${unit}`, value: inputValue } - setOptions([...options, newOption]) - formRef.current.setFieldsValue({ [fieldName]: inputValue }) - } + navigate('/compute/machines') } - const handleSubmit = async () => { - if (!formRef.current) { - return + const dataDiskColumns: TableProps['columns'] = [ + { + title: '名称', + key: 'name', + ellipsis: true, + render: (_, dv) => dv.metadata!.name + }, + { + title: '存储类', + key: 'storageClassName', + ellipsis: true, + render: (_, dv) => dv.spec.pvc?.storageClassName + }, + { + title: '访问模式', + key: 'capacity', + ellipsis: true, + render: (_, dv) => dv.spec.pvc?.accessModes?.[0] + }, + { + title: '容量', + key: 'capacity', + ellipsis: true, + render: (_, dv) => dv.spec.pvc?.resources?.requests?.storage + }, + { + title: '操作', + key: 'action', + width: 100, + align: 'center', + render: (_, dv) => ( { + const dataDisks = formRef.current?.getFieldsValue().dataDisks + const newDisks = dataDisks?.filter((item) => !(namespaceNameKey(item) === namespaceNameKey(dv))) + formRef.current?.setFieldsValue({ dataDisks: newDisks }) + }}>移除) } + ] - try { - await formRef.current.validateFields() - - const fields = formRef.current.getFieldsValue() - - const namespaceName = { name: fields.name, namespace: fields.namespace } - const cpuMem = { cpu: fields.cpu, memory: fields.memory } - const rootDisk = { image: fields.rootDisk, capacity: fields.rootDiskCapacity } - const instance = newVirtualMachine( - namespaceName, - cpuMem, - rootDisk, - fields.dataDisks || [], - fields.networks, - fields.cloudInit - ) - await clients.createResource(ResourceType.VIRTUAL_MACHINE, instance) - navigate('/compute/machines') - } catch (err: any) { - const errorMessage = err.errorFields?.map((field: any, idx: number) => `${idx + 1}. ${field.errors}`).join('
') || getErrorMessage(err) - notification.error({ - message: getResourceName(ResourceType.VIRTUAL_MACHINE), - description: ( -
- ) - }) + const networkColumns: TableProps['columns'] = [ + { + title: 'Network', + key: 'network', + ellipsis: true, + render: (_, cfg) => cfg.network + }, + { + title: 'Interface', + key: 'interface', + ellipsis: true, + render: (_, cfg) => cfg.interface + }, + { + title: '默认网络', + key: 'default', + ellipsis: true, + render: (_, cfg) => cfg.default ? "是" : "否" + }, + { + title: 'Multus CR', + key: 'multusCR', + ellipsis: true, + render: (_, cfg) => namespaceNameKey(cfg.multus) + }, + { + title: 'VPC', + key: 'vpc', + ellipsis: true, + render: (_, cfg) => { + if (typeof cfg.subnet === 'object') { + return cfg.subnet?.spec?.vpc + } + } + }, + { + title: '子网', + key: 'subnet', + ellipsis: true, + render: (_, cfg) => typeof cfg.subnet === 'string' ? cfg.subnet : cfg.subnet?.metadata!.name + }, + { + title: 'IP 地址池', + key: 'ippool', + ellipsis: true, + render: (_, cfg) => { + const ippool = typeof cfg.ippool === 'string' ? cfg.ippool : cfg.ippool?.metadata!.name + return ippool || "自动分配" + }, + }, + { + title: 'IP 地址', + key: 'ipAddress', + ellipsis: true, + render: (_, cfg) => cfg.ipAddress || "自动分配" + }, + { + title: 'MAC 地址', + key: 'macAddress', + ellipsis: true, + render: (_, cfg) => cfg.macAddress || "自动分配" + }, + { + title: '操作', + key: 'action', + width: 100, + align: 'center', + fixed: 'right', + render: (_, cfg) => ( { + const networks = formRef.current?.getFieldsValue().networks + const newNetworks = networks?.filter((item) => item.multus !== cfg.multus) + formRef.current?.setFieldsValue({ networks: newNetworks }) + }}>移除) } - } + ] return ( <> @@ -122,13 +198,10 @@ export default () => { labelAlign="left" colon={false} formRef={formRef} - onReset={() => { - formRef.current?.resetFields() - setReset((pre) => !pre) - }} submitter={{ onSubmit: handleSubmit, - render: (_, dom) => {dom} + // no need to reset form + render: (_, dom) => {dom?.[1]} }} > { rules={[{ required: true, validator() { - return formRef.current?.getFieldValue("rootDisk") ? Promise.resolve() : Promise.reject() + return formRef.current?.getFieldsValue().rootDisk ? Promise.resolve() : Promise.reject() }, message: "添加操作系统" }]} > { - formRef.current?.getFieldValue("rootDisk")?.metadata.name ? ( + formRef.current?.getFieldsValue().rootDisk ? ( - + - {formRef.current?.getFieldValue("rootDisk").metadata.name} + {formRef.current?.getFieldsValue().rootDisk?.metadata!.name} - {capacity(formRef.current?.getFieldValue("rootDisk"))} + {formRef.current?.getFieldsValue().rootDisk?.spec?.pvc?.resources?.requests?.storage} ) : ("") } @@ -197,90 +270,43 @@ export default () => { - ( - <> - {menu} - -
- setInputCpuValue(value || undefined)} - placeholder="新增处理器核心数" - /> - -
- - ) - }} rules={[{ required: true, - pattern: /^[1-9]\d*$/, - message: "选择处理器核心数" + message: "输入处理器核心数" }]} - /> - + + min={1} + max={1024} + step={1} + style={{ width: 440 }} + formatter={(value) => `${value} Core`} + parser={(value) => value?.replace(' Core', '') as unknown as number} + /> + + + ( - <> - {menu} - -
- setInputMemValue(value || undefined)} - placeholder="新增内存大小" - /> - -
- - ) - }} rules={[{ required: true, - pattern: /^[1-9]\d*$/, - message: "选择内存大小" + message: "输入内存大小" }]} - /> + > + + min={1} + max={1024} + step={1} + style={{ width: 440 }} + formatter={(value) => `${value} Gi`} + parser={(value) => value?.replace(' Gi', '') as unknown as number} + /> +
@@ -311,13 +337,24 @@ export default () => { rules={[{ required: true, validator() { - const rootDiskCapacity = formRef.current?.getFieldValue("rootDiskCapacity") - const rootDisk = formRef.current?.getFieldValue("rootDisk") - if (!rootDisk) { + const fields = formRef.current?.getFieldsValue() + const rootDiskCapacity = fields?.rootDiskCapacity + const rootDiskStorage = fields?.rootDisk?.spec?.pvc?.resources?.requests?.storage + if (!rootDiskStorage || !rootDiskCapacity) { return Promise.resolve() } - const [value] = formatMemory(rootDisk.spec.pvc.resources.requests.storage) - if (rootDiskCapacity <= parseInt(value)) { + + let value = 0 + if (typeof rootDiskStorage === "string") { + const parse = parseMemoryValue(rootDiskStorage) + if (parse) { + value = parse[0] + } + } else { + value = rootDiskStorage + } + + if (rootDiskCapacity <= value) { return Promise.reject() } return Promise.resolve() @@ -340,12 +377,14 @@ export default () => { label="数据盘" > { - formRef.current?.getFieldValue("dataDisks") && ( + formRef.current?.getFieldsValue().dataDisks && ( namespaceNameKey(dv.metadata)} + columns={dataDiskColumns} + dataSource={formRef.current?.getFieldsValue().dataDisks} + pagination={false} + rowKey={(dv) => namespaceNameKey(dv)} /> ) } @@ -367,7 +406,7 @@ export default () => { rules={[{ required: true, validator() { - const networks = formRef.current?.getFieldValue("networks") + const networks = formRef.current?.getFieldsValue().networks if (!networks || networks.length == 0) { return Promise.reject() } @@ -377,13 +416,15 @@ export default () => { }]} > { - formRef.current?.getFieldValue("networks") && ( + formRef.current?.getFieldsValue().networks && (
namespaceNameKey(cfg.multus)} /> ) @@ -407,7 +448,7 @@ export default () => { maxHeight="100vh" extensions={[langYaml()]} onChange={(value) => { - formRef.current?.setFieldValue("cloudInit", value) + formRef.current?.setFieldsValue({ cloudInit: value }) }} /> @@ -418,10 +459,10 @@ export default () => { setOpenDrawer((prevState) => ({ ...prevState, rootDisk: false }))} - current={formRef.current?.getFieldValue("rootDisk")} + current={formRef.current?.getFieldsValue().rootDisk} onConfirm={(data) => { setOpenDrawer((prevState) => ({ ...prevState, rootDisk: false })) - formRef.current?.setFieldValue("rootDisk", data) + formRef.current?.setFieldsValue({ rootDisk: data }) formRef.current?.validateFields(["rootDisk"]) }} /> @@ -430,10 +471,10 @@ export default () => { open={openDrawer.dataDisk} onCanel={() => setOpenDrawer((prevState) => ({ ...prevState, dataDisk: false }))} - current={formRef.current?.getFieldValue("dataDisks")} + current={formRef.current?.getFieldsValue().dataDisks} onConfirm={(data) => { setOpenDrawer((prevState) => ({ ...prevState, dataDisk: false })) - formRef.current?.setFieldValue("dataDisks", data) + formRef.current?.setFieldsValue({ dataDisks: data }) formRef.current?.validateFields(["dataDisks"]) }} /> @@ -442,7 +483,7 @@ export default () => { open={openDrawer.network} onCanel={() => setOpenDrawer((prevState) => ({ ...prevState, network: false }))} onConfirm={(data) => { - const networks = formRef.current?.getFieldValue("networks") || [] + const networks = formRef.current?.getFieldsValue().networks || [] const hasElement = networks.some((cfg: any) => { return namespaceNameKey(cfg.multus) === namespaceNameKey(data.multus) }) @@ -451,129 +492,10 @@ export default () => { } const newNetworks = networks.concat(data) setOpenDrawer((prevState) => ({ ...prevState, network: false })) - formRef.current?.setFieldValue("networks", newNetworks) + formRef.current?.setFieldsValue({ networks: newNetworks }) formRef.current?.validateFields(["networks"]) }} /> ) } - -const getDataDiskColumns = (formRef: React.MutableRefObject) => { - const dataDiskColumns: TableProps['columns'] = [ - { - title: '名称', - key: 'name', - ellipsis: true, - render: (_, dv) => dv.metadata.name - }, - { - title: '存储类', - key: 'storageClassName', - ellipsis: true, - render: (_, dv) => dv.spec.pvc.storageClassName - }, - { - title: '访问模式', - key: 'capacity', - ellipsis: true, - render: (_, dv) => dv.spec.pvc.accessModes?.[0] - }, - { - title: '容量', - key: 'capacity', - ellipsis: true, - render: (_, dv) => capacity(dv) - }, - { - title: '操作', - key: 'action', - width: 100, - align: 'center', - render: (_, dv) => ( { - if (!formRef.current) { - return - } - const dataDisks = formRef.current.getFieldValue('dataDisks') - const newDisks = dataDisks.filter((item: any) => !(namespaceNameKey(item) === namespaceNameKey(dv))) - formRef.current.setFieldValue('dataDisks', newDisks) - }}>移除) - } - ] - return dataDiskColumns -} - -const getNetworkColumns = (formRef: React.MutableRefObject) => { - const networkColumns: TableProps['columns'] = [ - { - title: 'Network', - key: 'network', - ellipsis: true, - render: (_, cfg) => cfg.network - }, - { - title: 'Interface', - key: 'interface', - ellipsis: true, - render: (_, cfg) => cfg.interface - }, - { - title: '默认网络', - key: 'default', - ellipsis: true, - render: (_, cfg) => cfg.default ? "是" : "否" - }, - { - title: 'Multus CR', - key: 'multusCR', - ellipsis: true, - render: (_, cfg) => namespaceNameKey(cfg.multus) - }, - { - title: 'VPC', - key: 'vpc', - ellipsis: true, - render: (_, cfg) => cfg.subnet?.spec.vpc - }, - { - title: '子网', - key: 'subnet', - ellipsis: true, - render: (_, cfg) => cfg.subnet?.metadata.name - }, - { - title: 'IP 地址池', - key: 'ippool', - ellipsis: true, - render: (_, cfg) => cfg.ippool?.metadata.name || "自动分配" - }, - { - title: 'IP 地址', - key: 'ipAddress', - ellipsis: true, - render: (_, cfg) => cfg.ipAddress || "自动分配" - }, - { - title: 'MAC 地址', - key: 'macAddress', - ellipsis: true, - render: (_, cfg) => cfg.macAddress || "自动分配" - }, - { - title: '操作', - key: 'action', - width: 100, - align: 'center', - fixed: 'right', - render: (_, cfg) => ( { - if (!formRef.current) { - return - } - const networks = formRef.current.getFieldValue('networks') - const newNetworks = networks.filter((item: any) => item.multus !== cfg.multus) - formRef.current.setFieldValue('networks', newNetworks) - }}>移除) - } - ] - return networkColumns -} diff --git a/src/pages/compute/machine/detail/event.tsx b/src/pages/compute/machine/detail/event.tsx index 3b76db6..68fa1c0 100644 --- a/src/pages/compute/machine/detail/event.tsx +++ b/src/pages/compute/machine/detail/event.tsx @@ -1,5 +1,4 @@ -import { ResourceType } from "@/clients/ts/types/types" -import { classNames, formatTimestamp, getErrorMessage } from "@/utils/utils" +import { classNames, formatTimestamp } from "@/utils/utils" import { App, Button, Drawer, Flex, Table, TableProps } from "antd" import { LoadingOutlined } from '@ant-design/icons' import { useEffect, useRef, useState } from "react" @@ -7,7 +6,6 @@ import { useNamespaceFromURL } from "@/hooks/use-query-params-from-url" import { namespaceNameKey } from "@/utils/k8s" import { yaml as langYaml } from "@codemirror/lang-yaml" import { watchVirtualMachineEvents } from "@/clients/event" -import { getResourceName } from "@/clients/clients" import CodeMirror from '@uiw/react-codemirror' import codeMirrorStyles from "@/common/styles/code-mirror.module.less" import commonStyles from "@/common/styles/common.module.less" @@ -32,16 +30,10 @@ export default () => { useEffect(() => { abortCtrl.current?.abort() abortCtrl.current = new AbortController() - watchVirtualMachineEvents(ns, setEvents, setLoading, abortCtrl.current.signal).catch(err => { - notification.error({ - message: getResourceName(ResourceType.EVENT), - description: getErrorMessage(err) - }) - }) + watchVirtualMachineEvents(ns, setEvents, setLoading, abortCtrl.current.signal, notification) }, [ns]) useUnmount(() => { - console.log("Unmounting watcher", getResourceName(ResourceType.EVENT)) abortCtrl.current?.abort() }) diff --git a/src/pages/compute/machine/detail/network.tsx b/src/pages/compute/machine/detail/network.tsx index 86e5092..54cb8de 100644 --- a/src/pages/compute/machine/detail/network.tsx +++ b/src/pages/compute/machine/detail/network.tsx @@ -1,16 +1,15 @@ import { useEffect, useRef, useState } from 'react' import { App, Modal, Space, Table, TableProps } from 'antd' -import { classNames, getErrorMessage } from '@/utils/utils' -import { ResourceType } from '@/clients/ts/types/types' -import { extractNamespaceAndName, namespaceNameKey } from '@/utils/k8s' +import { classNames } from '@/utils/utils' +import { namespaceNameKey } from '@/utils/k8s' import { LoadingOutlined } from '@ant-design/icons' -import { clients, getResourceName } from '@/clients/clients' -import { deleteNetwork, NetworkConfig, updateNetwork } from '../virtualmachine' +import { deleteNetwork, generateNetwork } from '../virtualmachine' import { NetworkDrawer } from '../components/network-drawer' import { VirtualMachinePowerStateRequest_PowerState } from '@/clients/ts/management/virtualmachine/v1alpha1/virtualmachine' import { useNamespaceFromURL } from '@/hooks/use-query-params-from-url' import { VirtualMachineSummary, watchVirtualMachineSummary } from '@/clients/virtual-machine-summary' -import { listSubnetsForVirtualMachine, VirtualMachineNetworkDataSourceType } from '@/clients/subnet' +import { listVirtualMachineNetwork, VirtualMachineNetworkType } from '@/clients/subnet' +import { getVirtualMachine, manageVirtualMachinePowerState, updateVirtualMachine } from '@/clients/virtual-machine' import useUnmount from '@/hooks/use-unmount' import commonStyles from '@/common/styles/common.module.less' @@ -21,67 +20,53 @@ export default () => { const [open, setOpen] = useState(false) - const [networkConfig, setNetworkConfig] = useState() + const [networkConfig, setNetworkConfig] = useState() const abortCtrl = useRef() + const [loading, setLoading] = useState(true) + const [summary, setSummary] = useState() const [vmNetLoading, setVMNetLoading] = useState(true) - const [virtualMachineNetworks, setVirtualMachineNetworks] = useState() + + const [virtualMachineNetworks, setVirtualMachineNetworks] = useState() useEffect(() => { abortCtrl.current?.abort() abortCtrl.current = new AbortController() - watchVirtualMachineSummary(ns, setSummary, setLoading, abortCtrl.current.signal).catch(err => { - notification.error({ - message: getResourceName(ResourceType.VIRTUAL_MACHINE_SUMMARY), - description: getErrorMessage(err) - }) - }) + watchVirtualMachineSummary(ns, setSummary, setLoading, abortCtrl.current.signal, notification) }, [ns]) useEffect(() => { if (!summary) { return } - setVMNetLoading(true) - listSubnetsForVirtualMachine(summary).then(items => { - setVirtualMachineNetworks(items) - }).catch(err => { - notification.error({ - message: getResourceName(ResourceType.VIRTUAL_MACHINE_SUMMARY), - description: getErrorMessage(err) - }) - }).finally(() => { - setVMNetLoading(false) - }) + listVirtualMachineNetwork(summary, setVirtualMachineNetworks, setVMNetLoading, notification) }, [summary]) useUnmount(() => { - console.log("Unmounting watcher", getResourceName(ResourceType.VIRTUAL_MACHINE_SUMMARY)) abortCtrl.current?.abort() }) - const handleConfirmNetwork = async (networkConfig: NetworkConfig) => { - const namespace = summary?.metadata?.namespace - const name = summary?.metadata?.name - if (!namespace || !name) { + const handleConfirmNetwork = async (networkConfig: VirtualMachineNetworkType) => { + if (!summary) { return } - - try { - const vm = await clients.getResource(ResourceType.VIRTUAL_MACHINE, { namespace, name }) - updateNetwork(vm, networkConfig) - await clients.updateResource(ResourceType.VIRTUAL_MACHINE, vm) - await clients.manageVirtualMachinePowerState({ namespace, name }, VirtualMachinePowerStateRequest_PowerState.REBOOT) - setOpen(false) - } catch (err: any) { - notification.error({ message: getResourceName(ResourceType.VIRTUAL_MACHINE), description: getErrorMessage(err) }) - } + const ns = { namespace: summary.metadata!.namespace, name: summary.metadata!.name } + const vm = await getVirtualMachine(ns, undefined, undefined, notification) + generateNetwork(vm, networkConfig) + await updateVirtualMachine(vm, undefined, undefined, notification) + await manageVirtualMachinePowerState(ns, VirtualMachinePowerStateRequest_PowerState.REBOOT, undefined, notification) + setOpen(false) } const handleRemoveNetwork = (netName: string) => { + if (!summary) { + return + } + const ns = { namespace: summary.metadata!.namespace, name: summary.metadata!.name } + Modal.confirm({ title: "Remove Network?", content: `You are about to remove the network "${namespaceNameKey(summary)}". Please confirm.`, @@ -90,19 +75,15 @@ export default () => { cancelText: 'Cancel', okButtonProps: { disabled: false }, onOk: async () => { - try { - const vm = await clients.getResource(ResourceType.VIRTUAL_MACHINE, extractNamespaceAndName(summary)) - deleteNetwork(vm, netName) - await clients.updateResource(ResourceType.VIRTUAL_MACHINE, vm) - await clients.manageVirtualMachinePowerState(extractNamespaceAndName(vm), VirtualMachinePowerStateRequest_PowerState.REBOOT) - } catch (err: any) { - notification.error({ message: getResourceName(ResourceType.VIRTUAL_MACHINE), description: getErrorMessage(err) }) - } + const vm = await getVirtualMachine(ns, undefined, undefined, notification) + deleteNetwork(vm, netName) + await updateVirtualMachine(vm, undefined, undefined, notification) + await manageVirtualMachinePowerState(ns, VirtualMachinePowerStateRequest_PowerState.REBOOT, undefined, notification) } }) } - const columns: TableProps['columns'] = [ + const columns: TableProps['columns'] = [ { title: 'Network', dataIndex: "network", @@ -115,7 +96,6 @@ export default () => { }, { title: '默认网络', - dataIndex: "default", ellipsis: true, render: (_, record) => record.default ? "是" : "否" }, @@ -131,8 +111,8 @@ export default () => { }, { title: 'Multus CR', - dataIndex: 'multus', - ellipsis: true + ellipsis: true, + render: (_, record) => record.multus ? namespaceNameKey(record.multus) : "" }, { title: 'VPC', @@ -161,7 +141,7 @@ export default () => { setNetworkConfig(record) setOpen(true) }}>编辑 - handleRemoveNetwork(record.name)}>删除 + handleRemoveNetwork(record.name!)}>删除 ) } diff --git a/src/pages/compute/machine/detail/overview.tsx b/src/pages/compute/machine/detail/overview.tsx index 7b4bcc8..78c603d 100644 --- a/src/pages/compute/machine/detail/overview.tsx +++ b/src/pages/compute/machine/detail/overview.tsx @@ -1,14 +1,12 @@ import { App, Space, Spin } from "antd" import { useEffect, useRef, useState } from "react" -import { ResourceType } from "@/clients/ts/types/types" -import { clients, getResourceName } from "@/clients/clients" import { ProCard, ProDescriptions } from "@ant-design/pro-components" import { LoadingOutlined } from '@ant-design/icons' -import { classNames, formatTimestamp, getErrorMessage, updateNestedValue } from "@/utils/utils" +import { classNames, formatTimestamp, updateNestedValue } from "@/utils/utils" import { yaml as langYaml } from "@codemirror/lang-yaml" import { useNamespaceFromURL } from "@/hooks/use-query-params-from-url" import { DataVolume, getRootDisk } from "@/clients/data-volume" -import { VirtualMachine, watchVirtualMachine } from "@/clients/virtual-machine" +import { updateVirtualMachine, VirtualMachine, watchVirtualMachine } from "@/clients/virtual-machine" import CodeMirror from '@uiw/react-codemirror' import codeMirrorStyles from "@/common/styles/code-mirror.module.less" import commonStyles from "@/common/styles/common.module.less" @@ -25,6 +23,7 @@ export default () => { const ns = useNamespaceFromURL() const [loading, setLoading] = useState(true) + const [virtualMachine, setVirtualMachine] = useState() const abortCtrl = useRef() @@ -32,12 +31,7 @@ export default () => { useEffect(() => { abortCtrl.current?.abort() abortCtrl.current = new AbortController() - watchVirtualMachine(ns, setVirtualMachine, setLoading, abortCtrl.current.signal).catch(err => { - notification.error({ - message: getResourceName(ResourceType.VIRTUAL_MACHINE), - description: getErrorMessage(err) - }) - }) + watchVirtualMachine(ns, setVirtualMachine, setLoading, abortCtrl.current.signal) }, [ns]) const [rootDiskLoading, setRootDiskLoading] = useState(true) @@ -47,18 +41,10 @@ export default () => { if (!virtualMachine) { return } - getRootDisk(setRootDiskLoading, virtualMachine).then(dv => { - setRootDisk(dv) - }).catch(err => { - notification.error({ - message: getResourceName(ResourceType.DATA_VOLUME), - description: getErrorMessage(err) - }) - }) + getRootDisk(virtualMachine, setRootDisk, setRootDiskLoading) }, [virtualMachine]) useUnmount(() => { - console.log("Unmounting watcher", getResourceName(ResourceType.VIRTUAL_MACHINE)) abortCtrl.current?.abort() }) @@ -74,16 +60,9 @@ export default () => { } const handleSave = async (keypath: any, newInfo: any, oriInfo: any) => { - try { - const deepCopyOriInfo = JSON.parse(JSON.stringify(oriInfo)) - updateNestedValue(keypath as string[], newInfo, deepCopyOriInfo, true) - await clients.updateResource(ResourceType.VIRTUAL_MACHINE, deepCopyOriInfo) - } catch (err) { - notification.error({ - message: getResourceName(ResourceType.VIRTUAL_MACHINE), - description: getErrorMessage(err) - }) - } + const deepCopyOriInfo = JSON.parse(JSON.stringify(oriInfo)) + updateNestedValue(keypath as string[], newInfo, deepCopyOriInfo, true) + await updateVirtualMachine(deepCopyOriInfo, undefined, undefined, notification) } return ( diff --git a/src/pages/compute/machine/detail/storage.tsx b/src/pages/compute/machine/detail/storage.tsx index c471978..caac0f3 100644 --- a/src/pages/compute/machine/detail/storage.tsx +++ b/src/pages/compute/machine/detail/storage.tsx @@ -1,9 +1,6 @@ -import { ResourceType } from "@/clients/ts/types/types" -import { getErrorMessage } from "@/utils/utils" import { App, Badge, Modal, Space, Table, TableProps } from "antd" import { instances as labels } from '@/clients/ts/label/labels.gen' import { LoadingOutlined, StopOutlined } from '@ant-design/icons' -import { getResourceName } from "@/clients/clients" import { VirtualMachinePowerStateRequest_PowerState } from "@/clients/ts/management/virtualmachine/v1alpha1/virtualmachine" import { useEffect, useRef, useState } from "react" import { VirtualMachineSummary, watchVirtualMachineSummary } from "@/clients/virtual-machine-summary" @@ -26,16 +23,10 @@ export default () => { useEffect(() => { abortCtrl.current?.abort() abortCtrl.current = new AbortController() - watchVirtualMachineSummary(ns, setSummary, setLoading, abortCtrl.current?.signal).catch(err => { - notification.error({ - message: getResourceName(ResourceType.VIRTUAL_MACHINE_SUMMARY), - description: getErrorMessage(err) - }) - }) + watchVirtualMachineSummary(ns, setSummary, setLoading, abortCtrl.current?.signal, notification) }, [ns]) useUnmount(() => { - console.log("Unmounting watcher", getResourceName(ResourceType.VIRTUAL_MACHINE_SUMMARY)) abortCtrl.current?.abort() }) @@ -44,28 +35,24 @@ export default () => { } const handleMount = async (name: string) => { - const vmNamespace = summary?.metadata!.namespace! - const vmName = summary?.metadata!.name! - try { - const vm = await getVirtualMachine({ namespace: vmNamespace, name: vmName }) - vm.spec?.template?.spec?.domain?.devices?.disks?.push({ - name: name, - disk: { bus: "virtio" } - }) - await updateVirtualMachine(vm) - await manageVirtualMachinePowerState({ namespace: vmNamespace, name: vmName }, VirtualMachinePowerStateRequest_PowerState.REBOOT) - } catch (err: any) { - notification.error({ - message: getResourceName(ResourceType.VIRTUAL_MACHINE), - description: getErrorMessage(err) - }) + if (!summary) { + return } + const ns = { namespace: summary.metadata!.namespace, name: summary.metadata!.name } + const vm = await getVirtualMachine(ns, undefined, undefined, notification) + vm.spec?.template?.spec?.domain?.devices?.disks?.push({ + name: name, + disk: { bus: "virtio" } + }) + await updateVirtualMachine(vm, undefined, undefined, notification) + await manageVirtualMachinePowerState(ns, VirtualMachinePowerStateRequest_PowerState.REBOOT, undefined, notification) } const handleUnmount = async (name: string) => { - const vmNamespace = summary?.metadata!.namespace! - const vmName = summary?.metadata!.name! - + if (!summary) { + return + } + const ns = { namespace: summary.metadata!.namespace, name: summary.metadata!.name } Modal.confirm({ title: "Unmount Disk?", content: `You are about to unmount the disk "${name}". Please confirm.`, @@ -74,27 +61,21 @@ export default () => { cancelText: 'Cancel', okButtonProps: { disabled: false }, onOk: async () => { - try { - const vm = await getVirtualMachine({ namespace: vmNamespace, name: vmName }) - if (vm.spec?.template?.spec?.domain.devices.disks) { - vm.spec.template.spec.domain.devices.disks = vm.spec.template.spec.domain.devices.disks.filter((disk) => disk.name !== name) - } - await updateVirtualMachine(vm) - await manageVirtualMachinePowerState({ namespace: vmNamespace, name: vmName }, VirtualMachinePowerStateRequest_PowerState.REBOOT) - } catch (err: any) { - notification.error({ - message: getResourceName(ResourceType.VIRTUAL_MACHINE), - description: getErrorMessage(err) - }) + const vm = await getVirtualMachine(ns, undefined, undefined, notification) + if (vm.spec?.template?.spec?.domain.devices.disks) { + vm.spec.template.spec.domain.devices.disks = vm.spec.template.spec.domain.devices.disks.filter((disk) => disk.name !== name) } + await updateVirtualMachine(vm, undefined, undefined, notification) + await manageVirtualMachinePowerState(ns, VirtualMachinePowerStateRequest_PowerState.REBOOT, undefined, notification) } }) } const handleRemove = async (name: string) => { - const vmNamespace = summary?.metadata!.namespace! - const vmName = summary?.metadata!.name! - + if (!summary) { + return + } + const ns = { namespace: summary.metadata!.namespace, name: summary.metadata!.name } Modal.confirm({ title: "Remove Disk?", content: `You are about to remove the disk "${name}". Please confirm.`, @@ -103,22 +84,15 @@ export default () => { cancelText: 'Cancel', okButtonProps: { disabled: false }, onOk: async () => { - try { - const vm = await getVirtualMachine({ namespace: vmNamespace, name: vmName }) - if (vm.spec?.template?.spec?.domain.devices.disks) { - vm.spec.template.spec.domain.devices.disks = vm.spec.template.spec.domain.devices.disks.filter((disk: any) => disk.name !== name) - } - if (vm.spec?.template?.spec?.volumes) { - vm.spec.template.spec.volumes = vm.spec.template.spec.volumes.filter((disk: any) => disk.name !== name) - } - await updateVirtualMachine(vm) - await manageVirtualMachinePowerState({ namespace: vmNamespace, name: vmName }, VirtualMachinePowerStateRequest_PowerState.REBOOT) - } catch (err: any) { - notification.error({ - message: getResourceName(ResourceType.VIRTUAL_MACHINE), - description: getErrorMessage(err) - }) + const vm = await getVirtualMachine(ns, undefined, undefined, notification) + if (vm.spec?.template?.spec?.domain.devices.disks) { + vm.spec.template.spec.domain.devices.disks = vm.spec.template.spec.domain.devices.disks.filter((disk: any) => disk.name !== name) + } + if (vm.spec?.template?.spec?.volumes) { + vm.spec.template.spec.volumes = vm.spec.template.spec.volumes.filter((disk: any) => disk.name !== name) } + await updateVirtualMachine(vm, undefined, undefined, notification) + await manageVirtualMachinePowerState(ns, VirtualMachinePowerStateRequest_PowerState.REBOOT, undefined, notification) } }) } diff --git a/src/pages/compute/machine/detail/yaml.tsx b/src/pages/compute/machine/detail/yaml.tsx index 6993176..8fb279f 100644 --- a/src/pages/compute/machine/detail/yaml.tsx +++ b/src/pages/compute/machine/detail/yaml.tsx @@ -1,11 +1,8 @@ import { App } from "antd" import { DetailYaml } from "@/components/detail-yaml" -import { ResourceType } from "@/clients/ts/types/types" import { useNamespaceFromURL } from "@/hooks/use-query-params-from-url" import { useEffect, useRef, useState } from "react" import { updateVirtualMachine, VirtualMachine, watchVirtualMachine } from "@/clients/virtual-machine" -import { getResourceName } from "@/clients/clients" -import { getErrorMessage } from "@/utils/utils" import { useNavigate } from "react-router" import useUnmount from "@/hooks/use-unmount" @@ -17,6 +14,7 @@ export default () => { const ns = useNamespaceFromURL() const [loading, setLoading] = useState(true) + const [virtualMachine, setVirtualMachine] = useState() const abortCtrl = useRef() @@ -24,29 +22,15 @@ export default () => { useEffect(() => { abortCtrl.current?.abort() abortCtrl.current = new AbortController() - watchVirtualMachine(ns, setVirtualMachine, setLoading, abortCtrl.current.signal).catch(err => { - notification.error({ - message: getResourceName(ResourceType.VIRTUAL_MACHINE), - description: getErrorMessage(err) - }) - }) + watchVirtualMachine(ns, setVirtualMachine, setLoading, abortCtrl.current.signal, notification) }, [ns]) useUnmount(() => { - console.log("Unmounting watcher", getResourceName(ResourceType.VIRTUAL_MACHINE)) abortCtrl.current?.abort() }) - const handleSave = (data: any) => { - data = data as VirtualMachine - try { - updateVirtualMachine(data) - } catch (err) { - notification.error({ - message: getResourceName(ResourceType.VIRTUAL_MACHINE), - description: getErrorMessage(err) - }) - } + const handleSave = async (data: any) => { + await updateVirtualMachine(data as VirtualMachine, undefined, undefined, notification) } const handleCancel = () => { diff --git a/src/pages/compute/machine/list/index.tsx b/src/pages/compute/machine/list/index.tsx index 125e292..997c6d6 100644 --- a/src/pages/compute/machine/list/index.tsx +++ b/src/pages/compute/machine/list/index.tsx @@ -3,16 +3,16 @@ import { App, Button, Flex, Modal, Popover, Space, Tag } from 'antd' import { useEffect, useRef, useState } from 'react' import { Link, NavLink } from 'react-router-dom' import { VirtualMachinePowerStateRequest_PowerState } from '@/clients/ts/management/virtualmachine/v1alpha1/virtualmachine' -import { filterNullish, formatTimestamp, generateMessage, getErrorMessage } from '@/utils/utils' +import { filterNullish, formatTimestamp, generateMessage } from '@/utils/utils' import { useNamespace } from '@/common/context' import { FieldSelector, ResourceType } from '@/clients/ts/types/types' -import { clients, getPowerStateName, getResourceName } from '@/clients/clients' +import { getPowerStateName, getResourceName } from '@/clients/clients' import { instances as annotations } from '@/clients/ts/annotation/annotations.gen' import { WatchOptions } from '@/clients/ts/management/resource/v1alpha1/watch' import { rootDisk } from '@/utils/parse-summary' import { getNamespaceFieldSelector, replaceDots } from '@/utils/search' import { CustomTable, SearchItem } from '@/components/custom-table' -import { VirtualMachine } from '@/clients/virtual-machine' +import { deleteVirtualMachines, manageVirtualMachinesPowerState, VirtualMachine } from '@/clients/virtual-machine' import { VirtualMachineSummary, watchVirtualMachineSummarys } from '@/clients/virtual-machine-summary' import { DataVolume } from '@/clients/data-volume' import type { ProColumns } from '@ant-design/pro-components' @@ -23,112 +23,6 @@ import OperatingSystem from '@/components/operating-system' import VirtualMachineManagement from '../components/management' import useUnmount from '@/hooks/use-unmount' -export default () => { - const { notification } = App.useApp() - - const { namespace } = useNamespace() - - const [selectedRows, setSelectedRows] = useState([]) - - const [defaultFieldSelectors, setDefaultFieldSelectors] = useState(filterNullish([getNamespaceFieldSelector(namespace)])) - const [opts, setOpts] = useState(WatchOptions.create({ - fieldSelectorGroup: { operator: "&&", fieldSelectors: defaultFieldSelectors } - })) - - useEffect(() => { - setDefaultFieldSelectors(filterNullish([getNamespaceFieldSelector(namespace)])) - }, [namespace]) - - const [loading, setLoading] = useState(true) - const [resources, setResources] = useState() - - const abortCtrl = useRef() - - useEffect(() => { - abortCtrl.current?.abort() - abortCtrl.current = new AbortController() - watchVirtualMachineSummarys(setResources, setLoading, abortCtrl.current.signal, opts).catch(err => { - notification.error({ - message: getResourceName(ResourceType.VIRTUAL_MACHINE), - description: getErrorMessage(err) - }) - }) - }, [opts]) - - useUnmount(() => { - console.log("Unmounting watcher", getResourceName(ResourceType.VIRTUAL_MACHINE_SUMMARY)) - abortCtrl.current?.abort() - }) - - const handleBatchManageVirtualMachinePowerState = async (state: VirtualMachinePowerStateRequest_PowerState) => { - const statusText = getPowerStateName(state) - Modal.confirm({ - title: `Batch ${statusText} virtual machines?`, - content: generateMessage(selectedRows, `You are about to ${statusText} the "{names}" virtual machines. Please confirm.`, `You are about to ${statusText} {count} virtual machines, including "{names}". Please confirm.`), - okText: `Confirm ${statusText}`, - okType: 'danger', - cancelText: 'Cancel', - okButtonProps: { disabled: false }, - onOk: async () => { - clients.batchManageVirtualMachinePowerState(selectedRows, state).catch(err => { - notification.error({ - message: `Failed to batch ${statusText} virtual machines`, - description: getErrorMessage(err) - }) - }) - } - }) - } - - const handleBatchDeleteVirtualMachines = async () => { - const resourceName = getResourceName(ResourceType.VIRTUAL_MACHINE) - Modal.confirm({ - title: `Batch delete ${resourceName}?`, - content: generateMessage(selectedRows, `You are about to delete the following ${resourceName}: "{names}", please confirm.`, `You are about to delete the following ${resourceName}: "{names}" and {count} others, please confirm.`), - okText: 'Confirm Delete', - okType: 'danger', - cancelText: 'Cancel', - okButtonProps: { disabled: false }, - onOk: async () => { - clients.batchDeleteResources(ResourceType.VIRTUAL_MACHINE, selectedRows).catch(err => { - notification.error({ - message: `Batch delete of ${resourceName} failed`, - description: getErrorMessage(err) - }) - }) - } - }) - } - - return ( - - searchItems={searchItems} - loading={loading} - updateWatchOptions={setOpts} - onSelectRows={(rows) => setSelectedRows(rows)} - defaultFieldSelectors={defaultFieldSelectors} - storageKey="virtual-machine-list-table-columns" - columns={columns} - dataSource={resources} - toolbar={{ - actions: [ - - ] - }} - tableAlertOptionRender={() => { - return ( - - handleBatchManageVirtualMachinePowerState(VirtualMachinePowerStateRequest_PowerState.ON)}>批量开机 - handleBatchManageVirtualMachinePowerState(VirtualMachinePowerStateRequest_PowerState.REBOOT)}>批量重启 - handleBatchManageVirtualMachinePowerState(VirtualMachinePowerStateRequest_PowerState.OFF)}>批量关机 - handleBatchDeleteVirtualMachines()}>批量删除 - - ) - }} - /> - ) -} - const searchItems: SearchItem[] = [ { fieldPath: "metadata.name", name: "Name", operator: "*=" }, { @@ -333,3 +227,99 @@ const columns: ProColumns[] = [ render: (_, summary) => } ] + +export default () => { + const { notification } = App.useApp() + + const { namespace } = useNamespace() + + const [selectedRows, setSelectedRows] = useState() + + const [defaultFieldSelectors, setDefaultFieldSelectors] = useState(filterNullish([getNamespaceFieldSelector(namespace)])) + const [opts, setOpts] = useState(WatchOptions.create({ + fieldSelectorGroup: { operator: "&&", fieldSelectors: defaultFieldSelectors } + })) + + useEffect(() => { + setDefaultFieldSelectors(filterNullish([getNamespaceFieldSelector(namespace)])) + }, [namespace]) + + const [loading, setLoading] = useState(true) + + const [resources, setResources] = useState() + + const abortCtrl = useRef() + + useEffect(() => { + abortCtrl.current?.abort() + abortCtrl.current = new AbortController() + watchVirtualMachineSummarys(setResources, setLoading, abortCtrl.current.signal, opts, notification) + }, [opts]) + + useUnmount(() => { + abortCtrl.current?.abort() + }) + + const handleManageVirtualMachinesPowerState = async (state: VirtualMachinePowerStateRequest_PowerState) => { + const vms = selectedRows?.map(item => item.status?.virtualMachine as VirtualMachine).filter(Boolean) + if (!vms || vms.length === 0) { + return + } + const statusText = getPowerStateName(state) + Modal.confirm({ + title: `Confirm batch "${statusText}" operation`, + content: `Are you sure you want to "${statusText}" the selected virtual machines?`, + okText: `${statusText}`, + okType: 'danger', + cancelText: 'Cancel', + onOk: async () => { + await manageVirtualMachinesPowerState(vms, state) + } + }) + } + + const handleDeleteVirtualMachines = async () => { + const vms = selectedRows?.map(item => item.status?.virtualMachine as VirtualMachine).filter(Boolean) + if (!vms || vms.length === 0) { + return + } + Modal.confirm({ + title: `Confirm batch deletion of virtual machines`, + content: `Are you sure you want to delete the selected virtual machines? This action cannot be undone.`, + okText: 'Delete', + okType: 'danger', + cancelText: 'Cancel', + onOk: async () => { + await deleteVirtualMachines(vms, undefined, notification) + } + }) + } + + return ( + + searchItems={searchItems} + loading={loading} + updateWatchOptions={setOpts} + onSelectRows={(rows) => setSelectedRows(rows)} + defaultFieldSelectors={defaultFieldSelectors} + key="virtual-machine-list-table-columns" + columns={columns} + dataSource={resources} + toolbar={{ + actions: [ + + ] + }} + tableAlertOptionRender={() => { + return ( + + handleManageVirtualMachinesPowerState(VirtualMachinePowerStateRequest_PowerState.ON)}>开机 + handleManageVirtualMachinesPowerState(VirtualMachinePowerStateRequest_PowerState.REBOOT)}>重启 + handleManageVirtualMachinesPowerState(VirtualMachinePowerStateRequest_PowerState.OFF)}>关机 + handleDeleteVirtualMachines()}>删除 + + ) + }} + /> + ) +} diff --git a/src/pages/compute/machine/virtualmachine/index.ts b/src/pages/compute/machine/virtualmachine/index.ts index 82b691a..2e57a99 100644 --- a/src/pages/compute/machine/virtualmachine/index.ts +++ b/src/pages/compute/machine/virtualmachine/index.ts @@ -2,75 +2,148 @@ import { instances as labels } from '@/clients/ts/label/labels.gen' import { instances as annotations } from '@/clients/ts/annotation/annotations.gen' import { generateKubeovnNetworkAnnon } from '@/utils/utils' import { namespaceNameKey, parseNamespaceNameKey } from '@/utils/k8s' -import { virtualmachineYaml } from './template' import { NamespaceName } from '@/clients/ts/types/types' -import * as yaml from 'js-yaml' +import { VirtualMachine } from '@/clients/virtual-machine' +import { DataVolume } from '@/clients/data-volume' +import { VirtualMachineNetworkType } from '@/clients/subnet' export const defaultNetworkAnno = "v1.multus-cni.io/default-network" -export const newVirtualMachine = (ns: NamespaceName, cm: { cpu: number, memory: number }, rootDisk: { image: any, capacity: number }, dataDisks: any[], netcfgs: NetworkConfig[], cloudInit: string) => { - const instance: any = yaml.load(virtualmachineYaml) - - instance.metadata.name = ns.name - instance.metadata.namespace = ns.namespace +export const newVirtualMachine = (ns: NamespaceName, cm: { cpu: number, memory: number }, rootDisk: { image: DataVolume, capacity: number }, dataDisks: DataVolume[], netcfgs: VirtualMachineNetworkType[], cloudInit: string) => { + console.log(ns, cm, rootDisk, dataDisks, netcfgs, cloudInit, "xxxx") + const rootDiskName = generateRootDiskName(ns.name) + + const instance: VirtualMachine = { + apiVersion: "kubevirt.io/v1", + kind: "VirtualMachine", + metadata: { + name: ns.name, + namespace: ns.namespace, + }, + spec: { + dataVolumeTemplates: [ + { + metadata: { + name: rootDiskName, + labels: { + [labels.VinkDatavolumeType.name]: "root", + [labels.VinkOperatingSystem.name]: rootDisk.image.metadata!.labels?.[labels.VinkOperatingSystem.name], + [labels.VinkOperatingSystemVersion.name]: rootDisk.image.metadata!.labels?.[labels.VinkOperatingSystemVersion.name], + }, + annotations: { + [annotations.VinkDatavolumeOwner.name]: ns.name + } + }, + spec: { + pvc: { + accessModes: ["ReadWriteOnce"], + resources: { + requests: { + storage: `${rootDisk.capacity}Gi`, + }, + }, + storageClassName: "local-path", + }, + source: { + pvc: { + name: rootDisk.image.metadata!.name, + namespace: rootDisk.image.metadata!.namespace, + }, + }, + }, + }, + ], + runStrategy: "Always", + template: { + metadata: { + annotations: {} + }, + spec: { + architecture: "amd64", + domain: { + cpu: { + cores: cm.cpu, + model: "host-model", + sockets: 1, + threads: 2 + }, + memory: { + guest: `${cm.memory}Gi` + }, + devices: { + disks: [ + { + name: rootDiskName, + disk: { bus: "virtio" }, + bootOrder: 1 + }, + { + name: "cloudinit", + disk: { bus: "virtio" } + } + ], + interfaces: [] + }, + features: { + acpi: { + enabled: true + } + }, + machine: { + type: "q35" + }, + resources: { + requests: { + memory: `${cm.memory / 2}Gi`, + cpu: `${250 * cm.cpu}m` + }, + limits: { + memory: `${cm.memory}Gi`, + cpu: cm.cpu + } + } + }, + networks: [], + volumes: [ + { + name: rootDiskName, + dataVolume: { + name: rootDiskName + }, + }, + { + name: "cloudinit", + cloudInitNoCloud: { + userDataBase64: btoa(cloudInit) + } + } + ] + } + } + } + } - updateCpuMem(instance, cm.cpu, cm.memory) - updateRootDisk(instance, rootDisk.image, rootDisk.capacity) - updateDataDisks(instance, dataDisks) - updateCloudInit(instance, cloudInit) + generateDataDisks(instance, dataDisks) netcfgs.forEach(netcfg => { - updateNetwork(instance, netcfg) + generateNetwork(instance, netcfg) }) return instance } -export const updateCpuMem = (vm: any, cpu: number, memory: number) => { - vm.spec.template.spec.domain.cpu.cores = cpu - vm.spec.template.spec.domain.memory.guest = `${memory}Gi` - - vm.spec.template.spec.domain.resources.requests.memory = `${memory / 2}Gi` - vm.spec.template.spec.domain.resources.requests.cpu = `${250 * cpu}m` - - vm.spec.template.spec.domain.resources.limits.memory = `${memory}Gi` - vm.spec.template.spec.domain.resources.limits.cpu = cpu -} - -export const updateRootDisk = (vm: any, image: any, capacity: number) => { - const rootDiskName = generateRootDiskName(vm.metadata.name) - - vm.spec.dataVolumeTemplates[0].metadata.name = rootDiskName - vm.spec.dataVolumeTemplates[0].metadata.labels[labels.VinkDatavolumeType.name] = "root" - vm.spec.dataVolumeTemplates[0].metadata.labels[labels.VinkOperatingSystem.name] = image.metadata.labels[labels.VinkOperatingSystem.name] - vm.spec.dataVolumeTemplates[0].metadata.labels[labels.VinkOperatingSystemVersion.name] = image.metadata.labels[labels.VinkOperatingSystemVersion.name] - vm.spec.dataVolumeTemplates[0].metadata.annotations[annotations.VinkDatavolumeOwner.name] = vm.metadata.name - vm.spec.dataVolumeTemplates[0].spec.pvc.resources.requests.storage = `${capacity}Gi` - vm.spec.dataVolumeTemplates[0].spec.source.pvc.name = image.metadata.name - vm.spec.dataVolumeTemplates[0].spec.source.pvc.namespace = image.metadata.namespace - - const additionalRootDisk = { dataVolume: { name: rootDiskName }, name: rootDiskName } - vm.spec.template.spec.volumes.push(additionalRootDisk) - - vm.spec.template.spec.domain.devices.disks = vm.spec.template.spec.domain.devices.disks.filter((disk: any) => { - if (disk.bootOrder === 1) { - delete disk.bootOrder - } - return disk.name !== additionalRootDisk.name - }) - - vm.spec.template.spec.domain.devices.disks.push({ - name: additionalRootDisk.name, - disk: { bus: "virtio" }, - bootOrder: 1 - }) -} - -export const updateDataDisks = (vm: any, dataDisks: any[]) => { +export const generateDataDisks = (vm: VirtualMachine, dataDisks: DataVolume[]) => { const additionalDataDisks = dataDisks.map((disk: any) => ({ dataVolume: { name: disk.metadata.name }, name: disk.metadata.name })) || [] + + vm.spec.template.spec = vm.spec.template.spec || { domain: { devices: {} }, volumes: [] } + vm.spec.template.spec.domain = vm.spec.template.spec.domain || { devices: {} } + vm.spec.template.spec.domain.devices = vm.spec.template.spec.domain.devices || {} + vm.spec.template.spec.volumes = vm.spec.template.spec.volumes || [] + vm.spec.template.spec.domain.devices.disks = vm.spec.template.spec.domain.devices.disks || [] + const allVolumes = [...vm.spec.template.spec.volumes, ...additionalDataDisks] vm.spec.template.spec.volumes = Array.from( new Map(allVolumes.map((vol) => [vol.name, vol])).values() @@ -92,40 +165,116 @@ export const updateDataDisks = (vm: any, dataDisks: any[]) => { } } -export const updateCloudInit = (vm: any, cloudInit: string) => { - const additionalCloudInit = { cloudInitNoCloud: { userDataBase64: btoa(cloudInit) }, name: "cloudinit" } - vm.spec.template.spec.volumes = vm.spec.template.spec.volumes.filter((vol: any) => { - return !vol.cloudInitNoCloud - }) - vm.spec.template.spec.volumes.push(additionalCloudInit) - - vm.spec.template.spec.domain.devices.disks.push({ - name: additionalCloudInit.name, - disk: { bus: "virtio" } - }) - vm.spec.template.spec.domain.devices.disks = Array.from( - new Map(vm.spec.template.spec.domain.devices.disks.map((disk: any) => [disk.name, disk])).values() - ) -} - -export interface NetworkConfig { - name?: string - network: "multus" | "pod" - interface: "masquerade" | "bridge" | "slirp" | "sriov" - multus: NamespaceName | any - subnet: string | any - ippool?: string | any - ipAddress?: string - macAddress?: string - default?: boolean -} - -export const updateNetwork = (vm: any, netcfg: NetworkConfig) => { - const isAddAction = (!netcfg.name || netcfg.name.length === 0) - - if (!vm.spec.template.spec.networks) { - vm.spec.template.spec.networks = [] - } +// export interface NetworkConfig { +// name?: string +// network: string +// interface: string +// multus: NamespaceName | Multus +// subnet: string | Subnet +// ippool?: string | IPPool +// ipAddress?: string +// macAddress?: string +// default?: boolean +// } + +// export const generateNetwork = (vm: VirtualMachine, netcfg: NetworkConfig) => { +// vm.spec.template.spec = vm.spec.template.spec || { domain: { devices: {} }, volumes: [] } +// vm.spec.template.spec.domain = vm.spec.template.spec.domain || { devices: {} } +// vm.spec.template.spec.networks = vm.spec.template.spec.networks || [] + +// const multusNameMap = new Map() +// const networkNameMap = new Map() +// for (const element of vm.spec.template.spec.networks) { +// if (element.multus) { +// multusNameMap.set(element.multus.networkName, true) +// } +// networkNameMap.set(element.name, true) +// } + +// if (vm.spec.template.metadata!.annotations?.[defaultNetworkAnno]) { +// multusNameMap.set(vm.spec.template.metadata!.annotations[defaultNetworkAnno], true) +// } + +// if (!netcfg.name) { +// for (let number = 1; ; number++) { +// const element = `net-${number}` +// if (networkNameMap.has(element)) { +// continue +// } +// netcfg.name = element +// break +// } +// } + +// vm.spec.template.spec.networks = vm.spec.template.spec.networks.filter((net: any) => { +// return net.name !== netcfg.name +// }) + +// vm.spec.template.metadata!.annotations = vm.spec.template.metadata!.annotations || {} + +// let multusName = namespaceNameKey(netcfg.multus) + +// const net: any = { name: netcfg.name } +// if (netcfg.interface === "masquerade") { +// net.pod = {} +// vm.spec.template.metadata!.annotations[defaultNetworkAnno] = multusName +// } else { +// net.multus = { default: netcfg.default, networkName: multusName } +// if (vm.spec.template.metadata!.annotations[defaultNetworkAnno] === multusName) { +// delete vm.spec.template.metadata!.annotations[defaultNetworkAnno] +// } +// } +// vm.spec.template.spec.networks.push(net) + +// if (!vm.spec.template.spec.domain.devices.interfaces) { +// vm.spec.template.spec.domain.devices.interfaces = [] +// } + +// vm.spec.template.spec.domain.devices.interfaces = vm.spec.template.spec.domain.devices.interfaces.filter((net: any) => { +// return net.name !== netcfg.name +// }) +// vm.spec.template.spec.domain.devices.interfaces.push({ +// name: netcfg.name, +// [netcfg.interface]: {} +// }) + +// let subnetName = "" +// if (typeof netcfg.subnet === "string") { +// subnetName = netcfg.subnet +// } else { +// subnetName = netcfg.subnet.metadata!.name! +// } + +// vm.spec.template.metadata!.annotations[generateKubeovnNetworkAnnon(netcfg.multus, "logical_switch")] = subnetName + +// if (netcfg.ippool) { +// let ippoolName = "" +// if (typeof netcfg.ippool === "string") { +// ippoolName = netcfg.ippool +// } else if (typeof netcfg.ippool === "object") { +// ippoolName = netcfg.ippool!.metadata.name +// } + +// if (ippoolName.length > 0) { +// vm.spec.template.metadata!.annotations[generateKubeovnNetworkAnnon(netcfg.multus, "ip_pool")] = ippoolName +// } +// } + +// if (netcfg.ipAddress && netcfg.ipAddress.length > 0) { +// vm.spec.template.metadata!.annotations[generateKubeovnNetworkAnnon(netcfg.multus, "ip_address")] = netcfg.ipAddress +// } + +// if (netcfg.macAddress && netcfg.macAddress.length > 0) { +// vm.spec.template.metadata!.annotations[generateKubeovnNetworkAnnon(netcfg.multus, "mac_address")] = netcfg.macAddress +// } + +// configureNetworkDefault(vm.spec.template.spec.networks) +// } + +export const generateNetwork = (vm: VirtualMachine, netcfg: VirtualMachineNetworkType) => { + vm.spec.template.spec = vm.spec.template.spec || { domain: { devices: {} }, volumes: [] } + vm.spec.template.spec.domain = vm.spec.template.spec.domain || { devices: {} } + vm.spec.template.spec.networks = vm.spec.template.spec.networks || [] const multusNameMap = new Map() const networkNameMap = new Map() @@ -136,17 +285,11 @@ export const updateNetwork = (vm: any, netcfg: NetworkConfig) => { networkNameMap.set(element.name, true) } - if (vm.spec.template.metadata.annotations?.[defaultNetworkAnno]) { - multusNameMap.set(vm.spec.template.metadata.annotations[defaultNetworkAnno], true) - } - - if (isAddAction && multusNameMap.has(namespaceNameKey(netcfg.multus))) { - throw new Error( - `Invalid network configuration: 'name' is required but missing or empty. Additionally, a configuration with the same key (${namespaceNameKey(netcfg.multus)}) already exists in multusNameMap.` - ) + if (vm.spec.template.metadata!.annotations?.[defaultNetworkAnno]) { + multusNameMap.set(vm.spec.template.metadata!.annotations[defaultNetworkAnno], true) } - if (isAddAction) { + if (!netcfg.name) { for (let number = 1; ; number++) { const element = `net-${number}` if (networkNameMap.has(element)) { @@ -161,20 +304,18 @@ export const updateNetwork = (vm: any, netcfg: NetworkConfig) => { return net.name !== netcfg.name }) - if (!vm.spec.template.metadata.annotations) { - vm.spec.template.metadata.annotations = {} - } + vm.spec.template.metadata!.annotations = vm.spec.template.metadata!.annotations || {} let multusName = namespaceNameKey(netcfg.multus) const net: any = { name: netcfg.name } if (netcfg.interface === "masquerade") { net.pod = {} - vm.spec.template.metadata.annotations[defaultNetworkAnno] = multusName + vm.spec.template.metadata!.annotations[defaultNetworkAnno] = multusName } else { net.multus = { default: netcfg.default, networkName: multusName } - if (vm.spec.template.metadata.annotations[defaultNetworkAnno] === multusName) { - delete vm.spec.template.metadata.annotations[defaultNetworkAnno] + if (vm.spec.template.metadata!.annotations[defaultNetworkAnno] === multusName) { + delete vm.spec.template.metadata!.annotations[defaultNetworkAnno] } } vm.spec.template.spec.networks.push(net) @@ -191,37 +332,48 @@ export const updateNetwork = (vm: any, netcfg: NetworkConfig) => { [netcfg.interface]: {} }) - let subnetName = netcfg.subnet - if (typeof netcfg.subnet === "object") { - subnetName = netcfg.subnet.metadata.name + let subnetName = "" + if (netcfg.subnet) { + if (typeof netcfg.subnet === "string") { + subnetName = netcfg.subnet + } else { + subnetName = netcfg.subnet.metadata!.name! + } } - vm.spec.template.metadata.annotations[generateKubeovnNetworkAnnon(netcfg.multus, "logical_switch")] = subnetName + vm.spec.template.metadata!.annotations[generateKubeovnNetworkAnnon(netcfg.multus, "logical_switch")] = subnetName if (netcfg.ippool) { - let ippoolName = netcfg.ippool - if (typeof netcfg.ippool === "object") { - ippoolName = netcfg.ippool.metadata.name + let ippoolName = "" + if (typeof netcfg.ippool === "string") { + ippoolName = netcfg.ippool + } else { + ippoolName = netcfg.ippool.metadata!.name } if (ippoolName.length > 0) { - vm.spec.template.metadata.annotations[generateKubeovnNetworkAnnon(netcfg.multus, "ip_pool")] = ippoolName + vm.spec.template.metadata!.annotations[generateKubeovnNetworkAnnon(netcfg.multus, "ip_pool")] = ippoolName } } if (netcfg.ipAddress && netcfg.ipAddress.length > 0) { - vm.spec.template.metadata.annotations[generateKubeovnNetworkAnnon(netcfg.multus, "ip_address")] = netcfg.ipAddress + vm.spec.template.metadata!.annotations[generateKubeovnNetworkAnnon(netcfg.multus, "ip_address")] = netcfg.ipAddress } if (netcfg.macAddress && netcfg.macAddress.length > 0) { - vm.spec.template.metadata.annotations[generateKubeovnNetworkAnnon(netcfg.multus, "mac_address")] = netcfg.macAddress + vm.spec.template.metadata!.annotations[generateKubeovnNetworkAnnon(netcfg.multus, "mac_address")] = netcfg.macAddress } configureNetworkDefault(vm.spec.template.spec.networks) } -export const deleteNetwork = (vm: any, netName: string) => { +export const deleteNetwork = (vm: VirtualMachine, netName: string) => { + vm.spec.template.spec = vm.spec.template.spec || { domain: { devices: {} }, volumes: [] } + vm.spec.template.spec.domain = vm.spec.template.spec.domain || { devices: {} } + vm.spec.template.spec.networks = vm.spec.template.spec.networks || [] + vm.spec.template.spec.domain.devices.interfaces = vm.spec.template.spec.domain.devices.interfaces || [] + const net = vm.spec.template.spec.networks.find((net: any) => net.name === netName) - if (net && vm.spec.template.metadata.annotations) { + if (net && vm.spec.template.metadata!.annotations) { let ns: NamespaceName | undefined = undefined if (net.multus) { const part = net.multus.networkName.split("/") @@ -229,30 +381,36 @@ export const deleteNetwork = (vm: any, netName: string) => { ns = { namespace: part[0], name: part[1] } } } else if (net.pod) { - const value = vm.spec.template.metadata.annotations[defaultNetworkAnno] + const value = vm.spec.template.metadata!.annotations[defaultNetworkAnno] if (value && value.length > 0) { - ns = parseNamespaceNameKey(vm.spec.template.metadata.annotations[defaultNetworkAnno]) - delete vm.spec.template.metadata.annotations[defaultNetworkAnno] + ns = parseNamespaceNameKey(vm.spec.template.metadata!.annotations[defaultNetworkAnno]) + delete vm.spec.template.metadata!.annotations[defaultNetworkAnno] } } if (ns) { - delete vm.spec.template.metadata.annotations[generateKubeovnNetworkAnnon(ns, "ip_address")] - delete vm.spec.template.metadata.annotations[generateKubeovnNetworkAnnon(ns, "mac_address")] - delete vm.spec.template.metadata.annotations[generateKubeovnNetworkAnnon(ns, "ip_pool")] - delete vm.spec.template.metadata.annotations[generateKubeovnNetworkAnnon(ns, "logical_switch")] + delete vm.spec.template.metadata!.annotations[generateKubeovnNetworkAnnon(ns, "ip_address")] + delete vm.spec.template.metadata!.annotations[generateKubeovnNetworkAnnon(ns, "mac_address")] + delete vm.spec.template.metadata!.annotations[generateKubeovnNetworkAnnon(ns, "ip_pool")] + delete vm.spec.template.metadata!.annotations[generateKubeovnNetworkAnnon(ns, "logical_switch")] } } vm.spec.template.spec.domain.devices.interfaces = vm.spec.template.spec.domain.devices.interfaces.filter((net: any) => { return net.name !== netName }) - vm.spec.template.spec.networks = vm.spec.template.spec.networks.filter((net: any) => { + vm.spec.template.spec.networks = vm.spec.template.spec.networks.filter((net) => { return net.name !== netName }) configureNetworkDefault(vm.spec.template.spec.networks) } -const configureNetworkDefault = (networks: any[]) => { +const configureNetworkDefault = (networks: { + multus?: { default?: boolean; networkName: string; }; name: string; + pod?: { vmIPv6NetworkCIDR?: string; vmNetworkCIDR?: string; } +}[]) => { + if (networks.length === 0) { + return + } if (networks.some((net) => net.pod)) { networks.forEach((net) => { if (net.multus) net.multus.default = false @@ -262,6 +420,7 @@ const configureNetworkDefault = (networks: any[]) => { const defaultNets = networks.filter((net) => net.multus && net.multus.default) if (defaultNets.length === 0) { + networks[0].multus = networks[0].multus || { networkName: "", default: false } networks[0].multus.default = true return } @@ -283,3 +442,15 @@ const configureNetworkDefault = (networks: any[]) => { const generateRootDiskName = (name: string) => { return `${name}-root` } + +export const defaultCloudInit = ` +#cloud-config +ssh_pwauth: true +disable_root: false +chpasswd: {"list": "root:dangerous", expire: False} + +runcmd: +- dhclient -r && dhclient +- sed -i "/#\?PermitRootLogin/s/^.*$/PermitRootLogin yes/g" /etc/ssh/sshd_config +- systemctl restart sshd.service +` diff --git a/src/pages/compute/machine/virtualmachine/template.ts b/src/pages/compute/machine/virtualmachine/template.ts deleted file mode 100644 index b7ae4c6..0000000 --- a/src/pages/compute/machine/virtualmachine/template.ts +++ /dev/null @@ -1,66 +0,0 @@ -export const virtualmachineYaml = ` -apiVersion: kubevirt.io/v1 -kind: VirtualMachine -metadata: - name: "" - namespace: "" -spec: - dataVolumeTemplates: - - metadata: - name: "" - labels: {} - annotations: {} - spec: - pvc: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: "" - storageClassName: local-path - source: - pvc: - name: "" - namespace: "" - runStrategy: Always - template: - metadata: - annotations: {} - spec: - architecture: amd64 - domain: - cpu: - cores: 1 - model: host-model - sockets: 1 - threads: 2 - memory: - guest: 1Gi - devices: - disks: [] - interfaces: [] - features: - acpi: - enabled: true - machine: - type: q35 - resources: - requests: - memory: 1Gi - cpu: "0" - limits: {} - networks: [] - volumes: [] -` - -export const defaultCloudInit = ` -#cloud-config -ssh_pwauth: true -disable_root: false -chpasswd: {"list": "root:dangerous", expire: False} - -runcmd: -- dhclient -r && dhclient -- sed -i "/#\?PermitRootLogin/s/^.*$/PermitRootLogin yes/g" /etc/ssh/sshd_config -- systemctl restart sshd.service -` diff --git a/src/pages/network/ippool/create/index.tsx b/src/pages/network/ippool/create/index.tsx index c44343a..392ff12 100644 --- a/src/pages/network/ippool/create/index.tsx +++ b/src/pages/network/ippool/create/index.tsx @@ -2,9 +2,7 @@ import { App } from 'antd' import { ippoolYaml } from './crd-template' import { useNavigate } from 'react-router-dom' import { CreateCRDWithYaml } from '@/components/create-crd-with-yaml' -import { clients, getResourceName } from '@/clients/clients' -import { ResourceType } from '@/clients/ts/types/types' -import { getErrorMessage } from '@/utils/utils' +import { createIPPool, IPPool } from '@/clients/ippool' import * as yaml from 'js-yaml' export default () => { @@ -13,13 +11,9 @@ export default () => { const navigate = useNavigate() const submit = async (data: string) => { - try { - const ipppolObject: any = yaml.load(data) - await clients.createResource(ResourceType.IPPOOL, ipppolObject) - navigate('/network/ippools') - } catch (err: any) { - notification.error({ message: getResourceName(ResourceType.IPPOOL), description: getErrorMessage(err) }) - } + const ippool: IPPool = yaml.load(data) as IPPool + await createIPPool(ippool, undefined, undefined, notification) + navigate('/network/ippools') } return ( diff --git a/src/pages/network/ippool/detail/index.tsx b/src/pages/network/ippool/detail/index.tsx index 6947e85..4d8293b 100644 --- a/src/pages/network/ippool/detail/index.tsx +++ b/src/pages/network/ippool/detail/index.tsx @@ -1,9 +1,46 @@ import { DetailYaml } from "@/components/detail-yaml" -import { ResourceType } from "@/clients/ts/types/types" -import { useWatchResourceInNamespaceName } from "@/hooks/use-resource" +import { useEffect, useRef, useState } from "react" +import { IPPool, updateIPPool, watchIPPool } from "@/clients/ippool" +import { App } from "antd" +import { useNamespaceFromURL } from "@/hooks/use-query-params-from-url" +import { useNavigate } from "react-router" +import useUnmount from "@/hooks/use-unmount" export default () => { - const { resource, loading } = useWatchResourceInNamespaceName(ResourceType.IPPOOL) + const { notification } = App.useApp() - return + const navigate = useNavigate() + + const ns = useNamespaceFromURL() + + const [loading, setLoading] = useState(true) + + const [ippool, setIPPool] = useState() + + const abortCtrl = useRef() + + useEffect(() => { + abortCtrl.current?.abort() + abortCtrl.current = new AbortController() + watchIPPool(ns, setIPPool, setLoading, abortCtrl.current.signal, notification) + }, [ns]) + + useUnmount(() => { + abortCtrl.current?.abort() + }) + + const handleSave = async (data: any) => { + await updateIPPool(data as IPPool, undefined, undefined, notification) + } + + const handleCancel = () => { + navigate('/network/ippools') + } + + return } diff --git a/src/pages/network/ippool/list/index.tsx b/src/pages/network/ippool/list/index.tsx index 74fbc6e..c8555c5 100644 --- a/src/pages/network/ippool/list/index.tsx +++ b/src/pages/network/ippool/list/index.tsx @@ -1,18 +1,23 @@ import { PlusOutlined } from '@ant-design/icons' import { App, Button, Dropdown, Flex, MenuProps, Modal, Popover, Space, Tag } from 'antd' -import { useRef, useState } from 'react' -import { extractNamespaceAndName } from '@/utils/k8s' +import { useEffect, useRef, useState } from 'react' import { Link, NavLink } from 'react-router-dom' -import { dataSource, formatTimestamp, generateMessage, getErrorMessage } from '@/utils/utils' -import { clients, getResourceName } from '@/clients/clients' -import { ResourceType } from '@/clients/ts/types/types' -import { NotificationInstance } from 'antd/lib/notification/interface' +import { formatTimestamp } from '@/utils/utils' +import { NamespaceName } from '@/clients/ts/types/types' import { EllipsisOutlined } from '@ant-design/icons' import { CustomTable, SearchItem } from '@/components/custom-table' import { WatchOptions } from '@/clients/ts/management/resource/v1alpha1/watch' -import { useWatchResources } from '@/hooks/use-resource' +import { deleteIPPool, deleteIPPools, IPPool, watchIPPools } from '@/clients/ippool' import type { ActionType, ProColumns } from '@ant-design/pro-components' import commonStyles from '@/common/styles/common.module.less' +import useUnmount from '@/hooks/use-unmount' + +const searchItems: SearchItem[] = [ + { fieldPath: "metadata.name", name: "Name", operator: "*=" }, + { fieldPath: "spec.subnet", name: "Subnet", operator: "*=" }, + { fieldPath: "spec.ips[*]", name: "IPs", operator: "*=" }, + { fieldPath: "spec.namespaces[*]", name: "Namespace", operator: "*=" } +] export default () => { const { notification } = App.useApp() @@ -21,85 +26,79 @@ export default () => { const actionRef = useRef() - const columns = columnsFunc(actionRef, notification) - const [opts, setOpts] = useState(WatchOptions.create()) - const { resources, loading } = useWatchResources(ResourceType.IPPOOL, opts) + const [loading, setLoading] = useState(true) + + const [resources, setResources] = useState() + + const abortCtrl = useRef() - const handleBatchDeleteIPPool = async () => { - const resourceName = getResourceName(ResourceType.IPPOOL) + useEffect(() => { + abortCtrl.current?.abort() + abortCtrl.current = new AbortController() + watchIPPools(setResources, setLoading, abortCtrl.current.signal, opts, notification) + }, [opts]) + + useUnmount(() => { + abortCtrl.current?.abort() + }) + + const handleDeleteIPPools = async () => { Modal.confirm({ - title: `Batch delete ${resourceName}?`, - content: generateMessage(selectedRows, `You are about to delete the following ${resourceName}: "{names}", please confirm.`, `You are about to delete the following ${resourceName}: "{names}" and {count} others, please confirm.`), - okText: 'Confirm Delete', + title: `Confirm batch deletion of IP pools?`, + content: `Are you sure you want to delete the selected IP pools? This action cannot be undone.`, + okText: 'Delete', okType: 'danger', cancelText: 'Cancel', - okButtonProps: { disabled: false }, onOk: async () => { - clients.batchDeleteResources(ResourceType.IPPOOL, selectedRows).catch(err => { - notification.error({ - message: `Batch delete of ${resourceName} failed`, - description: getErrorMessage(err) - }) - }) + await deleteIPPools(selectedRows, undefined, notification) } }) } - return ( - setSelectedRows(rows)} - storageKey="ippool-list-table-columns" - columns={columns} - dataSource={dataSource(resources)} - tableAlertOptionRender={() => { - return ( - - handleBatchDeleteIPPool()}>批量删除 - - ) - }} - toolbar={{ - actions: [ - - ] - }} - /> - ) -} - -const searchItems: SearchItem[] = [ - { fieldPath: "metadata.name", name: "Name", operator: "*=" }, - { fieldPath: "spec.subnet", name: "Subnet", operator: "*=" }, - { fieldPath: "spec.ips[*]", name: "IPs", operator: "*=" }, - { fieldPath: "spec.namespaces[*]", name: "Namespace", operator: "*=" } -] + const actionItemsFunc = (ippool: IPPool) => { + const ns: NamespaceName = { namespace: "", name: ippool.metadata!.name } + const items: MenuProps['items'] = [{ + key: 'delete', + danger: true, + onClick: () => { + Modal.confirm({ + title: "Confirm deletion of IP pool", + content: `Are you sure you want to delete the IP pool "${ns.name}"? This action cannot be undone.`, + okText: 'Delete', + okType: 'danger', + cancelText: 'Cancel', + onOk: async () => { + await deleteIPPool(ns, undefined, notification) + } + }) + }, + label: "删除" + }] + return items + } -const columnsFunc = (actionRef: any, notification: NotificationInstance) => { - const columns: ProColumns[] = [ + const columns: ProColumns[] = [ { key: 'name', title: '名称', fixed: 'left', ellipsis: true, - render: (_, ippool) => {ippool.metadata.name} + render: (_, ippool) => {ippool.metadata!.name} }, { key: 'subnet', title: '子网', ellipsis: true, - render: (_, ippool) => ippool.spec.subnet + render: (_, ippool) => ippool.spec?.subnet }, { key: 'ips', title: 'IPs', ellipsis: true, render: (_, ippool) => { - let ips = ippool.spec.ips + let ips = ippool.spec?.ips if (!ips || ips.length === 0) { return } @@ -114,7 +113,7 @@ const columnsFunc = (actionRef: any, notification: NotificationInstance) => { ) - const parts = ips[0].split("..") + const parts = (ips[0] as string).split("..") let display = parts.length > 1 ? `${parts[0]}..` : parts[0] return ( @@ -130,14 +129,14 @@ const columnsFunc = (actionRef: any, notification: NotificationInstance) => { title: '命名空间', ellipsis: true, render: (_, ippool) => { - let nss = ippool.spec.namespaces + let nss = ippool.spec?.namespaces if (!nss || nss.length === 0) { return } const content = ( - {nss.map((element: any, index: any) => ( + {nss.map((element, index) => ( {element} @@ -158,9 +157,7 @@ const columnsFunc = (actionRef: any, notification: NotificationInstance) => { title: '创建时间', width: 160, ellipsis: true, - render: (_, ippool) => { - return formatTimestamp(ippool.metadata.creationTimestamp) - } + render: (_, ippool) => formatTimestamp(ippool.metadata!.creationTimestamp) }, { key: 'action', @@ -169,7 +166,7 @@ const columnsFunc = (actionRef: any, notification: NotificationInstance) => { width: 90, align: 'center', render: (_, ippool) => { - const items = actionItemsFunc(ippool, actionRef, notification) + const items = actionItemsFunc(ippool) return ( @@ -178,48 +175,28 @@ const columnsFunc = (actionRef: any, notification: NotificationInstance) => { } } ] - return columns -} - -const actionItemsFunc = (ippool: any, actionRef: any, notification: NotificationInstance) => { - const name = ippool.metadata.name - const items: MenuProps['items'] = [ - { - key: 'edit', - label: "编辑" - }, - { - key: 'bindlabel', - label: '绑定标签' - }, - { - key: 'divider-3', - type: 'divider' - }, - { - key: 'delete', - danger: true, - onClick: () => { - Modal.confirm({ - title: "Delete IP pool?", - content: `You are about to delete the IP pool "${name}". Please confirm.`, - okText: 'Confirm delete', - okType: 'danger', - cancelText: 'Cancel', - okButtonProps: { disabled: false }, - onOk: async () => { - try { - await clients.deleteResource(ResourceType.IPPOOL, extractNamespaceAndName(ippool)) - actionRef.current?.reload() - } catch (err) { - notification.error({ message: getResourceName(ResourceType.IPPOOL), description: getErrorMessage(err) }) - } - } - }) - }, - label: "删除" - } - ] - return items + return ( + setSelectedRows(rows)} + key="ippool-list-table-columns" + columns={columns} + dataSource={resources} + tableAlertOptionRender={() => { + return ( + + handleDeleteIPPools()}>批量删除 + + ) + }} + toolbar={{ + actions: [ + + ] + }} + /> + ) } diff --git a/src/pages/network/multus/create/index.tsx b/src/pages/network/multus/create/index.tsx index 40f7381..aa9d9b9 100644 --- a/src/pages/network/multus/create/index.tsx +++ b/src/pages/network/multus/create/index.tsx @@ -2,9 +2,7 @@ import { App } from 'antd' import { useNavigate } from 'react-router-dom' import { multusYaml } from './crd-template' import { CreateCRDWithYaml } from '@/components/create-crd-with-yaml' -import { clients, getResourceName } from '@/clients/clients' -import { ResourceType } from '@/clients/ts/types/types' -import { getErrorMessage } from '@/utils/utils' +import { createMultus, Multus } from '@/clients/multus' import * as yaml from 'js-yaml' export default () => { @@ -13,13 +11,9 @@ export default () => { const navigate = useNavigate() const submit = async (data: string) => { - try { - const multusObject: any = yaml.load(data) - await clients.createResource(ResourceType.MULTUS, multusObject) - navigate('/network/multus') - } catch (err: any) { - notification.error({ message: getResourceName(ResourceType.MULTUS), description: getErrorMessage(err) }) - } + const multus = yaml.load(data) as Multus + await createMultus(multus, undefined, undefined, notification) + navigate('/network/multus') } return ( diff --git a/src/pages/network/multus/detail/index.tsx b/src/pages/network/multus/detail/index.tsx index b925d75..b3ead6a 100644 --- a/src/pages/network/multus/detail/index.tsx +++ b/src/pages/network/multus/detail/index.tsx @@ -1,9 +1,46 @@ import { DetailYaml } from "@/components/detail-yaml" -import { ResourceType } from "@/clients/ts/types/types" -import { useWatchResourceInNamespaceName } from "@/hooks/use-resource" +import { useEffect, useRef, useState } from "react" +import { App } from "antd" +import { useNamespaceFromURL } from "@/hooks/use-query-params-from-url" +import { useNavigate } from "react-router" +import { Multus, updateMultus, watchMultus } from "@/clients/multus" +import useUnmount from "@/hooks/use-unmount" export default () => { - const { resource, loading } = useWatchResourceInNamespaceName(ResourceType.MULTUS) + const { notification } = App.useApp() - return + const navigate = useNavigate() + + const ns = useNamespaceFromURL() + + const [loading, setLoading] = useState(true) + + const [multus, setMultus] = useState() + + const abortCtrl = useRef() + + useEffect(() => { + abortCtrl.current?.abort() + abortCtrl.current = new AbortController() + watchMultus(ns, setMultus, setLoading, abortCtrl.current.signal, notification) + }, [ns]) + + useUnmount(() => { + abortCtrl.current?.abort() + }) + + const handleSave = async (data: any) => { + await updateMultus(data as Multus, undefined, undefined, notification) + } + + const handleCancel = () => { + navigate('/network/multus') + } + + return } diff --git a/src/pages/network/multus/list/index.tsx b/src/pages/network/multus/list/index.tsx index feaa8bd..3677717 100644 --- a/src/pages/network/multus/list/index.tsx +++ b/src/pages/network/multus/list/index.tsx @@ -2,18 +2,22 @@ import { PlusOutlined } from '@ant-design/icons' import { App, Button, Dropdown, MenuProps, Modal, Space } from 'antd' import { useEffect, useRef, useState } from 'react' import { Link, NavLink } from 'react-router-dom' -import { dataSource, filterNullish, formatTimestamp, generateMessage, getErrorMessage, getProvider } from '@/utils/utils' +import { filterNullish, formatTimestamp, getProvider } from '@/utils/utils' import { useNamespace } from '@/common/context' -import { clients, getResourceName } from '@/clients/clients' -import { FieldSelector, ResourceType } from '@/clients/ts/types/types' -import { NotificationInstance } from 'antd/lib/notification/interface' +import { FieldSelector, NamespaceName } from '@/clients/ts/types/types' import { EllipsisOutlined } from '@ant-design/icons' import { CustomTable, SearchItem } from '@/components/custom-table' import { WatchOptions } from '@/clients/ts/management/resource/v1alpha1/watch' -import { useWatchResources } from '@/hooks/use-resource' +import { getNamespaceFieldSelector } from '@/utils/search' +import { deleteMultus, deleteMultuses, Multus, watchMultuses } from '@/clients/multus' +import { namespaceNameKey } from '@/utils/k8s' import type { ActionType, ProColumns } from '@ant-design/pro-components' import commonStyles from '@/common/styles/common.module.less' -import { getNamespaceFieldSelector } from '@/utils/search' +import useUnmount from '@/hooks/use-unmount' + +const searchItems: SearchItem[] = [ + { fieldPath: "metadata.name", name: "Name", operator: "*=" } +] export default () => { const { notification } = App.useApp() @@ -24,8 +28,6 @@ export default () => { const actionRef = useRef() - const columns = columnsFunc(actionRef, notification) - const [defaultFieldSelectors, setDefaultFieldSelectors] = useState(filterNullish([getNamespaceFieldSelector(namespace)])) const [opts, setOpts] = useState(WatchOptions.create({ fieldSelectorGroup: { operator: "&&", fieldSelectors: defaultFieldSelectors } @@ -35,24 +37,31 @@ export default () => { setDefaultFieldSelectors(filterNullish([getNamespaceFieldSelector(namespace)])) }, [namespace]) - const { resources, loading } = useWatchResources(ResourceType.MULTUS, opts) + const [loading, setLoading] = useState(true) + + const [resources, setResources] = useState() + + const abortCtrl = useRef() + + useEffect(() => { + abortCtrl.current?.abort() + abortCtrl.current = new AbortController() + watchMultuses(setResources, setLoading, abortCtrl.current.signal, opts, notification) + }, [opts]) + + useUnmount(() => { + abortCtrl.current?.abort() + }) - const handleBatchDeleteMultus = async () => { - const resourceName = getResourceName(ResourceType.MULTUS) + const handleDeleteMultus = async () => { Modal.confirm({ - title: `Batch delete ${resourceName}?`, - content: generateMessage(selectedRows, `You are about to delete the following ${resourceName}: "{names}", please confirm.`, `You are about to delete the following ${resourceName}: "{names}" and {count} others, please confirm.`), - okText: 'Confirm Delete', + title: `Confirm batch deletion of multus`, + content: `Are you sure you want to delete the selected multus? This action cannot be undone.`, + okText: 'Delete', okType: 'danger', cancelText: 'Cancel', - okButtonProps: { disabled: false }, onOk: async () => { - clients.batchDeleteResources(ResourceType.MULTUS, selectedRows).catch(err => { - notification.error({ - message: `Batch delete of ${resourceName} failed`, - description: getErrorMessage(err) - }) - }) + await deleteMultuses(selectedRows, setLoading, notification) } }) } @@ -61,44 +70,37 @@ export default () => { actionRef.current?.reload() }, [namespace]) - return ( - setSelectedRows(rows)} - defaultFieldSelectors={defaultFieldSelectors} - storageKey="multus-list-table-columns" - columns={columns} - dataSource={dataSource(resources)} - tableAlertOptionRender={() => { - return ( - - handleBatchDeleteMultus()}>批量删除 - - ) - }} - toolbar={{ - actions: [ - - ] - }} - /> - ) -} - -const searchItems: SearchItem[] = [ - { fieldPath: "metadata.name", name: "Name", operator: "*=" } -] + const actionItemsFunc = (mc: Multus) => { + const ns: NamespaceName = { namespace: mc.metadata!.namespace, name: mc.metadata!.name } + const items: MenuProps['items'] = [ + { + key: 'delete', + danger: true, + onClick: () => { + Modal.confirm({ + title: "Confirm deletion of multus", + content: `Are you sure you want to delete the multus "${namespaceNameKey(ns)}"? This action cannot be undone.`, + okText: 'Delete', + okType: 'danger', + cancelText: 'Cancel', + onOk: async () => { + await deleteMultus(ns, undefined, notification) + } + }) + }, + label: "删除" + } + ] + return items + } -const columnsFunc = (actionRef: any, notification: NotificationInstance) => { - const columns: ProColumns[] = [ + const columns: ProColumns[] = [ { key: 'name', title: '名称', fixed: 'left', ellipsis: true, - render: (_, mc) => {mc.metadata.name} + render: (_, mc) => {mc.metadata!.name} }, { key: 'provider', @@ -111,9 +113,7 @@ const columnsFunc = (actionRef: any, notification: NotificationInstance) => { title: '创建时间', width: 160, ellipsis: true, - render: (_, mc) => { - return formatTimestamp(mc.metadata.creationTimestamp) - } + render: (_, mc) => formatTimestamp(mc.metadata!.creationTimestamp) }, { key: 'action', @@ -122,7 +122,7 @@ const columnsFunc = (actionRef: any, notification: NotificationInstance) => { width: 90, align: 'center', render: (_, mc) => { - const items = actionItemsFunc(mc, actionRef, notification) + const items = actionItemsFunc(mc) return ( @@ -131,49 +131,29 @@ const columnsFunc = (actionRef: any, notification: NotificationInstance) => { } } ] - return columns -} - -const actionItemsFunc = (mc: any, actionRef: any, notification: NotificationInstance) => { - const namespace = mc.metadata.namespace - const name = mc.metadata.name - const items: MenuProps['items'] = [ - { - key: 'edit', - label: "编辑" - }, - { - key: 'bindlabel', - label: '绑定标签' - }, - { - key: 'divider-3', - type: 'divider' - }, - { - key: 'delete', - danger: true, - onClick: () => { - Modal.confirm({ - title: "Delete Multus configuration?", - content: `You are about to delete the Multus configuration "${namespace}/${name}". Please confirm.`, - okText: 'Confirm delete', - okType: 'danger', - cancelText: 'Cancel', - okButtonProps: { disabled: false }, - onOk: async () => { - try { - await clients.deleteResource(ResourceType.MULTUS, { namespace: namespace, name: name }) - actionRef.current?.reload() - } catch (err) { - notification.error({ message: getResourceName(ResourceType.MULTUS), description: getErrorMessage(err) }) - } - } - }) - }, - label: "删除" - } - ] - return items + return ( + setSelectedRows(rows)} + defaultFieldSelectors={defaultFieldSelectors} + key="multus-list-table-columns" + columns={columns} + dataSource={resources} + tableAlertOptionRender={() => { + return ( + + handleDeleteMultus()}>批量删除 + + ) + }} + toolbar={{ + actions: [ + + ] + }} + /> + ) } diff --git a/src/pages/network/subnet/create/index.tsx b/src/pages/network/subnet/create/index.tsx index 1420608..dde38ef 100644 --- a/src/pages/network/subnet/create/index.tsx +++ b/src/pages/network/subnet/create/index.tsx @@ -2,10 +2,8 @@ import { App } from 'antd' import { subnetYaml } from './crd-template' import { useNavigate } from 'react-router-dom' import { CreateCRDWithYaml } from '@/components/create-crd-with-yaml' -import { clients, getResourceName } from '@/clients/clients' -import { ResourceType } from '@/clients/ts/types/types' +import { createSubnet, Subnet } from '@/clients/subnet' import * as yaml from 'js-yaml' -import { getErrorMessage } from '@/utils/utils' export default () => { const { notification } = App.useApp() @@ -13,13 +11,9 @@ export default () => { const navigate = useNavigate() const submit = async (data: string) => { - try { - const subnetObject: any = yaml.load(data) - await clients.createResource(ResourceType.SUBNET, subnetObject) - navigate('/network/subnets') - } catch (err: any) { - notification.error({ message: getResourceName(ResourceType.MULTUS), description: getErrorMessage(err) }) - } + const subnet = yaml.load(data) as Subnet + await createSubnet(subnet, undefined, undefined, notification) + navigate('/network/subnets') } return ( diff --git a/src/pages/network/subnet/detail/index.tsx b/src/pages/network/subnet/detail/index.tsx index 3f187ae..266dfcd 100644 --- a/src/pages/network/subnet/detail/index.tsx +++ b/src/pages/network/subnet/detail/index.tsx @@ -1,9 +1,46 @@ import { DetailYaml } from "@/components/detail-yaml" -import { ResourceType } from "@/clients/ts/types/types" -import { useWatchResourceInNamespaceName } from "@/hooks/use-resource" +import { useEffect, useRef, useState } from "react" +import { App } from "antd" +import { useNamespaceFromURL } from "@/hooks/use-query-params-from-url" +import { useNavigate } from "react-router" +import { Subnet, updateSubnet, watchSubnet } from "@/clients/subnet" +import useUnmount from "@/hooks/use-unmount" export default () => { - const { resource, loading } = useWatchResourceInNamespaceName(ResourceType.SUBNET) + const { notification } = App.useApp() - return + const navigate = useNavigate() + + const ns = useNamespaceFromURL() + + const [loading, setLoading] = useState(true) + + const [subnet, setSubnet] = useState() + + const abortCtrl = useRef() + + useEffect(() => { + abortCtrl.current?.abort() + abortCtrl.current = new AbortController() + watchSubnet(ns, setSubnet, setLoading, abortCtrl.current.signal, notification) + }, [ns]) + + useUnmount(() => { + abortCtrl.current?.abort() + }) + + const handleSave = async (data: any) => { + await updateSubnet(data as Subnet, undefined, undefined, notification) + } + + const handleCancel = () => { + navigate('/network/subnets') + } + + return } diff --git a/src/pages/network/subnet/list/index.tsx b/src/pages/network/subnet/list/index.tsx index 145aaa4..46944d0 100644 --- a/src/pages/network/subnet/list/index.tsx +++ b/src/pages/network/subnet/list/index.tsx @@ -1,20 +1,33 @@ import { PlusOutlined } from '@ant-design/icons' import { App, Badge, Button, Dropdown, Flex, MenuProps, Modal, Popover, Space, Tag } from 'antd' -import { useRef, useState } from 'react' -import { extractNamespaceAndName } from '@/utils/k8s' +import { useEffect, useRef, useState } from 'react' import { Link, NavLink } from 'react-router-dom' -import { dataSource, formatTimestamp, generateMessage, getErrorMessage } from '@/utils/utils' -import { ResourceType } from '@/clients/ts/types/types' -import { clients, getResourceName } from '@/clients/clients' -import { useWatchResources } from '@/hooks/use-resource' -import { NotificationInstance } from 'antd/es/notification/interface' +import { formatTimestamp } from '@/utils/utils' +import { NamespaceName } from '@/clients/ts/types/types' import { subnetStatus } from '@/utils/resource-status' import { EllipsisOutlined } from '@ant-design/icons' import { WatchOptions } from '@/clients/ts/management/resource/v1alpha1/watch' import { CustomTable, SearchItem } from '@/components/custom-table' +import { deleteSubnet, deleteSubnets, Subnet, watchSubnets } from '@/clients/subnet' import type { ActionType, ProColumns } from '@ant-design/pro-components' import commonStyles from '@/common/styles/common.module.less' +import useUnmount from '@/hooks/use-unmount' +const searchItems: SearchItem[] = [ + { fieldPath: "metadata.name", name: "Name", operator: "*=" }, + { + fieldPath: "status.conditions[*].type", name: "Status", + items: [ + { inputValue: "Ready", values: ["Ready"], operator: '=' }, + { inputValue: "NotReady", values: ["Ready"], operator: '!=' }, + ] + }, + { fieldPath: "spec.provider", name: "Provider", operator: "*=" }, + { fieldPath: "spec.vpc", name: "VPC", operator: "*=" }, + { fieldPath: "spec.cidrBlock", name: "CIDR", operator: "*=" }, + { fieldPath: "spec.gateway", name: "Gateway", operator: "*=" }, + { fieldPath: "spec.namespaces[*]", name: "Namespace", operator: "*=" } +] export default () => { const { notification } = App.useApp() @@ -23,81 +36,70 @@ export default () => { const actionRef = useRef() - const columns = columnsFunc(actionRef, notification) - const [opts, setOpts] = useState(WatchOptions.create()) - const { resources, loading } = useWatchResources(ResourceType.SUBNET, opts) + const [loading, setLoading] = useState(true) + + const [resources, setResources] = useState() + + const abortCtrl = useRef() - const handleBatchDeleteSubnet = async () => { - const resourceName = getResourceName(ResourceType.SUBNET) + useEffect(() => { + abortCtrl.current?.abort() + abortCtrl.current = new AbortController() + watchSubnets(setResources, setLoading, abortCtrl.current.signal, opts, notification) + }, [opts]) + + useUnmount(() => { + abortCtrl.current?.abort() + }) + + const handleDeleteSubnets = async () => { Modal.confirm({ - title: `Batch delete ${resourceName}?`, - content: generateMessage(selectedRows, `You are about to delete the following ${resourceName}: "{names}", please confirm.`, `You are about to delete the following ${resourceName}: "{names}" and {count} others, please confirm.`), - okText: 'Confirm Delete', + title: `Confirm batch deletion of subnets`, + content: `Are you sure you want to delete the selected subnets? This action cannot be undone.`, + okText: 'Delete', okType: 'danger', cancelText: 'Cancel', - okButtonProps: { disabled: false }, onOk: async () => { - clients.batchDeleteResources(ResourceType.SUBNET, selectedRows).catch(err => { - notification.error({ - message: `Batch delete of ${resourceName} failed`, - description: getErrorMessage(err) - }) - }) + await deleteSubnets(selectedRows, undefined, notification) } }) } - return ( - setSelectedRows(rows)} - storageKey="subnets-list-table-columns" - columns={columns} - dataSource={dataSource(resources)} - tableAlertOptionRender={() => { - return ( - - handleBatchDeleteSubnet()}>批量删除 - - ) - }} - toolbar={{ - actions: [ - - ] - }} - /> - ) -} + const actionItemsFunc = (subnet: Subnet) => { + const ns: NamespaceName = { namespace: "", name: subnet.metadata!.name! } -const searchItems: SearchItem[] = [ - { fieldPath: "metadata.name", name: "Name", operator: "*=" }, - { - fieldPath: "status.conditions[*].type", name: "Status", - items: [ - { inputValue: "Ready", values: ["Ready"], operator: '=' }, - { inputValue: "NotReady", values: ["Ready"], operator: '!=' }, + const items: MenuProps['items'] = [ + { + key: 'delete', + danger: true, + onClick: () => { + Modal.confirm({ + title: "Confirm deletion of subnet", + content: `Are you sure you want to delete the subnet "${ns.name}"? This action cannot be undone.`, + okText: 'Delete', + okType: 'danger', + cancelText: 'Cancel', + onOk: async () => { + await deleteSubnet(ns, undefined, notification) + actionRef.current?.reload() + } + }) + }, + label: "删除" + } ] - }, - { fieldPath: "spec.provider", name: "Provider", operator: "*=" }, - { fieldPath: "spec.vpc", name: "VPC", operator: "*=" }, - { fieldPath: "spec.cidrBlock", name: "CIDR", operator: "*=" }, - { fieldPath: "spec.gateway", name: "Gateway", operator: "*=" }, - { fieldPath: "spec.namespaces[*]", name: "Namespace", operator: "*=" } -] + return items + } -const columnsFunc = (actionRef: any, notification: NotificationInstance) => { - const columns: ProColumns[] = [ + const columns: ProColumns[] = [ { key: 'name', title: '名称', fixed: 'left', ellipsis: true, - render: (_, subnet) => {subnet.metadata.name} + render: (_, subnet) => {subnet.metadata!.name} }, { key: 'status', @@ -109,32 +111,32 @@ const columnsFunc = (actionRef: any, notification: NotificationInstance) => { key: 'gateway', title: 'Gateway', ellipsis: true, - render: (_, subnet) => subnet.spec.gateway + render: (_, subnet) => subnet.spec?.gateway }, { key: 'cidr', title: 'CIDR', ellipsis: true, - render: (_, subnet) => subnet.spec.cidrBlock + render: (_, subnet) => subnet.spec?.cidrBlock }, { key: 'provider', title: 'Provider', ellipsis: true, - render: (_, subnet) => subnet.spec.provider + render: (_, subnet) => subnet.spec?.provider }, { key: 'vpc', title: 'VPC', ellipsis: true, - render: (_, subnet) => subnet.spec.vpc + render: (_, subnet) => subnet.spec?.vpc }, { key: 'namespace', title: '命名空间', ellipsis: true, render: (_, subnet) => { - let nss = subnet.spec.namespaces + let nss = subnet.spec?.namespaces if (!nss || nss.length === 0) { return } @@ -161,20 +163,20 @@ const columnsFunc = (actionRef: any, notification: NotificationInstance) => { key: 'protocol', title: 'Protocol', ellipsis: true, - render: (_, subnet) => subnet.spec.protocol + render: (_, subnet) => subnet.spec?.protocol }, { key: 'nat', title: 'NAT', ellipsis: true, - render: (_, subnet) => String(subnet.spec.natOutgoing) + render: (_, subnet) => String(subnet.spec?.natOutgoing) }, { key: 'available', title: '可用 IP 数量', ellipsis: true, render: (_, subnet) => { - const count = subnet.spec.protocol == "IPv4" ? subnet.status?.v4availableIPs : subnet.status?.v6availableIPs + const count = subnet.spec?.protocol == "IPv4" ? subnet.status?.v4availableIPs : subnet.status?.v6availableIPs return String(count ? count : 0) } }, @@ -183,7 +185,7 @@ const columnsFunc = (actionRef: any, notification: NotificationInstance) => { title: '已用 IP 数量', ellipsis: true, render: (_, subnet) => { - const count = subnet.spec.protocol == "IPv4" ? subnet.status?.v4usingIPs : subnet.status?.v6usingIPs + const count = subnet.spec?.protocol == "IPv4" ? subnet.status?.v4usingIPs : subnet.status?.v6usingIPs return String(count ? count : 0) } }, @@ -192,9 +194,7 @@ const columnsFunc = (actionRef: any, notification: NotificationInstance) => { title: '创建时间', width: 160, ellipsis: true, - render: (_, subnet) => { - return formatTimestamp(subnet.metadata.creationTimestamp) - } + render: (_, subnet) => formatTimestamp(subnet.metadata!.creationTimestamp) }, { key: 'action', @@ -203,7 +203,7 @@ const columnsFunc = (actionRef: any, notification: NotificationInstance) => { width: 90, align: 'center', render: (_, subnet) => { - const items = actionItemsFunc(subnet, actionRef, notification) + const items = actionItemsFunc(subnet) return ( @@ -212,48 +212,28 @@ const columnsFunc = (actionRef: any, notification: NotificationInstance) => { } } ] - return columns -} - -const actionItemsFunc = (subnet: any, actionRef: any, notification: NotificationInstance) => { - const name = subnet.metadata.name - const items: MenuProps['items'] = [ - { - key: 'edit', - label: "编辑" - }, - { - key: 'bindlabel', - label: '绑定标签' - }, - { - key: 'divider-3', - type: 'divider' - }, - { - key: 'delete', - danger: true, - onClick: () => { - Modal.confirm({ - title: "Delete subnet?", - content: `You are about to delete the subnet "${name}". Please confirm.`, - okText: 'Confirm delete', - okType: 'danger', - cancelText: 'Cancel', - okButtonProps: { disabled: false }, - onOk: async () => { - try { - await clients.deleteResource(ResourceType.SUBNET, extractNamespaceAndName(subnet)) - actionRef.current?.reload() - } catch (err) { - notification.error({ message: getResourceName(ResourceType.SUBNET), description: getErrorMessage(err) }) - } - } - }) - }, - label: "删除" - } - ] - return items + return ( + setSelectedRows(rows)} + key="subnets-list-table-columns" + columns={columns} + dataSource={resources} + tableAlertOptionRender={() => { + return ( + + handleDeleteSubnets()}>批量删除 + + ) + }} + toolbar={{ + actions: [ + + ] + }} + /> + ) } diff --git a/src/pages/network/vpc/create/index.tsx b/src/pages/network/vpc/create/index.tsx index 502cfae..519f714 100644 --- a/src/pages/network/vpc/create/index.tsx +++ b/src/pages/network/vpc/create/index.tsx @@ -2,9 +2,7 @@ import { App } from 'antd' import { vpcYaml } from './crd-template' import { useNavigate } from 'react-router-dom' import { CreateCRDWithYaml } from '@/components/create-crd-with-yaml' -import { clients, getResourceName } from '@/clients/clients' -import { ResourceType } from '@/clients/ts/types/types' -import { getErrorMessage } from '@/utils/utils' +import { createVPC, VPC } from '@/clients/vpc' import * as yaml from 'js-yaml' export default () => { @@ -13,13 +11,9 @@ export default () => { const navigate = useNavigate() const submit = async (data: string) => { - try { - const vpcObject: any = yaml.load(data) - await clients.createResource(ResourceType.VPC, vpcObject) - navigate('/network/vpcs') - } catch (err: any) { - notification.error({ message: getResourceName(ResourceType.VPC), description: getErrorMessage(err) }) - } + const vpc = yaml.load(data) as VPC + await createVPC(vpc, undefined, undefined, notification) + navigate('/network/vpcs') } return ( diff --git a/src/pages/network/vpc/detail/index.tsx b/src/pages/network/vpc/detail/index.tsx index 6defbf3..1a242cd 100644 --- a/src/pages/network/vpc/detail/index.tsx +++ b/src/pages/network/vpc/detail/index.tsx @@ -1,9 +1,46 @@ import { DetailYaml } from "@/components/detail-yaml" -import { ResourceType } from "@/clients/ts/types/types" -import { useWatchResourceInNamespaceName } from "@/hooks/use-resource" +import { useEffect, useRef, useState } from "react" +import { App } from "antd" +import { useNamespaceFromURL } from "@/hooks/use-query-params-from-url" +import { useNavigate } from "react-router" +import { updateVPC, VPC, watchVPC } from "@/clients/vpc" +import useUnmount from "@/hooks/use-unmount" export default () => { - const { resource, loading } = useWatchResourceInNamespaceName(ResourceType.VPC) + const { notification } = App.useApp() - return + const navigate = useNavigate() + + const ns = useNamespaceFromURL() + + const [loading, setLoading] = useState(true) + + const [vpc, setVPC] = useState() + + const abortCtrl = useRef() + + useEffect(() => { + abortCtrl.current?.abort() + abortCtrl.current = new AbortController() + watchVPC(ns, setVPC, setLoading, abortCtrl.current.signal, notification) + }, [ns]) + + useUnmount(() => { + abortCtrl.current?.abort() + }) + + const handleSave = async (data: any) => { + await updateVPC(data as VPC, undefined, undefined, notification) + } + + const handleCancel = () => { + navigate('/network/vpcs') + } + + return } diff --git a/src/pages/network/vpc/list/index.tsx b/src/pages/network/vpc/list/index.tsx index ec9126b..c0d692a 100644 --- a/src/pages/network/vpc/list/index.tsx +++ b/src/pages/network/vpc/list/index.tsx @@ -1,18 +1,22 @@ import { PlusOutlined } from '@ant-design/icons' import { App, Button, Dropdown, Flex, MenuProps, Modal, Popover, Space, Tag } from 'antd' -import { useRef, useState } from 'react' -import { extractNamespaceAndName } from '@/utils/k8s' +import { useEffect, useRef, useState } from 'react' import { Link, NavLink } from 'react-router-dom' -import { dataSource, formatTimestamp, generateMessage, getErrorMessage } from '@/utils/utils' -import { clients, getResourceName } from '@/clients/clients' -import { ResourceType } from '@/clients/ts/types/types' -import { NotificationInstance } from 'antd/lib/notification/interface' +import { formatTimestamp } from '@/utils/utils' +import { NamespaceName } from '@/clients/ts/types/types' import { EllipsisOutlined } from '@ant-design/icons' import { CustomTable, SearchItem } from '@/components/custom-table' import { WatchOptions } from '@/clients/ts/management/resource/v1alpha1/watch' -import { useWatchResources } from '@/hooks/use-resource' +import { deleteVPC, deleteVPCs, VPC, watchVPCs } from '@/clients/vpc' import type { ActionType, ProColumns } from '@ant-design/pro-components' import commonStyles from '@/common/styles/common.module.less' +import useUnmount from '@/hooks/use-unmount' + +const searchItems: SearchItem[] = [ + { fieldPath: "metadata.name", name: "Name", operator: "*=" }, + { fieldPath: "spec.namespaces[*]", name: "Namespace", operator: "*=" }, + { fieldPath: "status.subnets[*]", name: "Subnet", operator: "*=" } +] export default () => { const { notification } = App.useApp() @@ -21,85 +25,82 @@ export default () => { const actionRef = useRef() - const columns = columnsFunc(actionRef, notification) - const [opts, setOpts] = useState(WatchOptions.create()) - const { resources, loading } = useWatchResources(ResourceType.VPC, opts) + const [loading, setLoading] = useState(true) + + const [resources, setResources] = useState() + + const abortCtrl = useRef() + + useEffect(() => { + abortCtrl.current?.abort() + abortCtrl.current = new AbortController() + watchVPCs(setResources, setLoading, abortCtrl.current.signal, opts, notification) + }, [opts]) - const handleBatchDeleteVPC = async () => { - const resourceName = getResourceName(ResourceType.VPC) + useUnmount(() => { + abortCtrl.current?.abort() + }) + + const handleDeleteVPCs = async () => { Modal.confirm({ - title: `Batch delete ${resourceName}?`, - content: generateMessage(selectedRows, `You are about to delete the following ${resourceName}: "{names}", please confirm.`, `You are about to delete the following ${resourceName}: "{names}" and {count} others, please confirm.`), - okText: 'Confirm Delete', + title: `Confirm batch deletion of VPCs`, + content: `Are you sure you want to delete the selected VPCs? This action cannot be undone.`, + okText: 'Delete', okType: 'danger', cancelText: 'Cancel', - okButtonProps: { disabled: false }, onOk: async () => { - clients.batchDeleteResources(ResourceType.VPC, selectedRows).catch(err => { - notification.error({ - message: `Batch delete of ${resourceName} failed`, - description: getErrorMessage(err) - }) - }) + await deleteVPCs(selectedRows, undefined, notification) } }) } - return ( - setSelectedRows(rows)} - storageKey="vpcs-list-table-columns" - columns={columns} - dataSource={dataSource(resources)} - tableAlertOptionRender={() => { - return ( - - handleBatchDeleteVPC()}>批量删除 - - ) - }} - toolbar={{ - actions: [ - - ] - }} - /> - ) -} - -const searchItems: SearchItem[] = [ - { fieldPath: "metadata.name", name: "Name", operator: "*=" }, - { fieldPath: "spec.namespaces[*]", name: "Namespace", operator: "*=" }, - { fieldPath: "status.subnets[*]", name: "Subnet", operator: "*=" } -] + const actionItemsFunc = (vpc: VPC) => { + const ns: NamespaceName = { namespace: "", name: vpc.metadata!.name } + const items: MenuProps['items'] = [ + { + key: 'delete', + danger: true, + onClick: () => { + Modal.confirm({ + title: "Confirm deletion of VPC", + content: `Are you sure you want to delete the VPC "${ns.name}"? This action cannot be undone.`, + okText: 'Delete', + okType: 'danger', + cancelText: 'Cancel', + onOk: async () => { + await deleteVPC(ns, undefined, notification) + } + }) + }, + label: "删除" + } + ] + return items + } -const columnsFunc = (actionRef: any, notification: NotificationInstance) => { - const columns: ProColumns[] = [ + const columns: ProColumns[] = [ { key: 'name', title: '名称', fixed: 'left', ellipsis: true, - render: (_, vpc) => {vpc.metadata.name} + render: (_, vpc) => {vpc.metadata!.name} }, { key: 'namespace', title: '命名空间', ellipsis: true, render: (_, vpc) => { - let nss = vpc.spec.namespaces + let nss = vpc.spec?.namespaces if (!nss || nss.length === 0) { return } const content = ( - {nss.map((element: any, index: any) => ( + {nss.map((element, index) => ( {element} @@ -126,7 +127,7 @@ const columnsFunc = (actionRef: any, notification: NotificationInstance) => { const content = ( - {vpc.status.subnets.map((element: any, index: any) => ( + {vpc.status.subnets.map((element, index) => ( {element} @@ -147,9 +148,7 @@ const columnsFunc = (actionRef: any, notification: NotificationInstance) => { title: '创建时间', width: 160, ellipsis: true, - render: (_, mc) => { - return formatTimestamp(mc.metadata.creationTimestamp) - } + render: (_, vpc) => formatTimestamp(vpc.metadata!.creationTimestamp) }, { key: 'action', @@ -157,8 +156,8 @@ const columnsFunc = (actionRef: any, notification: NotificationInstance) => { fixed: 'right', width: 90, align: 'center', - render: (_, mc) => { - const items = actionItemsFunc(mc, actionRef, notification) + render: (_, vpc) => { + const items = actionItemsFunc(vpc) return ( @@ -167,48 +166,28 @@ const columnsFunc = (actionRef: any, notification: NotificationInstance) => { } } ] - return columns -} - -const actionItemsFunc = (m: any, actionRef: any, notification: NotificationInstance) => { - const name = m.metadata.name - const items: MenuProps['items'] = [ - { - key: 'edit', - label: "编辑" - }, - { - key: 'bindlabel', - label: '绑定标签' - }, - { - key: 'divider-3', - type: 'divider' - }, - { - key: 'delete', - danger: true, - onClick: () => { - Modal.confirm({ - title: "Delete VPC?", - content: `You are about to delete the VPC "${name}". Please confirm.`, - okText: 'Confirm delete', - okType: 'danger', - cancelText: 'Cancel', - okButtonProps: { disabled: false }, - onOk: async () => { - try { - await clients.deleteResource(ResourceType.VPC, extractNamespaceAndName(m)) - actionRef.current?.reload() - } catch (err) { - notification.error({ message: getResourceName(ResourceType.VPC), description: getErrorMessage(err) }) - } - } - }) - }, - label: "删除" - } - ] - return items + return ( + setSelectedRows(rows)} + key="vpcs-list-table-columns" + columns={columns} + dataSource={resources} + tableAlertOptionRender={() => { + return ( + + handleDeleteVPCs()}>批量删除 + + ) + }} + toolbar={{ + actions: [ + + ] + }} + /> + ) } diff --git a/src/pages/storage/datavolume/index.ts b/src/pages/storage/datavolume/index.ts index 1423120..c6bda4d 100644 --- a/src/pages/storage/datavolume/index.ts +++ b/src/pages/storage/datavolume/index.ts @@ -1,7 +1,6 @@ -import * as yaml from 'js-yaml' -import { dataVolumYaml } from './template' import { instances as labels } from "@/clients/ts/label/labels.gen" import { NamespaceName } from '@/clients/ts/types/types' +import { DataVolume } from '@/clients/data-volume' const defaultAccessMode = "ReadWriteOnce" @@ -13,12 +12,35 @@ const getProtocol = (url: string): string | null => { return match ? match[1] : null } -export const newSystemImage = (namespaceName: NamespaceName, imageSource: string, imageCapacity: number, family: string, version: string) => { - const instance: any = yaml.load(dataVolumYaml) - - instance.metadata.name = namespaceName.name - instance.metadata.namespace = namespaceName.namespace - instance.spec.pvc.resources.requests.storage = `${imageCapacity}Gi` +export const newSystemImage = (ns: NamespaceName, imageSource: string, imageCapacity: number, family: string, version: string) => { + const instance: DataVolume = { + apiVersion: "cdi.kubevirt.io/v1beta1", + kind: "DataVolume", + metadata: { + name: ns.name, + namespace: ns.namespace, + labels: { + [labels.VinkDatavolumeType.name]: "image", + [labels.VinkOperatingSystem.name]: family, + [labels.VinkOperatingSystemVersion.name]: version + }, + annotations: { + "cdi.kubevirt.io/storage.bind.immediate.requested": "true" + } + }, + spec: { + pvc: { + accessModes: ["ReadWriteOnce"], + resources: { + requests: { + storage: `${imageCapacity}Gi` + } + }, + storageClassName: "local-path" + }, + source: {} + } + } switch (getProtocol(imageSource)) { case "docker": @@ -33,24 +55,36 @@ export const newSystemImage = (namespaceName: NamespaceName, imageSource: string break } - instance.metadata.labels[labels.VinkDatavolumeType.name] = "image" - instance.metadata.labels[labels.VinkOperatingSystem.name] = family - instance.metadata.labels[labels.VinkOperatingSystemVersion.name] = version - return instance } -export const newDataDisk = (namespaceName: NamespaceName, diskCapacity: number, storageClass?: string, accessMode?: string) => { - const instance: any = yaml.load(dataVolumYaml) - - instance.metadata.name = namespaceName.name - instance.metadata.namespace = namespaceName.namespace - instance.spec.pvc.resources.requests.storage = `${diskCapacity}Gi` - instance.spec.source = { blank: {} } - - instance.spec.pvc.accessModes = [(accessMode && accessMode.length > 0) ? accessMode : defaultAccessMode] - instance.spec.pvc.storageClassName = (storageClass && storageClass.length > 0) ? storageClass : defaultStorageClass - instance.metadata.labels[labels.VinkDatavolumeType.name] = "data" +export const newDataDisk = (ns: NamespaceName, diskCapacity: number, storageClass?: string, accessMode?: string) => { + const instance: DataVolume = { + apiVersion: "cdi.kubevirt.io/v1beta1", + kind: "DataVolume", + metadata: { + name: ns.name, + namespace: ns.namespace, + labels: { + [labels.VinkDatavolumeType.name]: "data" + }, + annotations: { + "cdi.kubevirt.io/storage.bind.immediate.requested": "true" + } + }, + spec: { + pvc: { + accessModes: [(accessMode && accessMode.length > 0) ? accessMode : defaultAccessMode], + resources: { + requests: { + storage: `${diskCapacity}Gi` + } + }, + storageClassName: (storageClass && storageClass.length > 0) ? storageClass : defaultStorageClass + }, + source: { blank: {} } + } + } return instance } diff --git a/src/pages/storage/datavolume/template.ts b/src/pages/storage/datavolume/template.ts deleted file mode 100644 index 6e0ed85..0000000 --- a/src/pages/storage/datavolume/template.ts +++ /dev/null @@ -1,19 +0,0 @@ -export const dataVolumYaml = ` -apiVersion: cdi.kubevirt.io/v1beta1 -kind: DataVolume -metadata: - name: "" - namespace: "" - labels: {} - annotations: - cdi.kubevirt.io/storage.bind.immediate.requested: "true" -spec: - pvc: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: "" - storageClassName: local-path - source: {} -` diff --git a/src/pages/storage/disk/create/index.tsx b/src/pages/storage/disk/create/index.tsx index f20b5ca..5a3052b 100644 --- a/src/pages/storage/disk/create/index.tsx +++ b/src/pages/storage/disk/create/index.tsx @@ -3,64 +3,78 @@ import { App, Button, InputNumber, Space } from 'antd' import { useEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import { useNamespace } from '@/common/context' -import { classNames, getErrorMessage } from '@/utils/utils' -import { ResourceType } from '@/clients/ts/types/types' -import { clients, getResourceName } from '@/clients/clients' +import { classNames } from '@/utils/utils' import { PlusOutlined } from '@ant-design/icons' import { newDataDisk } from '../../datavolume' +import { createDataVolume } from '@/clients/data-volume' +import { listStorageClass, StorageClass } from '@/clients/storage-class' import type { ProFormInstance } from '@ant-design/pro-components' import formStyles from "@/common/styles/form.module.less" +interface formValues { + namespace: string + name: string + dataDiskCapacity: number + storageClass: string + accessMode: string + description: string +} + +const accessModeOptions = [ + { + value: "ReadWriteOnce", + label: "ReadWriteOnce" + }, + { + value: "ReadOnlyMany", + label: "ReadOnlyMany" + }, + { + value: "ReadWriteMany", + label: "ReadWriteMany" + }, + { + value: "ReadWriteOncePod", + label: "ReadWriteOncePod" + } +] + export default () => { const { notification } = App.useApp() const { namespace } = useNamespace() - const formRef = useRef() + const formRef = useRef>() const navigate = useNavigate() const [enableOpts, setEnableOpts] = useState<{ storageClass: boolean, accessMode: boolean }>({ storageClass: false, accessMode: false }) useEffect(() => { - formRef.current?.setFieldsValue({ - namespace: namespace - }) + formRef.current?.setFieldsValue({ namespace: namespace }) }, [namespace]) - const { storageClass } = useStorageClass(enableOpts.storageClass) + const [storageClass, setStorageClass] = useState() + useEffect(() => { + if (!enableOpts.storageClass) { + return + } + listStorageClass(setStorageClass, undefined, undefined, notification) + }, [enableOpts.storageClass]) const handleSubmit = async () => { if (!formRef.current) { - return + throw new Error("formRef is not initialized") } - try { - await formRef.current.validateFields() - - const fields = formRef.current.getFieldsValue() - - const namespaceName = { namespace: fields.namespace, name: fields.name } - - const instance = newDataDisk( - namespaceName, - fields.dataDiskCapacity, - fields.storageClass, - fields.accessMode - ) - - await clients.createResource(ResourceType.DATA_VOLUME, instance) - navigate('/storage/disks') - } catch (err: any) { - const errorMessage = err.errorFields?.map((field: any, idx: number) => `${idx + 1}. ${field.errors}`).join('
') || getErrorMessage(err) - notification.error({ - message: getResourceName(ResourceType.DATA_VOLUME), - description: ( -
- ) - }) - } + await formRef.current.validateFields() + const fields = formRef.current.getFieldsValue() + const ns = { namespace: fields.namespace, name: fields.name } + const instance = newDataDisk(ns, fields.dataDiskCapacity, fields.storageClass, fields.accessMode) + await createDataVolume(instance, undefined, undefined, notification) + + navigate('/storage/disks') } return ( @@ -133,12 +147,12 @@ export default () => { width="lg" name="storageClass" placeholder="选择存储类" - options={[ - ...storageClass.map((ns: any) => ({ - value: ns.metadata.name, - label: ns.metadata.name + options={ + (storageClass || []).map((sc) => ({ + value: sc.metadata.name, + label: sc.metadata.name })) - ]} + } /> } - ] - }} - /> - ) -} - -const searchItems: SearchItem[] = [ - { fieldPath: "metadata.name", name: "Name", operator: "*=" }, - { - fieldPath: "status.phase", name: "Status", - items: [ - { inputValue: "Succeeded", values: ["Succeeded"], operator: '=' }, - { inputValue: "Failed", values: ["Failed"], operator: '=' }, - { inputValue: 'Provisioning', values: ["ImportInProgress", "CloneInProgress", "CloneFromSnapshotSourceInProgress", "SmartClonePVCInProgress", "CSICloneInProgress", "ExpansionInProgress", "NamespaceTransferInProgress", "PrepClaimInProgress", "RebindInProgress"], operator: '~=' }, - ] - }, - { - fieldPath: `metadata.labels.${replaceDots("vink.kubevm.io/datavolume.type")}`, name: "Type", - items: [ - { inputValue: "Root", values: ["Root"], operator: '=' }, - { inputValue: "Data", values: ["Data"], operator: '=' }, + const actionItemsFunc = (dv: DataVolume, notification: NotificationInstance) => { + const ns = { namespace: dv.metadata!.namespace, name: dv.metadata!.name } + const items: MenuProps['items'] = [ + { + key: 'delete', + danger: true, + onClick: () => { + Modal.confirm({ + title: "Confirm deletion of data disk", + content: `Are you sure you want to delete the data disk "${namespaceNameKey(ns)}"? This action cannot be undone.`, + okText: 'Delete', + okType: 'danger', + cancelText: 'Cancel', + onOk: async () => { + await deleteDataVolume(ns, undefined, notification) + } + }) + }, + label: "删除" + } ] - }, - // { - // fieldPath: `metadata.annotations.${replaceDots("vink.kubevm.io/virtualmachine.binding")}`, name: "Resource Usage", - // items: [ - // { inputValue: "In Use", values: [""], operator: '!=' }, - // { inputValue: "Idle", values: [""], operator: '=' }, - // ] - // }, - { fieldPath: `metadata.annotations.${replaceDots("vink.kubevm.io/virtualmachine.binding")}`, name: "Owner", operator: "*=" } -] + return items + } -const columnsFunc = (notification: NotificationInstance) => { - const columns: ProColumns[] = [ + const columns: ProColumns[] = [ { key: 'name', title: '名称', fixed: 'left', ellipsis: true, - render: (_, dv) => {dv.metadata.name} + render: (_, dv) => {dv.metadata!.name} }, { key: 'status', title: '状态', ellipsis: true, - render: (_, dv) => { - const displayStatus = parseFloat(dv.status.progress) === 100 ? dataVolumeStatusMap[dv.status.phase].text : dv.status.progress - return - } + render: (_, dv) => }, - // { - // key: 'binding', - // title: '资源占用', - // ellipsis: true, - // render: (_, dv) => { - // const binding = dv.metadata.annotations[annotations.VinkDatavolumeOwner.name] - // if (!binding) { - // return "空闲" - // } - // const parse = JSON.parse(binding) - // return parse && parse.length > 0 ? "使用中" : "空闲" - // } - // }, { key: 'owner', title: 'Owner', ellipsis: true, render: (_, dv) => { - const owners = dv.metadata.annotations[annotations.VinkDatavolumeOwner.name] + const owners = dv.metadata!.annotations?.[annotations.VinkDatavolumeOwner.name] if (!owners) { return } const parse: string[] = JSON.parse(owners) + if (parse.length === 0) { + return + } const content = ( @@ -187,31 +176,26 @@ const columnsFunc = (notification: NotificationInstance) => { title: '容量', key: 'capacity', ellipsis: true, - render: (_, dv) => { - const [value, uint] = formatMemory(dv.spec.pvc.resources.requests.storage) - return `${value} ${uint}` - } + render: (_, dv) => dv.spec?.pvc?.resources?.requests?.storage }, { title: '存储类', key: 'storageClassName', ellipsis: true, - render: (_, dv) => dv.spec.pvc.storageClassName + render: (_, dv) => dv.spec.pvc?.storageClassName }, { title: '访问模式', key: 'accessModes', ellipsis: true, - render: (_, dv) => dv.spec.pvc.accessModes[0] + render: (_, dv) => dv.spec.pvc?.accessModes?.[0] }, { key: 'created', title: '创建时间', width: 160, ellipsis: true, - render: (_, dv) => { - return formatTimestamp(dv.metadata.creationTimestamp) - } + render: (_, dv) => formatTimestamp(dv.metadata!.creationTimestamp) }, { key: 'action', @@ -229,48 +213,29 @@ const columnsFunc = (notification: NotificationInstance) => { } } ] - return columns -} - -const actionItemsFunc = (vm: any, notification: NotificationInstance) => { - const namespace = vm.metadata.namespace - const name = vm.metadata.name - const items: MenuProps['items'] = [ - { - key: 'edit', - label: "编辑" - }, - { - key: 'bindlabel', - label: '绑定标签' - }, - { - key: 'divider-3', - type: 'divider' - }, - { - key: 'delete', - danger: true, - onClick: () => { - Modal.confirm({ - title: "Delete system image?", - content: `You are about to delete the disk "${namespace}/${name}". Please confirm.`, - okText: 'Confirm delete', - okType: 'danger', - cancelText: 'Cancel', - okButtonProps: { disabled: false }, - onOk: async () => { - try { - await clients.deleteResource(ResourceType.DATA_VOLUME, { namespace, name }) - } catch (err) { - notification.error({ message: getResourceName(ResourceType.DATA_VOLUME), description: getErrorMessage(err) }) - } - } - }) - }, - label: "删除" - } - ] - return items + return ( + setSelectedRows(rows)} + defaultFieldSelectors={defaultFieldSelectors} + key="disk-list-table-columns" + columns={columns} + dataSource={resources} + tableAlertOptionRender={() => { + return ( + + handleDeleteDataDisks()}>批量删除 + + ) + }} + toolbar={{ + actions: [ + + ] + }} + /> + ) } diff --git a/src/pages/storage/image/create/index.tsx b/src/pages/storage/image/create/index.tsx index 616852a..686b743 100644 --- a/src/pages/storage/image/create/index.tsx +++ b/src/pages/storage/image/create/index.tsx @@ -3,26 +3,143 @@ import { App, InputNumber, Space, AutoComplete } from 'antd' import { useEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import { useNamespace } from '@/common/context' -import { classNames, getErrorMessage } from '@/utils/utils' -import { clients, getResourceName } from '@/clients/clients' -import { ResourceType } from '@/clients/ts/types/types' +import { classNames } from '@/utils/utils' import { newSystemImage } from '../../datavolume' +import { createDataVolume } from '@/clients/data-volume' import type { ProFormInstance } from '@ant-design/pro-components' import OperatingSystem from '@/components/operating-system' import formStyles from "@/common/styles/form.module.less" import styles from "./styles/index.module.less" +interface formValues { + namespace: string + name: string + description: string + operatingSystem: string[] + imageSource: string + imageCapacity: number +} + +const imageSources: any = { + "ubuntu": { + "22.04": { + "baseUrl": "https://cloud-images.ubuntu.com/jammy/current", + "tags": [ + "jammy-server-cloudimg-amd64-disk-kvm.img", + "jammy-server-cloudimg-amd64.img" + ] + } + }, + "centos": { + "10": { + "baseUrl": "https://cloud.centos.org/centos/10-stream/images", + "tags": [ + "CentOS-Stream-GenericCloud-10-latest.x86_64.qcow2" + ] + } + }, + "debian": { + "12": { + "baseUrl": "https://cdimage.debian.org/cdimage/cloud/bookworm/latest", + "tags": [ + "debian-12-generic-amd64.qcow2", + "debian-12-genericcloud-amd64.qcow2", + "debian-12-nocloud-amd64.qcow2" + ] + } + } +} + +const options = [ + { + value: 'ubuntu', + label: , + children: [ + { + value: '20.04', + label: '20.04', + }, + { + value: '22.04', + label: '22.04', + }, + ], + }, + { + value: 'centos', + label: , + children: [ + { + value: '7', + label: '7', + }, + { + value: '8', + label: '8', + }, + { + value: '9', + label: '9', + }, + { + value: '10', + label: '10', + } + ], + }, + { + value: 'debian', + label: , + children: [ + { + value: '9', + label: '9', + }, + { + value: '10', + label: '10', + }, + { + value: '11', + label: '11', + }, + { + value: '12', + label: '12', + } + ], + }, + { + value: 'linux', + label: , + }, + { + value: 'windows', + label: , + children: [ + { + value: '10', + label: '10', + }, + { + value: '11', + label: '11', + } + ] + } +] + export default () => { const { notification } = App.useApp() const { namespace } = useNamespace() - const formRef = useRef() + const formRef = useRef>() const navigate = useNavigate() useEffect(() => { - formRef.current?.setFieldValue("namespace", namespace) + formRef.current?.setFieldsValue({ namespace: namespace }) if (namespace) { formRef.current?.validateFields(["namespace"]) } @@ -32,6 +149,9 @@ export default () => { const handleClick = () => { const fields = formRef.current?.getFieldsValue() + if (!fields) { + return + } const family = fields.operatingSystem?.[0] const version = fields.operatingSystem?.[1] @@ -49,35 +169,17 @@ export default () => { const handleSubmit = async () => { if (!formRef.current) { - return + throw new Error("formRef is not initialized") } - try { - await formRef.current.validateFields() + await formRef.current.validateFields() + const fields = formRef.current.getFieldsValue() + const ns = { namespace: fields.namespace, name: fields.name } + const instance = newSystemImage(ns, fields.imageSource, fields.imageCapacity, fields.operatingSystem?.[0], fields.operatingSystem?.[1]) - const fields = formRef.current.getFieldsValue() + await createDataVolume(instance, undefined, undefined, notification) - const namespaceName = { namespace: fields.namespace, name: fields.name } - - const instance = newSystemImage( - namespaceName, - fields.imageSource, - fields.imageCapacity, - fields.operatingSystem?.[0], - fields.operatingSystem?.[1] - ) - - await clients.createResource(ResourceType.DATA_VOLUME, instance) - navigate('/storage/images') - } catch (err: any) { - const errorMessage = err.errorFields?.map((field: any, idx: number) => `${idx + 1}. ${field.errors}`).join('
') || getErrorMessage(err) - notification.error({ - message: getResourceName(ResourceType.DATA_VOLUME), - description: ( -
- ) - }) - } + navigate('/storage/images') } return ( @@ -151,8 +253,10 @@ export default () => { rules={[{ required: true, validator() { - const imageCapacity = formRef.current?.getFieldValue("imageCapacity") - if (imageCapacity < 10) { + const fields = formRef.current?.getFieldsValue() + const imageCapacity = fields?.imageCapacity + + if (!imageCapacity || imageCapacity < 10) { return Promise.reject() } return Promise.resolve() @@ -193,112 +297,3 @@ export default () => { ) } - -const imageSources: any = { - "ubuntu": { - "22.04": { - "baseUrl": "https://cloud-images.ubuntu.com/jammy/current", - "tags": [ - "jammy-server-cloudimg-amd64-disk-kvm.img", - "jammy-server-cloudimg-amd64.img" - ] - } - }, - "centos": { - "10": { - "baseUrl": "https://cloud.centos.org/centos/10-stream/images", - "tags": [ - "CentOS-Stream-GenericCloud-10-latest.x86_64.qcow2" - ] - } - }, - "debian": { - "12": { - "baseUrl": "https://cdimage.debian.org/cdimage/cloud/bookworm/latest", - "tags": [ - "debian-12-generic-amd64.qcow2", - "debian-12-genericcloud-amd64.qcow2", - "debian-12-nocloud-amd64.qcow2" - ] - } - } -} - -const options = [ - { - value: 'ubuntu', - label: , - children: [ - { - value: '20.04', - label: '20.04', - }, - { - value: '22.04', - label: '22.04', - }, - ], - }, - { - value: 'centos', - label: , - children: [ - { - value: '7', - label: '7', - }, - { - value: '8', - label: '8', - }, - { - value: '9', - label: '9', - }, - { - value: '10', - label: '10', - } - ], - }, - { - value: 'debian', - label: , - children: [ - { - value: '9', - label: '9', - }, - { - value: '10', - label: '10', - }, - { - value: '11', - label: '11', - }, - { - value: '12', - label: '12', - } - ], - }, - { - value: 'linux', - label: , - }, - { - value: 'windows', - label: , - children: [ - { - value: '10', - label: '10', - }, - { - value: '11', - label: '11', - } - ] - } -] diff --git a/src/pages/storage/image/detail/index.tsx b/src/pages/storage/image/detail/index.tsx index 16cf625..a9d2c13 100644 --- a/src/pages/storage/image/detail/index.tsx +++ b/src/pages/storage/image/detail/index.tsx @@ -1,9 +1,46 @@ +import { App } from "antd" import { DetailYaml } from "@/components/detail-yaml" -import { ResourceType } from "@/clients/ts/types/types" -import { useWatchResourceInNamespaceName } from "@/hooks/use-resource" +import { useNamespaceFromURL } from "@/hooks/use-query-params-from-url" +import { useEffect, useRef, useState } from "react" +import { useNavigate } from "react-router" +import { DataVolume, updateDataVolume, watchDataVolume } from "@/clients/data-volume" +import useUnmount from "@/hooks/use-unmount" export default () => { - const { resource, loading } = useWatchResourceInNamespaceName(ResourceType.DATA_VOLUME) + const { notification } = App.useApp() - return + const navigate = useNavigate() + + const ns = useNamespaceFromURL() + + const [loading, setLoading] = useState(true) + + const [dataVolume, setDataVolume] = useState() + + const abortCtrl = useRef() + + useEffect(() => { + abortCtrl.current?.abort() + abortCtrl.current = new AbortController() + watchDataVolume(ns, setDataVolume, setLoading, abortCtrl.current.signal, notification) + }, [ns]) + + useUnmount(() => { + abortCtrl.current?.abort() + }) + + const handleSave = async (data: any) => { + await updateDataVolume(data as DataVolume, undefined, undefined, notification) + } + + const handleCancel = () => { + navigate('/storage/images') + } + + return } diff --git a/src/pages/storage/image/list/index.tsx b/src/pages/storage/image/list/index.tsx index c56e61a..0be87d9 100644 --- a/src/pages/storage/image/list/index.tsx +++ b/src/pages/storage/image/list/index.tsx @@ -1,26 +1,47 @@ import { PlusOutlined } from '@ant-design/icons' -import { App, Badge, Button, Dropdown, MenuProps, Modal, Space } from 'antd' -import { useEffect, useState } from 'react' -import { formatMemory, namespaceNameKey } from '@/utils/k8s' +import { App, Button, Dropdown, MenuProps, Modal, Space } from 'antd' +import { useEffect, useRef, useState } from 'react' +import { namespaceNameKey } from '@/utils/k8s' import { Link, NavLink } from 'react-router-dom' import { instances as labels } from "@/clients/ts/label/labels.gen" -import { dataSource, filterNullish, formatTimestamp, generateMessage, getErrorMessage } from '@/utils/utils' +import { filterNullish, formatTimestamp } from '@/utils/utils' import { useNamespace } from '@/common/context' -import { clients, getResourceName } from '@/clients/clients' -import { FieldSelector, ResourceType } from '@/clients/ts/types/types' -import { useWatchResources } from '@/hooks/use-resource' -import { NotificationInstance } from 'antd/lib/notification/interface' +import { FieldSelector } from '@/clients/ts/types/types' import { getNamespaceFieldSelector, replaceDots } from '@/utils/search' import { EllipsisOutlined } from '@ant-design/icons' -import { dataVolumeStatusMap } from '@/utils/resource-status' import { WatchOptions } from '@/clients/ts/management/resource/v1alpha1/watch' import { CustomTable, SearchItem } from '@/components/custom-table' +import { DataVolume, deleteDataVolume, deleteDataVolumes, watchDataVolumes } from '@/clients/data-volume' import type { ProColumns } from '@ant-design/pro-components' import OperatingSystem from '@/components/operating-system' import commonStyles from '@/common/styles/common.module.less' +import useUnmount from '@/hooks/use-unmount' +import DataVolumeStatus from '@/components/datavolume-status' const dvImageTypeSelector = { fieldPath: `metadata.labels.${replaceDots(labels.VinkDatavolumeType.name)}`, operator: "=", values: ["image"] } +const searchItems: SearchItem[] = [ + { fieldPath: "metadata.name", name: "Name", operator: "*=" }, + { + fieldPath: "status.phase", name: "Status", + items: [ + { inputValue: "Succeeded", values: ["Succeeded"], operator: '=' }, + { inputValue: "Failed", values: ["Failed"], operator: '=' }, + { inputValue: 'Provisioning', values: ["ImportInProgress", "CloneInProgress", "CloneFromSnapshotSourceInProgress", "SmartClonePVCInProgress", "CSICloneInProgress", "ExpansionInProgress", "NamespaceTransferInProgress", "PrepClaimInProgress", "RebindInProgress"], operator: '~=' }, + ] + }, + { + fieldPath: `metadata.labels.${replaceDots("vink.kubevm.io/virtualmachine.os")}`, name: "OS", + items: [ + { inputValue: "Ubuntu", values: ["Ubuntu"], operator: '=' }, + { inputValue: "CentOS", values: ["CentOS"], operator: '=' }, + { inputValue: "Debian", values: ["Debian"], operator: '=' }, + { inputValue: "Linux", values: ["Linux", "Ubuntu", "CentOS", "Debian"], operator: '~=' }, + { inputValue: "Windows", values: ["Windows"], operator: '=' }, + ] + } +] + export default () => { const { notification } = App.useApp() @@ -28,8 +49,6 @@ export default () => { const [selectedRows, setSelectedRows] = useState([]) - const columns = columnsFunc(notification) - const [defaultFieldSelectors, setDefaultFieldSelectors] = useState(filterNullish([getNamespaceFieldSelector(namespace), dvImageTypeSelector])) const [opts, setOpts] = useState(WatchOptions.create({ fieldSelectorGroup: { operator: "&&", fieldSelectors: defaultFieldSelectors } @@ -39,93 +58,72 @@ export default () => { setDefaultFieldSelectors(filterNullish([getNamespaceFieldSelector(namespace), dvImageTypeSelector])) }, [namespace]) - const { resources, loading } = useWatchResources(ResourceType.DATA_VOLUME, opts) + const [loading, setLoading] = useState(true) + + const [resources, setResources] = useState() - const handleBatchDeleteImage = async () => { - const resourceName = getResourceName(ResourceType.DATA_VOLUME) + const abortCtrl = useRef() + + useEffect(() => { + abortCtrl.current?.abort() + abortCtrl.current = new AbortController() + watchDataVolumes(setResources, setLoading, abortCtrl.current.signal, opts, notification) + }, [opts]) + + useUnmount(() => { + abortCtrl.current?.abort() + }) + + const handleDeleteImages = async () => { Modal.confirm({ - title: `Batch delete ${resourceName}?`, - content: generateMessage(selectedRows, `You are about to delete the following ${resourceName}: "{names}", please confirm.`, `You are about to delete the following ${resourceName}: "{names}" and {count} others, please confirm.`), - okText: 'Confirm Delete', + title: `Confirm batch deletion of operating system images`, + content: `Are you sure you want to delete the selected operating system images? This action cannot be undone.`, + okText: 'Delete', okType: 'danger', cancelText: 'Cancel', - okButtonProps: { disabled: false }, onOk: async () => { - clients.batchDeleteResources(ResourceType.DATA_VOLUME, selectedRows).catch(err => { - notification.error({ - message: `Batch delete of ${resourceName} failed`, - description: getErrorMessage(err) - }) - }) + await deleteDataVolumes(selectedRows, undefined, notification) } }) } - return ( - setSelectedRows(rows)} - defaultFieldSelectors={defaultFieldSelectors} - storageKey="image-list-table-columns" - columns={columns} - dataSource={dataSource(resources)} - tableAlertOptionRender={() => { - return ( - - handleBatchDeleteImage()}>批量删除 - - ) - }} - toolbar={{ - actions: [ - - ] - }} - /> - ) -} - -const searchItems: SearchItem[] = [ - { fieldPath: "metadata.name", name: "Name", operator: "*=" }, - { - fieldPath: "status.phase", name: "Status", - items: [ - { inputValue: "Succeeded", values: ["Succeeded"], operator: '=' }, - { inputValue: "Failed", values: ["Failed"], operator: '=' }, - { inputValue: 'Provisioning', values: ["ImportInProgress", "CloneInProgress", "CloneFromSnapshotSourceInProgress", "SmartClonePVCInProgress", "CSICloneInProgress", "ExpansionInProgress", "NamespaceTransferInProgress", "PrepClaimInProgress", "RebindInProgress"], operator: '~=' }, - ] - }, - { - fieldPath: `metadata.labels.${replaceDots("vink.kubevm.io/virtualmachine.os")}`, name: "OS", - items: [ - { inputValue: "Ubuntu", values: ["Ubuntu"], operator: '=' }, - { inputValue: "CentOS", values: ["CentOS"], operator: '=' }, - { inputValue: "Debian", values: ["Debian"], operator: '=' }, - { inputValue: "Linux", values: ["Linux", "Ubuntu", "CentOS", "Debian"], operator: '~=' }, - { inputValue: "Windows", values: ["Windows"], operator: '=' }, + const actionItemsFunc = (dv: DataVolume) => { + const ns = { namespace: dv.metadata!.namespace, name: dv.metadata!.name } + const items: MenuProps['items'] = [ + { + key: 'delete', + danger: true, + onClick: () => { + Modal.confirm({ + title: "Confirm deletion of operating system image", + content: `Are you sure you want to delete the operating system image "${namespaceNameKey(ns)}"? This action cannot be undone.`, + okText: 'Delete', + okType: 'danger', + cancelText: 'Cancel', + onOk: async () => { + await deleteDataVolume(ns, undefined, notification) + } + }) + }, + label: "删除" + } ] - }, -] + return items + } -const columnsFunc = (notification: NotificationInstance) => { - const columns: ProColumns[] = [ + const columns: ProColumns[] = [ { key: 'name', title: '名称', fixed: 'left', ellipsis: true, - render: (_, dv) => {dv.metadata.name} + render: (_, dv) => {dv.metadata!.name} }, { key: 'status', title: '状态', ellipsis: true, - render: (_, dv) => { - const displayStatus = parseFloat(dv.status.progress) === 100 ? dataVolumeStatusMap[dv.status.phase].text : dv.status.progress - return - } + render: (_, dv) => }, { key: 'operatingSystem', @@ -139,19 +137,14 @@ const columnsFunc = (notification: NotificationInstance) => { title: '容量', key: 'capacity', ellipsis: true, - render: (_, dv) => { - const [value, uint] = formatMemory(dv.spec.pvc.resources.requests.storage) - return `${value} ${uint}` - } + render: (_, dv) => dv.spec?.pvc?.resources?.requests?.storage }, { key: 'created', title: '创建时间', width: 160, ellipsis: true, - render: (_, dv) => { - return formatTimestamp(dv.metadata.creationTimestamp) - } + render: (_, dv) => formatTimestamp(dv.metadata!.creationTimestamp) }, { key: 'action', @@ -160,7 +153,7 @@ const columnsFunc = (notification: NotificationInstance) => { width: 90, align: 'center', render: (_, dv) => { - const items = actionItemsFunc(dv, notification) + const items = actionItemsFunc(dv) return ( @@ -169,47 +162,29 @@ const columnsFunc = (notification: NotificationInstance) => { } } ] - return columns -} - -const actionItemsFunc = (vm: any, notification: NotificationInstance) => { - const namespaceName = { namespace: vm.metadata.namespace, name: vm.metadata.name } - const items: MenuProps['items'] = [ - { - key: 'edit', - label: "编辑" - }, - { - key: 'bindlabel', - label: '绑定标签' - }, - { - key: 'divider-3', - type: 'divider' - }, - { - key: 'delete', - danger: true, - onClick: () => { - Modal.confirm({ - title: "Delete system image?", - content: `You are about to delete the system image "${namespaceNameKey(namespaceName)}". Please confirm.`, - okText: 'Confirm delete', - okType: 'danger', - cancelText: 'Cancel', - okButtonProps: { disabled: false }, - onOk: async () => { - try { - await clients.deleteResource(ResourceType.DATA_VOLUME, namespaceName) - } catch (err: any) { - notification.error({ message: getResourceName(ResourceType.DATA_VOLUME), description: getErrorMessage(err) }) - } - } - }) - }, - label: "删除" - } - ] - return items + return ( + setSelectedRows(rows)} + defaultFieldSelectors={defaultFieldSelectors} + key="image-list-table-columns" + columns={columns} + dataSource={resources} + tableAlertOptionRender={() => { + return ( + + handleDeleteImages()}>批量删除 + + ) + }} + toolbar={{ + actions: [ + + ] + }} + /> + ) } diff --git a/src/utils/k8s.ts b/src/utils/k8s.ts index 921d679..37c3170 100644 --- a/src/utils/k8s.ts +++ b/src/utils/k8s.ts @@ -8,6 +8,14 @@ export const formatOSFamily = (family: string): string => { return result } +export const parseMemoryValue = (value: string): [number, string] | undefined => { + const match = value.match(/^(\d+)(\w+)$/) + if (match) { + return [parseInt(match[1], 10), match[2]] + } + return undefined +} + export const formatMemory = (value: string): [string, string] => { const match = value.match(/^(\d+)(\w+)$/) if (match) { diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 2e3cf65..995077b 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -3,6 +3,7 @@ import { ColumnsState } from "@ant-design/pro-components" import { formatMemory, namespaceNameKey } from "./k8s" import { NamespaceName, ResourceType } from '@/clients/ts/types/types' import { VirtualMachine } from "@/clients/virtual-machine" +import { Multus } from "@/clients/multus" /** * Combines multiple class names into a single string. @@ -73,6 +74,15 @@ export const dataSource = (data: Map): any[] | undefined => { }) } +export const resourceSort = (items: any): any[] => { + if (!items || items.length == 0) { + return items + } + return items.sort((a: any, b: any) => { + return new Date(b.metadata.creationTimestamp).getTime() - new Date(a.metadata.creationTimestamp).getTime() + }) +} + export const generateMessage = (items: any[] | NamespaceName[], successMessage: string, multipleMessage: string) => { const namespaceNames = items.map(item => namespaceNameKey(item)) @@ -143,22 +153,26 @@ export const generateKubeovnNetworkAnnon = (multus: NamespaceName | any, name: s return `${prefix}.ovn.kubernetes.io/${name}` } -export const getProvider = (multusCR: any) => { +export const getProvider = (multus: Multus): string => { + if (!multus.spec?.config) { + return "" + } + const kubeovn = "kube-ovn" - const config = JSON.parse(multusCR.spec.config) + const config = JSON.parse(multus.spec.config) if (config.type == kubeovn) { - return config.provider + return config.provider as string } if (!config.plugins) { - return + return "" } for (let i = 0; i < config.plugins.length; i++) { const plugin = config.plugins[i] if (plugin.type == kubeovn) { - return plugin.provider + return plugin.provider as string } } - return + return "" } export const getErrorMessage = (err: unknown): string => { @@ -195,3 +209,20 @@ export const arraysAreEqual = (a: any[], b: any[]) => { export const filterNullish = (array: (T | null | undefined)[]): T[] => { return array.filter((item): item is T => item != null) } + +// export const addNewOption = ( +// inputValue: number | undefined, +// options: { label: string; value: number }[], +// setOptions: React.Dispatch>, +// fieldName: "cpu" | "memory" +// ) => { +// if (!inputValue || !formRef.current) { +// return +// } +// const unit = fieldName === "cpu" ? "Core" : "Gi" +// if (!options.some(opt => opt.value === inputValue)) { +// const newOption = { label: `${inputValue} ${unit}`, value: inputValue } +// setOptions([...options, newOption]) +// formRef.current.setFieldsValue({ [fieldName]: inputValue }) +// } +// } \ No newline at end of file