Skip to content
Open
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
3 changes: 3 additions & 0 deletions app/controllers/application_controller/explorer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ def x_history
'remove_security_group' => :s2,
'rename' => :s2,

'add_volume' => :s2,
'remove_volume' => :s2,

# specials
'perf' => :show,
'download_pdf' => :show,
Expand Down
38 changes: 38 additions & 0 deletions app/controllers/vm_common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,32 @@ def textual_summary_flash_list
helper_method :disable_check?
end

def add_volume
assert_privileges("vm_common_add_volume")
@record = find_record_with_rbac(VmOrTemplate, params[:id])
@edit ||= {}
@in_a_form = true

if @explorer
@refresh_partial = "vm_common/add_volume"
@edit[:explorer] = true
end
end
alias_method :instance_add_volume, :add_volume

def remove_volume
assert_privileges("vm_common_remove_volume")
@record = find_record_with_rbac(VmOrTemplate, params[:id])
@edit ||= {}
@in_a_form = true

if @explorer
@refresh_partial = "vm_common/remove_volume"
@edit[:explorer] = true
end
end
alias_method :instance_remove_volume, :remove_volume

# handle buttons pressed on the button bar
def button
@edit = session[:edit] # Restore @edit for adv search box
Expand Down Expand Up @@ -1297,6 +1323,18 @@ def set_right_cell_vars(options = {})
partial = "layouts/tl_show"
header = _("Timelines for %{virtual_machine} \"%{name}\"") % {:virtual_machine => ui_lookup(:table => table), :name => name}
action = nil
when "add_volume", "instance_add_volume"
partial = "vm_common/add_volume"
header = _("Add Volume to %{vm_or_template} \"%{name}\"") % {
:vm_or_template => ui_lookup(:table => table),
:name => name
}
when "remove_volume", "instance_remove_volume"
partial = "vm_common/remove_volume"
header = _("Remove Volume %{vm_or_template} \"%{name}\"") % {
:vm_or_template => ui_lookup(:table => table),
:name => name
}
else
# now take care of links on summary screen
partial = if @showtype == "details"
Expand Down
27 changes: 27 additions & 0 deletions app/controllers/vm_infra_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,33 @@ def index
redirect_to(:action => 'explorer')
end

def persistentvolumeclaims
@record = find_record_with_rbac(VmOrTemplate, params[:id])
pvcs = @record.persistentvolumeclaims(@record)

render :json => {
:resources => pvcs,
:vm_name => @record.name,
:vm_namespace => @record.location
}
rescue => e
render :json => {:error => e.message}, :status => 500
end

def attached_volumes
@record = find_record_with_rbac(VmOrTemplate, params[:id])
attached = @record.attached_volumes(@record)

render :json => {
:resources => attached,
:vm_name => @record.name,
:vm_namespace => @record.location
}

rescue => e
render :json => {:error => e.message}, :status => 500
end

private

def features
Expand Down
6 changes: 6 additions & 0 deletions app/helpers/application_helper/button/vm_attach_volume.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class ApplicationHelper::Button::VmAttachVolume < ApplicationHelper::Button::Basic
needs :@record
def visible?
@record.kind_of?(ManageIQ::Providers::Kubevirt::InfraManager::Vm)
end
end
6 changes: 6 additions & 0 deletions app/helpers/application_helper/button/vm_detach_volume.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class ApplicationHelper::Button::VmDetachVolume < ApplicationHelper::Button::Basic
needs :@record
def visible?
@record.kind_of?(ManageIQ::Providers::Kubevirt::InfraManager::Vm)
end
end
14 changes: 14 additions & 0 deletions app/helpers/application_helper/toolbar/x_vm_center.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,20 @@ class ApplicationHelper::Toolbar::XVmCenter < ApplicationHelper::Toolbar::Basic
t,
:klass => ApplicationHelper::Button::VmSnapshotAdd
),
button(
:instance_add_volume,
'fa fa-hdd-o fa-lg',
t = N_('Attach Volume'),
t,
:klass => ApplicationHelper::Button::VmAttachVolume
),
button(
:instance_remove_volume,
'fa fa-trash-o fa-lg',
t = N_('Detach Volume'),
t,
:klass => ApplicationHelper::Button::VmDetachVolume
),
]
),
])
Expand Down
130 changes: 130 additions & 0 deletions app/javascript/components/vm-infra/add-volume.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import React, { useState, useEffect, useMemo } from "react";
import PropTypes from "prop-types";
import { Grid } from "carbon-components-react";
import MiqFormRenderer from "../../forms/data-driven-form";
import { API } from "../../http_api";
import createSchema from "./add-volume.schema";
import miqRedirectBack from "../../helpers/miq-redirect-back";

