From 444ef60368c12810f81a15a5a2f46f27039d4551 Mon Sep 17 00:00:00 2001 From: JEFFREY-Bonson Date: Thu, 25 Jan 2024 20:19:08 +0530 Subject: [PATCH] Embedded method tree conversion --- .../components/AeInlineMethods/index.jsx | 96 +++++++++++++++ .../MiqTreeView/MiqTreeChildNode.jsx | 46 +++++++ .../components/MiqTreeView/MiqTreeNode.jsx | 54 +++++++++ .../MiqTreeView/MiqTreeParentNode.jsx | 49 ++++++++ .../components/MiqTreeView/helper.js | 19 +++ .../components/MiqTreeView/index.jsx | 112 ++++++++++++++++++ .../components/MiqTreeView/style.scss | 43 +++++++ .../packs/component-definitions-common.js | 2 + .../miq_ae_class/_class_instances.html.haml | 2 + 9 files changed, 423 insertions(+) create mode 100644 app/javascript/components/AeInlineMethods/index.jsx create mode 100644 app/javascript/components/MiqTreeView/MiqTreeChildNode.jsx create mode 100644 app/javascript/components/MiqTreeView/MiqTreeNode.jsx create mode 100644 app/javascript/components/MiqTreeView/MiqTreeParentNode.jsx create mode 100644 app/javascript/components/MiqTreeView/helper.js create mode 100644 app/javascript/components/MiqTreeView/index.jsx create mode 100644 app/javascript/components/MiqTreeView/style.scss diff --git a/app/javascript/components/AeInlineMethods/index.jsx b/app/javascript/components/AeInlineMethods/index.jsx new file mode 100644 index 00000000000..f4a12d72cf2 --- /dev/null +++ b/app/javascript/components/AeInlineMethods/index.jsx @@ -0,0 +1,96 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { Modal, Button, ModalBody } from 'carbon-components-react'; +import MiqTree from '../MiqTreeView'; + +/** Component to render a tree and to select an embedded method. */ +const AeInlineMethod = ({ type }) => { + const [data, setData] = useState({ + isModalOpen: false, + selectedNode: undefined, + list: [], + }); + + /** Function to show/hide the modal. */ + const showModal = (status) => { + setData({ + ...data, + isModalOpen: status, + }); + }; + + /** Function to render the Add method button. */ + const renderAddButton = () => ( + + ); + + console.log(data); + + const renderList = () => (data.list.map((item) => ( +
+
{item.fqname}
+
+ ))); + + return ( +
+ {renderAddButton()} + {renderList()} + showModal(false)} + onRequestSubmit={() => { + console.log('on onRequestSubmit'); + setData({ + ...data, + list: data.list.push(data.selectedNode), + }); + showModal(false); + }} + onSecondarySubmit={() => { + console.log('on onSecondarySubmit'); + showModal(false); + }} + > + + { + data.isModalOpen + && ( + { + setData({ + ...data, + selectedNode: item, + }); + }} + /> + ) + } + + + +
+ ); +}; + +export default AeInlineMethod; + +AeInlineMethod.propTypes = { + type: PropTypes.string.isRequired, +}; diff --git a/app/javascript/components/MiqTreeView/MiqTreeChildNode.jsx b/app/javascript/components/MiqTreeView/MiqTreeChildNode.jsx new file mode 100644 index 00000000000..39f0384c413 --- /dev/null +++ b/app/javascript/components/MiqTreeView/MiqTreeChildNode.jsx @@ -0,0 +1,46 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +/* eslint-disable import/no-cycle */ +import React from 'react'; +import PropTypes from 'prop-types'; +import MiqTreeNode from './MiqTreeNode'; + +/** A component to render the parent node of the tree. */ +const MiqTreeChildNode = ({ + node, onSelect, selectedNode, selectKey, +}) => ( +
+ { + node.nodes.map((item) => ( + onSelect(childItem)} + /> + )) + } +
+); + +export default MiqTreeChildNode; + +MiqTreeChildNode.propTypes = { + node: PropTypes.shape({ + key: PropTypes.string, + nodes: PropTypes.arrayOf(PropTypes.any), + state: PropTypes.shape({ + expanded: PropTypes.bool, + }), + }).isRequired, + selectKey: PropTypes.string.isRequired, + onSelect: PropTypes.func.isRequired, + selectedNode: PropTypes.shape({ + key: PropTypes.string, + }), +}; + +MiqTreeChildNode.defaultProps = { + selectedNode: undefined, +}; diff --git a/app/javascript/components/MiqTreeView/MiqTreeNode.jsx b/app/javascript/components/MiqTreeView/MiqTreeNode.jsx new file mode 100644 index 00000000000..d47bdc8d44a --- /dev/null +++ b/app/javascript/components/MiqTreeView/MiqTreeNode.jsx @@ -0,0 +1,54 @@ +/* eslint-disable import/no-cycle */ +import React from 'react'; +import PropTypes from 'prop-types'; +import MiqTreeParentNode from './MiqTreeParentNode'; +import MiqTreeChildNode from './MiqTreeChildNode'; + +/** A Recursive Functional component to render the Tree and its child nodes. */ +const MiqTreeNode = ({ + node, selectedNode, selectKey, onSelect, +}) => { + const isSelected = (selectedNode && selectedNode.key === node.key) || false; + + return ( +
+ onSelect(parentItem)} + /> + { + node.state.expanded && node.nodes && node.nodes.length > 0 && ( + onSelect(childItem)} + selectedNode={selectedNode} + selectKey={selectKey} + /> + ) + } +
+ ); +}; + +export default MiqTreeNode; + +MiqTreeNode.propTypes = { + node: PropTypes.shape({ + key: PropTypes.string, + nodes: PropTypes.arrayOf(PropTypes.any), + state: PropTypes.shape({ + expanded: PropTypes.bool, + }), + }).isRequired, + selectKey: PropTypes.string.isRequired, + onSelect: PropTypes.func.isRequired, + selectedNode: PropTypes.shape({ + key: PropTypes.string, + }), +}; + +MiqTreeNode.defaultProps = { + selectedNode: undefined, +}; diff --git a/app/javascript/components/MiqTreeView/MiqTreeParentNode.jsx b/app/javascript/components/MiqTreeView/MiqTreeParentNode.jsx new file mode 100644 index 00000000000..01f4ab9b4ae --- /dev/null +++ b/app/javascript/components/MiqTreeView/MiqTreeParentNode.jsx @@ -0,0 +1,49 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { CaretRight16, CaretDown16, CheckmarkFilled16 } from '@carbon/icons-react'; +import { selectableItem } from './helper'; + +/** A component to render the parent node of the tree. */ +const MiqTreeParentNode = ({ + node, selectKey, isSelected, onSelect, +}) => { + /** Function to render the down and right caret. */ + const renderCaret = (item) => { + if (!item) { + return undefined; + } + if (selectableItem(item, selectKey) || !item.lazyLoad) { + return undefined; + } + return item.state.expanded ? : ; + }; + + return ( +
onSelect(node)}> + {renderCaret(node)} +
+
{node.text}
+ {isSelected && } +
+ ); +}; + +export default MiqTreeParentNode; + +MiqTreeParentNode.propTypes = { + node: PropTypes.shape({ + icon: PropTypes.string, + text: PropTypes.string, + key: PropTypes.string, + nodes: PropTypes.arrayOf(PropTypes.any), + state: PropTypes.shape({ + expanded: PropTypes.bool, + }), + }).isRequired, + selectKey: PropTypes.string.isRequired, + isSelected: PropTypes.bool.isRequired, + onSelect: PropTypes.func.isRequired, +}; diff --git a/app/javascript/components/MiqTreeView/helper.js b/app/javascript/components/MiqTreeView/helper.js new file mode 100644 index 00000000000..7fe5274ad05 --- /dev/null +++ b/app/javascript/components/MiqTreeView/helper.js @@ -0,0 +1,19 @@ +export const TREE_CONFIG = { + aeInlineMethod: { + url: '/tree/automate_inline_methods', + selectKey: 'aem', + }, +}; + +/** Function to find the selected item from the tree data. */ +export const findNodeByKey = (array, keyToFind) => { + const flattenedArray = array.flatMap((item) => [item, ...(item.nodes || [])]); + const foundNode = flattenedArray.find((item) => item.key === keyToFind); + + return foundNode || flattenedArray + .filter((item) => item.nodes && item.nodes.length > 0) + .map((item) => findNodeByKey(item.nodes, keyToFind)) + .find(Boolean); +}; + +export const selectableItem = (child, selectKey) => child.key.split('-')[0] === selectKey; diff --git a/app/javascript/components/MiqTreeView/index.jsx b/app/javascript/components/MiqTreeView/index.jsx new file mode 100644 index 00000000000..f60e1c1405f --- /dev/null +++ b/app/javascript/components/MiqTreeView/index.jsx @@ -0,0 +1,112 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { Loading } from 'carbon-components-react'; +import MiqTreeNode from './MiqTreeNode'; +import { TREE_CONFIG, selectableItem, findNodeByKey } from './helper'; +import './style.scss'; + +const MiqTree = ({ type, onNodeSelect }) => { + const treeType = TREE_CONFIG[type]; + + const [data, setData] = useState({ + list: undefined, + isLoading: true, + selectedNode: undefined, + }); + + /** Function to update the data. */ + const updateData = (others) => { + setData({ + ...data, + ...others, + }); + }; + + /** A request is made to fetch the initial data during component load. */ + useEffect(() => { + http.get(treeType.url) + .then((response) => updateData({ list: response, isLoading: false })); + }, []); + + useEffect(() => { + onNodeSelect(data.selectedNode); + }, [data.selectedNode]); + + /** Function to select a node from tree. This triggers the useEffect. */ + const selectNode = (node) => { + const selectedNode = (data.selectedNode && data.selectedNode.key === node.key) ? undefined : node; + updateData({ selectedNode }); + }; + + /** Function to handle show the children of a node + * if child nodes are available, just expand the tree. + * else, request an API to fetch the child nodes and update the results. + */ + const expandTree = (item, node) => { + if (item.nodes && item.nodes.length > 0) { + item.state.expanded = true; + updateData({ list: [...data.list] }); + } else { + http.get(`${treeType.url}?id=${node.key}`) + .then((response) => { + item.nodes = response; + item.state.expanded = true; + updateData({ list: [...data.list] }); + }); + } + }; + + /** Function to collapse the tree to hide the children */ + const collapseTree = (item) => { + item.state.expanded = false; + updateData({ list: [...data.list] }); + }; + + /** Function to expand/collapse the tree. */ + const toggleTree = (node) => { + const item = findNodeByKey(data.list, node.key); + if (item) { + if (node.state.expanded) { + collapseTree(item); + } else { + expandTree(item, node); + } + } + }; + + /** Function to handle the click events of tree node. */ + const loadSelectedNode = (node) => (selectableItem(node, treeType.selectKey) + ? selectNode(node) + : toggleTree(node)); + + /** Function to render the tree contents. */ + const renderTree = (list) => (list && list.map((child) => ( + loadSelectedNode(node)} + /> + ))); + + /** Function to render the modal contents. */ + const renderTreeContent = () => ((data.list && data.list.length > 0) ? renderTree(data.list) : undefined); + + return ( +
+ { + data.isLoading + ? + : renderTreeContent() + } +
+ ); +}; + +export default MiqTree; + +MiqTree.propTypes = { + type: PropTypes.string.isRequired, + onNodeSelect: PropTypes.func.isRequired, +}; diff --git a/app/javascript/components/MiqTreeView/style.scss b/app/javascript/components/MiqTreeView/style.scss new file mode 100644 index 00000000000..0ea22775911 --- /dev/null +++ b/app/javascript/components/MiqTreeView/style.scss @@ -0,0 +1,43 @@ +.tree-row { + padding: 5px; + + &.parent-tree { + display: flex; + align-items: center; + gap: 5px; + cursor: pointer; + + &:hover { + background: lightblue; + } + + &.selected-node{ + background: #0f62fe; + color:#FFF; + + .tree-icon { + background: #FFF; + } + + .selected-node-check { + color: lightgreen; + } + } + } + + &.intend-right { + margin-left: 20px; + } + + + .tree-caret { + cursor: pointer; + width: 15px; + height: 15px; + } + + .tree-text { + display: flex; + flex-grow: 1; + } +} diff --git a/app/javascript/packs/component-definitions-common.js b/app/javascript/packs/component-definitions-common.js index a60f2a5c4fa..582c5863d7d 100644 --- a/app/javascript/packs/component-definitions-common.js +++ b/app/javascript/packs/component-definitions-common.js @@ -11,6 +11,7 @@ import { Toolbar } from '../components/toolbar'; import ActionForm from '../components/action-form'; import AddRemoveHostAggregateForm from '../components/host-aggregate-form/add-remove-host-aggregate-form'; import AddRemoveSecurityGroupForm from '../components/vm-cloud-add-remove-security-group-form'; +import AeInlineMethod from '../components/AeInlineMethods'; import AggregateStatusCard from '../components/aggregate_status_card'; import AnsibleCredentialsForm from '../components/ansible-credentials-form'; import AnsiblePlayBookEditCatalogForm from '../components/ansible-playbook-edit-catalog-form'; @@ -180,6 +181,7 @@ import ZoneForm from '../components/zone-form'; ManageIQ.component.addReact('ActionForm', ActionForm); ManageIQ.component.addReact('AddRemoveHostAggregateForm', AddRemoveHostAggregateForm); ManageIQ.component.addReact('AddRemoveSecurityGroupForm', AddRemoveSecurityGroupForm); +ManageIQ.component.addReact('AeInlineMethod', AeInlineMethod); ManageIQ.component.addReact('AggregateStatusCard', AggregateStatusCard); ManageIQ.component.addReact('AnsibleCredentialsForm', AnsibleCredentialsForm); ManageIQ.component.addReact('AnsiblePlayBookEditCatalogForm', AnsiblePlayBookEditCatalogForm); diff --git a/app/views/miq_ae_class/_class_instances.html.haml b/app/views/miq_ae_class/_class_instances.html.haml index c4b0b3e0eba..57a9b204262 100644 --- a/app/views/miq_ae_class/_class_instances.html.haml +++ b/app/views/miq_ae_class/_class_instances.html.haml @@ -1,3 +1,5 @@ += react('AeInlineMethod', {:type => "aeInlineMethod"}) + #class_instances_div - unless @angular_form - if !@in_a_form