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
16 changes: 8 additions & 8 deletions app/controllers/vm_common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -298,15 +298,15 @@ def snap_pressed
@active = @snap_selected.current? if @snap_selected
@center_toolbar = 'x_vm_snapshot'
@explorer = true
render :update do |page|
page << javascript_prologue
page << javascript_reload_toolbars

page.replace("flash_msg_div", :partial => "layouts/flash_msg")
page << "miqScrollTop();" if @flash_array.present?
page.replace("desc_content", :partial => "/vm_common/snapshots_desc",
:locals => {:selected => params[:id]})
formatted_time = format_timezone(@snap_selected[:create_time].to_time, Time.zone, "view")
if @snap_selected[:total_size].to_i == 0
formatted_size = ''
else
formatted_bytes = number_to_human_size(@snap_selected[:total_size], :precision => 2)
total_bytes = _("%{number} bytes") % {:number => number_with_delimiter(@snap_selected[:total_size], :delimiter => ",", :separator => ".")}
formatted_size = "%{formatted_number} (%{total})" % {:formatted_number => formatted_bytes, :total => total_bytes}
end
render :json => {:data => {:data => @snap_selected, :size => formatted_size, :time => formatted_time}}, :status => 200
end

def disks
Expand Down
81 changes: 81 additions & 0 deletions app/javascript/components/vm-snapshot-tree-select/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './styles.css';
import SnapshotTree from './snapshot-tree';

const VMSnapshotTreeSelect = ({
tree, snapshot, size, time, name,
}) => {
const [currentSnapshot, setCurrentSnapshot] = useState({ ...snapshot, size, time });

// eslint-disable-next-line react/prop-types
return (
<div>
<div className="snapshot-details-div">
<div className="snapshot-details">
<div className="snapshot-detail-title">
<p>
<b>
{__('Description')}
</b>
</p>
</div>
<div className="snapshot-detail-value">
{currentSnapshot.data ? currentSnapshot.data.description : currentSnapshot.description || ''}
</div>
</div>
<div className="snapshot-details">
<div className="snapshot-detail-title" id="size-title">
<p>
<b>
{__('Size')}
</b>
</p>
</div>
<div className="snapshot-detail-value">
{currentSnapshot.size || ''}
</div>
</div>
<div className="snapshot-details">
<div className="snapshot-detail-title" id="created-title">
<p>
<b>
{__('Created')}
</b>
</p>
</div>
<div className="snapshot-detail-value">
{currentSnapshot.time || ''}
</div>
</div>
</div>
<div className="snapshot-tree-title">
{__('Available Snapshots')}
</div>
{tree.tree_nodes[0] && tree.tree_nodes[0].nodes.length > 0
? <SnapshotTree nodes={tree.tree_nodes} setCurrentSnapshot={setCurrentSnapshot} />
: (
<div className="no-snapshots-message">
{sprintf(__('%s has no snapshots'), name)}
</div>
)}
</div>
);
};

VMSnapshotTreeSelect.propTypes = {
tree: PropTypes.objectOf(PropTypes.any).isRequired,
snapshot: PropTypes.objectOf(PropTypes.any),
size: PropTypes.string,
time: PropTypes.string,
name: PropTypes.string,
};

VMSnapshotTreeSelect.defaultProps = {
snapshot: {},
size: '',
time: '',
name: '',
};

export default VMSnapshotTreeSelect;
174 changes: 174 additions & 0 deletions app/javascript/components/vm-snapshot-tree-select/snapshot-tree.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import React, { useEffect, useState } from 'react';
import {
Camera16, ChevronRight16, ChevronDown16, VirtualMachine16,
} from '@carbon/icons-react';
import TreeView, { flattenTree } from 'react-accessible-treeview';
import './styles.css';
import PropTypes from 'prop-types';

const allNodeData = [];

const convertData = (node) => {
allNodeData.push(
{
name: node.text,
id: node.key,
selectable: node.selectable,
tooltip: node.tooltip,
icon: node.icon,
}
);
const treeData = {
name: node.text, // Use the `text` property as the `name`
id: node.key, // Use the `key` property as the `id`
children: node.nodes ? node.nodes.map(convertData) : [], // Recursively process children
};

return treeData;
};