const AddVolumeForm = ({ recordId, redirect }) => {
const [state, setState] = useState({
isLoading: true,
volumes: [],
});

const [isSubmitDisabled, setSubmitDisabled] = useState(true);

useEffect(() => {
const fetchPersistentVolumeClaims = async () => {
try {
setState(prev => ({ ...prev, isLoading: true, error: null }));

// Use the correct route for your Rails controller
const response = await fetch(`/vm_infra/${recordId}/persistentvolumeclaims`);
const data = await response.json();

if (!response.ok) {
throw new Error((data.error && data.error.message) || 'Failed to fetch persistent volume claims');
}

setState(prev => ({
...prev,
isLoading: false,
volumes: data.resources || [],
vmInfo: {
name: data.vm_name,
namespace: data.vm_namespace
}
}));
} catch (error) {
console.error('Error fetching PVCs:', error);
setState(prev => ({
...prev,
isLoading: false,
error: error.message,
volumes: []
}));
}
};

fetchPersistentVolumeClaims();
}, [recordId]);

const schema = useMemo(() => createSchema(state.volumes), [state.volumes]);

const onFormChange = (values) => {
if (values.volumeSourceType === "existing") {
setSubmitDisabled(!values.pvcName);
} else if (values.volumeSourceType === "new") {
setSubmitDisabled(!values.newVolumeName || !values.newVolumeSize);
} else {
setSubmitDisabled(true);
}
};

const onSubmit = (values) => {
const { volumeSourceType } = values;

let payload;

if (volumeSourceType === "existing") {
const volumeNameFinal =
values.volumeName && values.volumeName.trim()
? values.volumeName.trim()
: values.pvcName;

payload = {
action: "attach",
resource: {
pvc_name: values.pvcName,
volume_name: volumeNameFinal,
vm_id: recordId
}
};
} else {
payload = {
action: "create_and_attach_volume",
resource: {
volume_name: values.newVolumeName.trim(),
volume_size: values.newVolumeSize.trim(),
vm_id: recordId,
device: values.device_mountpoint ? values.device_mountpoint : ''
},
};
}

const request = API.post(`/api/container_volumes/${recordId}`, payload);

request.then(() => {
const message = sprintf(
__('Volume processed successfully.')
);
miqRedirectBack(message, 'success', redirect);
}).catch((error) => {
miqRedirectBack(error.message || __("Failed to attach volume"), "error", redirect);
}).finally(miqSparkleOff);

};

const onCancel = () =>
miqRedirectBack(__("Add Volume was cancelled by the user"), "warning", redirect);

return state.isLoading ? null : (
<Grid>
<MiqFormRenderer
schema={schema}
onSubmit={onSubmit}
onCancel={onCancel}
canSubmit={!isSubmitDisabled}
onStateUpdate={onFormChange}
/>
</Grid>
);
};

AddVolumeForm.propTypes = {
recordId: PropTypes.string.isRequired,
redirect: PropTypes.string.isRequired,
};

export default AddVolumeForm;
77 changes: 77 additions & 0 deletions app/javascript/components/vm-infra/add-volume.schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { componentTypes } from "@@ddf";

const createSchema = (volumes = []) => ({
fields: [
{
component: componentTypes.RADIO,
name: "volumeSourceType",
label: __("Volume Source Type"),
isRequired: true,
options: [
{ label: __("Select Existing PVC"), value: "existing" },
{ label: __("Create New PVC"), value: "new" },
],
initialValue: "existing",
},
// Existing PVC selection
{
component: componentTypes.SELECT,
name: "pvcName",
id: "pvcName",
label: __("Select Persistent Volume Claim"),
placeholder: volumes.length > 0 ? __("Select PVC") : __("No PVCs available"),
options: volumes.length > 0
? [
{ label: __("Select PVC"), value: null, isDisabled: true },
...volumes.map(({ metadata }) => ({
label: metadata.name,
value: metadata.name,
})),
]
: [{ label: __("No PVCs available"), value: "", isDisabled: true }],
condition: {
when: "volumeSourceType",
is: "existing",
},
isRequired: true,
validate: [{ type: "required", message: __("PVC selection is required") }],
},


// New volume name
{
component: componentTypes.TEXT_FIELD,
name: "newVolumeName",
id: "newVolumeName",
label: __("New Volume Name"),
isRequired: true,
condition: {
when: "volumeSourceType",
is: "new",
},
validate: [{ type: "required", message: __("Volume name is required") }],
},
// New volume size
{
component: componentTypes.TEXT_FIELD,
name: "newVolumeSize",
id: "newVolumeSize",
label: __("New Volume Size (e.g., 3Gi)"),
isRequired: true,
condition: {
when: "volumeSourceType",
is: "new",
},
validate: [
{ type: "required", message: __("Volume size is required") },
{
type: "pattern",
pattern: "^[0-9]+Gi$",
message: __("Size must be in Gi format (e.g., 3Gi)"),
},
],
},
],
});

export default createSchema;
Loading