Skip to content

Commit 350d2ac

Browse files
authored
Merge pull request #9639 from GilbertCherrie/convert_snapshot_tree
Convert vm snapshot form
2 parents 2fefe94 + 68cec28 commit 350d2ac

File tree

11 files changed

+4025
-44
lines changed

11 files changed

+4025
-44
lines changed

app/controllers/vm_common.rb

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -298,15 +298,15 @@ def snap_pressed
298298
@active = @snap_selected.current? if @snap_selected
299299
@center_toolbar = 'x_vm_snapshot'
300300
@explorer = true
301-
render :update do |page|
302-
page << javascript_prologue
303-
page << javascript_reload_toolbars
304-
305-
page.replace("flash_msg_div", :partial => "layouts/flash_msg")
306-
page << "miqScrollTop();" if @flash_array.present?
307-
page.replace("desc_content", :partial => "/vm_common/snapshots_desc",
308-
:locals => {:selected => params[:id]})
301+
formatted_time = format_timezone(@snap_selected[:create_time].to_time, Time.zone, "view")
302+
if @snap_selected[:total_size].to_i == 0
303+
formatted_size = ''
304+
else
305+
formatted_bytes = number_to_human_size(@snap_selected[:total_size], :precision => 2)
306+
total_bytes = _("%{number} bytes") % {:number => number_with_delimiter(@snap_selected[:total_size], :delimiter => ",", :separator => ".")}
307+
formatted_size = "%{formatted_number} (%{total})" % {:formatted_number => formatted_bytes, :total => total_bytes}
309308
end
309+
render :json => {:data => {:data => @snap_selected, :size => formatted_size, :time => formatted_time}}, :status => 200
310310
end
311311

