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) => (
+
+ )));
+
+ 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