From bd60d2149bc9b53c8f0f3e40eb1a4be345eb1518 Mon Sep 17 00:00:00 2001 From: "George Gelashvili, Matt Kleiman and Oliver Switzer" Date: Thu, 20 Apr 2017 17:43:56 -0400 Subject: [PATCH 2/2] Add a tree implemented in React --- web/karma.conf.js | 27 ---- .../browser/templates/browser/js/browser.js | 44 ++---- web/pgadmin/static/jsx/components.jsx | 2 + .../jsx/tree_menu/components/server_tree.jsx | 50 +++++++ .../static/jsx/tree_menu/components/tree_node.jsx | 61 ++++++++ .../jsx/tree_menu/services/server_node_service.js | 58 ++++++++ .../tree_menu/components/server_tree_spec.jsx | 60 ++++++++ .../tree_menu/components/tree_node_spec.jsx | 116 ++++++++++++++++ .../services/server_node_service_spec.jsx | 154 +++++++++++++++++++++ 9 files changed, 513 insertions(+), 59 deletions(-) create mode 100644 web/pgadmin/static/jsx/tree_menu/components/server_tree.jsx create mode 100644 web/pgadmin/static/jsx/tree_menu/components/tree_node.jsx create mode 100644 web/pgadmin/static/jsx/tree_menu/services/server_node_service.js create mode 100644 web/regression/javascript/tree_menu/components/server_tree_spec.jsx create mode 100644 web/regression/javascript/tree_menu/components/tree_node_spec.jsx create mode 100644 web/regression/javascript/tree_menu/services/server_node_service_spec.jsx diff --git a/web/karma.conf.js b/web/karma.conf.js index d3bf3d15..55003ad6 100644 --- a/web/karma.conf.js +++ b/web/karma.conf.js @@ -60,10 +60,6 @@ module.exports = function conf(config) { } }, { - test: /.*jquery\.aci.*/, - loader: 'imports-loader?jQuery=jquery' - }, - { test: /.*slickgrid\/slick\.(?!core)*/, loader: 'imports-loader?' + 'jquery.ui' + @@ -101,27 +97,6 @@ module.exports = function conf(config) { ',jquery.event.drag' + '!exports-loader?' + 'Slick.Grid' - }, - { - test: /.*tree_menu.*/, - loader: 'imports-loader?' + - 'jquery.ui' + - ',jquery.event.drag' + - ',aciPlugin' - }, - { - test: /.*jquery\.aciPlugin.*/, - loader: 'imports-loader?' + - 'jquery.ui' + - ',jquery.event.drag' + - ',this=>window' - }, - { - test: /.*jquery\.aciTree.*/, - loader: 'imports-loader?' + - 'jquery.ui' + - ',jquery.event.drag' + - ',this=>window' } ] }, @@ -136,8 +111,6 @@ module.exports = function conf(config) { ], extensions: ['.js', '.jsx'], alias: { - 'aciPlugin': sourcesDir + '/vendor/aciTree/jquery.aciPlugin.min', - 'aciTree': sourcesDir + '/vendor/aciTree/jquery.aciTree', 'alertify': sourcesDir + '/vendor/alertifyjs/alertify', 'jquery': sourcesDir + '/vendor/jquery/jquery-1.11.2', 'jquery.ui': sourcesDir + '/vendor/jquery-ui/jquery-ui-1.11.3', diff --git a/web/pgadmin/browser/templates/browser/js/browser.js b/web/pgadmin/browser/templates/browser/js/browser.js index a663ae86..062b8554 100644 --- a/web/pgadmin/browser/templates/browser/js/browser.js +++ b/web/pgadmin/browser/templates/browser/js/browser.js @@ -1,14 +1,16 @@ define('pgadmin.browser', ['require', 'jquery', 'underscore', 'underscore.string', 'bootstrap', - 'pgadmin', 'alertify', 'codemirror', 'codemirror/mode/sql/sql', 'wcdocker', - 'jquery.contextmenu', 'jquery.aciplugin', 'jquery.acitree', + 'pgadmin', 'alertify', 'codemirror', + 'sources/generated/reactComponents', + 'codemirror/mode/sql/sql', 'wcdocker', + 'jquery.contextmenu', 'pgadmin.alertifyjs', 'pgadmin.browser.messages', 'pgadmin.browser.menu', 'pgadmin.browser.panel', 'pgadmin.browser.error', 'pgadmin.browser.frame', 'pgadmin.browser.node', 'pgadmin.browser.collection' ], -function(require, $, _, S, Bootstrap, pgAdmin, Alertify, CodeMirror) { +function(require, $, _, S, Bootstrap, pgAdmin, Alertify, CodeMirror, reactComponents) { // Some scripts do export their object in the window only. // Generally the one, which do no have AMD support. @@ -44,40 +46,18 @@ function(require, $, _, S, Bootstrap, pgAdmin, Alertify, CodeMirror) { _.each(data, function(d){ d._label = d.label; d.label = _.escape(d.label); - }) + }); return data; }; var initializeBrowserTree = pgAdmin.Browser.initializeBrowserTree = - function(b) { - $('#tree').aciTree({ - ajax: { - url: '{{ url_for('browser.get_nodes') }}', - converters: { - 'text json': processTreeData, - } - }, - ajaxHook: function(item, settings) { - if (item != null) { - var d = this.itemData(item); - n = b.Nodes[d._type]; - if (n) - settings.url = n.generate_url(item, 'children', d, true); - } - }, - loaderDelay: 100, - show: { - duration: 75 - }, - hide: { - duration: 75 - }, - view: { - duration: 75 - } - }); + function(browser) { + + serverTreeElement = reactComponents.React.createElement( + reactComponents.ServerTree, {serverGroups: ['Servers']}); + reactComponents.render(serverTreeElement, $('#tree')[0]); + - b.tree = $('#tree').aciTree('api'); }; // Extend the browser class attributes diff --git a/web/pgadmin/static/jsx/components.jsx b/web/pgadmin/static/jsx/components.jsx index 63b72535..edf99e42 100644 --- a/web/pgadmin/static/jsx/components.jsx +++ b/web/pgadmin/static/jsx/components.jsx @@ -1,7 +1,9 @@ +import {ServerTree} from "./tree_menu/components/server_tree.jsx"; import {render} from "react-dom"; import React from "react"; export { + ServerTree, render, React } \ No newline at end of file diff --git a/web/pgadmin/static/jsx/tree_menu/components/server_tree.jsx b/web/pgadmin/static/jsx/tree_menu/components/server_tree.jsx new file mode 100644 index 00000000..7f81cd40 --- /dev/null +++ b/web/pgadmin/static/jsx/tree_menu/components/server_tree.jsx @@ -0,0 +1,50 @@ +import React from "react"; +import {ServerNodeService} from "../services/server_node_service"; +import {TreeNode} from "./tree_node"; + +export class ServerTree extends React.Component { + + constructor(props) { + super(props); + this.state = { + expanded: false, + serverGroups: [] + }; + self = this; + + ServerNodeService.fetchChildrenFrom({nodeType: "nodes", nodeId: null}).then((nodes) => { + self.setState({serverGroups: nodes}) + }); + } + + render() { + return ( + ); + } +} + +ServerTree.propTypes = { +}; + + + // ); + diff --git a/web/pgadmin/static/jsx/tree_menu/components/tree_node.jsx b/web/pgadmin/static/jsx/tree_menu/components/tree_node.jsx new file mode 100644 index 00000000..aa104e18 --- /dev/null +++ b/web/pgadmin/static/jsx/tree_menu/components/tree_node.jsx @@ -0,0 +1,61 @@ +import React from "react"; +import {ServerNodeService} from "../services/server_node_service" + +export class TreeNode extends React.Component { + + constructor(props) { + super(props); + this.state = { + name: props.name, + nodeType: props.nodeType, + nodeId: props.nodeId, + expanded: false, + childNodes: [] + }; + this.onToggle = this.onToggle.bind(this); + + this.styles = { + children: { + + marginLeft: "-20px" + } + } + } + + onToggle() { + let callArgs = {nodeType: this.props.nodeType, nodeId: this.props.nodeId}; + ServerNodeService.fetchChildrenFrom(callArgs).then((newChildren) => { + this.setState({childNodes: newChildren, expanded: !this.state.expanded}); + }); + } + + displayChildNodes() { + if(this.state.expanded) { + return ( + + ); + } + } + + render() { + return ( +
+
{this.state.name}
+
{this.displayChildNodes()}
+
); + } +} + +TreeNode.propTypes = { + name: React.PropTypes.string.isRequired, + nodeType: React.PropTypes.string.isRequired, + nodeId: React.PropTypes.number.isRequired +}; diff --git a/web/pgadmin/static/jsx/tree_menu/services/server_node_service.js b/web/pgadmin/static/jsx/tree_menu/services/server_node_service.js new file mode 100644 index 00000000..839734b4 --- /dev/null +++ b/web/pgadmin/static/jsx/tree_menu/services/server_node_service.js @@ -0,0 +1,58 @@ +////////////////////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2017, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////////////////// + +import axios from 'axios'; + +// baseUrl = "/browser"; + +export class ServerNodeService { + static fetchChildrenFrom(parent) { + return axios.get(ServerNodeService.buildUrl(parent)) + .then((response) => { + return ServerNodeService.createNodeChildren(response['data']) + }) + .catch((error) => { + if (error.response) { + ServerNodeService.errorCallback(error.response.errormsg); + } + }); + }; + + static baseUrl() { + return "/browser"; + }; + + static buildUrl(parent) { + let url = ServerNodeService.baseUrl() + '/' + parent.nodeType; + if (parent.nodeType != 'nodes'){ + url += '/children/'; + if (parent.nodeType != 'server-group') { + url += '1/' + } + url += parent.nodeId; + } + return url; + }; + + static createNodeChildren(response) { + let nodeChildren = _.map(response['data'], function (datum) { + let changedNodeId = parseInt(datum['id'].split('/').pop()); + return { + name: datum['label'], + nodeType: datum['_type'], + nodeId: changedNodeId + } + }); + return nodeChildren; + }; + + static setErrorCallback(callback) { + ServerNodeService.errorCallback = callback; + }; +} diff --git a/web/regression/javascript/tree_menu/components/server_tree_spec.jsx b/web/regression/javascript/tree_menu/components/server_tree_spec.jsx new file mode 100644 index 00000000..79ba94d9 --- /dev/null +++ b/web/regression/javascript/tree_menu/components/server_tree_spec.jsx @@ -0,0 +1,60 @@ +////////////////////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2017, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////////////////// + +import React from "react"; +import {ServerTree} from "../../../../pgadmin/static/jsx/tree_menu/components/server_tree"; +import {TreeNode} from "../../../../pgadmin/static/jsx/tree_menu/components/tree_node"; +import { mount } from 'enzyme'; +import {ServerNodeService} from "../../../../pgadmin/static/jsx/tree_menu/services/server_node_service.js"; + + +describe("ServerTree", () => { + let topLevelResponse; + beforeEach(() => { + topLevelResponse = [{ + name: 'Servers', + nodeType: 'server-group', + nodeId: 1, + }]; + + spyOn(ServerNodeService, 'fetchChildrenFrom').and.returnValue(new Promise((resolve) => { + resolve(topLevelResponse) + })); + }); + + it("initializes the top TreeNode with [nodes and null nodeId]"); + + it("renders the top Tree Node as closed", (done) => { + + const serverTree = mount( + ); + + setImmediate(() => { + expect(serverTree.find("TreeNode").length).toBe(1); + done(); + }); + }); + + it("renders the top Tree Node as 'Servers'", (done) => { + + const serverTree = mount( + ); + + setImmediate(() => { + let treeNode = serverTree.find("TreeNode")//.childAt(0); + expect(treeNode.text()).toBe("Servers"); + done(); + }); + }); + + describe("when there are multiple Server Groups", () => { + it("renders multiple Server Group TreeNodes"); + }); +}); \ No newline at end of file diff --git a/web/regression/javascript/tree_menu/components/tree_node_spec.jsx b/web/regression/javascript/tree_menu/components/tree_node_spec.jsx new file mode 100644 index 00000000..3ee1fab3 --- /dev/null +++ b/web/regression/javascript/tree_menu/components/tree_node_spec.jsx @@ -0,0 +1,116 @@ +////////////////////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2017, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////////////////// + +import React from "react"; +import {TreeNode} from "../../../../pgadmin/static/jsx/tree_menu/components/tree_node"; +import {ServerNodeService} from "../../../../pgadmin/static/jsx/tree_menu/services/server_node_service.js"; +import {mount} from 'enzyme'; + +describe("TreeNode", () => { + + it("renders itself as closed", () => { + const treeNode = mount(); + + expect(treeNode.hasClass('closed')).toBe(true); + expect(treeNode.find("TreeNode").length).toBe(1); + }); + + it("renders itself as its constructed name prop", () => { + const treeNode = mount(); + + expect(treeNode.text()).toBe("some server"); + }); + + describe("when the user opens the tree node", () => { + let treeNode; + + beforeEach(function () { + treeNode = mount(); + let response = [ + { + name: "a server", + nodeType: "server", + nodeId: 1 + }, + { + name: "another server", + nodeType: "server", + nodeId: 2 + } + ]; + + spyOn(ServerNodeService, 'fetchChildrenFrom').and.returnValue( + new Promise((resolve) => { + resolve(response) + }) + ); + }); + + it("calls service to fetch children correctly", () => { + + treeNode.find('.node-name').simulate('click'); + + let parentArgs = {nodeType: "server-group", nodeId: 1}; + expect(ServerNodeService.fetchChildrenFrom).toHaveBeenCalledWith(parentArgs); + }); + + it("renders the node as opened", () => { + + treeNode.find('.node-name').simulate('click'); + + setImmediate(() => { + expect(treeNode.hasClass('closed')).toBe(false); + expect(treeNode.hasClass('opened')).toBe(true); + }); + }); + + it("renders tree node children with names", () => { + treeNode.find('.node-name').simulate('click'); + + setImmediate(() => { + expect(treeNode.find('TreeNode').length).toBe(1 + 2); + expect(treeNode.text()).toContain("a server"); + expect(treeNode.text()).toContain("another server"); + }); + }); + + describe("when the user clicks again on the node", function () { + it("should close it", function (done) { + treeNode.find('.node-name').simulate('click'); + treeNode.find('.node-name').simulate('click'); + setImmediate(() => { + expect(treeNode.find("TreeNode").length).toBe(1); + expect(treeNode.hasClass('closed')).toBe(true); + expect(treeNode.hasClass('opened')).toBe(false); + done(); + }); + }); + }); + + it("renders the node children as 'loading...'"); + describe("when the data loading succeeds", () => { + + it("renders tree node children with icons corresponding to their types", () => { + + }); + }); + }); +}); \ No newline at end of file diff --git a/web/regression/javascript/tree_menu/services/server_node_service_spec.jsx b/web/regression/javascript/tree_menu/services/server_node_service_spec.jsx new file mode 100644 index 00000000..7aac0e0e --- /dev/null +++ b/web/regression/javascript/tree_menu/services/server_node_service_spec.jsx @@ -0,0 +1,154 @@ +////////////////////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2017, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////////////////// + +import {ServerNodeService} from "../../../../pgadmin/static/jsx/tree_menu/services/server_node_service"; +import axios from 'axios'; + +describe('ServerNodeService', () => { + describe('fetchChildrenFrom', () => { + let ajaxResponse, url, parentNode; + + beforeEach(function () { + parentNode = {nodeType: "parentType", nodeId: 1}; + url = '/browser/nodes/'; + + ajaxResponse = { + data: { + info: "", + errormsg: "", + data: [ + { + _type: "type1", + in_recovery: null, + server_type: "pg", + db: "postgres", + module: "module.one", + connected: false, + user: null, + inode: true, + id: "longer/path/to/server/1", + icon: "icon-server-not-connected", + _pid: 1, + label: "label one", + version: null, + _id: "1", + wal_pause: null + }, + { + _type: "type2", + in_recovery: null, + server_type: "pg", + db: "postgres", + module: "module.two", + connected: false, + user: null, + inode: true, + id: "longer/path/to/server/2", + icon: "icon-server-not-connected", + _pid: 1, + label: "label two", + version: null, + _id: "1", + wal_pause: null + } + ], + result: null, + success: 1 + } + }; + }); + + it("it fetches the browser node"); + + describe("calls the right place", () => { + beforeEach(function () { + spyOn(axios, 'get').and.returnValue(new Promise(function (resolve, reject) { + })); + }); + + it("when called for the top level", () => { + + let parentArgs = {nodeType: "nodes", nodeId: null}; + ServerNodeService.fetchChildrenFrom(parentArgs); + + let expectedUrl = "/browser/nodes"; + expect(axios.get).toHaveBeenCalledWith(expectedUrl); + }); + + it("when called for the second level", () => { + let parentArgs = {nodeType: "server-group", nodeId: 3}; + + ServerNodeService.fetchChildrenFrom(parentArgs); + + let expectedUrl = "/browser/server-group/children/3"; + expect(axios.get).toHaveBeenCalledWith(expectedUrl); + }); + + it("when called for deeper nodes", () => { + let parentArgs = {nodeType: "server", nodeId: 3}; + + ServerNodeService.fetchChildrenFrom(parentArgs); + + let expectedUrl = "/browser/server/children/1/3"; + expect(axios.get).toHaveBeenCalledWith(expectedUrl); + }); + + }); + + describe("when backend is not connected to server", () => { + it("calls a callback with the error message", (done) => { + let ajaxErrorResponse = { + response: { + info: "", + errormsg: "Connection to the server has been lost.", + data: null, + result: null, + success: 0 + } + }; + + spyOn(axios, 'get').and.returnValue(new Promise((resolve, reject) => { + reject(ajaxErrorResponse) + })); + + let errorCallback = jasmine.createSpy('errorCallback'); + ServerNodeService.setErrorCallback(errorCallback); + + ServerNodeService.fetchChildrenFrom(parentNode).then(() => { + expect(errorCallback).toHaveBeenCalledWith("Connection to the server has been lost."); + done(); + }); + }); + }); + + describe("when the call is successful", () => { + it("converts the data into the expected format", (done) => { + spyOn(axios, 'get').and.returnValue(new Promise((resolve) => { + resolve(ajaxResponse) + })); + + let expectedResult = [{ + name: "label one", + nodeType: "type1", + nodeId: 1 + }, { + name: "label two", + nodeType: "type2", + nodeId: 2 + }]; + + ServerNodeService.fetchChildrenFrom(parentNode).then((children) => { + expect(children).toEqual(expectedResult); + done(); + }); + }); + }); + + }); +}); -- 2.12.0