diff --git a/plugins/topology/README.md b/plugins/topology/README.md index 108e8a5e33..99e8cbe3e2 100644 --- a/plugins/topology/README.md +++ b/plugins/topology/README.md @@ -1,6 +1,6 @@ # Topology plugin for Backstage -The Topology plugin enables you to visualize the workloads such as Deployment, Job, Daemonset, Statefulset, CronJob, and Pods powering any service on the Kubernetes cluster. +The Topology plugin enables you to visualize the workloads such as Deployment, Job, Daemonset, Statefulset, CronJob, Pods and Virtual Machines powering any service on the Kubernetes cluster. ## For administrators @@ -110,6 +110,38 @@ The following permission must be granted to the [`ClusterRole`](https://backstag plural: 'taskruns' ``` +##### To view the Virtual Machines + +- Ensure that read access is granted to the VirtualMachines resource in the [`ClusterRole`](https://backstage.io/docs/features/kubernetes/configuration#role-based-access-control). You can use the following code to do so: + + ```yaml + ... + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + name: backstage-read-only + rules: + ... + - apiGroups: + - kubevirt.io + resources: + - virtualmachines + verbs: + - get + - list + ``` + +- The following code must be added to the `kubernetes.customResources` property in the [`app-config.yaml`](https://backstage.io/docs/features/kubernetes/configuration#configuring-kubernetes-clusters) file to view the VirtualMachine nodes on the topology plugin: + + ```yaml + kubernetes: + ... + customResources: + - group: 'kubevirt.io' + apiVersion: 'v1' + plural: 'virtualmachines' + ``` + ##### To enable the Source Code Editor - Ensure that read access is granted to the `CheClusters` resource in the [`ClusterRole`](https://backstage.io/docs/features/kubernetes/configuration#role-based-access-control) as shown in the following example code: diff --git a/plugins/topology/src/__fixtures__/1-deployments.ts b/plugins/topology/src/__fixtures__/1-deployments.ts index 89508b363d..6c55451b9d 100644 --- a/plugins/topology/src/__fixtures__/1-deployments.ts +++ b/plugins/topology/src/__fixtures__/1-deployments.ts @@ -54,6 +54,408 @@ export const customResourceRoute = { }, }; export const mockKubernetesResponse = { + virtualMachines: [ + { + apiVersion: 'kubevirt.io/v1', + kind: 'VirtualMachine', + metadata: { + annotations: { + 'kubemacpool.io/transaction-timestamp': + '2024-08-07T10:46:35.842565627Z', + 'kubevirt.io/latest-observed-api-version': 'v1', + }, + creationTimestamp: '2024-08-07T10:46:02Z', + finalizers: ['kubevirt.io/virtualMachineControllerFinalize'], + generation: 1, + labels: { + app: 'fedora-turquoise-rooster-85', + 'backstage.io/kubernetes-id': 'nationalparks-py', + 'kubevirt.io/dynamic-credentials-support': 'true', + 'vm.kubevirt.io/template': 'fedora-server-small', + 'vm.kubevirt.io/template.namespace': 'openshift', + 'vm.kubevirt.io/template.revision': '1', + 'vm.kubevirt.io/template.version': 'v0.29.1', + }, + name: 'fedora-turquoise-rooster-85', + namespace: 'mitesh', + resourceVersion: '307175', + uid: 'd6a524d8-a41e-42b3-8c61-3e99c7e76234', + }, + spec: { + dataVolumeTemplates: [ + { + apiVersion: 'cdi.kubevirt.io/v1beta1', + kind: 'DataVolume', + metadata: { + creationTimestamp: null, + name: 'fedora-turquoise-rooster-85', + }, + spec: { + sourceRef: { + kind: 'DataSource', + name: 'fedora', + namespace: 'openshift-virtualization-os-images', + }, + storage: { + resources: { + requests: { + storage: '30Gi', + }, + }, + }, + }, + }, + ], + running: true, + template: { + metadata: { + annotations: { + 'vm.kubevirt.io/flavor': 'small', + 'vm.kubevirt.io/os': 'fedora', + 'vm.kubevirt.io/workload': 'server', + }, + creationTimestamp: null, + labels: { + 'kubevirt.io/domain': 'fedora-turquoise-rooster-85', + 'kubevirt.io/size': 'small', + 'network.kubevirt.io/headlessService': 'headless', + }, + }, + spec: { + architecture: 'amd64', + domain: { + cpu: { + cores: 1, + sockets: 1, + threads: 1, + }, + devices: { + disks: [ + { + disk: { + bus: 'virtio', + }, + name: 'rootdisk', + }, + { + disk: { + bus: 'virtio', + }, + name: 'cloudinitdisk', + }, + ], + interfaces: [ + { + macAddress: '02:05:b4:00:00:01', + masquerade: {}, + model: 'virtio', + name: 'default', + }, + ], + rng: {}, + }, + features: { + acpi: {}, + smm: { + enabled: true, + }, + }, + firmware: { + bootloader: { + efi: {}, + }, + }, + machine: { + type: 'pc-q35-rhel9.4.0', + }, + memory: { + guest: '2Gi', + }, + resources: {}, + }, + networks: [ + { + name: 'default', + pod: {}, + }, + ], + terminationGracePeriodSeconds: 180, + volumes: [ + { + dataVolume: { + name: 'fedora-turquoise-rooster-85', + }, + name: 'rootdisk', + }, + { + cloudInitNoCloud: { + userData: + '#cloud-config\nuser: fedora\npassword: kfi7-yxx7-ub8h\nchpasswd: { expire: False }', + }, + name: 'cloudinitdisk', + }, + ], + }, + }, + }, + status: { + conditions: [ + { + lastProbeTime: '2024-08-07T10:46:03Z', + lastTransitionTime: '2024-08-07T10:46:03Z', + message: 'Guest VM is not reported as running', + reason: 'GuestNotRunning', + status: 'False', + type: 'Ready', + }, + { + lastProbeTime: null, + lastTransitionTime: null, + message: "Not all of the VMI's DVs are ready", + reason: 'NotAllDVsReady', + status: 'False', + type: 'DataVolumesReady', + }, + { + lastProbeTime: null, + lastTransitionTime: '2024-08-07T10:46:03Z', + message: + '0/6 nodes are available: 3 Insufficient devices.kubevirt.io/kvm, 3 node(s) had untolerated taint {node-role.kubernetes.io/master: }. preemption: 0/6 nodes are available: 3 No preemption victims found for incoming pod, 3 Preemption is not helpful for scheduling.', + reason: 'Unschedulable', + status: 'False', + type: 'PodScheduled', + }, + ], + created: true, + desiredGeneration: 1, + observedGeneration: 1, + printableStatus: 'ErrorUnschedulable', + runStrategy: 'Always', + volumeSnapshotStatuses: [ + { + enabled: true, + name: 'rootdisk', + }, + { + enabled: false, + name: 'cloudinitdisk', + reason: + 'Snapshot is not supported for this volumeSource type [cloudinitdisk]', + }, + ], + }, + }, + { + apiVersion: 'kubevirt.io/v1', + kind: 'VirtualMachine', + metadata: { + annotations: { + 'kubemacpool.io/transaction-timestamp': + '2024-08-07T10:47:43.467479762Z', + 'kubevirt.io/storage-observed-api-version': 'v1', + }, + creationTimestamp: '2024-08-07T10:47:23Z', + finalizers: ['kubevirt.io/virtualMachineControllerFinalize'], + generation: 1, + labels: { + app: 'win2k22-purple-aphid-31', + 'backstage.io/kubernetes-id': 'nationalparks-py', + 'vm.kubevirt.io/template': 'windows2k22-server-medium', + 'vm.kubevirt.io/template.namespace': 'openshift', + 'vm.kubevirt.io/template.revision': '1', + 'vm.kubevirt.io/template.version': 'v0.29.1', + }, + name: 'win2k22-purple-aphid-31', + namespace: 'mitesh', + resourceVersion: '308786', + uid: '1957cf88-52c0-4e61-ad1d-211eaa72e56b', + }, + spec: { + dataVolumeTemplates: [ + { + apiVersion: 'cdi.kubevirt.io/v1beta1', + kind: 'DataVolume', + metadata: { + creationTimestamp: null, + name: 'win2k22-purple-aphid-31', + }, + spec: { + sourceRef: { + kind: 'DataSource', + name: 'win2k22', + namespace: 'openshift-virtualization-os-images', + }, + storage: { + resources: { + requests: { + storage: '60Gi', + }, + }, + }, + }, + }, + ], + running: true, + template: { + metadata: { + annotations: { + 'vm.kubevirt.io/flavor': 'medium', + 'vm.kubevirt.io/os': 'windows2k22', + 'vm.kubevirt.io/workload': 'server', + }, + creationTimestamp: null, + labels: { + 'kubevirt.io/domain': 'win2k22-purple-aphid-31', + 'kubevirt.io/size': 'medium', + 'network.kubevirt.io/headlessService': 'headless', + }, + }, + spec: { + architecture: 'amd64', + domain: { + clock: { + timer: { + hpet: { + present: false, + }, + hyperv: {}, + pit: { + tickPolicy: 'delay', + }, + rtc: { + tickPolicy: 'catchup', + }, + }, + utc: {}, + }, + cpu: { + cores: 1, + sockets: 1, + threads: 1, + }, + devices: { + disks: [ + { + disk: { + bus: 'sata', + }, + name: 'rootdisk', + }, + { + cdrom: { + bus: 'sata', + }, + name: 'windows-drivers-disk', + }, + ], + inputs: [ + { + bus: 'usb', + name: 'tablet', + type: 'tablet', + }, + ], + interfaces: [ + { + macAddress: '02:05:b4:00:00:02', + masquerade: {}, + model: 'e1000e', + name: 'default', + }, + ], + tpm: {}, + }, + features: { + acpi: {}, + apic: {}, + hyperv: { + frequencies: {}, + ipi: {}, + reenlightenment: {}, + relaxed: {}, + reset: {}, + runtime: {}, + spinlocks: { + spinlocks: 8191, + }, + synic: {}, + synictimer: { + direct: {}, + }, + tlbflush: {}, + vapic: {}, + vpindex: {}, + }, + smm: {}, + }, + firmware: { + bootloader: { + efi: { + secureBoot: true, + }, + }, + }, + machine: { + type: 'pc-q35-rhel9.4.0', + }, + memory: { + guest: '4Gi', + }, + resources: {}, + }, + networks: [ + { + name: 'default', + pod: {}, + }, + ], + terminationGracePeriodSeconds: 3600, + volumes: [ + { + dataVolume: { + name: 'win2k22-purple-aphid-31', + }, + name: 'rootdisk', + }, + { + containerDisk: { + image: + 'registry.redhat.io/container-native-virtualization/virtio-win-rhel9@sha256:584857c3d7cee20877a4ea135fb58fd7721dfc04a23a6c76580eba9facd6e6c0', + }, + name: 'windows-drivers-disk', + }, + ], + }, + }, + }, + status: { + conditions: [ + { + lastProbeTime: '2024-08-07T10:47:24Z', + lastTransitionTime: '2024-08-07T10:47:24Z', + message: 'VMI does not exist', + reason: 'VMINotExists', + status: 'False', + type: 'Ready', + }, + ], + printableStatus: 'Provisioning', + volumeSnapshotStatuses: [ + { + enabled: false, + name: 'rootdisk', + reason: 'PVC not found', + }, + { + enabled: false, + name: 'windows-drivers-disk', + reason: + 'Snapshot is not supported for this volumeSource type [windows-drivers-disk]', + }, + ], + }, + }, + ], pods: [ { kind: 'Pod', @@ -2511,5 +2913,8 @@ export const mockK8sResourcesData = { routes: { data: mockKubernetesResponse.routes, }, + virtualmachines: { + data: mockKubernetesResponse.virtualMachines, + }, }, }; diff --git a/plugins/topology/src/components/Graph/BaseNode.tsx b/plugins/topology/src/components/Graph/BaseNode.tsx index a741aa1427..eb21b70b41 100644 --- a/plugins/topology/src/components/Graph/BaseNode.tsx +++ b/plugins/topology/src/components/Graph/BaseNode.tsx @@ -41,6 +41,7 @@ type BaseNodeProps = { nodeStatus?: NodeStatus; showStatusBackground?: boolean; alertVariant?: NodeStatus; + truncateLength?: number; } & Partial & Partial; diff --git a/plugins/topology/src/components/Graph/TopologyComponentFactory.tsx b/plugins/topology/src/components/Graph/TopologyComponentFactory.tsx index 432654f3c4..ac402e7147 100644 --- a/plugins/topology/src/components/Graph/TopologyComponentFactory.tsx +++ b/plugins/topology/src/components/Graph/TopologyComponentFactory.tsx @@ -8,11 +8,13 @@ import { import { TYPE_APPLICATION_GROUP, TYPE_CONNECTS_TO, + TYPE_VM, TYPE_WORKLOAD, } from '../../const'; import DefaultGraph from './DefaultGraph'; import EdgeConnect from './EdgeConnect'; import GroupNode from './GroupNode'; +import VMNode from './VMNode'; import WorkloadNode from './WorkloadNode'; const TopologyComponentFactory = (kind: ModelKind, type: string) => { @@ -20,6 +22,8 @@ const TopologyComponentFactory = (kind: ModelKind, type: string) => { return withPanZoom()(withSelection()(DefaultGraph)); } switch (type) { + case TYPE_VM: + return withDragNode()(withSelection()(VMNode)); case TYPE_WORKLOAD: return withDragNode()(withSelection()(WorkloadNode)); case TYPE_APPLICATION_GROUP: diff --git a/plugins/topology/src/components/Graph/VMNode.tsx b/plugins/topology/src/components/Graph/VMNode.tsx new file mode 100644 index 0000000000..7ad2661f2f --- /dev/null +++ b/plugins/topology/src/components/Graph/VMNode.tsx @@ -0,0 +1,88 @@ +import React from 'react'; + +import { makeStyles } from '@material-ui/core'; +import { VirtualMachineIcon } from '@patternfly/react-icons/dist/esm/icons/virtual-machine-icon'; +import { + WithDragNodeProps, + WithSelectionProps, +} from '@patternfly/react-topology'; + +import { RESOURCE_NAME_TRUNCATE_LENGTH } from '../../const'; +import BaseNode from './BaseNode'; + +const VM_STATUS_GAP = 7; +const VM_STATUS_WIDTH = 7; +const VM_STATUS_RADIUS = 7; +type VmNodeProps = { + element: any; + hover?: boolean; + dragging?: boolean; + edgeDragging?: boolean; + highlight?: boolean; + canDrop?: boolean; + dropTarget?: boolean; + children: any; +} & Partial; + +const VmNode = ({ + element, + onSelect, + canDrop, + dropTarget, + children, + ...rest +}: VmNodeProps) => { + const { width, height } = element.getBounds(); + const vmData = element.getData().data; + const { kind, osImage } = vmData; + const iconRadius = Math.min(width, height) * 0.25; + + const onNodeSelect = (e: React.MouseEvent) => { + const params = new URLSearchParams(window.location.search); + params.set('selectedId', element.getId()); + history.replaceState(null, '', `?${params.toString()}`); + if (onSelect) onSelect(e); + }; + const imageProps = { + x: width / 2 - iconRadius, + y: height / 2 - iconRadius, + width: iconRadius * 2, + height: iconRadius * 2, + }; + const imageComponent = osImage ? ( + + ) : ( + + ); + + const useStyles = makeStyles({ + kubevirtbg: { + fill: 'var(--pf-v5-global--BackgroundColor--light-100)', + }, + }); + const classes = useStyles(); + return ( + + + + {imageComponent} + + + ); +}; + +export default VmNode; diff --git a/plugins/topology/src/components/Topology/TopologyComponent.test.tsx b/plugins/topology/src/components/Topology/TopologyComponent.test.tsx index 40d63d2c63..80dfc32e82 100644 --- a/plugins/topology/src/components/Topology/TopologyComponent.test.tsx +++ b/plugins/topology/src/components/Topology/TopologyComponent.test.tsx @@ -5,6 +5,11 @@ import { render } from '@testing-library/react'; import { TopologyComponent } from './TopologyComponent'; +jest.mock('@material-ui/core', () => ({ + ...jest.requireActual('@material-ui/core'), + makeStyles: jest.fn().mockReturnValue(() => ({})), +})); + jest.mock('../../hooks/useK8sObjectsResponse', () => ({ useK8sObjectsResponse: () => ({ watchResourcesData: { diff --git a/plugins/topology/src/components/Topology/TopologyComponent.tsx b/plugins/topology/src/components/Topology/TopologyComponent.tsx index 6a9ffabded..04601e960c 100644 --- a/plugins/topology/src/components/Topology/TopologyComponent.tsx +++ b/plugins/topology/src/components/Topology/TopologyComponent.tsx @@ -44,6 +44,7 @@ export const TopologyComponent = () => { TektonModels.pipelineruns, TektonModels.pipelines, ModelsPlural.checlusters, + ModelsPlural.virtualmachines, ]; const k8sResourcesContextData = useK8sObjectsResponse(watchedResources); diff --git a/plugins/topology/src/components/Topology/TopologyViewWorkloadComponent.tsx b/plugins/topology/src/components/Topology/TopologyViewWorkloadComponent.tsx index 0f27f6907f..b587ef5f3d 100644 --- a/plugins/topology/src/components/Topology/TopologyViewWorkloadComponent.tsx +++ b/plugins/topology/src/components/Topology/TopologyViewWorkloadComponent.tsx @@ -13,7 +13,7 @@ import { VisualizationSurface, } from '@patternfly/react-topology'; -import { TYPE_WORKLOAD } from '../../const'; +import { TYPE_VM, TYPE_WORKLOAD } from '../../const'; import { K8sResourcesContext } from '../../hooks/K8sResourcesContext'; import { useSideBar } from '../../hooks/useSideBar'; import { useWorkloadsWatcher } from '../../hooks/useWorkloadWatcher'; @@ -85,7 +85,12 @@ const TopologyViewWorkloadComponent = ({ ? (controller.getElementById(selectedId) as BaseNode) : null; setSelectedNode(selectedNode); - if (selectedNode && selectedNode.getType() === TYPE_WORKLOAD) + + if ( + selectedNode && + (selectedNode.getType() === TYPE_WORKLOAD || + selectedNode.getType() === TYPE_VM) + ) setSideBarOpen(true); else { setSideBarOpen(false); @@ -97,7 +102,10 @@ const TopologyViewWorkloadComponent = ({ const id = ids[0] ? ids[0] : ''; const selNode = controller.getElementById(id) as BaseNode; setSelectedNode(selNode); - if (!id || selNode.getType() !== TYPE_WORKLOAD) { + if ( + !id || + (selNode.getType() !== TYPE_WORKLOAD && selNode.getType() !== TYPE_VM) + ) { removeSelectedIdParam(); } }); @@ -131,6 +139,7 @@ const TopologyViewWorkloadComponent = ({ {allErrors && allErrors.length > 0 && ( )} + {clusters.length < 1 ? ( diff --git a/plugins/topology/src/const.ts b/plugins/topology/src/const.ts index 9691eee0e1..92384598ca 100644 --- a/plugins/topology/src/const.ts +++ b/plugins/topology/src/const.ts @@ -16,7 +16,10 @@ export const GROUP_PADDING = [ export const MAXSHOWRESCOUNT = 3; +export const RESOURCE_NAME_TRUNCATE_LENGTH = 13; + export const TYPE_WORKLOAD = 'workload'; +export const TYPE_VM = 'virtualmachine'; export const TYPE_APPLICATION_GROUP = 'part-of'; export const TYPE_CONNECTS_TO = 'connects-to'; export const INSTANCE_LABEL = 'app.kubernetes.io/instance'; diff --git a/plugins/topology/src/data-transforms/data-transformer.test.ts b/plugins/topology/src/data-transforms/data-transformer.test.ts index 430eefc7d4..75f7b430cf 100644 --- a/plugins/topology/src/data-transforms/data-transformer.test.ts +++ b/plugins/topology/src/data-transforms/data-transformer.test.ts @@ -1,12 +1,15 @@ +import { NodeShape } from '@patternfly/react-topology'; + import { mockK8sResourcesData } from '../__fixtures__/1-deployments'; +import { TYPE_VM } from '../const'; import { getBaseTopologyDataModel } from './data-transformer'; describe('data-transformer', () => { - it('should return base topology data model with 3 nodes and 0 edges', () => { + it('should return base topology data model with 5 nodes and 0 edges', () => { const baseDataModel = getBaseTopologyDataModel( mockK8sResourcesData.watchResourcesData as any, ); - expect(baseDataModel.nodes).toHaveLength(3); + expect(baseDataModel.nodes).toHaveLength(5); expect(baseDataModel.edges).toHaveLength(0); }); @@ -16,6 +19,9 @@ describe('data-transformer', () => { deployments: { data: [mockK8sResourcesData.watchResourcesData.deployments.data[0]], }, + virtualmachines: { + data: [], + }, }; const baseDataModel = getBaseTopologyDataModel( mockWatchResourcesData as any, @@ -23,4 +29,22 @@ describe('data-transformer', () => { expect(baseDataModel.nodes).toHaveLength(1); expect(baseDataModel.edges).toHaveLength(0); }); + it('should return 2 Virtual Machine nodes and each Virtual Machine node shape should be Rectangle', () => { + const mockWatchResourcesData = { + ...mockK8sResourcesData.watchResourcesData, + deployments: { + data: [], + }, + }; + const baseDataModel = getBaseTopologyDataModel( + mockWatchResourcesData as any, + ); + const virtualMachineNodes = baseDataModel.nodes?.filter( + node => node.type === TYPE_VM, + ); + expect(virtualMachineNodes).toHaveLength(2); + virtualMachineNodes?.forEach(node => { + expect(node.shape).toBe(NodeShape.rect); + }); + }); }); diff --git a/plugins/topology/src/data-transforms/data-transformer.ts b/plugins/topology/src/data-transforms/data-transformer.ts index d11e410d67..3eff6a5c68 100644 --- a/plugins/topology/src/data-transforms/data-transformer.ts +++ b/plugins/topology/src/data-transforms/data-transformer.ts @@ -1,9 +1,10 @@ import { V1Service } from '@kubernetes/client-node'; -import { Model, NodeModel } from '@patternfly/react-topology'; +import { Model, NodeModel, NodeShape } from '@patternfly/react-topology'; -import { TYPE_APPLICATION_GROUP, TYPE_WORKLOAD } from '../const'; +import { TYPE_APPLICATION_GROUP, TYPE_VM, TYPE_WORKLOAD } from '../const'; import { CronJobModel } from '../models'; import { K8sResponseData, K8sWorkloadResource } from '../types/types'; +import { VM_TYPE } from '../types/vms'; import { getPipelinesDataForResource } from '../utils/pipeline-utils'; import { getPodsDataForResource } from '../utils/pod-resource-utils'; import { @@ -27,6 +28,7 @@ import { mergeGroup, WorkloadModelProps, } from '../utils/transform-utils'; +import { VirtualMachineModel } from '../vm-models'; export const getBaseTopologyDataModel = (resources: K8sResponseData): Model => { const baseDataModel: Model = { @@ -34,7 +36,7 @@ export const getBaseTopologyDataModel = (resources: K8sResponseData): Model => { edges: [], }; - WORKLOAD_TYPES.forEach((key: string) => { + [VM_TYPE, ...WORKLOAD_TYPES].forEach((key: string) => { if (resources?.[key]?.data?.length) { const typedDataModel: Model = { nodes: [], @@ -47,7 +49,9 @@ export const getBaseTopologyDataModel = (resources: K8sResponseData): Model => { const data = createTopologyNodeData( resource, item, - TYPE_WORKLOAD, + resource.kind === VirtualMachineModel.kind + ? TYPE_VM + : TYPE_WORKLOAD, 'icon-default', getUrlForResource(resources, resource), { @@ -66,16 +70,31 @@ export const getBaseTopologyDataModel = (resources: K8sResponseData): Model => { jobsData: getJobsDataForResource(resources, resource), } : {}), - pipelinesData: getPipelinesDataForResource(resources, resource), - cheCluster: getCheCluster(resources), + + ...(resource.kind !== VirtualMachineModel.kind + ? { + pipelinesData: getPipelinesDataForResource( + resources, + resource, + ), + cheCluster: getCheCluster(resources), + } + : {}), }, ); typedDataModel.nodes?.push( getTopologyNodeItem( resource, - TYPE_WORKLOAD, + resource.kind === VirtualMachineModel.kind + ? TYPE_VM + : TYPE_WORKLOAD, data, WorkloadModelProps, + undefined, + undefined, + resource.kind === VirtualMachineModel.kind + ? NodeShape.rect + : undefined, ), ); mergeGroup( diff --git a/plugins/topology/src/hooks/useSideBar.tsx b/plugins/topology/src/hooks/useSideBar.tsx index 65a1abc684..61acfcd569 100644 --- a/plugins/topology/src/hooks/useSideBar.tsx +++ b/plugins/topology/src/hooks/useSideBar.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { BaseNode } from '@patternfly/react-topology'; import TopologySideBar from '../components/Topology/TopologySideBar/TopologySideBar'; -import { TYPE_WORKLOAD } from '../const'; +import { TYPE_VM, TYPE_WORKLOAD } from '../const'; export const useSideBar = (): [ React.ReactNode, @@ -30,15 +30,17 @@ export const useSideBar = (): [ ); }, [params]); - const sideBar = selectedNode && selectedNode.getType() === TYPE_WORKLOAD && ( - { - setSideBarOpen(false); - removeSelectedIdParam(); - }} - node={selectedNode} - /> - ); + const sideBar = selectedNode && + (selectedNode.getType() === TYPE_WORKLOAD || + selectedNode.getType() === TYPE_VM) && ( + { + setSideBarOpen(false); + removeSelectedIdParam(); + }} + node={selectedNode} + /> + ); return [ sideBar, diff --git a/plugins/topology/src/models.ts b/plugins/topology/src/models.ts index d2c36f026a..bb834439ed 100644 --- a/plugins/topology/src/models.ts +++ b/plugins/topology/src/models.ts @@ -8,6 +8,7 @@ import { TaskRunModel, } from './pipeline-models'; import { GroupVersionKind, Model } from './types/types'; +import { VirtualMachineGVK, VirtualMachineModel } from './vm-models'; export const ReplicaSetGVK: GroupVersionKind = { apiVersion: 'v1', @@ -71,7 +72,6 @@ export const CheClusterGVK: GroupVersionKind = { apiGroup: 'org.eclipse.che', kind: 'CheCluster', }; - export enum ModelsPlural { deployments = 'deployments', pods = 'pods', @@ -86,6 +86,7 @@ export enum ModelsPlural { pipelines = 'pipelines', pipelineruns = 'pipelineruns', checlusters = 'checlusters', + virtualmachines = 'virtualmachines', } export const resourceGVKs: { [key: string]: GroupVersionKind } = { @@ -103,6 +104,7 @@ export const resourceGVKs: { [key: string]: GroupVersionKind } = { [PipelineModelsPlural.pipelines]: PipelineGVK, [PipelineModelsPlural.taskruns]: TaskRunGVK, [ModelsPlural.checlusters]: CheClusterGVK, + [ModelsPlural.virtualmachines]: VirtualMachineGVK, }; export const DeploymentModel: Model = { @@ -187,4 +189,5 @@ export const resourceModels = { [PipelineRunModel.kind]: PipelineRunModel, [TaskRunModel.kind]: TaskRunModel, [CheClusterModel.kind]: CheClusterModel, + [VirtualMachineModel.kind]: VirtualMachineModel, }; diff --git a/plugins/topology/src/types/vms.ts b/plugins/topology/src/types/vms.ts new file mode 100644 index 0000000000..c8d3b8753b --- /dev/null +++ b/plugins/topology/src/types/vms.ts @@ -0,0 +1,3 @@ +import { ModelsPlural } from '../models'; + +export const VM_TYPE: string = ModelsPlural.virtualmachines; diff --git a/plugins/topology/src/utils/resource-utils.ts b/plugins/topology/src/utils/resource-utils.ts index 0df77a61f1..708078770d 100644 --- a/plugins/topology/src/utils/resource-utils.ts +++ b/plugins/topology/src/utils/resource-utils.ts @@ -19,6 +19,7 @@ import { K8sResponseData, K8sWorkloadResource, } from '../types/types'; +import { VM_TYPE } from '../types/vms'; import { LabelSelector } from './label-selector'; import { getJobsForCronJob, @@ -53,7 +54,7 @@ export const createOverviewItemForType = ( type: string, resource: K8sWorkloadResource, ): OverviewItem | undefined => { - if (!WORKLOAD_TYPES.includes(type)) { + if (![...WORKLOAD_TYPES, VM_TYPE].includes(type)) { return undefined; } switch (type) { diff --git a/plugins/topology/src/vm-models.ts b/plugins/topology/src/vm-models.ts new file mode 100644 index 0000000000..f191df127f --- /dev/null +++ b/plugins/topology/src/vm-models.ts @@ -0,0 +1,14 @@ +import { GroupVersionKind, Model } from './types/types'; + +export const VirtualMachineGVK: GroupVersionKind = { + apiVersion: 'v1', + apiGroup: 'kubevirt.io', + kind: 'VirtualMachine', +}; +export const VirtualMachineModel: Model = { + ...VirtualMachineGVK, + abbr: 'VM', + labelPlural: 'VirtualMachines', + color: '#2b9af3', + plural: 'virtualmachines', +};