Skip to content

Commit d88660c

Browse files
Convert vm snapshot page to react
1 parent c2bd3bf commit d88660c

File tree

11 files changed

+4009
-42
lines changed

11 files changed

+4009
-42
lines changed

app/controllers/vm_common.rb

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -298,15 +298,14 @@ 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+
number_to_human_size(@snap_selected[:total_size], :precision => 2)
303+
if @snap_selected[:total_size] == nil || @snap_selected[:total_size] == 0
304+
formatted_size = ''
305+
else
306+
formatted_size = _("%{number} bytes") % {:number => number_with_delimiter(@snap_selected[:total_size], :delimiter => ",", :separator => ".")}
309307
end
308+
render :json => {:data => {:data => @snap_selected, :size => formatted_size, :time => formatted_time}}, :status => 200
310309
end
311310

312311
def disks
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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, selected, size, time,
8+
}) => {
9+
const [snapshot, setSnapshot] = useState({ ...selected, 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+
{snapshot.data ? snapshot.data.description : snapshot.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+
{snapshot.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+
{snapshot.time || ''}
49+
</div>
50+
</div>
51+
</div>
52+
<SnapshotTree nodes={tree.tree_nodes} setSnapshot={setSnapshot} />
53+
</div>
54+
);
55+
};
56+
57+
VMSnapshotTreeSelect.propTypes = {
58+
tree: PropTypes.objectOf(PropTypes.any).isRequired,
59+
selected: PropTypes.objectOf(PropTypes.any),
60+
size: PropTypes.string,
61+
time: PropTypes.string,
62+
};
63+
64+
VMSnapshotTreeSelect.defaultProps = {
65+
selected: {},
66+
size: '',
67+
time: '',
68+
};
69+
70+
export default VMSnapshotTreeSelect;
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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, setSnapshot }) => {
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+
setSnapshot(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+
setSnapshot: PropTypes.func.isRequired,
172+
};
173+
174+
SnapshotTree.defaultProps = {
175+
};
176+
177+
export default SnapshotTree;
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
#size-title {
15+
}
16+
17+
#created-title {
18+
}
19+
20+
.snapshot-detail-value {
21+
display: inline-flex;
22+
}
23+
}
24+
25+
.checkbox {
26+
font-size: 16px;
27+
user-select: none;
28+
min-height: 320px;
29+
padding: 20px;
30+
box-sizing: content-box;
31+
}
32+
33+
.selected-snapshot {
34+
background-color: #0f62fe;
35+
color: white;
36+
width: 100%;
37+
margin-left: 0px;
38+
pointer-events: none;
39+
40+
.arrow-div {
41+
pointer-events: all;
42+
}
43+
44+
span {
45+
pointer-events: all;
46+
}
47+
}
48+
49+
.arrow-div {
50+
display: inline-flex;
51+
margin-right: 5px;
52+
}
53+
54+
.node-icon-div {
55+
margin-right: 5px;
56+
display: inline-flex;
57+
}
58+
59+
.checkbox .tree,
60+
.checkbox .tree-node,
61+
.checkbox .tree-node-group {
62+
list-style: none;
63+
margin: 0;
64+
padding: 0;
65+
}
66+
67+
.checkbox .tree-branch-wrapper,
68+
.checkbox .tree-node__leaf {
69+
outline: none;
70+
}
71+
72+
.checkbox .tree-node {
73+
cursor: pointer;
74+
}
75+
76+
.checkbox .tree-node .name:hover {
77+
background: rgba(0, 0, 0, 0.1);
78+
}
79+
80+
.checkbox .tree-node--focused .name {
81+
background: rgba(0, 0, 0, 0.2);
82+
}
83+
84+
.checkbox .tree-node {
85+
display: inline-block;
86+
}
87+
88+
.checkbox .checkbox-icon {
89+
margin: 0 5px;
90+
vertical-align: middle;
91+
}
92+
93+
.checkbox button {
94+
border: none;
95+
background: transparent;
96+
cursor: pointer;
97+
}
98+
99+
.checkbox .arrow {
100+
margin-left: 5px;
101+
vertical-align: middle;
102+
}
103+
104+
.checkbox .arrow--open {
105+
transform: rotate(90deg);
106+
}

app/javascript/packs/component-definitions-common.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ import VmFloatingIPsForm from '../components/vm-floating-ips/vm-floating-ips-for
162162
import VmResizeForm from '../components/vm-resize-form/vm-resize-form';
163163
import VmServerRelationshipForm from '../components/vm-server-relationship-form';
164164
import VmSnapshotForm from '../components/vm-snapshot-form/vm-snapshot-form';
165+
import VmSnapshotTreeSelect from '../components/vm-snapshot-tree-select';
165166
import VolumeMappingForm from '../components/volume-mapping-form';
166167
import WidgetChart from '../components/dashboard-widgets/widget-chart';
167168
import WidgetError from '../components/dashboard-widgets/widget-error';
@@ -350,6 +351,7 @@ ManageIQ.component.addReact('VmFloatingIPsForm', VmFloatingIPsForm);
350351
ManageIQ.component.addReact('VmResizeForm', VmResizeForm);
351352
ManageIQ.component.addReact('VmServerRelationshipForm', VmServerRelationshipForm);
352353
ManageIQ.component.addReact('VmSnapshotForm', VmSnapshotForm);
354+
ManageIQ.component.addReact('VmSnapshotTreeSelect', VmSnapshotTreeSelect);
353355
ManageIQ.component.addReact('VolumeMappingForm', VolumeMappingForm);
354356
ManageIQ.component.addReact('WidgetChart', WidgetChart);
355357
ManageIQ.component.addReact('WidgetError', WidgetError);

0 commit comments

Comments
 (0)