Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion kubevirt/.eslintcache
Original file line number Diff line number Diff line change
@@ -1 +1 @@
[{"/home/klokev20/headlamp-plugins/kubevirt/src/headlamp-plugin.d.ts":"1","/home/klokev20/headlamp-plugins/kubevirt/src/index.tsx":"2","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/Terminal/Terminal.tsx":"3","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachineInstance/Details.tsx":"4","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachineInstance/List.tsx":"5","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachineInstance/VirtualMachineInstance.tsx":"6","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachines/Details.tsx":"7","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachines/List.tsx":"8","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachines/VirtualMachine.tsx":"9","/home/klokev20/headlamp-plugins/kubevirt/src/storybook.test.tsx":"10"},{"size":680,"mtime":1762330749267,"results":"11","hashOfConfig":"12"},{"size":1575,"mtime":1762333810208,"results":"13","hashOfConfig":"12"},{"size":7407,"mtime":1762330749267,"results":"14","hashOfConfig":"12"},{"size":2486,"mtime":1762330749267,"results":"15","hashOfConfig":"12"},{"size":3772,"mtime":1762334239548,"results":"16","hashOfConfig":"12"},{"size":1180,"mtime":1762330749267,"results":"17","hashOfConfig":"12"},{"size":5301,"mtime":1762330749267,"results":"18","hashOfConfig":"12"},{"size":3379,"mtime":1762334402395,"results":"19","hashOfConfig":"12"},{"size":1203,"mtime":1762330749267,"results":"20","hashOfConfig":"12"},{"size":176,"mtime":1762330749267,"results":"21","hashOfConfig":"12"},{"filePath":"22","messages":"23","suppressedMessages":"24","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"1o2l0oj",{"filePath":"25","messages":"26","suppressedMessages":"27","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"28","messages":"29","suppressedMessages":"30","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"31","messages":"32","suppressedMessages":"33","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"34","messages":"35","suppressedMessages":"36","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"37","messages":"38","suppressedMessages":"39","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"40","messages":"41","suppressedMessages":"42","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"43","messages":"44","suppressedMessages":"45","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"46","messages":"47","suppressedMessages":"48","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"49","messages":"50","suppressedMessages":"51","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/home/klokev20/headlamp-plugins/kubevirt/src/headlamp-plugin.d.ts",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/index.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/Terminal/Terminal.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachineInstance/Details.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachineInstance/List.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachineInstance/VirtualMachineInstance.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachines/Details.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachines/List.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachines/VirtualMachine.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/storybook.test.tsx",[],[]]
[{"/home/klokev20/headlamp-plugins/kubevirt/src/headlamp-plugin.d.ts":"1","/home/klokev20/headlamp-plugins/kubevirt/src/index.tsx":"2","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/Terminal/Terminal.tsx":"3","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachineInstance/Details.tsx":"4","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachineInstance/List.tsx":"5","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachineInstance/VirtualMachineInstance.tsx":"6","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachines/Details.tsx":"7","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachines/List.tsx":"8","/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachines/VirtualMachine.tsx":"9","/home/klokev20/headlamp-plugins/kubevirt/src/storybook.test.tsx":"10"},{"size":680,"mtime":1762503095333,"results":"11","hashOfConfig":"12"},{"size":1575,"mtime":1762503095333,"results":"13","hashOfConfig":"12"},{"size":7407,"mtime":1762503095334,"results":"14","hashOfConfig":"12"},{"size":2486,"mtime":1762503095334,"results":"15","hashOfConfig":"12"},{"size":3772,"mtime":1762503095334,"results":"16","hashOfConfig":"12"},{"size":1636,"mtime":1762503095334,"results":"17","hashOfConfig":"12"},{"size":12085,"mtime":1762508534665,"results":"18","hashOfConfig":"12"},{"size":3379,"mtime":1762503095334,"results":"19","hashOfConfig":"12"},{"size":2211,"mtime":1762507882178,"results":"20","hashOfConfig":"12"},{"size":176,"mtime":1762503095334,"results":"21","hashOfConfig":"12"},{"filePath":"22","messages":"23","suppressedMessages":"24","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"1o2l0oj",{"filePath":"25","messages":"26","suppressedMessages":"27","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"28","messages":"29","suppressedMessages":"30","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"31","messages":"32","suppressedMessages":"33","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"34","messages":"35","suppressedMessages":"36","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"37","messages":"38","suppressedMessages":"39","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"40","messages":"41","suppressedMessages":"42","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"43","messages":"44","suppressedMessages":"45","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"46","messages":"47","suppressedMessages":"48","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"49","messages":"50","suppressedMessages":"51","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/home/klokev20/headlamp-plugins/kubevirt/src/headlamp-plugin.d.ts",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/index.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/Terminal/Terminal.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachineInstance/Details.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachineInstance/List.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachineInstance/VirtualMachineInstance.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachines/Details.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachines/List.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/kubevirt/VirtualMachines/VirtualMachine.tsx",[],[],"/home/klokev20/headlamp-plugins/kubevirt/src/storybook.test.tsx",[],[]]
6 changes: 3 additions & 3 deletions kubevirt/artifacthub-pkg.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
version: v0.0.1-beta5
version: v0.0.1-beta6
name: buttah_kubevirt
displayName: Kubevirt
createdAt: "2025-01-01T00:00:00Z"
logoURL: "https://github.com/kubevirt/community/blob/main/logo/KubeVirt_icon.png?raw=true"
description: "A plugin for managing KubeVirt virtual machines within a Kubernetes cluster."
annotations:
headlamp/plugin/archive-url: "https://github.com/buttahtoast/headlamp-plugins/releases/download/kubevirt-v0.0.1-beta5/kubevirt-v0.0.1-beta5.tar.gz"
headlamp/plugin/archive-checksum: "SHA256:77cfd22a90964804b16780f66c627f4e913223f6a6a4b2e0e5efb2b24ba36467"
headlamp/plugin/archive-url: "https://github.com/buttahtoast/headlamp-plugins/releases/download/kubevirt-v0.0.1-beta6/kubevirt-v0.0.1-beta6.tar.gz"
headlamp/plugin/archive-checksum: "SHA256:68a98b4204f182154b55f1ac899461a6112cf1698ba226da4991d44c0453da8a"
headlamp/plugin/version-compat: ">=0.24"
headlamp/plugin/distro-compat: "in-cluster,web,docker-desktop,desktop"
links:
Expand Down
2 changes: 1 addition & 1 deletion kubevirt/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "kubevirt",
"version": "v0.0.1-beta5",
"version": "v0.0.1-beta6",
"description": "A plugin for managing KubeVirt virtual machines within a Kubernetes cluster.",
"scripts": {
"start": "headlamp-plugin start",
Expand Down
1 change: 1 addition & 0 deletions kubevirt/src/kubevirt/VirtualMachineInstance/Details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default function VirtualMachineInstanceDetails(props: VirtualMachineInsta
name={name}
namespace={namespace}
resourceType={VirtualMachineInstance}
withEvents
extraInfo={item =>
item && [
{
Expand Down
226 changes: 160 additions & 66 deletions kubevirt/src/kubevirt/VirtualMachines/Details.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ApiProxy } from '@kinvolk/headlamp-plugin/lib';
import { Link, Resource } from '@kinvolk/headlamp-plugin/lib/components/common';
import { ActionButton } from '@kinvolk/headlamp-plugin/lib/components/common';
import { Chip } from '@mui/material';
import { useSnackbar } from 'notistack';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
Expand All @@ -22,76 +23,131 @@ export default function VirtualMachineDetails(props: VirtualMachineDetailsProps)
const [showTerminal, setShowTerminal] = useState(false);

const [podName, setPodName] = useState<string | null>(null);
const [nodeName, setNodeName] = useState<string | null>(null);
useEffect(() => {
const fetchPodName = async () => {
const fetchInitial = async () => {
try {
const podName = await getPodName(name, namespace);
setPodName(podName);
const info = await getPodInfo(name, namespace);
setPodName(info.podName);
setNodeName(info.nodeName);
} catch (error) {
console.error('Failed to get pod name', error);
console.error('Failed to get pod info', error);
}
};

fetchPodName();
fetchInitial();

const queryParams = new URLSearchParams();
queryParams.append('labelSelector', `vm.kubevirt.io/name=${name}`);
queryParams.append('watch', 'true');
const url = `/api/v1/namespaces/${namespace}/pods?${queryParams.toString()}`;

const onStream = (result, disconnect, error) => {
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stream callback parameters lack type annotations. Add proper TypeScript types for the parameters: result: any, disconnect: () => void, error?: Error.

Copilot uses AI. Check for mistakes.
if (error) {
console.error('Stream error:', error);
disconnect();
return;
}

const event = result;
if (event.type === 'ADDED' || event.type === 'MODIFIED') {
const pod = event.object;
setPodName(pod.metadata.name);
setNodeName(pod.spec.nodeName || 'Unknown');
} else if (event.type === 'DELETED') {
setPodName('Unknown');
setNodeName('Unknown');
}
};

const { cancel: cancelPod } = ApiProxy.stream(url, onStream, { isJson: true });
return () => {
cancelPod();
};
}, [name, namespace]);
return (
<Resource.DetailsGrid
name={name}
namespace={namespace}
resourceType={VirtualMachine}
withEvents
extraInfo={item =>
item && [
{
name: t('Status'),
value: item?.jsonData.status.printableStatus,
},
{
name: 'VirtualMachineInstance',
value: (
<Link
routeName="/kubevirt/virtualmachinesinstances/:namespace/:name"
params={{ name: item.getName(), namespace: item.getNamespace() }}
>
{item.getName()}
</Link>
),
},
{
name: 'Pod',
value: (
<Link
routeName="pod"
params={{
name: podName,
namespace: item.getNamespace(),
}}
>
{podName}
</Link>
),
},
]
item
? [
{
name: t('Status'),
value: (
<Chip
label={item?.jsonData.status.printableStatus || 'Unknown'}
color={getStatusColor(item?.jsonData.status.printableStatus || 'Unknown')}
variant="outlined"
/>
),
},
{
name: 'VirtualMachineInstance',
value: (
<Link
routeName="/kubevirt/virtualmachinesinstances/:namespace/:name"
params={{ name: item.getName(), namespace: item.getNamespace() }}
>
{item.getName()}
</Link>
),
},
{
name: 'Pod',
value:
podName && podName !== 'Unknown' ? (
<Link
routeName="pod"
params={{
name: podName,
namespace: item.getNamespace(),
}}
>
{podName}
</Link>
) : (
'Unknown'
),
},
{
name: 'Node',
value:
nodeName && nodeName !== 'Unknown' ? (
<Link routeName="node" params={{ name: nodeName }}>
{nodeName}
</Link>
) : (
'Unknown'
),
},
]
: null
}
extraSections={item =>
item && [
{
id: 'status',
section: <Resource.ConditionsSection resource={item?.jsonData} />,
},
{
id: 'headlamp.vm-terminal',
section: (
<Terminal
open={showTerminal}
key="terminal"
item={item}
onClose={() => {
setShowTerminal(false);
}}
/>
),
},
]
item
? [
{
id: 'status',
section: <Resource.ConditionsSection resource={item?.jsonData} />,
},
{
id: 'headlamp.vm-terminal',
section: (
<Terminal
open={showTerminal}
key="terminal"
item={item}
onClose={() => {
setShowTerminal(false);
}}
/>
),
},
]
: null
}
actions={item => {
if (!item) return [];
Expand All @@ -112,7 +168,6 @@ export default function VirtualMachineDetails(props: VirtualMachineDetailsProps)
description={t('Start')}
icon="mdi:play"
onClick={() => {
console.log('Starting ' + item.getName());
item
.start()
.then(() =>
Expand All @@ -136,7 +191,6 @@ export default function VirtualMachineDetails(props: VirtualMachineDetailsProps)
description={t('Stop')}
icon="mdi:stop"
onClick={() => {
console.log('Stopping ' + item.getName());
item
.stop()
.then(() =>
Expand All @@ -160,7 +214,6 @@ export default function VirtualMachineDetails(props: VirtualMachineDetailsProps)
description={t('Pause')}
icon="mdi:pause"
onClick={() => {
console.log('Pausing ' + item.getName());
item
.pause()
.then(() =>
Expand All @@ -179,6 +232,29 @@ export default function VirtualMachineDetails(props: VirtualMachineDetailsProps)
});
}

if (isRunning) {
actionsList.push({
id: 'migrate',
action: (
<ActionButton
description={t('Live Migrate')}
icon="mdi:swap-horizontal"
onClick={() => {
item
.migrate()
.then(() => {
enqueueSnackbar(t('Live migration initiated'), { variant: 'success' });
})
.catch(e => {
console.error('Migration failed', e);
enqueueSnackbar(t('Failed to initiate live migration'), { variant: 'error' });
});
}}
/>
),
});
}

if (isPaused) {
actionsList.push({
id: 'unpause',
Expand All @@ -187,7 +263,6 @@ export default function VirtualMachineDetails(props: VirtualMachineDetailsProps)
description={t('Unpause')}
icon="mdi:play-pause"
onClick={() => {
console.log('Unpausing ' + item.getName());
item
.unpause()
.then(() =>
Expand Down Expand Up @@ -227,17 +302,36 @@ export default function VirtualMachineDetails(props: VirtualMachineDetailsProps)
);
}

async function getPodName(name: string, namespace: string): Promise<string> {
function getStatusColor(status: string) {
if (status === 'Running') return 'success';
if (status === 'Failed') return 'error';
if (['Migrating', 'Starting', 'Stopping'].includes(status)) return 'warning';
return 'default';
}

async function getPodInfo(
name: string,
namespace: string
): Promise<{ podName: string; nodeName: string }> {
const request = ApiProxy.request;
const queryParams = new URLSearchParams();
let response;
queryParams.append('labelSelector', `vm.kubevirt.io/name=${name}`);
try {
response = await request(`/api/v1/namespaces/${namespace}/pods?${queryParams.toString()}`, {
method: 'GET',
});
const response = await request(
`/api/v1/namespaces/${namespace}/pods?${queryParams.toString()}`,
{
method: 'GET',
}
);
const pod = response?.items[0];
if (pod) {
return {
podName: pod.metadata.name,
nodeName: pod.spec.nodeName || 'Unknown',
};
}
return { podName: 'Unknown', nodeName: 'Unknown' };
} catch (error) {
return 'Unknown';
return { podName: 'Unknown', nodeName: 'Unknown' };
}
return response?.items[0]?.metadata?.name || 'Unknown';
}
Loading