const SnapshotTree = ({ nodes, setCurrentSnapshot }) => {
const [selectedNode, setSelectedNode] = useState('');

const data = {
name: '',
children: nodes.map(convertData),
};
const treeData = flattenTree(data);
const expandedIds = [];
treeData.forEach((node) => {
expandedIds.push(node.id);

allNodeData.forEach((nodeData) => {
if (nodeData.id === node.id) {
const metadata = {
selectable: nodeData.selectable || false,
tooltip: nodeData.tooltip || nodeData.name,
icon: nodeData.icon || 'fa fa-camera',
};
node.metadata = metadata;
}
});
});

const nodeClick = (e, node) => {
if (node.metadata.selectable === false) {
// If the clicked node is already selected or root is selected, do nothing
return;
}

const ids = node.id.split('-');
const shortId = ids[ids.length - 1];
miqSparkleOn();
http.post(`/${ManageIQ.controller}/snap_pressed/${encodeURIComponent(shortId)}`).then((response) => {
if (response.data) {
const tempData = response.data;
tempData.size = response.data.size;
tempData.time = response.data.time;
setCurrentSnapshot(tempData);
}
miqSparkleOff();
});

e.stopPropagation();
setSelectedNode(e.target.id);
};

const addSelectedClassName = () => {
// Remove 'selected' class from all elements
const selectedElements = document.querySelectorAll('.selected-snapshot');
selectedElements.forEach((el) => {
el.classList.remove('selected-snapshot');
});

const selectedElement = document.getElementById(selectedNode);
if (selectedElement) {
selectedElement.parentNode.classList.add('selected-snapshot');
}
};

useEffect(() => {
addSelectedClassName();
}, [selectedNode]);

useEffect(() => {
treeData.forEach((node) => {
if (node.name.includes(__('(Active)'))) {
setSelectedNode(node.id);
}
});
}, []);

const ArrowIcon = (isOpen) => {
let icon = <ChevronRight16 />;
if (isOpen && isOpen.isOpen) {
icon = <ChevronDown16 />;
}
return <div className="arrow-div">{icon}</div>;
};

const NodeIcon = (icon) => {
if (icon === 'pficon pficon-virtual-machine') {
return <VirtualMachine16 />;
}
return <Camera16 />;
};

// First pull in node data and go through flattened tree to add metadata like icons and selectable
// Then add icons, tooltip and special handling

return (
<div>
<div className="checkbox">
<TreeView
data={treeData}
aria-label="Single select"
multiSelect={false}
onExpand={addSelectedClassName}
defaultExpandedIds={expandedIds}
propagateSelectUpwards
togglableSelect
nodeAction="check"
nodeRenderer={({
element,
isBranch,
isExpanded,
getNodeProps,
level,
handleExpand,
}) => (
<div
{...getNodeProps({ onClick: handleExpand })}
style={{ paddingLeft: 40 * (level - 1) }}
>
{isBranch && <ArrowIcon isOpen={isExpanded} />}
{element.metadata && element.metadata.icon && (
<div className="node-icon-div">
<NodeIcon icon={element.metadata.icon} />
</div>
)}
<span
key={element.id}
id={element.id}
onClick={(e) => nodeClick(e, element)}
onKeyDown={(e) => e.key === 'Enter' && nodeClick(e)}
role="button"
tabIndex={0}
className="name"
>
{element.name}
</span>
</div>
)}
/>
</div>
</div>
);
};

SnapshotTree.propTypes = {
nodes: PropTypes.arrayOf(PropTypes.any).isRequired,
setCurrentSnapshot: PropTypes.func.isRequired,
};

export default SnapshotTree;
110 changes: 110 additions & 0 deletions app/javascript/components/vm-snapshot-tree-select/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
.snapshot-details-div {
.snapshot-details {
margin-left: 30px;
margin-bottom: 20px;
}

.snapshot-detail-title {
width: 80px;
justify-content: right;
display: inline-flex;
margin-right: 30px;
}

.snapshot-detail-value {
display: inline-flex;
}
}

.snapshot-tree-title {
font-size: 20px;
padding-left: 20px;
padding-top: 40px;
}

.no-snapshots-message {
padding-left: 20px;
}

.checkbox {
font-size: 16px;
user-select: none;
min-height: 320px;
padding: 20px;
box-sizing: content-box;
}

.selected-snapshot {
background-color: #0f62fe;
color: white;
width: 100%;
margin-left: 0px;
pointer-events: none;

.arrow-div {
pointer-events: all;
}

span {
pointer-events: all;
}
}

.arrow-div {
display: inline-flex;
margin-right: 5px;
}

.node-icon-div {
margin-right: 5px;
display: inline-flex;
}

.checkbox .tree,
.checkbox .tree-node,
.checkbox .tree-node-group {
list-style: none;
margin: 0;
padding: 0;
}

.checkbox .tree-branch-wrapper,
.checkbox .tree-node__leaf {
outline: none;
}

.checkbox .tree-node {
cursor: pointer;
}

.checkbox .tree-node .name:hover {
background: rgba(0, 0, 0, 0.1);
}

.checkbox .tree-node--focused .name {
background: rgba(0, 0, 0, 0.2);
}

.checkbox .tree-node {
display: inline-block;
}

.checkbox .checkbox-icon {
margin: 0 5px;
vertical-align: middle;
}

.checkbox button {
border: none;
background: transparent;
cursor: pointer;
}

.checkbox .arrow {
margin-left: 5px;
vertical-align: middle;
}

.checkbox .arrow--open {
transform: rotate(90deg);
}
Loading