-
Notifications
You must be signed in to change notification settings - Fork 365
Convert vm snapshot form #9639
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Convert vm snapshot form #9639
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -298,15 +298,14 @@ 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") | ||
number_to_human_size(@snap_selected[:total_size], :precision => 2) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line isn't doing anything on its own. Should this be used below in the formatted bytes section? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just copied the original code for this part. Is the original code wrong here?
|
||
if @snap_selected[:total_size] == nil || @snap_selected[:total_size] == 0 | ||
formatted_size = '' | ||
else | ||
formatted_size = _("%{number} bytes") % {:number => number_with_delimiter(@snap_selected[:total_size], :delimiter => ",", :separator => ".")} | ||
end | ||
render :json => {:data => {:data => @snap_selected, :size => formatted_size, :time => formatted_time}}, :status => 200 | ||
end | ||
|
||
def disks | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import React, { useState } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import './styles.css'; | ||
import SnapshotTree from './snapshot-tree'; | ||
|
||
const VMSnapshotTreeSelect = ({ | ||
tree, selected, size, time, | ||
}) => { | ||
const [snapshot, setSnapshot] = useState({ ...selected, 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"> | ||
{snapshot.data ? snapshot.data.description : snapshot.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"> | ||
{snapshot.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"> | ||
{snapshot.time || ''} | ||
</div> | ||
</div> | ||
</div> | ||
<SnapshotTree nodes={tree.tree_nodes} setSnapshot={setSnapshot} /> | ||
</div> | ||
); | ||
}; | ||
|
||
VMSnapshotTreeSelect.propTypes = { | ||
tree: PropTypes.objectOf(PropTypes.any).isRequired, | ||
selected: PropTypes.objectOf(PropTypes.any), | ||
size: PropTypes.string, | ||
time: PropTypes.string, | ||
}; | ||
|
||
VMSnapshotTreeSelect.defaultProps = { | ||
selected: {}, | ||
size: '', | ||
time: '', | ||
}; | ||
|
||
export default VMSnapshotTreeSelect; |
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, setSnapshot }) => { | ||
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; | ||
setSnapshot(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, | ||
setSnapshot: PropTypes.func.isRequired, | ||
}; | ||
|
||
export default SnapshotTree; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
.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; | ||
} | ||
} | ||
|
||
.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); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be in the users time zone?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or should this be in UTC and we convert on the client side?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line converts it to the user's timezone in the MIQ settings, converting timezones is messy on JS and would be difficult to match the original formatting which is why I kept it on the ruby side