312312
def disks
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import React, { useState } from 'react';
2+
import PropTypes from 'prop-types';
3+
import './styles.css';
4+
import SnapshotTree from './snapshot-tree';
5+
6+
const VMSnapshotTreeSelect = ({
7+
tree, snapshot, size, time, name,
8+
}) => {
9+
const [currentSnapshot, setCurrentSnapshot] = useState({ ...snapshot, size, time });
10+
11+
// eslint-disable-next-line react/prop-types
12+
return (
13+
<div>
14+
<div className="snapshot-details-div">
15+
<div className="snapshot-details">
16+
<div className="snapshot-detail-title">
17+
<p>
18+
<b>
19+
{__('Description')}
20+
</b>
21+
</p>
22+
</div>
23+
<div className="snapshot-detail-value">
24+
{currentSnapshot.data ? currentSnapshot.data.description : currentSnapshot.description || ''}
25+
</div>
26+
</div>
27+
<div className="snapshot-details">
28+
<div className="snapshot-detail-title" id="size-title">
29+
<p>
30+
<b>
31+
{__('Size')}
32+
</b>
33+
</p>
34+
</div>
35+
<div className="snapshot-detail-value">
36+
{currentSnapshot.size || ''}
37+
</div>
38+
</div>
39+
<div className="snapshot-details">
40+
<div className="snapshot-detail-title" id="created-title">
41+
<p>
42+
<b>
43+
{__('Created')}
44+
</b>
45+
</p>
46+
</div>
47+
<div className="snapshot-detail-value">
48+
{currentSnapshot.time || ''}
49+
</div>
50+
</div>
51+
</div>
52+
<div className="snapshot-tree-title">
53+
{__('Available Snapshots')}
54+
</div>
55+
{tree.tree_nodes[0] && tree.tree_nodes[0].nodes.length > 0
56+
? <SnapshotTree nodes={tree.tree_nodes} setCurrentSnapshot={setCurrentSnapshot} />
57+
: (
58+
<div className="no-snapshots-message">
59+
{sprintf(__('%s has no snapshots'), name)}
60+
</div>
61+
)}
62+
</div>
63+
);
64+
};
65+
66+
VMSnapshotTreeSelect.propTypes = {
67+
tree: PropTypes.objectOf(PropTypes.any).isRequired,
68+
snapshot: PropTypes.objectOf(PropTypes.any),
69+
size: PropTypes.string,
70+
time: PropTypes.string,
71+
name: PropTypes.string,
72+
};
73+
74+
VMSnapshotTreeSelect.defaultProps = {
75+
snapshot: {},
76+
size: '',
77+
time: '',
78+
name: '',
79+
};
80+
81+
export default VMSnapshotTreeSelect;
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import React, { useEffect, useState } from 'react';
2+
import {
3+
Camera16, ChevronRight16, ChevronDown16, VirtualMachine16,
4+
} from '@carbon/icons-react';
5+
import TreeView, { flattenTree } from 'react-accessible-treeview';
6+
import './styles.css';
7+
import PropTypes from 'prop-types';
8+
9+
const allNodeData = [];
10+
11+
const convertData = (node) => {
12+
allNodeData.push(
13+
{
14+
name: node.text,
15+
id: node.key,
16+
selectable: node.selectable,
17+
tooltip: node.tooltip,
18+
icon: node.icon,
19+
}
20+
);
21+
const treeData = {
22+
name: node.text, // Use the `text` property as the `name`
23+
id: node.key, // Use the `key` property as the `id`
24+
children: node.nodes ? node.nodes.map(convertData) : [], // Recursively process children
25+
};
26+
27+
return treeData;
28+
};
29+
30+
const SnapshotTree = ({ nodes, setCurrentSnapshot }) => {
31+
const [selectedNode, setSelectedNode] = useState('');
32+
33+
const data = {
34+
name: '',
35+
children: nodes.map(convertData),
36+
};
37+
const treeData = flattenTree(data);
38+
const expandedIds = [];
39+
treeData.forEach((node) => {
40+
expandedIds.push(node.id);
41+
42+
allNodeData.forEach((nodeData) => {
43+
if (nodeData.id === node.id) {
44+
const metadata = {
45+
selectable: nodeData.selectable || false,
46+
tooltip: nodeData.tooltip || nodeData.name,
47+
icon: nodeData.icon || 'fa fa-camera',
48+
};
49+
node.metadata = metadata;
50+
}
51+
});
52+
});
53+
54+
const nodeClick = (e, node) => {
55+
if (node.metadata.selectable === false) {
56+
// If the clicked node is already selected or root is selected, do nothing
57+
return;
58+
}
59+
60+
const ids = node.id.split('-');
61+
const shortId = ids[ids.length - 1];
62+
miqSparkleOn();
63+
http.post(`/${ManageIQ.controller}/snap_pressed/${encodeURIComponent(shortId)}`).then((response) => {
64+
if (response.data) {
65+
const tempData = response.data;
66+
tempData.size = response.data.size;
67+
tempData.time = response.data.time;
68+
setCurrentSnapshot(tempData);
69+
}
70+
miqSparkleOff();
71+
});
72+
73+
e.stopPropagation();
74+
setSelectedNode(e.target.id);
75+
};
76+
77+
const addSelectedClassName = () => {
78+
// Remove 'selected' class from all elements
79+
const selectedElements = document.querySelectorAll('.selected-snapshot');
80+
selectedElements.forEach((el) => {
81+
el.classList.remove('selected-snapshot');
82+
});
83+
84+
const selectedElement = document.getElementById(selectedNode);
85+
if (selectedElement) {
86+
selectedElement.parentNode.classList.add('selected-snapshot');
87+
}
88+
};
89+
90+
useEffect(() => {
91+
addSelectedClassName();
92+
}, [selectedNode]);
93+
94+
useEffect(() => {
95+
treeData.forEach((node) => {
96+
if (node.name.includes(__('(Active)'))) {
97+
setSelectedNode(node.id);
98+
}
99+
});
100+
}, []);
101+
102+
const ArrowIcon = (isOpen) => {
103+
let icon = <ChevronRight16 />;
104+
if (isOpen && isOpen.isOpen) {
105+
icon = <ChevronDown16 />;
106+
}
107+
return <div className="arrow-div">{icon}</div>;
108+
};
109+
110+
const NodeIcon = (icon) => {
111+
if (icon === 'pficon pficon-virtual-machine') {
112+
return <VirtualMachine16 />;
113+
}
114+
return <Camera16 />;
115+
};
116+
117+
// First pull in node data and go through flattened tree to add metadata like icons and selectable
118+
// Then add icons, tooltip and special handling
119+
120+
return (
121+
<div>
122+
<div className="checkbox">
123+
<TreeView
124+
data={treeData}
125+
aria-label="Single select"
126+
multiSelect={false}
127+
onExpand={addSelectedClassName}
128+
defaultExpandedIds={expandedIds}
129+
propagateSelectUpwards
130+
togglableSelect
131+
nodeAction="check"
132+
nodeRenderer={({
133+
element,
134+
isBranch,
135+
isExpanded,
136+
getNodeProps,
137+
level,
138+
handleExpand,
139+
}) => (
140+
<div
141+
{...getNodeProps({ onClick: handleExpand })}
142+
style={{ paddingLeft: 40 * (level - 1) }}
143+
>
144+
{isBranch && <ArrowIcon isOpen={isExpanded} />}
145+
{element.metadata && element.metadata.icon && (
146+
<div className="node-icon-div">
147+
<NodeIcon icon={element.metadata.icon} />
148+
</div>
149+
)}
150+
<span
151+
key={element.id}
152+
id={element.id}
153+
onClick={(e) => nodeClick(e, element)}
154+
onKeyDown={(e) => e.key === 'Enter' && nodeClick(e)}
155+
role="button"
156+
tabIndex={0}
157+
className="name"
158+
>
159+
{element.name}
160+
</span>
161+
</div>
162+
)}
163+
/>
164+
</div>
165+
</div>
166+
);
167+
};
168+
169+
SnapshotTree.propTypes = {
170+
nodes: PropTypes.arrayOf(PropTypes.any).isRequired,
171+
setCurrentSnapshot: PropTypes.func.isRequired,
172+
};
173+
174+
export default SnapshotTree;
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
.snapshot-details-div {
2+
.snapshot-details {
3+
margin-left: 30px;
4+
margin-bottom: 20px;
5+
}
6+
7+
.snapshot-detail-title {
8+
width: 80px;
9+
justify-content: right;
10+
display: inline-flex;
11+
margin-right: 30px;
12+
}
13+
14+
.snapshot-detail-value {
15+
display: inline-flex;
16+
}
17+
}
18+
19+
.snapshot-tree-title {
20+
font-size: 20px;
21+
padding-left: 20px;
22+
padding-top: 40px;
23+
}
24+
25+
.no-snapshots-message {
26+
padding-left: 20px;
27+
}
28+
29+
.checkbox {
30+
font-size: 16px;
31+
user-select: none;
32+
min-height: 320px;
33+
padding: 20px;
34+
box-sizing: content-box;
35+
}
36+
37+
.selected-snapshot {
38+
background-color: #0f62fe;
39+
color: white;
40+
width: 100%;
41+
margin-left: 0px;
42+
pointer-events: none;
43+
44+
.arrow-div {
45+
pointer-events: all;
46+
}
47+
48+
span {
49+
pointer-events: all;
50+
}
51+
}
52+
53+
.arrow-div {
54+
display: inline-flex;
55+
margin-right: 5px;
56+
}
57+
58+
.node-icon-div {
59+
margin-right: 5px;
60+
display: inline-flex;
61+
}
62+
63+
.checkbox .tree,
64+
.checkbox .tree-node,
65+
.checkbox .tree-node-group {
66+
list-style: none;
67+
margin: 0;
68+
padding: 0;
69+
}
70+
71+
.checkbox .tree-branch-wrapper,
72+
.checkbox .tree-node__leaf {
73+
outline: none;
74+
}
75+
76+
.checkbox .tree-node {
77+
cursor: pointer;
78+
}
79+
80+
.checkbox .tree-node .name:hover {
81+
background: rgba(0, 0, 0, 0.1);
82+
}
83+
84+
.checkbox .tree-node--focused .name {
85+
background: rgba(0, 0, 0, 0.2);
86+
}
87+
88+
.checkbox .tree-node {
89+
display: inline-block;
90+
}
91+
92+
.checkbox .checkbox-icon {
93+
margin: 0 5px;
94+
vertical-align: middle;
95+
}
96+
97+
.checkbox button {
98+
border: none;
99+
background: transparent;
100+
cursor: pointer;
101+
}
102+
103+
.checkbox .arrow {
104+
margin-left: 5px;
105+
vertical-align: middle;
106+
}
107+
108+
.checkbox .arrow--open {
109+
transform: rotate(90deg);
110+
}

0 commit comments

Comments
 (0)