public inbox for [email protected]
help / color / mirror / Atom feed[pgadmin4][patch] Initial patch to decouple from ACI Tree
69+ messages / 10 participants
[nested] [flat]
* [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-02 18:45 Joao De Almeida Pereira <[email protected]>
0 siblings, 2 replies; 69+ messages in thread
From: Joao De Almeida Pereira @ 2018-04-02 18:45 UTC (permalink / raw)
To: pgadmin-hackers
Hi Hackers,
Attached you can find the patch that will start to decouple pgAdmin from
ACITree library.
This patch is intended to be merged after 3.0, because we do not want to
cause any entropy or delay the release, but we want to start the discussion
and show some code.
This job that we started is a massive tech debt chore that will take some
time to finalize and we would love the help of the community to do it.
*Summary of the patch:*
0001 patch:
- Creates a new tree that will allow us to create a separation between the
application and ACI Tree
- Creates a Fake Tree (Test double, for reference on the available test
doubles: https://martinfowler.com/bliki/TestDouble.html) that can be used
to inplace to replace the ACITree and also encapsulate the new tree
behavior on our tests
- Adds tests for all the tree functionalities
0002 patch:
- Extracts, refactors, adds tests and remove dependency from ACI Tree on:
- getTreeNodeHierarchy
- on backup.js: menu_enabled, menu_enabled_server,
start_backup_global_server, backup_objects
- on datagrid.js: show_data_grid, get_panel_title, show_query_tool
- Start using sprintf-js as Underscore.String is deprecating sprintf
function
This patch represents only 10 calls to ACITree.itemData out of 176 that are
spread around our code
*In Depth look on the process behind the patch:*
We started writing this patch with the idea that we need to decouple
pgAdmin4 from ACITree, because ACITree is no longer supported, the
documentation is non existent and ACITree is no longer being actively
developed.
Our process:
1. We "randomly" selected a function that is part of the ACITree. From this
point we decided to replace that function with our own version. The
function that we choose was "itemData".
The function gives us all the "data" that a specific node of the tree find.
Given in order to replace the tree we would need to have a function that
would give us the same information. We had 2 options:
a) Create a tree with a function called itemData
Pros:
- At first view this was the simpler solution
- Would keep the status quo
Cons:
- Not a OOP approach
- Not very flexible
b) Create a tree that would return a node given an ID and then the node
would be responsible for giving it's data.
Pros:
- OOP Approach
- More flexible and we do not need to bring the tree around, just a node
Cons:
- Break the current status quo
Given these 2 options we decided to go for a more OOP approach creating a
Tree and a TreeNode classes, that in the future will be renamed to
ACITreeWrapper and TreeNode.
2. After we decided on the starting point we searched for occurrences of
the function "itemData" and we found out that there were 303 occurrences of
"itemData" in the code and roughly 176 calls to the function itself (some
of the hits were variable names).
3. We selected the first file on the search and found the function that was
responsible for calling the itemData function.
4. Extracted the function to a separate file
5. Wrap this function with tests
6. Refactor the function to ES6, give more declarative names to variables
and break the functions into smaller chunks
7. When all the tests were passing we replaced ACITree with our Tree
8. We ensured that all tests were passing
9. Remove function from the original file and use the new function
10. Ensure everything still works
11. Find the next function and execute from step 4 until all the functions
are replaced, refactored and tested.
As you can see by the process this is a pretty huge undertake, because of
the number of calls to the function. This is just the first step on the
direction of completely isolating the ACITree so that we can solve the
problem with a large number of elements on the tree.
*What is on our radar that we need to address:*
- Finish the complete decoupling of the ACITree
- Performance of the current tree implementation
- Tweak the naming of the Tree class to explicitly tell us this is to use
only with ACITree.
Thanks
Joao
Attachments:
[application/octet-stream] 0001-Tree-of-information-on-the-current-status-of-the-app.patch (14.1K, 3-0001-Tree-of-information-on-the-current-status-of-the-app.patch)
download | inline diff:
diff --git a/web/pgadmin/static/js/tree/tree.js b/web/pgadmin/static/js/tree/tree.js
new file mode 100644
index 00000000..ed67aa85
--- /dev/null
+++ b/web/pgadmin/static/js/tree/tree.js
@@ -0,0 +1,134 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+export class TreeNode {
+ constructor(id, data, parent) {
+ this.id = id;
+ this.data = data;
+ this.parentNode = parent;
+ this.path = this.id;
+ if (parent !== null && parent !== undefined && parent.path !== undefined) {
+ this.path = parent.path + '.' + id;
+ }
+ this.children = [];
+ }
+
+ hasParent() {
+ return this.parentNode !== null && this.parentNode !== undefined;
+ }
+
+ parent() {
+ return this.parentNode;
+ }
+
+ getData() {
+ if (this.data === undefined) {
+ return undefined;
+ } else if (this.data === null) {
+ return null;
+ }
+ return Object.assign({}, this.data);
+ }
+}
+
+export class Tree {
+ constructor() {
+ this.rootNode = new TreeNode(undefined, {});
+ this.aciTreeApi = undefined;
+ }
+
+ addNewNode(id, data, path) {
+ const parent = this.findNode(path);
+ return this.createOrUpdateNode(id, data, parent);
+ }
+
+ findNode(path) {
+ if (path.length === 0) {
+ return this.rootNode;
+ }
+ return findInTree(this.rootNode, path.join('.'));
+ }
+
+ findNodeByDomElement(domElement) {
+ const path = this.translateTreeNodeIdFromACITree(domElement);
+ if(!path || !path[0]) {
+ return undefined;
+ }
+
+ return this.findNode(path);
+ }
+
+ selected() {
+ return this.aciTreeApi.selected();
+ }
+
+ register($treeJQuery) {
+ $treeJQuery.on('acitree', function (event, api, item, eventName) {
+ if (api.isItem(item)) {
+ if (eventName === 'added') {
+ const id = api.getId(item);
+ const data = api.itemData(item);
+ const parentId = this.translateTreeNodeIdFromACITree(api.parent(item));
+ this.addNewNode(id, data, parentId);
+ }
+ }
+ }.bind(this));
+ this.aciTreeApi = $treeJQuery.aciTree('api');
+ }
+
+ createOrUpdateNode(id, data, parent) {
+ const oldNode = this.findNode([parent.path, id]);
+ if (oldNode !== null) {
+ oldNode.data = Object.assign({}, data);
+ return oldNode;
+ }
+
+ const node = new TreeNode(id, data, parent);
+ if (parent === this.rootNode) {
+ node.parentNode = null;
+ }
+ parent.children.push(node);
+ return node;
+ }
+
+ translateTreeNodeIdFromACITree(aciTreeNode) {
+ let currentTreeNode = aciTreeNode;
+ let path = [];
+ while (currentTreeNode !== null && currentTreeNode !== undefined && currentTreeNode.length > 0) {
+ path.unshift(this.aciTreeApi.getId(currentTreeNode));
+ if (this.aciTreeApi.hasParent(currentTreeNode)) {
+ currentTreeNode = this.aciTreeApi.parent(currentTreeNode);
+ } else {
+ break;
+ }
+ }
+ return path;
+ }
+}
+
+export let tree = new Tree();
+
+function findInTree(rootNode, path) {
+ if (path === null) {
+ return rootNode;
+ }
+ return (function findInNode(currentNode) {
+ for (let i = 0, length = currentNode.children.length; i < length; i++) {
+ const calculatedNode = findInNode(currentNode.children[i]);
+ if (calculatedNode !== null) {
+ return calculatedNode;
+ }
+ }
+
+ if (currentNode.path === path) {
+ return currentNode;
+ } else {
+ return null;
+ }
+ })(rootNode);
+}
diff --git a/web/regression/javascript/tree/tree_fake.js b/web/regression/javascript/tree/tree_fake.js
new file mode 100644
index 00000000..a0983c67
--- /dev/null
+++ b/web/regression/javascript/tree/tree_fake.js
@@ -0,0 +1,55 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {Tree} from "../../../pgadmin/static/js/tree/tree";
+
+export class TreeFake extends Tree {
+ constructor() {
+ super();
+ this.aciTreeToOurTreeTranslator = {};
+ }
+
+ addNewNode(id, data, path) {
+ this.aciTreeToOurTreeTranslator[id] = path.concat(id);
+ return super.addNewNode(id, data, path);
+ }
+
+ hasParent(aciTreeNode) {
+ return this.translateTreeNodeIdFromACITree(aciTreeNode).length > 1;
+ }
+
+ parent(aciTreeNode) {
+ if (this.hasParent(aciTreeNode)) {
+ let path = this.translateTreeNodeIdFromACITree(aciTreeNode);
+ return [{id: this.findNode(path).parent().id}];
+ }
+
+ return null;
+ }
+
+ translateTreeNodeIdFromACITree(aciTreeNode) {
+ return this.aciTreeToOurTreeTranslator[aciTreeNode[0].id];
+ }
+
+ itemData(aciTreeNode) {
+ let path = this.translateTreeNodeIdFromACITree(aciTreeNode);
+ if(path === undefined || path === null) {
+ return undefined;
+ }
+ return this.findNode(path).getData();
+ }
+
+ selected() {
+ return this.selectedNode;
+ }
+
+ setSelectedNode(selectedNode) {
+ this.selectedNode = selectedNode;
+ }
+}
diff --git a/web/regression/javascript/tree/tree_spec.js b/web/regression/javascript/tree/tree_spec.js
new file mode 100644
index 00000000..e8930d84
--- /dev/null
+++ b/web/regression/javascript/tree/tree_spec.js
@@ -0,0 +1,284 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+import {Tree, TreeNode} from "../../../pgadmin/static/js/tree/tree";
+import {TreeFake} from "./tree_fake";
+
+const context = describe;
+
+const treeTests = (treeClass, setDefaultCallBack) => {
+ let tree;
+ beforeEach(() => {
+ tree = new treeClass();
+ });
+
+ describe('#addNewNode', () => {
+ describe('when add a new root element', () => {
+ beforeEach(() => {
+ tree.addNewNode('some new node', {data: 'interesting'}, []);
+ });
+
+ it('can be retrieved', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.data).toEqual({data: 'interesting'});
+ });
+
+ it('return false for #hasParent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.hasParent()).toBe(false);
+ });
+
+ it('return null for #parent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.parent()).toBeNull();
+ });
+ });
+
+ describe('when add a new element as a child', () => {
+ let parentNode;
+ beforeEach(() => {
+ parentNode = tree.addNewNode('parent node', {data: 'parent data'}, []);
+ tree.addNewNode('some new node', {data: 'interesting'}, ['parent' +
+ ' node']);
+ });
+
+ it('can be retrieved', () => {
+ const node = tree.findNode(['parent node', 'some new node']);
+ expect(node.data).toEqual({data: 'interesting'});
+ });
+
+ it('return true for #hasParent()', () => {
+ const node = tree.findNode(['parent node', 'some new node']);
+ expect(node.hasParent()).toBe(true);
+ });
+
+ it('return "parent node" object for #parent()', () => {
+ const node = tree.findNode(['parent node', 'some new node']);
+ expect(node.parent()).toEqual(parentNode);
+ });
+ });
+
+ describe('when add an element that already exists under a parent', () => {
+ let parentNode;
+ beforeEach(() => {
+ parentNode = tree.addNewNode('parent node', {data: 'parent data'}, []);
+ tree.addNewNode('some new node', {data: 'interesting'}, ['parent' +
+ ' node']);
+ });
+
+ it('does not add a new child', () => {
+ tree.addNewNode('some new node', {data: 'interesting 1'}, ['parent' +
+ ' node']);
+ const parentNode = tree.findNode(['parent node']);
+ expect(parentNode.children.length).toBe(1);
+ });
+
+ it('updates the existing node data', () => {
+ tree.addNewNode('some new node', {data: 'interesting 1'}, ['parent' +
+ ' node']);
+ const node = tree.findNode(['parent node', 'some new node']);
+ expect(node.data).toEqual({data: 'interesting 1'});
+ });
+ });
+ });
+
+ describe('#translateTreeNodeIdFromACITree', () => {
+ let aciTreeApi;
+ beforeEach(() => {
+ aciTreeApi = jasmine.createSpyObj('ACITreeApi', [
+ 'hasParent',
+ 'parent',
+ 'getId',
+ ]);
+
+ aciTreeApi.getId.and.callFake((node) => {
+ return node[0].id;
+ });
+ tree.aciTreeApi = aciTreeApi;
+ });
+
+ describe('When tree as a single level', () => {
+ beforeEach(() => {
+ aciTreeApi.hasParent.and.returnValue(false);
+ });
+
+ it('returns an array with the ID of the first level', () => {
+ let node = [{
+ id: 'some id',
+ }];
+ tree.addNewNode('some id', {}, []);
+
+ expect(tree.translateTreeNodeIdFromACITree(node)).toEqual(['some id']);
+ });
+ });
+
+ describe('When tree as a 2 levels', () => {
+ describe('When we try to retrieve the node in the second level', () => {
+ it('returns an array with the ID of the first level and second level', () => {
+ aciTreeApi.hasParent.and.returnValues(true, false);
+ aciTreeApi.parent.and.returnValue([{
+ id: 'parent id'
+ }]);
+ let node = [{
+ id: 'some id',
+ }];
+
+ tree.addNewNode('parent id', {}, []);
+ tree.addNewNode('some id', {}, ['parent id']);
+
+ expect(tree.translateTreeNodeIdFromACITree(node))
+ .toEqual(['parent id', 'some id']);
+ });
+ });
+ });
+ });
+
+ describe('#selected', () => {
+ context('a node in the tree is selected', () => {
+ it('returns that node object', () => {
+ let selectedNode = new TreeNode('bamm', {}, []);
+ setDefaultCallBack(tree, selectedNode)
+ expect(tree.selected()).toEqual(selectedNode);
+ });
+ });
+ });
+
+ describe('#findNodeByTreeElement', () => {
+ context('retrieve data from node not found', () => {
+ it('return undefined', () => {
+ let aciTreeApi = jasmine.createSpyObj('ACITreeApi', [
+ 'hasParent',
+ 'parent',
+ 'getId',
+ ]);
+
+ aciTreeApi.getId.and.callFake((node) => {
+ return node[0].id;
+ });
+ tree.aciTreeApi = aciTreeApi;
+ expect(tree.findNodeByDomElement(['<li>something</li>'])).toBeUndefined();
+ });
+ });
+ });
+};
+
+describe('tree tests', () => {
+ describe('TreeNode', () => {
+ describe('#hasParent', () => {
+ context('parent is null', () => {
+ it('returns false', () => {
+ let treeNode = new TreeNode('123', {}, null);
+ expect(treeNode.hasParent()).toBe(false);
+ });
+ });
+ context('parent is undefined', () => {
+ it('returns false', () => {
+ let treeNode = new TreeNode('123', {}, undefined);
+ expect(treeNode.hasParent()).toBe(false);
+ });
+ });
+ context('parent exists', () => {
+ it('returns true', () => {
+ let parentNode = new TreeNode('456', {}, undefined);
+ let treeNode = new TreeNode('123', {}, parentNode);
+ expect(treeNode.hasParent()).toBe(true);
+ });
+ });
+ });
+ });
+
+ describe('Tree', () => {
+ function realTreeSelectNode(tree, selectedNode) {
+ let aciTreeApi = jasmine.createSpyObj('ACITreeApi', [
+ 'selected',
+ ]);
+ tree.aciTreeApi = aciTreeApi;
+ aciTreeApi.selected.and.returnValue(selectedNode);
+ }
+
+ treeTests(Tree, realTreeSelectNode);
+ });
+
+ describe('TreeFake', () => {
+ function fakeTreeSelectNode(tree, selectedNode) {
+ tree.setSelectedNode(selectedNode);
+ }
+
+ treeTests(TreeFake, fakeTreeSelectNode);
+
+ describe('#hasParent', () => {
+ context('tree contains multiple levels', () => {
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, []);
+ tree.addNewNode('level2', {data: 'interesting'}, ['level1']);
+ });
+
+ context('node is at the first level', () => {
+ it('returns false', () => {
+ expect(tree.hasParent([{id: 'level1'}])).toBe(false);
+ });
+ });
+
+ context('node is at the second level', () => {
+ it('returns true', () => {
+ expect(tree.hasParent([{id: 'level2'}])).toBe(true);
+ });
+ });
+ });
+ });
+
+ describe('#parent', () => {
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, []);
+ tree.addNewNode('level2', {data: 'interesting'}, ['level1']);
+ });
+
+ context('node is the root', () => {
+ it('returns null', () => {
+ expect(tree.parent([{id: 'level1'}])).toBeNull();
+ });
+ });
+
+ context('node is not root', () => {
+ it('returns root element', () => {
+ expect(tree.parent([{id: 'level2'}])).toEqual([{id: 'level1'}]);
+ });
+ });
+ });
+
+ describe('#itemData', () => {
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, []);
+ tree.addNewNode('level2', {data: 'expected data'}, ['level1']);
+ });
+
+ context('retrieve data from the node', () => {
+ it('return the node data', () => {
+ expect(tree.itemData([{id: 'level2'}])).toEqual({
+ data: 'expected' +
+ ' data'
+ })
+ });
+ });
+
+ context('retrieve data from node not found', () => {
+ it('return undefined', () => {
+ expect(tree.itemData([{id: 'bamm'}])).toBeUndefined();
+ });
+ });
+ });
+ });
+});
+
--
2.15.1
[application/octet-stream] 0002-Extractions-of-multiples-locations-where-we-used-the.patch (142.9K, 4-0002-Extractions-of-multiples-locations-where-we-used-the.patch)
download | inline diff:
diff --git a/web/package.json b/web/package.json
index 3ce86a11..f8d87607 100644
--- a/web/package.json
+++ b/web/package.json
@@ -84,6 +84,7 @@
"slickgrid": "git+https://github.com/6pac/SlickGrid.git#2.3.7",
"snapsvg": "^0.5.1",
"spectrum-colorpicker": "^1.8.0",
+ "sprintf-js": "^1.1.1",
"underscore": "^1.8.3",
"underscore.string": "^3.3.4",
"watchify": "~3.9.0",
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
index a7fd4c7c..10dc7e3b 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
@@ -450,29 +450,7 @@ define('pgadmin.node.schema', [
});
pgBrowser.tableChildTreeNodeHierarchy = function(i) {
- var idx = 0,
- res = {},
- t = pgBrowser.tree;
-
- do {
- var d = t.itemData(i);
- if (
- d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId
- ) {
- if (d._type === 'partition' || d._type === 'table') {
- if (!('table' in res)) {
- res['table'] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- } else {
- res[d._type] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- } while (i);
-
- return res;
+ return this.getTreeNodeHierarchy(i);
};
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js
index 72b6eb0c..1fdacf52 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js
@@ -88,7 +88,6 @@ define('pgadmin.node.column', [
if (!pgBrowser.Nodes['column']) {
pgBrowser.Nodes['column'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table', 'view', 'mview'],
collection_type: ['coll-table', 'coll-view', 'coll-mview'],
type: 'column',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js
index bf7ef556..b1cb2518 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js
@@ -8,7 +8,6 @@ define('pgadmin.node.check_constraint', [
// Check Constraint Node
if (!pgBrowser.Nodes['check_constraint']) {
pgAdmin.Browser.Nodes['check_constraint'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'check_constraint',
label: gettext('Check'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js
index 0bbf66a1..adccf2e9 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js
@@ -605,7 +605,6 @@ define('pgadmin.node.exclusion_constraint', [
// Extend the browser's node class for exclusion constraint node
if (!pgBrowser.Nodes['exclusion_constraint']) {
pgAdmin.Browser.Nodes['exclusion_constraint'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'exclusion_constraint',
label: gettext('Exclusion constraint'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js
index 4997d175..788cecfc 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js
@@ -603,7 +603,6 @@ define('pgadmin.node.foreign_key', [
// Extend the browser's node class for foreign key node
if (!pgBrowser.Nodes['foreign_key']) {
pgAdmin.Browser.Nodes['foreign_key'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'foreign_key',
label: gettext('Foreign key'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js
index d3a6cff4..0ad0f054 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js
@@ -20,7 +20,6 @@ define('pgadmin.node.primary_key', [
parent_type: ['table','partition'],
canDrop: true,
canDropCascade: true,
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
Init: function() {
/* Avoid multiple registration of menus */
if (this.initialized)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js
index 769185d6..18d3ca33 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js
@@ -20,7 +20,6 @@ define('pgadmin.node.unique_constraint', [
parent_type: ['table','partition'],
canDrop: true,
canDropCascade: true,
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
Init: function() {
/* Avoid multiple registration of menus */
if (this.initialized)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js
index cb242cd2..9c0e24fe 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js
@@ -12,14 +12,12 @@ define('pgadmin.node.constraints', [
node: 'constraints',
label: gettext('Constraints'),
type: 'coll-constraints',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
columns: ['name', 'comment'],
});
}
if (!pgBrowser.Nodes['constraints']) {
pgAdmin.Browser.Nodes['constraints'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'constraints',
label: gettext('Constraints'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js
index ec2b4da1..2b687c1a 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js
@@ -13,7 +13,6 @@ define('pgadmin.node.index', [
node: 'index',
label: gettext('Indexes'),
type: 'coll-index',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
sqlAlterHelp: 'sql-alterindex.html',
sqlCreateHelp: 'sql-createindex.html',
dialogHelp: url_for('help.static', {'filename': 'index_dialog.html'}),
@@ -215,7 +214,6 @@ define('pgadmin.node.index', [
if (!pgBrowser.Nodes['index']) {
pgAdmin.Browser.Nodes['index'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table', 'view', 'mview', 'partition'],
collection_type: ['coll-table', 'coll-view'],
sqlAlterHelp: 'sql-alterindex.html',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js
index fd53f743..ef8eb534 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js
@@ -1,10 +1,12 @@
define([
+ 'sources/tree/pgadmin_tree_node',
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.backgrid',
'pgadmin.browser.collection', 'pgadmin.browser.table.partition.utils',
],
function(
+ pgadminTreeNode,
gettext, url_for, $, _, S, pgAdmin, pgBrowser, Alertify, Backform, Backgrid
) {
@@ -13,7 +15,6 @@ function(
pgAdmin.Browser.Collection.extend({
node: 'partition',
label: gettext('Partitions'),
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'coll-partition',
columns: [
'name', 'schema', 'partition_value', 'is_partitioned', 'description',
@@ -80,36 +81,6 @@ function(
},
]);
},
- getTreeNodeHierarchy: function(i) {
- var idx = 0,
- res = {},
- t = pgBrowser.tree;
-
- do {
- var d = t.itemData(i);
- if (
- d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId
- ) {
- if (d._type == 'partition' && 'partition' in res) {
- if (!('table' in res)) {
- res['table'] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- } else if (d._type == 'table') {
- if (!('table' in res)) {
- res['table'] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- } else {
- res[d._type] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- } while (i);
-
- return res;
- },
generate_url: function(item, type, d, with_id, info) {
if (_.indexOf([
'stats', 'statistics', 'dependency', 'dependent', 'reset',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js
index 3af61754..354909f3 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js
@@ -16,7 +16,6 @@ define('pgadmin.node.rule', [
node: 'rule',
label: gettext('Rules'),
type: 'coll-rule',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
columns: ['name', 'owner', 'comment'],
});
}
@@ -35,7 +34,6 @@ define('pgadmin.node.rule', [
*/
if (!pgBrowser.Nodes['rule']) {
pgAdmin.Browser.Nodes['rule'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table','view', 'partition'],
type: 'rule',
sqlAlterHelp: 'sql-alterrule.html',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
index 647d1bfe..99d60243 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
@@ -26,7 +26,6 @@ define('pgadmin.node.table', [
if (!pgBrowser.Nodes['table']) {
pgBrowser.Nodes['table'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'table',
label: gettext('Table'),
collection_type: 'coll-table',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js
index fbe7659e..9a1ce2e0 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js
@@ -29,14 +29,12 @@ define('pgadmin.node.trigger', [
node: 'trigger',
label: gettext('Triggers'),
type: 'coll-trigger',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
columns: ['name', 'description'],
});
}
if (!pgBrowser.Nodes['trigger']) {
pgAdmin.Browser.Nodes['trigger'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table', 'view', 'partition'],
collection_type: ['coll-table', 'coll-view'],
type: 'trigger',
diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js
index 46b60332..d38049db 100644
--- a/web/pgadmin/browser/static/js/browser.js
+++ b/web/pgadmin/browser/static/js/browser.js
@@ -1,4 +1,5 @@
define('pgadmin.browser', [
+ 'sources/tree/tree',
'sources/gettext', 'sources/url_for', 'require', 'jquery', 'underscore', 'underscore.string',
'bootstrap', 'sources/pgadmin', 'pgadmin.alertifyjs', 'bundled_codemirror',
'sources/check_node_visibility', 'pgadmin.browser.utils', 'wcdocker',
@@ -10,6 +11,7 @@ define('pgadmin.browser', [
'sources/codemirror/addon/fold/pgadmin-sqlfoldcode',
'pgadmin.browser.keyboard',
], function(
+ tree,
gettext, url_for, require, $, _, S, Bootstrap, pgAdmin, Alertify,
codemirror, checkNodeVisibility
) {
@@ -84,6 +86,7 @@ define('pgadmin.browser', [
});
b.tree = $('#tree').aciTree('api');
+ b.treeMenu.register($('#tree'));
};
// Extend the browser class attributes
@@ -98,6 +101,7 @@ define('pgadmin.browser', [
editor:null,
// Left hand browser tree
tree:null,
+ treeMenu: new tree.Tree(),
// list of script to be loaded, when a certain type of node is loaded
// It will be used to register extensions, tools, child node scripts,
// etc.
diff --git a/web/pgadmin/browser/static/js/node.js b/web/pgadmin/browser/static/js/node.js
index ad582483..3baf554a 100644
--- a/web/pgadmin/browser/static/js/node.js
+++ b/web/pgadmin/browser/static/js/node.js
@@ -1,9 +1,12 @@
define('pgadmin.browser.node', [
+ 'sources/tree/pgadmin_tree_node',
'sources/gettext', 'jquery', 'underscore', 'underscore.string', 'sources/pgadmin',
'pgadmin.browser.menu', 'backbone', 'pgadmin.alertifyjs', 'pgadmin.browser.datamodel',
'backform', 'sources/browser/generate_url', 'sources/utils', 'pgadmin.browser.utils',
'pgadmin.backform',
-], function(gettext, $, _, S, pgAdmin, Menu, Backbone, Alertify, pgBrowser, Backform, generateUrl, commonUtils) {
+], function(
+ pgadminTreeNode,
+ gettext, $, _, S, pgAdmin, Menu, Backbone, Alertify, pgBrowser, Backform, generateUrl, commonUtils) {
var wcDocker = window.wcDocker,
keyCode = {
@@ -1566,7 +1569,6 @@ define('pgadmin.browser.node', [
* depends, statistics
*/
generate_url: function(item, type, d, with_id, info) {
-
var opURL = {
'create': 'obj',
'drop': 'obj',
@@ -1608,24 +1610,7 @@ define('pgadmin.browser.node', [
Collection: pgBrowser.DataCollection,
// Base class for Node Data Model
Model: pgBrowser.DataModel,
- getTreeNodeHierarchy: function(i) {
- var idx = 0,
- res = {},
- t = pgBrowser.tree,
- d;
- do {
- d = t.itemData(i);
- if (d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId) {
- res[d._type] = _.extend({}, d, {
- 'priority': idx,
- });
- idx -= 1;
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- } while (i);
-
- return res;
- },
+ getTreeNodeHierarchy: pgadminTreeNode.getTreeNodeHierarchyFromIdentifier.bind(pgBrowser),
cache: function(url, node_info, level, data) {
var cached = this.cached = this.cached || {},
hash = url,
diff --git a/web/pgadmin/static/js/backup/backup_dialog.js b/web/pgadmin/static/js/backup/backup_dialog.js
new file mode 100644
index 00000000..779170ee
--- /dev/null
+++ b/web/pgadmin/static/js/backup/backup_dialog.js
@@ -0,0 +1,142 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../gettext';
+import {sprintf} from 'sprintf-js';
+import Backform from '../backform.pgadmin';
+import {DialogFactory} from './backup_dialog_factory';
+
+export class BackupDialog {
+ constructor(pgBrowser, $, alertify, BackupModel, backform = Backform) {
+ this.pgBrowser = pgBrowser;
+ this.jquery = $;
+ this.alertify = alertify;
+ this.backupModel = BackupModel;
+ this.backform = backform;
+ }
+
+ draw(action, item, params) {
+ const serverInformation = this.retrieveAncestorOfTypeServer(item);
+
+ if (!serverInformation) {
+ return;
+ }
+
+ if (!this.hasBinariesConfiguration(serverInformation)) {
+ return;
+ }
+
+ const typeOfDialog = BackupDialog.typeOfDialog(params);
+
+ const dialog = this.createOrGetDialog(typeOfDialog);
+ dialog(true).resizeTo('60%', '50%');
+ }
+
+ static typeOfDialog(params) {
+ if(params === null) {
+ return 'backup_objects';
+ }
+ let typeOfDialog = 'server';
+ if (!_.isUndefined(params['globals']) && params['globals']) {
+ typeOfDialog = 'globals';
+ }
+ return typeOfDialog;
+ }
+
+ static dialogTitle(typeOfDialog) {
+ if(typeOfDialog === 'backup_objects') {
+ return null;
+ }
+ return ((typeOfDialog === 'globals') ?
+ gettext('Backup Globals...') :
+ gettext('Backup Server...'));
+ }
+
+ createOrGetDialog(typeOfDialog) {
+ const dialogName = this.dialogName(typeOfDialog);
+ const dialogTitle = BackupDialog.dialogTitle(typeOfDialog);
+
+ if (!this.alertify[dialogName]) {
+ const self = this;
+ this.alertify.dialog(dialogName, function factory() {
+ return self.dialogFactory(dialogTitle, typeOfDialog);
+ });
+ }
+ return this.alertify[dialogName];
+ }
+
+ dialogName(typeOfDialog) {
+ if(typeOfDialog === 'backup_objects') {
+ return typeOfDialog;
+ }
+ return 'BackupDialog_' + typeOfDialog;
+ }
+
+ hasBinariesConfiguration(serverInformation) {
+ const module = 'paths';
+ let preference_name = 'pg_bin_dir';
+ let msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
+
+ if ((serverInformation.type && serverInformation.type === 'ppas') ||
+ serverInformation.server_type === 'ppas') {
+ preference_name = 'ppas_bin_dir';
+ msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
+ }
+ const preference = this.pgBrowser.get_preference(module, preference_name);
+
+ if (preference) {
+ if (!preference.value) {
+ this.alertify.alert(gettext('Configuration required'), msg);
+ return false;
+ }
+ } else {
+ this.alertify.alert(
+ gettext('Backup Error'),
+ sprintf(gettext('Failed to load preference %s of module %s'), preference_name, module)
+ );
+ return false;
+ }
+ return true;
+ }
+
+ retrieveAncestorOfTypeServer(item) {
+ let serverInformation = null;
+ let aciTreeItem = item || this.pgBrowser.treeMenu.selected();
+ let treeNode = this.pgBrowser.treeMenu.findNodeByDomElement(aciTreeItem);
+
+ while (treeNode) {
+ const node_data = treeNode.getData();
+ if (node_data._type === 'server') {
+ serverInformation = node_data;
+ break;
+ }
+
+ if (treeNode.hasParent()) {
+ treeNode = treeNode.parent();
+ } else {
+ this.alertify.alert(
+ gettext('Backup Error'),
+ gettext('Please select server or child node from the browser tree.')
+ );
+ break;
+ }
+ }
+ return serverInformation;
+ }
+
+ dialogFactory(dialogTitle, typeOfDialog) {
+ const factory = new DialogFactory(
+ this.pgBrowser,
+ this.jquery,
+ this.alertify,
+ this.backupModel,
+ this.backform);
+ return factory.create(dialogTitle, typeOfDialog);
+ }
+}
diff --git a/web/pgadmin/static/js/backup/backup_dialog_factory.js b/web/pgadmin/static/js/backup/backup_dialog_factory.js
new file mode 100644
index 00000000..2fd491d6
--- /dev/null
+++ b/web/pgadmin/static/js/backup/backup_dialog_factory.js
@@ -0,0 +1,56 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import * as commonUtils from '../utils';
+import {DialogWrapper} from './backup_dialog_wrapper';
+
+export class DialogFactory {
+ constructor(pgBrowser, $, alertify, BackupModel, backform) {
+ this.pgBrowser = pgBrowser;
+ this.jquery = $;
+ this.alertify = alertify;
+ this.backupModel = BackupModel;
+ this.backform = backform;
+ }
+
+ create(dialogTitle, typeOfDialog) {
+ const dialogContainerSelector = '<div class=\'backup_dialog\'></div>';
+ return new DialogWrapper(dialogContainerSelector, this, dialogTitle, typeOfDialog);
+ }
+
+ static wasBackupButtonPressed(event) {
+ return event.button['data-btn-name'] === 'backup';
+ }
+
+ static wasHelpButtonPressed(e) {
+ return e.button.element.name === 'dialog_help'
+ || e.button.element.name === 'object_help';
+ }
+
+ static getSelectedNode(selectedTreeNode) {
+ if (!DialogFactory.isNodeSelected(selectedTreeNode)) {
+ return undefined;
+ }
+ const treeNodeData = selectedTreeNode.getData();
+ if (treeNodeData) {
+ return treeNodeData;
+ }
+ return undefined;
+ }
+
+ static focusOnDialog(dialog) {
+ const container = dialog.$el.find('.tab-content:first > .tab-pane.active:first');
+ commonUtils.findAndSetFocus(container);
+ }
+
+ static isNodeSelected(selectedTreeNode) {
+ return selectedTreeNode;
+ }
+}
+
diff --git a/web/pgadmin/static/js/backup/backup_dialog_wrapper.js b/web/pgadmin/static/js/backup/backup_dialog_wrapper.js
new file mode 100644
index 00000000..e5c803c0
--- /dev/null
+++ b/web/pgadmin/static/js/backup/backup_dialog_wrapper.js
@@ -0,0 +1,270 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {getTreeNodeHierarchyFromElement} from '../tree/pgadmin_tree_node';
+import axios from 'axios/index';
+import {DialogFactory} from './backup_dialog_factory';
+import gettext from '../gettext';
+import url_for from '../url_for';
+import _ from 'underscore';
+
+export class DialogWrapper {
+ constructor(dialogContainerSelector, factory, dialogTitle, typeOfDialog) {
+ this.hooks = {
+ // Triggered when the dialog is closed
+ onclose: function () {
+ if (this.view) {
+ // clear our backform model/view
+ this.view.remove({
+ data: true,
+ internal: true,
+ silent: true,
+ });
+ }
+ },
+ };
+ this.dialogContainerSelector = dialogContainerSelector;
+ this.factory = factory;
+ this.dialogTitle = dialogTitle;
+ this.typeOfDialog = typeOfDialog;
+ }
+
+ main(title) {
+ this.set('title', title);
+ }
+
+ build() {
+ this.factory.alertify.pgDialogBuild.apply(this);
+ }
+
+ setup() {
+ return {
+ buttons: [{
+ text: '',
+ className: 'btn btn-default pull-left fa fa-lg fa-info',
+ attrs: {
+ name: 'object_help',
+ type: 'button',
+ url: 'backup.html',
+ label: gettext('Backup'),
+ },
+ }, {
+ text: '',
+ key: 112,
+ className: 'btn btn-default pull-left fa fa-lg fa-question',
+ attrs: {
+ name: 'dialog_help',
+ type: 'button',
+ label: gettext('Backup'),
+ url: url_for('help.static', {
+ 'filename': 'backup_dialog.html',
+ }),
+ },
+ }, {
+ text: gettext('Backup'),
+ key: 13,
+ className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
+ 'data-btn-name': 'backup',
+ }, {
+ text: gettext('Cancel'),
+ key: 27,
+ className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
+ 'data-btn-name': 'cancel',
+ }],
+ // Set options for dialog
+ options: {
+ title: this.dialogTitle,
+ //disable both padding and overflow control.
+ padding: !1,
+ overflow: !1,
+ model: 0,
+ resizable: true,
+ maximizable: true,
+ pinnable: false,
+ closableByDimmer: false,
+ modal: false,
+ },
+ };
+ }
+
+ prepare() {
+ this.disableBackupButton();
+
+ const $container = this.factory.jquery(this.dialogContainerSelector);
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = DialogFactory.getSelectedNode(selectedTreeNode);
+ if (!selectedTreeNodeData) {
+ return;
+ }
+
+ const node = this.factory.pgBrowser.Nodes[selectedTreeNodeData._type];
+ if (this.dialogTitle === null) {
+ const title = `Backup (${node.label}: ${selectedTreeNodeData.label})`;
+ this.main(title);
+ }
+
+ const treeInfo = getTreeNodeHierarchyFromElement(this.factory.pgBrowser, selectedTreeNode);
+ const dialog = this.createDialog(node, treeInfo, this.typeOfDialog, $container);
+ this.addAlertifyClassToBackupNodeChildNodes();
+ dialog.render();
+
+ this.elements.content.appendChild($container.get(0));
+
+ DialogFactory.focusOnDialog(dialog);
+ this.setListenersForFilenameChanges();
+ }
+
+ callback(event) {
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = DialogFactory.getSelectedNode(selectedTreeNode);
+ const node = selectedTreeNodeData && this.factory.pgBrowser.Nodes[selectedTreeNodeData._type];
+
+ if (DialogFactory.wasHelpButtonPressed(event)) {
+ event.cancel = true;
+ this.factory.pgBrowser.showHelp(
+ event.button.element.name,
+ event.button.element.getAttribute('url'),
+ node,
+ selectedTreeNode,
+ event.button.element.getAttribute('label')
+ );
+ return;
+ }
+
+ if (DialogFactory.wasBackupButtonPressed(event)) {
+
+ if (!selectedTreeNodeData)
+ return;
+
+ const serverIdentifier = this.retrieveServerIdentifier(node, selectedTreeNode);
+
+ const factory = this.factory;
+ let urlShortcut = 'backup.create_server_job';
+ if (this.typeOfDialog === 'backup_objects') {
+ urlShortcut = 'backup.create_object_job';
+ }
+ const baseUrl = url_for(urlShortcut, {
+ 'sid': serverIdentifier,
+ });
+
+ this.setExtraParameters(selectedTreeNode);
+
+ let service = axios.create({});
+ service.post(
+ baseUrl,
+ this.view.model.toJSON()
+ ).then(function () {
+ factory.alertify.success(gettext('Backup job created.'), 5);
+ factory.pgBrowser.Events.trigger('pgadmin-bgprocess:created', factory);
+ }).catch(function (error) {
+ try {
+ const err = error.response.data;
+ factory.alertify.alert(
+ gettext('Backup job failed.'),
+ err.errormsg
+ );
+ } catch (e) {
+ console.warn(e.stack || e);
+ }
+ });
+ }
+ }
+
+ addAlertifyClassToBackupNodeChildNodes() {
+ this.factory.jquery(this.elements.body.childNodes[0]).addClass(
+ 'alertify_tools_dialog_properties obj_properties'
+ );
+ }
+
+ getSelectedNode() {
+ const tree = this.factory.pgBrowser.treeMenu;
+ const selectedNode = tree.selected();
+ if (selectedNode) {
+ return tree.findNodeByDomElement(selectedNode);
+ } else {
+ return undefined;
+ }
+ }
+
+ disableBackupButton() {
+ this.__internal.buttons[2].element.disabled = true;
+ }
+
+ enableBackupButton() {
+ this.__internal.buttons[2].element.disabled = false;
+ }
+
+ createDialog(node, treeInfo, typeOfDialog, $container) {
+ let attributes = {};
+ if (typeOfDialog !== 'backup_objects') {
+ attributes['type'] = typeOfDialog;
+ }
+ // Instance of backbone model
+ const newModel = new this.factory.backupModel(attributes, {
+ node_info: treeInfo,
+ });
+ const fields = this.factory.backform.generateViewSchema(
+ treeInfo, newModel, 'create', node, treeInfo.server, true
+ );
+
+ return this.view = new this.factory.backform.Dialog({
+ el: $container,
+ model: newModel,
+ schema: fields,
+ });
+ }
+
+ retrieveServerIdentifier(node, selectedTreeNode) {
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.factory.pgBrowser,
+ selectedTreeNode
+ );
+ return treeInfo.server._id;
+ }
+
+ setListenersForFilenameChanges() {
+ const self = this;
+
+ this.view.model.on('change', function () {
+ if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
+ this.errorModel.clear();
+ self.enableBackupButton();
+ } else {
+ self.disableBackupButton();
+ this.errorModel.set('file', gettext('Please provide a filename'));
+ }
+ });
+ }
+
+ setExtraParameters(selectedTreeNode) {
+ if (this.typeOfDialog === 'backup_objects') {
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.factory.pgBrowser,
+ selectedTreeNode
+ );
+
+ this.view.model.set('database', treeInfo.database._label);
+
+ const nodeData = selectedTreeNode.getData();
+ if (nodeData._type === 'schema') {
+ this.view.model.set('schemas', [nodeData._label]);
+ }
+
+ if (nodeData._type === 'table') {
+ this.view.model.set('tables', [
+ [treeInfo.schema._label, nodeData._label],
+ ]);
+ }
+
+ if (_.isEmpty(this.view.model.get('ratio'))) {
+ this.view.model.unset('ratio');
+ }
+ }
+ }
+}
diff --git a/web/pgadmin/static/js/backup/menu_utils.js b/web/pgadmin/static/js/backup/menu_utils.js
new file mode 100644
index 00000000..f67a436a
--- /dev/null
+++ b/web/pgadmin/static/js/backup/menu_utils.js
@@ -0,0 +1,60 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export const backupSupportedNodes = [
+ 'database', 'schema', 'table', 'partition',
+];
+
+function isNodeTypeSupported(nodeDataType, parentNodeType) {
+ return _.indexOf(backupSupportedNodes, nodeDataType) !== -1
+ && parentNodeType !== 'catalog';
+}
+
+function isProvidedDataValid(treeNodeData) {
+ return !_.isUndefined(treeNodeData) && !_.isNull(treeNodeData);
+}
+
+function doesNodeHaveMenu(treeNodeData) {
+ return (treeNodeData._type === 'database' && treeNodeData.allowConn)
+ || treeNodeData._type !== 'database';
+}
+
+function retrieveParentNodeType(treeNode) {
+ if (!treeNode.hasParent()) {
+ return null;
+ }
+ if(treeNode.parent().getData() === null) {
+ return null;
+ }
+ return treeNode.parent().getData()._type;
+}
+
+export function menuEnabled(treeNodeData, domTreeNode) {
+ let treeNode = this.treeMenu.findNodeByDomElement(domTreeNode);
+ if(!treeNode) {
+ return false;
+ }
+ let parentNodeType = retrieveParentNodeType(treeNode);
+
+ if (isProvidedDataValid(treeNodeData) && !_.isNull(parentNodeType)) {
+ return isNodeTypeSupported(treeNodeData._type, parentNodeType)
+ && doesNodeHaveMenu(treeNodeData);
+ } else {
+ return false;
+ }
+}
+
+function isNodeAServerAndConnected(treeNodeData) {
+ return (('server' === treeNodeData._type) && treeNodeData.connected);
+}
+
+export function menuEnabledServer(treeNodeData) {
+ return isProvidedDataValid(treeNodeData)
+ && isNodeAServerAndConnected(treeNodeData);
+}
diff --git a/web/pgadmin/static/js/datagrid/get_panel_title.js b/web/pgadmin/static/js/datagrid/get_panel_title.js
new file mode 100644
index 00000000..f5a5664d
--- /dev/null
+++ b/web/pgadmin/static/js/datagrid/get_panel_title.js
@@ -0,0 +1,33 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {getTreeNodeHierarchyFromIdentifier} from '../tree/pgadmin_tree_node';
+
+function getDatabaseLabel(parentData) {
+ return parentData.database ? parentData.database.label
+ : parentData.server.db;
+}
+
+function isServerInformationAvailable(parentData) {
+ return parentData.server === undefined;
+}
+
+export function getPanelTitle(pgBrowser) {
+ const selected_item = pgBrowser.treeMenu.selected();
+
+ const parentData = getTreeNodeHierarchyFromIdentifier
+ .call(pgBrowser, selected_item);
+ if (isServerInformationAvailable(parentData)) {
+ return;
+ }
+
+ const db_label = getDatabaseLabel(parentData);
+
+ return `${db_label} on ${parentData.server.user.name}@${parentData.server.label}`;
+}
diff --git a/web/pgadmin/static/js/datagrid/show_data.js b/web/pgadmin/static/js/datagrid/show_data.js
new file mode 100644
index 00000000..a414fd77
--- /dev/null
+++ b/web/pgadmin/static/js/datagrid/show_data.js
@@ -0,0 +1,92 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import gettext from '../gettext';
+import url_for from '../url_for';
+import {getTreeNodeHierarchyFromIdentifier} from '../../../static/js/tree/pgadmin_tree_node';
+
+export function showDataGrid(
+ datagrid,
+ pgBrowser,
+ alertify,
+ connectionData,
+ aciTreeIdentifier
+) {
+ const node = pgBrowser.treeMenu.findNodeByDomElement(aciTreeIdentifier);
+ if (node === undefined || !node.getData()) {
+ alertify.alert(
+ gettext('Data Grid Error'),
+ gettext('No object selected.')
+ );
+ return;
+ }
+
+ const parentData = getTreeNodeHierarchyFromIdentifier.call(
+ pgBrowser,
+ aciTreeIdentifier
+ );
+
+ if (hasServerOrDatabaseConfiguration(parentData)
+ || !hasSchemaOrCatalogOrViewInformation(parentData)) {
+ return;
+ }
+
+ let namespaceName = retrieveNameSpaceName(parentData);
+ const baseUrl = generateUrl(connectionData, node.getData(), parentData);
+ const grid_title = generateDatagridTitle(parentData, namespaceName, node.getData());
+
+ datagrid.create_transaction(
+ baseUrl,
+ null,
+ 'false',
+ parentData.server.server_type,
+ '',
+ grid_title,
+ ''
+ );
+}
+
+
+function retrieveNameSpaceName(parentData) {
+ if (parentData.schema !== undefined) {
+ return parentData.schema.label;
+ }
+ else if (parentData.view !== undefined) {
+ return parentData.view.label;
+ }
+ else if (parentData.catalog !== undefined) {
+ return parentData.catalog.label;
+ }
+ return '';
+}
+
+function generateUrl(connectionData, nodeData, parentData) {
+ const url_params = {
+ 'cmd_type': connectionData.mnuid,
+ 'obj_type': nodeData._type,
+ 'sgid': parentData.server_group._id,
+ 'sid': parentData.server._id,
+ 'did': parentData.database._id,
+ 'obj_id': nodeData._id,
+ };
+
+ return url_for('datagrid.initialize_datagrid', url_params);
+}
+
+function hasServerOrDatabaseConfiguration(parentData) {
+ return parentData.server === undefined || parentData.database === undefined;
+}
+
+function hasSchemaOrCatalogOrViewInformation(parentData) {
+ return parentData.schema !== undefined || parentData.view !== undefined ||
+ parentData.catalog !== undefined;
+}
+
+function generateDatagridTitle(parentData, namespaceName, nodeData) {
+ return `${parentData.server.label} - ${parentData.database.label} - ${namespaceName}.${nodeData.label}`;
+}
diff --git a/web/pgadmin/static/js/datagrid/show_query_tool.js b/web/pgadmin/static/js/datagrid/show_query_tool.js
new file mode 100644
index 00000000..0436e0fd
--- /dev/null
+++ b/web/pgadmin/static/js/datagrid/show_query_tool.js
@@ -0,0 +1,63 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../gettext';
+import url_for from '../url_for';
+import {getTreeNodeHierarchyFromIdentifier} from '../../../static/js/tree/pgadmin_tree_node';
+
+function hasDatabaseInformation(parentData) {
+ return parentData.database;
+}
+
+function generateUrl(parentData) {
+ let url_endpoint = 'datagrid.initialize_query_tool';
+ let url_params = {
+ 'sgid': parentData.server_group._id,
+ 'sid': parentData.server._id,
+ };
+
+ if (hasDatabaseInformation(parentData)) {
+ url_params['did'] = parentData.database._id;
+ url_endpoint = 'datagrid.initialize_query_tool_with_did';
+ }
+
+ return url_for(url_endpoint, url_params);
+}
+
+function hasServerInformations(parentData) {
+ return parentData.server === undefined;
+}
+
+export function showQueryTool(datagrid, pgBrowser, alertify, url,
+ aciTreeIdentifier, panelTitle) {
+ const sURL = url || '';
+ const queryToolTitle = panelTitle || '';
+
+ const currentNode = pgBrowser.treeMenu.findNodeByDomElement(aciTreeIdentifier);
+ if (currentNode === undefined) {
+ alertify.alert(
+ gettext('Query Tool Error'),
+ gettext('No object selected.')
+ );
+ return;
+ }
+
+ const parentData = getTreeNodeHierarchyFromIdentifier.call(
+ pgBrowser, aciTreeIdentifier);
+
+ if (hasServerInformations(parentData)) {
+ return;
+ }
+
+ const baseUrl = generateUrl(parentData);
+
+ datagrid.create_transaction(
+ baseUrl, null, 'true',
+ parentData.server.server_type, sURL, queryToolTitle, '', false);
+}
diff --git a/web/pgadmin/static/js/tree/pgadmin_tree_node.js b/web/pgadmin/static/js/tree/pgadmin_tree_node.js
new file mode 100644
index 00000000..bccbe587
--- /dev/null
+++ b/web/pgadmin/static/js/tree/pgadmin_tree_node.js
@@ -0,0 +1,43 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+export function getTreeNodeHierarchyFromElement(pgBrowser, treeNode) {
+ return getTreeNodeHierarchy.call(pgBrowser, treeNode);
+}
+
+export function getTreeNodeHierarchyFromIdentifier(aciTreeNodeIdentifier) {
+ let identifier = this.treeMenu.translateTreeNodeIdFromACITree(aciTreeNodeIdentifier);
+ let currentNode = this.treeMenu.findNode(identifier);
+ return getTreeNodeHierarchy.call(this, currentNode);
+}
+
+export function getTreeNodeHierarchy(currentNode) {
+ let idx = 0;
+ let result = {};
+
+ do {
+ const currentNodeData = currentNode.getData();
+ if (currentNodeData._type in this.Nodes && this.Nodes[currentNodeData._type].hasId) {
+ const nodeType = mapType(currentNodeData._type);
+ if (result[nodeType] === undefined) {
+ result[nodeType] = _.extend({}, currentNodeData, {
+ 'priority': idx,
+ });
+ idx -= 1;
+ }
+ }
+ currentNode = currentNode.hasParent() ? currentNode.parent() : null;
+ } while (currentNode);
+
+ return result;
+}
+
+function mapType(type) {
+ return type === 'partition' ? 'table' : type;
+}
diff --git a/web/pgadmin/tools/backup/static/js/backup.js b/web/pgadmin/tools/backup/static/js/backup.js
index ddbfaee4..ffbae127 100644
--- a/web/pgadmin/tools/backup/static/js/backup.js
+++ b/web/pgadmin/tools/backup/static/js/backup.js
@@ -3,9 +3,10 @@ define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'pgadmin.alertifyjs', 'backbone', 'pgadmin.backgrid',
'pgadmin.backform', 'pgadmin.browser', 'sources/utils',
+ 'sources/backup/menu_utils', 'sources/backup/backup_dialog',
], function(
gettext, url_for, $, _, S, alertify, Backbone, Backgrid, Backform, pgBrowser,
-commonUtils
+commonUtils, menuUtils, globalBackupDialog
) {
// if module is already initialized, refer to that.
@@ -394,48 +395,6 @@ commonUtils
this.initialized = true;
- // Define list of nodes on which backup context menu option appears
- var backup_supported_nodes = [
- 'database', 'schema', 'table', 'partition',
- ];
-
- /**
- Enable/disable backup menu in tools based
- on node selected
- if selected node is present in supported_nodes,
- menu will be enabled otherwise disabled.
- Also, hide it for system view in catalogs
- */
- var menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData,
- parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
-
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(backup_supported_nodes, d._type) !== -1 &&
- parent_data._type != 'catalog') {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
- var menu_enabled_server = function(itemData) {
- // If server node selected && connected
- if (!_.isUndefined(itemData) && !_.isNull(itemData))
- return (('server' === itemData._type) && itemData.connected);
- else
- return false;
- };
-
// Define the nodes on which the menus to be appear
var menus = [{
name: 'backup_global',
@@ -445,7 +404,7 @@ commonUtils
priority: 12,
label: gettext('Backup Globals...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_server',
module: this,
@@ -454,7 +413,7 @@ commonUtils
priority: 12,
label: gettext('Backup Server...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_global_ctx',
module: this,
@@ -464,7 +423,7 @@ commonUtils
priority: 12,
label: gettext('Backup Globals...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_server_ctx',
module: this,
@@ -474,7 +433,7 @@ commonUtils
priority: 12,
label: gettext('Backup Server...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_object',
module: this,
@@ -483,20 +442,20 @@ commonUtils
priority: 11,
label: gettext('Backup...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled,
+ enable: menuUtils.menuEnabled.bind(pgBrowser),
}];
- for (var idx = 0; idx < backup_supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.backupSupportedNodes.length; idx++) {
menus.push({
- name: 'backup_' + backup_supported_nodes[idx],
- node: backup_supported_nodes[idx],
+ name: 'backup_' + menuUtils.backupSupportedNodes[idx],
+ node: menuUtils.backupSupportedNodes[idx],
module: this,
applies: ['context'],
callback: 'backup_objects',
priority: 11,
label: gettext('Backup...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled,
+ enable: menuUtils.menuEnabled.bind(pgBrowser),
});
}
@@ -521,531 +480,25 @@ commonUtils
},
// Callback to draw Backup Dialog for globals/server
- start_backup_global_server: function(action, item, params) {
- var i = item || pgBrowser.tree.selected(),
- server_data = null;
-
- while (i) {
- var node_data = pgBrowser.tree.itemData(i);
- if (node_data._type == 'server') {
- server_data = node_data;
- break;
- }
-
- if (pgBrowser.tree.hasParent(i)) {
- i = $(pgBrowser.tree.parent(i));
- } else {
- alertify.alert(
- gettext('Backup Error'),
- gettext('Please select server or child node from the browser tree.')
- );
- break;
- }
- }
-
- if (!server_data) {
- return;
- }
-
- var module = 'paths',
- preference_name = 'pg_bin_dir',
- msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
-
- if ((server_data.type && server_data.type == 'ppas') ||
- server_data.server_type == 'ppas') {
- preference_name = 'ppas_bin_dir';
- msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
- }
-
- var preference = pgBrowser.get_preference(module, preference_name);
-
- if (preference) {
- if (!preference.value) {
- alertify.alert(gettext('Configuration required'), msg);
- return;
- }
- } else {
- alertify.alert(
- gettext('Backup Error'),
- S(gettext('Failed to load preference %s of module %s')).sprintf(preference_name, module).value()
- );
- return;
- }
-
- var of_type = undefined;
-
- // Set Notes according to type of backup
- if (!_.isUndefined(params['globals']) && params['globals']) {
- of_type = 'globals';
- } else {
- of_type = 'server';
- }
-
- var DialogName = 'BackupDialog_' + of_type,
- DialogTitle = ((of_type == 'globals') ?
- gettext('Backup Globals...') :
- gettext('Backup Server...'));
-
- if (!alertify[DialogName]) {
- alertify.dialog(DialogName, function factory() {
- return {
- main: function(title) {
- this.set('title', title);
- },
- build: function() {
- alertify.pgDialogBuild.apply(this);
- },
- setup: function() {
- return {
- buttons: [{
- text: '',
- className: 'btn btn-default pull-left fa fa-lg fa-info',
- attrs: {
- name: 'object_help',
- type: 'button',
- url: 'backup.html',
- label: gettext('Backup'),
- },
- }, {
- text: '',
- key: 112,
- className: 'btn btn-default pull-left fa fa-lg fa-question',
- attrs: {
- name: 'dialog_help',
- type: 'button',
- label: gettext('Backup'),
- url: url_for('help.static', {
- 'filename': 'backup_dialog.html',
- }),
- },
- }, {
- text: gettext('Backup'),
- key: 13,
- className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
- 'data-btn-name': 'backup',
- }, {
- text: gettext('Cancel'),
- key: 27,
- className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
- 'data-btn-name': 'cancel',
- }],
- // Set options for dialog
- options: {
- title: DialogTitle,
- //disable both padding and overflow control.
- padding: !1,
- overflow: !1,
- model: 0,
- resizable: true,
- maximizable: true,
- pinnable: false,
- closableByDimmer: false,
- modal: false,
- },
- };
- },
- hooks: {
- // Triggered when the dialog is closed
- onclose: function() {
- if (this.view) {
- // clear our backform model/view
- this.view.remove({
- data: true,
- internal: true,
- silent: true,
- });
- }
- },
- },
- prepare: function() {
- var self = this;
- // Disable Backup button until user provides Filename
- this.__internal.buttons[2].element.disabled = true;
-
- var $container = $('<div class=\'backup_dialog\'></div>');
- // Find current/selected node
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (!d)
- return;
- // Create treeInfo
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
- // Instance of backbone model
- var newModel = new BackupModel({
- type: of_type,
- }, {
- node_info: treeInfo,
- }),
- fields = Backform.generateViewSchema(
- treeInfo, newModel, 'create', node, treeInfo.server, true
- );
-
- var view = this.view = new Backform.Dialog({
- el: $container,
- model: newModel,
- schema: fields,
- });
- // Add our class to alertify
- $(this.elements.body.childNodes[0]).addClass(
- 'alertify_tools_dialog_properties obj_properties'
- );
- // Render dialog
- view.render();
-
- this.elements.content.appendChild($container.get(0));
-
- var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
- commonUtils.findAndSetFocus(container);
-
- // Listen to model & if filename is provided then enable Backup button
- this.view.model.on('change', function() {
- if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
- this.errorModel.clear();
- self.__internal.buttons[2].element.disabled = false;
- } else {
- self.__internal.buttons[2].element.disabled = true;
- this.errorModel.set('file', gettext('Please provide a filename'));
- }
- });
- },
- // Callback functions when click on the buttons of the Alertify dialogs
- callback: function(e) {
- // Fetch current server id
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (e.button.element.name == 'dialog_help' || e.button.element.name == 'object_help') {
- e.cancel = true;
- pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
- node, i, e.button.element.getAttribute('label'));
- return;
- }
-
- if (e.button['data-btn-name'] === 'backup') {
-
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- var self = this,
- baseUrl = url_for('backup.create_server_job', {
- 'sid': treeInfo.server._id,
- }),
- args = this.view.model.toJSON();
-
- $.ajax({
- url: baseUrl,
- method: 'POST',
- data: {
- 'data': JSON.stringify(args),
- },
- success: function(res) {
- if (res.success) {
- alertify.success(gettext('Backup job created.'), 5);
- pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
- } else {
- console.warn(res);
- }
- },
- error: function(xhr) {
- try {
- var err = $.parseJSON(xhr.responseText);
- alertify.alert(
- gettext('Backup job failed.'),
- err.errormsg
- );
- } catch (e) {
- console.warn(e.stack || e);
- }
- },
- });
- }
- },
- };
- });
- }
- alertify[DialogName](true).resizeTo('60%', '50%');
+ start_backup_global_server: function(action, treeItem, params) {
+ let dialog = new globalBackupDialog.BackupDialog(
+ pgBrowser,
+ $,
+ alertify,
+ BackupModel
+ );
+ dialog.draw(action, treeItem, params);
},
// Callback to draw Backup Dialog for objects
backup_objects: function(action, treeItem) {
-
- var i = treeItem || pgBrowser.tree.selected(),
- server_data = null;
-
- while (i) {
- var node_data = pgBrowser.tree.itemData(i);
- if (node_data._type == 'server') {
- server_data = node_data;
- break;
- }
-
- if (pgBrowser.tree.hasParent(i)) {
- i = $(pgBrowser.tree.parent(i));
- } else {
- alertify.alert(
- gettext('Backup Error'),
- gettext('Please select server or child node from tree.')
- );
- break;
- }
- }
-
- if (!server_data) {
- return;
- }
-
- var module = 'paths',
- preference_name = 'pg_bin_dir',
- msg = gettext('Please set binary path for PostgreSQL Server from preferences.');
-
- if ((server_data.type && server_data.type == 'ppas') ||
- server_data.server_type == 'ppas') {
- preference_name = 'ppas_bin_dir';
- msg = gettext('Please set binary path for EDB Postgres Advanced Server from preferences.');
- }
-
- var preference = pgBrowser.get_preference(module, preference_name);
-
- if (preference) {
- if (!preference.value) {
- alertify.alert(gettext('Configuration required'), msg);
- return;
- }
- } else {
- alertify.alert(
- gettext('Backup Error'),
- S(gettext('Failed to load preference %s of module %s')).sprintf(preference_name, module).value()
- );
- return;
- }
-
- var title = S(gettext('Backup (%s: %s)')),
- tree = pgBrowser.tree,
- item = treeItem || tree.selected(),
- data = item && item.length == 1 && tree.itemData(item),
- node = data && data._type && pgBrowser.Nodes[data._type];
-
- if (!node)
- return;
-
- title = title.sprintf(node.label, data.label).value();
-
- if (!alertify.backup_objects) {
- // Create Dialog title on the fly with node details
- alertify.dialog('backup_objects', function factory() {
- return {
- main: function(title) {
- this.set('title', title);
- },
- build: function() {
- alertify.pgDialogBuild.apply(this);
- },
- setup: function() {
- return {
- buttons: [{
- text: '',
- className: 'btn btn-default pull-left fa fa-lg fa-info',
- attrs: {
- name: 'object_help',
- type: 'button',
- url: 'backup.html',
- label: gettext('Backup'),
- },
- }, {
- text: '',
- key: 112,
- className: 'btn btn-default pull-left fa fa-lg fa-question',
- attrs: {
- name: 'dialog_help',
- type: 'button',
- label: gettext('Backup'),
- url: url_for('help.static', {
- 'filename': 'backup_dialog.html',
- }),
- },
- }, {
- text: gettext('Backup'),
- key: 13,
- className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
- 'data-btn-name': 'backup',
- }, {
- text: gettext('Cancel'),
- key: 27,
- className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
- 'data-btn-name': 'cancel',
- }],
- // Set options for dialog
- options: {
- title: title,
- //disable both padding and overflow control.
- padding: !1,
- overflow: !1,
- model: 0,
- resizable: true,
- maximizable: true,
- pinnable: false,
- closableByDimmer: false,
- modal: false,
- },
- };
- },
- hooks: {
- // triggered when the dialog is closed
- onclose: function() {
- if (this.view) {
- this.view.remove({
- data: true,
- internal: true,
- silent: true,
- });
- }
- },
- },
- prepare: function() {
- var self = this;
- // Disable Backup button until user provides Filename
- this.__internal.buttons[2].element.disabled = true;
- var $container = $('<div class=\'backup_dialog\'></div>');
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- var newModel = new BackupObjectModel({}, {
- node_info: treeInfo,
- }),
- fields = Backform.generateViewSchema(
- treeInfo, newModel, 'create', node, treeInfo.server, true
- );
-
- var view = this.view = new Backform.Dialog({
- el: $container,
- model: newModel,
- schema: fields,
- });
-
- $(this.elements.body.childNodes[0]).addClass(
- 'alertify_tools_dialog_properties obj_properties'
- );
-
- view.render();
-
- this.elements.content.appendChild($container.get(0));
-
- if(view) {
- view.$el.attr('tabindex', -1);
- // var dialogTabNavigator = pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
- commonUtils.findAndSetFocus(container);
- }
- // Listen to model & if filename is provided then enable Backup button
- this.view.model.on('change', function() {
- if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
- this.errorModel.clear();
- self.__internal.buttons[2].element.disabled = false;
- } else {
- self.__internal.buttons[2].element.disabled = true;
- this.errorModel.set('file', gettext('Please provide filename'));
- }
- });
-
- },
- // Callback functions when click on the buttons of the Alertify dialogs
- callback: function(e) {
- // Fetch current server id
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (e.button.element.name == 'dialog_help' || e.button.element.name == 'object_help') {
- e.cancel = true;
- pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
- node, i, e.button.element.getAttribute('label'));
- return;
- }
-
- if (e.button['data-btn-name'] === 'backup') {
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- // Set current database into model
- this.view.model.set('database', treeInfo.database._label);
-
- // We will remove once object tree is implemented
- // If selected node is Schema then add it in model
- if (d._type == 'schema') {
- var schemas = [];
- schemas.push(d._label);
- this.view.model.set('schemas', schemas);
- }
- // If selected node is Table then add it in model along with
- // its schema
- if (d._type == 'table') {
- this.view.model.set(
- 'tables', [
- [treeInfo.schema._label, d._label],
- ]
- );
- }
-
- // Remove ratio attribute from model if it has empty string.
- // The valid value can be between 0 to 9.
- if (_.isEmpty(this.view.model.get('ratio'))) {
- this.view.model.unset('ratio');
- }
-
- var self = this,
- baseUrl = url_for('backup.create_object_job', {
- 'sid': treeInfo.server._id,
- }),
- args = this.view.model.toJSON();
-
- $.ajax({
- url: baseUrl,
- method: 'POST',
- data: {
- 'data': JSON.stringify(args),
- },
- success: function(res) {
- if (res.success) {
- alertify.success(gettext('Backup job created.'), 5);
- pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
- }
- },
- error: function(xhr) {
- try {
- var err = $.parseJSON(xhr.responseText);
- alertify.alert(
- gettext('Backup job failed.'),
- err.errormsg
- );
- } catch (e) {
- console.warn(e.stack || e);
- }
- },
- });
- }
- },
- };
- });
- }
- alertify.backup_objects(title).resizeTo('65%', '60%');
+ let dialog = new globalBackupDialog.BackupDialog(
+ pgBrowser,
+ $,
+ alertify,
+ BackupObjectModel
+ );
+ dialog.draw(action, treeItem, null);
},
};
return pgBrowser.Backup;
diff --git a/web/pgadmin/tools/datagrid/static/js/datagrid.js b/web/pgadmin/tools/datagrid/static/js/datagrid.js
index 473f20ad..d8d1c902 100644
--- a/web/pgadmin/tools/datagrid/static/js/datagrid.js
+++ b/web/pgadmin/tools/datagrid/static/js/datagrid.js
@@ -1,10 +1,13 @@
define('pgadmin.datagrid', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'pgadmin.alertifyjs', 'sources/pgadmin', 'bundled_codemirror',
- 'sources/sqleditor_utils', 'backbone', 'wcdocker',
+ 'sources/sqleditor_utils', 'backbone', 'sources/datagrid/show_data',
+ 'sources/datagrid/get_panel_title',
+ 'sources/datagrid/show_query_tool',
+ 'wcdocker',
], function(
gettext, url_for, $, _, alertify, pgAdmin, codemirror, sqlEditorUtils,
- Backbone
+ Backbone, showData, panelTitle, showQueryTool
) {
// Some scripts do export their object in the window only.
// Generally the one, which do no have AMD support.
@@ -161,55 +164,7 @@ define('pgadmin.datagrid', [
// This is a callback function to show data when user click on menu item.
show_data_grid: function(data, i) {
- var self = this,
- d = pgAdmin.Browser.tree.itemData(i);
- if (d === undefined) {
- alertify.alert(
- gettext('Data Grid Error'),
- gettext('No object selected.')
- );
- return;
- }
-
- // Get the parent data from the tree node hierarchy.
- var node = pgBrowser.Nodes[d._type],
- parentData = node.getTreeNodeHierarchy(i);
-
- // If server, database or schema is undefined then return from the function.
- if (parentData.server === undefined || parentData.database === undefined) {
- return;
- }
- // If schema, view, catalog object all are undefined then return from the function.
- if (parentData.schema === undefined && parentData.view === undefined &&
- parentData.catalog === undefined) {
- return;
- }
-
- var nsp_name = '';
-
- if (parentData.schema != undefined) {
- nsp_name = parentData.schema.label;
- }
- else if (parentData.view != undefined) {
- nsp_name = parentData.view.label;
- }
- else if (parentData.catalog != undefined) {
- nsp_name = parentData.catalog.label;
- }
- var url_params = {
- 'cmd_type': data.mnuid,
- 'obj_type': d._type,
- 'sgid': parentData.server_group._id,
- 'sid': parentData.server._id,
- 'did': parentData.database._id,
- 'obj_id': d._id,
- };
-
- var baseUrl = url_for('datagrid.initialize_datagrid', url_params);
- var grid_title = parentData.server.label + ' - ' + parentData.database.label + ' - '
- + nsp_name + '.' + d.label;
-
- self.create_transaction(baseUrl, null, 'false', parentData.server.server_type, '', grid_title, '');
+ showData.showDataGrid(this, pgBrowser, alertify, data, i);
},
// This is a callback function to show filtered data when user click on menu item.
@@ -382,63 +337,12 @@ define('pgadmin.datagrid', [
},
get_panel_title: function() {
- // Get the parent data from the tree node hierarchy.
- var tree = pgAdmin.Browser.tree,
- selected_item = tree.selected(),
- item_data = tree.itemData(selected_item);
-
- var node = pgBrowser.Nodes[item_data._type],
- parentData = node.getTreeNodeHierarchy(selected_item);
-
- // If server, database is undefined then return from the function.
- if (parentData.server === undefined) {
- return;
- }
- // If Database is not available then use default db
- var db_label = parentData.database ? parentData.database.label
- : parentData.server.db;
-
- var grid_title = db_label + ' on ' + parentData.server.user.name + '@' +
- parentData.server.label;
- return grid_title;
+ return panelTitle.getPanelTitle(pgBrowser);
},
// This is a callback function to show query tool when user click on menu item.
- show_query_tool: function(url, i, panel_title) {
- var sURL = url || '',
- d = pgAdmin.Browser.tree.itemData(i);
-
- panel_title = panel_title || '';
- if (d === undefined) {
- alertify.alert(
- gettext('Query Tool Error'),
- gettext('No object selected.')
- );
- return;
- }
-
- // Get the parent data from the tree node hierarchy.
- var node = pgBrowser.Nodes[d._type],
- parentData = node.getTreeNodeHierarchy(i);
-
- // If server, database is undefined then return from the function.
- if (parentData.server === undefined) {
- return;
- }
-
- var url_params = {
- 'sgid': parentData.server_group._id,
- 'sid': parentData.server._id,
- },
- url_endpoint = 'datagrid.initialize_query_tool';
- // If database not present then use Maintenance database
- // We will handle this at server side
- if (parentData.database) {
- url_params['did'] = parentData.database._id;
- url_endpoint = 'datagrid.initialize_query_tool_with_did';
- }
- var baseUrl = url_for(url_endpoint, url_params);
-
- this.create_transaction(baseUrl, null, 'true', parentData.server.server_type, sURL, panel_title, '', false);
+ show_query_tool: function(url, aciTreeIdentifier, panelTitle) {
+ showQueryTool.showQueryTool(this, pgBrowser, alertify, url,
+ aciTreeIdentifier, panelTitle);
},
create_transaction: function(baseUrl, target, is_query_tool, server_type, sURL, panel_title, sql_filter, recreate) {
var self = this;
diff --git a/web/regression/javascript/backup/backup_dialog_spec.js b/web/regression/javascript/backup/backup_dialog_spec.js
new file mode 100644
index 00000000..e272f094
--- /dev/null
+++ b/web/regression/javascript/backup/backup_dialog_spec.js
@@ -0,0 +1,651 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {BackupDialog} from '../../../pgadmin/static/js/backup/backup_dialog';
+import {TreeFake} from '../tree/tree_fake';
+import MockAdapter from 'axios-mock-adapter';
+import axios from 'axios/index';
+import {FakeModel} from '../fake_model';
+import {DialogWrapper} from "../../../pgadmin/static/js/backup/backup_dialog_wrapper";
+
+const context = describe;
+
+describe('ObjectBackupDialog', () => {
+ let backupDialog;
+ let backupNode;
+ let pgBrowser;
+ let jquerySpy;
+ let alertifySpy;
+ let backupModelSpy;
+ let backform;
+
+
+ let rootNode;
+ let serverTreeNode;
+ let databaseTreeNode;
+ let ppasServerTreeNode;
+ let noDataNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: {
+ hasId: true,
+ label: 'server',
+ getTreeNodeHierarchy: jasmine.createSpy('server.getTreeNodeHierarchy'),
+ },
+ database: {
+ hasId: true,
+ label: 'database',
+ getTreeNodeHierarchy: jasmine.createSpy('db.getTreeNodeHierarchy'),
+ },
+ schema: {
+ hasId: true,
+ label: 'schema',
+ getTreeNodeHierarchy: jasmine.createSpy('db.getTreeNodeHierarchy'),
+ },
+ }
+ };
+ pgBrowser.Nodes.server.hasId = true;
+ pgBrowser.Nodes.database.hasId = true;
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ backupModelSpy = jasmine.createSpy('backupModelSpy');
+ backupNode = {};
+ backform = jasmine.createSpyObj('backform', ['generateViewSchema', 'Dialog']);
+
+ rootNode = pgBrowser.treeMenu.addNewNode('level1', {}, []);
+ serverTreeNode = pgBrowser.treeMenu.addNewNode('level1.1', {
+ _type: 'server',
+ _id: 10,
+ }, ['level1']);
+ databaseTreeNode = pgBrowser.treeMenu.addNewNode(
+ 'level1.1.1', {
+ _type: 'database',
+ _id: 11,
+ label: 'some_database',
+ _label: 'some_database_label',
+ }, ['level1', 'level1.1']);
+ ppasServerTreeNode = pgBrowser.treeMenu.addNewNode('level1.2', {
+ _type: 'server',
+ server_type: 'ppas',
+ }, ['level1']);
+ pgBrowser.treeMenu.addNewNode('level3', {}, ['level1', 'level1.2']);
+ noDataNode = pgBrowser.treeMenu.addNewNode(
+ 'level3.1', undefined, ['level1', 'level1.2', 'level3']);
+ });
+
+ describe('#draw', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ alertifySpy['backup_objects'] = jasmine.createSpy('backup_objects');
+ backupDialog = new BackupDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ backupModelSpy
+ );
+
+ pgBrowser.get_preference = jasmine.createSpy('get_preferences');
+ });
+
+ context('there are no ancestors of the type server', () => {
+ it('does not create a dialog', () => {
+ pgBrowser.treeMenu.setSelectedNode([rootNode]);
+ backupDialog.draw(null, null, null);
+ expect(alertifySpy['backup_objects']).not.toHaveBeenCalled();
+ });
+
+ it('display an alert with a Backup Error', () => {
+ backupDialog.draw(null, [rootNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Please select server or child node from the browser tree.'
+ );
+ });
+ });
+
+ context('there is an ancestor of the type server', () => {
+ context('no preference can be found', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue(undefined);
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference pg_bin_dir of module paths'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference ppas_bin_dir of module paths'
+ );
+ });
+ });
+ });
+
+ context('preference can be found', () => {
+ context('binary folder is not configured', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue({});
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the PostgreSQL Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the EDB Advanced Server Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+ });
+
+ context('binary folder is configured', () => {
+ let backupDialogResizeToSpy;
+ let serverResizeToSpy;
+ beforeEach(() => {
+ backupDialogResizeToSpy = jasmine.createSpyObj('backupDialogResizeToSpy', ['resizeTo']);
+ alertifySpy['backup_objects'].and
+ .returnValue(backupDialogResizeToSpy);
+ pgBrowser.get_preference.and.returnValue({value: '/some/path'});
+ });
+
+ it('displays the dialog', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy['backup_objects']).toHaveBeenCalledWith(true);
+ expect(backupDialogResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
+ });
+ });
+ });
+ });
+ });
+
+ describe('#dialogFactory', () => {
+ describe('#prepare', () => {
+ let backupJQueryContainer;
+ let viewSchema;
+ let generatedBackupModel;
+ let backupNodeChildNode;
+ beforeEach(() => {
+ backupJQueryContainer = jasmine.createSpyObj('backupJQueryContainer', ['get']);
+ backupJQueryContainer.get.and.returnValue(backupJQueryContainer);
+ viewSchema = {};
+ generatedBackupModel = {};
+ backupNode.__internal = {
+ buttons: [
+ {}, {},
+ {
+ element: {
+ disabled: false,
+ }
+ }
+ ]
+ };
+ backupNode.elements = {
+ body: {
+ childNodes: [
+ {}
+ ]
+ },
+ content: jasmine.createSpyObj('content', ['appendChild']),
+ };
+
+ backform.generateViewSchema.and.returnValue(viewSchema);
+ backupModelSpy.and.returnValue(generatedBackupModel);
+
+ backupDialog = new BackupDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ backupModelSpy,
+ backform
+ );
+
+ backupNodeChildNode = jasmine.createSpyObj('something', ['addClass']);
+
+ jquerySpy.and.callFake((selector) => {
+ if (selector === '<div class=\'backup_dialog\'></div>') {
+ return backupJQueryContainer;
+ } else if (selector === backupNode.elements.body.childNodes[0]) {
+ return backupNodeChildNode;
+ }
+ })
+ });
+
+ context('selected tree node has no data', () => {
+ beforeEach(() => {
+ pgBrowser.treeMenu.setSelectedNode([noDataNode]);
+ const dialog = backupDialog.dialogFactory(null, 'backup_objects');
+ Object.assign(dialog, backupNode).prepare();
+ });
+
+ it('does not create a backform dialog', () => {
+ expect(backform.Dialog).not.toHaveBeenCalledWith();
+ });
+
+ it('disables the button submit button until a filename is selected', () => {
+ expect(backupNode.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('no tree element is selected', () => {
+ beforeEach(() => {
+ const dialog = backupDialog.dialogFactory(null, 'backup_objects');
+ Object.assign(dialog, backupNode).prepare();
+ });
+
+ it('does not create a backform dialog', () => {
+ expect(backform.Dialog).not.toHaveBeenCalledWith();
+ });
+
+ it('disables the button submit button until a filename is selected', () => {
+ expect(backupNode.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('tree element is selected', () => {
+ let treeHierarchyInformation;
+ let dialogSpy;
+ let dialog;
+ beforeEach(() => {
+ treeHierarchyInformation = {
+ database: {
+ _type: 'database',
+ _id: 11,
+ label: 'some_database',
+ _label: 'some_database_label',
+ priority: 0,
+ },
+ server: {
+ _type: 'server',
+ _id: 10,
+ priority: -1,
+ }
+ };
+ pgBrowser.treeMenu.setSelectedNode([{id: 'level1.1.1'}]);
+ pgBrowser.Nodes['database'].getTreeNodeHierarchy.and
+ .returnValue(treeHierarchyInformation);
+ dialogSpy = jasmine.createSpyObj('newView', ['render']);
+ dialogSpy.$el = jasmine.createSpyObj('$el', ['find']);
+ dialogSpy.model = jasmine.createSpyObj('model', ['on'])
+ dialogSpy.$el.find.and.returnValue([]);
+
+ backform.Dialog.and.returnValue(dialogSpy);
+
+ dialog = backupDialog.dialogFactory(null, 'backup_objects');
+ dialog.set = jasmine.createSpy('dialog.set');
+ Object.assign(dialog, backupNode).prepare();
+ });
+
+ it('creates a backform dialog and displays it', () => {
+ expect(backform.Dialog).toHaveBeenCalledWith({
+ el: backupJQueryContainer,
+ model: generatedBackupModel,
+ schema: viewSchema,
+ });
+
+ expect(dialogSpy.render).toHaveBeenCalled();
+ });
+
+ it('add alertify classes to backup node childnode', () => {
+ expect(backupNodeChildNode.addClass)
+ .toHaveBeenCalledWith('alertify_tools_dialog_properties obj_properties');
+ });
+
+ it('disables the button submit button until a filename is selected', () => {
+ expect(backupNode.__internal.buttons[2].element.disabled).toBe(true);
+ });
+
+ it('generates a new backup model', () => {
+ expect(backupModelSpy).toHaveBeenCalledWith(
+ {},
+ {node_info: treeHierarchyInformation}
+ );
+ });
+
+ it('add the new dialog to the backup node HTML', () => {
+ expect(backupNode.elements.content.appendChild).toHaveBeenCalledWith(backupJQueryContainer);
+ });
+
+ it('sets the title to "Backup (database: some_database)"', () => {
+ expect(dialog.set).toHaveBeenCalledWith('title', 'Backup (database: some_database)');
+ });
+ });
+ });
+
+ describe('#onButtonClicked', () => {
+ let backupJQueryContainer;
+ let backupNodeChildNode;
+ let dialog;
+ let networkMock;
+
+ beforeEach(() => {
+ networkMock = new MockAdapter(axios);
+ backupJQueryContainer = jasmine.createSpyObj('backupJQueryContainer', ['get']);
+ backupJQueryContainer.get.and.returnValue(backupJQueryContainer);
+ pgBrowser.showHelp = jasmine.createSpy('showHelp');
+ alertifySpy = jasmine.createSpyObj('alertify', ['success', 'alert']);
+
+ backupDialog = new BackupDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ backupModelSpy,
+ backform
+ );
+
+ backupNodeChildNode = jasmine.createSpyObj('something', ['addClass']);
+
+ dialog = Object.assign(
+ backupDialog.dialogFactory(null, 'backup_objects'),
+ backupNode
+ );
+ });
+
+ afterEach(() => {
+ networkMock.restore();
+ });
+
+ context('dialog help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ pgBrowser.treeMenu.setSelectedNode([{id: 'level1.1.1'}]);
+ networkCalled = false;
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ element: {
+ name: 'dialog_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ }
+ }
+ }
+ };
+ dialog.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'dialog_help',
+ 'http://someurl',
+ pgBrowser.Nodes['database'],
+ databaseTreeNode,
+ 'some label'
+ )
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('object help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ pgBrowser.treeMenu.setSelectedNode([{id: 'level1.1.1'}]);
+ networkCalled = false;
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ element: {
+ name: 'object_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ }
+ }
+ }
+ };
+ dialog.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'object_help',
+ 'http://someurl',
+ pgBrowser.Nodes['database'],
+ databaseTreeNode,
+ 'some label'
+ )
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('backup button was pressed', () => {
+ context('no tree node is selected', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onPost('/backup/job/10/object').reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ }
+ }
+ }
+ };
+ dialog.callback(event);
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('selected node has no data', () => {
+ let networkCalled;
+ beforeEach(() => {
+ pgBrowser.treeMenu.setSelectedNode([noDataNode]);
+ networkCalled = false;
+ networkMock.onPost('/backup/job/10/object').reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ }
+ }
+ }
+ };
+ dialog.callback(event);
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('selected node has data', () => {
+ beforeEach(() => {
+ pgBrowser.treeMenu.setSelectedNode([{id: 'level1.1.1'}]);
+ pgBrowser.Events = jasmine.createSpyObj('Events', ['trigger']);
+
+ backupNode.view = {
+ model: new FakeModel(),
+ };
+ backupNode.view.model.set('dqoute', false);
+ backupNode.view.model.set('file', 'test');
+ backupNode.view.model.set('verbose', true);
+
+ const event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ }
+ }
+ }
+ };
+ Object.assign(dialog, backupNode).callback(event);
+ });
+ context('creation of backup job successfull', () => {
+ let dataSentToServer;
+ beforeEach(() => {
+ networkMock.onPost('/backup/job/10/object').reply((request) => {
+ dataSentToServer = request.data;
+ return [200, {}];
+ });
+ });
+
+ it('creates an success alert box', (done) => {
+ setTimeout(() => {
+ expect(alertifySpy.success).toHaveBeenCalledWith('Backup job' +
+ ' created.', 5);
+ done();
+ }, 0);
+ });
+
+ it('trigger background process', (done) => {
+ setTimeout(() => {
+ expect(pgBrowser.Events.trigger).toHaveBeenCalledWith(
+ 'pgadmin-bgprocess:created',
+ jasmine.anything()
+ );
+ done();
+ }, 0);
+ });
+
+ it('send correct data to server', () => {
+ expect(dataSentToServer).toEqual(JSON.stringify({
+ dqoute: false,
+ file: 'test',
+ verbose: true,
+ database: 'some_database_label'
+ }));
+ });
+ });
+
+ context('error creating backup job', () => {
+ beforeEach(() => {
+ const response = {
+ errormsg: 'some error message'
+ };
+
+ networkMock.onPost('/backup/job/10/object').reply(() => {
+ return [500, response];
+ });
+ });
+
+ it('creates an alert box', (done) => {
+ setTimeout(() => {
+ expect(alertifySpy.alert).toHaveBeenCalledWith('Backup job' +
+ ' failed.', 'some error message');
+ done();
+ }, 0);
+ });
+ });
+ });
+ });
+ });
+ });
+
+ describe('BackupDialogWrapper', () => {
+ describe('#setExtraParameters', () => {
+ let model;
+ let dialog;
+ let schemaNode;
+ let tableNode;
+ beforeEach(() => {
+ model = new FakeModel();
+ dialog = new DialogWrapper({}, {
+ pgBrowser: pgBrowser,
+ }, '', 'backup_objects');
+ dialog.view = {
+ model: model,
+ };
+ schemaNode = pgBrowser.treeMenu.addNewNode(
+ 'schema', {
+ _label: 'schema_label',
+ _type: 'schema',
+ }, ['level1', 'level1.1', 'level1.1.1']);
+ tableNode = pgBrowser.treeMenu.addNewNode(
+ 'table', {
+ _label: 'table_label',
+ _type: 'table',
+ }, ['level1', 'level1.1', 'level1.1.1', 'schema']);
+ });
+
+ context('type is schema', () => {
+ it('add schema array to the model', () => {
+ dialog.setExtraParameters(schemaNode);
+ expect(model.get('schemas')).toEqual(['schema_label']);
+ });
+ });
+
+ context('type is table', () => {
+ it('add schema and table to the model', () => {
+ dialog.setExtraParameters(tableNode);
+ expect(model.get('tables')).toEqual([['schema_label', 'table_label']]);
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/backup/global_server_backup_dialog_spec.js b/web/regression/javascript/backup/global_server_backup_dialog_spec.js
new file mode 100644
index 00000000..0b229038
--- /dev/null
+++ b/web/regression/javascript/backup/global_server_backup_dialog_spec.js
@@ -0,0 +1,585 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {BackupDialog} from '../../../pgadmin/static/js/backup/backup_dialog';
+import {TreeFake} from '../tree/tree_fake';
+import MockAdapter from 'axios-mock-adapter';
+import axios from 'axios/index';
+import {FakeModel} from '../fake_model';
+
+const context = describe;
+
+describe('GlobalServerBackupDialog', () => {
+ let backupDialog;
+ let backupNode;
+ let pgBrowser;
+ let jquerySpy;
+ let alertifySpy;
+ let backupModelSpy;
+ let backform;
+
+
+ let rootNode;
+ let serverTreeNode;
+ let ppasServerTreeNode;
+ let noDataNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: jasmine.createSpyObj('Node[server]', ['getTreeNodeHierarchy']),
+ }
+ };
+ pgBrowser.Nodes.server.hasId = true;
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ backupModelSpy = jasmine.createSpy('backupModelSpy');
+ backupNode = {};
+ backform = jasmine.createSpyObj('backform', ['generateViewSchema', 'Dialog']);
+
+ rootNode = pgBrowser.treeMenu.addNewNode('level1', {}, []);
+ serverTreeNode = pgBrowser.treeMenu.addNewNode('level1.1', {
+ _type: 'server',
+ _id: 10,
+ }, ['level1']);
+ ppasServerTreeNode = pgBrowser.treeMenu.addNewNode('level1.2', {
+ _type: 'server',
+ server_type: 'ppas',
+ }, ['level1']);
+ pgBrowser.treeMenu.addNewNode('level3', {}, ['level1', 'level1.2']);
+ noDataNode = pgBrowser.treeMenu.addNewNode(
+ 'level3.1', undefined, ['level1', 'level1.2', 'level3']);
+ });
+
+ describe('#draw', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ alertifySpy['BackupDialog_globals'] = jasmine.createSpy('BackupDialog_globals');
+ alertifySpy['BackupDialog_server'] = jasmine.createSpy('BackupDialog_server');
+ backupDialog = new BackupDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ backupModelSpy
+ );
+
+ pgBrowser.get_preference = jasmine.createSpy('get_preferences');
+ });
+
+ context('there are no ancestors of the type server', () => {
+ it('does not create a dialog', () => {
+ pgBrowser.treeMenu.setSelectedNode([{id: 'level1'}]);
+ backupDialog.draw(null, null, null);
+ expect(alertifySpy['BackupDialog_globals']).not.toHaveBeenCalled();
+ expect(alertifySpy['BackupDialog_server']).not.toHaveBeenCalled();
+ });
+
+ it('display an alert with a Backup Error', () => {
+ backupDialog.draw(null, [rootNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Please select server or child node from the browser tree.'
+ );
+ });
+ });
+
+ context('there is an ancestor of the type server', () => {
+ context('no preference can be found', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue(undefined);
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference pg_bin_dir of module paths'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference ppas_bin_dir of module paths'
+ );
+ });
+ });
+ });
+
+ context('preference can be found', () => {
+ context('binary folder is not configured', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue({});
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the PostgreSQL Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the EDB Advanced Server Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+ });
+
+ context('binary folder is configured', () => {
+ let globalResizeToSpy;
+ let serverResizeToSpy;
+ beforeEach(() => {
+ globalResizeToSpy = jasmine.createSpyObj('globals', ['resizeTo']);
+ alertifySpy['BackupDialog_globals'].and
+ .returnValue(globalResizeToSpy);
+ serverResizeToSpy = jasmine.createSpyObj('server', ['resizeTo']);
+ alertifySpy['BackupDialog_server'].and
+ .returnValue(serverResizeToSpy);
+ pgBrowser.get_preference.and.returnValue({value: '/some/path'});
+ });
+
+ context('dialog for global backup', () => {
+ it('displays the dialog', () => {
+ backupDialog.draw(null, [serverTreeNode], {globals: true});
+ expect(alertifySpy['BackupDialog_globals']).toHaveBeenCalledWith(true);
+ expect(globalResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
+ });
+ });
+
+ context('dialog for global backup', () => {
+ it('displays the dialog', () => {
+ backupDialog.draw(null, [serverTreeNode], {server: true});
+ expect(alertifySpy['BackupDialog_server']).toHaveBeenCalledWith(true);
+ expect(serverResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
+ });
+ });
+ });
+ });
+ });
+ });
+
+ describe('#dialogFactory', () => {
+ describe('#prepare', () => {
+ let backupJQueryContainer;
+ let viewSchema;
+ let generatedBackupModel;
+ let backupNodeChildNode;
+ beforeEach(() => {
+ backupJQueryContainer = jasmine.createSpyObj('backupJQueryContainer', ['get']);
+ backupJQueryContainer.get.and.returnValue(backupJQueryContainer);
+ viewSchema = {};
+ generatedBackupModel = {};
+ backupNode.__internal = {
+ buttons: [
+ {}, {},
+ {
+ element: {
+ disabled: false,
+ }
+ }
+ ]
+ };
+ backupNode.elements = {
+ body: {
+ childNodes: [
+ {}
+ ]
+ },
+ content: jasmine.createSpyObj('content', ['appendChild']),
+ };
+
+ backform.generateViewSchema.and.returnValue(viewSchema);
+ backupModelSpy.and.returnValue(generatedBackupModel);
+
+ backupDialog = new BackupDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ backupModelSpy,
+ backform
+ );
+
+ backupNodeChildNode = jasmine.createSpyObj('something', ['addClass']);
+
+ jquerySpy.and.callFake((selector) => {
+ if (selector === '<div class=\'backup_dialog\'></div>') {
+ return backupJQueryContainer;
+ } else if (selector === backupNode.elements.body.childNodes[0]) {
+ return backupNodeChildNode;
+ }
+ })
+ });
+
+ context('selected tree node has no data', () => {
+ beforeEach(() => {
+ pgBrowser.treeMenu.setSelectedNode([noDataNode]);
+ const dialog = backupDialog.dialogFactory('Some title', 'globals');
+ Object.assign(dialog, backupNode).prepare();
+ });
+
+ it('does not create a backform dialog', () => {
+ expect(backform.Dialog).not.toHaveBeenCalledWith();
+ });
+
+ it('disables the button submit button until a filename is selected', () => {
+ expect(backupNode.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('no tree element is selected', () => {
+ beforeEach(() => {
+ const dialog = backupDialog.dialogFactory('Some title', 'globals');
+ Object.assign(dialog, backupNode).prepare();
+ });
+
+ it('does not create a backform dialog', () => {
+ expect(backform.Dialog).not.toHaveBeenCalledWith();
+ });
+
+ it('disables the button submit button until a filename is selected', () => {
+ expect(backupNode.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('tree element is selected', () => {
+ let treeHierarchyInformation;
+ let dialogSpy;
+ beforeEach(() => {
+ treeHierarchyInformation = {
+ server: {
+ _type: 'server',
+ _id: 10,
+ priority: 0,
+ }
+ };
+ pgBrowser.treeMenu.setSelectedNode([serverTreeNode]);
+ pgBrowser.Nodes['server'].getTreeNodeHierarchy.and
+ .returnValue(treeHierarchyInformation);
+ dialogSpy = jasmine.createSpyObj('newView', ['render']);
+ dialogSpy.$el = jasmine.createSpyObj('$el', ['find']);
+ dialogSpy.model = jasmine.createSpyObj('model', ['on'])
+ dialogSpy.$el.find.and.returnValue([]);
+
+ backform.Dialog.and.returnValue(dialogSpy);
+
+ const dialog = backupDialog.dialogFactory('Some title', 'globals');
+ Object.assign(dialog, backupNode).prepare();
+ });
+
+ it('creates a backform dialog and displays it', () => {
+ expect(backform.Dialog).toHaveBeenCalledWith({
+ el: backupJQueryContainer,
+ model: generatedBackupModel,
+ schema: viewSchema,
+ });
+
+ expect(dialogSpy.render).toHaveBeenCalled();
+ });
+
+ it('add alertify classes to backup node childnode', () => {
+ expect(backupNodeChildNode.addClass)
+ .toHaveBeenCalledWith('alertify_tools_dialog_properties obj_properties');
+ });
+
+ it('disables the button submit button until a filename is selected', () => {
+ expect(backupNode.__internal.buttons[2].element.disabled).toBe(true);
+ });
+
+ it('generates a new backup model', () => {
+ expect(backupModelSpy).toHaveBeenCalledWith(
+ {type: 'globals'},
+ {node_info: treeHierarchyInformation}
+ );
+ });
+
+ it('add the new dialog to the backup node HTML', () => {
+ expect(backupNode.elements.content.appendChild).toHaveBeenCalledWith(backupJQueryContainer);
+ });
+ });
+ });
+
+ describe('#onButtonClicked', () => {
+ let backupJQueryContainer;
+ let backupNodeChildNode;
+ let dialog;
+ let networkMock;
+
+ beforeEach(() => {
+ networkMock = new MockAdapter(axios);
+ backupJQueryContainer = jasmine.createSpyObj('backupJQueryContainer', ['get']);
+ backupJQueryContainer.get.and.returnValue(backupJQueryContainer);
+ pgBrowser.showHelp = jasmine.createSpy('showHelp');
+ alertifySpy = jasmine.createSpyObj('alertify', ['success', 'alert']);
+
+ backupDialog = new BackupDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ backupModelSpy,
+ backform
+ );
+
+ backupNodeChildNode = jasmine.createSpyObj('something', ['addClass']);
+
+ dialog = Object.assign(backupDialog.dialogFactory('Some title', 'server'), backupNode);
+ });
+
+ afterEach(() => {
+ networkMock.restore();
+ });
+
+ context('dialog help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ pgBrowser.treeMenu.setSelectedNode([serverTreeNode]);
+ networkCalled = false;
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ element: {
+ name: 'dialog_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ }
+ }
+ }
+ };
+ dialog.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'dialog_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ )
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('object help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ pgBrowser.treeMenu.setSelectedNode([serverTreeNode]);
+ networkCalled = false;
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ element: {
+ name: 'object_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ }
+ }
+ }
+ };
+ dialog.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'object_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ )
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('backup button was pressed', () => {
+ context('no tree node is selected', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ }
+ }
+ }
+ };
+ dialog.callback(event);
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('selected node has no data', () => {
+ let networkCalled;
+ beforeEach(() => {
+ pgBrowser.treeMenu.setSelectedNode([noDataNode]);
+ networkCalled = false;
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ }
+ }
+ }
+ };
+ dialog.callback(event);
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('selected node has data', () => {
+ beforeEach(() => {
+ pgBrowser.treeMenu.setSelectedNode([serverTreeNode]);
+ pgBrowser.Events = jasmine.createSpyObj('Events', ['trigger']);
+
+ backupNode.view = {
+ model: new FakeModel(),
+ };
+ backupNode.view.model.set('dqoute', false);
+ backupNode.view.model.set('file', 'test');
+ backupNode.view.model.set('type', 'globals');
+ backupNode.view.model.set('verbose', true);
+
+ const event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ }
+ }
+ }
+ };
+ Object.assign(dialog, backupNode).callback(event);
+ });
+ context('creation of backup job successfull', () => {
+ let dataSentToServer;
+ beforeEach(() => {
+ networkMock.onPost('/backup/job/10').reply((request) => {
+ dataSentToServer = request.data;
+ return [200, {}];
+ });
+ });
+
+ it('creates an success alert box', (done) => {
+ setTimeout(() => {
+ expect(alertifySpy.success).toHaveBeenCalledWith('Backup job' +
+ ' created.', 5);
+ done();
+ }, 0);
+ });
+
+ it('trigger background process', (done) => {
+ setTimeout(() => {
+ expect(pgBrowser.Events.trigger).toHaveBeenCalledWith(
+ 'pgadmin-bgprocess:created',
+ jasmine.anything()
+ );
+ done();
+ }, 0);
+ });
+
+ it('send correct data to server', () => {
+ expect(dataSentToServer).toEqual(JSON.stringify({
+ dqoute: false,
+ file: 'test',
+ type: 'globals',
+ verbose: true
+ }));
+ });
+ });
+
+ context('error creating backup job', () => {
+ beforeEach(() => {
+ const response = {
+ errormsg: 'some error message'
+ };
+
+ networkMock.onPost('/backup/job/10').reply(() => {
+ return [500, response];
+ });
+ });
+
+ it('creates an alert box', (done) => {
+ setTimeout(() => {
+ expect(alertifySpy.alert).toHaveBeenCalledWith('Backup job' +
+ ' failed.', 'some error message');
+ done();
+ }, 0);
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/backup/menu_utils_spec.js b/web/regression/javascript/backup/menu_utils_spec.js
new file mode 100644
index 00000000..8f776976
--- /dev/null
+++ b/web/regression/javascript/backup/menu_utils_spec.js
@@ -0,0 +1,167 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+
+import {TreeFake} from "../tree/tree_fake";
+import {
+ menuEnabledServer,
+ menuEnabled
+} from "../../../pgadmin/static/js/backup/menu_utils";
+
+const context = describe;
+
+describe('backup.menuUtils', () => {
+ describe('#menuEnabled', () => {
+ let ourBrowser;
+ beforeEach(() => {
+ const tree = new TreeFake();
+ ourBrowser = {
+ treeMenu: tree,
+ };
+ tree.addNewNode('level1', {}, []);
+ tree.addNewNode('level1.1', {_type: 'catalog'}, ['level1']);
+ tree.addNewNode('level1.1.1', {_type: 'database'}, ['level1', 'level1.1']);
+ tree.addNewNode('level1.2', {_type: 'bamm'}, ['level1']);
+ tree.addNewNode('level1.2.1', {
+ _type: 'database',
+ allowConn: true
+ }, ['level1', 'level1.2']);
+ tree.addNewNode('level1.2.2', {
+ _type: 'database',
+ allowConn: false
+ }, ['level1', 'level1.2']);
+ tree.addNewNode('level1.2.3', {
+ _type: 'table'
+ }, ['level1', 'level1.2']);
+
+ tree.addNewNode('level2', {}, []);
+ tree.addNewNode('level2.1', null, ['level2']);
+ tree.addNewNode('level2.1.1', {}, ['level2', 'level2.1']);
+ });
+
+ context('When the current node is a root node', () => {
+ it('return false', () => {
+ expect(menuEnabled.apply(ourBrowser, [{}, [{id: 'level1'}]])).toBe(false);
+ });
+ });
+
+ context('when current node does not exist', () => {
+ it('return false', () => {
+ expect(menuEnabled.apply(ourBrowser, [{}, [{id: 'bamm'}]])).toBe(false);
+ });
+ });
+
+ context('When the current node is not a root node', () => {
+ context('parent data does not exist', () => {
+ it('returns false', () => {
+ expect(menuEnabled.apply(ourBrowser, [{}, [{id: 'level2.1.1'}]])).toBe(false);
+ });
+ });
+
+ context('parent as data', () => {
+ context('the current node type is in the supported node types', () => {
+ context('the parent is of the type catalog', () => {
+ it('returns false', () => {
+ expect(menuEnabled.apply(ourBrowser, [
+ {_type: 'schema'},
+ [{id: 'level1.1.1'}]
+ ])).toBe(false);
+ });
+ });
+ context('the parent is not of the type catalog', () => {
+ context('current node is of the type database', () => {
+ context('current node allows connection', () => {
+ it('returns true', () => {
+ expect(menuEnabled.apply(ourBrowser, [{
+ _type: 'database',
+ allowConn: true
+ }, [{id: 'level1.2.1'}]])).toBe(true);
+ });
+ });
+ context('current node do not allow connection', () => {
+ it('returns false', () => {
+ expect(menuEnabled.apply(ourBrowser, [{
+ _type: 'database',
+ allowConn: false
+ }, [{id: 'level1.2.2'}]])).toBe(false);
+ });
+ });
+ });
+ context('current node is not of the type database', () => {
+ it('returns true', () => {
+ expect(menuEnabled.apply(ourBrowser, [{
+ _type: 'schema'
+ }, [{id: 'level1.2.3'}]])).toBe(true);
+ });
+ });
+ });
+ });
+ context('the current node type is not in the supported node types', () => {
+ it('returns false', () => {
+ expect(menuEnabled.apply(ourBrowser, [{_type: 'catalog'}, [{id: 'level1.1'}]])).toBe(false);
+ });
+ });
+ });
+ });
+
+ context('provided node data is undefined', () => {
+ it('returns false', () => {
+ expect(menuEnabled.apply(ourBrowser, [undefined, [{id: 'level1'}]])).toBe(false);
+ });
+ });
+
+ context('provided node data is null', () => {
+ it('returns false', () => {
+ expect(menuEnabled.apply(ourBrowser, [null, [{id: 'level1'}]])).toBe(false);
+ });
+ });
+ });
+
+ describe('#menuEnabledServer', () => {
+ let ourBrowser;
+ beforeEach(() => {
+ const tree = new TreeFake();
+ ourBrowser = {
+ treeMenu: tree,
+ };
+ });
+
+ context('provided node data is undefined', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer(undefined)).toBe(false);
+ });
+ });
+
+ context('provided node data is null', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer(null)).toBe(false);
+ });
+ });
+
+ context('current node type is not of the type server', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer({_type: 'schema'})).toBe(false);
+ });
+ });
+
+ context('current node type is of the type server', () => {
+ context('is connected', () => {
+ it('returns true', () => {
+ expect(menuEnabledServer({_type: 'server', connected: true})).toBe(true);
+ });
+ });
+ context('is not connected', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer({_type: 'server', connected: false})).toBe(false);
+ });
+ });
+ });
+ });
+});
+
diff --git a/web/regression/javascript/datagrid/get_panel_title_spec.js b/web/regression/javascript/datagrid/get_panel_title_spec.js
new file mode 100644
index 00000000..3baedaa1
--- /dev/null
+++ b/web/regression/javascript/datagrid/get_panel_title_spec.js
@@ -0,0 +1,79 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {getPanelTitle} from "../../../pgadmin/static/js/datagrid/get_panel_title";
+import {TreeFake} from "../tree/tree_fake";
+
+const context = describe;
+const xcontext = xdescribe;
+
+describe('#getPanelTitle', () => {
+ let pgBrowser;
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ pgBrowser = {
+ treeMenu: tree,
+ Nodes: {
+ server: {
+ hasId: true,
+ _type: 'server',
+ },
+ database: {
+ hasId: true,
+ _type: 'database',
+ },
+ }
+ };
+ });
+
+ context('selected node does not belong to a server', () => {
+ it('returns undefined', () => {
+ tree.addNewNode('level1', {_type: 'server_groups'}, []);
+ tree.addNewNode('level1.1', {_type: 'other'}, ['level1']);
+ tree.setSelectedNode([{id: 'level1'}]);
+ expect(getPanelTitle(pgBrowser)).toBeUndefined();
+ });
+ });
+
+ context('selected node belong to a server', () => {
+ context('selected node does not belong to a database', () => {
+ it('returns the server label and the username', () => {
+ tree.addNewNode('level1', {
+ _type: 'server',
+ db: 'other db label',
+ user: {name: 'some user name'},
+ label: 'server label'
+ }, []);
+ tree.setSelectedNode([{id: 'level1'}]);
+ expect(getPanelTitle(pgBrowser))
+ .toBe('other db label on some user name@server label');
+ });
+ });
+
+ context('selected node belongs to a database', () => {
+ it('returns the database label and the username', () => {
+ tree.addNewNode('level1', {
+ _type: 'server',
+ db: 'other db label',
+ user: {name: 'some user name'},
+ label: 'server label'
+ }, []);
+ tree.addNewNode('level1.1', {
+ _type: 'database',
+ label: 'db label'
+ }, ['level1']);
+ tree.addNewNode('level1.1.1', {_type: 'table'}, ['level1', 'level1.1']);
+ tree.setSelectedNode([{id: 'level1.1.1'}]);
+ expect(getPanelTitle(pgBrowser))
+ .toBe('db label on some user name@server label');
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/datagrid/show_data_spec.js b/web/regression/javascript/datagrid/show_data_spec.js
new file mode 100644
index 00000000..e0ef8783
--- /dev/null
+++ b/web/regression/javascript/datagrid/show_data_spec.js
@@ -0,0 +1,159 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {showDataGrid} from "../../../pgadmin/static/js/datagrid/show_data";
+import {TreeFake} from "../tree/tree_fake";
+
+const context = describe;
+
+describe('#show_data', () => {
+ let datagrid;
+ let pgBrowser;
+ let alertify;
+ beforeEach(() => {
+ alertify = jasmine.createSpyObj('alertify', ['alert']);
+ datagrid = {
+ create_transaction: jasmine.createSpy('create_transaction'),
+ };
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server_group: {
+ _type: 'server_group',
+ hasId: true,
+ },
+ server: {
+ _type: 'server',
+ hasId: true,
+ },
+ database: {
+ _type: 'database',
+ hasId: true,
+ },
+ schema: {
+ _type: 'schema',
+ hasId: true,
+ },
+ view: {
+ _type: 'view',
+ hasId: true,
+ },
+ catalog: {
+ _type: 'catalog',
+ hasId: true,
+ },
+ }
+ };
+ pgBrowser.treeMenu.addNewNode('parent', {_type: 'parent'}, []);
+ pgBrowser.treeMenu.addNewNode('server_group1', {
+ _type: 'server_group',
+ _id: 1,
+ }, ['parent']);
+ pgBrowser.treeMenu.addNewNode('server1', {
+ _type: 'server',
+ label: 'server1',
+ server_type: 'pg',
+ _id: 2,
+ }, ['parent', 'server_group1']);
+ pgBrowser.treeMenu.addNewNode('database1', {
+ _type: 'database',
+ label: 'database1',
+ _id: 3,
+ }, ['parent', 'server_group1', 'server1']);
+ pgBrowser.treeMenu.addNewNode('schema1', {
+ _type: 'schema',
+ label: 'schema1',
+ _id: 4,
+ }, ['parent', 'server_group1', 'server1', 'database1']);
+ pgBrowser.treeMenu.addNewNode('view1', {
+ _type: 'view',
+ label: 'view1',
+ _id: 5,
+ }, ['parent', 'server_group1', 'server1', 'database1']);
+ pgBrowser.treeMenu.addNewNode('catalog1', {
+ _type: 'catalog',
+ label: 'catalog1',
+ _id: 6,
+ }, ['parent', 'server_group1', 'server1', 'database1']);
+ });
+
+ context('cannot find the tree node', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: '10'}]);
+ expect(datagrid.create_transaction).not.toHaveBeenCalled();
+ });
+
+ it('display alert', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: '10'}]);
+ expect(alertify.alert).toHaveBeenCalledWith(
+ 'Data Grid Error',
+ 'No object selected.'
+ );
+ });
+ });
+
+ context('current node is not underneath a server', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: 'parent'}]);
+ expect(datagrid.create_transaction).not.toHaveBeenCalled();
+ });
+ });
+
+ context('current node is not underneath a schema or view or catalog', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: 'database1'}]);
+ expect(datagrid.create_transaction).not.toHaveBeenCalled();
+ });
+ });
+
+ context('current node is underneath a schema', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {mnuid: 11}, [{id: 'schema1'}]);
+ expect(datagrid.create_transaction).toHaveBeenCalledWith(
+ '/initialize/datagrid/11/schema/1/2/3/4',
+ null,
+ 'false',
+ 'pg',
+ '',
+ 'server1 - database1 - schema1.schema1',
+ ''
+ );
+ });
+ });
+
+ context('current node is underneath a view', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {mnuid: 11}, [{id: 'view1'}]);
+ expect(datagrid.create_transaction).toHaveBeenCalledWith(
+ '/initialize/datagrid/11/view/1/2/3/5',
+ null,
+ 'false',
+ 'pg',
+ '',
+ 'server1 - database1 - view1.view1',
+ ''
+ );
+ });
+ });
+
+ context('current node is underneath a catalog', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {mnuid: 11}, [{id: 'catalog1'}]);
+ expect(datagrid.create_transaction).toHaveBeenCalledWith(
+ '/initialize/datagrid/11/catalog/1/2/3/6',
+ null,
+ 'false',
+ 'pg',
+ '',
+ 'server1 - database1 - catalog1.catalog1',
+ ''
+ );
+ });
+ });
+});
diff --git a/web/regression/javascript/datagrid/show_query_tool_spec.js b/web/regression/javascript/datagrid/show_query_tool_spec.js
new file mode 100644
index 00000000..b36dc4a7
--- /dev/null
+++ b/web/regression/javascript/datagrid/show_query_tool_spec.js
@@ -0,0 +1,119 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {TreeFake} from '../tree/tree_fake';
+import {showQueryTool} from '../../../pgadmin/static/js/datagrid/show_query_tool';
+
+const context = describe;
+
+describe('#showQueryTool', () => {
+ let queryTool;
+ let pgBrowser;
+ let alertify;
+ beforeEach(() => {
+ alertify = jasmine.createSpyObj('alertify', ['alert']);
+ queryTool = {
+ create_transaction: jasmine.createSpy('create_transaction'),
+ };
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server_group: {
+ _type: 'server_group',
+ hasId: true,
+ },
+ server: {
+ _type: 'server',
+ hasId: true,
+ },
+ database: {
+ _type: 'database',
+ hasId: true,
+ },
+ }
+ };
+ pgBrowser.treeMenu.addNewNode('parent', {_type: 'parent'}, []);
+ pgBrowser.treeMenu.addNewNode('server_group1', {
+ _type: 'server_group',
+ _id: 1,
+ }, ['parent']);
+ pgBrowser.treeMenu.addNewNode('server1', {
+ _type: 'server',
+ label: 'server1',
+ server_type: 'pg',
+ _id: 2,
+ }, ['parent', 'server_group1']);
+ pgBrowser.treeMenu.addNewNode('database1', {
+ _type: 'database',
+ label: 'database1',
+ _id: 3,
+ }, ['parent', 'server_group1', 'server1']);
+ });
+
+ context('cannot find the tree node', () => {
+ beforeEach(() => {
+ showQueryTool(queryTool, pgBrowser, alertify, '', [{id: '10'}], 'title');
+ });
+ it('does not create a transaction', () => {
+ expect(queryTool.create_transaction).not.toHaveBeenCalled();
+ });
+
+ it('display alert', () => {
+ expect(alertify.alert).toHaveBeenCalledWith(
+ 'Query Tool Error',
+ 'No object selected.'
+ );
+ });
+ });
+
+ context('current node is not underneath a server', () => {
+ it('does not create a transaction', () => {
+ showQueryTool(queryTool, pgBrowser, alertify, '', [{id: 'parent'}], 'title');
+ expect(queryTool.create_transaction).not.toHaveBeenCalled();
+ });
+
+ it('no alert is displayed', () => {
+ expect(alertify.alert).not.toHaveBeenCalled();
+ });
+ });
+
+ context('current node is underneath a server', () => {
+ context('current node is not underneath a database', () => {
+ it('creates a transaction', () => {
+ showQueryTool(queryTool, pgBrowser, alertify, 'http://someurl', [{id: 'server1'}], 'title');
+ expect(queryTool.create_transaction).toHaveBeenCalledWith(
+ '/initialize/query_tool/1/2',
+ null,
+ 'true',
+ 'pg',
+ 'http://someurl',
+ 'title',
+ '',
+ false
+ );
+ });
+ });
+
+ context('current node is underneath a database', () => {
+ it('creates a transaction', () => {
+ showQueryTool(queryTool, pgBrowser, alertify, 'http://someurl', [{id: 'database1'}], 'title');
+ expect(queryTool.create_transaction).toHaveBeenCalledWith(
+ '/initialize/query_tool/1/2/3',
+ null,
+ 'true',
+ 'pg',
+ 'http://someurl',
+ 'title',
+ '',
+ false
+ );
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/fake_endpoints.js b/web/regression/javascript/fake_endpoints.js
index 63ab05dc..e4ae7020 100644
--- a/web/regression/javascript/fake_endpoints.js
+++ b/web/regression/javascript/fake_endpoints.js
@@ -11,6 +11,12 @@ define(function () {
return {
'static': '/base/pgadmin/static/<path:filename>',
'sqleditor.poll': '/sqleditor/query_tool/poll/<path:trans_id>',
- 'sqleditor.query_tool_start': '/sqleditor/query_tool/start/<path:trans_id>'
+ 'sqleditor.query_tool_start': '/sqleditor/query_tool/start/<path:trans_id>',
+ 'backup.create_server_job': '/backup/job/<int:sid>',
+ 'backup.create_object_job': '/backup/job/<int:sid>/object',
+ 'datagrid.initialize_datagrid': '/initialize/datagrid/<int:cmd_type>/<obj_type>/<int:sgid>/<int:sid>/<int:did>/<int:obj_id>',
+ 'datagrid.initialize_query_tool': '/initialize/query_tool/<int:sgid>/<int:sid>',
+ 'datagrid.initialize_query_tool_with_did': '/initialize/query_tool/<int:sgid>/<int:sid>/<int:did>',
+
};
});
diff --git a/web/regression/javascript/fake_model.js b/web/regression/javascript/fake_model.js
new file mode 100644
index 00000000..acfaa532
--- /dev/null
+++ b/web/regression/javascript/fake_model.js
@@ -0,0 +1,21 @@
+export class FakeModel {
+ constructor() {
+ this.values = {};
+ }
+
+ set(key, value) {
+ this.values[key] = value;
+ }
+
+ get(key) {
+ return this.values[key];
+ }
+
+ unset(key) {
+ delete this.values[key];
+ }
+
+ toJSON() {
+ return Object.assign({}, this.values);
+ }
+}
diff --git a/web/regression/javascript/tree/pgadmin_tree_node_spec.js b/web/regression/javascript/tree/pgadmin_tree_node_spec.js
new file mode 100644
index 00000000..1cab65f0
--- /dev/null
+++ b/web/regression/javascript/tree/pgadmin_tree_node_spec.js
@@ -0,0 +1,353 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+import {
+ getTreeNodeHierarchyFromElement,
+ getTreeNodeHierarchyFromIdentifier
+} from "../../../pgadmin/static/js/tree/pgadmin_tree_node";
+import {Tree} from "../../../pgadmin/static/js/tree/tree";
+
+const context = describe;
+
+describe('tree#node#getTreeNodeHierarchy', () => {
+ let browser;
+ let newTree;
+ let translateTreeNodeIdFromACITreeSpy;
+ beforeEach(() => {
+ newTree = new Tree();
+ browser = {
+ Nodes: {
+ 'special one': {hasId: true},
+ 'child special': {hasId: true},
+ 'other type': {hasId: true},
+ 'table': {hasId: true},
+ 'partition': {hasId: true},
+ 'no id': {hasId: false},
+ }
+ };
+ browser.treeMenu = newTree;
+ translateTreeNodeIdFromACITreeSpy = spyOn(newTree, 'translateTreeNodeIdFromACITree');
+ });
+
+ context('getTreeNodeHierarchy is called with aciTreeNode object', () => {
+ describe('When the current node is root element', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one'
+ }, []);
+ translateTreeNodeIdFromACITreeSpy.and.returnValue(['root']);
+ });
+
+ it('returns a object with the element type passed data and priority == 0', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)('root');
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': 0,
+ }
+ });
+ });
+ });
+
+ describe('When the current node is not of a known type', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'do not exist'
+ }, []);
+ translateTreeNodeIdFromACITreeSpy.and.returnValue(['root']);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)('root');
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node type has no id', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'no id'
+ }, []);
+ translateTreeNodeIdFromACITreeSpy.and.returnValue(['root']);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)('root');
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node is at the second level', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one'
+ }, []);
+ newTree.addNewNode('first child', {
+ 'some key': 'some other value',
+ '_type': 'child special'
+ }, ['root']);
+ translateTreeNodeIdFromACITreeSpy.and.returnValue(['root', 'first' +
+ ' child']);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)('first child');
+ expect(result).toEqual({
+ 'child special': {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ 'priority': 0,
+ },
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+
+ context('When tree as "special type"', () => {
+ context('When "special type" have "other type"', () => {
+ context('When "other type" have "special type"', () => {
+ describe('When retrieving lead node', () => {
+ it('does not override previous node type data', () => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one'
+ }, []);
+ newTree.addNewNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'other type'
+ }, ['root']);
+ newTree.addNewNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ }, ['root', 'level 1']);
+
+ translateTreeNodeIdFromACITreeSpy.and.returnValue(['root', 'level 1', 'level 2']);
+
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)('level 2');
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ 'priority': 0,
+ },
+ 'other type': {
+ 'some key': 'some value',
+ '_type': 'other type',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+ });
+ });
+ });
+
+ context('When tree has table', () => {
+ context('when table has partition', () => {
+ it('returns table with partition parameters', () => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one'
+ }, []);
+ newTree.addNewNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'table'
+ }, ['root']);
+ newTree.addNewNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'partition',
+ 'some other key': 'some other value',
+ }, ['root', 'level 1']);
+
+ translateTreeNodeIdFromACITreeSpy.and.returnValue(['root', 'level 1', 'level 2']);
+
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)('level 2');
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ 'table': {
+ 'some key': 'expected value',
+ 'some other key': 'some other value',
+ '_type': 'partition',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+ });
+ });
+
+ context('getTreeNodeHierarchy is called with TreeNode object', () => {
+ let treeNode;
+ describe('When the current node is root element', () => {
+ beforeEach(() => {
+ treeNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one'
+ }, []);
+ });
+
+ it('returns a object with the element type passed data and priority == 0', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': 0,
+ }
+ });
+ });
+ });
+
+ describe('When the current node is not of a known type', () => {
+ beforeEach(() => {
+ treeNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'do not exist'
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node type has no id', () => {
+ beforeEach(() => {
+ treeNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'no id'
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node is at the second level', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one'
+ }, []);
+ treeNode = newTree.addNewNode('first child', {
+ 'some key': 'some other value',
+ '_type': 'child special'
+ }, ['root']);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'child special': {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ 'priority': 0,
+ },
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+
+ context('When tree as "special type"', () => {
+ context('When "special type" have "other type"', () => {
+ context('When "other type" have "special type"', () => {
+ describe('When retrieving lead node', () => {
+ it('does not override previous node type data', () => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one'
+ }, []);
+ newTree.addNewNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'other type'
+ }, ['root']);
+ treeNode = newTree.addNewNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ }, ['root', 'level 1']);
+
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ 'priority': 0,
+ },
+ 'other type': {
+ 'some key': 'some value',
+ '_type': 'other type',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+ });
+ });
+ });
+
+ context('When tree has table', () => {
+ context('when table has partition', () => {
+ it('returns table with partition parameters', () => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one'
+ }, []);
+ newTree.addNewNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'table'
+ }, ['root']);
+ treeNode = newTree.addNewNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'partition',
+ 'some other key': 'some other value',
+ }, ['root', 'level 1']);
+
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ 'table': {
+ 'some key': 'expected value',
+ 'some other key': 'some other value',
+ '_type': 'partition',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/yarn.lock b/web/yarn.lock
index 88edeb1f..da5e5f64 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -7422,7 +7422,7 @@ [email protected]:
version "1.1.0"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.0.tgz#cffcaf702daf65ea39bb4e0fa2b299cec1a1be46"
-sprintf-js@^1.0.3:
+sprintf-js@^1.0.3, sprintf-js@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.1.tgz#36be78320afe5801f6cea3ee78b6e5aab940ea0c"
--
2.15.1
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-04 16:17 Dave Page <[email protected]>
parent: Joao De Almeida Pereira <[email protected]>
1 sibling, 1 reply; 69+ messages in thread
From: Dave Page @ 2018-04-04 16:17 UTC (permalink / raw)
To: Joao De Almeida Pereira <[email protected]>; +Cc: pgadmin-hackers; Khushboo Vashi <[email protected]>; Murtuza Zabuawala <[email protected]>
Khushboo, Murtuza,
Can you spend some time reviewing this please? I've started playing with it
as well - the first thing that's irking me somewhat is the lack of
comments. Descriptive function names are all well and good, but sometimes a
little more is needed, especially for less experienced developers or
newcomers to the application!
On Mon, Apr 2, 2018 at 7:45 PM, Joao De Almeida Pereira <
[email protected]> wrote:
> Hi Hackers,
>
> Attached you can find the patch that will start to decouple pgAdmin from
> ACITree library.
> This patch is intended to be merged after 3.0, because we do not want to
> cause any entropy or delay the release, but we want to start the discussion
> and show some code.
>
> This job that we started is a massive tech debt chore that will take some
> time to finalize and we would love the help of the community to do it.
>
> *Summary of the patch:*
> 0001 patch:
> - Creates a new tree that will allow us to create a separation between
> the application and ACI Tree
> - Creates a Fake Tree (Test double, for reference on the available test
> doubles: https://martinfowler.com/bliki/TestDouble.html) that can be used
> to inplace to replace the ACITree and also encapsulate the new tree
> behavior on our tests
> - Adds tests for all the tree functionalities
>
> 0002 patch:
> - Extracts, refactors, adds tests and remove dependency from ACI Tree on:
> - getTreeNodeHierarchy
> - on backup.js: menu_enabled, menu_enabled_server,
> start_backup_global_server, backup_objects
> - on datagrid.js: show_data_grid, get_panel_title, show_query_tool
> - Start using sprintf-js as Underscore.String is deprecating sprintf
> function
>
> This patch represents only 10 calls to ACITree.itemData out of 176 that
> are spread around our code
>
>
> *In Depth look on the process behind the patch:*
>
> We started writing this patch with the idea that we need to decouple
> pgAdmin4 from ACITree, because ACITree is no longer supported, the
> documentation is non existent and ACITree is no longer being actively
> developed.
>
> Our process:
> 1. We "randomly" selected a function that is part of the ACITree. From
> this point we decided to replace that function with our own version. The
> function that we choose was "itemData".
> The function gives us all the "data" that a specific node of the tree find.
> Given in order to replace the tree we would need to have a function that
> would give us the same information. We had 2 options:
> a) Create a tree with a function called itemData
> Pros:
> - At first view this was the simpler solution
> - Would keep the status quo
> Cons:
> - Not a OOP approach
> - Not very flexible
> b) Create a tree that would return a node given an ID and then the node
> would be responsible for giving it's data.
> Pros:
> - OOP Approach
> - More flexible and we do not need to bring the tree around, just a node
> Cons:
> - Break the current status quo
>
> Given these 2 options we decided to go for a more OOP approach creating a
> Tree and a TreeNode classes, that in the future will be renamed to
> ACITreeWrapper and TreeNode.
>
> 2. After we decided on the starting point we searched for occurrences of
> the function "itemData" and we found out that there were 303 occurrences of
> "itemData" in the code and roughly 176 calls to the function itself (some
> of the hits were variable names).
>
> 3. We selected the first file on the search and found the function that
> was responsible for calling the itemData function.
>
> 4. Extracted the function to a separate file
>
> 5. Wrap this function with tests
>
> 6. Refactor the function to ES6, give more declarative names to variables
> and break the functions into smaller chunks
>
> 7. When all the tests were passing we replaced ACITree with our Tree
>
> 8. We ensured that all tests were passing
>
> 9. Remove function from the original file and use the new function
>
> 10. Ensure everything still works
>
> 11. Find the next function and execute from step 4 until all the functions
> are replaced, refactored and tested.
>
> As you can see by the process this is a pretty huge undertake, because of
> the number of calls to the function. This is just the first step on the
> direction of completely isolating the ACITree so that we can solve the
> problem with a large number of elements on the tree.
>
> *What is on our radar that we need to address:*
> - Finish the complete decoupling of the ACITree
> - Performance of the current tree implementation
> - Tweak the naming of the Tree class to explicitly tell us this is to use
> only with ACITree.
>
>
> Thanks
> Joao
>
>
--
Dave Page
Blog: http://pgsnake.blogspot.com
Twitter: @pgsnake
EnterpriseDB UK: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-05 03:35 Khushboo Vashi <[email protected]>
parent: Dave Page <[email protected]>
0 siblings, 0 replies; 69+ messages in thread
From: Khushboo Vashi @ 2018-04-05 03:35 UTC (permalink / raw)
To: Dave Page <[email protected]>; +Cc: Joao De Almeida Pereira <[email protected]>; pgadmin-hackers; Murtuza Zabuawala <[email protected]>
On Wed, Apr 4, 2018 at 9:47 PM, Dave Page <[email protected]> wrote:
> Khushboo, Murtuza,
>
> Can you spend some time reviewing this please? I've started playing with
> it as well - the first thing that's irking me somewhat is the lack of
> comments. Descriptive function names are all well and good, but sometimes a
> little more is needed, especially for less experienced developers or
> newcomers to the application!
>
> Sure, already in our list.
> On Mon, Apr 2, 2018 at 7:45 PM, Joao De Almeida Pereira <
> [email protected]> wrote:
>
>> Hi Hackers,
>>
>> Attached you can find the patch that will start to decouple pgAdmin from
>> ACITree library.
>> This patch is intended to be merged after 3.0, because we do not want to
>> cause any entropy or delay the release, but we want to start the discussion
>> and show some code.
>>
>> This job that we started is a massive tech debt chore that will take some
>> time to finalize and we would love the help of the community to do it.
>>
>> *Summary of the patch:*
>> 0001 patch:
>> - Creates a new tree that will allow us to create a separation between
>> the application and ACI Tree
>> - Creates a Fake Tree (Test double, for reference on the available test
>> doubles: https://martinfowler.com/bliki/TestDouble.html) that can be
>> used to inplace to replace the ACITree and also encapsulate the new tree
>> behavior on our tests
>> - Adds tests for all the tree functionalities
>>
>> 0002 patch:
>> - Extracts, refactors, adds tests and remove dependency from ACI Tree on:
>> - getTreeNodeHierarchy
>> - on backup.js: menu_enabled, menu_enabled_server,
>> start_backup_global_server, backup_objects
>> - on datagrid.js: show_data_grid, get_panel_title, show_query_tool
>> - Start using sprintf-js as Underscore.String is deprecating sprintf
>> function
>>
>> This patch represents only 10 calls to ACITree.itemData out of 176 that
>> are spread around our code
>>
>>
>> *In Depth look on the process behind the patch:*
>>
>> We started writing this patch with the idea that we need to decouple
>> pgAdmin4 from ACITree, because ACITree is no longer supported, the
>> documentation is non existent and ACITree is no longer being actively
>> developed.
>>
>> Our process:
>> 1. We "randomly" selected a function that is part of the ACITree. From
>> this point we decided to replace that function with our own version. The
>> function that we choose was "itemData".
>> The function gives us all the "data" that a specific node of the tree
>> find.
>> Given in order to replace the tree we would need to have a function that
>> would give us the same information. We had 2 options:
>> a) Create a tree with a function called itemData
>> Pros:
>> - At first view this was the simpler solution
>> - Would keep the status quo
>> Cons:
>> - Not a OOP approach
>> - Not very flexible
>> b) Create a tree that would return a node given an ID and then the node
>> would be responsible for giving it's data.
>> Pros:
>> - OOP Approach
>> - More flexible and we do not need to bring the tree around, just a node
>> Cons:
>> - Break the current status quo
>>
>> Given these 2 options we decided to go for a more OOP approach creating a
>> Tree and a TreeNode classes, that in the future will be renamed to
>> ACITreeWrapper and TreeNode.
>>
>> 2. After we decided on the starting point we searched for occurrences of
>> the function "itemData" and we found out that there were 303 occurrences of
>> "itemData" in the code and roughly 176 calls to the function itself (some
>> of the hits were variable names).
>>
>> 3. We selected the first file on the search and found the function that
>> was responsible for calling the itemData function.
>>
>> 4. Extracted the function to a separate file
>>
>> 5. Wrap this function with tests
>>
>> 6. Refactor the function to ES6, give more declarative names to variables
>> and break the functions into smaller chunks
>>
>> 7. When all the tests were passing we replaced ACITree with our Tree
>>
>> 8. We ensured that all tests were passing
>>
>> 9. Remove function from the original file and use the new function
>>
>> 10. Ensure everything still works
>>
>> 11. Find the next function and execute from step 4 until all the
>> functions are replaced, refactored and tested.
>>
>> As you can see by the process this is a pretty huge undertake, because of
>> the number of calls to the function. This is just the first step on the
>> direction of completely isolating the ACITree so that we can solve the
>> problem with a large number of elements on the tree.
>>
>> *What is on our radar that we need to address:*
>> - Finish the complete decoupling of the ACITree
>> - Performance of the current tree implementation
>> - Tweak the naming of the Tree class to explicitly tell us this is to
>> use only with ACITree.
>>
>>
>> Thanks
>> Joao
>>
>>
>
>
> --
> Dave Page
> Blog: http://pgsnake.blogspot.com
> Twitter: @pgsnake
>
> EnterpriseDB UK: http://www.enterprisedb.com
> The Enterprise PostgreSQL Company
>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-05 10:31 Khushboo Vashi <[email protected]>
parent: Joao De Almeida Pereira <[email protected]>
1 sibling, 1 reply; 69+ messages in thread
From: Khushboo Vashi @ 2018-04-05 10:31 UTC (permalink / raw)
To: Joao De Almeida Pereira <[email protected]>; +Cc: pgadmin-hackers
Hi Joao,
Can you please rebase the second patch?
Thanks,
Khushboo
On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida Pereira <
[email protected]> wrote:
> Hi Hackers,
>
> Attached you can find the patch that will start to decouple pgAdmin from
> ACITree library.
> This patch is intended to be merged after 3.0, because we do not want to
> cause any entropy or delay the release, but we want to start the discussion
> and show some code.
>
> This job that we started is a massive tech debt chore that will take some
> time to finalize and we would love the help of the community to do it.
>
> *Summary of the patch:*
> 0001 patch:
> - Creates a new tree that will allow us to create a separation between
> the application and ACI Tree
> - Creates a Fake Tree (Test double, for reference on the available test
> doubles: https://martinfowler.com/bliki/TestDouble.html) that can be used
> to inplace to replace the ACITree and also encapsulate the new tree
> behavior on our tests
> - Adds tests for all the tree functionalities
>
> 0002 patch:
> - Extracts, refactors, adds tests and remove dependency from ACI Tree on:
> - getTreeNodeHierarchy
> - on backup.js: menu_enabled, menu_enabled_server,
> start_backup_global_server, backup_objects
> - on datagrid.js: show_data_grid, get_panel_title, show_query_tool
> - Start using sprintf-js as Underscore.String is deprecating sprintf
> function
>
> This patch represents only 10 calls to ACITree.itemData out of 176 that
> are spread around our code
>
>
> *In Depth look on the process behind the patch:*
>
> We started writing this patch with the idea that we need to decouple
> pgAdmin4 from ACITree, because ACITree is no longer supported, the
> documentation is non existent and ACITree is no longer being actively
> developed.
>
> Our process:
> 1. We "randomly" selected a function that is part of the ACITree. From
> this point we decided to replace that function with our own version. The
> function that we choose was "itemData".
> The function gives us all the "data" that a specific node of the tree find.
> Given in order to replace the tree we would need to have a function that
> would give us the same information. We had 2 options:
> a) Create a tree with a function called itemData
> Pros:
> - At first view this was the simpler solution
> - Would keep the status quo
> Cons:
> - Not a OOP approach
> - Not very flexible
> b) Create a tree that would return a node given an ID and then the node
> would be responsible for giving it's data.
> Pros:
> - OOP Approach
> - More flexible and we do not need to bring the tree around, just a node
> Cons:
> - Break the current status quo
>
> Given these 2 options we decided to go for a more OOP approach creating a
> Tree and a TreeNode classes, that in the future will be renamed to
> ACITreeWrapper and TreeNode.
>
> 2. After we decided on the starting point we searched for occurrences of
> the function "itemData" and we found out that there were 303 occurrences of
> "itemData" in the code and roughly 176 calls to the function itself (some
> of the hits were variable names).
>
> 3. We selected the first file on the search and found the function that
> was responsible for calling the itemData function.
>
> 4. Extracted the function to a separate file
>
> 5. Wrap this function with tests
>
> 6. Refactor the function to ES6, give more declarative names to variables
> and break the functions into smaller chunks
>
> 7. When all the tests were passing we replaced ACITree with our Tree
>
> 8. We ensured that all tests were passing
>
> 9. Remove function from the original file and use the new function
>
> 10. Ensure everything still works
>
> 11. Find the next function and execute from step 4 until all the functions
> are replaced, refactored and tested.
>
> As you can see by the process this is a pretty huge undertake, because of
> the number of calls to the function. This is just the first step on the
> direction of completely isolating the ACITree so that we can solve the
> problem with a large number of elements on the tree.
>
> *What is on our radar that we need to address:*
> - Finish the complete decoupling of the ACITree
> - Performance of the current tree implementation
> - Tweak the naming of the Tree class to explicitly tell us this is to use
> only with ACITree.
>
>
> Thanks
> Joao
>
>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-05 13:38 Joao De Almeida Pereira <[email protected]>
parent: Khushboo Vashi <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Joao De Almeida Pereira @ 2018-04-05 13:38 UTC (permalink / raw)
To: Khushboo Vashi <[email protected]>; +Cc: pgadmin-hackers
Hi Khushboo,
Attached you can find both patches rebased
Thanks
On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
[email protected]> wrote:
> Hi Joao,
>
> Can you please rebase the second patch?
>
> Thanks,
> Khushboo
>
>
> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida Pereira <
> [email protected]> wrote:
>
>> Hi Hackers,
>>
>> Attached you can find the patch that will start to decouple pgAdmin from
>> ACITree library.
>> This patch is intended to be merged after 3.0, because we do not want to
>> cause any entropy or delay the release, but we want to start the discussion
>> and show some code.
>>
>> This job that we started is a massive tech debt chore that will take some
>> time to finalize and we would love the help of the community to do it.
>>
>> *Summary of the patch:*
>> 0001 patch:
>> - Creates a new tree that will allow us to create a separation between
>> the application and ACI Tree
>> - Creates a Fake Tree (Test double, for reference on the available test
>> doubles: https://martinfowler.com/bliki/TestDouble.html) that can be
>> used to inplace to replace the ACITree and also encapsulate the new tree
>> behavior on our tests
>> - Adds tests for all the tree functionalities
>>
>> 0002 patch:
>> - Extracts, refactors, adds tests and remove dependency from ACI Tree on:
>> - getTreeNodeHierarchy
>> - on backup.js: menu_enabled, menu_enabled_server,
>> start_backup_global_server, backup_objects
>> - on datagrid.js: show_data_grid, get_panel_title, show_query_tool
>> - Start using sprintf-js as Underscore.String is deprecating sprintf
>> function
>>
>> This patch represents only 10 calls to ACITree.itemData out of 176 that
>> are spread around our code
>>
>>
>> *In Depth look on the process behind the patch:*
>>
>> We started writing this patch with the idea that we need to decouple
>> pgAdmin4 from ACITree, because ACITree is no longer supported, the
>> documentation is non existent and ACITree is no longer being actively
>> developed.
>>
>> Our process:
>> 1. We "randomly" selected a function that is part of the ACITree. From
>> this point we decided to replace that function with our own version. The
>> function that we choose was "itemData".
>> The function gives us all the "data" that a specific node of the tree
>> find.
>> Given in order to replace the tree we would need to have a function that
>> would give us the same information. We had 2 options:
>> a) Create a tree with a function called itemData
>> Pros:
>> - At first view this was the simpler solution
>> - Would keep the status quo
>> Cons:
>> - Not a OOP approach
>> - Not very flexible
>> b) Create a tree that would return a node given an ID and then the node
>> would be responsible for giving it's data.
>> Pros:
>> - OOP Approach
>> - More flexible and we do not need to bring the tree around, just a node
>> Cons:
>> - Break the current status quo
>>
>> Given these 2 options we decided to go for a more OOP approach creating a
>> Tree and a TreeNode classes, that in the future will be renamed to
>> ACITreeWrapper and TreeNode.
>>
>> 2. After we decided on the starting point we searched for occurrences of
>> the function "itemData" and we found out that there were 303 occurrences of
>> "itemData" in the code and roughly 176 calls to the function itself (some
>> of the hits were variable names).
>>
>> 3. We selected the first file on the search and found the function that
>> was responsible for calling the itemData function.
>>
>> 4. Extracted the function to a separate file
>>
>> 5. Wrap this function with tests
>>
>> 6. Refactor the function to ES6, give more declarative names to variables
>> and break the functions into smaller chunks
>>
>> 7. When all the tests were passing we replaced ACITree with our Tree
>>
>> 8. We ensured that all tests were passing
>>
>> 9. Remove function from the original file and use the new function
>>
>> 10. Ensure everything still works
>>
>> 11. Find the next function and execute from step 4 until all the
>> functions are replaced, refactored and tested.
>>
>> As you can see by the process this is a pretty huge undertake, because of
>> the number of calls to the function. This is just the first step on the
>> direction of completely isolating the ACITree so that we can solve the
>> problem with a large number of elements on the tree.
>>
>> *What is on our radar that we need to address:*
>> - Finish the complete decoupling of the ACITree
>> - Performance of the current tree implementation
>> - Tweak the naming of the Tree class to explicitly tell us this is to
>> use only with ACITree.
>>
>>
>> Thanks
>> Joao
>>
>>
>
Attachments:
[text/x-patch] 0001-Tree-of-information-on-the-current-status-of-the-app_v1.patch (14.1K, 3-0001-Tree-of-information-on-the-current-status-of-the-app_v1.patch)
download | inline diff:
diff --git a/web/pgadmin/static/js/tree/tree.js b/web/pgadmin/static/js/tree/tree.js
new file mode 100644
index 00000000..ed67aa85
--- /dev/null
+++ b/web/pgadmin/static/js/tree/tree.js
@@ -0,0 +1,134 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+export class TreeNode {
+ constructor(id, data, parent) {
+ this.id = id;
+ this.data = data;
+ this.parentNode = parent;
+ this.path = this.id;
+ if (parent !== null && parent !== undefined && parent.path !== undefined) {
+ this.path = parent.path + '.' + id;
+ }
+ this.children = [];
+ }
+
+ hasParent() {
+ return this.parentNode !== null && this.parentNode !== undefined;
+ }
+
+ parent() {
+ return this.parentNode;
+ }
+
+ getData() {
+ if (this.data === undefined) {
+ return undefined;
+ } else if (this.data === null) {
+ return null;
+ }
+ return Object.assign({}, this.data);
+ }
+}
+
+export class Tree {
+ constructor() {
+ this.rootNode = new TreeNode(undefined, {});
+ this.aciTreeApi = undefined;
+ }
+
+ addNewNode(id, data, path) {
+ const parent = this.findNode(path);
+ return this.createOrUpdateNode(id, data, parent);
+ }
+
+ findNode(path) {
+ if (path.length === 0) {
+ return this.rootNode;
+ }
+ return findInTree(this.rootNode, path.join('.'));
+ }
+
+ findNodeByDomElement(domElement) {
+ const path = this.translateTreeNodeIdFromACITree(domElement);
+ if(!path || !path[0]) {
+ return undefined;
+ }
+
+ return this.findNode(path);
+ }
+
+ selected() {
+ return this.aciTreeApi.selected();
+ }
+
+ register($treeJQuery) {
+ $treeJQuery.on('acitree', function (event, api, item, eventName) {
+ if (api.isItem(item)) {
+ if (eventName === 'added') {
+ const id = api.getId(item);
+ const data = api.itemData(item);
+ const parentId = this.translateTreeNodeIdFromACITree(api.parent(item));
+ this.addNewNode(id, data, parentId);
+ }
+ }
+ }.bind(this));
+ this.aciTreeApi = $treeJQuery.aciTree('api');
+ }
+
+ createOrUpdateNode(id, data, parent) {
+ const oldNode = this.findNode([parent.path, id]);
+ if (oldNode !== null) {
+ oldNode.data = Object.assign({}, data);
+ return oldNode;
+ }
+
+ const node = new TreeNode(id, data, parent);
+ if (parent === this.rootNode) {
+ node.parentNode = null;
+ }
+ parent.children.push(node);
+ return node;
+ }
+
+ translateTreeNodeIdFromACITree(aciTreeNode) {
+ let currentTreeNode = aciTreeNode;
+ let path = [];
+ while (currentTreeNode !== null && currentTreeNode !== undefined && currentTreeNode.length > 0) {
+ path.unshift(this.aciTreeApi.getId(currentTreeNode));
+ if (this.aciTreeApi.hasParent(currentTreeNode)) {
+ currentTreeNode = this.aciTreeApi.parent(currentTreeNode);
+ } else {
+ break;
+ }
+ }
+ return path;
+ }
+}
+
+export let tree = new Tree();
+
+function findInTree(rootNode, path) {
+ if (path === null) {
+ return rootNode;
+ }
+ return (function findInNode(currentNode) {
+ for (let i = 0, length = currentNode.children.length; i < length; i++) {
+ const calculatedNode = findInNode(currentNode.children[i]);
+ if (calculatedNode !== null) {
+ return calculatedNode;
+ }
+ }
+
+ if (currentNode.path === path) {
+ return currentNode;
+ } else {
+ return null;
+ }
+ })(rootNode);
+}
diff --git a/web/regression/javascript/tree/tree_fake.js b/web/regression/javascript/tree/tree_fake.js
new file mode 100644
index 00000000..a0983c67
--- /dev/null
+++ b/web/regression/javascript/tree/tree_fake.js
@@ -0,0 +1,55 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {Tree} from "../../../pgadmin/static/js/tree/tree";
+
+export class TreeFake extends Tree {
+ constructor() {
+ super();
+ this.aciTreeToOurTreeTranslator = {};
+ }
+
+ addNewNode(id, data, path) {
+ this.aciTreeToOurTreeTranslator[id] = path.concat(id);
+ return super.addNewNode(id, data, path);
+ }
+
+ hasParent(aciTreeNode) {
+ return this.translateTreeNodeIdFromACITree(aciTreeNode).length > 1;
+ }
+
+ parent(aciTreeNode) {
+ if (this.hasParent(aciTreeNode)) {
+ let path = this.translateTreeNodeIdFromACITree(aciTreeNode);
+ return [{id: this.findNode(path).parent().id}];
+ }
+
+ return null;
+ }
+
+ translateTreeNodeIdFromACITree(aciTreeNode) {
+ return this.aciTreeToOurTreeTranslator[aciTreeNode[0].id];
+ }
+
+ itemData(aciTreeNode) {
+ let path = this.translateTreeNodeIdFromACITree(aciTreeNode);
+ if(path === undefined || path === null) {
+ return undefined;
+ }
+ return this.findNode(path).getData();
+ }
+
+ selected() {
+ return this.selectedNode;
+ }
+
+ setSelectedNode(selectedNode) {
+ this.selectedNode = selectedNode;
+ }
+}
diff --git a/web/regression/javascript/tree/tree_spec.js b/web/regression/javascript/tree/tree_spec.js
new file mode 100644
index 00000000..e8930d84
--- /dev/null
+++ b/web/regression/javascript/tree/tree_spec.js
@@ -0,0 +1,284 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+import {Tree, TreeNode} from "../../../pgadmin/static/js/tree/tree";
+import {TreeFake} from "./tree_fake";
+
+const context = describe;
+
+const treeTests = (treeClass, setDefaultCallBack) => {
+ let tree;
+ beforeEach(() => {
+ tree = new treeClass();
+ });
+
+ describe('#addNewNode', () => {
+ describe('when add a new root element', () => {
+ beforeEach(() => {
+ tree.addNewNode('some new node', {data: 'interesting'}, []);
+ });
+
+ it('can be retrieved', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.data).toEqual({data: 'interesting'});
+ });
+
+ it('return false for #hasParent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.hasParent()).toBe(false);
+ });
+
+ it('return null for #parent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.parent()).toBeNull();
+ });
+ });
+
+ describe('when add a new element as a child', () => {
+ let parentNode;
+ beforeEach(() => {
+ parentNode = tree.addNewNode('parent node', {data: 'parent data'}, []);
+ tree.addNewNode('some new node', {data: 'interesting'}, ['parent' +
+ ' node']);
+ });
+
+ it('can be retrieved', () => {
+ const node = tree.findNode(['parent node', 'some new node']);
+ expect(node.data).toEqual({data: 'interesting'});
+ });
+
+ it('return true for #hasParent()', () => {
+ const node = tree.findNode(['parent node', 'some new node']);
+ expect(node.hasParent()).toBe(true);
+ });
+
+ it('return "parent node" object for #parent()', () => {
+ const node = tree.findNode(['parent node', 'some new node']);
+ expect(node.parent()).toEqual(parentNode);
+ });
+ });
+
+ describe('when add an element that already exists under a parent', () => {
+ let parentNode;
+ beforeEach(() => {
+ parentNode = tree.addNewNode('parent node', {data: 'parent data'}, []);
+ tree.addNewNode('some new node', {data: 'interesting'}, ['parent' +
+ ' node']);
+ });
+
+ it('does not add a new child', () => {
+ tree.addNewNode('some new node', {data: 'interesting 1'}, ['parent' +
+ ' node']);
+ const parentNode = tree.findNode(['parent node']);
+ expect(parentNode.children.length).toBe(1);
+ });
+
+ it('updates the existing node data', () => {
+ tree.addNewNode('some new node', {data: 'interesting 1'}, ['parent' +
+ ' node']);
+ const node = tree.findNode(['parent node', 'some new node']);
+ expect(node.data).toEqual({data: 'interesting 1'});
+ });
+ });
+ });
+
+ describe('#translateTreeNodeIdFromACITree', () => {
+ let aciTreeApi;
+ beforeEach(() => {
+ aciTreeApi = jasmine.createSpyObj('ACITreeApi', [
+ 'hasParent',
+ 'parent',
+ 'getId',
+ ]);
+
+ aciTreeApi.getId.and.callFake((node) => {
+ return node[0].id;
+ });
+ tree.aciTreeApi = aciTreeApi;
+ });
+
+ describe('When tree as a single level', () => {
+ beforeEach(() => {
+ aciTreeApi.hasParent.and.returnValue(false);
+ });
+
+ it('returns an array with the ID of the first level', () => {
+ let node = [{
+ id: 'some id',
+ }];
+ tree.addNewNode('some id', {}, []);
+
+ expect(tree.translateTreeNodeIdFromACITree(node)).toEqual(['some id']);
+ });
+ });
+
+ describe('When tree as a 2 levels', () => {
+ describe('When we try to retrieve the node in the second level', () => {
+ it('returns an array with the ID of the first level and second level', () => {
+ aciTreeApi.hasParent.and.returnValues(true, false);
+ aciTreeApi.parent.and.returnValue([{
+ id: 'parent id'
+ }]);
+ let node = [{
+ id: 'some id',
+ }];
+
+ tree.addNewNode('parent id', {}, []);
+ tree.addNewNode('some id', {}, ['parent id']);
+
+ expect(tree.translateTreeNodeIdFromACITree(node))
+ .toEqual(['parent id', 'some id']);
+ });
+ });
+ });
+ });
+
+ describe('#selected', () => {
+ context('a node in the tree is selected', () => {
+ it('returns that node object', () => {
+ let selectedNode = new TreeNode('bamm', {}, []);
+ setDefaultCallBack(tree, selectedNode)
+ expect(tree.selected()).toEqual(selectedNode);
+ });
+ });
+ });
+
+ describe('#findNodeByTreeElement', () => {
+ context('retrieve data from node not found', () => {
+ it('return undefined', () => {
+ let aciTreeApi = jasmine.createSpyObj('ACITreeApi', [
+ 'hasParent',
+ 'parent',
+ 'getId',
+ ]);
+
+ aciTreeApi.getId.and.callFake((node) => {
+ return node[0].id;
+ });
+ tree.aciTreeApi = aciTreeApi;
+ expect(tree.findNodeByDomElement(['<li>something</li>'])).toBeUndefined();
+ });
+ });
+ });
+};
+
+describe('tree tests', () => {
+ describe('TreeNode', () => {
+ describe('#hasParent', () => {
+ context('parent is null', () => {
+ it('returns false', () => {
+ let treeNode = new TreeNode('123', {}, null);
+ expect(treeNode.hasParent()).toBe(false);
+ });
+ });
+ context('parent is undefined', () => {
+ it('returns false', () => {
+ let treeNode = new TreeNode('123', {}, undefined);
+ expect(treeNode.hasParent()).toBe(false);
+ });
+ });
+ context('parent exists', () => {
+ it('returns true', () => {
+ let parentNode = new TreeNode('456', {}, undefined);
+ let treeNode = new TreeNode('123', {}, parentNode);
+ expect(treeNode.hasParent()).toBe(true);
+ });
+ });
+ });
+ });
+
+ describe('Tree', () => {
+ function realTreeSelectNode(tree, selectedNode) {
+ let aciTreeApi = jasmine.createSpyObj('ACITreeApi', [
+ 'selected',
+ ]);
+ tree.aciTreeApi = aciTreeApi;
+ aciTreeApi.selected.and.returnValue(selectedNode);
+ }
+
+ treeTests(Tree, realTreeSelectNode);
+ });
+
+ describe('TreeFake', () => {
+ function fakeTreeSelectNode(tree, selectedNode) {
+ tree.setSelectedNode(selectedNode);
+ }
+
+ treeTests(TreeFake, fakeTreeSelectNode);
+
+ describe('#hasParent', () => {
+ context('tree contains multiple levels', () => {
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, []);
+ tree.addNewNode('level2', {data: 'interesting'}, ['level1']);
+ });
+
+ context('node is at the first level', () => {
+ it('returns false', () => {
+ expect(tree.hasParent([{id: 'level1'}])).toBe(false);
+ });
+ });
+
+ context('node is at the second level', () => {
+ it('returns true', () => {
+ expect(tree.hasParent([{id: 'level2'}])).toBe(true);
+ });
+ });
+ });
+ });
+
+ describe('#parent', () => {
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, []);
+ tree.addNewNode('level2', {data: 'interesting'}, ['level1']);
+ });
+
+ context('node is the root', () => {
+ it('returns null', () => {
+ expect(tree.parent([{id: 'level1'}])).toBeNull();
+ });
+ });
+
+ context('node is not root', () => {
+ it('returns root element', () => {
+ expect(tree.parent([{id: 'level2'}])).toEqual([{id: 'level1'}]);
+ });
+ });
+ });
+
+ describe('#itemData', () => {
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, []);
+ tree.addNewNode('level2', {data: 'expected data'}, ['level1']);
+ });
+
+ context('retrieve data from the node', () => {
+ it('return the node data', () => {
+ expect(tree.itemData([{id: 'level2'}])).toEqual({
+ data: 'expected' +
+ ' data'
+ })
+ });
+ });
+
+ context('retrieve data from node not found', () => {
+ it('return undefined', () => {
+ expect(tree.itemData([{id: 'bamm'}])).toBeUndefined();
+ });
+ });
+ });
+ });
+});
+
[text/x-patch] 0002-Extractions-of-multiples-locations-where-we-used-the_v2.patch (142.1K, 4-0002-Extractions-of-multiples-locations-where-we-used-the_v2.patch)
download | inline diff:
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
index a7fd4c7c..10dc7e3b 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
@@ -450,29 +450,7 @@ define('pgadmin.node.schema', [
});
pgBrowser.tableChildTreeNodeHierarchy = function(i) {
- var idx = 0,
- res = {},
- t = pgBrowser.tree;
-
- do {
- var d = t.itemData(i);
- if (
- d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId
- ) {
- if (d._type === 'partition' || d._type === 'table') {
- if (!('table' in res)) {
- res['table'] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- } else {
- res[d._type] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- } while (i);
-
- return res;
+ return this.getTreeNodeHierarchy(i);
};
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js
index 72b6eb0c..1fdacf52 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js
@@ -88,7 +88,6 @@ define('pgadmin.node.column', [
if (!pgBrowser.Nodes['column']) {
pgBrowser.Nodes['column'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table', 'view', 'mview'],
collection_type: ['coll-table', 'coll-view', 'coll-mview'],
type: 'column',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js
index bf7ef556..b1cb2518 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js
@@ -8,7 +8,6 @@ define('pgadmin.node.check_constraint', [
// Check Constraint Node
if (!pgBrowser.Nodes['check_constraint']) {
pgAdmin.Browser.Nodes['check_constraint'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'check_constraint',
label: gettext('Check'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js
index 0bbf66a1..adccf2e9 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js
@@ -605,7 +605,6 @@ define('pgadmin.node.exclusion_constraint', [
// Extend the browser's node class for exclusion constraint node
if (!pgBrowser.Nodes['exclusion_constraint']) {
pgAdmin.Browser.Nodes['exclusion_constraint'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'exclusion_constraint',
label: gettext('Exclusion constraint'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js
index 4997d175..788cecfc 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js
@@ -603,7 +603,6 @@ define('pgadmin.node.foreign_key', [
// Extend the browser's node class for foreign key node
if (!pgBrowser.Nodes['foreign_key']) {
pgAdmin.Browser.Nodes['foreign_key'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'foreign_key',
label: gettext('Foreign key'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js
index d3a6cff4..0ad0f054 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js
@@ -20,7 +20,6 @@ define('pgadmin.node.primary_key', [
parent_type: ['table','partition'],
canDrop: true,
canDropCascade: true,
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
Init: function() {
/* Avoid multiple registration of menus */
if (this.initialized)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js
index 769185d6..18d3ca33 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js
@@ -20,7 +20,6 @@ define('pgadmin.node.unique_constraint', [
parent_type: ['table','partition'],
canDrop: true,
canDropCascade: true,
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
Init: function() {
/* Avoid multiple registration of menus */
if (this.initialized)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js
index cb242cd2..9c0e24fe 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js
@@ -12,14 +12,12 @@ define('pgadmin.node.constraints', [
node: 'constraints',
label: gettext('Constraints'),
type: 'coll-constraints',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
columns: ['name', 'comment'],
});
}
if (!pgBrowser.Nodes['constraints']) {
pgAdmin.Browser.Nodes['constraints'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'constraints',
label: gettext('Constraints'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js
index ec2b4da1..2b687c1a 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js
@@ -13,7 +13,6 @@ define('pgadmin.node.index', [
node: 'index',
label: gettext('Indexes'),
type: 'coll-index',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
sqlAlterHelp: 'sql-alterindex.html',
sqlCreateHelp: 'sql-createindex.html',
dialogHelp: url_for('help.static', {'filename': 'index_dialog.html'}),
@@ -215,7 +214,6 @@ define('pgadmin.node.index', [
if (!pgBrowser.Nodes['index']) {
pgAdmin.Browser.Nodes['index'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table', 'view', 'mview', 'partition'],
collection_type: ['coll-table', 'coll-view'],
sqlAlterHelp: 'sql-alterindex.html',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js
index fd53f743..ef8eb534 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js
@@ -1,10 +1,12 @@
define([
+ 'sources/tree/pgadmin_tree_node',
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.backgrid',
'pgadmin.browser.collection', 'pgadmin.browser.table.partition.utils',
],
function(
+ pgadminTreeNode,
gettext, url_for, $, _, S, pgAdmin, pgBrowser, Alertify, Backform, Backgrid
) {
@@ -13,7 +15,6 @@ function(
pgAdmin.Browser.Collection.extend({
node: 'partition',
label: gettext('Partitions'),
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'coll-partition',
columns: [
'name', 'schema', 'partition_value', 'is_partitioned', 'description',
@@ -80,36 +81,6 @@ function(
},
]);
},
- getTreeNodeHierarchy: function(i) {
- var idx = 0,
- res = {},
- t = pgBrowser.tree;
-
- do {
- var d = t.itemData(i);
- if (
- d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId
- ) {
- if (d._type == 'partition' && 'partition' in res) {
- if (!('table' in res)) {
- res['table'] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- } else if (d._type == 'table') {
- if (!('table' in res)) {
- res['table'] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- } else {
- res[d._type] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- } while (i);
-
- return res;
- },
generate_url: function(item, type, d, with_id, info) {
if (_.indexOf([
'stats', 'statistics', 'dependency', 'dependent', 'reset',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js
index 3af61754..354909f3 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js
@@ -16,7 +16,6 @@ define('pgadmin.node.rule', [
node: 'rule',
label: gettext('Rules'),
type: 'coll-rule',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
columns: ['name', 'owner', 'comment'],
});
}
@@ -35,7 +34,6 @@ define('pgadmin.node.rule', [
*/
if (!pgBrowser.Nodes['rule']) {
pgAdmin.Browser.Nodes['rule'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table','view', 'partition'],
type: 'rule',
sqlAlterHelp: 'sql-alterrule.html',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
index 647d1bfe..99d60243 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
@@ -26,7 +26,6 @@ define('pgadmin.node.table', [
if (!pgBrowser.Nodes['table']) {
pgBrowser.Nodes['table'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'table',
label: gettext('Table'),
collection_type: 'coll-table',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js
index fbe7659e..9a1ce2e0 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js
@@ -29,14 +29,12 @@ define('pgadmin.node.trigger', [
node: 'trigger',
label: gettext('Triggers'),
type: 'coll-trigger',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
columns: ['name', 'description'],
});
}
if (!pgBrowser.Nodes['trigger']) {
pgAdmin.Browser.Nodes['trigger'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table', 'view', 'partition'],
collection_type: ['coll-table', 'coll-view'],
type: 'trigger',
diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js
index 0f640a90..c94616dd 100644
--- a/web/pgadmin/browser/static/js/browser.js
+++ b/web/pgadmin/browser/static/js/browser.js
@@ -1,4 +1,5 @@
define('pgadmin.browser', [
+ 'sources/tree/tree',
'sources/gettext', 'sources/url_for', 'require', 'jquery', 'underscore', 'underscore.string',
'bootstrap', 'sources/pgadmin', 'pgadmin.alertifyjs', 'bundled_codemirror',
'sources/check_node_visibility', 'sources/modify_animation', 'pgadmin.browser.utils', 'wcdocker',
@@ -10,6 +11,7 @@ define('pgadmin.browser', [
'sources/codemirror/addon/fold/pgadmin-sqlfoldcode',
'pgadmin.browser.keyboard',
], function(
+ tree,
gettext, url_for, require, $, _, S, Bootstrap, pgAdmin, Alertify,
codemirror, checkNodeVisibility, modifyAnimation
) {
@@ -86,6 +88,7 @@ define('pgadmin.browser', [
});
b.tree = $('#tree').aciTree('api');
+ b.treeMenu.register($('#tree'));
};
// Extend the browser class attributes
@@ -100,6 +103,7 @@ define('pgadmin.browser', [
editor:null,
// Left hand browser tree
tree:null,
+ treeMenu: new tree.Tree(),
// list of script to be loaded, when a certain type of node is loaded
// It will be used to register extensions, tools, child node scripts,
// etc.
diff --git a/web/pgadmin/browser/static/js/node.js b/web/pgadmin/browser/static/js/node.js
index ad582483..3baf554a 100644
--- a/web/pgadmin/browser/static/js/node.js
+++ b/web/pgadmin/browser/static/js/node.js
@@ -1,9 +1,12 @@
define('pgadmin.browser.node', [
+ 'sources/tree/pgadmin_tree_node',
'sources/gettext', 'jquery', 'underscore', 'underscore.string', 'sources/pgadmin',
'pgadmin.browser.menu', 'backbone', 'pgadmin.alertifyjs', 'pgadmin.browser.datamodel',
'backform', 'sources/browser/generate_url', 'sources/utils', 'pgadmin.browser.utils',
'pgadmin.backform',
-], function(gettext, $, _, S, pgAdmin, Menu, Backbone, Alertify, pgBrowser, Backform, generateUrl, commonUtils) {
+], function(
+ pgadminTreeNode,
+ gettext, $, _, S, pgAdmin, Menu, Backbone, Alertify, pgBrowser, Backform, generateUrl, commonUtils) {
var wcDocker = window.wcDocker,
keyCode = {
@@ -1566,7 +1569,6 @@ define('pgadmin.browser.node', [
* depends, statistics
*/
generate_url: function(item, type, d, with_id, info) {
-
var opURL = {
'create': 'obj',
'drop': 'obj',
@@ -1608,24 +1610,7 @@ define('pgadmin.browser.node', [
Collection: pgBrowser.DataCollection,
// Base class for Node Data Model
Model: pgBrowser.DataModel,
- getTreeNodeHierarchy: function(i) {
- var idx = 0,
- res = {},
- t = pgBrowser.tree,
- d;
- do {
- d = t.itemData(i);
- if (d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId) {
- res[d._type] = _.extend({}, d, {
- 'priority': idx,
- });
- idx -= 1;
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- } while (i);
-
- return res;
- },
+ getTreeNodeHierarchy: pgadminTreeNode.getTreeNodeHierarchyFromIdentifier.bind(pgBrowser),
cache: function(url, node_info, level, data) {
var cached = this.cached = this.cached || {},
hash = url,
diff --git a/web/pgadmin/static/js/backup/backup_dialog.js b/web/pgadmin/static/js/backup/backup_dialog.js
new file mode 100644
index 00000000..779170ee
--- /dev/null
+++ b/web/pgadmin/static/js/backup/backup_dialog.js
@@ -0,0 +1,142 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../gettext';
+import {sprintf} from 'sprintf-js';
+import Backform from '../backform.pgadmin';
+import {DialogFactory} from './backup_dialog_factory';
+
+export class BackupDialog {
+ constructor(pgBrowser, $, alertify, BackupModel, backform = Backform) {
+ this.pgBrowser = pgBrowser;
+ this.jquery = $;
+ this.alertify = alertify;
+ this.backupModel = BackupModel;
+ this.backform = backform;
+ }
+
+ draw(action, item, params) {
+ const serverInformation = this.retrieveAncestorOfTypeServer(item);
+
+ if (!serverInformation) {
+ return;
+ }
+
+ if (!this.hasBinariesConfiguration(serverInformation)) {
+ return;
+ }
+
+ const typeOfDialog = BackupDialog.typeOfDialog(params);
+
+ const dialog = this.createOrGetDialog(typeOfDialog);
+ dialog(true).resizeTo('60%', '50%');
+ }
+
+ static typeOfDialog(params) {
+ if(params === null) {
+ return 'backup_objects';
+ }
+ let typeOfDialog = 'server';
+ if (!_.isUndefined(params['globals']) && params['globals']) {
+ typeOfDialog = 'globals';
+ }
+ return typeOfDialog;
+ }
+
+ static dialogTitle(typeOfDialog) {
+ if(typeOfDialog === 'backup_objects') {
+ return null;
+ }
+ return ((typeOfDialog === 'globals') ?
+ gettext('Backup Globals...') :
+ gettext('Backup Server...'));
+ }
+
+ createOrGetDialog(typeOfDialog) {
+ const dialogName = this.dialogName(typeOfDialog);
+ const dialogTitle = BackupDialog.dialogTitle(typeOfDialog);
+
+ if (!this.alertify[dialogName]) {
+ const self = this;
+ this.alertify.dialog(dialogName, function factory() {
+ return self.dialogFactory(dialogTitle, typeOfDialog);
+ });
+ }
+ return this.alertify[dialogName];
+ }
+
+ dialogName(typeOfDialog) {
+ if(typeOfDialog === 'backup_objects') {
+ return typeOfDialog;
+ }
+ return 'BackupDialog_' + typeOfDialog;
+ }
+
+ hasBinariesConfiguration(serverInformation) {
+ const module = 'paths';
+ let preference_name = 'pg_bin_dir';
+ let msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
+
+ if ((serverInformation.type && serverInformation.type === 'ppas') ||
+ serverInformation.server_type === 'ppas') {
+ preference_name = 'ppas_bin_dir';
+ msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
+ }
+ const preference = this.pgBrowser.get_preference(module, preference_name);
+
+ if (preference) {
+ if (!preference.value) {
+ this.alertify.alert(gettext('Configuration required'), msg);
+ return false;
+ }
+ } else {
+ this.alertify.alert(
+ gettext('Backup Error'),
+ sprintf(gettext('Failed to load preference %s of module %s'), preference_name, module)
+ );
+ return false;
+ }
+ return true;
+ }
+
+ retrieveAncestorOfTypeServer(item) {
+ let serverInformation = null;
+ let aciTreeItem = item || this.pgBrowser.treeMenu.selected();
+ let treeNode = this.pgBrowser.treeMenu.findNodeByDomElement(aciTreeItem);
+
+ while (treeNode) {
+ const node_data = treeNode.getData();
+ if (node_data._type === 'server') {
+ serverInformation = node_data;
+ break;
+ }
+
+ if (treeNode.hasParent()) {
+ treeNode = treeNode.parent();
+ } else {
+ this.alertify.alert(
+ gettext('Backup Error'),
+ gettext('Please select server or child node from the browser tree.')
+ );
+ break;
+ }
+ }
+ return serverInformation;
+ }
+
+ dialogFactory(dialogTitle, typeOfDialog) {
+ const factory = new DialogFactory(
+ this.pgBrowser,
+ this.jquery,
+ this.alertify,
+ this.backupModel,
+ this.backform);
+ return factory.create(dialogTitle, typeOfDialog);
+ }
+}
diff --git a/web/pgadmin/static/js/backup/backup_dialog_factory.js b/web/pgadmin/static/js/backup/backup_dialog_factory.js
new file mode 100644
index 00000000..2fd491d6
--- /dev/null
+++ b/web/pgadmin/static/js/backup/backup_dialog_factory.js
@@ -0,0 +1,56 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import * as commonUtils from '../utils';
+import {DialogWrapper} from './backup_dialog_wrapper';
+
+export class DialogFactory {
+ constructor(pgBrowser, $, alertify, BackupModel, backform) {
+ this.pgBrowser = pgBrowser;
+ this.jquery = $;
+ this.alertify = alertify;
+ this.backupModel = BackupModel;
+ this.backform = backform;
+ }
+
+ create(dialogTitle, typeOfDialog) {
+ const dialogContainerSelector = '<div class=\'backup_dialog\'></div>';
+ return new DialogWrapper(dialogContainerSelector, this, dialogTitle, typeOfDialog);
+ }
+
+ static wasBackupButtonPressed(event) {
+ return event.button['data-btn-name'] === 'backup';
+ }
+
+ static wasHelpButtonPressed(e) {
+ return e.button.element.name === 'dialog_help'
+ || e.button.element.name === 'object_help';
+ }
+
+ static getSelectedNode(selectedTreeNode) {
+ if (!DialogFactory.isNodeSelected(selectedTreeNode)) {
+ return undefined;
+ }
+ const treeNodeData = selectedTreeNode.getData();
+ if (treeNodeData) {
+ return treeNodeData;
+ }
+ return undefined;
+ }
+
+ static focusOnDialog(dialog) {
+ const container = dialog.$el.find('.tab-content:first > .tab-pane.active:first');
+ commonUtils.findAndSetFocus(container);
+ }
+
+ static isNodeSelected(selectedTreeNode) {
+ return selectedTreeNode;
+ }
+}
+
diff --git a/web/pgadmin/static/js/backup/backup_dialog_wrapper.js b/web/pgadmin/static/js/backup/backup_dialog_wrapper.js
new file mode 100644
index 00000000..e5c803c0
--- /dev/null
+++ b/web/pgadmin/static/js/backup/backup_dialog_wrapper.js
@@ -0,0 +1,270 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {getTreeNodeHierarchyFromElement} from '../tree/pgadmin_tree_node';
+import axios from 'axios/index';
+import {DialogFactory} from './backup_dialog_factory';
+import gettext from '../gettext';
+import url_for from '../url_for';
+import _ from 'underscore';
+
+export class DialogWrapper {
+ constructor(dialogContainerSelector, factory, dialogTitle, typeOfDialog) {
+ this.hooks = {
+ // Triggered when the dialog is closed
+ onclose: function () {
+ if (this.view) {
+ // clear our backform model/view
+ this.view.remove({
+ data: true,
+ internal: true,
+ silent: true,
+ });
+ }
+ },
+ };
+ this.dialogContainerSelector = dialogContainerSelector;
+ this.factory = factory;
+ this.dialogTitle = dialogTitle;
+ this.typeOfDialog = typeOfDialog;
+ }
+
+ main(title) {
+ this.set('title', title);
+ }
+
+ build() {
+ this.factory.alertify.pgDialogBuild.apply(this);
+ }
+
+ setup() {
+ return {
+ buttons: [{
+ text: '',
+ className: 'btn btn-default pull-left fa fa-lg fa-info',
+ attrs: {
+ name: 'object_help',
+ type: 'button',
+ url: 'backup.html',
+ label: gettext('Backup'),
+ },
+ }, {
+ text: '',
+ key: 112,
+ className: 'btn btn-default pull-left fa fa-lg fa-question',
+ attrs: {
+ name: 'dialog_help',
+ type: 'button',
+ label: gettext('Backup'),
+ url: url_for('help.static', {
+ 'filename': 'backup_dialog.html',
+ }),
+ },
+ }, {
+ text: gettext('Backup'),
+ key: 13,
+ className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
+ 'data-btn-name': 'backup',
+ }, {
+ text: gettext('Cancel'),
+ key: 27,
+ className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
+ 'data-btn-name': 'cancel',
+ }],
+ // Set options for dialog
+ options: {
+ title: this.dialogTitle,
+ //disable both padding and overflow control.
+ padding: !1,
+ overflow: !1,
+ model: 0,
+ resizable: true,
+ maximizable: true,
+ pinnable: false,
+ closableByDimmer: false,
+ modal: false,
+ },
+ };
+ }
+
+ prepare() {
+ this.disableBackupButton();
+
+ const $container = this.factory.jquery(this.dialogContainerSelector);
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = DialogFactory.getSelectedNode(selectedTreeNode);
+ if (!selectedTreeNodeData) {
+ return;
+ }
+
+ const node = this.factory.pgBrowser.Nodes[selectedTreeNodeData._type];
+ if (this.dialogTitle === null) {
+ const title = `Backup (${node.label}: ${selectedTreeNodeData.label})`;
+ this.main(title);
+ }
+
+ const treeInfo = getTreeNodeHierarchyFromElement(this.factory.pgBrowser, selectedTreeNode);
+ const dialog = this.createDialog(node, treeInfo, this.typeOfDialog, $container);
+ this.addAlertifyClassToBackupNodeChildNodes();
+ dialog.render();
+
+ this.elements.content.appendChild($container.get(0));
+
+ DialogFactory.focusOnDialog(dialog);
+ this.setListenersForFilenameChanges();
+ }
+
+ callback(event) {
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = DialogFactory.getSelectedNode(selectedTreeNode);
+ const node = selectedTreeNodeData && this.factory.pgBrowser.Nodes[selectedTreeNodeData._type];
+
+ if (DialogFactory.wasHelpButtonPressed(event)) {
+ event.cancel = true;
+ this.factory.pgBrowser.showHelp(
+ event.button.element.name,
+ event.button.element.getAttribute('url'),
+ node,
+ selectedTreeNode,
+ event.button.element.getAttribute('label')
+ );
+ return;
+ }
+
+ if (DialogFactory.wasBackupButtonPressed(event)) {
+
+ if (!selectedTreeNodeData)
+ return;
+
+ const serverIdentifier = this.retrieveServerIdentifier(node, selectedTreeNode);
+
+ const factory = this.factory;
+ let urlShortcut = 'backup.create_server_job';
+ if (this.typeOfDialog === 'backup_objects') {
+ urlShortcut = 'backup.create_object_job';
+ }
+ const baseUrl = url_for(urlShortcut, {
+ 'sid': serverIdentifier,
+ });
+
+ this.setExtraParameters(selectedTreeNode);
+
+ let service = axios.create({});
+ service.post(
+ baseUrl,
+ this.view.model.toJSON()
+ ).then(function () {
+ factory.alertify.success(gettext('Backup job created.'), 5);
+ factory.pgBrowser.Events.trigger('pgadmin-bgprocess:created', factory);
+ }).catch(function (error) {
+ try {
+ const err = error.response.data;
+ factory.alertify.alert(
+ gettext('Backup job failed.'),
+ err.errormsg
+ );
+ } catch (e) {
+ console.warn(e.stack || e);
+ }
+ });
+ }
+ }
+
+ addAlertifyClassToBackupNodeChildNodes() {
+ this.factory.jquery(this.elements.body.childNodes[0]).addClass(
+ 'alertify_tools_dialog_properties obj_properties'
+ );
+ }
+
+ getSelectedNode() {
+ const tree = this.factory.pgBrowser.treeMenu;
+ const selectedNode = tree.selected();
+ if (selectedNode) {
+ return tree.findNodeByDomElement(selectedNode);
+ } else {
+ return undefined;
+ }
+ }
+
+ disableBackupButton() {
+ this.__internal.buttons[2].element.disabled = true;
+ }
+
+ enableBackupButton() {
+ this.__internal.buttons[2].element.disabled = false;
+ }
+
+ createDialog(node, treeInfo, typeOfDialog, $container) {
+ let attributes = {};
+ if (typeOfDialog !== 'backup_objects') {
+ attributes['type'] = typeOfDialog;
+ }
+ // Instance of backbone model
+ const newModel = new this.factory.backupModel(attributes, {
+ node_info: treeInfo,
+ });
+ const fields = this.factory.backform.generateViewSchema(
+ treeInfo, newModel, 'create', node, treeInfo.server, true
+ );
+
+ return this.view = new this.factory.backform.Dialog({
+ el: $container,
+ model: newModel,
+ schema: fields,
+ });
+ }
+
+ retrieveServerIdentifier(node, selectedTreeNode) {
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.factory.pgBrowser,
+ selectedTreeNode
+ );
+ return treeInfo.server._id;
+ }
+
+ setListenersForFilenameChanges() {
+ const self = this;
+
+ this.view.model.on('change', function () {
+ if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
+ this.errorModel.clear();
+ self.enableBackupButton();
+ } else {
+ self.disableBackupButton();
+ this.errorModel.set('file', gettext('Please provide a filename'));
+ }
+ });
+ }
+
+ setExtraParameters(selectedTreeNode) {
+ if (this.typeOfDialog === 'backup_objects') {
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.factory.pgBrowser,
+ selectedTreeNode
+ );
+
+ this.view.model.set('database', treeInfo.database._label);
+
+ const nodeData = selectedTreeNode.getData();
+ if (nodeData._type === 'schema') {
+ this.view.model.set('schemas', [nodeData._label]);
+ }
+
+ if (nodeData._type === 'table') {
+ this.view.model.set('tables', [
+ [treeInfo.schema._label, nodeData._label],
+ ]);
+ }
+
+ if (_.isEmpty(this.view.model.get('ratio'))) {
+ this.view.model.unset('ratio');
+ }
+ }
+ }
+}
diff --git a/web/pgadmin/static/js/backup/menu_utils.js b/web/pgadmin/static/js/backup/menu_utils.js
new file mode 100644
index 00000000..f67a436a
--- /dev/null
+++ b/web/pgadmin/static/js/backup/menu_utils.js
@@ -0,0 +1,60 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export const backupSupportedNodes = [
+ 'database', 'schema', 'table', 'partition',
+];
+
+function isNodeTypeSupported(nodeDataType, parentNodeType) {
+ return _.indexOf(backupSupportedNodes, nodeDataType) !== -1
+ && parentNodeType !== 'catalog';
+}
+
+function isProvidedDataValid(treeNodeData) {
+ return !_.isUndefined(treeNodeData) && !_.isNull(treeNodeData);
+}
+
+function doesNodeHaveMenu(treeNodeData) {
+ return (treeNodeData._type === 'database' && treeNodeData.allowConn)
+ || treeNodeData._type !== 'database';
+}
+
+function retrieveParentNodeType(treeNode) {
+ if (!treeNode.hasParent()) {
+ return null;
+ }
+ if(treeNode.parent().getData() === null) {
+ return null;
+ }
+ return treeNode.parent().getData()._type;
+}
+
+export function menuEnabled(treeNodeData, domTreeNode) {
+ let treeNode = this.treeMenu.findNodeByDomElement(domTreeNode);
+ if(!treeNode) {
+ return false;
+ }
+ let parentNodeType = retrieveParentNodeType(treeNode);
+
+ if (isProvidedDataValid(treeNodeData) && !_.isNull(parentNodeType)) {
+ return isNodeTypeSupported(treeNodeData._type, parentNodeType)
+ && doesNodeHaveMenu(treeNodeData);
+ } else {
+ return false;
+ }
+}
+
+function isNodeAServerAndConnected(treeNodeData) {
+ return (('server' === treeNodeData._type) && treeNodeData.connected);
+}
+
+export function menuEnabledServer(treeNodeData) {
+ return isProvidedDataValid(treeNodeData)
+ && isNodeAServerAndConnected(treeNodeData);
+}
diff --git a/web/pgadmin/static/js/datagrid/get_panel_title.js b/web/pgadmin/static/js/datagrid/get_panel_title.js
new file mode 100644
index 00000000..f5a5664d
--- /dev/null
+++ b/web/pgadmin/static/js/datagrid/get_panel_title.js
@@ -0,0 +1,33 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {getTreeNodeHierarchyFromIdentifier} from '../tree/pgadmin_tree_node';
+
+function getDatabaseLabel(parentData) {
+ return parentData.database ? parentData.database.label
+ : parentData.server.db;
+}
+
+function isServerInformationAvailable(parentData) {
+ return parentData.server === undefined;
+}
+
+export function getPanelTitle(pgBrowser) {
+ const selected_item = pgBrowser.treeMenu.selected();
+
+ const parentData = getTreeNodeHierarchyFromIdentifier
+ .call(pgBrowser, selected_item);
+ if (isServerInformationAvailable(parentData)) {
+ return;
+ }
+
+ const db_label = getDatabaseLabel(parentData);
+
+ return `${db_label} on ${parentData.server.user.name}@${parentData.server.label}`;
+}
diff --git a/web/pgadmin/static/js/datagrid/show_data.js b/web/pgadmin/static/js/datagrid/show_data.js
new file mode 100644
index 00000000..a414fd77
--- /dev/null
+++ b/web/pgadmin/static/js/datagrid/show_data.js
@@ -0,0 +1,92 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import gettext from '../gettext';
+import url_for from '../url_for';
+import {getTreeNodeHierarchyFromIdentifier} from '../../../static/js/tree/pgadmin_tree_node';
+
+export function showDataGrid(
+ datagrid,
+ pgBrowser,
+ alertify,
+ connectionData,
+ aciTreeIdentifier
+) {
+ const node = pgBrowser.treeMenu.findNodeByDomElement(aciTreeIdentifier);
+ if (node === undefined || !node.getData()) {
+ alertify.alert(
+ gettext('Data Grid Error'),
+ gettext('No object selected.')
+ );
+ return;
+ }
+
+ const parentData = getTreeNodeHierarchyFromIdentifier.call(
+ pgBrowser,
+ aciTreeIdentifier
+ );
+
+ if (hasServerOrDatabaseConfiguration(parentData)
+ || !hasSchemaOrCatalogOrViewInformation(parentData)) {
+ return;
+ }
+
+ let namespaceName = retrieveNameSpaceName(parentData);
+ const baseUrl = generateUrl(connectionData, node.getData(), parentData);
+ const grid_title = generateDatagridTitle(parentData, namespaceName, node.getData());
+
+ datagrid.create_transaction(
+ baseUrl,
+ null,
+ 'false',
+ parentData.server.server_type,
+ '',
+ grid_title,
+ ''
+ );
+}
+
+
+function retrieveNameSpaceName(parentData) {
+ if (parentData.schema !== undefined) {
+ return parentData.schema.label;
+ }
+ else if (parentData.view !== undefined) {
+ return parentData.view.label;
+ }
+ else if (parentData.catalog !== undefined) {
+ return parentData.catalog.label;
+ }
+ return '';
+}
+
+function generateUrl(connectionData, nodeData, parentData) {
+ const url_params = {
+ 'cmd_type': connectionData.mnuid,
+ 'obj_type': nodeData._type,
+ 'sgid': parentData.server_group._id,
+ 'sid': parentData.server._id,
+ 'did': parentData.database._id,
+ 'obj_id': nodeData._id,
+ };
+
+ return url_for('datagrid.initialize_datagrid', url_params);
+}
+
+function hasServerOrDatabaseConfiguration(parentData) {
+ return parentData.server === undefined || parentData.database === undefined;
+}
+
+function hasSchemaOrCatalogOrViewInformation(parentData) {
+ return parentData.schema !== undefined || parentData.view !== undefined ||
+ parentData.catalog !== undefined;
+}
+
+function generateDatagridTitle(parentData, namespaceName, nodeData) {
+ return `${parentData.server.label} - ${parentData.database.label} - ${namespaceName}.${nodeData.label}`;
+}
diff --git a/web/pgadmin/static/js/datagrid/show_query_tool.js b/web/pgadmin/static/js/datagrid/show_query_tool.js
new file mode 100644
index 00000000..0436e0fd
--- /dev/null
+++ b/web/pgadmin/static/js/datagrid/show_query_tool.js
@@ -0,0 +1,63 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../gettext';
+import url_for from '../url_for';
+import {getTreeNodeHierarchyFromIdentifier} from '../../../static/js/tree/pgadmin_tree_node';
+
+function hasDatabaseInformation(parentData) {
+ return parentData.database;
+}
+
+function generateUrl(parentData) {
+ let url_endpoint = 'datagrid.initialize_query_tool';
+ let url_params = {
+ 'sgid': parentData.server_group._id,
+ 'sid': parentData.server._id,
+ };
+
+ if (hasDatabaseInformation(parentData)) {
+ url_params['did'] = parentData.database._id;
+ url_endpoint = 'datagrid.initialize_query_tool_with_did';
+ }
+
+ return url_for(url_endpoint, url_params);
+}
+
+function hasServerInformations(parentData) {
+ return parentData.server === undefined;
+}
+
+export function showQueryTool(datagrid, pgBrowser, alertify, url,
+ aciTreeIdentifier, panelTitle) {
+ const sURL = url || '';
+ const queryToolTitle = panelTitle || '';
+
+ const currentNode = pgBrowser.treeMenu.findNodeByDomElement(aciTreeIdentifier);
+ if (currentNode === undefined) {
+ alertify.alert(
+ gettext('Query Tool Error'),
+ gettext('No object selected.')
+ );
+ return;
+ }
+
+ const parentData = getTreeNodeHierarchyFromIdentifier.call(
+ pgBrowser, aciTreeIdentifier);
+
+ if (hasServerInformations(parentData)) {
+ return;
+ }
+
+ const baseUrl = generateUrl(parentData);
+
+ datagrid.create_transaction(
+ baseUrl, null, 'true',
+ parentData.server.server_type, sURL, queryToolTitle, '', false);
+}
diff --git a/web/pgadmin/static/js/tree/pgadmin_tree_node.js b/web/pgadmin/static/js/tree/pgadmin_tree_node.js
new file mode 100644
index 00000000..bccbe587
--- /dev/null
+++ b/web/pgadmin/static/js/tree/pgadmin_tree_node.js
@@ -0,0 +1,43 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+export function getTreeNodeHierarchyFromElement(pgBrowser, treeNode) {
+ return getTreeNodeHierarchy.call(pgBrowser, treeNode);
+}
+
+export function getTreeNodeHierarchyFromIdentifier(aciTreeNodeIdentifier) {
+ let identifier = this.treeMenu.translateTreeNodeIdFromACITree(aciTreeNodeIdentifier);
+ let currentNode = this.treeMenu.findNode(identifier);
+ return getTreeNodeHierarchy.call(this, currentNode);
+}
+
+export function getTreeNodeHierarchy(currentNode) {
+ let idx = 0;
+ let result = {};
+
+ do {
+ const currentNodeData = currentNode.getData();
+ if (currentNodeData._type in this.Nodes && this.Nodes[currentNodeData._type].hasId) {
+ const nodeType = mapType(currentNodeData._type);
+ if (result[nodeType] === undefined) {
+ result[nodeType] = _.extend({}, currentNodeData, {
+ 'priority': idx,
+ });
+ idx -= 1;
+ }
+ }
+ currentNode = currentNode.hasParent() ? currentNode.parent() : null;
+ } while (currentNode);
+
+ return result;
+}
+
+function mapType(type) {
+ return type === 'partition' ? 'table' : type;
+}
diff --git a/web/pgadmin/tools/backup/static/js/backup.js b/web/pgadmin/tools/backup/static/js/backup.js
index ddbfaee4..ffbae127 100644
--- a/web/pgadmin/tools/backup/static/js/backup.js
+++ b/web/pgadmin/tools/backup/static/js/backup.js
@@ -3,9 +3,10 @@ define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'pgadmin.alertifyjs', 'backbone', 'pgadmin.backgrid',
'pgadmin.backform', 'pgadmin.browser', 'sources/utils',
+ 'sources/backup/menu_utils', 'sources/backup/backup_dialog',
], function(
gettext, url_for, $, _, S, alertify, Backbone, Backgrid, Backform, pgBrowser,
-commonUtils
+commonUtils, menuUtils, globalBackupDialog
) {
// if module is already initialized, refer to that.
@@ -394,48 +395,6 @@ commonUtils
this.initialized = true;
- // Define list of nodes on which backup context menu option appears
- var backup_supported_nodes = [
- 'database', 'schema', 'table', 'partition',
- ];
-
- /**
- Enable/disable backup menu in tools based
- on node selected
- if selected node is present in supported_nodes,
- menu will be enabled otherwise disabled.
- Also, hide it for system view in catalogs
- */
- var menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData,
- parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
-
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(backup_supported_nodes, d._type) !== -1 &&
- parent_data._type != 'catalog') {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
- var menu_enabled_server = function(itemData) {
- // If server node selected && connected
- if (!_.isUndefined(itemData) && !_.isNull(itemData))
- return (('server' === itemData._type) && itemData.connected);
- else
- return false;
- };
-
// Define the nodes on which the menus to be appear
var menus = [{
name: 'backup_global',
@@ -445,7 +404,7 @@ commonUtils
priority: 12,
label: gettext('Backup Globals...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_server',
module: this,
@@ -454,7 +413,7 @@ commonUtils
priority: 12,
label: gettext('Backup Server...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_global_ctx',
module: this,
@@ -464,7 +423,7 @@ commonUtils
priority: 12,
label: gettext('Backup Globals...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_server_ctx',
module: this,
@@ -474,7 +433,7 @@ commonUtils
priority: 12,
label: gettext('Backup Server...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_object',
module: this,
@@ -483,20 +442,20 @@ commonUtils
priority: 11,
label: gettext('Backup...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled,
+ enable: menuUtils.menuEnabled.bind(pgBrowser),
}];
- for (var idx = 0; idx < backup_supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.backupSupportedNodes.length; idx++) {
menus.push({
- name: 'backup_' + backup_supported_nodes[idx],
- node: backup_supported_nodes[idx],
+ name: 'backup_' + menuUtils.backupSupportedNodes[idx],
+ node: menuUtils.backupSupportedNodes[idx],
module: this,
applies: ['context'],
callback: 'backup_objects',
priority: 11,
label: gettext('Backup...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled,
+ enable: menuUtils.menuEnabled.bind(pgBrowser),
});
}
@@ -521,531 +480,25 @@ commonUtils
},
// Callback to draw Backup Dialog for globals/server
- start_backup_global_server: function(action, item, params) {
- var i = item || pgBrowser.tree.selected(),
- server_data = null;
-
- while (i) {
- var node_data = pgBrowser.tree.itemData(i);
- if (node_data._type == 'server') {
- server_data = node_data;
- break;
- }
-
- if (pgBrowser.tree.hasParent(i)) {
- i = $(pgBrowser.tree.parent(i));
- } else {
- alertify.alert(
- gettext('Backup Error'),
- gettext('Please select server or child node from the browser tree.')
- );
- break;
- }
- }
-
- if (!server_data) {
- return;
- }
-
- var module = 'paths',
- preference_name = 'pg_bin_dir',
- msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
-
- if ((server_data.type && server_data.type == 'ppas') ||
- server_data.server_type == 'ppas') {
- preference_name = 'ppas_bin_dir';
- msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
- }
-
- var preference = pgBrowser.get_preference(module, preference_name);
-
- if (preference) {
- if (!preference.value) {
- alertify.alert(gettext('Configuration required'), msg);
- return;
- }
- } else {
- alertify.alert(
- gettext('Backup Error'),
- S(gettext('Failed to load preference %s of module %s')).sprintf(preference_name, module).value()
- );
- return;
- }
-
- var of_type = undefined;
-
- // Set Notes according to type of backup
- if (!_.isUndefined(params['globals']) && params['globals']) {
- of_type = 'globals';
- } else {
- of_type = 'server';
- }
-
- var DialogName = 'BackupDialog_' + of_type,
- DialogTitle = ((of_type == 'globals') ?
- gettext('Backup Globals...') :
- gettext('Backup Server...'));
-
- if (!alertify[DialogName]) {
- alertify.dialog(DialogName, function factory() {
- return {
- main: function(title) {
- this.set('title', title);
- },
- build: function() {
- alertify.pgDialogBuild.apply(this);
- },
- setup: function() {
- return {
- buttons: [{
- text: '',
- className: 'btn btn-default pull-left fa fa-lg fa-info',
- attrs: {
- name: 'object_help',
- type: 'button',
- url: 'backup.html',
- label: gettext('Backup'),
- },
- }, {
- text: '',
- key: 112,
- className: 'btn btn-default pull-left fa fa-lg fa-question',
- attrs: {
- name: 'dialog_help',
- type: 'button',
- label: gettext('Backup'),
- url: url_for('help.static', {
- 'filename': 'backup_dialog.html',
- }),
- },
- }, {
- text: gettext('Backup'),
- key: 13,
- className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
- 'data-btn-name': 'backup',
- }, {
- text: gettext('Cancel'),
- key: 27,
- className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
- 'data-btn-name': 'cancel',
- }],
- // Set options for dialog
- options: {
- title: DialogTitle,
- //disable both padding and overflow control.
- padding: !1,
- overflow: !1,
- model: 0,
- resizable: true,
- maximizable: true,
- pinnable: false,
- closableByDimmer: false,
- modal: false,
- },
- };
- },
- hooks: {
- // Triggered when the dialog is closed
- onclose: function() {
- if (this.view) {
- // clear our backform model/view
- this.view.remove({
- data: true,
- internal: true,
- silent: true,
- });
- }
- },
- },
- prepare: function() {
- var self = this;
- // Disable Backup button until user provides Filename
- this.__internal.buttons[2].element.disabled = true;
-
- var $container = $('<div class=\'backup_dialog\'></div>');
- // Find current/selected node
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (!d)
- return;
- // Create treeInfo
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
- // Instance of backbone model
- var newModel = new BackupModel({
- type: of_type,
- }, {
- node_info: treeInfo,
- }),
- fields = Backform.generateViewSchema(
- treeInfo, newModel, 'create', node, treeInfo.server, true
- );
-
- var view = this.view = new Backform.Dialog({
- el: $container,
- model: newModel,
- schema: fields,
- });
- // Add our class to alertify
- $(this.elements.body.childNodes[0]).addClass(
- 'alertify_tools_dialog_properties obj_properties'
- );
- // Render dialog
- view.render();
-
- this.elements.content.appendChild($container.get(0));
-
- var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
- commonUtils.findAndSetFocus(container);
-
- // Listen to model & if filename is provided then enable Backup button
- this.view.model.on('change', function() {
- if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
- this.errorModel.clear();
- self.__internal.buttons[2].element.disabled = false;
- } else {
- self.__internal.buttons[2].element.disabled = true;
- this.errorModel.set('file', gettext('Please provide a filename'));
- }
- });
- },
- // Callback functions when click on the buttons of the Alertify dialogs
- callback: function(e) {
- // Fetch current server id
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (e.button.element.name == 'dialog_help' || e.button.element.name == 'object_help') {
- e.cancel = true;
- pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
- node, i, e.button.element.getAttribute('label'));
- return;
- }
-
- if (e.button['data-btn-name'] === 'backup') {
-
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- var self = this,
- baseUrl = url_for('backup.create_server_job', {
- 'sid': treeInfo.server._id,
- }),
- args = this.view.model.toJSON();
-
- $.ajax({
- url: baseUrl,
- method: 'POST',
- data: {
- 'data': JSON.stringify(args),
- },
- success: function(res) {
- if (res.success) {
- alertify.success(gettext('Backup job created.'), 5);
- pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
- } else {
- console.warn(res);
- }
- },
- error: function(xhr) {
- try {
- var err = $.parseJSON(xhr.responseText);
- alertify.alert(
- gettext('Backup job failed.'),
- err.errormsg
- );
- } catch (e) {
- console.warn(e.stack || e);
- }
- },
- });
- }
- },
- };
- });
- }
- alertify[DialogName](true).resizeTo('60%', '50%');
+ start_backup_global_server: function(action, treeItem, params) {
+ let dialog = new globalBackupDialog.BackupDialog(
+ pgBrowser,
+ $,
+ alertify,
+ BackupModel
+ );
+ dialog.draw(action, treeItem, params);
},
// Callback to draw Backup Dialog for objects
backup_objects: function(action, treeItem) {
-
- var i = treeItem || pgBrowser.tree.selected(),
- server_data = null;
-
- while (i) {
- var node_data = pgBrowser.tree.itemData(i);
- if (node_data._type == 'server') {
- server_data = node_data;
- break;
- }
-
- if (pgBrowser.tree.hasParent(i)) {
- i = $(pgBrowser.tree.parent(i));
- } else {
- alertify.alert(
- gettext('Backup Error'),
- gettext('Please select server or child node from tree.')
- );
- break;
- }
- }
-
- if (!server_data) {
- return;
- }
-
- var module = 'paths',
- preference_name = 'pg_bin_dir',
- msg = gettext('Please set binary path for PostgreSQL Server from preferences.');
-
- if ((server_data.type && server_data.type == 'ppas') ||
- server_data.server_type == 'ppas') {
- preference_name = 'ppas_bin_dir';
- msg = gettext('Please set binary path for EDB Postgres Advanced Server from preferences.');
- }
-
- var preference = pgBrowser.get_preference(module, preference_name);
-
- if (preference) {
- if (!preference.value) {
- alertify.alert(gettext('Configuration required'), msg);
- return;
- }
- } else {
- alertify.alert(
- gettext('Backup Error'),
- S(gettext('Failed to load preference %s of module %s')).sprintf(preference_name, module).value()
- );
- return;
- }
-
- var title = S(gettext('Backup (%s: %s)')),
- tree = pgBrowser.tree,
- item = treeItem || tree.selected(),
- data = item && item.length == 1 && tree.itemData(item),
- node = data && data._type && pgBrowser.Nodes[data._type];
-
- if (!node)
- return;
-
- title = title.sprintf(node.label, data.label).value();
-
- if (!alertify.backup_objects) {
- // Create Dialog title on the fly with node details
- alertify.dialog('backup_objects', function factory() {
- return {
- main: function(title) {
- this.set('title', title);
- },
- build: function() {
- alertify.pgDialogBuild.apply(this);
- },
- setup: function() {
- return {
- buttons: [{
- text: '',
- className: 'btn btn-default pull-left fa fa-lg fa-info',
- attrs: {
- name: 'object_help',
- type: 'button',
- url: 'backup.html',
- label: gettext('Backup'),
- },
- }, {
- text: '',
- key: 112,
- className: 'btn btn-default pull-left fa fa-lg fa-question',
- attrs: {
- name: 'dialog_help',
- type: 'button',
- label: gettext('Backup'),
- url: url_for('help.static', {
- 'filename': 'backup_dialog.html',
- }),
- },
- }, {
- text: gettext('Backup'),
- key: 13,
- className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
- 'data-btn-name': 'backup',
- }, {
- text: gettext('Cancel'),
- key: 27,
- className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
- 'data-btn-name': 'cancel',
- }],
- // Set options for dialog
- options: {
- title: title,
- //disable both padding and overflow control.
- padding: !1,
- overflow: !1,
- model: 0,
- resizable: true,
- maximizable: true,
- pinnable: false,
- closableByDimmer: false,
- modal: false,
- },
- };
- },
- hooks: {
- // triggered when the dialog is closed
- onclose: function() {
- if (this.view) {
- this.view.remove({
- data: true,
- internal: true,
- silent: true,
- });
- }
- },
- },
- prepare: function() {
- var self = this;
- // Disable Backup button until user provides Filename
- this.__internal.buttons[2].element.disabled = true;
- var $container = $('<div class=\'backup_dialog\'></div>');
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- var newModel = new BackupObjectModel({}, {
- node_info: treeInfo,
- }),
- fields = Backform.generateViewSchema(
- treeInfo, newModel, 'create', node, treeInfo.server, true
- );
-
- var view = this.view = new Backform.Dialog({
- el: $container,
- model: newModel,
- schema: fields,
- });
-
- $(this.elements.body.childNodes[0]).addClass(
- 'alertify_tools_dialog_properties obj_properties'
- );
-
- view.render();
-
- this.elements.content.appendChild($container.get(0));
-
- if(view) {
- view.$el.attr('tabindex', -1);
- // var dialogTabNavigator = pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
- commonUtils.findAndSetFocus(container);
- }
- // Listen to model & if filename is provided then enable Backup button
- this.view.model.on('change', function() {
- if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
- this.errorModel.clear();
- self.__internal.buttons[2].element.disabled = false;
- } else {
- self.__internal.buttons[2].element.disabled = true;
- this.errorModel.set('file', gettext('Please provide filename'));
- }
- });
-
- },
- // Callback functions when click on the buttons of the Alertify dialogs
- callback: function(e) {
- // Fetch current server id
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (e.button.element.name == 'dialog_help' || e.button.element.name == 'object_help') {
- e.cancel = true;
- pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
- node, i, e.button.element.getAttribute('label'));
- return;
- }
-
- if (e.button['data-btn-name'] === 'backup') {
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- // Set current database into model
- this.view.model.set('database', treeInfo.database._label);
-
- // We will remove once object tree is implemented
- // If selected node is Schema then add it in model
- if (d._type == 'schema') {
- var schemas = [];
- schemas.push(d._label);
- this.view.model.set('schemas', schemas);
- }
- // If selected node is Table then add it in model along with
- // its schema
- if (d._type == 'table') {
- this.view.model.set(
- 'tables', [
- [treeInfo.schema._label, d._label],
- ]
- );
- }
-
- // Remove ratio attribute from model if it has empty string.
- // The valid value can be between 0 to 9.
- if (_.isEmpty(this.view.model.get('ratio'))) {
- this.view.model.unset('ratio');
- }
-
- var self = this,
- baseUrl = url_for('backup.create_object_job', {
- 'sid': treeInfo.server._id,
- }),
- args = this.view.model.toJSON();
-
- $.ajax({
- url: baseUrl,
- method: 'POST',
- data: {
- 'data': JSON.stringify(args),
- },
- success: function(res) {
- if (res.success) {
- alertify.success(gettext('Backup job created.'), 5);
- pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
- }
- },
- error: function(xhr) {
- try {
- var err = $.parseJSON(xhr.responseText);
- alertify.alert(
- gettext('Backup job failed.'),
- err.errormsg
- );
- } catch (e) {
- console.warn(e.stack || e);
- }
- },
- });
- }
- },
- };
- });
- }
- alertify.backup_objects(title).resizeTo('65%', '60%');
+ let dialog = new globalBackupDialog.BackupDialog(
+ pgBrowser,
+ $,
+ alertify,
+ BackupObjectModel
+ );
+ dialog.draw(action, treeItem, null);
},
};
return pgBrowser.Backup;
diff --git a/web/pgadmin/tools/datagrid/static/js/datagrid.js b/web/pgadmin/tools/datagrid/static/js/datagrid.js
index 473f20ad..d8d1c902 100644
--- a/web/pgadmin/tools/datagrid/static/js/datagrid.js
+++ b/web/pgadmin/tools/datagrid/static/js/datagrid.js
@@ -1,10 +1,13 @@
define('pgadmin.datagrid', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'pgadmin.alertifyjs', 'sources/pgadmin', 'bundled_codemirror',
- 'sources/sqleditor_utils', 'backbone', 'wcdocker',
+ 'sources/sqleditor_utils', 'backbone', 'sources/datagrid/show_data',
+ 'sources/datagrid/get_panel_title',
+ 'sources/datagrid/show_query_tool',
+ 'wcdocker',
], function(
gettext, url_for, $, _, alertify, pgAdmin, codemirror, sqlEditorUtils,
- Backbone
+ Backbone, showData, panelTitle, showQueryTool
) {
// Some scripts do export their object in the window only.
// Generally the one, which do no have AMD support.
@@ -161,55 +164,7 @@ define('pgadmin.datagrid', [
// This is a callback function to show data when user click on menu item.
show_data_grid: function(data, i) {
- var self = this,
- d = pgAdmin.Browser.tree.itemData(i);
- if (d === undefined) {
- alertify.alert(
- gettext('Data Grid Error'),
- gettext('No object selected.')
- );
- return;
- }
-
- // Get the parent data from the tree node hierarchy.
- var node = pgBrowser.Nodes[d._type],
- parentData = node.getTreeNodeHierarchy(i);
-
- // If server, database or schema is undefined then return from the function.
- if (parentData.server === undefined || parentData.database === undefined) {
- return;
- }
- // If schema, view, catalog object all are undefined then return from the function.
- if (parentData.schema === undefined && parentData.view === undefined &&
- parentData.catalog === undefined) {
- return;
- }
-
- var nsp_name = '';
-
- if (parentData.schema != undefined) {
- nsp_name = parentData.schema.label;
- }
- else if (parentData.view != undefined) {
- nsp_name = parentData.view.label;
- }
- else if (parentData.catalog != undefined) {
- nsp_name = parentData.catalog.label;
- }
- var url_params = {
- 'cmd_type': data.mnuid,
- 'obj_type': d._type,
- 'sgid': parentData.server_group._id,
- 'sid': parentData.server._id,
- 'did': parentData.database._id,
- 'obj_id': d._id,
- };
-
- var baseUrl = url_for('datagrid.initialize_datagrid', url_params);
- var grid_title = parentData.server.label + ' - ' + parentData.database.label + ' - '
- + nsp_name + '.' + d.label;
-
- self.create_transaction(baseUrl, null, 'false', parentData.server.server_type, '', grid_title, '');
+ showData.showDataGrid(this, pgBrowser, alertify, data, i);
},
// This is a callback function to show filtered data when user click on menu item.
@@ -382,63 +337,12 @@ define('pgadmin.datagrid', [
},
get_panel_title: function() {
- // Get the parent data from the tree node hierarchy.
- var tree = pgAdmin.Browser.tree,
- selected_item = tree.selected(),
- item_data = tree.itemData(selected_item);
-
- var node = pgBrowser.Nodes[item_data._type],
- parentData = node.getTreeNodeHierarchy(selected_item);
-
- // If server, database is undefined then return from the function.
- if (parentData.server === undefined) {
- return;
- }
- // If Database is not available then use default db
- var db_label = parentData.database ? parentData.database.label
- : parentData.server.db;
-
- var grid_title = db_label + ' on ' + parentData.server.user.name + '@' +
- parentData.server.label;
- return grid_title;
+ return panelTitle.getPanelTitle(pgBrowser);
},
// This is a callback function to show query tool when user click on menu item.
- show_query_tool: function(url, i, panel_title) {
- var sURL = url || '',
- d = pgAdmin.Browser.tree.itemData(i);
-
- panel_title = panel_title || '';
- if (d === undefined) {
- alertify.alert(
- gettext('Query Tool Error'),
- gettext('No object selected.')
- );
- return;
- }
-
- // Get the parent data from the tree node hierarchy.
- var node = pgBrowser.Nodes[d._type],
- parentData = node.getTreeNodeHierarchy(i);
-
- // If server, database is undefined then return from the function.
- if (parentData.server === undefined) {
- return;
- }
-
- var url_params = {
- 'sgid': parentData.server_group._id,
- 'sid': parentData.server._id,
- },
- url_endpoint = 'datagrid.initialize_query_tool';
- // If database not present then use Maintenance database
- // We will handle this at server side
- if (parentData.database) {
- url_params['did'] = parentData.database._id;
- url_endpoint = 'datagrid.initialize_query_tool_with_did';
- }
- var baseUrl = url_for(url_endpoint, url_params);
-
- this.create_transaction(baseUrl, null, 'true', parentData.server.server_type, sURL, panel_title, '', false);
+ show_query_tool: function(url, aciTreeIdentifier, panelTitle) {
+ showQueryTool.showQueryTool(this, pgBrowser, alertify, url,
+ aciTreeIdentifier, panelTitle);
},
create_transaction: function(baseUrl, target, is_query_tool, server_type, sURL, panel_title, sql_filter, recreate) {
var self = this;
diff --git a/web/regression/javascript/backup/backup_dialog_spec.js b/web/regression/javascript/backup/backup_dialog_spec.js
new file mode 100644
index 00000000..e272f094
--- /dev/null
+++ b/web/regression/javascript/backup/backup_dialog_spec.js
@@ -0,0 +1,651 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {BackupDialog} from '../../../pgadmin/static/js/backup/backup_dialog';
+import {TreeFake} from '../tree/tree_fake';
+import MockAdapter from 'axios-mock-adapter';
+import axios from 'axios/index';
+import {FakeModel} from '../fake_model';
+import {DialogWrapper} from "../../../pgadmin/static/js/backup/backup_dialog_wrapper";
+
+const context = describe;
+
+describe('ObjectBackupDialog', () => {
+ let backupDialog;
+ let backupNode;
+ let pgBrowser;
+ let jquerySpy;
+ let alertifySpy;
+ let backupModelSpy;
+ let backform;
+
+
+ let rootNode;
+ let serverTreeNode;
+ let databaseTreeNode;
+ let ppasServerTreeNode;
+ let noDataNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: {
+ hasId: true,
+ label: 'server',
+ getTreeNodeHierarchy: jasmine.createSpy('server.getTreeNodeHierarchy'),
+ },
+ database: {
+ hasId: true,
+ label: 'database',
+ getTreeNodeHierarchy: jasmine.createSpy('db.getTreeNodeHierarchy'),
+ },
+ schema: {
+ hasId: true,
+ label: 'schema',
+ getTreeNodeHierarchy: jasmine.createSpy('db.getTreeNodeHierarchy'),
+ },
+ }
+ };
+ pgBrowser.Nodes.server.hasId = true;
+ pgBrowser.Nodes.database.hasId = true;
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ backupModelSpy = jasmine.createSpy('backupModelSpy');
+ backupNode = {};
+ backform = jasmine.createSpyObj('backform', ['generateViewSchema', 'Dialog']);
+
+ rootNode = pgBrowser.treeMenu.addNewNode('level1', {}, []);
+ serverTreeNode = pgBrowser.treeMenu.addNewNode('level1.1', {
+ _type: 'server',
+ _id: 10,
+ }, ['level1']);
+ databaseTreeNode = pgBrowser.treeMenu.addNewNode(
+ 'level1.1.1', {
+ _type: 'database',
+ _id: 11,
+ label: 'some_database',
+ _label: 'some_database_label',
+ }, ['level1', 'level1.1']);
+ ppasServerTreeNode = pgBrowser.treeMenu.addNewNode('level1.2', {
+ _type: 'server',
+ server_type: 'ppas',
+ }, ['level1']);
+ pgBrowser.treeMenu.addNewNode('level3', {}, ['level1', 'level1.2']);
+ noDataNode = pgBrowser.treeMenu.addNewNode(
+ 'level3.1', undefined, ['level1', 'level1.2', 'level3']);
+ });
+
+ describe('#draw', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ alertifySpy['backup_objects'] = jasmine.createSpy('backup_objects');
+ backupDialog = new BackupDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ backupModelSpy
+ );
+
+ pgBrowser.get_preference = jasmine.createSpy('get_preferences');
+ });
+
+ context('there are no ancestors of the type server', () => {
+ it('does not create a dialog', () => {
+ pgBrowser.treeMenu.setSelectedNode([rootNode]);
+ backupDialog.draw(null, null, null);
+ expect(alertifySpy['backup_objects']).not.toHaveBeenCalled();
+ });
+
+ it('display an alert with a Backup Error', () => {
+ backupDialog.draw(null, [rootNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Please select server or child node from the browser tree.'
+ );
+ });
+ });
+
+ context('there is an ancestor of the type server', () => {
+ context('no preference can be found', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue(undefined);
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference pg_bin_dir of module paths'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference ppas_bin_dir of module paths'
+ );
+ });
+ });
+ });
+
+ context('preference can be found', () => {
+ context('binary folder is not configured', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue({});
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the PostgreSQL Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the EDB Advanced Server Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+ });
+
+ context('binary folder is configured', () => {
+ let backupDialogResizeToSpy;
+ let serverResizeToSpy;
+ beforeEach(() => {
+ backupDialogResizeToSpy = jasmine.createSpyObj('backupDialogResizeToSpy', ['resizeTo']);
+ alertifySpy['backup_objects'].and
+ .returnValue(backupDialogResizeToSpy);
+ pgBrowser.get_preference.and.returnValue({value: '/some/path'});
+ });
+
+ it('displays the dialog', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy['backup_objects']).toHaveBeenCalledWith(true);
+ expect(backupDialogResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
+ });
+ });
+ });
+ });
+ });
+
+ describe('#dialogFactory', () => {
+ describe('#prepare', () => {
+ let backupJQueryContainer;
+ let viewSchema;
+ let generatedBackupModel;
+ let backupNodeChildNode;
+ beforeEach(() => {
+ backupJQueryContainer = jasmine.createSpyObj('backupJQueryContainer', ['get']);
+ backupJQueryContainer.get.and.returnValue(backupJQueryContainer);
+ viewSchema = {};
+ generatedBackupModel = {};
+ backupNode.__internal = {
+ buttons: [
+ {}, {},
+ {
+ element: {
+ disabled: false,
+ }
+ }
+ ]
+ };
+ backupNode.elements = {
+ body: {
+ childNodes: [
+ {}
+ ]
+ },
+ content: jasmine.createSpyObj('content', ['appendChild']),
+ };
+
+ backform.generateViewSchema.and.returnValue(viewSchema);
+ backupModelSpy.and.returnValue(generatedBackupModel);
+
+ backupDialog = new BackupDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ backupModelSpy,
+ backform
+ );
+
+ backupNodeChildNode = jasmine.createSpyObj('something', ['addClass']);
+
+ jquerySpy.and.callFake((selector) => {
+ if (selector === '<div class=\'backup_dialog\'></div>') {
+ return backupJQueryContainer;
+ } else if (selector === backupNode.elements.body.childNodes[0]) {
+ return backupNodeChildNode;
+ }
+ })
+ });
+
+ context('selected tree node has no data', () => {
+ beforeEach(() => {
+ pgBrowser.treeMenu.setSelectedNode([noDataNode]);
+ const dialog = backupDialog.dialogFactory(null, 'backup_objects');
+ Object.assign(dialog, backupNode).prepare();
+ });
+
+ it('does not create a backform dialog', () => {
+ expect(backform.Dialog).not.toHaveBeenCalledWith();
+ });
+
+ it('disables the button submit button until a filename is selected', () => {
+ expect(backupNode.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('no tree element is selected', () => {
+ beforeEach(() => {
+ const dialog = backupDialog.dialogFactory(null, 'backup_objects');
+ Object.assign(dialog, backupNode).prepare();
+ });
+
+ it('does not create a backform dialog', () => {
+ expect(backform.Dialog).not.toHaveBeenCalledWith();
+ });
+
+ it('disables the button submit button until a filename is selected', () => {
+ expect(backupNode.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('tree element is selected', () => {
+ let treeHierarchyInformation;
+ let dialogSpy;
+ let dialog;
+ beforeEach(() => {
+ treeHierarchyInformation = {
+ database: {
+ _type: 'database',
+ _id: 11,
+ label: 'some_database',
+ _label: 'some_database_label',
+ priority: 0,
+ },
+ server: {
+ _type: 'server',
+ _id: 10,
+ priority: -1,
+ }
+ };
+ pgBrowser.treeMenu.setSelectedNode([{id: 'level1.1.1'}]);
+ pgBrowser.Nodes['database'].getTreeNodeHierarchy.and
+ .returnValue(treeHierarchyInformation);
+ dialogSpy = jasmine.createSpyObj('newView', ['render']);
+ dialogSpy.$el = jasmine.createSpyObj('$el', ['find']);
+ dialogSpy.model = jasmine.createSpyObj('model', ['on'])
+ dialogSpy.$el.find.and.returnValue([]);
+
+ backform.Dialog.and.returnValue(dialogSpy);
+
+ dialog = backupDialog.dialogFactory(null, 'backup_objects');
+ dialog.set = jasmine.createSpy('dialog.set');
+ Object.assign(dialog, backupNode).prepare();
+ });
+
+ it('creates a backform dialog and displays it', () => {
+ expect(backform.Dialog).toHaveBeenCalledWith({
+ el: backupJQueryContainer,
+ model: generatedBackupModel,
+ schema: viewSchema,
+ });
+
+ expect(dialogSpy.render).toHaveBeenCalled();
+ });
+
+ it('add alertify classes to backup node childnode', () => {
+ expect(backupNodeChildNode.addClass)
+ .toHaveBeenCalledWith('alertify_tools_dialog_properties obj_properties');
+ });
+
+ it('disables the button submit button until a filename is selected', () => {
+ expect(backupNode.__internal.buttons[2].element.disabled).toBe(true);
+ });
+
+ it('generates a new backup model', () => {
+ expect(backupModelSpy).toHaveBeenCalledWith(
+ {},
+ {node_info: treeHierarchyInformation}
+ );
+ });
+
+ it('add the new dialog to the backup node HTML', () => {
+ expect(backupNode.elements.content.appendChild).toHaveBeenCalledWith(backupJQueryContainer);
+ });
+
+ it('sets the title to "Backup (database: some_database)"', () => {
+ expect(dialog.set).toHaveBeenCalledWith('title', 'Backup (database: some_database)');
+ });
+ });
+ });
+
+ describe('#onButtonClicked', () => {
+ let backupJQueryContainer;
+ let backupNodeChildNode;
+ let dialog;
+ let networkMock;
+
+ beforeEach(() => {
+ networkMock = new MockAdapter(axios);
+ backupJQueryContainer = jasmine.createSpyObj('backupJQueryContainer', ['get']);
+ backupJQueryContainer.get.and.returnValue(backupJQueryContainer);
+ pgBrowser.showHelp = jasmine.createSpy('showHelp');
+ alertifySpy = jasmine.createSpyObj('alertify', ['success', 'alert']);
+
+ backupDialog = new BackupDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ backupModelSpy,
+ backform
+ );
+
+ backupNodeChildNode = jasmine.createSpyObj('something', ['addClass']);
+
+ dialog = Object.assign(
+ backupDialog.dialogFactory(null, 'backup_objects'),
+ backupNode
+ );
+ });
+
+ afterEach(() => {
+ networkMock.restore();
+ });
+
+ context('dialog help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ pgBrowser.treeMenu.setSelectedNode([{id: 'level1.1.1'}]);
+ networkCalled = false;
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ element: {
+ name: 'dialog_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ }
+ }
+ }
+ };
+ dialog.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'dialog_help',
+ 'http://someurl',
+ pgBrowser.Nodes['database'],
+ databaseTreeNode,
+ 'some label'
+ )
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('object help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ pgBrowser.treeMenu.setSelectedNode([{id: 'level1.1.1'}]);
+ networkCalled = false;
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ element: {
+ name: 'object_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ }
+ }
+ }
+ };
+ dialog.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'object_help',
+ 'http://someurl',
+ pgBrowser.Nodes['database'],
+ databaseTreeNode,
+ 'some label'
+ )
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('backup button was pressed', () => {
+ context('no tree node is selected', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onPost('/backup/job/10/object').reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ }
+ }
+ }
+ };
+ dialog.callback(event);
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('selected node has no data', () => {
+ let networkCalled;
+ beforeEach(() => {
+ pgBrowser.treeMenu.setSelectedNode([noDataNode]);
+ networkCalled = false;
+ networkMock.onPost('/backup/job/10/object').reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ }
+ }
+ }
+ };
+ dialog.callback(event);
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('selected node has data', () => {
+ beforeEach(() => {
+ pgBrowser.treeMenu.setSelectedNode([{id: 'level1.1.1'}]);
+ pgBrowser.Events = jasmine.createSpyObj('Events', ['trigger']);
+
+ backupNode.view = {
+ model: new FakeModel(),
+ };
+ backupNode.view.model.set('dqoute', false);
+ backupNode.view.model.set('file', 'test');
+ backupNode.view.model.set('verbose', true);
+
+ const event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ }
+ }
+ }
+ };
+ Object.assign(dialog, backupNode).callback(event);
+ });
+ context('creation of backup job successfull', () => {
+ let dataSentToServer;
+ beforeEach(() => {
+ networkMock.onPost('/backup/job/10/object').reply((request) => {
+ dataSentToServer = request.data;
+ return [200, {}];
+ });
+ });
+
+ it('creates an success alert box', (done) => {
+ setTimeout(() => {
+ expect(alertifySpy.success).toHaveBeenCalledWith('Backup job' +
+ ' created.', 5);
+ done();
+ }, 0);
+ });
+
+ it('trigger background process', (done) => {
+ setTimeout(() => {
+ expect(pgBrowser.Events.trigger).toHaveBeenCalledWith(
+ 'pgadmin-bgprocess:created',
+ jasmine.anything()
+ );
+ done();
+ }, 0);
+ });
+
+ it('send correct data to server', () => {
+ expect(dataSentToServer).toEqual(JSON.stringify({
+ dqoute: false,
+ file: 'test',
+ verbose: true,
+ database: 'some_database_label'
+ }));
+ });
+ });
+
+ context('error creating backup job', () => {
+ beforeEach(() => {
+ const response = {
+ errormsg: 'some error message'
+ };
+
+ networkMock.onPost('/backup/job/10/object').reply(() => {
+ return [500, response];
+ });
+ });
+
+ it('creates an alert box', (done) => {
+ setTimeout(() => {
+ expect(alertifySpy.alert).toHaveBeenCalledWith('Backup job' +
+ ' failed.', 'some error message');
+ done();
+ }, 0);
+ });
+ });
+ });
+ });
+ });
+ });
+
+ describe('BackupDialogWrapper', () => {
+ describe('#setExtraParameters', () => {
+ let model;
+ let dialog;
+ let schemaNode;
+ let tableNode;
+ beforeEach(() => {
+ model = new FakeModel();
+ dialog = new DialogWrapper({}, {
+ pgBrowser: pgBrowser,
+ }, '', 'backup_objects');
+ dialog.view = {
+ model: model,
+ };
+ schemaNode = pgBrowser.treeMenu.addNewNode(
+ 'schema', {
+ _label: 'schema_label',
+ _type: 'schema',
+ }, ['level1', 'level1.1', 'level1.1.1']);
+ tableNode = pgBrowser.treeMenu.addNewNode(
+ 'table', {
+ _label: 'table_label',
+ _type: 'table',
+ }, ['level1', 'level1.1', 'level1.1.1', 'schema']);
+ });
+
+ context('type is schema', () => {
+ it('add schema array to the model', () => {
+ dialog.setExtraParameters(schemaNode);
+ expect(model.get('schemas')).toEqual(['schema_label']);
+ });
+ });
+
+ context('type is table', () => {
+ it('add schema and table to the model', () => {
+ dialog.setExtraParameters(tableNode);
+ expect(model.get('tables')).toEqual([['schema_label', 'table_label']]);
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/backup/global_server_backup_dialog_spec.js b/web/regression/javascript/backup/global_server_backup_dialog_spec.js
new file mode 100644
index 00000000..0b229038
--- /dev/null
+++ b/web/regression/javascript/backup/global_server_backup_dialog_spec.js
@@ -0,0 +1,585 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {BackupDialog} from '../../../pgadmin/static/js/backup/backup_dialog';
+import {TreeFake} from '../tree/tree_fake';
+import MockAdapter from 'axios-mock-adapter';
+import axios from 'axios/index';
+import {FakeModel} from '../fake_model';
+
+const context = describe;
+
+describe('GlobalServerBackupDialog', () => {
+ let backupDialog;
+ let backupNode;
+ let pgBrowser;
+ let jquerySpy;
+ let alertifySpy;
+ let backupModelSpy;
+ let backform;
+
+
+ let rootNode;
+ let serverTreeNode;
+ let ppasServerTreeNode;
+ let noDataNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: jasmine.createSpyObj('Node[server]', ['getTreeNodeHierarchy']),
+ }
+ };
+ pgBrowser.Nodes.server.hasId = true;
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ backupModelSpy = jasmine.createSpy('backupModelSpy');
+ backupNode = {};
+ backform = jasmine.createSpyObj('backform', ['generateViewSchema', 'Dialog']);
+
+ rootNode = pgBrowser.treeMenu.addNewNode('level1', {}, []);
+ serverTreeNode = pgBrowser.treeMenu.addNewNode('level1.1', {
+ _type: 'server',
+ _id: 10,
+ }, ['level1']);
+ ppasServerTreeNode = pgBrowser.treeMenu.addNewNode('level1.2', {
+ _type: 'server',
+ server_type: 'ppas',
+ }, ['level1']);
+ pgBrowser.treeMenu.addNewNode('level3', {}, ['level1', 'level1.2']);
+ noDataNode = pgBrowser.treeMenu.addNewNode(
+ 'level3.1', undefined, ['level1', 'level1.2', 'level3']);
+ });
+
+ describe('#draw', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ alertifySpy['BackupDialog_globals'] = jasmine.createSpy('BackupDialog_globals');
+ alertifySpy['BackupDialog_server'] = jasmine.createSpy('BackupDialog_server');
+ backupDialog = new BackupDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ backupModelSpy
+ );
+
+ pgBrowser.get_preference = jasmine.createSpy('get_preferences');
+ });
+
+ context('there are no ancestors of the type server', () => {
+ it('does not create a dialog', () => {
+ pgBrowser.treeMenu.setSelectedNode([{id: 'level1'}]);
+ backupDialog.draw(null, null, null);
+ expect(alertifySpy['BackupDialog_globals']).not.toHaveBeenCalled();
+ expect(alertifySpy['BackupDialog_server']).not.toHaveBeenCalled();
+ });
+
+ it('display an alert with a Backup Error', () => {
+ backupDialog.draw(null, [rootNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Please select server or child node from the browser tree.'
+ );
+ });
+ });
+
+ context('there is an ancestor of the type server', () => {
+ context('no preference can be found', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue(undefined);
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference pg_bin_dir of module paths'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference ppas_bin_dir of module paths'
+ );
+ });
+ });
+ });
+
+ context('preference can be found', () => {
+ context('binary folder is not configured', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue({});
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the PostgreSQL Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the EDB Advanced Server Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+ });
+
+ context('binary folder is configured', () => {
+ let globalResizeToSpy;
+ let serverResizeToSpy;
+ beforeEach(() => {
+ globalResizeToSpy = jasmine.createSpyObj('globals', ['resizeTo']);
+ alertifySpy['BackupDialog_globals'].and
+ .returnValue(globalResizeToSpy);
+ serverResizeToSpy = jasmine.createSpyObj('server', ['resizeTo']);
+ alertifySpy['BackupDialog_server'].and
+ .returnValue(serverResizeToSpy);
+ pgBrowser.get_preference.and.returnValue({value: '/some/path'});
+ });
+
+ context('dialog for global backup', () => {
+ it('displays the dialog', () => {
+ backupDialog.draw(null, [serverTreeNode], {globals: true});
+ expect(alertifySpy['BackupDialog_globals']).toHaveBeenCalledWith(true);
+ expect(globalResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
+ });
+ });
+
+ context('dialog for global backup', () => {
+ it('displays the dialog', () => {
+ backupDialog.draw(null, [serverTreeNode], {server: true});
+ expect(alertifySpy['BackupDialog_server']).toHaveBeenCalledWith(true);
+ expect(serverResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
+ });
+ });
+ });
+ });
+ });
+ });
+
+ describe('#dialogFactory', () => {
+ describe('#prepare', () => {
+ let backupJQueryContainer;
+ let viewSchema;
+ let generatedBackupModel;
+ let backupNodeChildNode;
+ beforeEach(() => {
+ backupJQueryContainer = jasmine.createSpyObj('backupJQueryContainer', ['get']);
+ backupJQueryContainer.get.and.returnValue(backupJQueryContainer);
+ viewSchema = {};
+ generatedBackupModel = {};
+ backupNode.__internal = {
+ buttons: [
+ {}, {},
+ {
+ element: {
+ disabled: false,
+ }
+ }
+ ]
+ };
+ backupNode.elements = {
+ body: {
+ childNodes: [
+ {}
+ ]
+ },
+ content: jasmine.createSpyObj('content', ['appendChild']),
+ };
+
+ backform.generateViewSchema.and.returnValue(viewSchema);
+ backupModelSpy.and.returnValue(generatedBackupModel);
+
+ backupDialog = new BackupDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ backupModelSpy,
+ backform
+ );
+
+ backupNodeChildNode = jasmine.createSpyObj('something', ['addClass']);
+
+ jquerySpy.and.callFake((selector) => {
+ if (selector === '<div class=\'backup_dialog\'></div>') {
+ return backupJQueryContainer;
+ } else if (selector === backupNode.elements.body.childNodes[0]) {
+ return backupNodeChildNode;
+ }
+ })
+ });
+
+ context('selected tree node has no data', () => {
+ beforeEach(() => {
+ pgBrowser.treeMenu.setSelectedNode([noDataNode]);
+ const dialog = backupDialog.dialogFactory('Some title', 'globals');
+ Object.assign(dialog, backupNode).prepare();
+ });
+
+ it('does not create a backform dialog', () => {
+ expect(backform.Dialog).not.toHaveBeenCalledWith();
+ });
+
+ it('disables the button submit button until a filename is selected', () => {
+ expect(backupNode.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('no tree element is selected', () => {
+ beforeEach(() => {
+ const dialog = backupDialog.dialogFactory('Some title', 'globals');
+ Object.assign(dialog, backupNode).prepare();
+ });
+
+ it('does not create a backform dialog', () => {
+ expect(backform.Dialog).not.toHaveBeenCalledWith();
+ });
+
+ it('disables the button submit button until a filename is selected', () => {
+ expect(backupNode.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('tree element is selected', () => {
+ let treeHierarchyInformation;
+ let dialogSpy;
+ beforeEach(() => {
+ treeHierarchyInformation = {
+ server: {
+ _type: 'server',
+ _id: 10,
+ priority: 0,
+ }
+ };
+ pgBrowser.treeMenu.setSelectedNode([serverTreeNode]);
+ pgBrowser.Nodes['server'].getTreeNodeHierarchy.and
+ .returnValue(treeHierarchyInformation);
+ dialogSpy = jasmine.createSpyObj('newView', ['render']);
+ dialogSpy.$el = jasmine.createSpyObj('$el', ['find']);
+ dialogSpy.model = jasmine.createSpyObj('model', ['on'])
+ dialogSpy.$el.find.and.returnValue([]);
+
+ backform.Dialog.and.returnValue(dialogSpy);
+
+ const dialog = backupDialog.dialogFactory('Some title', 'globals');
+ Object.assign(dialog, backupNode).prepare();
+ });
+
+ it('creates a backform dialog and displays it', () => {
+ expect(backform.Dialog).toHaveBeenCalledWith({
+ el: backupJQueryContainer,
+ model: generatedBackupModel,
+ schema: viewSchema,
+ });
+
+ expect(dialogSpy.render).toHaveBeenCalled();
+ });
+
+ it('add alertify classes to backup node childnode', () => {
+ expect(backupNodeChildNode.addClass)
+ .toHaveBeenCalledWith('alertify_tools_dialog_properties obj_properties');
+ });
+
+ it('disables the button submit button until a filename is selected', () => {
+ expect(backupNode.__internal.buttons[2].element.disabled).toBe(true);
+ });
+
+ it('generates a new backup model', () => {
+ expect(backupModelSpy).toHaveBeenCalledWith(
+ {type: 'globals'},
+ {node_info: treeHierarchyInformation}
+ );
+ });
+
+ it('add the new dialog to the backup node HTML', () => {
+ expect(backupNode.elements.content.appendChild).toHaveBeenCalledWith(backupJQueryContainer);
+ });
+ });
+ });
+
+ describe('#onButtonClicked', () => {
+ let backupJQueryContainer;
+ let backupNodeChildNode;
+ let dialog;
+ let networkMock;
+
+ beforeEach(() => {
+ networkMock = new MockAdapter(axios);
+ backupJQueryContainer = jasmine.createSpyObj('backupJQueryContainer', ['get']);
+ backupJQueryContainer.get.and.returnValue(backupJQueryContainer);
+ pgBrowser.showHelp = jasmine.createSpy('showHelp');
+ alertifySpy = jasmine.createSpyObj('alertify', ['success', 'alert']);
+
+ backupDialog = new BackupDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ backupModelSpy,
+ backform
+ );
+
+ backupNodeChildNode = jasmine.createSpyObj('something', ['addClass']);
+
+ dialog = Object.assign(backupDialog.dialogFactory('Some title', 'server'), backupNode);
+ });
+
+ afterEach(() => {
+ networkMock.restore();
+ });
+
+ context('dialog help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ pgBrowser.treeMenu.setSelectedNode([serverTreeNode]);
+ networkCalled = false;
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ element: {
+ name: 'dialog_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ }
+ }
+ }
+ };
+ dialog.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'dialog_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ )
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('object help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ pgBrowser.treeMenu.setSelectedNode([serverTreeNode]);
+ networkCalled = false;
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ element: {
+ name: 'object_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ }
+ }
+ }
+ };
+ dialog.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'object_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ )
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('backup button was pressed', () => {
+ context('no tree node is selected', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ }
+ }
+ }
+ };
+ dialog.callback(event);
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('selected node has no data', () => {
+ let networkCalled;
+ beforeEach(() => {
+ pgBrowser.treeMenu.setSelectedNode([noDataNode]);
+ networkCalled = false;
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ }
+ }
+ }
+ };
+ dialog.callback(event);
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('selected node has data', () => {
+ beforeEach(() => {
+ pgBrowser.treeMenu.setSelectedNode([serverTreeNode]);
+ pgBrowser.Events = jasmine.createSpyObj('Events', ['trigger']);
+
+ backupNode.view = {
+ model: new FakeModel(),
+ };
+ backupNode.view.model.set('dqoute', false);
+ backupNode.view.model.set('file', 'test');
+ backupNode.view.model.set('type', 'globals');
+ backupNode.view.model.set('verbose', true);
+
+ const event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ }
+ }
+ }
+ };
+ Object.assign(dialog, backupNode).callback(event);
+ });
+ context('creation of backup job successfull', () => {
+ let dataSentToServer;
+ beforeEach(() => {
+ networkMock.onPost('/backup/job/10').reply((request) => {
+ dataSentToServer = request.data;
+ return [200, {}];
+ });
+ });
+
+ it('creates an success alert box', (done) => {
+ setTimeout(() => {
+ expect(alertifySpy.success).toHaveBeenCalledWith('Backup job' +
+ ' created.', 5);
+ done();
+ }, 0);
+ });
+
+ it('trigger background process', (done) => {
+ setTimeout(() => {
+ expect(pgBrowser.Events.trigger).toHaveBeenCalledWith(
+ 'pgadmin-bgprocess:created',
+ jasmine.anything()
+ );
+ done();
+ }, 0);
+ });
+
+ it('send correct data to server', () => {
+ expect(dataSentToServer).toEqual(JSON.stringify({
+ dqoute: false,
+ file: 'test',
+ type: 'globals',
+ verbose: true
+ }));
+ });
+ });
+
+ context('error creating backup job', () => {
+ beforeEach(() => {
+ const response = {
+ errormsg: 'some error message'
+ };
+
+ networkMock.onPost('/backup/job/10').reply(() => {
+ return [500, response];
+ });
+ });
+
+ it('creates an alert box', (done) => {
+ setTimeout(() => {
+ expect(alertifySpy.alert).toHaveBeenCalledWith('Backup job' +
+ ' failed.', 'some error message');
+ done();
+ }, 0);
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/backup/menu_utils_spec.js b/web/regression/javascript/backup/menu_utils_spec.js
new file mode 100644
index 00000000..8f776976
--- /dev/null
+++ b/web/regression/javascript/backup/menu_utils_spec.js
@@ -0,0 +1,167 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+
+import {TreeFake} from "../tree/tree_fake";
+import {
+ menuEnabledServer,
+ menuEnabled
+} from "../../../pgadmin/static/js/backup/menu_utils";
+
+const context = describe;
+
+describe('backup.menuUtils', () => {
+ describe('#menuEnabled', () => {
+ let ourBrowser;
+ beforeEach(() => {
+ const tree = new TreeFake();
+ ourBrowser = {
+ treeMenu: tree,
+ };
+ tree.addNewNode('level1', {}, []);
+ tree.addNewNode('level1.1', {_type: 'catalog'}, ['level1']);
+ tree.addNewNode('level1.1.1', {_type: 'database'}, ['level1', 'level1.1']);
+ tree.addNewNode('level1.2', {_type: 'bamm'}, ['level1']);
+ tree.addNewNode('level1.2.1', {
+ _type: 'database',
+ allowConn: true
+ }, ['level1', 'level1.2']);
+ tree.addNewNode('level1.2.2', {
+ _type: 'database',
+ allowConn: false
+ }, ['level1', 'level1.2']);
+ tree.addNewNode('level1.2.3', {
+ _type: 'table'
+ }, ['level1', 'level1.2']);
+
+ tree.addNewNode('level2', {}, []);
+ tree.addNewNode('level2.1', null, ['level2']);
+ tree.addNewNode('level2.1.1', {}, ['level2', 'level2.1']);
+ });
+
+ context('When the current node is a root node', () => {
+ it('return false', () => {
+ expect(menuEnabled.apply(ourBrowser, [{}, [{id: 'level1'}]])).toBe(false);
+ });
+ });
+
+ context('when current node does not exist', () => {
+ it('return false', () => {
+ expect(menuEnabled.apply(ourBrowser, [{}, [{id: 'bamm'}]])).toBe(false);
+ });
+ });
+
+ context('When the current node is not a root node', () => {
+ context('parent data does not exist', () => {
+ it('returns false', () => {
+ expect(menuEnabled.apply(ourBrowser, [{}, [{id: 'level2.1.1'}]])).toBe(false);
+ });
+ });
+
+ context('parent as data', () => {
+ context('the current node type is in the supported node types', () => {
+ context('the parent is of the type catalog', () => {
+ it('returns false', () => {
+ expect(menuEnabled.apply(ourBrowser, [
+ {_type: 'schema'},
+ [{id: 'level1.1.1'}]
+ ])).toBe(false);
+ });
+ });
+ context('the parent is not of the type catalog', () => {
+ context('current node is of the type database', () => {
+ context('current node allows connection', () => {
+ it('returns true', () => {
+ expect(menuEnabled.apply(ourBrowser, [{
+ _type: 'database',
+ allowConn: true
+ }, [{id: 'level1.2.1'}]])).toBe(true);
+ });
+ });
+ context('current node do not allow connection', () => {
+ it('returns false', () => {
+ expect(menuEnabled.apply(ourBrowser, [{
+ _type: 'database',
+ allowConn: false
+ }, [{id: 'level1.2.2'}]])).toBe(false);
+ });
+ });
+ });
+ context('current node is not of the type database', () => {
+ it('returns true', () => {
+ expect(menuEnabled.apply(ourBrowser, [{
+ _type: 'schema'
+ }, [{id: 'level1.2.3'}]])).toBe(true);
+ });
+ });
+ });
+ });
+ context('the current node type is not in the supported node types', () => {
+ it('returns false', () => {
+ expect(menuEnabled.apply(ourBrowser, [{_type: 'catalog'}, [{id: 'level1.1'}]])).toBe(false);
+ });
+ });
+ });
+ });
+
+ context('provided node data is undefined', () => {
+ it('returns false', () => {
+ expect(menuEnabled.apply(ourBrowser, [undefined, [{id: 'level1'}]])).toBe(false);
+ });
+ });
+
+ context('provided node data is null', () => {
+ it('returns false', () => {
+ expect(menuEnabled.apply(ourBrowser, [null, [{id: 'level1'}]])).toBe(false);
+ });
+ });
+ });
+
+ describe('#menuEnabledServer', () => {
+ let ourBrowser;
+ beforeEach(() => {
+ const tree = new TreeFake();
+ ourBrowser = {
+ treeMenu: tree,
+ };
+ });
+
+ context('provided node data is undefined', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer(undefined)).toBe(false);
+ });
+ });
+
+ context('provided node data is null', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer(null)).toBe(false);
+ });
+ });
+
+ context('current node type is not of the type server', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer({_type: 'schema'})).toBe(false);
+ });
+ });
+
+ context('current node type is of the type server', () => {
+ context('is connected', () => {
+ it('returns true', () => {
+ expect(menuEnabledServer({_type: 'server', connected: true})).toBe(true);
+ });
+ });
+ context('is not connected', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer({_type: 'server', connected: false})).toBe(false);
+ });
+ });
+ });
+ });
+});
+
diff --git a/web/regression/javascript/datagrid/get_panel_title_spec.js b/web/regression/javascript/datagrid/get_panel_title_spec.js
new file mode 100644
index 00000000..3baedaa1
--- /dev/null
+++ b/web/regression/javascript/datagrid/get_panel_title_spec.js
@@ -0,0 +1,79 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {getPanelTitle} from "../../../pgadmin/static/js/datagrid/get_panel_title";
+import {TreeFake} from "../tree/tree_fake";
+
+const context = describe;
+const xcontext = xdescribe;
+
+describe('#getPanelTitle', () => {
+ let pgBrowser;
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ pgBrowser = {
+ treeMenu: tree,
+ Nodes: {
+ server: {
+ hasId: true,
+ _type: 'server',
+ },
+ database: {
+ hasId: true,
+ _type: 'database',
+ },
+ }
+ };
+ });
+
+ context('selected node does not belong to a server', () => {
+ it('returns undefined', () => {
+ tree.addNewNode('level1', {_type: 'server_groups'}, []);
+ tree.addNewNode('level1.1', {_type: 'other'}, ['level1']);
+ tree.setSelectedNode([{id: 'level1'}]);
+ expect(getPanelTitle(pgBrowser)).toBeUndefined();
+ });
+ });
+
+ context('selected node belong to a server', () => {
+ context('selected node does not belong to a database', () => {
+ it('returns the server label and the username', () => {
+ tree.addNewNode('level1', {
+ _type: 'server',
+ db: 'other db label',
+ user: {name: 'some user name'},
+ label: 'server label'
+ }, []);
+ tree.setSelectedNode([{id: 'level1'}]);
+ expect(getPanelTitle(pgBrowser))
+ .toBe('other db label on some user name@server label');
+ });
+ });
+
+ context('selected node belongs to a database', () => {
+ it('returns the database label and the username', () => {
+ tree.addNewNode('level1', {
+ _type: 'server',
+ db: 'other db label',
+ user: {name: 'some user name'},
+ label: 'server label'
+ }, []);
+ tree.addNewNode('level1.1', {
+ _type: 'database',
+ label: 'db label'
+ }, ['level1']);
+ tree.addNewNode('level1.1.1', {_type: 'table'}, ['level1', 'level1.1']);
+ tree.setSelectedNode([{id: 'level1.1.1'}]);
+ expect(getPanelTitle(pgBrowser))
+ .toBe('db label on some user name@server label');
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/datagrid/show_data_spec.js b/web/regression/javascript/datagrid/show_data_spec.js
new file mode 100644
index 00000000..e0ef8783
--- /dev/null
+++ b/web/regression/javascript/datagrid/show_data_spec.js
@@ -0,0 +1,159 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {showDataGrid} from "../../../pgadmin/static/js/datagrid/show_data";
+import {TreeFake} from "../tree/tree_fake";
+
+const context = describe;
+
+describe('#show_data', () => {
+ let datagrid;
+ let pgBrowser;
+ let alertify;
+ beforeEach(() => {
+ alertify = jasmine.createSpyObj('alertify', ['alert']);
+ datagrid = {
+ create_transaction: jasmine.createSpy('create_transaction'),
+ };
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server_group: {
+ _type: 'server_group',
+ hasId: true,
+ },
+ server: {
+ _type: 'server',
+ hasId: true,
+ },
+ database: {
+ _type: 'database',
+ hasId: true,
+ },
+ schema: {
+ _type: 'schema',
+ hasId: true,
+ },
+ view: {
+ _type: 'view',
+ hasId: true,
+ },
+ catalog: {
+ _type: 'catalog',
+ hasId: true,
+ },
+ }
+ };
+ pgBrowser.treeMenu.addNewNode('parent', {_type: 'parent'}, []);
+ pgBrowser.treeMenu.addNewNode('server_group1', {
+ _type: 'server_group',
+ _id: 1,
+ }, ['parent']);
+ pgBrowser.treeMenu.addNewNode('server1', {
+ _type: 'server',
+ label: 'server1',
+ server_type: 'pg',
+ _id: 2,
+ }, ['parent', 'server_group1']);
+ pgBrowser.treeMenu.addNewNode('database1', {
+ _type: 'database',
+ label: 'database1',
+ _id: 3,
+ }, ['parent', 'server_group1', 'server1']);
+ pgBrowser.treeMenu.addNewNode('schema1', {
+ _type: 'schema',
+ label: 'schema1',
+ _id: 4,
+ }, ['parent', 'server_group1', 'server1', 'database1']);
+ pgBrowser.treeMenu.addNewNode('view1', {
+ _type: 'view',
+ label: 'view1',
+ _id: 5,
+ }, ['parent', 'server_group1', 'server1', 'database1']);
+ pgBrowser.treeMenu.addNewNode('catalog1', {
+ _type: 'catalog',
+ label: 'catalog1',
+ _id: 6,
+ }, ['parent', 'server_group1', 'server1', 'database1']);
+ });
+
+ context('cannot find the tree node', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: '10'}]);
+ expect(datagrid.create_transaction).not.toHaveBeenCalled();
+ });
+
+ it('display alert', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: '10'}]);
+ expect(alertify.alert).toHaveBeenCalledWith(
+ 'Data Grid Error',
+ 'No object selected.'
+ );
+ });
+ });
+
+ context('current node is not underneath a server', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: 'parent'}]);
+ expect(datagrid.create_transaction).not.toHaveBeenCalled();
+ });
+ });
+
+ context('current node is not underneath a schema or view or catalog', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: 'database1'}]);
+ expect(datagrid.create_transaction).not.toHaveBeenCalled();
+ });
+ });
+
+ context('current node is underneath a schema', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {mnuid: 11}, [{id: 'schema1'}]);
+ expect(datagrid.create_transaction).toHaveBeenCalledWith(
+ '/initialize/datagrid/11/schema/1/2/3/4',
+ null,
+ 'false',
+ 'pg',
+ '',
+ 'server1 - database1 - schema1.schema1',
+ ''
+ );
+ });
+ });
+
+ context('current node is underneath a view', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {mnuid: 11}, [{id: 'view1'}]);
+ expect(datagrid.create_transaction).toHaveBeenCalledWith(
+ '/initialize/datagrid/11/view/1/2/3/5',
+ null,
+ 'false',
+ 'pg',
+ '',
+ 'server1 - database1 - view1.view1',
+ ''
+ );
+ });
+ });
+
+ context('current node is underneath a catalog', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {mnuid: 11}, [{id: 'catalog1'}]);
+ expect(datagrid.create_transaction).toHaveBeenCalledWith(
+ '/initialize/datagrid/11/catalog/1/2/3/6',
+ null,
+ 'false',
+ 'pg',
+ '',
+ 'server1 - database1 - catalog1.catalog1',
+ ''
+ );
+ });
+ });
+});
diff --git a/web/regression/javascript/datagrid/show_query_tool_spec.js b/web/regression/javascript/datagrid/show_query_tool_spec.js
new file mode 100644
index 00000000..b36dc4a7
--- /dev/null
+++ b/web/regression/javascript/datagrid/show_query_tool_spec.js
@@ -0,0 +1,119 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {TreeFake} from '../tree/tree_fake';
+import {showQueryTool} from '../../../pgadmin/static/js/datagrid/show_query_tool';
+
+const context = describe;
+
+describe('#showQueryTool', () => {
+ let queryTool;
+ let pgBrowser;
+ let alertify;
+ beforeEach(() => {
+ alertify = jasmine.createSpyObj('alertify', ['alert']);
+ queryTool = {
+ create_transaction: jasmine.createSpy('create_transaction'),
+ };
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server_group: {
+ _type: 'server_group',
+ hasId: true,
+ },
+ server: {
+ _type: 'server',
+ hasId: true,
+ },
+ database: {
+ _type: 'database',
+ hasId: true,
+ },
+ }
+ };
+ pgBrowser.treeMenu.addNewNode('parent', {_type: 'parent'}, []);
+ pgBrowser.treeMenu.addNewNode('server_group1', {
+ _type: 'server_group',
+ _id: 1,
+ }, ['parent']);
+ pgBrowser.treeMenu.addNewNode('server1', {
+ _type: 'server',
+ label: 'server1',
+ server_type: 'pg',
+ _id: 2,
+ }, ['parent', 'server_group1']);
+ pgBrowser.treeMenu.addNewNode('database1', {
+ _type: 'database',
+ label: 'database1',
+ _id: 3,
+ }, ['parent', 'server_group1', 'server1']);
+ });
+
+ context('cannot find the tree node', () => {
+ beforeEach(() => {
+ showQueryTool(queryTool, pgBrowser, alertify, '', [{id: '10'}], 'title');
+ });
+ it('does not create a transaction', () => {
+ expect(queryTool.create_transaction).not.toHaveBeenCalled();
+ });
+
+ it('display alert', () => {
+ expect(alertify.alert).toHaveBeenCalledWith(
+ 'Query Tool Error',
+ 'No object selected.'
+ );
+ });
+ });
+
+ context('current node is not underneath a server', () => {
+ it('does not create a transaction', () => {
+ showQueryTool(queryTool, pgBrowser, alertify, '', [{id: 'parent'}], 'title');
+ expect(queryTool.create_transaction).not.toHaveBeenCalled();
+ });
+
+ it('no alert is displayed', () => {
+ expect(alertify.alert).not.toHaveBeenCalled();
+ });
+ });
+
+ context('current node is underneath a server', () => {
+ context('current node is not underneath a database', () => {
+ it('creates a transaction', () => {
+ showQueryTool(queryTool, pgBrowser, alertify, 'http://someurl', [{id: 'server1'}], 'title');
+ expect(queryTool.create_transaction).toHaveBeenCalledWith(
+ '/initialize/query_tool/1/2',
+ null,
+ 'true',
+ 'pg',
+ 'http://someurl',
+ 'title',
+ '',
+ false
+ );
+ });
+ });
+
+ context('current node is underneath a database', () => {
+ it('creates a transaction', () => {
+ showQueryTool(queryTool, pgBrowser, alertify, 'http://someurl', [{id: 'database1'}], 'title');
+ expect(queryTool.create_transaction).toHaveBeenCalledWith(
+ '/initialize/query_tool/1/2/3',
+ null,
+ 'true',
+ 'pg',
+ 'http://someurl',
+ 'title',
+ '',
+ false
+ );
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/fake_endpoints.js b/web/regression/javascript/fake_endpoints.js
index 63ab05dc..e4ae7020 100644
--- a/web/regression/javascript/fake_endpoints.js
+++ b/web/regression/javascript/fake_endpoints.js
@@ -11,6 +11,12 @@ define(function () {
return {
'static': '/base/pgadmin/static/<path:filename>',
'sqleditor.poll': '/sqleditor/query_tool/poll/<path:trans_id>',
- 'sqleditor.query_tool_start': '/sqleditor/query_tool/start/<path:trans_id>'
+ 'sqleditor.query_tool_start': '/sqleditor/query_tool/start/<path:trans_id>',
+ 'backup.create_server_job': '/backup/job/<int:sid>',
+ 'backup.create_object_job': '/backup/job/<int:sid>/object',
+ 'datagrid.initialize_datagrid': '/initialize/datagrid/<int:cmd_type>/<obj_type>/<int:sgid>/<int:sid>/<int:did>/<int:obj_id>',
+ 'datagrid.initialize_query_tool': '/initialize/query_tool/<int:sgid>/<int:sid>',
+ 'datagrid.initialize_query_tool_with_did': '/initialize/query_tool/<int:sgid>/<int:sid>/<int:did>',
+
};
});
diff --git a/web/regression/javascript/fake_model.js b/web/regression/javascript/fake_model.js
new file mode 100644
index 00000000..acfaa532
--- /dev/null
+++ b/web/regression/javascript/fake_model.js
@@ -0,0 +1,21 @@
+export class FakeModel {
+ constructor() {
+ this.values = {};
+ }
+
+ set(key, value) {
+ this.values[key] = value;
+ }
+
+ get(key) {
+ return this.values[key];
+ }
+
+ unset(key) {
+ delete this.values[key];
+ }
+
+ toJSON() {
+ return Object.assign({}, this.values);
+ }
+}
diff --git a/web/regression/javascript/tree/pgadmin_tree_node_spec.js b/web/regression/javascript/tree/pgadmin_tree_node_spec.js
new file mode 100644
index 00000000..1cab65f0
--- /dev/null
+++ b/web/regression/javascript/tree/pgadmin_tree_node_spec.js
@@ -0,0 +1,353 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+import {
+ getTreeNodeHierarchyFromElement,
+ getTreeNodeHierarchyFromIdentifier
+} from "../../../pgadmin/static/js/tree/pgadmin_tree_node";
+import {Tree} from "../../../pgadmin/static/js/tree/tree";
+
+const context = describe;
+
+describe('tree#node#getTreeNodeHierarchy', () => {
+ let browser;
+ let newTree;
+ let translateTreeNodeIdFromACITreeSpy;
+ beforeEach(() => {
+ newTree = new Tree();
+ browser = {
+ Nodes: {
+ 'special one': {hasId: true},
+ 'child special': {hasId: true},
+ 'other type': {hasId: true},
+ 'table': {hasId: true},
+ 'partition': {hasId: true},
+ 'no id': {hasId: false},
+ }
+ };
+ browser.treeMenu = newTree;
+ translateTreeNodeIdFromACITreeSpy = spyOn(newTree, 'translateTreeNodeIdFromACITree');
+ });
+
+ context('getTreeNodeHierarchy is called with aciTreeNode object', () => {
+ describe('When the current node is root element', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one'
+ }, []);
+ translateTreeNodeIdFromACITreeSpy.and.returnValue(['root']);
+ });
+
+ it('returns a object with the element type passed data and priority == 0', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)('root');
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': 0,
+ }
+ });
+ });
+ });
+
+ describe('When the current node is not of a known type', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'do not exist'
+ }, []);
+ translateTreeNodeIdFromACITreeSpy.and.returnValue(['root']);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)('root');
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node type has no id', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'no id'
+ }, []);
+ translateTreeNodeIdFromACITreeSpy.and.returnValue(['root']);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)('root');
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node is at the second level', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one'
+ }, []);
+ newTree.addNewNode('first child', {
+ 'some key': 'some other value',
+ '_type': 'child special'
+ }, ['root']);
+ translateTreeNodeIdFromACITreeSpy.and.returnValue(['root', 'first' +
+ ' child']);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)('first child');
+ expect(result).toEqual({
+ 'child special': {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ 'priority': 0,
+ },
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+
+ context('When tree as "special type"', () => {
+ context('When "special type" have "other type"', () => {
+ context('When "other type" have "special type"', () => {
+ describe('When retrieving lead node', () => {
+ it('does not override previous node type data', () => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one'
+ }, []);
+ newTree.addNewNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'other type'
+ }, ['root']);
+ newTree.addNewNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ }, ['root', 'level 1']);
+
+ translateTreeNodeIdFromACITreeSpy.and.returnValue(['root', 'level 1', 'level 2']);
+
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)('level 2');
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ 'priority': 0,
+ },
+ 'other type': {
+ 'some key': 'some value',
+ '_type': 'other type',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+ });
+ });
+ });
+
+ context('When tree has table', () => {
+ context('when table has partition', () => {
+ it('returns table with partition parameters', () => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one'
+ }, []);
+ newTree.addNewNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'table'
+ }, ['root']);
+ newTree.addNewNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'partition',
+ 'some other key': 'some other value',
+ }, ['root', 'level 1']);
+
+ translateTreeNodeIdFromACITreeSpy.and.returnValue(['root', 'level 1', 'level 2']);
+
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)('level 2');
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ 'table': {
+ 'some key': 'expected value',
+ 'some other key': 'some other value',
+ '_type': 'partition',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+ });
+ });
+
+ context('getTreeNodeHierarchy is called with TreeNode object', () => {
+ let treeNode;
+ describe('When the current node is root element', () => {
+ beforeEach(() => {
+ treeNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one'
+ }, []);
+ });
+
+ it('returns a object with the element type passed data and priority == 0', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': 0,
+ }
+ });
+ });
+ });
+
+ describe('When the current node is not of a known type', () => {
+ beforeEach(() => {
+ treeNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'do not exist'
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node type has no id', () => {
+ beforeEach(() => {
+ treeNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'no id'
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node is at the second level', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one'
+ }, []);
+ treeNode = newTree.addNewNode('first child', {
+ 'some key': 'some other value',
+ '_type': 'child special'
+ }, ['root']);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'child special': {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ 'priority': 0,
+ },
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+
+ context('When tree as "special type"', () => {
+ context('When "special type" have "other type"', () => {
+ context('When "other type" have "special type"', () => {
+ describe('When retrieving lead node', () => {
+ it('does not override previous node type data', () => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one'
+ }, []);
+ newTree.addNewNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'other type'
+ }, ['root']);
+ treeNode = newTree.addNewNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ }, ['root', 'level 1']);
+
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ 'priority': 0,
+ },
+ 'other type': {
+ 'some key': 'some value',
+ '_type': 'other type',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+ });
+ });
+ });
+
+ context('When tree has table', () => {
+ context('when table has partition', () => {
+ it('returns table with partition parameters', () => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one'
+ }, []);
+ newTree.addNewNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'table'
+ }, ['root']);
+ treeNode = newTree.addNewNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'partition',
+ 'some other key': 'some other value',
+ }, ['root', 'level 1']);
+
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ 'table': {
+ 'some key': 'expected value',
+ 'some other key': 'some other value',
+ '_type': 'partition',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+ });
+ });
+});
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-06 08:48 Murtuza Zabuawala <[email protected]>
parent: Joao De Almeida Pereira <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Murtuza Zabuawala @ 2018-04-06 08:48 UTC (permalink / raw)
To: Joao De Almeida Pereira <[email protected]>; +Cc: Khushboo Vashi <[email protected]>; pgadmin-hackers
Hi Joao,
Patch looks good and working as expected.
I also agree with Dave, Can we please add some comments in each file which
can help us to understand the flow, I'm saying because now the code is
segregated in so many separate files it will be hard to keep track of the
flow from one file to another when debugging.
--
Regards,
Murtuza Zabuawala
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Thu, Apr 5, 2018 at 7:08 PM, Joao De Almeida Pereira <
[email protected]> wrote:
> Hi Khushboo,
> Attached you can find both patches rebased
>
> Thanks
>
>
> On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
> [email protected]> wrote:
>
>> Hi Joao,
>>
>> Can you please rebase the second patch?
>>
>> Thanks,
>> Khushboo
>>
>>
>> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida Pereira <
>> [email protected]> wrote:
>>
>>> Hi Hackers,
>>>
>>> Attached you can find the patch that will start to decouple pgAdmin from
>>> ACITree library.
>>> This patch is intended to be merged after 3.0, because we do not want to
>>> cause any entropy or delay the release, but we want to start the discussion
>>> and show some code.
>>>
>>> This job that we started is a massive tech debt chore that will take
>>> some time to finalize and we would love the help of the community to do it.
>>>
>>> *Summary of the patch:*
>>> 0001 patch:
>>> - Creates a new tree that will allow us to create a separation between
>>> the application and ACI Tree
>>> - Creates a Fake Tree (Test double, for reference on the available test
>>> doubles: https://martinfowler.com/bliki/TestDouble.html) that can be
>>> used to inplace to replace the ACITree and also encapsulate the new tree
>>> behavior on our tests
>>> - Adds tests for all the tree functionalities
>>>
>>> 0002 patch:
>>> - Extracts, refactors, adds tests and remove dependency from ACI Tree
>>> on:
>>> - getTreeNodeHierarchy
>>> - on backup.js: menu_enabled, menu_enabled_server,
>>> start_backup_global_server, backup_objects
>>> - on datagrid.js: show_data_grid, get_panel_title, show_query_tool
>>> - Start using sprintf-js as Underscore.String is deprecating sprintf
>>> function
>>>
>>> This patch represents only 10 calls to ACITree.itemData out of 176 that
>>> are spread around our code
>>>
>>>
>>> *In Depth look on the process behind the patch:*
>>>
>>> We started writing this patch with the idea that we need to decouple
>>> pgAdmin4 from ACITree, because ACITree is no longer supported, the
>>> documentation is non existent and ACITree is no longer being actively
>>> developed.
>>>
>>> Our process:
>>> 1. We "randomly" selected a function that is part of the ACITree. From
>>> this point we decided to replace that function with our own version. The
>>> function that we choose was "itemData".
>>> The function gives us all the "data" that a specific node of the tree
>>> find.
>>> Given in order to replace the tree we would need to have a function that
>>> would give us the same information. We had 2 options:
>>> a) Create a tree with a function called itemData
>>> Pros:
>>> - At first view this was the simpler solution
>>> - Would keep the status quo
>>> Cons:
>>> - Not a OOP approach
>>> - Not very flexible
>>> b) Create a tree that would return a node given an ID and then the
>>> node would be responsible for giving it's data.
>>> Pros:
>>> - OOP Approach
>>> - More flexible and we do not need to bring the tree around, just a node
>>> Cons:
>>> - Break the current status quo
>>>
>>> Given these 2 options we decided to go for a more OOP approach creating
>>> a Tree and a TreeNode classes, that in the future will be renamed to
>>> ACITreeWrapper and TreeNode.
>>>
>>> 2. After we decided on the starting point we searched for occurrences of
>>> the function "itemData" and we found out that there were 303 occurrences of
>>> "itemData" in the code and roughly 176 calls to the function itself (some
>>> of the hits were variable names).
>>>
>>> 3. We selected the first file on the search and found the function that
>>> was responsible for calling the itemData function.
>>>
>>> 4. Extracted the function to a separate file
>>>
>>> 5. Wrap this function with tests
>>>
>>> 6. Refactor the function to ES6, give more declarative names to
>>> variables and break the functions into smaller chunks
>>>
>>> 7. When all the tests were passing we replaced ACITree with our Tree
>>>
>>> 8. We ensured that all tests were passing
>>>
>>> 9. Remove function from the original file and use the new function
>>>
>>> 10. Ensure everything still works
>>>
>>> 11. Find the next function and execute from step 4 until all the
>>> functions are replaced, refactored and tested.
>>>
>>> As you can see by the process this is a pretty huge undertake, because
>>> of the number of calls to the function. This is just the first step on the
>>> direction of completely isolating the ACITree so that we can solve the
>>> problem with a large number of elements on the tree.
>>>
>>> *What is on our radar that we need to address:*
>>> - Finish the complete decoupling of the ACITree
>>> - Performance of the current tree implementation
>>> - Tweak the naming of the Tree class to explicitly tell us this is to
>>> use only with ACITree.
>>>
>>>
>>> Thanks
>>> Joao
>>>
>>>
>>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-06 19:13 Joao De Almeida Pereira <[email protected]>
parent: Murtuza Zabuawala <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Joao De Almeida Pereira @ 2018-04-06 19:13 UTC (permalink / raw)
To: Murtuza Zabuawala <[email protected]>; +Cc: Khushboo Vashi <[email protected]>; pgadmin-hackers
Hello Murtuza/Dave,
Yes now the extracted functions are spread into different files. The intent
would be to make the files as small as possible, and also to group and name
them in a way that would be easy to understand what each file is doing
without the need of opening it.
As a example:
static/js/backup will contain all the backup related functionality
inside of this folder we can see the file:
menu_utils.js At this moment in time we decided to group all the functions
that are related to the menu, but we can split that also if we believe it
is easier to see.
static/js/datagrid folder contains all the datagrid related functionality
Inside of the folder we can see the files:
get_panel_title.js is responsible for retrieving the name of the panel
show_data.js is responsible for showing the datagrid
show_query_tool.js is responsible for showing the query tool
Does this structure make sense?
Can you give an example of a comment that you think is missing and that
could help?
As a personal note, unless the algorithm is very obscure or very
complicated, I believe that if the code needs comments it is a signal that
something needs to change in terms of naming, structure of the part in
question. This being said, I am open to add some comments that might help
people.
Thanks
Joao
On Fri, Apr 6, 2018 at 4:48 AM Murtuza Zabuawala <
[email protected]> wrote:
> Hi Joao,
>
> Patch looks good and working as expected.
>
> I also agree with Dave, Can we please add some comments in each file which
> can help us to understand the flow, I'm saying because now the code is
> segregated in so many separate files it will be hard to keep track of the
> flow from one file to another when debugging.
>
>
> --
> Regards,
> Murtuza Zabuawala
> EnterpriseDB: http://www.enterprisedb.com
> The Enterprise PostgreSQL Company
>
>
> On Thu, Apr 5, 2018 at 7:08 PM, Joao De Almeida Pereira <
> [email protected]> wrote:
>
>> Hi Khushboo,
>> Attached you can find both patches rebased
>>
>> Thanks
>>
>>
>> On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
>> [email protected]> wrote:
>>
>>> Hi Joao,
>>>
>>> Can you please rebase the second patch?
>>>
>>> Thanks,
>>> Khushboo
>>>
>>>
>>> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida Pereira <
>>> [email protected]> wrote:
>>>
>>>> Hi Hackers,
>>>>
>>>> Attached you can find the patch that will start to decouple pgAdmin
>>>> from ACITree library.
>>>> This patch is intended to be merged after 3.0, because we do not want
>>>> to cause any entropy or delay the release, but we want to start the
>>>> discussion and show some code.
>>>>
>>>> This job that we started is a massive tech debt chore that will take
>>>> some time to finalize and we would love the help of the community to do it.
>>>>
>>>> *Summary of the patch:*
>>>> 0001 patch:
>>>> - Creates a new tree that will allow us to create a separation between
>>>> the application and ACI Tree
>>>> - Creates a Fake Tree (Test double, for reference on the available
>>>> test doubles: https://martinfowler.com/bliki/TestDouble.html) that can
>>>> be used to inplace to replace the ACITree and also encapsulate the new tree
>>>> behavior on our tests
>>>> - Adds tests for all the tree functionalities
>>>>
>>>> 0002 patch:
>>>> - Extracts, refactors, adds tests and remove dependency from ACI Tree
>>>> on:
>>>> - getTreeNodeHierarchy
>>>> - on backup.js: menu_enabled, menu_enabled_server,
>>>> start_backup_global_server, backup_objects
>>>> - on datagrid.js: show_data_grid, get_panel_title, show_query_tool
>>>> - Start using sprintf-js as Underscore.String is deprecating sprintf
>>>> function
>>>>
>>>> This patch represents only 10 calls to ACITree.itemData out of 176 that
>>>> are spread around our code
>>>>
>>>>
>>>> *In Depth look on the process behind the patch:*
>>>>
>>>> We started writing this patch with the idea that we need to decouple
>>>> pgAdmin4 from ACITree, because ACITree is no longer supported, the
>>>> documentation is non existent and ACITree is no longer being actively
>>>> developed.
>>>>
>>>> Our process:
>>>> 1. We "randomly" selected a function that is part of the ACITree. From
>>>> this point we decided to replace that function with our own version. The
>>>> function that we choose was "itemData".
>>>> The function gives us all the "data" that a specific node of the tree
>>>> find.
>>>> Given in order to replace the tree we would need to have a function
>>>> that would give us the same information. We had 2 options:
>>>> a) Create a tree with a function called itemData
>>>> Pros:
>>>> - At first view this was the simpler solution
>>>> - Would keep the status quo
>>>> Cons:
>>>> - Not a OOP approach
>>>> - Not very flexible
>>>> b) Create a tree that would return a node given an ID and then the
>>>> node would be responsible for giving it's data.
>>>> Pros:
>>>> - OOP Approach
>>>> - More flexible and we do not need to bring the tree around, just a
>>>> node
>>>> Cons:
>>>> - Break the current status quo
>>>>
>>>> Given these 2 options we decided to go for a more OOP approach creating
>>>> a Tree and a TreeNode classes, that in the future will be renamed to
>>>> ACITreeWrapper and TreeNode.
>>>>
>>>> 2. After we decided on the starting point we searched for occurrences
>>>> of the function "itemData" and we found out that there were 303 occurrences
>>>> of "itemData" in the code and roughly 176 calls to the function itself
>>>> (some of the hits were variable names).
>>>>
>>>> 3. We selected the first file on the search and found the function that
>>>> was responsible for calling the itemData function.
>>>>
>>>> 4. Extracted the function to a separate file
>>>>
>>>> 5. Wrap this function with tests
>>>>
>>>> 6. Refactor the function to ES6, give more declarative names to
>>>> variables and break the functions into smaller chunks
>>>>
>>>> 7. When all the tests were passing we replaced ACITree with our Tree
>>>>
>>>> 8. We ensured that all tests were passing
>>>>
>>>> 9. Remove function from the original file and use the new function
>>>>
>>>> 10. Ensure everything still works
>>>>
>>>> 11. Find the next function and execute from step 4 until all the
>>>> functions are replaced, refactored and tested.
>>>>
>>>> As you can see by the process this is a pretty huge undertake, because
>>>> of the number of calls to the function. This is just the first step on the
>>>> direction of completely isolating the ACITree so that we can solve the
>>>> problem with a large number of elements on the tree.
>>>>
>>>> *What is on our radar that we need to address:*
>>>> - Finish the complete decoupling of the ACITree
>>>> - Performance of the current tree implementation
>>>> - Tweak the naming of the Tree class to explicitly tell us this is to
>>>> use only with ACITree.
>>>>
>>>>
>>>> Thanks
>>>> Joao
>>>>
>>>>
>>>
>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-09 05:59 Khushboo Vashi <[email protected]>
parent: Joao De Almeida Pereira <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Khushboo Vashi @ 2018-04-09 05:59 UTC (permalink / raw)
To: Joao De Almeida Pereira <[email protected]>; +Cc: Murtuza Zabuawala <[email protected]>; pgadmin-hackers
Hi Joao,
I have reviewed your patch and have some suggestions.
On Sat, Apr 7, 2018 at 12:43 AM, Joao De Almeida Pereira <
[email protected]> wrote:
> Hello Murtuza/Dave,
> Yes now the extracted functions are spread into different files. The
> intent would be to make the files as small as possible, and also to group
> and name them in a way that would be easy to understand what each file is
> doing without the need of opening it.
> As a example:
> static/js/backup will contain all the backup related functionality inside
> of this folder we can see the file:
>
menu_utils.js At this moment in time we decided to group all the functions
> that are related to the menu, but we can split that also if we believe it
> is easier to see.
>
It's really very good to see the separated code for backup module. As we
have done for backup, we would like do it for other PG utilities like
restore, maintenance etc.
Considering this, we should separate the code in a way that some of the
common functionalities can be used for other modules like menu (as you
have mentioned above), dialogue factory etc.
Also, I think these functionalities should be in their respective static
folder instead of pgadmin/static.
> static/js/datagrid folder contains all the datagrid related functionality
>
Same as backup module, this should be in it's respective static/js folder.
> Inside of the folder we can see the files:
> get_panel_title.js is responsible for retrieving the name of the panel
> show_data.js is responsible for showing the datagrid
> show_query_tool.js is responsible for showing the query tool
>
> Does this structure make sense?
> Can you give an example of a comment that you think is missing and that
> could help?
>
> As a personal note, unless the algorithm is very obscure or very
> complicated, I believe that if the code needs comments it is a signal that
> something needs to change in terms of naming, structure of the part in
> question. This being said, I am open to add some comments that might help
> people.
>
You are right, with the help of naming convention and structure of the
code, any one can get the idea about the code. But it is very useful to
understand the code
very easily with the proper comments especially when there are multiple
developers working on a single project.
I found some of the places where it would be great to have comments.
- treeMenu: new tree.Tree() in a browser.js
- tree.js (especially Tree class)
Thanks
> Joao
>
>
> Thanks,
Khushboo
>
> On Fri, Apr 6, 2018 at 4:48 AM Murtuza Zabuawala <
> [email protected]> wrote:
>
>> Hi Joao,
>>
>> Patch looks good and working as expected.
>>
>> I also agree with Dave, Can we please add some comments in each file
>> which can help us to understand the flow, I'm saying because now the code
>> is segregated in so many separate files it will be hard to keep track of
>> the flow from one file to another when debugging.
>>
>>
>> --
>> Regards,
>> Murtuza Zabuawala
>> EnterpriseDB: http://www.enterprisedb.com
>> The Enterprise PostgreSQL Company
>>
>>
>> On Thu, Apr 5, 2018 at 7:08 PM, Joao De Almeida Pereira <
>> [email protected]> wrote:
>>
>>> Hi Khushboo,
>>> Attached you can find both patches rebased
>>>
>>> Thanks
>>>
>>>
>>> On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
>>> [email protected]> wrote:
>>>
>>>> Hi Joao,
>>>>
>>>> Can you please rebase the second patch?
>>>>
>>>> Thanks,
>>>> Khushboo
>>>>
>>>>
>>>> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida Pereira <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi Hackers,
>>>>>
>>>>> Attached you can find the patch that will start to decouple pgAdmin
>>>>> from ACITree library.
>>>>> This patch is intended to be merged after 3.0, because we do not want
>>>>> to cause any entropy or delay the release, but we want to start the
>>>>> discussion and show some code.
>>>>>
>>>>> This job that we started is a massive tech debt chore that will take
>>>>> some time to finalize and we would love the help of the community to do it.
>>>>>
>>>>> *Summary of the patch:*
>>>>> 0001 patch:
>>>>> - Creates a new tree that will allow us to create a separation
>>>>> between the application and ACI Tree
>>>>> - Creates a Fake Tree (Test double, for reference on the available
>>>>> test doubles: https://martinfowler.com/bliki/TestDouble.html) that
>>>>> can be used to inplace to replace the ACITree and also encapsulate the new
>>>>> tree behavior on our tests
>>>>> - Adds tests for all the tree functionalities
>>>>>
>>>>> 0002 patch:
>>>>> - Extracts, refactors, adds tests and remove dependency from ACI Tree
>>>>> on:
>>>>> - getTreeNodeHierarchy
>>>>> - on backup.js: menu_enabled, menu_enabled_server,
>>>>> start_backup_global_server, backup_objects
>>>>> - on datagrid.js: show_data_grid, get_panel_title, show_query_tool
>>>>> - Start using sprintf-js as Underscore.String is deprecating sprintf
>>>>> function
>>>>>
>>>>> This patch represents only 10 calls to ACITree.itemData out of 176
>>>>> that are spread around our code
>>>>>
>>>>>
>>>>> *In Depth look on the process behind the patch:*
>>>>>
>>>>> We started writing this patch with the idea that we need to decouple
>>>>> pgAdmin4 from ACITree, because ACITree is no longer supported, the
>>>>> documentation is non existent and ACITree is no longer being actively
>>>>> developed.
>>>>>
>>>>> Our process:
>>>>> 1. We "randomly" selected a function that is part of the ACITree. From
>>>>> this point we decided to replace that function with our own version. The
>>>>> function that we choose was "itemData".
>>>>> The function gives us all the "data" that a specific node of the tree
>>>>> find.
>>>>> Given in order to replace the tree we would need to have a function
>>>>> that would give us the same information. We had 2 options:
>>>>> a) Create a tree with a function called itemData
>>>>> Pros:
>>>>> - At first view this was the simpler solution
>>>>> - Would keep the status quo
>>>>> Cons:
>>>>> - Not a OOP approach
>>>>> - Not very flexible
>>>>> b) Create a tree that would return a node given an ID and then the
>>>>> node would be responsible for giving it's data.
>>>>> Pros:
>>>>> - OOP Approach
>>>>> - More flexible and we do not need to bring the tree around, just a
>>>>> node
>>>>> Cons:
>>>>> - Break the current status quo
>>>>>
>>>>> Given these 2 options we decided to go for a more OOP approach
>>>>> creating a Tree and a TreeNode classes, that in the future will be renamed
>>>>> to ACITreeWrapper and TreeNode.
>>>>>
>>>>> 2. After we decided on the starting point we searched for occurrences
>>>>> of the function "itemData" and we found out that there were 303 occurrences
>>>>> of "itemData" in the code and roughly 176 calls to the function itself
>>>>> (some of the hits were variable names).
>>>>>
>>>>> 3. We selected the first file on the search and found the function
>>>>> that was responsible for calling the itemData function.
>>>>>
>>>>> 4. Extracted the function to a separate file
>>>>>
>>>>> 5. Wrap this function with tests
>>>>>
>>>>> 6. Refactor the function to ES6, give more declarative names to
>>>>> variables and break the functions into smaller chunks
>>>>>
>>>>> 7. When all the tests were passing we replaced ACITree with our Tree
>>>>>
>>>>> 8. We ensured that all tests were passing
>>>>>
>>>>> 9. Remove function from the original file and use the new function
>>>>>
>>>>> 10. Ensure everything still works
>>>>>
>>>>> 11. Find the next function and execute from step 4 until all the
>>>>> functions are replaced, refactored and tested.
>>>>>
>>>>> As you can see by the process this is a pretty huge undertake, because
>>>>> of the number of calls to the function. This is just the first step on the
>>>>> direction of completely isolating the ACITree so that we can solve the
>>>>> problem with a large number of elements on the tree.
>>>>>
>>>>> *What is on our radar that we need to address:*
>>>>> - Finish the complete decoupling of the ACITree
>>>>> - Performance of the current tree implementation
>>>>> - Tweak the naming of the Tree class to explicitly tell us this is to
>>>>> use only with ACITree.
>>>>>
>>>>>
>>>>> Thanks
>>>>> Joao
>>>>>
>>>>>
>>>>
>>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-10 16:00 Joao De Almeida Pereira <[email protected]>
parent: Khushboo Vashi <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Joao De Almeida Pereira @ 2018-04-10 16:00 UTC (permalink / raw)
To: Khushboo Vashi <[email protected]>; +Cc: Murtuza Zabuawala <[email protected]>; pgadmin-hackers
Hello Khushboo
On Mon, Apr 9, 2018 at 1:59 AM Khushboo Vashi <
[email protected]> wrote:
> Hi Joao,
>
> I have reviewed your patch and have some suggestions.
>
> On Sat, Apr 7, 2018 at 12:43 AM, Joao De Almeida Pereira <
> [email protected]> wrote:
>
>> Hello Murtuza/Dave,
>> Yes now the extracted functions are spread into different files. The
>> intent would be to make the files as small as possible, and also to group
>> and name them in a way that would be easy to understand what each file is
>> doing without the need of opening it.
>> As a example:
>> static/js/backup will contain all the backup related functionality
>> inside of this folder we can see the file:
>>
> menu_utils.js At this moment in time we decided to group all the
>> functions that are related to the menu, but we can split that also if we
>> believe it is easier to see.
>>
> It's really very good to see the separated code for backup module. As we
> have done for backup, we would like do it for other PG utilities like
> restore, maintenance etc.
> Considering this, we should separate the code in a way that some of the
> common functionalities can be used for other modules like menu (as you
> have mentioned above), dialogue factory etc.
> Also, I think these functionalities should be in their respective static
> folder instead of pgadmin/static.
>
About the location of the files. The move of the files to pgadmin/static/js
was made on purpose in order to clearly separate Javascript from python
code.
The rational behind it was
- Create a clear separation between the backend and frontend
- Having Javascript code concentrated in a single place, hopefully, will
encourage to developers to look for a functionality, that is already
implemented in another modules, because they are right there. (When we
started this journey we realized that the 'nodes' have a big groups of code
that could be shared, but because the Javascript is spread everywhere it is
much harder to look for it)
There are some drawbacks of this separation:
- When creating a new module we will need to put the javascript in a
separate location from the backend code
>
>
>> static/js/datagrid folder contains all the datagrid related functionality
>>
> Same as backup module, this should be in it's respective static/js folder.
>
>> Inside of the folder we can see the files:
>> get_panel_title.js is responsible for retrieving the name of the panel
>> show_data.js is responsible for showing the datagrid
>> show_query_tool.js is responsible for showing the query tool
>>
>> Does this structure make sense?
>> Can you give an example of a comment that you think is missing and that
>> could help?
>>
>> As a personal note, unless the algorithm is very obscure or very
>> complicated, I believe that if the code needs comments it is a signal that
>> something needs to change in terms of naming, structure of the part in
>> question. This being said, I am open to add some comments that might help
>> people.
>>
> You are right, with the help of naming convention and structure of the
> code, any one can get the idea about the code. But it is very useful to
> understand the code
> very easily with the proper comments especially when there are multiple
> developers working on a single project.
>
> I found some of the places where it would be great to have comments.
>
> - treeMenu: new tree.Tree() in a browser.js
> - tree.js (especially Tree class)
>
About the comment point I need a more clear understanding on what kind of
comments you are looking for. Because when you read the function names you
understand the intent, what they are doing. The parameters also explain
what you need to pass into them.
If what you are looking for in these comments is the reasoning being the
change itself, then that should be present in the commit message. Specially
because this is going to be a very big patch with a very big number of
changes.
>
> Thanks
>> Joao
>>
>>
>> Thanks,
> Khushboo
>
>>
>> On Fri, Apr 6, 2018 at 4:48 AM Murtuza Zabuawala <
>> [email protected]> wrote:
>>
>>> Hi Joao,
>>>
>>> Patch looks good and working as expected.
>>>
>>> I also agree with Dave, Can we please add some comments in each file
>>> which can help us to understand the flow, I'm saying because now the code
>>> is segregated in so many separate files it will be hard to keep track of
>>> the flow from one file to another when debugging.
>>>
>>>
>>> --
>>> Regards,
>>> Murtuza Zabuawala
>>> EnterpriseDB: http://www.enterprisedb.com
>>> The Enterprise PostgreSQL Company
>>>
>>>
>>> On Thu, Apr 5, 2018 at 7:08 PM, Joao De Almeida Pereira <
>>> [email protected]> wrote:
>>>
>>>> Hi Khushboo,
>>>> Attached you can find both patches rebased
>>>>
>>>> Thanks
>>>>
>>>>
>>>> On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi Joao,
>>>>>
>>>>> Can you please rebase the second patch?
>>>>>
>>>>> Thanks,
>>>>> Khushboo
>>>>>
>>>>>
>>>>> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida Pereira <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hi Hackers,
>>>>>>
>>>>>> Attached you can find the patch that will start to decouple pgAdmin
>>>>>> from ACITree library.
>>>>>> This patch is intended to be merged after 3.0, because we do not want
>>>>>> to cause any entropy or delay the release, but we want to start the
>>>>>> discussion and show some code.
>>>>>>
>>>>>> This job that we started is a massive tech debt chore that will take
>>>>>> some time to finalize and we would love the help of the community to do it.
>>>>>>
>>>>>> *Summary of the patch:*
>>>>>> 0001 patch:
>>>>>> - Creates a new tree that will allow us to create a separation
>>>>>> between the application and ACI Tree
>>>>>> - Creates a Fake Tree (Test double, for reference on the available
>>>>>> test doubles: https://martinfowler.com/bliki/TestDouble.html) that
>>>>>> can be used to inplace to replace the ACITree and also encapsulate the new
>>>>>> tree behavior on our tests
>>>>>> - Adds tests for all the tree functionalities
>>>>>>
>>>>>> 0002 patch:
>>>>>> - Extracts, refactors, adds tests and remove dependency from ACI
>>>>>> Tree on:
>>>>>> - getTreeNodeHierarchy
>>>>>> - on backup.js: menu_enabled, menu_enabled_server,
>>>>>> start_backup_global_server, backup_objects
>>>>>> - on datagrid.js: show_data_grid, get_panel_title, show_query_tool
>>>>>> - Start using sprintf-js as Underscore.String is deprecating sprintf
>>>>>> function
>>>>>>
>>>>>> This patch represents only 10 calls to ACITree.itemData out of 176
>>>>>> that are spread around our code
>>>>>>
>>>>>>
>>>>>> *In Depth look on the process behind the patch:*
>>>>>>
>>>>>> We started writing this patch with the idea that we need to decouple
>>>>>> pgAdmin4 from ACITree, because ACITree is no longer supported, the
>>>>>> documentation is non existent and ACITree is no longer being actively
>>>>>> developed.
>>>>>>
>>>>>> Our process:
>>>>>> 1. We "randomly" selected a function that is part of the ACITree.
>>>>>> From this point we decided to replace that function with our own version.
>>>>>> The function that we choose was "itemData".
>>>>>> The function gives us all the "data" that a specific node of the tree
>>>>>> find.
>>>>>> Given in order to replace the tree we would need to have a function
>>>>>> that would give us the same information. We had 2 options:
>>>>>> a) Create a tree with a function called itemData
>>>>>> Pros:
>>>>>> - At first view this was the simpler solution
>>>>>> - Would keep the status quo
>>>>>> Cons:
>>>>>> - Not a OOP approach
>>>>>> - Not very flexible
>>>>>> b) Create a tree that would return a node given an ID and then the
>>>>>> node would be responsible for giving it's data.
>>>>>> Pros:
>>>>>> - OOP Approach
>>>>>> - More flexible and we do not need to bring the tree around, just a
>>>>>> node
>>>>>> Cons:
>>>>>> - Break the current status quo
>>>>>>
>>>>>> Given these 2 options we decided to go for a more OOP approach
>>>>>> creating a Tree and a TreeNode classes, that in the future will be renamed
>>>>>> to ACITreeWrapper and TreeNode.
>>>>>>
>>>>>> 2. After we decided on the starting point we searched for occurrences
>>>>>> of the function "itemData" and we found out that there were 303 occurrences
>>>>>> of "itemData" in the code and roughly 176 calls to the function itself
>>>>>> (some of the hits were variable names).
>>>>>>
>>>>>> 3. We selected the first file on the search and found the function
>>>>>> that was responsible for calling the itemData function.
>>>>>>
>>>>>> 4. Extracted the function to a separate file
>>>>>>
>>>>>> 5. Wrap this function with tests
>>>>>>
>>>>>> 6. Refactor the function to ES6, give more declarative names to
>>>>>> variables and break the functions into smaller chunks
>>>>>>
>>>>>> 7. When all the tests were passing we replaced ACITree with our Tree
>>>>>>
>>>>>> 8. We ensured that all tests were passing
>>>>>>
>>>>>> 9. Remove function from the original file and use the new function
>>>>>>
>>>>>> 10. Ensure everything still works
>>>>>>
>>>>>> 11. Find the next function and execute from step 4 until all the
>>>>>> functions are replaced, refactored and tested.
>>>>>>
>>>>>> As you can see by the process this is a pretty huge undertake,
>>>>>> because of the number of calls to the function. This is just the first step
>>>>>> on the direction of completely isolating the ACITree so that we can solve
>>>>>> the problem with a large number of elements on the tree.
>>>>>>
>>>>>> *What is on our radar that we need to address:*
>>>>>> - Finish the complete decoupling of the ACITree
>>>>>> - Performance of the current tree implementation
>>>>>> - Tweak the naming of the Tree class to explicitly tell us this is
>>>>>> to use only with ACITree.
>>>>>>
>>>>>>
>>>>>> Thanks
>>>>>> Joao
>>>>>>
>>>>>>
>>>>>
>>>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-18 14:22 Joao De Almeida Pereira <[email protected]>
parent: Joao De Almeida Pereira <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Joao De Almeida Pereira @ 2018-04-18 14:22 UTC (permalink / raw)
To: Khushboo Vashi <[email protected]>; +Cc: Murtuza Zabuawala <[email protected]>; pgadmin-hackers
Hi Hackers,
Any other comment about this patch?
Thanks
Joao
On Tue, Apr 10, 2018 at 12:00 PM Joao De Almeida Pereira <
[email protected]> wrote:
> Hello Khushboo
>
> On Mon, Apr 9, 2018 at 1:59 AM Khushboo Vashi <
> [email protected]> wrote:
>
>> Hi Joao,
>>
>> I have reviewed your patch and have some suggestions.
>>
>> On Sat, Apr 7, 2018 at 12:43 AM, Joao De Almeida Pereira <
>> [email protected]> wrote:
>>
>>> Hello Murtuza/Dave,
>>> Yes now the extracted functions are spread into different files. The
>>> intent would be to make the files as small as possible, and also to group
>>> and name them in a way that would be easy to understand what each file is
>>> doing without the need of opening it.
>>> As a example:
>>> static/js/backup will contain all the backup related functionality
>>> inside of this folder we can see the file:
>>>
>> menu_utils.js At this moment in time we decided to group all the
>>> functions that are related to the menu, but we can split that also if we
>>> believe it is easier to see.
>>>
>> It's really very good to see the separated code for backup module. As we
>> have done for backup, we would like do it for other PG utilities like
>> restore, maintenance etc.
>> Considering this, we should separate the code in a way that some of the
>> common functionalities can be used for other modules like menu (as you
>> have mentioned above), dialogue factory etc.
>> Also, I think these functionalities should be in their respective static
>> folder instead of pgadmin/static.
>>
>
> About the location of the files. The move of the files to
> pgadmin/static/js was made on purpose in order to clearly separate
> Javascript from python code.
> The rational behind it was
> - Create a clear separation between the backend and frontend
> - Having Javascript code concentrated in a single place, hopefully, will
> encourage to developers to look for a functionality, that is already
> implemented in another modules, because they are right there. (When we
> started this journey we realized that the 'nodes' have a big groups of code
> that could be shared, but because the Javascript is spread everywhere it is
> much harder to look for it)
>
>
> There are some drawbacks of this separation:
> - When creating a new module we will need to put the javascript in a
> separate location from the backend code
>
>
>>
>>
>>> static/js/datagrid folder contains all the datagrid related
>>> functionality
>>>
>> Same as backup module, this should be in it's respective static/js
>> folder.
>>
>>> Inside of the folder we can see the files:
>>> get_panel_title.js is responsible for retrieving the name of the panel
>>> show_data.js is responsible for showing the datagrid
>>> show_query_tool.js is responsible for showing the query tool
>>>
>>> Does this structure make sense?
>>> Can you give an example of a comment that you think is missing and that
>>> could help?
>>>
>>> As a personal note, unless the algorithm is very obscure or very
>>> complicated, I believe that if the code needs comments it is a signal that
>>> something needs to change in terms of naming, structure of the part in
>>> question. This being said, I am open to add some comments that might help
>>> people.
>>>
>> You are right, with the help of naming convention and structure of the
>> code, any one can get the idea about the code. But it is very useful to
>> understand the code
>> very easily with the proper comments especially when there are multiple
>> developers working on a single project.
>>
>> I found some of the places where it would be great to have comments.
>>
>> - treeMenu: new tree.Tree() in a browser.js
>> - tree.js (especially Tree class)
>>
> About the comment point I need a more clear understanding on what kind of
> comments you are looking for. Because when you read the function names you
> understand the intent, what they are doing. The parameters also explain
> what you need to pass into them.
>
> If what you are looking for in these comments is the reasoning being the
> change itself, then that should be present in the commit message. Specially
> because this is going to be a very big patch with a very big number of
> changes.
>
>>
>> Thanks
>>> Joao
>>>
>>>
>>> Thanks,
>> Khushboo
>>
>>>
>>> On Fri, Apr 6, 2018 at 4:48 AM Murtuza Zabuawala <
>>> [email protected]> wrote:
>>>
>>>> Hi Joao,
>>>>
>>>> Patch looks good and working as expected.
>>>>
>>>> I also agree with Dave, Can we please add some comments in each file
>>>> which can help us to understand the flow, I'm saying because now the code
>>>> is segregated in so many separate files it will be hard to keep track of
>>>> the flow from one file to another when debugging.
>>>>
>>>>
>>>> --
>>>> Regards,
>>>> Murtuza Zabuawala
>>>> EnterpriseDB: http://www.enterprisedb.com
>>>> The Enterprise PostgreSQL Company
>>>>
>>>>
>>>> On Thu, Apr 5, 2018 at 7:08 PM, Joao De Almeida Pereira <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi Khushboo,
>>>>> Attached you can find both patches rebased
>>>>>
>>>>> Thanks
>>>>>
>>>>>
>>>>> On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hi Joao,
>>>>>>
>>>>>> Can you please rebase the second patch?
>>>>>>
>>>>>> Thanks,
>>>>>> Khushboo
>>>>>>
>>>>>>
>>>>>> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida Pereira <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hi Hackers,
>>>>>>>
>>>>>>> Attached you can find the patch that will start to decouple pgAdmin
>>>>>>> from ACITree library.
>>>>>>> This patch is intended to be merged after 3.0, because we do not
>>>>>>> want to cause any entropy or delay the release, but we want to start the
>>>>>>> discussion and show some code.
>>>>>>>
>>>>>>> This job that we started is a massive tech debt chore that will take
>>>>>>> some time to finalize and we would love the help of the community to do it.
>>>>>>>
>>>>>>> *Summary of the patch:*
>>>>>>> 0001 patch:
>>>>>>> - Creates a new tree that will allow us to create a separation
>>>>>>> between the application and ACI Tree
>>>>>>> - Creates a Fake Tree (Test double, for reference on the available
>>>>>>> test doubles: https://martinfowler.com/bliki/TestDouble.html) that
>>>>>>> can be used to inplace to replace the ACITree and also encapsulate the new
>>>>>>> tree behavior on our tests
>>>>>>> - Adds tests for all the tree functionalities
>>>>>>>
>>>>>>> 0002 patch:
>>>>>>> - Extracts, refactors, adds tests and remove dependency from ACI
>>>>>>> Tree on:
>>>>>>> - getTreeNodeHierarchy
>>>>>>> - on backup.js: menu_enabled, menu_enabled_server,
>>>>>>> start_backup_global_server, backup_objects
>>>>>>> - on datagrid.js: show_data_grid, get_panel_title, show_query_tool
>>>>>>> - Start using sprintf-js as Underscore.String is deprecating sprintf
>>>>>>> function
>>>>>>>
>>>>>>> This patch represents only 10 calls to ACITree.itemData out of 176
>>>>>>> that are spread around our code
>>>>>>>
>>>>>>>
>>>>>>> *In Depth look on the process behind the patch:*
>>>>>>>
>>>>>>> We started writing this patch with the idea that we need to decouple
>>>>>>> pgAdmin4 from ACITree, because ACITree is no longer supported, the
>>>>>>> documentation is non existent and ACITree is no longer being actively
>>>>>>> developed.
>>>>>>>
>>>>>>> Our process:
>>>>>>> 1. We "randomly" selected a function that is part of the ACITree.
>>>>>>> From this point we decided to replace that function with our own version.
>>>>>>> The function that we choose was "itemData".
>>>>>>> The function gives us all the "data" that a specific node of the
>>>>>>> tree find.
>>>>>>> Given in order to replace the tree we would need to have a function
>>>>>>> that would give us the same information. We had 2 options:
>>>>>>> a) Create a tree with a function called itemData
>>>>>>> Pros:
>>>>>>> - At first view this was the simpler solution
>>>>>>> - Would keep the status quo
>>>>>>> Cons:
>>>>>>> - Not a OOP approach
>>>>>>> - Not very flexible
>>>>>>> b) Create a tree that would return a node given an ID and then the
>>>>>>> node would be responsible for giving it's data.
>>>>>>> Pros:
>>>>>>> - OOP Approach
>>>>>>> - More flexible and we do not need to bring the tree around, just a
>>>>>>> node
>>>>>>> Cons:
>>>>>>> - Break the current status quo
>>>>>>>
>>>>>>> Given these 2 options we decided to go for a more OOP approach
>>>>>>> creating a Tree and a TreeNode classes, that in the future will be renamed
>>>>>>> to ACITreeWrapper and TreeNode.
>>>>>>>
>>>>>>> 2. After we decided on the starting point we searched for
>>>>>>> occurrences of the function "itemData" and we found out that there were 303
>>>>>>> occurrences of "itemData" in the code and roughly 176 calls to the function
>>>>>>> itself (some of the hits were variable names).
>>>>>>>
>>>>>>> 3. We selected the first file on the search and found the function
>>>>>>> that was responsible for calling the itemData function.
>>>>>>>
>>>>>>> 4. Extracted the function to a separate file
>>>>>>>
>>>>>>> 5. Wrap this function with tests
>>>>>>>
>>>>>>> 6. Refactor the function to ES6, give more declarative names to
>>>>>>> variables and break the functions into smaller chunks
>>>>>>>
>>>>>>> 7. When all the tests were passing we replaced ACITree with our Tree
>>>>>>>
>>>>>>> 8. We ensured that all tests were passing
>>>>>>>
>>>>>>> 9. Remove function from the original file and use the new function
>>>>>>>
>>>>>>> 10. Ensure everything still works
>>>>>>>
>>>>>>> 11. Find the next function and execute from step 4 until all the
>>>>>>> functions are replaced, refactored and tested.
>>>>>>>
>>>>>>> As you can see by the process this is a pretty huge undertake,
>>>>>>> because of the number of calls to the function. This is just the first step
>>>>>>> on the direction of completely isolating the ACITree so that we can solve
>>>>>>> the problem with a large number of elements on the tree.
>>>>>>>
>>>>>>> *What is on our radar that we need to address:*
>>>>>>> - Finish the complete decoupling of the ACITree
>>>>>>> - Performance of the current tree implementation
>>>>>>> - Tweak the naming of the Tree class to explicitly tell us this is
>>>>>>> to use only with ACITree.
>>>>>>>
>>>>>>>
>>>>>>> Thanks
>>>>>>> Joao
>>>>>>>
>>>>>>>
>>>>>>
>>>>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-24 19:13 Joao De Almeida Pereira <[email protected]>
parent: Joao De Almeida Pereira <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Joao De Almeida Pereira @ 2018-04-24 19:13 UTC (permalink / raw)
To: Khushboo Vashi <[email protected]>; +Cc: Murtuza Zabuawala <[email protected]>; pgadmin-hackers
Hi Hackers,
Can someone review and merge this patch?
Thanks
Joao
On Wed, Apr 18, 2018 at 10:23 AM Joao De Almeida Pereira <
[email protected]> wrote:
> Hi Hackers,
> Any other comment about this patch?
>
> Thanks
> Joao
>
> On Tue, Apr 10, 2018 at 12:00 PM Joao De Almeida Pereira <
> [email protected]> wrote:
>
>> Hello Khushboo
>>
>> On Mon, Apr 9, 2018 at 1:59 AM Khushboo Vashi <
>> [email protected]> wrote:
>>
>>> Hi Joao,
>>>
>>> I have reviewed your patch and have some suggestions.
>>>
>>> On Sat, Apr 7, 2018 at 12:43 AM, Joao De Almeida Pereira <
>>> [email protected]> wrote:
>>>
>>>> Hello Murtuza/Dave,
>>>> Yes now the extracted functions are spread into different files. The
>>>> intent would be to make the files as small as possible, and also to group
>>>> and name them in a way that would be easy to understand what each file is
>>>> doing without the need of opening it.
>>>> As a example:
>>>> static/js/backup will contain all the backup related functionality
>>>> inside of this folder we can see the file:
>>>>
>>> menu_utils.js At this moment in time we decided to group all the
>>>> functions that are related to the menu, but we can split that also if we
>>>> believe it is easier to see.
>>>>
>>> It's really very good to see the separated code for backup module. As we
>>> have done for backup, we would like do it for other PG utilities like
>>> restore, maintenance etc.
>>> Considering this, we should separate the code in a way that some of the
>>> common functionalities can be used for other modules like menu (as you
>>> have mentioned above), dialogue factory etc.
>>> Also, I think these functionalities should be in their respective static
>>> folder instead of pgadmin/static.
>>>
>>
>> About the location of the files. The move of the files to
>> pgadmin/static/js was made on purpose in order to clearly separate
>> Javascript from python code.
>> The rational behind it was
>> - Create a clear separation between the backend and frontend
>> - Having Javascript code concentrated in a single place, hopefully, will
>> encourage to developers to look for a functionality, that is already
>> implemented in another modules, because they are right there. (When we
>> started this journey we realized that the 'nodes' have a big groups of code
>> that could be shared, but because the Javascript is spread everywhere it is
>> much harder to look for it)
>>
>>
>> There are some drawbacks of this separation:
>> - When creating a new module we will need to put the javascript in a
>> separate location from the backend code
>>
>>
>>>
>>>
>>>> static/js/datagrid folder contains all the datagrid related
>>>> functionality
>>>>
>>> Same as backup module, this should be in it's respective static/js
>>> folder.
>>>
>>>> Inside of the folder we can see the files:
>>>> get_panel_title.js is responsible for retrieving the name of the panel
>>>> show_data.js is responsible for showing the datagrid
>>>> show_query_tool.js is responsible for showing the query tool
>>>>
>>>> Does this structure make sense?
>>>> Can you give an example of a comment that you think is missing and that
>>>> could help?
>>>>
>>>> As a personal note, unless the algorithm is very obscure or very
>>>> complicated, I believe that if the code needs comments it is a signal that
>>>> something needs to change in terms of naming, structure of the part in
>>>> question. This being said, I am open to add some comments that might help
>>>> people.
>>>>
>>> You are right, with the help of naming convention and structure of the
>>> code, any one can get the idea about the code. But it is very useful to
>>> understand the code
>>> very easily with the proper comments especially when there are multiple
>>> developers working on a single project.
>>>
>>> I found some of the places where it would be great to have comments.
>>>
>>> - treeMenu: new tree.Tree() in a browser.js
>>> - tree.js (especially Tree class)
>>>
>> About the comment point I need a more clear understanding on what kind of
>> comments you are looking for. Because when you read the function names you
>> understand the intent, what they are doing. The parameters also explain
>> what you need to pass into them.
>>
>> If what you are looking for in these comments is the reasoning being the
>> change itself, then that should be present in the commit message. Specially
>> because this is going to be a very big patch with a very big number of
>> changes.
>>
>>>
>>> Thanks
>>>> Joao
>>>>
>>>>
>>>> Thanks,
>>> Khushboo
>>>
>>>>
>>>> On Fri, Apr 6, 2018 at 4:48 AM Murtuza Zabuawala <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi Joao,
>>>>>
>>>>> Patch looks good and working as expected.
>>>>>
>>>>> I also agree with Dave, Can we please add some comments in each file
>>>>> which can help us to understand the flow, I'm saying because now the code
>>>>> is segregated in so many separate files it will be hard to keep track of
>>>>> the flow from one file to another when debugging.
>>>>>
>>>>>
>>>>> --
>>>>> Regards,
>>>>> Murtuza Zabuawala
>>>>> EnterpriseDB: http://www.enterprisedb.com
>>>>> The Enterprise PostgreSQL Company
>>>>>
>>>>>
>>>>> On Thu, Apr 5, 2018 at 7:08 PM, Joao De Almeida Pereira <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hi Khushboo,
>>>>>> Attached you can find both patches rebased
>>>>>>
>>>>>> Thanks
>>>>>>
>>>>>>
>>>>>> On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hi Joao,
>>>>>>>
>>>>>>> Can you please rebase the second patch?
>>>>>>>
>>>>>>> Thanks,
>>>>>>> Khushboo
>>>>>>>
>>>>>>>
>>>>>>> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida Pereira <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hi Hackers,
>>>>>>>>
>>>>>>>> Attached you can find the patch that will start to decouple pgAdmin
>>>>>>>> from ACITree library.
>>>>>>>> This patch is intended to be merged after 3.0, because we do not
>>>>>>>> want to cause any entropy or delay the release, but we want to start the
>>>>>>>> discussion and show some code.
>>>>>>>>
>>>>>>>> This job that we started is a massive tech debt chore that will
>>>>>>>> take some time to finalize and we would love the help of the community to
>>>>>>>> do it.
>>>>>>>>
>>>>>>>> *Summary of the patch:*
>>>>>>>> 0001 patch:
>>>>>>>> - Creates a new tree that will allow us to create a separation
>>>>>>>> between the application and ACI Tree
>>>>>>>> - Creates a Fake Tree (Test double, for reference on the available
>>>>>>>> test doubles: https://martinfowler.com/bliki/TestDouble.html) that
>>>>>>>> can be used to inplace to replace the ACITree and also encapsulate the new
>>>>>>>> tree behavior on our tests
>>>>>>>> - Adds tests for all the tree functionalities
>>>>>>>>
>>>>>>>> 0002 patch:
>>>>>>>> - Extracts, refactors, adds tests and remove dependency from ACI
>>>>>>>> Tree on:
>>>>>>>> - getTreeNodeHierarchy
>>>>>>>> - on backup.js: menu_enabled, menu_enabled_server,
>>>>>>>> start_backup_global_server, backup_objects
>>>>>>>> - on datagrid.js: show_data_grid, get_panel_title, show_query_tool
>>>>>>>> - Start using sprintf-js as Underscore.String is deprecating
>>>>>>>> sprintf function
>>>>>>>>
>>>>>>>> This patch represents only 10 calls to ACITree.itemData out of 176
>>>>>>>> that are spread around our code
>>>>>>>>
>>>>>>>>
>>>>>>>> *In Depth look on the process behind the patch:*
>>>>>>>>
>>>>>>>> We started writing this patch with the idea that we need to
>>>>>>>> decouple pgAdmin4 from ACITree, because ACITree is no longer supported, the
>>>>>>>> documentation is non existent and ACITree is no longer being actively
>>>>>>>> developed.
>>>>>>>>
>>>>>>>> Our process:
>>>>>>>> 1. We "randomly" selected a function that is part of the ACITree.
>>>>>>>> From this point we decided to replace that function with our own version.
>>>>>>>> The function that we choose was "itemData".
>>>>>>>> The function gives us all the "data" that a specific node of the
>>>>>>>> tree find.
>>>>>>>> Given in order to replace the tree we would need to have a function
>>>>>>>> that would give us the same information. We had 2 options:
>>>>>>>> a) Create a tree with a function called itemData
>>>>>>>> Pros:
>>>>>>>> - At first view this was the simpler solution
>>>>>>>> - Would keep the status quo
>>>>>>>> Cons:
>>>>>>>> - Not a OOP approach
>>>>>>>> - Not very flexible
>>>>>>>> b) Create a tree that would return a node given an ID and then
>>>>>>>> the node would be responsible for giving it's data.
>>>>>>>> Pros:
>>>>>>>> - OOP Approach
>>>>>>>> - More flexible and we do not need to bring the tree around, just
>>>>>>>> a node
>>>>>>>> Cons:
>>>>>>>> - Break the current status quo
>>>>>>>>
>>>>>>>> Given these 2 options we decided to go for a more OOP approach
>>>>>>>> creating a Tree and a TreeNode classes, that in the future will be renamed
>>>>>>>> to ACITreeWrapper and TreeNode.
>>>>>>>>
>>>>>>>> 2. After we decided on the starting point we searched for
>>>>>>>> occurrences of the function "itemData" and we found out that there were 303
>>>>>>>> occurrences of "itemData" in the code and roughly 176 calls to the function
>>>>>>>> itself (some of the hits were variable names).
>>>>>>>>
>>>>>>>> 3. We selected the first file on the search and found the function
>>>>>>>> that was responsible for calling the itemData function.
>>>>>>>>
>>>>>>>> 4. Extracted the function to a separate file
>>>>>>>>
>>>>>>>> 5. Wrap this function with tests
>>>>>>>>
>>>>>>>> 6. Refactor the function to ES6, give more declarative names to
>>>>>>>> variables and break the functions into smaller chunks
>>>>>>>>
>>>>>>>> 7. When all the tests were passing we replaced ACITree with our Tree
>>>>>>>>
>>>>>>>> 8. We ensured that all tests were passing
>>>>>>>>
>>>>>>>> 9. Remove function from the original file and use the new function
>>>>>>>>
>>>>>>>> 10. Ensure everything still works
>>>>>>>>
>>>>>>>> 11. Find the next function and execute from step 4 until all the
>>>>>>>> functions are replaced, refactored and tested.
>>>>>>>>
>>>>>>>> As you can see by the process this is a pretty huge undertake,
>>>>>>>> because of the number of calls to the function. This is just the first step
>>>>>>>> on the direction of completely isolating the ACITree so that we can solve
>>>>>>>> the problem with a large number of elements on the tree.
>>>>>>>>
>>>>>>>> *What is on our radar that we need to address:*
>>>>>>>> - Finish the complete decoupling of the ACITree
>>>>>>>> - Performance of the current tree implementation
>>>>>>>> - Tweak the naming of the Tree class to explicitly tell us this is
>>>>>>>> to use only with ACITree.
>>>>>>>>
>>>>>>>>
>>>>>>>> Thanks
>>>>>>>> Joao
>>>>>>>>
>>>>>>>>
>>>>>>>
>>>>>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-25 07:52 Dave Page <[email protected]>
parent: Joao De Almeida Pereira <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Dave Page @ 2018-04-25 07:52 UTC (permalink / raw)
To: Joao De Almeida Pereira <[email protected]>; Ashesh Vashi <[email protected]>; +Cc: Khushboo Vashi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
Ashesh; you had agreed to work on this early this week. Please ensure you
do so today.
Thanks.
On Tue, Apr 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
[email protected]> wrote:
> Hi Hackers,
>
> Can someone review and merge this patch?
>
> Thanks
> Joao
>
> On Wed, Apr 18, 2018 at 10:23 AM Joao De Almeida Pereira <
> [email protected]> wrote:
>
>> Hi Hackers,
>> Any other comment about this patch?
>>
>> Thanks
>> Joao
>>
>> On Tue, Apr 10, 2018 at 12:00 PM Joao De Almeida Pereira <
>> [email protected]> wrote:
>>
>>> Hello Khushboo
>>>
>>> On Mon, Apr 9, 2018 at 1:59 AM Khushboo Vashi <
>>> [email protected]> wrote:
>>>
>>>> Hi Joao,
>>>>
>>>> I have reviewed your patch and have some suggestions.
>>>>
>>>> On Sat, Apr 7, 2018 at 12:43 AM, Joao De Almeida Pereira <
>>>> [email protected]> wrote:
>>>>
>>>>> Hello Murtuza/Dave,
>>>>> Yes now the extracted functions are spread into different files. The
>>>>> intent would be to make the files as small as possible, and also to group
>>>>> and name them in a way that would be easy to understand what each file is
>>>>> doing without the need of opening it.
>>>>> As a example:
>>>>> static/js/backup will contain all the backup related functionality
>>>>> inside of this folder we can see the file:
>>>>>
>>>> menu_utils.js At this moment in time we decided to group all the
>>>>> functions that are related to the menu, but we can split that also if we
>>>>> believe it is easier to see.
>>>>>
>>>> It's really very good to see the separated code for backup module. As
>>>> we have done for backup, we would like do it for other PG utilities like
>>>> restore, maintenance etc.
>>>> Considering this, we should separate the code in a way that some of the
>>>> common functionalities can be used for other modules like menu (as you
>>>> have mentioned above), dialogue factory etc.
>>>> Also, I think these functionalities should be in their respective
>>>> static folder instead of pgadmin/static.
>>>>
>>>
>>> About the location of the files. The move of the files to
>>> pgadmin/static/js was made on purpose in order to clearly separate
>>> Javascript from python code.
>>> The rational behind it was
>>> - Create a clear separation between the backend and frontend
>>> - Having Javascript code concentrated in a single place, hopefully, will
>>> encourage to developers to look for a functionality, that is already
>>> implemented in another modules, because they are right there. (When we
>>> started this journey we realized that the 'nodes' have a big groups of code
>>> that could be shared, but because the Javascript is spread everywhere it is
>>> much harder to look for it)
>>>
>>>
>>> There are some drawbacks of this separation:
>>> - When creating a new module we will need to put the javascript in a
>>> separate location from the backend code
>>>
>>>
>>>>
>>>>
>>>>> static/js/datagrid folder contains all the datagrid related
>>>>> functionality
>>>>>
>>>> Same as backup module, this should be in it's respective static/js
>>>> folder.
>>>>
>>>>> Inside of the folder we can see the files:
>>>>> get_panel_title.js is responsible for retrieving the name of the panel
>>>>> show_data.js is responsible for showing the datagrid
>>>>> show_query_tool.js is responsible for showing the query tool
>>>>>
>>>>> Does this structure make sense?
>>>>> Can you give an example of a comment that you think is missing and
>>>>> that could help?
>>>>>
>>>>> As a personal note, unless the algorithm is very obscure or very
>>>>> complicated, I believe that if the code needs comments it is a signal that
>>>>> something needs to change in terms of naming, structure of the part in
>>>>> question. This being said, I am open to add some comments that might help
>>>>> people.
>>>>>
>>>> You are right, with the help of naming convention and structure of the
>>>> code, any one can get the idea about the code. But it is very useful to
>>>> understand the code
>>>> very easily with the proper comments especially when there are multiple
>>>> developers working on a single project.
>>>>
>>>> I found some of the places where it would be great to have comments.
>>>>
>>>> - treeMenu: new tree.Tree() in a browser.js
>>>> - tree.js (especially Tree class)
>>>>
>>> About the comment point I need a more clear understanding on what kind
>>> of comments you are looking for. Because when you read the function names
>>> you understand the intent, what they are doing. The parameters also explain
>>> what you need to pass into them.
>>>
>>> If what you are looking for in these comments is the reasoning being the
>>> change itself, then that should be present in the commit message. Specially
>>> because this is going to be a very big patch with a very big number of
>>> changes.
>>>
>>>>
>>>> Thanks
>>>>> Joao
>>>>>
>>>>>
>>>>> Thanks,
>>>> Khushboo
>>>>
>>>>>
>>>>> On Fri, Apr 6, 2018 at 4:48 AM Murtuza Zabuawala <murtuza.zabuawala@
>>>>> enterprisedb.com> wrote:
>>>>>
>>>>>> Hi Joao,
>>>>>>
>>>>>> Patch looks good and working as expected.
>>>>>>
>>>>>> I also agree with Dave, Can we please add some comments in each file
>>>>>> which can help us to understand the flow, I'm saying because now the code
>>>>>> is segregated in so many separate files it will be hard to keep track of
>>>>>> the flow from one file to another when debugging.
>>>>>>
>>>>>>
>>>>>> --
>>>>>> Regards,
>>>>>> Murtuza Zabuawala
>>>>>> EnterpriseDB: http://www.enterprisedb.com
>>>>>> The Enterprise PostgreSQL Company
>>>>>>
>>>>>>
>>>>>> On Thu, Apr 5, 2018 at 7:08 PM, Joao De Almeida Pereira <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hi Khushboo,
>>>>>>> Attached you can find both patches rebased
>>>>>>>
>>>>>>> Thanks
>>>>>>>
>>>>>>>
>>>>>>> On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hi Joao,
>>>>>>>>
>>>>>>>> Can you please rebase the second patch?
>>>>>>>>
>>>>>>>> Thanks,
>>>>>>>> Khushboo
>>>>>>>>
>>>>>>>>
>>>>>>>> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida Pereira <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Hi Hackers,
>>>>>>>>>
>>>>>>>>> Attached you can find the patch that will start to decouple
>>>>>>>>> pgAdmin from ACITree library.
>>>>>>>>> This patch is intended to be merged after 3.0, because we do not
>>>>>>>>> want to cause any entropy or delay the release, but we want to start the
>>>>>>>>> discussion and show some code.
>>>>>>>>>
>>>>>>>>> This job that we started is a massive tech debt chore that will
>>>>>>>>> take some time to finalize and we would love the help of the community to
>>>>>>>>> do it.
>>>>>>>>>
>>>>>>>>> *Summary of the patch:*
>>>>>>>>> 0001 patch:
>>>>>>>>> - Creates a new tree that will allow us to create a separation
>>>>>>>>> between the application and ACI Tree
>>>>>>>>> - Creates a Fake Tree (Test double, for reference on the
>>>>>>>>> available test doubles: https://martinfowler.
>>>>>>>>> com/bliki/TestDouble.html) that can be used to inplace to replace
>>>>>>>>> the ACITree and also encapsulate the new tree behavior on our tests
>>>>>>>>> - Adds tests for all the tree functionalities
>>>>>>>>>
>>>>>>>>> 0002 patch:
>>>>>>>>> - Extracts, refactors, adds tests and remove dependency from ACI
>>>>>>>>> Tree on:
>>>>>>>>> - getTreeNodeHierarchy
>>>>>>>>> - on backup.js: menu_enabled, menu_enabled_server,
>>>>>>>>> start_backup_global_server, backup_objects
>>>>>>>>> - on datagrid.js: show_data_grid, get_panel_title, show_query_tool
>>>>>>>>> - Start using sprintf-js as Underscore.String is deprecating
>>>>>>>>> sprintf function
>>>>>>>>>
>>>>>>>>> This patch represents only 10 calls to ACITree.itemData out of 176
>>>>>>>>> that are spread around our code
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> *In Depth look on the process behind the patch:*
>>>>>>>>>
>>>>>>>>> We started writing this patch with the idea that we need to
>>>>>>>>> decouple pgAdmin4 from ACITree, because ACITree is no longer supported, the
>>>>>>>>> documentation is non existent and ACITree is no longer being actively
>>>>>>>>> developed.
>>>>>>>>>
>>>>>>>>> Our process:
>>>>>>>>> 1. We "randomly" selected a function that is part of the ACITree.
>>>>>>>>> From this point we decided to replace that function with our own version.
>>>>>>>>> The function that we choose was "itemData".
>>>>>>>>> The function gives us all the "data" that a specific node of the
>>>>>>>>> tree find.
>>>>>>>>> Given in order to replace the tree we would need to have a
>>>>>>>>> function that would give us the same information. We had 2 options:
>>>>>>>>> a) Create a tree with a function called itemData
>>>>>>>>> Pros:
>>>>>>>>> - At first view this was the simpler solution
>>>>>>>>> - Would keep the status quo
>>>>>>>>> Cons:
>>>>>>>>> - Not a OOP approach
>>>>>>>>> - Not very flexible
>>>>>>>>> b) Create a tree that would return a node given an ID and then
>>>>>>>>> the node would be responsible for giving it's data.
>>>>>>>>> Pros:
>>>>>>>>> - OOP Approach
>>>>>>>>> - More flexible and we do not need to bring the tree around, just
>>>>>>>>> a node
>>>>>>>>> Cons:
>>>>>>>>> - Break the current status quo
>>>>>>>>>
>>>>>>>>> Given these 2 options we decided to go for a more OOP approach
>>>>>>>>> creating a Tree and a TreeNode classes, that in the future will be renamed
>>>>>>>>> to ACITreeWrapper and TreeNode.
>>>>>>>>>
>>>>>>>>> 2. After we decided on the starting point we searched for
>>>>>>>>> occurrences of the function "itemData" and we found out that there were 303
>>>>>>>>> occurrences of "itemData" in the code and roughly 176 calls to the function
>>>>>>>>> itself (some of the hits were variable names).
>>>>>>>>>
>>>>>>>>> 3. We selected the first file on the search and found the function
>>>>>>>>> that was responsible for calling the itemData function.
>>>>>>>>>
>>>>>>>>> 4. Extracted the function to a separate file
>>>>>>>>>
>>>>>>>>> 5. Wrap this function with tests
>>>>>>>>>
>>>>>>>>> 6. Refactor the function to ES6, give more declarative names to
>>>>>>>>> variables and break the functions into smaller chunks
>>>>>>>>>
>>>>>>>>> 7. When all the tests were passing we replaced ACITree with our
>>>>>>>>> Tree
>>>>>>>>>
>>>>>>>>> 8. We ensured that all tests were passing
>>>>>>>>>
>>>>>>>>> 9. Remove function from the original file and use the new function
>>>>>>>>>
>>>>>>>>> 10. Ensure everything still works
>>>>>>>>>
>>>>>>>>> 11. Find the next function and execute from step 4 until all the
>>>>>>>>> functions are replaced, refactored and tested.
>>>>>>>>>
>>>>>>>>> As you can see by the process this is a pretty huge undertake,
>>>>>>>>> because of the number of calls to the function. This is just the first step
>>>>>>>>> on the direction of completely isolating the ACITree so that we can solve
>>>>>>>>> the problem with a large number of elements on the tree.
>>>>>>>>>
>>>>>>>>> *What is on our radar that we need to address:*
>>>>>>>>> - Finish the complete decoupling of the ACITree
>>>>>>>>> - Performance of the current tree implementation
>>>>>>>>> - Tweak the naming of the Tree class to explicitly tell us this
>>>>>>>>> is to use only with ACITree.
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> Thanks
>>>>>>>>> Joao
>>>>>>>>>
>>>>>>>>>
>>>>>>>>
>>>>>>
--
Dave Page
Blog: http://pgsnake.blogspot.com
Twitter: @pgsnake
EnterpriseDB UK: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-27 09:15 Dave Page <[email protected]>
parent: Dave Page <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Dave Page @ 2018-04-27 09:15 UTC (permalink / raw)
To: Joao De Almeida Pereira <[email protected]>; Ashesh Vashi <[email protected]>; +Cc: Khushboo Vashi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
How is your work on this going Ashesh? Will you be committing today?
On Wed, Apr 25, 2018 at 8:52 AM, Dave Page <[email protected]> wrote:
> Ashesh; you had agreed to work on this early this week. Please ensure you
> do so today.
>
> Thanks.
>
> On Tue, Apr 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
> [email protected]> wrote:
>
>> Hi Hackers,
>>
>> Can someone review and merge this patch?
>>
>> Thanks
>> Joao
>>
>> On Wed, Apr 18, 2018 at 10:23 AM Joao De Almeida Pereira <
>> [email protected]> wrote:
>>
>>> Hi Hackers,
>>> Any other comment about this patch?
>>>
>>> Thanks
>>> Joao
>>>
>>> On Tue, Apr 10, 2018 at 12:00 PM Joao De Almeida Pereira <
>>> [email protected]> wrote:
>>>
>>>> Hello Khushboo
>>>>
>>>> On Mon, Apr 9, 2018 at 1:59 AM Khushboo Vashi <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi Joao,
>>>>>
>>>>> I have reviewed your patch and have some suggestions.
>>>>>
>>>>> On Sat, Apr 7, 2018 at 12:43 AM, Joao De Almeida Pereira <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hello Murtuza/Dave,
>>>>>> Yes now the extracted functions are spread into different files. The
>>>>>> intent would be to make the files as small as possible, and also to group
>>>>>> and name them in a way that would be easy to understand what each file is
>>>>>> doing without the need of opening it.
>>>>>> As a example:
>>>>>> static/js/backup will contain all the backup related functionality
>>>>>> inside of this folder we can see the file:
>>>>>>
>>>>> menu_utils.js At this moment in time we decided to group all the
>>>>>> functions that are related to the menu, but we can split that also if we
>>>>>> believe it is easier to see.
>>>>>>
>>>>> It's really very good to see the separated code for backup module. As
>>>>> we have done for backup, we would like do it for other PG utilities like
>>>>> restore, maintenance etc.
>>>>> Considering this, we should separate the code in a way that some of
>>>>> the common functionalities can be used for other modules like menu (as you
>>>>> have mentioned above), dialogue factory etc.
>>>>> Also, I think these functionalities should be in their respective
>>>>> static folder instead of pgadmin/static.
>>>>>
>>>>
>>>> About the location of the files. The move of the files to
>>>> pgadmin/static/js was made on purpose in order to clearly separate
>>>> Javascript from python code.
>>>> The rational behind it was
>>>> - Create a clear separation between the backend and frontend
>>>> - Having Javascript code concentrated in a single place, hopefully,
>>>> will encourage to developers to look for a functionality, that is already
>>>> implemented in another modules, because they are right there. (When we
>>>> started this journey we realized that the 'nodes' have a big groups of code
>>>> that could be shared, but because the Javascript is spread everywhere it is
>>>> much harder to look for it)
>>>>
>>>>
>>>> There are some drawbacks of this separation:
>>>> - When creating a new module we will need to put the javascript in a
>>>> separate location from the backend code
>>>>
>>>>
>>>>>
>>>>>
>>>>>> static/js/datagrid folder contains all the datagrid related
>>>>>> functionality
>>>>>>
>>>>> Same as backup module, this should be in it's respective static/js
>>>>> folder.
>>>>>
>>>>>> Inside of the folder we can see the files:
>>>>>> get_panel_title.js is responsible for retrieving the name of the
>>>>>> panel
>>>>>> show_data.js is responsible for showing the datagrid
>>>>>> show_query_tool.js is responsible for showing the query tool
>>>>>>
>>>>>> Does this structure make sense?
>>>>>> Can you give an example of a comment that you think is missing and
>>>>>> that could help?
>>>>>>
>>>>>> As a personal note, unless the algorithm is very obscure or very
>>>>>> complicated, I believe that if the code needs comments it is a signal that
>>>>>> something needs to change in terms of naming, structure of the part in
>>>>>> question. This being said, I am open to add some comments that might help
>>>>>> people.
>>>>>>
>>>>> You are right, with the help of naming convention and structure of the
>>>>> code, any one can get the idea about the code. But it is very useful to
>>>>> understand the code
>>>>> very easily with the proper comments especially when there are
>>>>> multiple developers working on a single project.
>>>>>
>>>>> I found some of the places where it would be great to have comments.
>>>>>
>>>>> - treeMenu: new tree.Tree() in a browser.js
>>>>> - tree.js (especially Tree class)
>>>>>
>>>> About the comment point I need a more clear understanding on what kind
>>>> of comments you are looking for. Because when you read the function names
>>>> you understand the intent, what they are doing. The parameters also explain
>>>> what you need to pass into them.
>>>>
>>>> If what you are looking for in these comments is the reasoning being
>>>> the change itself, then that should be present in the commit message.
>>>> Specially because this is going to be a very big patch with a very big
>>>> number of changes.
>>>>
>>>>>
>>>>> Thanks
>>>>>> Joao
>>>>>>
>>>>>>
>>>>>> Thanks,
>>>>> Khushboo
>>>>>
>>>>>>
>>>>>> On Fri, Apr 6, 2018 at 4:48 AM Murtuza Zabuawala <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hi Joao,
>>>>>>>
>>>>>>> Patch looks good and working as expected.
>>>>>>>
>>>>>>> I also agree with Dave, Can we please add some comments in each file
>>>>>>> which can help us to understand the flow, I'm saying because now the code
>>>>>>> is segregated in so many separate files it will be hard to keep track of
>>>>>>> the flow from one file to another when debugging.
>>>>>>>
>>>>>>>
>>>>>>> --
>>>>>>> Regards,
>>>>>>> Murtuza Zabuawala
>>>>>>> EnterpriseDB: http://www.enterprisedb.com
>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>
>>>>>>>
>>>>>>> On Thu, Apr 5, 2018 at 7:08 PM, Joao De Almeida Pereira <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hi Khushboo,
>>>>>>>> Attached you can find both patches rebased
>>>>>>>>
>>>>>>>> Thanks
>>>>>>>>
>>>>>>>>
>>>>>>>> On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Hi Joao,
>>>>>>>>>
>>>>>>>>> Can you please rebase the second patch?
>>>>>>>>>
>>>>>>>>> Thanks,
>>>>>>>>> Khushboo
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida Pereira <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Hi Hackers,
>>>>>>>>>>
>>>>>>>>>> Attached you can find the patch that will start to decouple
>>>>>>>>>> pgAdmin from ACITree library.
>>>>>>>>>> This patch is intended to be merged after 3.0, because we do not
>>>>>>>>>> want to cause any entropy or delay the release, but we want to start the
>>>>>>>>>> discussion and show some code.
>>>>>>>>>>
>>>>>>>>>> This job that we started is a massive tech debt chore that will
>>>>>>>>>> take some time to finalize and we would love the help of the community to
>>>>>>>>>> do it.
>>>>>>>>>>
>>>>>>>>>> *Summary of the patch:*
>>>>>>>>>> 0001 patch:
>>>>>>>>>> - Creates a new tree that will allow us to create a separation
>>>>>>>>>> between the application and ACI Tree
>>>>>>>>>> - Creates a Fake Tree (Test double, for reference on the
>>>>>>>>>> available test doubles: https://martinfowler.
>>>>>>>>>> com/bliki/TestDouble.html) that can be used to inplace to
>>>>>>>>>> replace the ACITree and also encapsulate the new tree behavior on our tests
>>>>>>>>>> - Adds tests for all the tree functionalities
>>>>>>>>>>
>>>>>>>>>> 0002 patch:
>>>>>>>>>> - Extracts, refactors, adds tests and remove dependency from ACI
>>>>>>>>>> Tree on:
>>>>>>>>>> - getTreeNodeHierarchy
>>>>>>>>>> - on backup.js: menu_enabled, menu_enabled_server,
>>>>>>>>>> start_backup_global_server, backup_objects
>>>>>>>>>> - on datagrid.js: show_data_grid, get_panel_title, show_query_tool
>>>>>>>>>> - Start using sprintf-js as Underscore.String is deprecating
>>>>>>>>>> sprintf function
>>>>>>>>>>
>>>>>>>>>> This patch represents only 10 calls to ACITree.itemData out of
>>>>>>>>>> 176 that are spread around our code
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> *In Depth look on the process behind the patch:*
>>>>>>>>>>
>>>>>>>>>> We started writing this patch with the idea that we need to
>>>>>>>>>> decouple pgAdmin4 from ACITree, because ACITree is no longer supported, the
>>>>>>>>>> documentation is non existent and ACITree is no longer being actively
>>>>>>>>>> developed.
>>>>>>>>>>
>>>>>>>>>> Our process:
>>>>>>>>>> 1. We "randomly" selected a function that is part of the ACITree.
>>>>>>>>>> From this point we decided to replace that function with our own version.
>>>>>>>>>> The function that we choose was "itemData".
>>>>>>>>>> The function gives us all the "data" that a specific node of the
>>>>>>>>>> tree find.
>>>>>>>>>> Given in order to replace the tree we would need to have a
>>>>>>>>>> function that would give us the same information. We had 2 options:
>>>>>>>>>> a) Create a tree with a function called itemData
>>>>>>>>>> Pros:
>>>>>>>>>> - At first view this was the simpler solution
>>>>>>>>>> - Would keep the status quo
>>>>>>>>>> Cons:
>>>>>>>>>> - Not a OOP approach
>>>>>>>>>> - Not very flexible
>>>>>>>>>> b) Create a tree that would return a node given an ID and then
>>>>>>>>>> the node would be responsible for giving it's data.
>>>>>>>>>> Pros:
>>>>>>>>>> - OOP Approach
>>>>>>>>>> - More flexible and we do not need to bring the tree around,
>>>>>>>>>> just a node
>>>>>>>>>> Cons:
>>>>>>>>>> - Break the current status quo
>>>>>>>>>>
>>>>>>>>>> Given these 2 options we decided to go for a more OOP approach
>>>>>>>>>> creating a Tree and a TreeNode classes, that in the future will be renamed
>>>>>>>>>> to ACITreeWrapper and TreeNode.
>>>>>>>>>>
>>>>>>>>>> 2. After we decided on the starting point we searched for
>>>>>>>>>> occurrences of the function "itemData" and we found out that there were 303
>>>>>>>>>> occurrences of "itemData" in the code and roughly 176 calls to the function
>>>>>>>>>> itself (some of the hits were variable names).
>>>>>>>>>>
>>>>>>>>>> 3. We selected the first file on the search and found the
>>>>>>>>>> function that was responsible for calling the itemData function.
>>>>>>>>>>
>>>>>>>>>> 4. Extracted the function to a separate file
>>>>>>>>>>
>>>>>>>>>> 5. Wrap this function with tests
>>>>>>>>>>
>>>>>>>>>> 6. Refactor the function to ES6, give more declarative names to
>>>>>>>>>> variables and break the functions into smaller chunks
>>>>>>>>>>
>>>>>>>>>> 7. When all the tests were passing we replaced ACITree with our
>>>>>>>>>> Tree
>>>>>>>>>>
>>>>>>>>>> 8. We ensured that all tests were passing
>>>>>>>>>>
>>>>>>>>>> 9. Remove function from the original file and use the new function
>>>>>>>>>>
>>>>>>>>>> 10. Ensure everything still works
>>>>>>>>>>
>>>>>>>>>> 11. Find the next function and execute from step 4 until all the
>>>>>>>>>> functions are replaced, refactored and tested.
>>>>>>>>>>
>>>>>>>>>> As you can see by the process this is a pretty huge undertake,
>>>>>>>>>> because of the number of calls to the function. This is just the first step
>>>>>>>>>> on the direction of completely isolating the ACITree so that we can solve
>>>>>>>>>> the problem with a large number of elements on the tree.
>>>>>>>>>>
>>>>>>>>>> *What is on our radar that we need to address:*
>>>>>>>>>> - Finish the complete decoupling of the ACITree
>>>>>>>>>> - Performance of the current tree implementation
>>>>>>>>>> - Tweak the naming of the Tree class to explicitly tell us this
>>>>>>>>>> is to use only with ACITree.
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> Thanks
>>>>>>>>>> Joao
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>
>>>>>>>
>
>
> --
> Dave Page
> Blog: http://pgsnake.blogspot.com
> Twitter: @pgsnake
>
> EnterpriseDB UK: http://www.enterprisedb.com
> The Enterprise PostgreSQL Company
>
--
Dave Page
Blog: http://pgsnake.blogspot.com
Twitter: @pgsnake
EnterpriseDB UK: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-27 09:34 Ashesh Vashi <[email protected]>
parent: Dave Page <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Ashesh Vashi @ 2018-04-27 09:34 UTC (permalink / raw)
To: Dave Page <[email protected]>; +Cc: Joao Pedro De Almeida Pereira <[email protected]>; khushboo.vashi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
I have quite a few comments for the patch.
I will send them soon.
On Fri, Apr 27, 2018, 14:45 Dave Page <[email protected]> wrote:
> How is your work on this going Ashesh? Will you be committing today?
>
> On Wed, Apr 25, 2018 at 8:52 AM, Dave Page <[email protected]> wrote:
>
>> Ashesh; you had agreed to work on this early this week. Please ensure you
>> do so today.
>>
>> Thanks.
>>
>> On Tue, Apr 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
>> [email protected]> wrote:
>>
>>> Hi Hackers,
>>>
>>> Can someone review and merge this patch?
>>>
>>> Thanks
>>> Joao
>>>
>>> On Wed, Apr 18, 2018 at 10:23 AM Joao De Almeida Pereira <
>>> [email protected]> wrote:
>>>
>>>> Hi Hackers,
>>>> Any other comment about this patch?
>>>>
>>>> Thanks
>>>> Joao
>>>>
>>>> On Tue, Apr 10, 2018 at 12:00 PM Joao De Almeida Pereira <
>>>> [email protected]> wrote:
>>>>
>>>>> Hello Khushboo
>>>>>
>>>>> On Mon, Apr 9, 2018 at 1:59 AM Khushboo Vashi <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hi Joao,
>>>>>>
>>>>>> I have reviewed your patch and have some suggestions.
>>>>>>
>>>>>> On Sat, Apr 7, 2018 at 12:43 AM, Joao De Almeida Pereira <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hello Murtuza/Dave,
>>>>>>> Yes now the extracted functions are spread into different files. The
>>>>>>> intent would be to make the files as small as possible, and also to group
>>>>>>> and name them in a way that would be easy to understand what each file is
>>>>>>> doing without the need of opening it.
>>>>>>> As a example:
>>>>>>> static/js/backup will contain all the backup related functionality
>>>>>>> inside of this folder we can see the file:
>>>>>>>
>>>>>> menu_utils.js At this moment in time we decided to group all the
>>>>>>> functions that are related to the menu, but we can split that also if we
>>>>>>> believe it is easier to see.
>>>>>>>
>>>>>> It's really very good to see the separated code for backup module. As
>>>>>> we have done for backup, we would like do it for other PG utilities like
>>>>>> restore, maintenance etc.
>>>>>> Considering this, we should separate the code in a way that some of
>>>>>> the common functionalities can be used for other modules like menu (as you
>>>>>> have mentioned above), dialogue factory etc.
>>>>>> Also, I think these functionalities should be in their respective
>>>>>> static folder instead of pgadmin/static.
>>>>>>
>>>>>
>>>>> About the location of the files. The move of the files to
>>>>> pgadmin/static/js was made on purpose in order to clearly separate
>>>>> Javascript from python code.
>>>>> The rational behind it was
>>>>> - Create a clear separation between the backend and frontend
>>>>> - Having Javascript code concentrated in a single place, hopefully,
>>>>> will encourage to developers to look for a functionality, that is already
>>>>> implemented in another modules, because they are right there. (When we
>>>>> started this journey we realized that the 'nodes' have a big groups of code
>>>>> that could be shared, but because the Javascript is spread everywhere it is
>>>>> much harder to look for it)
>>>>>
>>>>>
>>>>> There are some drawbacks of this separation:
>>>>> - When creating a new module we will need to put the javascript in a
>>>>> separate location from the backend code
>>>>>
>>>>>
>>>>>>
>>>>>>
>>>>>>> static/js/datagrid folder contains all the datagrid related
>>>>>>> functionality
>>>>>>>
>>>>>> Same as backup module, this should be in it's respective static/js
>>>>>> folder.
>>>>>>
>>>>>>> Inside of the folder we can see the files:
>>>>>>> get_panel_title.js is responsible for retrieving the name of the
>>>>>>> panel
>>>>>>> show_data.js is responsible for showing the datagrid
>>>>>>> show_query_tool.js is responsible for showing the query tool
>>>>>>>
>>>>>>> Does this structure make sense?
>>>>>>> Can you give an example of a comment that you think is missing and
>>>>>>> that could help?
>>>>>>>
>>>>>>> As a personal note, unless the algorithm is very obscure or very
>>>>>>> complicated, I believe that if the code needs comments it is a signal that
>>>>>>> something needs to change in terms of naming, structure of the part in
>>>>>>> question. This being said, I am open to add some comments that might help
>>>>>>> people.
>>>>>>>
>>>>>> You are right, with the help of naming convention and structure of
>>>>>> the code, any one can get the idea about the code. But it is very useful to
>>>>>> understand the code
>>>>>> very easily with the proper comments especially when there are
>>>>>> multiple developers working on a single project.
>>>>>>
>>>>>> I found some of the places where it would be great to have comments.
>>>>>>
>>>>>> - treeMenu: new tree.Tree() in a browser.js
>>>>>> - tree.js (especially Tree class)
>>>>>>
>>>>> About the comment point I need a more clear understanding on what kind
>>>>> of comments you are looking for. Because when you read the function names
>>>>> you understand the intent, what they are doing. The parameters also explain
>>>>> what you need to pass into them.
>>>>>
>>>>> If what you are looking for in these comments is the reasoning being
>>>>> the change itself, then that should be present in the commit message.
>>>>> Specially because this is going to be a very big patch with a very big
>>>>> number of changes.
>>>>>
>>>>>>
>>>>>> Thanks
>>>>>>> Joao
>>>>>>>
>>>>>>>
>>>>>>> Thanks,
>>>>>> Khushboo
>>>>>>
>>>>>>>
>>>>>>> On Fri, Apr 6, 2018 at 4:48 AM Murtuza Zabuawala <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hi Joao,
>>>>>>>>
>>>>>>>> Patch looks good and working as expected.
>>>>>>>>
>>>>>>>> I also agree with Dave, Can we please add some comments in each
>>>>>>>> file which can help us to understand the flow, I'm saying because now the
>>>>>>>> code is segregated in so many separate files it will be hard to keep track
>>>>>>>> of the flow from one file to another when debugging.
>>>>>>>>
>>>>>>>>
>>>>>>>> --
>>>>>>>> Regards,
>>>>>>>> Murtuza Zabuawala
>>>>>>>> EnterpriseDB: http://www.enterprisedb.com
>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>
>>>>>>>>
>>>>>>>> On Thu, Apr 5, 2018 at 7:08 PM, Joao De Almeida Pereira <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Hi Khushboo,
>>>>>>>>> Attached you can find both patches rebased
>>>>>>>>>
>>>>>>>>> Thanks
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Hi Joao,
>>>>>>>>>>
>>>>>>>>>> Can you please rebase the second patch?
>>>>>>>>>>
>>>>>>>>>> Thanks,
>>>>>>>>>> Khushboo
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida Pereira <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>
>>>>>>>>>>> Attached you can find the patch that will start to decouple
>>>>>>>>>>> pgAdmin from ACITree library.
>>>>>>>>>>> This patch is intended to be merged after 3.0, because we do not
>>>>>>>>>>> want to cause any entropy or delay the release, but we want to start the
>>>>>>>>>>> discussion and show some code.
>>>>>>>>>>>
>>>>>>>>>>> This job that we started is a massive tech debt chore that will
>>>>>>>>>>> take some time to finalize and we would love the help of the community to
>>>>>>>>>>> do it.
>>>>>>>>>>>
>>>>>>>>>>> *Summary of the patch:*
>>>>>>>>>>> 0001 patch:
>>>>>>>>>>> - Creates a new tree that will allow us to create a separation
>>>>>>>>>>> between the application and ACI Tree
>>>>>>>>>>> - Creates a Fake Tree (Test double, for reference on the
>>>>>>>>>>> available test doubles:
>>>>>>>>>>> https://martinfowler.com/bliki/TestDouble.html) that can be
>>>>>>>>>>> used to inplace to replace the ACITree and also encapsulate the new tree
>>>>>>>>>>> behavior on our tests
>>>>>>>>>>> - Adds tests for all the tree functionalities
>>>>>>>>>>>
>>>>>>>>>>> 0002 patch:
>>>>>>>>>>> - Extracts, refactors, adds tests and remove dependency from
>>>>>>>>>>> ACI Tree on:
>>>>>>>>>>> - getTreeNodeHierarchy
>>>>>>>>>>> - on backup.js: menu_enabled, menu_enabled_server,
>>>>>>>>>>> start_backup_global_server, backup_objects
>>>>>>>>>>> - on datagrid.js: show_data_grid, get_panel_title,
>>>>>>>>>>> show_query_tool
>>>>>>>>>>> - Start using sprintf-js as Underscore.String is deprecating
>>>>>>>>>>> sprintf function
>>>>>>>>>>>
>>>>>>>>>>> This patch represents only 10 calls to ACITree.itemData out of
>>>>>>>>>>> 176 that are spread around our code
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> *In Depth look on the process behind the patch:*
>>>>>>>>>>>
>>>>>>>>>>> We started writing this patch with the idea that we need to
>>>>>>>>>>> decouple pgAdmin4 from ACITree, because ACITree is no longer supported, the
>>>>>>>>>>> documentation is non existent and ACITree is no longer being actively
>>>>>>>>>>> developed.
>>>>>>>>>>>
>>>>>>>>>>> Our process:
>>>>>>>>>>> 1. We "randomly" selected a function that is part of the
>>>>>>>>>>> ACITree. From this point we decided to replace that function with our own
>>>>>>>>>>> version. The function that we choose was "itemData".
>>>>>>>>>>> The function gives us all the "data" that a specific node of the
>>>>>>>>>>> tree find.
>>>>>>>>>>> Given in order to replace the tree we would need to have a
>>>>>>>>>>> function that would give us the same information. We had 2 options:
>>>>>>>>>>> a) Create a tree with a function called itemData
>>>>>>>>>>> Pros:
>>>>>>>>>>> - At first view this was the simpler solution
>>>>>>>>>>> - Would keep the status quo
>>>>>>>>>>> Cons:
>>>>>>>>>>> - Not a OOP approach
>>>>>>>>>>> - Not very flexible
>>>>>>>>>>> b) Create a tree that would return a node given an ID and then
>>>>>>>>>>> the node would be responsible for giving it's data.
>>>>>>>>>>> Pros:
>>>>>>>>>>> - OOP Approach
>>>>>>>>>>> - More flexible and we do not need to bring the tree around,
>>>>>>>>>>> just a node
>>>>>>>>>>> Cons:
>>>>>>>>>>> - Break the current status quo
>>>>>>>>>>>
>>>>>>>>>>> Given these 2 options we decided to go for a more OOP approach
>>>>>>>>>>> creating a Tree and a TreeNode classes, that in the future will be renamed
>>>>>>>>>>> to ACITreeWrapper and TreeNode.
>>>>>>>>>>>
>>>>>>>>>>> 2. After we decided on the starting point we searched for
>>>>>>>>>>> occurrences of the function "itemData" and we found out that there were 303
>>>>>>>>>>> occurrences of "itemData" in the code and roughly 176 calls to the function
>>>>>>>>>>> itself (some of the hits were variable names).
>>>>>>>>>>>
>>>>>>>>>>> 3. We selected the first file on the search and found the
>>>>>>>>>>> function that was responsible for calling the itemData function.
>>>>>>>>>>>
>>>>>>>>>>> 4. Extracted the function to a separate file
>>>>>>>>>>>
>>>>>>>>>>> 5. Wrap this function with tests
>>>>>>>>>>>
>>>>>>>>>>> 6. Refactor the function to ES6, give more declarative names to
>>>>>>>>>>> variables and break the functions into smaller chunks
>>>>>>>>>>>
>>>>>>>>>>> 7. When all the tests were passing we replaced ACITree with our
>>>>>>>>>>> Tree
>>>>>>>>>>>
>>>>>>>>>>> 8. We ensured that all tests were passing
>>>>>>>>>>>
>>>>>>>>>>> 9. Remove function from the original file and use the new
>>>>>>>>>>> function
>>>>>>>>>>>
>>>>>>>>>>> 10. Ensure everything still works
>>>>>>>>>>>
>>>>>>>>>>> 11. Find the next function and execute from step 4 until all the
>>>>>>>>>>> functions are replaced, refactored and tested.
>>>>>>>>>>>
>>>>>>>>>>> As you can see by the process this is a pretty huge undertake,
>>>>>>>>>>> because of the number of calls to the function. This is just the first step
>>>>>>>>>>> on the direction of completely isolating the ACITree so that we can solve
>>>>>>>>>>> the problem with a large number of elements on the tree.
>>>>>>>>>>>
>>>>>>>>>>> *What is on our radar that we need to address:*
>>>>>>>>>>> - Finish the complete decoupling of the ACITree
>>>>>>>>>>> - Performance of the current tree implementation
>>>>>>>>>>> - Tweak the naming of the Tree class to explicitly tell us this
>>>>>>>>>>> is to use only with ACITree.
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> Thanks
>>>>>>>>>>> Joao
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>
>>
>>
>> --
>> Dave Page
>> Blog: http://pgsnake.blogspot.com
>> Twitter: @pgsnake
>>
>> EnterpriseDB UK: http://www.enterprisedb.com
>> The Enterprise PostgreSQL Company
>>
>
>
>
> --
> Dave Page
> Blog: http://pgsnake.blogspot.com
> Twitter: @pgsnake
>
> EnterpriseDB UK: http://www.enterprisedb.com
> The Enterprise PostgreSQL Company
>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-27 22:25 Joao De Almeida Pereira <[email protected]>
parent: Ashesh Vashi <[email protected]>
0 siblings, 2 replies; 69+ messages in thread
From: Joao De Almeida Pereira @ 2018-04-27 22:25 UTC (permalink / raw)
To: Ashesh Vashi <[email protected]>; +Cc: Dave Page <[email protected]>; Khushboo Vashi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
Hi Hackers,
As you are aware we kept on working on the patch, so we are attaching to
this email a new version of the patch.
This new version contains all the changes in the previous one plus more
extractions of functions and refactoring of code.
The objective of this patch is to create a separation between pgAdmin and
the ACI Tree. We are doing this because we realized that at this point in
time we have the ACI Tree all over the code of pgAdmin. I found a very
interesting article that really talks about this:
https://medium.freecodecamp.org/code-dependencies-are-the-devil-35ed28b556d
In this patch there are some visions and ideas about the location of the
code, the way to organize it and also try to pave the future for a
application that is stable, easy to develop on and that can be release at a
times notice.
We are investing a big chunk of our time in doing this refactoring, but
while doing that we also try to respond to the patches sent to the mailing
list. We would like the feedback from the community because we believe this
is a changing point for the application. The idea is to change the way we
develop this application, instead of only correcting a bug of developing a
feature, with every commit we should correct the bug or develop a feature
but leave the code a little better than we found it (Refactoring,
refactoring, refactoring). This is hard work but that is what the users
from pgAdmin expect from this community of developers.
======
It is a huge patch
86 files changed, 5492 inserts, 1840 deletions
and we would like to get your feedback as soon as possible, because we are
continuing to work on it which means it is going to grow in size.
At this point in time we still have 124 of 176 calls to the function
itemData from ACITree.
What does each patch contain:
0001:
Very simple patch, we found out that the linter was not looking into all
the javascript test files, so this patch will ensure it is
0002:
New Tree abstraction. This patch contains the new Tree that works as an
adaptor for ACI Tree and is going to be used on all the extractions that we
are doing
0003:
Code that extracts, wrap with tests and replace ACI Tree invocations.
We start creating new pattern for the location of Javascript files and
their structure.
Create patterns for creation of dialogs (backup and restore)
Thanks
Joao
On Fri, Apr 27, 2018 at 5:34 AM Ashesh Vashi <[email protected]>
wrote:
> I have quite a few comments for the patch.
> I will send them soon.
>
> On Fri, Apr 27, 2018, 14:45 Dave Page <[email protected]> wrote:
>
>> How is your work on this going Ashesh? Will you be committing today?
>>
>> On Wed, Apr 25, 2018 at 8:52 AM, Dave Page <[email protected]> wrote:
>>
>>> Ashesh; you had agreed to work on this early this week. Please ensure
>>> you do so today.
>>>
>>> Thanks.
>>>
>>> On Tue, Apr 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
>>> [email protected]> wrote:
>>>
>>>> Hi Hackers,
>>>>
>>>> Can someone review and merge this patch?
>>>>
>>>> Thanks
>>>> Joao
>>>>
>>>> On Wed, Apr 18, 2018 at 10:23 AM Joao De Almeida Pereira <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi Hackers,
>>>>> Any other comment about this patch?
>>>>>
>>>>> Thanks
>>>>> Joao
>>>>>
>>>>> On Tue, Apr 10, 2018 at 12:00 PM Joao De Almeida Pereira <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hello Khushboo
>>>>>>
>>>>>> On Mon, Apr 9, 2018 at 1:59 AM Khushboo Vashi <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hi Joao,
>>>>>>>
>>>>>>> I have reviewed your patch and have some suggestions.
>>>>>>>
>>>>>>> On Sat, Apr 7, 2018 at 12:43 AM, Joao De Almeida Pereira <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hello Murtuza/Dave,
>>>>>>>> Yes now the extracted functions are spread into different files.
>>>>>>>> The intent would be to make the files as small as possible, and also to
>>>>>>>> group and name them in a way that would be easy to understand what each
>>>>>>>> file is doing without the need of opening it.
>>>>>>>> As a example:
>>>>>>>> static/js/backup will contain all the backup related functionality
>>>>>>>> inside of this folder we can see the file:
>>>>>>>>
>>>>>>> menu_utils.js At this moment in time we decided to group all the
>>>>>>>> functions that are related to the menu, but we can split that also if we
>>>>>>>> believe it is easier to see.
>>>>>>>>
>>>>>>> It's really very good to see the separated code for backup module.
>>>>>>> As we have done for backup, we would like do it for other PG utilities like
>>>>>>> restore, maintenance etc.
>>>>>>> Considering this, we should separate the code in a way that some of
>>>>>>> the common functionalities can be used for other modules like menu (as you
>>>>>>> have mentioned above), dialogue factory etc.
>>>>>>> Also, I think these functionalities should be in their respective
>>>>>>> static folder instead of pgadmin/static.
>>>>>>>
>>>>>>
>>>>>> About the location of the files. The move of the files to
>>>>>> pgadmin/static/js was made on purpose in order to clearly separate
>>>>>> Javascript from python code.
>>>>>> The rational behind it was
>>>>>> - Create a clear separation between the backend and frontend
>>>>>> - Having Javascript code concentrated in a single place, hopefully,
>>>>>> will encourage to developers to look for a functionality, that is already
>>>>>> implemented in another modules, because they are right there. (When we
>>>>>> started this journey we realized that the 'nodes' have a big groups of code
>>>>>> that could be shared, but because the Javascript is spread everywhere it is
>>>>>> much harder to look for it)
>>>>>>
>>>>>>
>>>>>> There are some drawbacks of this separation:
>>>>>> - When creating a new module we will need to put the javascript in a
>>>>>> separate location from the backend code
>>>>>>
>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>> static/js/datagrid folder contains all the datagrid related
>>>>>>>> functionality
>>>>>>>>
>>>>>>> Same as backup module, this should be in it's respective static/js
>>>>>>> folder.
>>>>>>>
>>>>>>>> Inside of the folder we can see the files:
>>>>>>>> get_panel_title.js is responsible for retrieving the name of the
>>>>>>>> panel
>>>>>>>> show_data.js is responsible for showing the datagrid
>>>>>>>> show_query_tool.js is responsible for showing the query tool
>>>>>>>>
>>>>>>>> Does this structure make sense?
>>>>>>>> Can you give an example of a comment that you think is missing and
>>>>>>>> that could help?
>>>>>>>>
>>>>>>>> As a personal note, unless the algorithm is very obscure or very
>>>>>>>> complicated, I believe that if the code needs comments it is a signal that
>>>>>>>> something needs to change in terms of naming, structure of the part in
>>>>>>>> question. This being said, I am open to add some comments that might help
>>>>>>>> people.
>>>>>>>>
>>>>>>> You are right, with the help of naming convention and structure of
>>>>>>> the code, any one can get the idea about the code. But it is very useful to
>>>>>>> understand the code
>>>>>>> very easily with the proper comments especially when there are
>>>>>>> multiple developers working on a single project.
>>>>>>>
>>>>>>> I found some of the places where it would be great to have comments.
>>>>>>>
>>>>>>> - treeMenu: new tree.Tree() in a browser.js
>>>>>>> - tree.js (especially Tree class)
>>>>>>>
>>>>>> About the comment point I need a more clear understanding on what
>>>>>> kind of comments you are looking for. Because when you read the function
>>>>>> names you understand the intent, what they are doing. The parameters also
>>>>>> explain what you need to pass into them.
>>>>>>
>>>>>> If what you are looking for in these comments is the reasoning being
>>>>>> the change itself, then that should be present in the commit message.
>>>>>> Specially because this is going to be a very big patch with a very big
>>>>>> number of changes.
>>>>>>
>>>>>>>
>>>>>>> Thanks
>>>>>>>> Joao
>>>>>>>>
>>>>>>>>
>>>>>>>> Thanks,
>>>>>>> Khushboo
>>>>>>>
>>>>>>>>
>>>>>>>> On Fri, Apr 6, 2018 at 4:48 AM Murtuza Zabuawala <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Hi Joao,
>>>>>>>>>
>>>>>>>>> Patch looks good and working as expected.
>>>>>>>>>
>>>>>>>>> I also agree with Dave, Can we please add some comments in each
>>>>>>>>> file which can help us to understand the flow, I'm saying because now the
>>>>>>>>> code is segregated in so many separate files it will be hard to keep track
>>>>>>>>> of the flow from one file to another when debugging.
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> --
>>>>>>>>> Regards,
>>>>>>>>> Murtuza Zabuawala
>>>>>>>>> EnterpriseDB: http://www.enterprisedb.com
>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> On Thu, Apr 5, 2018 at 7:08 PM, Joao De Almeida Pereira <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Hi Khushboo,
>>>>>>>>>> Attached you can find both patches rebased
>>>>>>>>>>
>>>>>>>>>> Thanks
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>
>>>>>>>>>>> Can you please rebase the second patch?
>>>>>>>>>>>
>>>>>>>>>>> Thanks,
>>>>>>>>>>> Khushboo
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida Pereira <
>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>
>>>>>>>>>>>> Attached you can find the patch that will start to decouple
>>>>>>>>>>>> pgAdmin from ACITree library.
>>>>>>>>>>>> This patch is intended to be merged after 3.0, because we do
>>>>>>>>>>>> not want to cause any entropy or delay the release, but we want to start
>>>>>>>>>>>> the discussion and show some code.
>>>>>>>>>>>>
>>>>>>>>>>>> This job that we started is a massive tech debt chore that will
>>>>>>>>>>>> take some time to finalize and we would love the help of the community to
>>>>>>>>>>>> do it.
>>>>>>>>>>>>
>>>>>>>>>>>> *Summary of the patch:*
>>>>>>>>>>>> 0001 patch:
>>>>>>>>>>>> - Creates a new tree that will allow us to create a separation
>>>>>>>>>>>> between the application and ACI Tree
>>>>>>>>>>>> - Creates a Fake Tree (Test double, for reference on the
>>>>>>>>>>>> available test doubles:
>>>>>>>>>>>> https://martinfowler.com/bliki/TestDouble.html) that can be
>>>>>>>>>>>> used to inplace to replace the ACITree and also encapsulate the new tree
>>>>>>>>>>>> behavior on our tests
>>>>>>>>>>>> - Adds tests for all the tree functionalities
>>>>>>>>>>>>
>>>>>>>>>>>> 0002 patch:
>>>>>>>>>>>> - Extracts, refactors, adds tests and remove dependency from
>>>>>>>>>>>> ACI Tree on:
>>>>>>>>>>>> - getTreeNodeHierarchy
>>>>>>>>>>>> - on backup.js: menu_enabled, menu_enabled_server,
>>>>>>>>>>>> start_backup_global_server, backup_objects
>>>>>>>>>>>> - on datagrid.js: show_data_grid, get_panel_title,
>>>>>>>>>>>> show_query_tool
>>>>>>>>>>>> - Start using sprintf-js as Underscore.String is deprecating
>>>>>>>>>>>> sprintf function
>>>>>>>>>>>>
>>>>>>>>>>>> This patch represents only 10 calls to ACITree.itemData out of
>>>>>>>>>>>> 176 that are spread around our code
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> *In Depth look on the process behind the patch:*
>>>>>>>>>>>>
>>>>>>>>>>>> We started writing this patch with the idea that we need to
>>>>>>>>>>>> decouple pgAdmin4 from ACITree, because ACITree is no longer supported, the
>>>>>>>>>>>> documentation is non existent and ACITree is no longer being actively
>>>>>>>>>>>> developed.
>>>>>>>>>>>>
>>>>>>>>>>>> Our process:
>>>>>>>>>>>> 1. We "randomly" selected a function that is part of the
>>>>>>>>>>>> ACITree. From this point we decided to replace that function with our own
>>>>>>>>>>>> version. The function that we choose was "itemData".
>>>>>>>>>>>> The function gives us all the "data" that a specific node of
>>>>>>>>>>>> the tree find.
>>>>>>>>>>>> Given in order to replace the tree we would need to have a
>>>>>>>>>>>> function that would give us the same information. We had 2 options:
>>>>>>>>>>>> a) Create a tree with a function called itemData
>>>>>>>>>>>> Pros:
>>>>>>>>>>>> - At first view this was the simpler solution
>>>>>>>>>>>> - Would keep the status quo
>>>>>>>>>>>> Cons:
>>>>>>>>>>>> - Not a OOP approach
>>>>>>>>>>>> - Not very flexible
>>>>>>>>>>>> b) Create a tree that would return a node given an ID and
>>>>>>>>>>>> then the node would be responsible for giving it's data.
>>>>>>>>>>>> Pros:
>>>>>>>>>>>> - OOP Approach
>>>>>>>>>>>> - More flexible and we do not need to bring the tree around,
>>>>>>>>>>>> just a node
>>>>>>>>>>>> Cons:
>>>>>>>>>>>> - Break the current status quo
>>>>>>>>>>>>
>>>>>>>>>>>> Given these 2 options we decided to go for a more OOP approach
>>>>>>>>>>>> creating a Tree and a TreeNode classes, that in the future will be renamed
>>>>>>>>>>>> to ACITreeWrapper and TreeNode.
>>>>>>>>>>>>
>>>>>>>>>>>> 2. After we decided on the starting point we searched for
>>>>>>>>>>>> occurrences of the function "itemData" and we found out that there were 303
>>>>>>>>>>>> occurrences of "itemData" in the code and roughly 176 calls to the function
>>>>>>>>>>>> itself (some of the hits were variable names).
>>>>>>>>>>>>
>>>>>>>>>>>> 3. We selected the first file on the search and found the
>>>>>>>>>>>> function that was responsible for calling the itemData function.
>>>>>>>>>>>>
>>>>>>>>>>>> 4. Extracted the function to a separate file
>>>>>>>>>>>>
>>>>>>>>>>>> 5. Wrap this function with tests
>>>>>>>>>>>>
>>>>>>>>>>>> 6. Refactor the function to ES6, give more declarative names to
>>>>>>>>>>>> variables and break the functions into smaller chunks
>>>>>>>>>>>>
>>>>>>>>>>>> 7. When all the tests were passing we replaced ACITree with our
>>>>>>>>>>>> Tree
>>>>>>>>>>>>
>>>>>>>>>>>> 8. We ensured that all tests were passing
>>>>>>>>>>>>
>>>>>>>>>>>> 9. Remove function from the original file and use the new
>>>>>>>>>>>> function
>>>>>>>>>>>>
>>>>>>>>>>>> 10. Ensure everything still works
>>>>>>>>>>>>
>>>>>>>>>>>> 11. Find the next function and execute from step 4 until all
>>>>>>>>>>>> the functions are replaced, refactored and tested.
>>>>>>>>>>>>
>>>>>>>>>>>> As you can see by the process this is a pretty huge undertake,
>>>>>>>>>>>> because of the number of calls to the function. This is just the first step
>>>>>>>>>>>> on the direction of completely isolating the ACITree so that we can solve
>>>>>>>>>>>> the problem with a large number of elements on the tree.
>>>>>>>>>>>>
>>>>>>>>>>>> *What is on our radar that we need to address:*
>>>>>>>>>>>> - Finish the complete decoupling of the ACITree
>>>>>>>>>>>> - Performance of the current tree implementation
>>>>>>>>>>>> - Tweak the naming of the Tree class to explicitly tell us
>>>>>>>>>>>> this is to use only with ACITree.
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> Thanks
>>>>>>>>>>>> Joao
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>
>>>
>>>
>>> --
>>> Dave Page
>>> Blog: http://pgsnake.blogspot.com
>>> Twitter: @pgsnake
>>>
>>> EnterpriseDB UK: http://www.enterprisedb.com
>>> The Enterprise PostgreSQL Company
>>>
>>
>>
>>
>> --
>> Dave Page
>> Blog: http://pgsnake.blogspot.com
>> Twitter: @pgsnake
>>
>> EnterpriseDB UK: http://www.enterprisedb.com
>> The Enterprise PostgreSQL Company
>>
>
Attachments:
[application/octet-stream] 0001-Ensure-the-JS-linter-is-running-on-all-the-tests-fil.patch (978B, 3-0001-Ensure-the-JS-linter-is-running-on-all-the-tests-fil.patch)
download | inline diff:
diff --git a/web/pga_eslint.js b/web/pga_eslint.js
index 8c4e3803..11511d67 100644
--- a/web/pga_eslint.js
+++ b/web/pga_eslint.js
@@ -1,6 +1,7 @@
#!/usr/bin/env node
/* eslint no-console:off */
+/* eslint no-undef:off */
'use strict';
const debug = (process.argv.indexOf('--debug') > -1),
@@ -18,9 +19,9 @@ if (debug) {
require('debug').enable('eslint:*,-eslint:code-path');
}
-const fs = require('fs'),
- path = require('path'),
- read = (dir) =>
+const fs = require('fs');
+const path = require('path');
+const read = (dir) =>
fs.readdirSync(dir)
.reduce((files, file) =>
fs.statSync(path.join(dir, file)).isDirectory() ?
@@ -38,6 +39,6 @@ process.exitCode = eslint_cli.execute(
file_argv > -1 ? argv : argv.concat(
read(path.join(__dirname, 'pgadmin'))
).concat(
- 'regression/javascript/**/*.jsx regression/javascript/**/*.js *.js'
+ ['regression/javascript/**/*.jsx','regression/javascript/**/*.js','*.js']
)
);
--
2.16.2
[application/octet-stream] 0002-New-tree-implementation.patch (21.9K, 4-0002-New-tree-implementation.patch)
download | inline diff:
diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js
index 897d2708..fc68089f 100644
--- a/web/pgadmin/browser/static/js/browser.js
+++ b/web/pgadmin/browser/static/js/browser.js
@@ -1,4 +1,5 @@
define('pgadmin.browser', [
+ 'sources/tree/tree',
'sources/gettext', 'sources/url_for', 'require', 'jquery', 'underscore', 'underscore.string',
'bootstrap', 'sources/pgadmin', 'pgadmin.alertifyjs', 'bundled_codemirror',
'sources/check_node_visibility', 'sources/modify_animation', 'pgadmin.browser.utils', 'wcdocker',
@@ -10,6 +11,7 @@ define('pgadmin.browser', [
'sources/codemirror/addon/fold/pgadmin-sqlfoldcode',
'pgadmin.browser.keyboard',
], function(
+ tree,
gettext, url_for, require, $, _, S, Bootstrap, pgAdmin, Alertify,
codemirror, checkNodeVisibility, modifyAnimation
) {
@@ -86,6 +88,7 @@ define('pgadmin.browser', [
});
b.tree = $('#tree').aciTree('api');
+ b.treeMenu.register($('#tree'));
};
// Extend the browser class attributes
@@ -100,6 +103,7 @@ define('pgadmin.browser', [
editor:null,
// Left hand browser tree
tree:null,
+ treeMenu: new tree.Tree(),
// list of script to be loaded, when a certain type of node is loaded
// It will be used to register extensions, tools, child node scripts,
// etc.
diff --git a/web/pgadmin/static/js/tree/tree.js b/web/pgadmin/static/js/tree/tree.js
new file mode 100644
index 00000000..02685b48
--- /dev/null
+++ b/web/pgadmin/static/js/tree/tree.js
@@ -0,0 +1,187 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+export class TreeNode {
+ constructor(id, data, domNode, parent) {
+ this.id = id;
+ this.data = data;
+ this.setParent(parent);
+ this.children = [];
+ this.domNode = domNode;
+ }
+
+ hasParent() {
+ return this.parentNode !== null && this.parentNode !== undefined;
+ }
+
+ parent() {
+ return this.parentNode;
+ }
+
+ setParent(parent) {
+ this.parentNode = parent;
+ this.path = this.id;
+ if (parent !== null && parent !== undefined && parent.path !== undefined) {
+ this.path = parent.path + '.' + this.id;
+ }
+ }
+
+ getData() {
+ if (this.data === undefined) {
+ return undefined;
+ } else if (this.data === null) {
+ return null;
+ }
+ return Object.assign({}, this.data);
+ }
+
+ getHtmlIdentifier() {
+ return this.domNode;
+ }
+
+ reload(tree) {
+ this.unload(tree);
+ tree.aciTreeApi.setInode(this.domNode);
+ tree.aciTreeApi.deselect(this.domNode);
+ setTimeout(() => {
+ tree.selectNode(this.domNode);
+ }, 0);
+ }
+
+ unload(tree) {
+ this.children = [];
+ tree.aciTreeApi.unload(this.domNode);
+ }
+
+ anyParent(condition) {
+ let node = this;
+
+ while (node.hasParent()) {
+ node = node.parent();
+ if (condition(node)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ anyFamilyMember(condition) {
+ if(condition(this)) {
+ return true;
+ }
+
+ return this.anyParent(condition);
+ }
+}
+
+export class Tree {
+ constructor() {
+ this.rootNode = new TreeNode(undefined, {});
+ this.aciTreeApi = undefined;
+ }
+
+ addNewNode(id, data, domNode, parentPath) {
+ const parent = this.findNode(parentPath);
+ return this.createOrUpdateNode(id, data, parent, domNode);
+ }
+
+ findNode(path) {
+ if (path === null || path === undefined || path.length === 0) {
+ return this.rootNode;
+ }
+ return findInTree(this.rootNode, path.join('.'));
+ }
+
+ findNodeByDomElement(domElement) {
+ const path = this.translateTreeNodeIdFromACITree(domElement);
+ if(!path || !path[0]) {
+ return undefined;
+ }
+
+ return this.findNode(path);
+ }
+
+ selected() {
+ return this.aciTreeApi.selected();
+ }
+
+ selectNode(aciTreeIdentifier) {
+ this.aciTreeApi.select(aciTreeIdentifier);
+ }
+
+ register($treeJQuery) {
+ $treeJQuery.on('acitree', function (event, api, item, eventName) {
+ if (api.isItem(item)) {
+ if (eventName === 'added') {
+ const id = api.getId(item);
+ const data = api.itemData(item);
+ const parentId = this.translateTreeNodeIdFromACITree(api.parent(item));
+ this.addNewNode(id, data, item, parentId);
+ }
+ }
+ }.bind(this));
+ this.aciTreeApi = $treeJQuery.aciTree('api');
+ }
+
+ createOrUpdateNode(id, data, parent, domNode) {
+ let oldNodePath = [id];
+ if(parent !== null && parent !== undefined) {
+ oldNodePath = [parent.path, id];
+ }
+ const oldNode = this.findNode(oldNodePath);
+ if (oldNode !== null) {
+ oldNode.data = Object.assign({}, data);
+ return oldNode;
+ }
+
+ const node = new TreeNode(id, data, domNode, parent);
+ if (parent === this.rootNode) {
+ node.parentNode = null;
+ }
+ parent.children.push(node);
+ return node;
+ }
+
+ translateTreeNodeIdFromACITree(aciTreeNode) {
+ let currentTreeNode = aciTreeNode;
+ let path = [];
+ while (currentTreeNode !== null && currentTreeNode !== undefined && currentTreeNode.length > 0) {
+ path.unshift(this.aciTreeApi.getId(currentTreeNode));
+ if (this.aciTreeApi.hasParent(currentTreeNode)) {
+ currentTreeNode = this.aciTreeApi.parent(currentTreeNode);
+ } else {
+ break;
+ }
+ }
+ return path;
+ }
+}
+
+export let tree = new Tree();
+
+function findInTree(rootNode, path) {
+ if (path === null) {
+ return rootNode;
+ }
+ return (function findInNode(currentNode) {
+ for (let i = 0, length = currentNode.children.length; i < length; i++) {
+ const calculatedNode = findInNode(currentNode.children[i]);
+ if (calculatedNode !== null) {
+ return calculatedNode;
+ }
+ }
+
+ if (currentNode.path === path) {
+ return currentNode;
+ } else {
+ return null;
+ }
+ })(rootNode);
+}
diff --git a/web/regression/javascript/tree/tree_fake.js b/web/regression/javascript/tree/tree_fake.js
new file mode 100644
index 00000000..b285a45f
--- /dev/null
+++ b/web/regression/javascript/tree/tree_fake.js
@@ -0,0 +1,69 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {Tree} from '../../../pgadmin/static/js/tree/tree';
+
+export class TreeFake extends Tree {
+ constructor() {
+ super();
+ this.aciTreeToOurTreeTranslator = {};
+ this.aciTreeApi = jasmine.createSpyObj(
+ ['ACITreeApi'], ['setInode', 'unload', 'deselect', 'select']);
+ }
+
+ addNewNode(id, data, domNode, path) {
+ this.aciTreeToOurTreeTranslator[id] = [id];
+ if (path !== null && path !== undefined) {
+ this.aciTreeToOurTreeTranslator[id] = path.concat(id);
+ }
+ return super.addNewNode(id, data, domNode, path);
+ }
+
+ addChild(parent, child) {
+ child.setParent(parent);
+ this.aciTreeToOurTreeTranslator[child.id] = this.aciTreeToOurTreeTranslator[parent.id].concat(child.id);
+ parent.children.push(child);
+ }
+
+ hasParent(aciTreeNode) {
+ return this.translateTreeNodeIdFromACITree(aciTreeNode).length > 1;
+ }
+
+ parent(aciTreeNode) {
+ if (this.hasParent(aciTreeNode)) {
+ let path = this.translateTreeNodeIdFromACITree(aciTreeNode);
+ return [{id: this.findNode(path).parent().id}];
+ }
+
+ return null;
+ }
+
+ translateTreeNodeIdFromACITree(aciTreeNode) {
+ if(aciTreeNode === undefined || aciTreeNode[0] === undefined) {
+ return null;
+ }
+ return this.aciTreeToOurTreeTranslator[aciTreeNode[0].id];
+ }
+
+ itemData(aciTreeNode) {
+ let node = this.findNodeByDomElement(aciTreeNode);
+ if (node === undefined || node === null) {
+ return undefined;
+ }
+ return node.getData();
+ }
+
+ selected() {
+ return this.selectedNode;
+ }
+
+ selectNode(selectedNode) {
+ this.selectedNode = selectedNode;
+ }
+}
diff --git a/web/regression/javascript/tree/tree_spec.js b/web/regression/javascript/tree/tree_spec.js
new file mode 100644
index 00000000..164ceb5b
--- /dev/null
+++ b/web/regression/javascript/tree/tree_spec.js
@@ -0,0 +1,421 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+import {Tree, TreeNode} from '../../../pgadmin/static/js/tree/tree';
+import {TreeFake} from './tree_fake';
+
+const context = describe;
+
+const treeTests = (treeClass, setDefaultCallBack) => {
+ let tree;
+ beforeEach(() => {
+ tree = new treeClass();
+ });
+
+ describe('#addNewNode', () => {
+ describe('when add a new root element', () => {
+ context('using [] as the parent', () => {
+ beforeEach(() => {
+ tree.addNewNode('some new node', {data: 'interesting'}, undefined, []);
+ });
+
+ it('can be retrieved', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.data).toEqual({data: 'interesting'});
+ });
+
+ it('return false for #hasParent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.hasParent()).toBe(false);
+ });
+
+ it('return null for #parent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.parent()).toBeNull();
+ });
+ });
+
+ context('using null as the parent', () => {
+ beforeEach(() => {
+ tree.addNewNode('some new node', {data: 'interesting'}, undefined, null);
+ });
+
+ it('can be retrieved', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.data).toEqual({data: 'interesting'});
+ });
+
+ it('return false for #hasParent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.hasParent()).toBe(false);
+ });
+
+ it('return null for #parent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.parent()).toBeNull();
+ });
+ });
+
+ context('using undefined as the parent', () => {
+ beforeEach(() => {
+ tree.addNewNode('some new node', {data: 'interesting'});
+ });
+
+ it('can be retrieved', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.data).toEqual({data: 'interesting'});
+ });
+
+ it('return false for #hasParent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.hasParent()).toBe(false);
+ });
+
+ it('return null for #parent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.parent()).toBeNull();
+ });
+ });
+ });
+
+ describe('when add a new element as a child', () => {
+ let parentNode;
+ beforeEach(() => {
+ parentNode = tree.addNewNode('parent node', {data: 'parent data'}, undefined, []);
+ tree.addNewNode('some new node', {data: 'interesting'}, undefined, ['parent' +
+ ' node']);
+ });
+
+ it('can be retrieved', () => {
+ const node = tree.findNode(['parent node', 'some new node']);
+ expect(node.data).toEqual({data: 'interesting'});
+ });
+
+ it('return true for #hasParent()', () => {
+ const node = tree.findNode(['parent node', 'some new node']);
+ expect(node.hasParent()).toBe(true);
+ });
+
+ it('return "parent node" object for #parent()', () => {
+ const node = tree.findNode(['parent node', 'some new node']);
+ expect(node.parent()).toEqual(parentNode);
+ });
+ });
+
+ describe('when add an element that already exists under a parent', () => {
+ beforeEach(() => {
+ tree.addNewNode('parent node', {data: 'parent data'}, undefined, []);
+ tree.addNewNode('some new node', {data: 'interesting'}, undefined, ['parent' +
+ ' node']);
+ });
+
+ it('does not add a new child', () => {
+ tree.addNewNode('some new node', {data: 'interesting 1'}, undefined, ['parent' +
+ ' node']);
+ const parentNode = tree.findNode(['parent node']);
+ expect(parentNode.children.length).toBe(1);
+ });
+
+ it('updates the existing node data', () => {
+ tree.addNewNode('some new node', {data: 'interesting 1'}, undefined, ['parent' +
+ ' node']);
+ const node = tree.findNode(['parent node', 'some new node']);
+ expect(node.data).toEqual({data: 'interesting 1'});
+ });
+ });
+ });
+
+ describe('#translateTreeNodeIdFromACITree', () => {
+ let aciTreeApi;
+ beforeEach(() => {
+ aciTreeApi = jasmine.createSpyObj('ACITreeApi', [
+ 'hasParent',
+ 'parent',
+ 'getId',
+ ]);
+
+ aciTreeApi.getId.and.callFake((node) => {
+ return node[0].id;
+ });
+ tree.aciTreeApi = aciTreeApi;
+ });
+
+ describe('When tree as a single level', () => {
+ beforeEach(() => {
+ aciTreeApi.hasParent.and.returnValue(false);
+ });
+
+ it('returns an array with the ID of the first level', () => {
+ let node = [{
+ id: 'some id',
+ }];
+ tree.addNewNode('some id', {}, undefined, []);
+
+ expect(tree.translateTreeNodeIdFromACITree(node)).toEqual(['some id']);
+ });
+ });
+
+ describe('When tree as a 2 levels', () => {
+ describe('When we try to retrieve the node in the second level', () => {
+ it('returns an array with the ID of the first level and second level', () => {
+ aciTreeApi.hasParent.and.returnValues(true, false);
+ aciTreeApi.parent.and.returnValue([{
+ id: 'parent id',
+ }]);
+ let node = [{
+ id: 'some id',
+ }];
+
+ tree.addNewNode('parent id', {}, undefined, []);
+ tree.addNewNode('some id', {}, undefined, ['parent id']);
+
+ expect(tree.translateTreeNodeIdFromACITree(node))
+ .toEqual(['parent id', 'some id']);
+ });
+ });
+ });
+ });
+
+ describe('#selected', () => {
+ context('a node in the tree is selected', () => {
+ it('returns that node object', () => {
+ let selectedNode = new TreeNode('bamm', {}, []);
+ setDefaultCallBack(tree, selectedNode);
+ expect(tree.selected()).toEqual(selectedNode);
+ });
+ });
+ });
+
+ describe('#findNodeByTreeElement', () => {
+ context('retrieve data from node not found', () => {
+ it('return undefined', () => {
+ let aciTreeApi = jasmine.createSpyObj('ACITreeApi', [
+ 'hasParent',
+ 'parent',
+ 'getId',
+ ]);
+
+ aciTreeApi.getId.and.callFake((node) => {
+ return node[0].id;
+ });
+ tree.aciTreeApi = aciTreeApi;
+ expect(tree.findNodeByDomElement(['<li>something</li>'])).toBeUndefined();
+ });
+ });
+ });
+};
+
+describe('tree tests', () => {
+ describe('TreeNode', () => {
+ describe('#hasParent', () => {
+ context('parent is null', () => {
+ it('returns false', () => {
+ let treeNode = new TreeNode('123', {}, [], null);
+ expect(treeNode.hasParent()).toBe(false);
+ });
+ });
+ context('parent is undefined', () => {
+ it('returns false', () => {
+ let treeNode = new TreeNode('123', {}, [], undefined);
+ expect(treeNode.hasParent()).toBe(false);
+ });
+ });
+ context('parent exists', () => {
+ it('returns true', () => {
+ let parentNode = new TreeNode('456', {}, []);
+ let treeNode = new TreeNode('123', {}, [], parentNode);
+ expect(treeNode.hasParent()).toBe(true);
+ });
+ });
+ });
+
+ describe('#reload', () => {
+ let tree;
+ let level2;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, [{id: 'level1'}], []);
+ level2 = tree.addNewNode('level2', {data: 'data'}, [{id: 'level2'}], ['level1']);
+ tree.addNewNode('level3', {data: 'more data'}, [{id: 'level3'}], ['level1', 'level2']);
+
+ tree.aciTreeApi = jasmine.createSpyObj(
+ 'ACITreeApi', ['setInode', 'unload', 'deselect', 'select']);
+ });
+
+ it('reloads the node and its children', () => {
+ level2.reload(tree);
+ expect(tree.findNodeByDomElement([{id: 'level2'}])).toEqual(level2);
+ });
+
+ it('does not reload the children of node', () => {
+ level2.reload(tree);
+ expect(tree.findNodeByDomElement([{id: 'level3'}])).toBeNull();
+ });
+
+ it('select the node', (done) => {
+ level2.reload(tree);
+ setTimeout(() => {
+ expect(tree.selected()).toEqual([{id: 'level2'}]);
+ done();
+ }, 20);
+ });
+
+ describe('ACITree specific', () => {
+ it('sets the current node as a Inode, changing the Icon back to +', () => {
+ level2.reload(tree);
+ expect(tree.aciTreeApi.setInode).toHaveBeenCalledWith([{id: 'level2'}]);
+ });
+
+ it('deselect the node and selects it again to trigger ACI tree' +
+ ' events', (done) => {
+ level2.reload(tree);
+ setTimeout(() => {
+ expect(tree.aciTreeApi.deselect).toHaveBeenCalledWith([{id: 'level2'}]);
+ done();
+ }, 20);
+ });
+ });
+ });
+
+ describe('#unload', () => {
+ let tree;
+ let level2;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, ['<li>level1</li>'], []);
+ level2 = tree.addNewNode('level2', {data: 'data'}, ['<li>level2</li>'], ['level1']);
+ tree.addNewNode('level3', {data: 'more data'}, ['<li>level3</li>'], ['level1', 'level2']);
+ tree.aciTreeApi = jasmine.createSpyObj('ACITreeApi', ['unload']);
+ });
+
+ it('unloads the children of the current node', () => {
+ level2.unload(tree);
+ expect(tree.findNodeByDomElement([{id: 'level2'}])).toEqual(level2);
+ expect(tree.findNodeByDomElement([{id: 'level3'}])).toBeNull();
+ });
+
+ it('calls unload on the ACI Tree', () => {
+ level2.unload(tree);
+ expect(tree.aciTreeApi.unload).toHaveBeenCalledWith(['<li>level2</li>']);
+ });
+ });
+ });
+
+ describe('Tree', () => {
+ function realTreeSelectNode(tree, selectedNode) {
+ let aciTreeApi = jasmine.createSpyObj('ACITreeApi', [
+ 'selected',
+ ]);
+ tree.aciTreeApi = aciTreeApi;
+ aciTreeApi.selected.and.returnValue(selectedNode);
+ }
+
+ treeTests(Tree, realTreeSelectNode);
+ });
+
+ describe('TreeFake', () => {
+ function fakeTreeSelectNode(tree, selectedNode) {
+ tree.selectNode(selectedNode);
+ }
+
+ treeTests(TreeFake, fakeTreeSelectNode);
+
+ describe('#hasParent', () => {
+ context('tree contains multiple levels', () => {
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, undefined, []);
+ tree.addNewNode('level2', {data: 'interesting'}, undefined, ['level1']);
+ });
+
+ context('node is at the first level', () => {
+ it('returns false', () => {
+ expect(tree.hasParent([{id: 'level1'}])).toBe(false);
+ });
+ });
+
+ context('node is at the second level', () => {
+ it('returns true', () => {
+ expect(tree.hasParent([{id: 'level2'}])).toBe(true);
+ });
+ });
+ });
+ });
+
+ describe('#parent', () => {
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, undefined, []);
+ tree.addNewNode('level2', {data: 'interesting'}, undefined, ['level1']);
+ });
+
+ context('node is the root', () => {
+ it('returns null', () => {
+ expect(tree.parent([{id: 'level1'}])).toBeNull();
+ });
+ });
+
+ context('node is not root', () => {
+ it('returns root element', () => {
+ expect(tree.parent([{id: 'level2'}])).toEqual([{id: 'level1'}]);
+ });
+ });
+ });
+
+ describe('#itemData', () => {
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, undefined, []);
+ tree.addNewNode('level2', {data: 'expected data'}, undefined, ['level1']);
+ });
+
+ context('retrieve data from the node', () => {
+ it('return the node data', () => {
+ expect(tree.itemData([{id: 'level2'}])).toEqual({
+ data: 'expected' +
+ ' data',
+ });
+ });
+ });
+
+ context('retrieve data from node not found', () => {
+ it('return undefined', () => {
+ expect(tree.itemData([{id: 'bamm'}])).toBeUndefined();
+ });
+ });
+ });
+
+ describe('#addChild', () => {
+ let root, child;
+ beforeEach(() => {
+ let tree = new TreeFake();
+ root = tree.addNewNode('root', {}, [{id: 'root'}]);
+ child = new TreeNode('node.1', {}, [{id: 'node.1'}]);
+ tree.addChild(root, child);
+ });
+
+ it('adds a new child to a node', () => {
+ expect(root.children).toEqual([child]);
+ });
+
+ it('changes the parent of the child node', () => {
+ expect(root.children[0].parentNode).toEqual(root);
+ expect(child.parentNode).toEqual(root);
+ });
+
+ it('changes the path of the child', () => {
+ expect(child.path).toEqual('root.node.1');
+ });
+ });
+ });
+});
+
--
2.16.2
[application/octet-stream] 0003-Extract-add-tests-and-refactor-code-to-use-the-new-T.patch (271.9K, 5-0003-Extract-add-tests-and-refactor-code-to-use-the-new-T.patch)
download | inline diff:
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.js
index 9015d8d2..5a3a4bc0 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.js
@@ -1,8 +1,9 @@
define('pgadmin.node.collation', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
+ 'sources/menu/can_create',
'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser) {
+], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, canCreate) {
if (!pgBrowser.Nodes['coll-collation']) {
pgAdmin.Browser.Nodes['coll-collation'] =
@@ -223,32 +224,7 @@ define('pgadmin.node.collation', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create collation
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-collation' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-collation', item, data);
},
});
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.js
index 403ca471..1a38b083 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.js
@@ -2,9 +2,11 @@
define('pgadmin.node.domain', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid',
+ 'sources/menu/can_create',
'pgadmin.browser.collection',
], function(
- gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid
+ gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid,
+ canCreate
) {
// Define Domain Collection Node
@@ -297,32 +299,7 @@ define('pgadmin.node.domain', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create domain
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-domain' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-domain', item, data);
},
isDisabled: function(m){
if (!m.isNew()) {
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table.js
index 160db83f..0e820343 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table.js
@@ -2,9 +2,11 @@
define('pgadmin.node.foreign_table', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid',
+ 'sources/menu/can_create',
'pgadmin.browser.collection',
], function(
- gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid
+ gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid,
+ canCreate
) {
if (!pgBrowser.Nodes['coll-foreign_table']) {
@@ -660,32 +662,7 @@ define('pgadmin.node.foreign_table', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create foreign table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-foreign_table' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-foreign_table', item, data);
},
});
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.js
index 89806681..f2fe85be 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.js
@@ -1,9 +1,11 @@
define('pgadmin.node.fts_configuration', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid',
+ 'sources/menu/can_create',
'pgadmin.browser.collection',
], function(
- gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid
+ gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid,
+ canCreate
) {
// Model for tokens control
@@ -578,32 +580,7 @@ define('pgadmin.node.fts_configuration', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create fts configuration
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-fts_configuration' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-fts_configuration', item, data);
},
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary.js
index ed83feb1..f1a330f7 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary.js
@@ -1,8 +1,10 @@
define('pgadmin.node.fts_dictionary', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
+ 'sources/menu/can_create',
'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform) {
+], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform,
+ canCreate) {
// Extend the browser's node model class to create a option/value pair
var OptionLabelModel = pgAdmin.Browser.Node.Model.extend({
@@ -187,32 +189,7 @@ define('pgadmin.node.fts_dictionary', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create fts dictionary
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-fts_dictionary' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-fts_dictionary', item, data);
},
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser.js
index 92c0786e..daea35de 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser.js
@@ -1,7 +1,9 @@
define('pgadmin.node.fts_parser', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
- 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser) {
+ 'sources/pgadmin', 'pgadmin.browser',
+ 'sources/menu/can_create',
+ 'pgadmin.browser.collection',
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, canCreate) {
// Extend the collection class for fts parser
if (!pgBrowser.Nodes['coll-fts_parser']) {
@@ -200,32 +202,7 @@ define('pgadmin.node.fts_parser', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create fts parser
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-fts_parser' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-fts_parser', item, data);
},
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template.js
index 606a57a6..0c008c83 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template.js
@@ -1,7 +1,9 @@
define('pgadmin.node.fts_template', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
- 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser) {
+ 'sources/pgadmin', 'pgadmin.browser',
+ 'sources/menu/can_create',
+ 'pgadmin.browser.collection',
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, canCreate) {
// Extend the collection class for fts template
if (!pgBrowser.Nodes['coll-fts_template']) {
@@ -140,32 +142,7 @@ define('pgadmin.node.fts_template', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create fts fts_template
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-fts_template' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-fts_template', item, data);
},
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function.js
index 6e405165..40636446 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function.js
@@ -2,8 +2,10 @@
define('pgadmin.node.function', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
+ 'sources/menu/can_create',
'pgadmin.browser.collection', 'pgadmin.browser.server.privilege',
-], function(gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform) {
+], function(gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform,
+ canCreate) {
if (!pgBrowser.Nodes['coll-function']) {
pgBrowser.Nodes['coll-function'] =
@@ -439,32 +441,7 @@ define('pgadmin.node.function', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create Function
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-function' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-function', item, data);
},
});
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js
index aeb8271b..11b587ed 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js
@@ -2,8 +2,9 @@
define('pgadmin.node.trigger_function', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
+ 'sources/menu/can_create',
'pgadmin.browser.collection', 'pgadmin.browser.server.privilege',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform) {
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, canCreate) {
if (!pgBrowser.Nodes['coll-trigger_function']) {
pgBrowser.Nodes['coll-trigger_function'] =
@@ -358,32 +359,7 @@ define('pgadmin.node.trigger_function', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create Function
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-trigger_function' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-trigger_function', item, data);
},
});
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence.js
index 57c95acd..0a59efb0 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence.js
@@ -1,8 +1,10 @@
define('pgadmin.node.sequence', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
+ 'sources/menu/can_create',
'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform) {
+], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform,
+ canCreate) {
// Extend the browser's collection class for sequence collection
if (!pgBrowser.Nodes['coll-sequence']) {
@@ -61,32 +63,7 @@ define('pgadmin.node.sequence', [
canDrop: pgBrowser.Nodes['schema'].canChildDrop,
canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create collation
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-sequence' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-sequence', item, data);
},
// Define the model for sequence node.
model: pgBrowser.Node.Model.extend({
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
index a7fd4c7c..6d003578 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
@@ -1,8 +1,10 @@
define('pgadmin.node.schema', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid',
+ 'sources/schema/can_drop_child',
'pgadmin.browser.collection', 'pgadmin.browser.server.privilege',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, Backgrid) {
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, Backgrid,
+canDropChild) {
// VacuumSettings Collection to display all settings parameters as Grid
Backform.VacuumCollectionControl =
@@ -428,51 +430,12 @@ define('pgadmin.node.schema', [
// This function will checks whether we can allow user to
// drop object or not based on location within schema & catalog
canChildDrop: function(itemData, item) {
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create collation
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if(prev_d && prev_d._type == 'catalog') {
- return false;
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canDropChild.canDropChild(pgBrowser, itemData, item);
},
});
pgBrowser.tableChildTreeNodeHierarchy = function(i) {
- var idx = 0,
- res = {},
- t = pgBrowser.tree;
-
- do {
- var d = t.itemData(i);
- if (
- d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId
- ) {
- if (d._type === 'partition' || d._type === 'table') {
- if (!('table' in res)) {
- res['table'] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- } else {
- res[d._type] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- } while (i);
-
- return res;
+ return this.getTreeNodeHierarchy(i);
};
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js
index 72b6eb0c..1fdacf52 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js
@@ -88,7 +88,6 @@ define('pgadmin.node.column', [
if (!pgBrowser.Nodes['column']) {
pgBrowser.Nodes['column'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table', 'view', 'mview'],
collection_type: ['coll-table', 'coll-view', 'coll-mview'],
type: 'column',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js
index bf7ef556..b1cb2518 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js
@@ -8,7 +8,6 @@ define('pgadmin.node.check_constraint', [
// Check Constraint Node
if (!pgBrowser.Nodes['check_constraint']) {
pgAdmin.Browser.Nodes['check_constraint'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'check_constraint',
label: gettext('Check'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js
index 0bbf66a1..adccf2e9 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js
@@ -605,7 +605,6 @@ define('pgadmin.node.exclusion_constraint', [
// Extend the browser's node class for exclusion constraint node
if (!pgBrowser.Nodes['exclusion_constraint']) {
pgAdmin.Browser.Nodes['exclusion_constraint'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'exclusion_constraint',
label: gettext('Exclusion constraint'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js
index 4997d175..788cecfc 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js
@@ -603,7 +603,6 @@ define('pgadmin.node.foreign_key', [
// Extend the browser's node class for foreign key node
if (!pgBrowser.Nodes['foreign_key']) {
pgAdmin.Browser.Nodes['foreign_key'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'foreign_key',
label: gettext('Foreign key'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js
index d3a6cff4..0ad0f054 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js
@@ -20,7 +20,6 @@ define('pgadmin.node.primary_key', [
parent_type: ['table','partition'],
canDrop: true,
canDropCascade: true,
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
Init: function() {
/* Avoid multiple registration of menus */
if (this.initialized)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js
index 769185d6..18d3ca33 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js
@@ -20,7 +20,6 @@ define('pgadmin.node.unique_constraint', [
parent_type: ['table','partition'],
canDrop: true,
canDropCascade: true,
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
Init: function() {
/* Avoid multiple registration of menus */
if (this.initialized)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js
index cb242cd2..9c0e24fe 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js
@@ -12,14 +12,12 @@ define('pgadmin.node.constraints', [
node: 'constraints',
label: gettext('Constraints'),
type: 'coll-constraints',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
columns: ['name', 'comment'],
});
}
if (!pgBrowser.Nodes['constraints']) {
pgAdmin.Browser.Nodes['constraints'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'constraints',
label: gettext('Constraints'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js
index ec2b4da1..2b687c1a 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js
@@ -13,7 +13,6 @@ define('pgadmin.node.index', [
node: 'index',
label: gettext('Indexes'),
type: 'coll-index',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
sqlAlterHelp: 'sql-alterindex.html',
sqlCreateHelp: 'sql-createindex.html',
dialogHelp: url_for('help.static', {'filename': 'index_dialog.html'}),
@@ -215,7 +214,6 @@ define('pgadmin.node.index', [
if (!pgBrowser.Nodes['index']) {
pgAdmin.Browser.Nodes['index'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table', 'view', 'mview', 'partition'],
collection_type: ['coll-table', 'coll-view'],
sqlAlterHelp: 'sql-alterindex.html',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js
index fd53f743..17a0e6ce 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js
@@ -1,11 +1,15 @@
define([
+ 'sources/tree/pgadmin_tree_node',
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.backgrid',
+ 'sources/menu/can_create',
'pgadmin.browser.collection', 'pgadmin.browser.table.partition.utils',
],
function(
- gettext, url_for, $, _, S, pgAdmin, pgBrowser, Alertify, Backform, Backgrid
+ pgadminTreeNode,
+ gettext, url_for, $, _, S, pgAdmin, pgBrowser, Alertify, Backform, Backgrid,
+ canCreate
) {
if (!pgBrowser.Nodes['coll-partition']) {
@@ -13,7 +17,6 @@ function(
pgAdmin.Browser.Collection.extend({
node: 'partition',
label: gettext('Partitions'),
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'coll-partition',
columns: [
'name', 'schema', 'partition_value', 'is_partitioned', 'description',
@@ -80,36 +83,6 @@ function(
},
]);
},
- getTreeNodeHierarchy: function(i) {
- var idx = 0,
- res = {},
- t = pgBrowser.tree;
-
- do {
- var d = t.itemData(i);
- if (
- d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId
- ) {
- if (d._type == 'partition' && 'partition' in res) {
- if (!('table' in res)) {
- res['table'] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- } else if (d._type == 'table') {
- if (!('table' in res)) {
- res['table'] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- } else {
- res[d._type] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- } while (i);
-
- return res;
- },
generate_url: function(item, type, d, with_id, info) {
if (_.indexOf([
'stats', 'statistics', 'dependency', 'dependent', 'reset',
@@ -1219,32 +1192,7 @@ function(
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-table' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null;
- var prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-table', item, data);
},
// Check to whether table has disable trigger(s)
canCreate_with_trigger_enable: function(itemData, item, data) {
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js
index 3af61754..354909f3 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js
@@ -16,7 +16,6 @@ define('pgadmin.node.rule', [
node: 'rule',
label: gettext('Rules'),
type: 'coll-rule',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
columns: ['name', 'owner', 'comment'],
});
}
@@ -35,7 +34,6 @@ define('pgadmin.node.rule', [
*/
if (!pgBrowser.Nodes['rule']) {
pgAdmin.Browser.Nodes['rule'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table','view', 'partition'],
type: 'rule',
sqlAlterHelp: 'sql-alterrule.html',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
index 647d1bfe..c050c469 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
@@ -1,13 +1,17 @@
define('pgadmin.node.table', [
+ 'sources/table',
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.backgrid',
'pgadmin.tables.js/show_advanced_tab',
+ 'sources/menu/can_create',
+
'pgadmin.browser.collection', 'pgadmin.node.column',
'pgadmin.node.constraints', 'pgadmin.browser.table.partition.utils',
], function(
+ tableFunctions,
gettext, url_for, $, _, S, pgAdmin, pgBrowser, Alertify, Backform, Backgrid,
- ShowAdvancedTab
+ ShowAdvancedTab, canCreate
) {
if (!pgBrowser.Nodes['coll-table']) {
@@ -26,7 +30,6 @@ define('pgadmin.node.table', [
if (!pgBrowser.Nodes['table']) {
pgBrowser.Nodes['table'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'table',
label: gettext('Table'),
collection_type: 'coll-table',
@@ -118,53 +121,21 @@ define('pgadmin.node.table', [
callbacks: {
/* Enable trigger(s) on table */
enable_triggers_on_table: function(args) {
- var params = {'enable': true };
- this.callbacks.set_triggers.apply(this, [args, params]);
+ tableFunctions.enableTriggers(
+ pgBrowser.treeMenu,
+ Alertify,
+ this.generate_url.bind(this),
+ args
+ );
},
/* Disable trigger(s) on table */
disable_triggers_on_table: function(args) {
- var params = {'enable': false };
- this.callbacks.set_triggers.apply(this, [args, params]);
- },
- set_triggers: function(args, params) {
- // This function will send request to enable or
- // disable triggers on table level
- var input = args || {},
- obj = this,
- t = pgBrowser.tree,
- i = input.item || t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined;
- if (!d)
- return false;
-
- $.ajax({
- url: obj.generate_url(i, 'set_trigger' , d, true),
- type:'PUT',
- data: params,
- dataType: 'json',
- success: function(res) {
- if (res.success == 1) {
- Alertify.success(res.info);
- t.unload(i);
- t.setInode(i);
- t.deselect(i);
- setTimeout(function() {
- t.select(i);
- }, 10);
- }
- },
- error: function(xhr) {
- try {
- var err = $.parseJSON(xhr.responseText);
- if (err.success == 0) {
- Alertify.error(err.errormsg);
- }
- } catch (e) {
- console.warn(e.stack || e);
- }
- t.unload(i);
- },
- });
+ tableFunctions.disableTriggers(
+ pgBrowser.treeMenu,
+ Alertify,
+ this.generate_url.bind(this),
+ args
+ );
},
/* Truncate table */
truncate_table: function(args) {
@@ -1329,32 +1300,7 @@ define('pgadmin.node.table', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-table' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-table', item, data);
},
// Check to whether table has disable trigger(s)
canCreate_with_trigger_enable: function(itemData, item, data) {
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js
index fbe7659e..9a1ce2e0 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js
@@ -29,14 +29,12 @@ define('pgadmin.node.trigger', [
node: 'trigger',
label: gettext('Triggers'),
type: 'coll-trigger',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
columns: ['name', 'description'],
});
}
if (!pgBrowser.Nodes['trigger']) {
pgAdmin.Browser.Nodes['trigger'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table', 'view', 'partition'],
collection_type: ['coll-table', 'coll-view'],
type: 'trigger',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.js
index c1c24861..91ce615f 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.js
@@ -1,8 +1,9 @@
define('pgadmin.node.type', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
- 'pgadmin.backgrid', 'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, Backgrid) {
+ 'pgadmin.backgrid', 'sources/menu/can_create', 'pgadmin.browser.collection',
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, Backgrid,
+ canCreate) {
if (!pgBrowser.Nodes['coll-type']) {
pgBrowser.Nodes['coll-type'] =
@@ -912,32 +913,7 @@ define('pgadmin.node.type', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-type' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-type', item, data);
},
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.js
index 795bbfcf..15a517b8 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.js
@@ -1,8 +1,11 @@
define('pgadmin.node.mview', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.alertifyjs', 'pgadmin.browser',
- 'pgadmin.backform', 'pgadmin.browser.server.privilege',
-], function(gettext, url_for, $, _, pgAdmin, Alertify, pgBrowser, Backform) {
+ 'pgadmin.backform',
+ 'sources/menu/can_create',
+ 'pgadmin.browser.server.privilege',
+], function(gettext, url_for, $, _, pgAdmin, Alertify, pgBrowser, Backform,
+ canCreate) {
/**
Create and add a view collection into nodes
@@ -241,37 +244,7 @@ define('pgadmin.node.mview', [
and hide for system view in catalogs.
*/
canCreate: function(itemData, item, data) {
-
- // If check is false then, we will allow create menu
- if (data && data.check === false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
-
- // To iterate over tree to check parent node
- while (i) {
-
- // If it is schema then allow user to create view
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-mview' == d._type) {
-
- // Check if we are not child of view
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
-
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-mview', item, data);
},
refresh_mview: function(args) {
var input = args || {},
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js
index 5755a509..e4258188 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js
@@ -1,9 +1,12 @@
define('pgadmin.node.view', [
'sources/gettext',
'sources/url_for', 'jquery', 'underscore', 'sources/pgadmin',
- 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.browser.server.privilege',
+ 'pgadmin.browser', 'pgadmin.backform',
+ 'sources/menu/can_create',
+ 'pgadmin.browser.server.privilege',
'pgadmin.node.rule',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform) {
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform,
+ canCreate) {
/**
Create and add a view collection into nodes
@@ -203,38 +206,7 @@ define('pgadmin.node.view', [
and hide for system view in catalogs.
*/
canCreate: function(itemData, item, data) {
-
- // If check is false then, we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
-
- // To iterate over tree to check parent node
- while (i) {
-
- // If it is schema then allow user to create view
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-view' == d._type) {
-
- // Check if we are not child of view
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
-
- // by default we do not want to allow create menu
- return true;
-
+ return canCreate.canCreate(pgBrowser, 'coll-view', item, data);
},
});
}
diff --git a/web/pgadmin/browser/static/js/node.js b/web/pgadmin/browser/static/js/node.js
index ad582483..3baf554a 100644
--- a/web/pgadmin/browser/static/js/node.js
+++ b/web/pgadmin/browser/static/js/node.js
@@ -1,9 +1,12 @@
define('pgadmin.browser.node', [
+ 'sources/tree/pgadmin_tree_node',
'sources/gettext', 'jquery', 'underscore', 'underscore.string', 'sources/pgadmin',
'pgadmin.browser.menu', 'backbone', 'pgadmin.alertifyjs', 'pgadmin.browser.datamodel',
'backform', 'sources/browser/generate_url', 'sources/utils', 'pgadmin.browser.utils',
'pgadmin.backform',
-], function(gettext, $, _, S, pgAdmin, Menu, Backbone, Alertify, pgBrowser, Backform, generateUrl, commonUtils) {
+], function(
+ pgadminTreeNode,
+ gettext, $, _, S, pgAdmin, Menu, Backbone, Alertify, pgBrowser, Backform, generateUrl, commonUtils) {
var wcDocker = window.wcDocker,
keyCode = {
@@ -1566,7 +1569,6 @@ define('pgadmin.browser.node', [
* depends, statistics
*/
generate_url: function(item, type, d, with_id, info) {
-
var opURL = {
'create': 'obj',
'drop': 'obj',
@@ -1608,24 +1610,7 @@ define('pgadmin.browser.node', [
Collection: pgBrowser.DataCollection,
// Base class for Node Data Model
Model: pgBrowser.DataModel,
- getTreeNodeHierarchy: function(i) {
- var idx = 0,
- res = {},
- t = pgBrowser.tree,
- d;
- do {
- d = t.itemData(i);
- if (d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId) {
- res[d._type] = _.extend({}, d, {
- 'priority': idx,
- });
- idx -= 1;
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- } while (i);
-
- return res;
- },
+ getTreeNodeHierarchy: pgadminTreeNode.getTreeNodeHierarchyFromIdentifier.bind(pgBrowser),
cache: function(url, node_info, level, data) {
var cached = this.cached = this.cached || {},
hash = url,
diff --git a/web/pgadmin/static/js/alertify/dialog.js b/web/pgadmin/static/js/alertify/dialog.js
new file mode 100644
index 00000000..958296c3
--- /dev/null
+++ b/web/pgadmin/static/js/alertify/dialog.js
@@ -0,0 +1,107 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../gettext';
+import {sprintf} from 'sprintf-js';
+import {DialogFactory} from './dialog_factory';
+import Backform from '../backform.pgadmin';
+
+export class Dialog {
+ constructor(errorAlertTitle,
+ dialogContainerSelector,
+ pgBrowser, $, alertify, DialogModel,
+ backform = Backform) {
+ this.errorAlertTitle = errorAlertTitle;
+ this.alertify = alertify;
+ this.pgBrowser = pgBrowser;
+ this.jquery = $;
+ this.dialogModel = DialogModel;
+ this.backform = backform;
+ this.dialogContainerSelector = dialogContainerSelector;
+ }
+
+ retrieveAncestorOfTypeServer(item) {
+ let serverInformation = null;
+ let aciTreeItem = item || this.pgBrowser.treeMenu.selected();
+ let treeNode = this.pgBrowser.treeMenu.findNodeByDomElement(aciTreeItem);
+
+ while (treeNode) {
+ const node_data = treeNode.getData();
+ if (node_data._type === 'server') {
+ serverInformation = node_data;
+ break;
+ }
+
+ if (treeNode.hasParent()) {
+ treeNode = treeNode.parent();
+ } else {
+ this.alertify.alert(
+ gettext(this.errorAlertTitle),
+ gettext('Please select server or child node from the browser tree.')
+ );
+ break;
+ }
+ }
+ return serverInformation;
+ }
+
+ hasBinariesConfiguration(serverInformation) {
+ const module = 'paths';
+ let preference_name = 'pg_bin_dir';
+ let msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
+
+ if ((serverInformation.type && serverInformation.type === 'ppas') ||
+ serverInformation.server_type === 'ppas') {
+ preference_name = 'ppas_bin_dir';
+ msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
+ }
+ const preference = this.pgBrowser.get_preference(module, preference_name);
+
+ if (preference) {
+ if (!preference.value) {
+ this.alertify.alert(gettext('Configuration required'), msg);
+ return false;
+ }
+ } else {
+ this.alertify.alert(
+ gettext(this.errorAlertTitle),
+ sprintf(gettext('Failed to load preference %s of module %s'), preference_name, module)
+ );
+ return false;
+ }
+ return true;
+ }
+
+ dialogName() {
+ return undefined;
+ }
+
+ createOrGetDialog(dialogTitle, typeOfDialog) {
+ const dialogName = this.dialogName(typeOfDialog);
+
+ if (!this.alertify[dialogName]) {
+ const self = this;
+ this.alertify.dialog(dialogName, function factory() {
+ return self.dialogFactory(dialogTitle, typeOfDialog);
+ });
+ }
+ return this.alertify[dialogName];
+ }
+
+ dialogFactory(dialogTitle, typeOfDialog) {
+ const factory = new DialogFactory(
+ this.pgBrowser,
+ this.jquery,
+ this.alertify,
+ this.dialogModel,
+ this.backform,
+ this.dialogContainerSelector);
+ return factory.create(dialogTitle, typeOfDialog);
+ }
+}
diff --git a/web/pgadmin/static/js/alertify/dialog_factory.js b/web/pgadmin/static/js/alertify/dialog_factory.js
new file mode 100644
index 00000000..806f8687
--- /dev/null
+++ b/web/pgadmin/static/js/alertify/dialog_factory.js
@@ -0,0 +1,52 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import * as BackupDialog from '../backup/backup_dialog_wrapper';
+import {RestoreDialogWrapper} from '../restore/restore_dialog_wrapper';
+
+export class DialogFactory {
+ constructor(pgBrowser, $,
+ alertify, DialogModel,
+ backform, dialogContainerSelector) {
+ this.pgBrowser = pgBrowser;
+ this.jquery = $;
+ this.alertify = alertify;
+ this.dialogModel = DialogModel;
+ this.backform = backform;
+ this.dialogContainerSelector = dialogContainerSelector;
+ }
+
+ create(dialogTitle, typeOfDialog) {
+ if (typeOfDialog === 'restore') {
+ return this.createRestoreDialog(dialogTitle, typeOfDialog);
+ } else {
+ return this.createBackupDialog(dialogTitle, typeOfDialog);
+ }
+ }
+
+ createRestoreDialog(dialogTitle, typeOfDialog) {
+ return new RestoreDialogWrapper(
+ this.dialogContainerSelector, dialogTitle, typeOfDialog,
+ this.jquery,
+ this.pgBrowser,
+ this.alertify,
+ this.dialogModel,
+ this.backform);
+ }
+
+ createBackupDialog(dialogTitle, typeOfDialog) {
+ return new BackupDialog.BackupDialogWrapper(
+ this.dialogContainerSelector, dialogTitle, typeOfDialog,
+ this.jquery,
+ this.pgBrowser,
+ this.alertify,
+ this.dialogModel,
+ this.backform);
+ }
+}
diff --git a/web/pgadmin/static/js/alertify/dialog_wrapper.js b/web/pgadmin/static/js/alertify/dialog_wrapper.js
new file mode 100644
index 00000000..b5ff8204
--- /dev/null
+++ b/web/pgadmin/static/js/alertify/dialog_wrapper.js
@@ -0,0 +1,57 @@
+import * as commonUtils from '../utils';
+
+export class DialogWrapper {
+ constructor(
+ dialogContainerSelector, dialogTitle, jquery, pgBrowser,
+ alertify, dialogModel, backform) {
+ this.hooks = {
+ onclose: function () {
+ if (this.view) {
+ this.view.remove({
+ data: true,
+ internal: true,
+ silent: true,
+ });
+ }
+ },
+ };
+ this.dialogContainerSelector = dialogContainerSelector;
+ this.dialogTitle = dialogTitle;
+ this.jquery = jquery;
+ this.pgBrowser = pgBrowser;
+ this.alertify = alertify;
+ this.dialogModel = dialogModel;
+ this.backform = backform;
+ }
+
+ build() {
+ this.alertify.pgDialogBuild.apply(this);
+ }
+
+ wasHelpButtonPressed(e) {
+ return e.button.element.name === 'dialog_help'
+ || e.button.element.name === 'object_help';
+ }
+
+ getSelectedNodeData(selectedTreeNode) {
+ if (!this.isNodeSelected(selectedTreeNode)) {
+ return undefined;
+ }
+ const treeNodeData = selectedTreeNode.getData();
+ if (treeNodeData) {
+ return treeNodeData;
+ }
+ return undefined;
+ }
+
+ focusOnDialog(dialog) {
+ dialog.$el.attr('tabindex', -1);
+ this.pgBrowser.keyboardNavigation.getDialogTabNavigator(dialog);
+ const container = dialog.$el.find('.tab-content:first > .tab-pane.active:first');
+ commonUtils.findAndSetFocus(container);
+ }
+
+ isNodeSelected(selectedTreeNode) {
+ return selectedTreeNode;
+ }
+}
diff --git a/web/pgadmin/static/js/backup/backup_dialog.js b/web/pgadmin/static/js/backup/backup_dialog.js
new file mode 100644
index 00000000..aa4461f5
--- /dev/null
+++ b/web/pgadmin/static/js/backup/backup_dialog.js
@@ -0,0 +1,65 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../gettext';
+import Backform from '../backform.pgadmin';
+import {Dialog} from '../alertify/dialog';
+
+export class BackupDialog extends Dialog {
+ constructor(pgBrowser, $, alertify, BackupModel, backform = Backform) {
+ super('Backup Error',
+ '<div class=\'backup_dialog\'></div>',
+ pgBrowser, $, alertify, BackupModel, backform);
+ }
+
+ draw(action, aciTreeItem, params) {
+ const serverInformation = this.retrieveAncestorOfTypeServer(aciTreeItem);
+
+ if (!serverInformation) {
+ return;
+ }
+
+ if (!this.hasBinariesConfiguration(serverInformation)) {
+ return;
+ }
+
+ const typeOfDialog = BackupDialog.typeOfDialog(params);
+
+ const dialog = this.createOrGetDialog(BackupDialog.dialogTitle(typeOfDialog),
+ typeOfDialog);
+ dialog(true).resizeTo('60%', '50%');
+ }
+
+ static typeOfDialog(params) {
+ if(params === null) {
+ return 'backup_objects';
+ }
+ let typeOfDialog = 'server';
+ if (!_.isUndefined(params['globals']) && params['globals']) {
+ typeOfDialog = 'globals';
+ }
+ return typeOfDialog;
+ }
+
+ static dialogTitle(typeOfDialog) {
+ if(typeOfDialog === 'backup_objects') {
+ return null;
+ }
+ return ((typeOfDialog === 'globals') ?
+ gettext('Backup Globals...') :
+ gettext('Backup Server...'));
+ }
+
+ dialogName(typeOfDialog) {
+ if(typeOfDialog === 'backup_objects') {
+ return typeOfDialog;
+ }
+ return 'BackupDialog_' + typeOfDialog;
+ }
+}
diff --git a/web/pgadmin/static/js/backup/backup_dialog_wrapper.js b/web/pgadmin/static/js/backup/backup_dialog_wrapper.js
new file mode 100644
index 00000000..7b6250f7
--- /dev/null
+++ b/web/pgadmin/static/js/backup/backup_dialog_wrapper.js
@@ -0,0 +1,258 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {getTreeNodeHierarchyFromElement} from '../tree/pgadmin_tree_node';
+import axios from 'axios/index';
+import gettext from '../gettext';
+import url_for from '../url_for';
+import _ from 'underscore';
+import {DialogWrapper} from '../alertify/dialog_wrapper';
+
+export class BackupDialogWrapper extends DialogWrapper {
+ constructor(dialogContainerSelector, dialogTitle, typeOfDialog,
+ jquery, pgBrowser, alertify, dialogModel, backform) {
+ super(dialogContainerSelector, dialogTitle, jquery,
+ pgBrowser, alertify, dialogModel, backform);
+ this.typeOfDialog = typeOfDialog;
+ }
+
+ main(title) {
+ this.set('title', title);
+ }
+
+ setup() {
+ return {
+ buttons: [{
+ text: '',
+ className: 'btn btn-default pull-left fa fa-lg fa-info',
+ attrs: {
+ name: 'object_help',
+ type: 'button',
+ url: 'backup.html',
+ label: gettext('Backup'),
+ },
+ }, {
+ text: '',
+ key: 112,
+ className: 'btn btn-default pull-left fa fa-lg fa-question',
+ attrs: {
+ name: 'dialog_help',
+ type: 'button',
+ label: gettext('Backup'),
+ url: url_for('help.static', {
+ 'filename': 'backup_dialog.html',
+ }),
+ },
+ }, {
+ text: gettext('Backup'),
+ key: 13,
+ className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
+ 'data-btn-name': 'backup',
+ }, {
+ text: gettext('Cancel'),
+ key: 27,
+ className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
+ 'data-btn-name': 'cancel',
+ }],
+ // Set options for dialog
+ options: {
+ title: this.dialogTitle,
+ //disable both padding and overflow control.
+ padding: !1,
+ overflow: !1,
+ model: 0,
+ resizable: true,
+ maximizable: true,
+ pinnable: false,
+ closableByDimmer: false,
+ modal: false,
+ },
+ };
+ }
+
+ prepare() {
+ this.disableBackupButton();
+
+ const $container = this.jquery(this.dialogContainerSelector);
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = this.getSelectedNodeData(selectedTreeNode);
+ if (!selectedTreeNodeData) {
+ return;
+ }
+
+ const node = this.pgBrowser.Nodes[selectedTreeNodeData._type];
+ if (this.dialogTitle === null) {
+ const title = `Backup (${node.label}: ${selectedTreeNodeData.label})`;
+ this.main(title);
+ }
+
+ const treeInfo = getTreeNodeHierarchyFromElement(this.pgBrowser, selectedTreeNode);
+ const dialog = this.createDialog(node, treeInfo, this.typeOfDialog, $container);
+ this.addAlertifyClassToBackupNodeChildNodes();
+ dialog.render();
+
+ this.elements.content.appendChild($container.get(0));
+
+ this.focusOnDialog(dialog);
+ this.setListenersForFilenameChanges();
+ }
+
+ callback(event) {
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = this.getSelectedNodeData(selectedTreeNode);
+ const node = selectedTreeNodeData && this.pgBrowser.Nodes[selectedTreeNodeData._type];
+
+ if (this.wasHelpButtonPressed(event)) {
+ event.cancel = true;
+ this.pgBrowser.showHelp(
+ event.button.element.name,
+ event.button.element.getAttribute('url'),
+ node,
+ selectedTreeNode,
+ event.button.element.getAttribute('label')
+ );
+ return;
+ }
+
+ if (this.wasBackupButtonPressed(event)) {
+
+ if (!selectedTreeNodeData)
+ return;
+
+ const serverIdentifier = this.retrieveServerIdentifier(node, selectedTreeNode);
+
+ const dialog = this;
+ let urlShortcut = 'backup.create_server_job';
+ if (this.typeOfDialog === 'backup_objects') {
+ urlShortcut = 'backup.create_object_job';
+ }
+ const baseUrl = url_for(urlShortcut, {
+ 'sid': serverIdentifier,
+ });
+
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.pgBrowser,
+ selectedTreeNode
+ );
+
+ this.setExtraParameters(selectedTreeNode, treeInfo);
+
+ let service = axios.create({});
+ service.post(
+ baseUrl,
+ this.view.model.toJSON()
+ ).then(function () {
+ dialog.alertify.success(gettext('Backup job created.'), 5);
+ dialog.pgBrowser.Events.trigger('pgadmin-bgprocess:created', dialog);
+ }).catch(function (error) {
+ try {
+ const err = error.response.data;
+ dialog.alertify.alert(
+ gettext('Backup job failed.'),
+ err.errormsg
+ );
+ } catch (e) {
+ console.warn(e.stack || e);
+ }
+ });
+ }
+ }
+
+ addAlertifyClassToBackupNodeChildNodes() {
+ this.jquery(this.elements.body.childNodes[0]).addClass(
+ 'alertify_tools_dialog_properties obj_properties'
+ );
+ }
+
+ getSelectedNode() {
+ const tree = this.pgBrowser.treeMenu;
+ const selectedNode = tree.selected();
+ if (selectedNode) {
+ return tree.findNodeByDomElement(selectedNode);
+ } else {
+ return undefined;
+ }
+ }
+
+ disableBackupButton() {
+ this.__internal.buttons[2].element.disabled = true;
+ }
+
+ enableBackupButton() {
+ this.__internal.buttons[2].element.disabled = false;
+ }
+
+ createDialog(node, treeInfo, typeOfDialog, $container) {
+ let attributes = {};
+ if (typeOfDialog !== 'backup_objects') {
+ attributes['type'] = typeOfDialog;
+ }
+ // Instance of backbone model
+ const newModel = new this.dialogModel(attributes, {
+ node_info: treeInfo,
+ });
+ const fields = this.backform.generateViewSchema(
+ treeInfo, newModel, 'create', node, treeInfo.server, true
+ );
+
+ return this.view = new this.backform.Dialog({
+ el: $container,
+ model: newModel,
+ schema: fields,
+ });
+ }
+
+ retrieveServerIdentifier(node, selectedTreeNode) {
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.pgBrowser,
+ selectedTreeNode
+ );
+ return treeInfo.server._id;
+ }
+
+ setListenersForFilenameChanges() {
+ const self = this;
+
+ this.view.model.on('change', function () {
+ if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
+ this.errorModel.clear();
+ self.enableBackupButton();
+ } else {
+ self.disableBackupButton();
+ this.errorModel.set('file', gettext('Please provide a filename'));
+ }
+ });
+ }
+
+ setExtraParameters(selectedTreeNode, treeInfo) {
+ if (this.typeOfDialog === 'backup_objects') {
+
+ this.view.model.set('database', treeInfo.database._label);
+
+ const nodeData = selectedTreeNode.getData();
+ if (nodeData._type === 'schema') {
+ this.view.model.set('schemas', [nodeData._label]);
+ }
+
+ if (nodeData._type === 'table') {
+ this.view.model.set('tables', [
+ [treeInfo.schema._label, nodeData._label],
+ ]);
+ }
+
+ if (_.isEmpty(this.view.model.get('ratio'))) {
+ this.view.model.unset('ratio');
+ }
+ }
+ }
+
+ wasBackupButtonPressed(event) {
+ return event.button['data-btn-name'] === 'backup';
+ }
+}
diff --git a/web/pgadmin/static/js/backup/menu_utils.js b/web/pgadmin/static/js/backup/menu_utils.js
new file mode 100644
index 00000000..d3c1b143
--- /dev/null
+++ b/web/pgadmin/static/js/backup/menu_utils.js
@@ -0,0 +1,23 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {isProvidedDataValid} from '../menu/menu_enabled';
+
+export const backupSupportedNodes = [
+ 'database', 'schema', 'table', 'partition',
+];
+
+function isNodeAServerAndConnected(treeNodeData) {
+ return (('server' === treeNodeData._type) && treeNodeData.connected);
+}
+
+export function menuEnabledServer(treeNodeData) {
+ return isProvidedDataValid(treeNodeData)
+ && isNodeAServerAndConnected(treeNodeData);
+}
diff --git a/web/pgadmin/static/js/datagrid/get_panel_title.js b/web/pgadmin/static/js/datagrid/get_panel_title.js
new file mode 100644
index 00000000..f5a5664d
--- /dev/null
+++ b/web/pgadmin/static/js/datagrid/get_panel_title.js
@@ -0,0 +1,33 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {getTreeNodeHierarchyFromIdentifier} from '../tree/pgadmin_tree_node';
+
+function getDatabaseLabel(parentData) {
+ return parentData.database ? parentData.database.label
+ : parentData.server.db;
+}
+
+function isServerInformationAvailable(parentData) {
+ return parentData.server === undefined;
+}
+
+export function getPanelTitle(pgBrowser) {
+ const selected_item = pgBrowser.treeMenu.selected();
+
+ const parentData = getTreeNodeHierarchyFromIdentifier
+ .call(pgBrowser, selected_item);
+ if (isServerInformationAvailable(parentData)) {
+ return;
+ }
+
+ const db_label = getDatabaseLabel(parentData);
+
+ return `${db_label} on ${parentData.server.user.name}@${parentData.server.label}`;
+}
diff --git a/web/pgadmin/static/js/datagrid/show_data.js b/web/pgadmin/static/js/datagrid/show_data.js
new file mode 100644
index 00000000..a414fd77
--- /dev/null
+++ b/web/pgadmin/static/js/datagrid/show_data.js
@@ -0,0 +1,92 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import gettext from '../gettext';
+import url_for from '../url_for';
+import {getTreeNodeHierarchyFromIdentifier} from '../../../static/js/tree/pgadmin_tree_node';
+
+export function showDataGrid(
+ datagrid,
+ pgBrowser,
+ alertify,
+ connectionData,
+ aciTreeIdentifier
+) {
+ const node = pgBrowser.treeMenu.findNodeByDomElement(aciTreeIdentifier);
+ if (node === undefined || !node.getData()) {
+ alertify.alert(
+ gettext('Data Grid Error'),
+ gettext('No object selected.')
+ );
+ return;
+ }
+
+ const parentData = getTreeNodeHierarchyFromIdentifier.call(
+ pgBrowser,
+ aciTreeIdentifier
+ );
+
+ if (hasServerOrDatabaseConfiguration(parentData)
+ || !hasSchemaOrCatalogOrViewInformation(parentData)) {
+ return;
+ }
+
+ let namespaceName = retrieveNameSpaceName(parentData);
+ const baseUrl = generateUrl(connectionData, node.getData(), parentData);
+ const grid_title = generateDatagridTitle(parentData, namespaceName, node.getData());
+
+ datagrid.create_transaction(
+ baseUrl,
+ null,
+ 'false',
+ parentData.server.server_type,
+ '',
+ grid_title,
+ ''
+ );
+}
+
+
+function retrieveNameSpaceName(parentData) {
+ if (parentData.schema !== undefined) {
+ return parentData.schema.label;
+ }
+ else if (parentData.view !== undefined) {
+ return parentData.view.label;
+ }
+ else if (parentData.catalog !== undefined) {
+ return parentData.catalog.label;
+ }
+ return '';
+}
+
+function generateUrl(connectionData, nodeData, parentData) {
+ const url_params = {
+ 'cmd_type': connectionData.mnuid,
+ 'obj_type': nodeData._type,
+ 'sgid': parentData.server_group._id,
+ 'sid': parentData.server._id,
+ 'did': parentData.database._id,
+ 'obj_id': nodeData._id,
+ };
+
+ return url_for('datagrid.initialize_datagrid', url_params);
+}
+
+function hasServerOrDatabaseConfiguration(parentData) {
+ return parentData.server === undefined || parentData.database === undefined;
+}
+
+function hasSchemaOrCatalogOrViewInformation(parentData) {
+ return parentData.schema !== undefined || parentData.view !== undefined ||
+ parentData.catalog !== undefined;
+}
+
+function generateDatagridTitle(parentData, namespaceName, nodeData) {
+ return `${parentData.server.label} - ${parentData.database.label} - ${namespaceName}.${nodeData.label}`;
+}
diff --git a/web/pgadmin/static/js/datagrid/show_query_tool.js b/web/pgadmin/static/js/datagrid/show_query_tool.js
new file mode 100644
index 00000000..0436e0fd
--- /dev/null
+++ b/web/pgadmin/static/js/datagrid/show_query_tool.js
@@ -0,0 +1,63 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../gettext';
+import url_for from '../url_for';
+import {getTreeNodeHierarchyFromIdentifier} from '../../../static/js/tree/pgadmin_tree_node';
+
+function hasDatabaseInformation(parentData) {
+ return parentData.database;
+}
+
+function generateUrl(parentData) {
+ let url_endpoint = 'datagrid.initialize_query_tool';
+ let url_params = {
+ 'sgid': parentData.server_group._id,
+ 'sid': parentData.server._id,
+ };
+
+ if (hasDatabaseInformation(parentData)) {
+ url_params['did'] = parentData.database._id;
+ url_endpoint = 'datagrid.initialize_query_tool_with_did';
+ }
+
+ return url_for(url_endpoint, url_params);
+}
+
+function hasServerInformations(parentData) {
+ return parentData.server === undefined;
+}
+
+export function showQueryTool(datagrid, pgBrowser, alertify, url,
+ aciTreeIdentifier, panelTitle) {
+ const sURL = url || '';
+ const queryToolTitle = panelTitle || '';
+
+ const currentNode = pgBrowser.treeMenu.findNodeByDomElement(aciTreeIdentifier);
+ if (currentNode === undefined) {
+ alertify.alert(
+ gettext('Query Tool Error'),
+ gettext('No object selected.')
+ );
+ return;
+ }
+
+ const parentData = getTreeNodeHierarchyFromIdentifier.call(
+ pgBrowser, aciTreeIdentifier);
+
+ if (hasServerInformations(parentData)) {
+ return;
+ }
+
+ const baseUrl = generateUrl(parentData);
+
+ datagrid.create_transaction(
+ baseUrl, null, 'true',
+ parentData.server.server_type, sURL, queryToolTitle, '', false);
+}
diff --git a/web/pgadmin/static/js/grant/wizard/menu_utils.js b/web/pgadmin/static/js/grant/wizard/menu_utils.js
new file mode 100644
index 00000000..b56ce3cd
--- /dev/null
+++ b/web/pgadmin/static/js/grant/wizard/menu_utils.js
@@ -0,0 +1,16 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export const supportedNodes = [
+ 'schema', 'coll-function', 'coll-sequence',
+ 'coll-table', 'coll-view', 'coll-procedure',
+ 'coll-mview', 'database', 'coll-trigger_function',
+];
+
+
diff --git a/web/pgadmin/static/js/maintenance/menu_utils.js b/web/pgadmin/static/js/maintenance/menu_utils.js
new file mode 100644
index 00000000..8cde1baa
--- /dev/null
+++ b/web/pgadmin/static/js/maintenance/menu_utils.js
@@ -0,0 +1,13 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export const maintenanceSupportedNodes = [
+ 'database', 'table', 'primary_key',
+ 'unique_constraint', 'index', 'partition',
+];
diff --git a/web/pgadmin/static/js/menu/can_create.js b/web/pgadmin/static/js/menu/can_create.js
new file mode 100644
index 00000000..476be437
--- /dev/null
+++ b/web/pgadmin/static/js/menu/can_create.js
@@ -0,0 +1,37 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+/////////////////////////////////////////////////////////////
+
+export function canCreate(pgBrowser, childOfCatalogType, item, data) {
+ //If check is false then , we will allow create menu
+ if (data && data.check === false) {
+ return true;
+ }
+
+ let node = pgBrowser.treeMenu.findNodeByDomElement(item);
+
+ if (node.anyFamilyMember(parentCatalogOfTableChild.bind(null, childOfCatalogType))) {
+ return false;
+ }
+
+ return true;
+}
+
+function parentCatalogOfTableChild(arg, node) {
+ if (arg === node.getData()._type) {
+ if (node.hasParent()) {
+
+ let parent = node.parent();
+ if ('catalog' === parent.getData()._type) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
diff --git a/web/pgadmin/static/js/menu/menu_enabled.js b/web/pgadmin/static/js/menu/menu_enabled.js
new file mode 100644
index 00000000..05ff0f4a
--- /dev/null
+++ b/web/pgadmin/static/js/menu/menu_enabled.js
@@ -0,0 +1,52 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+function isNodeTypeSupported(backupSupportedNodes, nodeDataType, treeNode) {
+ return _.indexOf(backupSupportedNodes, nodeDataType) !== -1
+ && ancestorWithTypeCatalogDoesNotExists(treeNode);
+}
+
+export function isProvidedDataValid(treeNodeData) {
+ return !_.isUndefined(treeNodeData) && !_.isNull(treeNodeData);
+}
+
+function doesNodeHaveMenu(treeNodeData) {
+ return (treeNodeData._type === 'database' && treeNodeData.allowConn)
+ || treeNodeData._type !== 'database';
+}
+
+function ancestorWithTypeCatalogDoesNotExists(treeNode) {
+ let currentNode = treeNode;
+
+ while(currentNode.hasParent() && treeNode.parent().getData() !== null) {
+ if(currentNode.parent().getData()._type === 'catalog') {
+ return false;
+ }
+
+ currentNode = currentNode.parent();
+ }
+
+ return true;
+}
+
+export function menuEnabled(tree, backupSupportedNodes, treeNodeData, domTreeNode) {
+ let treeNode = tree.findNodeByDomElement(domTreeNode);
+ if (!treeNode) {
+ return false;
+ }
+
+ if (isProvidedDataValid(treeNodeData)) {
+ return isNodeTypeSupported(backupSupportedNodes, treeNodeData._type, treeNode)
+ && doesNodeHaveMenu(treeNodeData);
+ } else {
+ return false;
+ }
+}
+
+
diff --git a/web/pgadmin/static/js/restore/menu_utils.js b/web/pgadmin/static/js/restore/menu_utils.js
new file mode 100644
index 00000000..2d35c951
--- /dev/null
+++ b/web/pgadmin/static/js/restore/menu_utils.js
@@ -0,0 +1,18 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export const restoreSupportedNodes = [
+ 'database',
+ 'schema',
+ 'table',
+ 'function',
+ 'trigger',
+ 'index',
+ 'partition',
+];
diff --git a/web/pgadmin/static/js/restore/restore_dialog.js b/web/pgadmin/static/js/restore/restore_dialog.js
new file mode 100644
index 00000000..237b8412
--- /dev/null
+++ b/web/pgadmin/static/js/restore/restore_dialog.js
@@ -0,0 +1,53 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../gettext';
+import {sprintf} from 'sprintf-js';
+import Backform from '../backform.pgadmin';
+import {Dialog} from '../alertify/dialog';
+
+export class RestoreDialog extends Dialog {
+ constructor(pgBrowser, $, alertify, RestoreModel, backform = Backform) {
+ super('Restore Error',
+ '<div class=\'restore_dialog\'></div>',
+ pgBrowser, $, alertify, RestoreModel, backform);
+ }
+
+ draw(action, aciTreeItem) {
+
+ const serverInformation = this.retrieveAncestorOfTypeServer(aciTreeItem);
+
+ if (!serverInformation) {
+ return;
+ }
+
+ if (!this.hasBinariesConfiguration(serverInformation)) {
+ return;
+ }
+
+ let aciTreeItem1 = aciTreeItem || this.pgBrowser.treeMenu.selected();
+ let item = this.pgBrowser.treeMenu.findNodeByDomElement(aciTreeItem1);
+ const data = item.getData();
+ const node = this.pgBrowser.Nodes[data._type];
+
+ if (!node)
+ return;
+
+ let title = sprintf(gettext('Restore (%s: %s)'), node.label, data.label);
+
+ this.createOrGetDialog(title, 'restore');
+
+ this.alertify.pg_restore(title, aciTreeItem1, data, node).resizeTo('65%', '60%');
+ }
+
+ dialogName() {
+ return 'pg_restore';
+ }
+}
+
diff --git a/web/pgadmin/static/js/restore/restore_dialog_wrapper.js b/web/pgadmin/static/js/restore/restore_dialog_wrapper.js
new file mode 100644
index 00000000..13d781fb
--- /dev/null
+++ b/web/pgadmin/static/js/restore/restore_dialog_wrapper.js
@@ -0,0 +1,255 @@
+import {getTreeNodeHierarchyFromElement} from '../tree/pgadmin_tree_node';
+import axios from 'axios/index';
+import _ from 'underscore';
+import gettext from '../gettext';
+import url_for from '../url_for';
+import {DialogWrapper} from '../alertify/dialog_wrapper';
+
+export class RestoreDialogWrapper extends DialogWrapper {
+ constructor(dialogContainerSelector, dialogTitle, typeOfDialog,
+ jquery, pgBrowser, alertify, dialogModel, backform) {
+ super(dialogContainerSelector, dialogTitle, jquery,
+ pgBrowser, alertify, dialogModel, backform);
+ }
+
+ main(title, item, data, node) {
+ this.set('title', title);
+ this.setting('pg_node', node);
+ this.setting('pg_item', item);
+ this.setting('pg_item_data', data);
+ }
+
+ setup() {
+ return {
+ buttons: [{
+ text: '',
+ className: 'btn btn-default pull-left fa fa-lg fa-info',
+ attrs: {
+ name: 'object_help',
+ type: 'button',
+ url: 'backup.html',
+ label: gettext('Restore'),
+ },
+ }, {
+ text: '',
+ key: 112,
+ className: 'btn btn-default pull-left fa fa-lg fa-question',
+ attrs: {
+ name: 'dialog_help',
+ type: 'button',
+ label: gettext('Restore'),
+ url: url_for('help.static', {
+ 'filename': 'restore_dialog.html',
+ }),
+ },
+ }, {
+ text: gettext('Restore'),
+ key: 13,
+ className: 'btn btn-primary fa fa-upload pg-alertify-button',
+ restore: true,
+ 'data-btn-name': 'restore',
+ }, {
+ text: gettext('Cancel'),
+ key: 27,
+ className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
+ restore: false,
+ 'data-btn-name': 'cancel',
+ }],
+ // Set options for dialog
+ options: {
+ title: this.dialogTitle,
+ //disable both padding and overflow control.
+ padding: !1,
+ overflow: !1,
+ model: 0,
+ resizable: true,
+ maximizable: true,
+ pinnable: false,
+ closableByDimmer: false,
+ modal: false,
+ },
+ };
+ }
+
+ prepare() {
+ this.disableRestoreButton();
+
+ const $container = this.jquery(this.dialogContainerSelector);
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = this.getSelectedNodeData(selectedTreeNode);
+ if (!selectedTreeNodeData) {
+ return;
+ }
+
+ const node = this.pgBrowser.Nodes[selectedTreeNodeData._type];
+
+ const treeInfo = getTreeNodeHierarchyFromElement(this.pgBrowser, selectedTreeNode);
+ const dialog = this.createDialog(node, treeInfo, $container);
+ this.addAlertifyClassToRestoreNodeChildNodes();
+ dialog.render();
+
+ this.elements.content.appendChild($container.get(0));
+
+ this.focusOnDialog(dialog);
+ this.setListenersForFilenameChanges();
+ }
+
+ callback(event) {
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = this.getSelectedNodeData(selectedTreeNode);
+ const node = selectedTreeNodeData && this.pgBrowser.Nodes[selectedTreeNodeData._type];
+
+ if (this.wasHelpButtonPressed(event)) {
+ event.cancel = true;
+ this.pgBrowser.showHelp(
+ event.button.element.name,
+ event.button.element.getAttribute('url'),
+ node,
+ selectedTreeNode,
+ event.button.element.getAttribute('label')
+ );
+ return;
+ }
+
+ if (this.wasRestoreButtonPressed(event)) {
+
+ if (!selectedTreeNodeData)
+ return;
+
+ const serverIdentifier = this.retrieveServerIdentifier(node, selectedTreeNode);
+
+ const dialogWrapper = this;
+ let urlShortcut = 'restore.create_job';
+
+ const baseUrl = url_for(urlShortcut, {
+ 'sid': serverIdentifier,
+ });
+
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.pgBrowser,
+ selectedTreeNode
+ );
+
+ this.setExtraParameters(selectedTreeNode, treeInfo);
+
+ let service = axios.create({});
+ service.post(
+ baseUrl,
+ this.view.model.toJSON()
+ ).then(function () {
+ dialogWrapper.alertify.success(gettext('Restore job created.'), 5);
+ dialogWrapper.pgBrowser.Events.trigger('pgadmin-bgprocess:created', dialogWrapper);
+ }).catch(function (error) {
+ try {
+ const err = error.response.data;
+ dialogWrapper.alertify.alert(
+ gettext('Restore job failed.'),
+ err.errormsg
+ );
+ } catch (e) {
+ console.warn(e.stack || e);
+ }
+ });
+ }
+ }
+
+ addAlertifyClassToRestoreNodeChildNodes() {
+ this.jquery(this.elements.body.childNodes[0]).addClass(
+ 'alertify_tools_dialog_properties obj_properties'
+ );
+ }
+
+ getSelectedNode() {
+ const tree = this.pgBrowser.treeMenu;
+ const selectedNode = tree.selected();
+ if (selectedNode) {
+ return tree.findNodeByDomElement(selectedNode);
+ } else {
+ return undefined;
+ }
+ }
+
+ disableRestoreButton() {
+ this.__internal.buttons[2].element.disabled = true;
+ }
+
+ enableRestoreButton() {
+ this.__internal.buttons[2].element.disabled = false;
+ }
+
+ createDialog(node, treeInfo, $container) {
+ const newModel = new this.dialogModel({
+ node_data: node,
+ }, {
+ node_info: treeInfo,
+ });
+ const fields = this.backform.generateViewSchema(
+ treeInfo, newModel, 'create', node, treeInfo.server, true
+ );
+
+ return this.view = new this.backform.Dialog({
+ el: $container,
+ model: newModel,
+ schema: fields,
+ });
+ }
+
+ retrieveServerIdentifier(node, selectedTreeNode) {
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.pgBrowser,
+ selectedTreeNode
+ );
+ return treeInfo.server._id;
+ }
+
+ setListenersForFilenameChanges() {
+ const self = this;
+
+ this.view.model.on('change', function () {
+ if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
+ this.errorModel.clear();
+ self.enableRestoreButton();
+ } else {
+ self.disableRestoreButton();
+ this.errorModel.set('file', gettext('Please provide a filename'));
+ }
+ });
+ }
+
+ setExtraParameters(selectedTreeNode, treeInfo) {
+ this.view.model.set('database', treeInfo.database._label);
+ if (!this.view.model.get('custom')) {
+ const nodeData = selectedTreeNode.getData();
+
+ switch (nodeData._type) {
+ case 'schema':
+ this.view.model.set('schemas', [nodeData._label]);
+ break;
+ case 'table':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('tables', [nodeData._label]);
+ break;
+ case 'function':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('functions', [nodeData._label]);
+ break;
+ case 'index':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('indexes', [nodeData._label]);
+ break;
+ case 'trigger':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('triggers', [nodeData._label]);
+ break;
+ case 'trigger_func':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('trigger_funcs', [nodeData._label]);
+ break;
+ }
+ }
+ }
+
+ wasRestoreButtonPressed(event) {
+ return event.button['data-btn-name'] === 'restore';
+ }
+}
diff --git a/web/pgadmin/static/js/schema/can_drop_child.js b/web/pgadmin/static/js/schema/can_drop_child.js
new file mode 100644
index 00000000..84e25b5c
--- /dev/null
+++ b/web/pgadmin/static/js/schema/can_drop_child.js
@@ -0,0 +1,18 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export function canDropChild(pgBrowser, itemData, item) {
+ let node = pgBrowser.treeMenu.findNodeByDomElement(item);
+
+ if (node.anyParent((parent) => parent.getData()._type === 'catalog')) {
+ return false;
+ }
+
+ return true;
+}
diff --git a/web/pgadmin/static/js/table/enable_disable_triggers.js b/web/pgadmin/static/js/table/enable_disable_triggers.js
new file mode 100644
index 00000000..2d792043
--- /dev/null
+++ b/web/pgadmin/static/js/table/enable_disable_triggers.js
@@ -0,0 +1,52 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import axios from 'axios';
+
+export function disableTriggers(tree, alertify, generateUrl, args) {
+ return setTriggers(tree, alertify, generateUrl, args, {enable: 'false' });
+}
+export function enableTriggers(tree, alertify, generateUrl, args) {
+ return setTriggers(tree, alertify, generateUrl, args, {enable: 'true' });
+}
+
+function setTriggers(tree, alertify, generateUrl, args, params) {
+ const treeNode = retrieveTreeNode(args, tree);
+
+ if (!treeNode || treeNode.getData() === null || treeNode.getData() === undefined)
+ return false;
+
+ axios.put(
+ generateUrl(treeNode.getHtmlIdentifier(), 'set_trigger', treeNode.getData(), true),
+ params
+ )
+ .then((res) => {
+ if (res.data.success === 1) {
+ alertify.success(res.data.info);
+ treeNode.reload(tree);
+ }
+ })
+ .catch((xhr) => {
+ try {
+ const err = xhr.response.data;
+ if (err.success === 0) {
+ alertify.error(err.errormsg);
+ }
+ } catch (e) {
+ console.warn(e.stack || e);
+ }
+ treeNode.unload(tree);
+ });
+}
+
+function retrieveTreeNode(args, tree) {
+ const input = args || {};
+ const domElementIdentifier = input.item || tree.selected();
+ return tree.findNodeByDomElement(domElementIdentifier);
+}
diff --git a/web/pgadmin/static/js/table/index.js b/web/pgadmin/static/js/table/index.js
new file mode 100644
index 00000000..40d0c22d
--- /dev/null
+++ b/web/pgadmin/static/js/table/index.js
@@ -0,0 +1,10 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export * from './enable_disable_triggers';
diff --git a/web/pgadmin/static/js/tree/pgadmin_tree_node.js b/web/pgadmin/static/js/tree/pgadmin_tree_node.js
new file mode 100644
index 00000000..bccbe587
--- /dev/null
+++ b/web/pgadmin/static/js/tree/pgadmin_tree_node.js
@@ -0,0 +1,43 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+export function getTreeNodeHierarchyFromElement(pgBrowser, treeNode) {
+ return getTreeNodeHierarchy.call(pgBrowser, treeNode);
+}
+
+export function getTreeNodeHierarchyFromIdentifier(aciTreeNodeIdentifier) {
+ let identifier = this.treeMenu.translateTreeNodeIdFromACITree(aciTreeNodeIdentifier);
+ let currentNode = this.treeMenu.findNode(identifier);
+ return getTreeNodeHierarchy.call(this, currentNode);
+}
+
+export function getTreeNodeHierarchy(currentNode) {
+ let idx = 0;
+ let result = {};
+
+ do {
+ const currentNodeData = currentNode.getData();
+ if (currentNodeData._type in this.Nodes && this.Nodes[currentNodeData._type].hasId) {
+ const nodeType = mapType(currentNodeData._type);
+ if (result[nodeType] === undefined) {
+ result[nodeType] = _.extend({}, currentNodeData, {
+ 'priority': idx,
+ });
+ idx -= 1;
+ }
+ }
+ currentNode = currentNode.hasParent() ? currentNode.parent() : null;
+ } while (currentNode);
+
+ return result;
+}
+
+function mapType(type) {
+ return type === 'partition' ? 'table' : type;
+}
diff --git a/web/pgadmin/tools/backup/static/js/backup.js b/web/pgadmin/tools/backup/static/js/backup.js
index ddbfaee4..6459beaf 100644
--- a/web/pgadmin/tools/backup/static/js/backup.js
+++ b/web/pgadmin/tools/backup/static/js/backup.js
@@ -3,9 +3,11 @@ define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'pgadmin.alertifyjs', 'backbone', 'pgadmin.backgrid',
'pgadmin.backform', 'pgadmin.browser', 'sources/utils',
+ 'sources/backup/menu_utils', 'sources/backup/backup_dialog',
+ 'sources/menu/menu_enabled',
], function(
gettext, url_for, $, _, S, alertify, Backbone, Backgrid, Backform, pgBrowser,
-commonUtils
+commonUtils, menuUtils, globalBackupDialog, menuEnabled
) {
// if module is already initialized, refer to that.
@@ -394,48 +396,6 @@ commonUtils
this.initialized = true;
- // Define list of nodes on which backup context menu option appears
- var backup_supported_nodes = [
- 'database', 'schema', 'table', 'partition',
- ];
-
- /**
- Enable/disable backup menu in tools based
- on node selected
- if selected node is present in supported_nodes,
- menu will be enabled otherwise disabled.
- Also, hide it for system view in catalogs
- */
- var menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData,
- parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
-
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(backup_supported_nodes, d._type) !== -1 &&
- parent_data._type != 'catalog') {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
- var menu_enabled_server = function(itemData) {
- // If server node selected && connected
- if (!_.isUndefined(itemData) && !_.isNull(itemData))
- return (('server' === itemData._type) && itemData.connected);
- else
- return false;
- };
-
// Define the nodes on which the menus to be appear
var menus = [{
name: 'backup_global',
@@ -445,7 +405,7 @@ commonUtils
priority: 12,
label: gettext('Backup Globals...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_server',
module: this,
@@ -454,7 +414,7 @@ commonUtils
priority: 12,
label: gettext('Backup Server...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_global_ctx',
module: this,
@@ -464,7 +424,7 @@ commonUtils
priority: 12,
label: gettext('Backup Globals...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_server_ctx',
module: this,
@@ -474,7 +434,7 @@ commonUtils
priority: 12,
label: gettext('Backup Server...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_object',
module: this,
@@ -483,20 +443,22 @@ commonUtils
priority: 11,
label: gettext('Backup...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null,
+ pgBrowser.treeMenu, menuUtils.backupSupportedNodes),
}];
- for (var idx = 0; idx < backup_supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.backupSupportedNodes.length; idx++) {
menus.push({
- name: 'backup_' + backup_supported_nodes[idx],
- node: backup_supported_nodes[idx],
+ name: 'backup_' + menuUtils.backupSupportedNodes[idx],
+ node: menuUtils.backupSupportedNodes[idx],
module: this,
applies: ['context'],
callback: 'backup_objects',
priority: 11,
label: gettext('Backup...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null,
+ pgBrowser.treeMenu, menuUtils.backupSupportedNodes),
});
}
@@ -521,531 +483,25 @@ commonUtils
},
// Callback to draw Backup Dialog for globals/server
- start_backup_global_server: function(action, item, params) {
- var i = item || pgBrowser.tree.selected(),
- server_data = null;
-
- while (i) {
- var node_data = pgBrowser.tree.itemData(i);
- if (node_data._type == 'server') {
- server_data = node_data;
- break;
- }
-
- if (pgBrowser.tree.hasParent(i)) {
- i = $(pgBrowser.tree.parent(i));
- } else {
- alertify.alert(
- gettext('Backup Error'),
- gettext('Please select server or child node from the browser tree.')
- );
- break;
- }
- }
-
- if (!server_data) {
- return;
- }
-
- var module = 'paths',
- preference_name = 'pg_bin_dir',
- msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
-
- if ((server_data.type && server_data.type == 'ppas') ||
- server_data.server_type == 'ppas') {
- preference_name = 'ppas_bin_dir';
- msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
- }
-
- var preference = pgBrowser.get_preference(module, preference_name);
-
- if (preference) {
- if (!preference.value) {
- alertify.alert(gettext('Configuration required'), msg);
- return;
- }
- } else {
- alertify.alert(
- gettext('Backup Error'),
- S(gettext('Failed to load preference %s of module %s')).sprintf(preference_name, module).value()
- );
- return;
- }
-
- var of_type = undefined;
-
- // Set Notes according to type of backup
- if (!_.isUndefined(params['globals']) && params['globals']) {
- of_type = 'globals';
- } else {
- of_type = 'server';
- }
-
- var DialogName = 'BackupDialog_' + of_type,
- DialogTitle = ((of_type == 'globals') ?
- gettext('Backup Globals...') :
- gettext('Backup Server...'));
-
- if (!alertify[DialogName]) {
- alertify.dialog(DialogName, function factory() {
- return {
- main: function(title) {
- this.set('title', title);
- },
- build: function() {
- alertify.pgDialogBuild.apply(this);
- },
- setup: function() {
- return {
- buttons: [{
- text: '',
- className: 'btn btn-default pull-left fa fa-lg fa-info',
- attrs: {
- name: 'object_help',
- type: 'button',
- url: 'backup.html',
- label: gettext('Backup'),
- },
- }, {
- text: '',
- key: 112,
- className: 'btn btn-default pull-left fa fa-lg fa-question',
- attrs: {
- name: 'dialog_help',
- type: 'button',
- label: gettext('Backup'),
- url: url_for('help.static', {
- 'filename': 'backup_dialog.html',
- }),
- },
- }, {
- text: gettext('Backup'),
- key: 13,
- className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
- 'data-btn-name': 'backup',
- }, {
- text: gettext('Cancel'),
- key: 27,
- className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
- 'data-btn-name': 'cancel',
- }],
- // Set options for dialog
- options: {
- title: DialogTitle,
- //disable both padding and overflow control.
- padding: !1,
- overflow: !1,
- model: 0,
- resizable: true,
- maximizable: true,
- pinnable: false,
- closableByDimmer: false,
- modal: false,
- },
- };
- },
- hooks: {
- // Triggered when the dialog is closed
- onclose: function() {
- if (this.view) {
- // clear our backform model/view
- this.view.remove({
- data: true,
- internal: true,
- silent: true,
- });
- }
- },
- },
- prepare: function() {
- var self = this;
- // Disable Backup button until user provides Filename
- this.__internal.buttons[2].element.disabled = true;
-
- var $container = $('<div class=\'backup_dialog\'></div>');
- // Find current/selected node
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (!d)
- return;
- // Create treeInfo
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
- // Instance of backbone model
- var newModel = new BackupModel({
- type: of_type,
- }, {
- node_info: treeInfo,
- }),
- fields = Backform.generateViewSchema(
- treeInfo, newModel, 'create', node, treeInfo.server, true
- );
-
- var view = this.view = new Backform.Dialog({
- el: $container,
- model: newModel,
- schema: fields,
- });
- // Add our class to alertify
- $(this.elements.body.childNodes[0]).addClass(
- 'alertify_tools_dialog_properties obj_properties'
- );
- // Render dialog
- view.render();
-
- this.elements.content.appendChild($container.get(0));
-
- var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
- commonUtils.findAndSetFocus(container);
-
- // Listen to model & if filename is provided then enable Backup button
- this.view.model.on('change', function() {
- if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
- this.errorModel.clear();
- self.__internal.buttons[2].element.disabled = false;
- } else {
- self.__internal.buttons[2].element.disabled = true;
- this.errorModel.set('file', gettext('Please provide a filename'));
- }
- });
- },
- // Callback functions when click on the buttons of the Alertify dialogs
- callback: function(e) {
- // Fetch current server id
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (e.button.element.name == 'dialog_help' || e.button.element.name == 'object_help') {
- e.cancel = true;
- pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
- node, i, e.button.element.getAttribute('label'));
- return;
- }
-
- if (e.button['data-btn-name'] === 'backup') {
-
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- var self = this,
- baseUrl = url_for('backup.create_server_job', {
- 'sid': treeInfo.server._id,
- }),
- args = this.view.model.toJSON();
-
- $.ajax({
- url: baseUrl,
- method: 'POST',
- data: {
- 'data': JSON.stringify(args),
- },
- success: function(res) {
- if (res.success) {
- alertify.success(gettext('Backup job created.'), 5);
- pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
- } else {
- console.warn(res);
- }
- },
- error: function(xhr) {
- try {
- var err = $.parseJSON(xhr.responseText);
- alertify.alert(
- gettext('Backup job failed.'),
- err.errormsg
- );
- } catch (e) {
- console.warn(e.stack || e);
- }
- },
- });
- }
- },
- };
- });
- }
- alertify[DialogName](true).resizeTo('60%', '50%');
+ start_backup_global_server: function(action, treeItem, params) {
+ let dialog = new globalBackupDialog.BackupDialog(
+ pgBrowser,
+ $,
+ alertify,
+ BackupModel
+ );
+ dialog.draw(action, treeItem, params);
},
// Callback to draw Backup Dialog for objects
backup_objects: function(action, treeItem) {
-
- var i = treeItem || pgBrowser.tree.selected(),
- server_data = null;
-
- while (i) {
- var node_data = pgBrowser.tree.itemData(i);
- if (node_data._type == 'server') {
- server_data = node_data;
- break;
- }
-
- if (pgBrowser.tree.hasParent(i)) {
- i = $(pgBrowser.tree.parent(i));
- } else {
- alertify.alert(
- gettext('Backup Error'),
- gettext('Please select server or child node from tree.')
- );
- break;
- }
- }
-
- if (!server_data) {
- return;
- }
-
- var module = 'paths',
- preference_name = 'pg_bin_dir',
- msg = gettext('Please set binary path for PostgreSQL Server from preferences.');
-
- if ((server_data.type && server_data.type == 'ppas') ||
- server_data.server_type == 'ppas') {
- preference_name = 'ppas_bin_dir';
- msg = gettext('Please set binary path for EDB Postgres Advanced Server from preferences.');
- }
-
- var preference = pgBrowser.get_preference(module, preference_name);
-
- if (preference) {
- if (!preference.value) {
- alertify.alert(gettext('Configuration required'), msg);
- return;
- }
- } else {
- alertify.alert(
- gettext('Backup Error'),
- S(gettext('Failed to load preference %s of module %s')).sprintf(preference_name, module).value()
- );
- return;
- }
-
- var title = S(gettext('Backup (%s: %s)')),
- tree = pgBrowser.tree,
- item = treeItem || tree.selected(),
- data = item && item.length == 1 && tree.itemData(item),
- node = data && data._type && pgBrowser.Nodes[data._type];
-
- if (!node)
- return;
-
- title = title.sprintf(node.label, data.label).value();
-
- if (!alertify.backup_objects) {
- // Create Dialog title on the fly with node details
- alertify.dialog('backup_objects', function factory() {
- return {
- main: function(title) {
- this.set('title', title);
- },
- build: function() {
- alertify.pgDialogBuild.apply(this);
- },
- setup: function() {
- return {
- buttons: [{
- text: '',
- className: 'btn btn-default pull-left fa fa-lg fa-info',
- attrs: {
- name: 'object_help',
- type: 'button',
- url: 'backup.html',
- label: gettext('Backup'),
- },
- }, {
- text: '',
- key: 112,
- className: 'btn btn-default pull-left fa fa-lg fa-question',
- attrs: {
- name: 'dialog_help',
- type: 'button',
- label: gettext('Backup'),
- url: url_for('help.static', {
- 'filename': 'backup_dialog.html',
- }),
- },
- }, {
- text: gettext('Backup'),
- key: 13,
- className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
- 'data-btn-name': 'backup',
- }, {
- text: gettext('Cancel'),
- key: 27,
- className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
- 'data-btn-name': 'cancel',
- }],
- // Set options for dialog
- options: {
- title: title,
- //disable both padding and overflow control.
- padding: !1,
- overflow: !1,
- model: 0,
- resizable: true,
- maximizable: true,
- pinnable: false,
- closableByDimmer: false,
- modal: false,
- },
- };
- },
- hooks: {
- // triggered when the dialog is closed
- onclose: function() {
- if (this.view) {
- this.view.remove({
- data: true,
- internal: true,
- silent: true,
- });
- }
- },
- },
- prepare: function() {
- var self = this;
- // Disable Backup button until user provides Filename
- this.__internal.buttons[2].element.disabled = true;
- var $container = $('<div class=\'backup_dialog\'></div>');
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- var newModel = new BackupObjectModel({}, {
- node_info: treeInfo,
- }),
- fields = Backform.generateViewSchema(
- treeInfo, newModel, 'create', node, treeInfo.server, true
- );
-
- var view = this.view = new Backform.Dialog({
- el: $container,
- model: newModel,
- schema: fields,
- });
-
- $(this.elements.body.childNodes[0]).addClass(
- 'alertify_tools_dialog_properties obj_properties'
- );
-
- view.render();
-
- this.elements.content.appendChild($container.get(0));
-
- if(view) {
- view.$el.attr('tabindex', -1);
- // var dialogTabNavigator = pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
- commonUtils.findAndSetFocus(container);
- }
- // Listen to model & if filename is provided then enable Backup button
- this.view.model.on('change', function() {
- if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
- this.errorModel.clear();
- self.__internal.buttons[2].element.disabled = false;
- } else {
- self.__internal.buttons[2].element.disabled = true;
- this.errorModel.set('file', gettext('Please provide filename'));
- }
- });
-
- },
- // Callback functions when click on the buttons of the Alertify dialogs
- callback: function(e) {
- // Fetch current server id
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (e.button.element.name == 'dialog_help' || e.button.element.name == 'object_help') {
- e.cancel = true;
- pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
- node, i, e.button.element.getAttribute('label'));
- return;
- }
-
- if (e.button['data-btn-name'] === 'backup') {
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- // Set current database into model
- this.view.model.set('database', treeInfo.database._label);
-
- // We will remove once object tree is implemented
- // If selected node is Schema then add it in model
- if (d._type == 'schema') {
- var schemas = [];
- schemas.push(d._label);
- this.view.model.set('schemas', schemas);
- }
- // If selected node is Table then add it in model along with
- // its schema
- if (d._type == 'table') {
- this.view.model.set(
- 'tables', [
- [treeInfo.schema._label, d._label],
- ]
- );
- }
-
- // Remove ratio attribute from model if it has empty string.
- // The valid value can be between 0 to 9.
- if (_.isEmpty(this.view.model.get('ratio'))) {
- this.view.model.unset('ratio');
- }
-
- var self = this,
- baseUrl = url_for('backup.create_object_job', {
- 'sid': treeInfo.server._id,
- }),
- args = this.view.model.toJSON();
-
- $.ajax({
- url: baseUrl,
- method: 'POST',
- data: {
- 'data': JSON.stringify(args),
- },
- success: function(res) {
- if (res.success) {
- alertify.success(gettext('Backup job created.'), 5);
- pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
- }
- },
- error: function(xhr) {
- try {
- var err = $.parseJSON(xhr.responseText);
- alertify.alert(
- gettext('Backup job failed.'),
- err.errormsg
- );
- } catch (e) {
- console.warn(e.stack || e);
- }
- },
- });
- }
- },
- };
- });
- }
- alertify.backup_objects(title).resizeTo('65%', '60%');
+ let dialog = new globalBackupDialog.BackupDialog(
+ pgBrowser,
+ $,
+ alertify,
+ BackupObjectModel
+ );
+ dialog.draw(action, treeItem, null);
},
};
return pgBrowser.Backup;
diff --git a/web/pgadmin/tools/datagrid/static/js/datagrid.js b/web/pgadmin/tools/datagrid/static/js/datagrid.js
index b0ed60f6..f8b2cfa2 100644
--- a/web/pgadmin/tools/datagrid/static/js/datagrid.js
+++ b/web/pgadmin/tools/datagrid/static/js/datagrid.js
@@ -1,10 +1,13 @@
define('pgadmin.datagrid', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'pgadmin.alertifyjs', 'sources/pgadmin', 'bundled_codemirror',
- 'sources/sqleditor_utils', 'backbone', 'wcdocker',
+ 'sources/sqleditor_utils', 'backbone', 'sources/datagrid/show_data',
+ 'sources/datagrid/get_panel_title',
+ 'sources/datagrid/show_query_tool',
+ 'wcdocker',
], function(
gettext, url_for, $, _, alertify, pgAdmin, codemirror, sqlEditorUtils,
- Backbone
+ Backbone, showData, panelTitle, showQueryTool
) {
// Some scripts do export their object in the window only.
// Generally the one, which do no have AMD support.
@@ -161,55 +164,7 @@ define('pgadmin.datagrid', [
// This is a callback function to show data when user click on menu item.
show_data_grid: function(data, i) {
- var self = this,
- d = pgAdmin.Browser.tree.itemData(i);
- if (d === undefined) {
- alertify.alert(
- gettext('Data Grid Error'),
- gettext('No object selected.')
- );
- return;
- }
-
- // Get the parent data from the tree node hierarchy.
- var node = pgBrowser.Nodes[d._type],
- parentData = node.getTreeNodeHierarchy(i);
-
- // If server, database or schema is undefined then return from the function.
- if (parentData.server === undefined || parentData.database === undefined) {
- return;
- }
- // If schema, view, catalog object all are undefined then return from the function.
- if (parentData.schema === undefined && parentData.view === undefined &&
- parentData.catalog === undefined) {
- return;
- }
-
- var nsp_name = '';
-
- if (parentData.schema != undefined) {
- nsp_name = parentData.schema.label;
- }
- else if (parentData.view != undefined) {
- nsp_name = parentData.view.label;
- }
- else if (parentData.catalog != undefined) {
- nsp_name = parentData.catalog.label;
- }
- var url_params = {
- 'cmd_type': data.mnuid,
- 'obj_type': d._type,
- 'sgid': parentData.server_group._id,
- 'sid': parentData.server._id,
- 'did': parentData.database._id,
- 'obj_id': d._id,
- };
-
- var baseUrl = url_for('datagrid.initialize_datagrid', url_params);
- var grid_title = parentData.server.label + ' - ' + parentData.database.label + ' - '
- + nsp_name + '.' + d.label;
-
- self.create_transaction(baseUrl, null, 'false', parentData.server.server_type, '', grid_title, '');
+ showData.showDataGrid(this, pgBrowser, alertify, data, i);
},
// This is a callback function to show filtered data when user click on menu item.
@@ -384,63 +339,12 @@ define('pgadmin.datagrid', [
},
get_panel_title: function() {
- // Get the parent data from the tree node hierarchy.
- var tree = pgAdmin.Browser.tree,
- selected_item = tree.selected(),
- item_data = tree.itemData(selected_item);
-
- var node = pgBrowser.Nodes[item_data._type],
- parentData = node.getTreeNodeHierarchy(selected_item);
-
- // If server, database is undefined then return from the function.
- if (parentData.server === undefined) {
- return;
- }
- // If Database is not available then use default db
- var db_label = parentData.database ? parentData.database.label
- : parentData.server.db;
-
- var grid_title = db_label + ' on ' + parentData.server.user.name + '@' +
- parentData.server.label;
- return grid_title;
+ return panelTitle.getPanelTitle(pgBrowser);
},
// This is a callback function to show query tool when user click on menu item.
- show_query_tool: function(url, i, panel_title) {
- var sURL = url || '',
- d = pgAdmin.Browser.tree.itemData(i);
-
- panel_title = panel_title || '';
- if (d === undefined) {
- alertify.alert(
- gettext('Query Tool Error'),
- gettext('No object selected.')
- );
- return;
- }
-
- // Get the parent data from the tree node hierarchy.
- var node = pgBrowser.Nodes[d._type],
- parentData = node.getTreeNodeHierarchy(i);
-
- // If server, database is undefined then return from the function.
- if (parentData.server === undefined) {
- return;
- }
-
- var url_params = {
- 'sgid': parentData.server_group._id,
- 'sid': parentData.server._id,
- },
- url_endpoint = 'datagrid.initialize_query_tool';
- // If database not present then use Maintenance database
- // We will handle this at server side
- if (parentData.database) {
- url_params['did'] = parentData.database._id;
- url_endpoint = 'datagrid.initialize_query_tool_with_did';
- }
- var baseUrl = url_for(url_endpoint, url_params);
-
- this.create_transaction(baseUrl, null, 'true', parentData.server.server_type, sURL, panel_title, '', false);
+ show_query_tool: function(url, aciTreeIdentifier, panelTitle) {
+ showQueryTool.showQueryTool(this, pgBrowser, alertify, url,
+ aciTreeIdentifier, panelTitle);
},
create_transaction: function(baseUrl, target, is_query_tool, server_type, sURL, panel_title, sql_filter, recreate) {
var self = this;
diff --git a/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js b/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js
index 750887ec..56eb378a 100644
--- a/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js
+++ b/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js
@@ -2,12 +2,16 @@
define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'pgadmin.alertifyjs', 'pgadmin.backgrid', 'pgadmin.backform',
- 'pgadmin.browser', 'pgadmin.browser.node', 'backgrid.select.all',
+ 'pgadmin.browser', 'pgadmin.browser.node',
+ 'sources/grant/wizard/menu_utils',
+ 'sources/menu/menu_enabled',
+
+ 'backgrid.select.all',
'backgrid.filter', 'pgadmin.browser.server.privilege',
'pgadmin.browser.wizard',
], function(
gettext, url_for, $, _, Backbone, Alertify, Backgrid, Backform, pgBrowser,
- pgNode
+ pgNode, menuUtils, menuEnabled
) {
// if module is already initialized, refer to that.
@@ -143,41 +147,6 @@ define([
this.initialized = true;
- // Define list of nodes on which grant wizard context menu option appears
- var supported_nodes = [
- 'schema', 'coll-function', 'coll-sequence',
- 'coll-table', 'coll-view', 'coll-procedure',
- 'coll-mview', 'database', 'coll-trigger_function',
- ],
-
- /**
- Enable/disable grantwizard menu in tools based
- on node selected
- if selected node is present in supported_nodes,
- menu will be enabled otherwise disabled.
- Also, hide it for system view in catalogs
- */
- menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
- var parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(supported_nodes, d._type) !== -1 &&
- parent_data._type != 'catalog') {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
// Define the nodes on which the menus to be appear
var menus = [{
name: 'grant_wizard_schema',
@@ -187,21 +156,23 @@ define([
priority: 14,
label: gettext('Grant Wizard...'),
icon: 'fa fa-unlock-alt',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null,
+ pgBrowser.treeMenu, menuUtils.supportedNodes),
}];
// Add supported menus into the menus list
- for (var idx = 0; idx < supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.supportedNodes.length; idx++) {
menus.push({
- name: 'grant_wizard_schema_context_' + supported_nodes[idx],
- node: supported_nodes[idx],
+ name: 'grant_wizard_schema_context_' + menuUtils.supportedNodes[idx],
+ node: menuUtils.supportedNodes[idx],
module: this,
applies: ['context'],
callback: 'start_grant_wizard',
priority: 14,
label: gettext('Grant Wizard...'),
icon: 'fa fa-unlock-alt',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null,
+ pgBrowser.treeMenu, menuUtils.supportedNodes),
});
}
pgBrowser.add_menus(menus);
diff --git a/web/pgadmin/tools/import_export/static/js/import_export.js b/web/pgadmin/tools/import_export/static/js/import_export.js
index 3058f122..bbb045cd 100644
--- a/web/pgadmin/tools/import_export/static/js/import_export.js
+++ b/web/pgadmin/tools/import_export/static/js/import_export.js
@@ -1,10 +1,13 @@
define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'underscore.string', 'pgadmin.alertifyjs',
'sources/pgadmin', 'pgadmin.browser', 'backbone', 'backgrid', 'backform',
- 'sources/utils', 'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser.node.ui',
+ 'sources/utils',
+ 'sources/menu/menu_enabled',
+
+ 'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser.node.ui',
], function(
gettext, url_for, $, _, S, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid,
-Backform, commonUtils
+Backform, commonUtils, menuEnabled
) {
pgAdmin = pgAdmin || window.pgAdmin || {};
@@ -383,25 +386,6 @@ Backform, commonUtils
this.initialized = true;
- /*
- * Enable/disable import menu in tools based on node selected. Import
- * menu will be enabled only when user select table node.
- */
- var menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
- var parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data))
- return (
- (_.indexOf(['table'], d._type) !== -1 &&
- parent_data._type != 'catalog') ? true : false
- );
- else
- return false;
- };
-
// Initialize the context menu to display the import options when user open the context menu for table
pgBrowser.add_menus([{
name: 'import',
@@ -413,7 +397,7 @@ Backform, commonUtils
priority: 10,
label: gettext('Import/Export...'),
icon: 'fa fa-shopping-cart',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null, pgBrowser.treeMenu, ['table']),
}]);
},
diff --git a/web/pgadmin/tools/maintenance/static/js/maintenance.js b/web/pgadmin/tools/maintenance/static/js/maintenance.js
index 81e45944..c9976d53 100644
--- a/web/pgadmin/tools/maintenance/static/js/maintenance.js
+++ b/web/pgadmin/tools/maintenance/static/js/maintenance.js
@@ -2,11 +2,14 @@ define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'pgadmin.alertifyjs', 'sources/pgadmin', 'pgadmin.browser', 'backbone',
'backgrid', 'backform', 'sources/utils',
+ 'sources/maintenance/menu_utils',
+ 'sources/menu/menu_enabled',
'pgadmin.backform', 'pgadmin.backgrid',
'pgadmin.browser.node.ui',
], function(
gettext, url_for, $, _, S, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid,
- Backform, commonUtils
+ Backform, commonUtils,
+ menuUtils, menuEnabled
) {
pgAdmin = pgAdmin || window.pgAdmin || {};
@@ -168,36 +171,6 @@ define([
this.initialized = true;
- var maintenance_supported_nodes = [
- 'database', 'table', 'primary_key',
- 'unique_constraint', 'index', 'partition',
- ];
-
- /**
- Enable/disable Maintenance menu in tools based on node selected.
- Maintenance menu will be enabled only when user select table and database node.
- */
- var menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
- var parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(maintenance_supported_nodes, d._type) !== -1 &&
- parent_data._type != 'catalog') {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
var menus = [{
name: 'maintenance',
module: this,
@@ -206,21 +179,23 @@ define([
priority: 10,
label: gettext('Maintenance...'),
icon: 'fa fa-wrench',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null,
+ pgBrowser.treeMenu, menuUtils.maintenanceSupportedNodes),
}];
// Add supported menus into the menus list
- for (var idx = 0; idx < maintenance_supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.maintenanceSupportedNodes.length; idx++) {
menus.push({
- name: 'maintenance_context_' + maintenance_supported_nodes[idx],
- node: maintenance_supported_nodes[idx],
+ name: 'maintenance_context_' + menuUtils.maintenanceSupportedNodes[idx],
+ node: menuUtils.maintenanceSupportedNodes[idx],
module: this,
applies: ['context'],
callback: 'callback_maintenance',
priority: 10,
label: gettext('Maintenance...'),
icon: 'fa fa-wrench',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null,
+ pgBrowser.treeMenu, menuUtils.maintenanceSupportedNodes),
});
}
pgBrowser.add_menus(menus);
diff --git a/web/pgadmin/tools/restore/static/js/restore.js b/web/pgadmin/tools/restore/static/js/restore.js
index 5c082a9f..6c9e74ed 100644
--- a/web/pgadmin/tools/restore/static/js/restore.js
+++ b/web/pgadmin/tools/restore/static/js/restore.js
@@ -1,11 +1,12 @@
-// Restore dialog
define('tools.restore', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'underscore.string', 'pgadmin.alertifyjs', 'pgadmin.browser',
- 'pgadmin.backgrid', 'pgadmin.backform', 'sources/utils',
+ 'pgadmin.backgrid', 'pgadmin.backform', 'sources/utils', 'sources/restore/menu_utils',
+ 'sources/menu/menu_enabled',
+ 'sources/restore/restore_dialog',
], function(
gettext, url_for, $, _, Backbone, S, alertify, pgBrowser, Backgrid, Backform,
-commonUtils
+commonUtils, menuUtils, menuEnabled, restoreDialog
) {
// if module is already initialized, refer to that.
@@ -307,59 +308,6 @@ commonUtils
this.initialized = true;
- // Define list of nodes on which restore context menu option appears
- var restore_supported_nodes = [
- 'database', 'schema',
- 'table', 'function',
- 'trigger', 'index',
- 'partition',
- ];
-
- /**
- Enable/disable restore menu in tools based
- on node selected
- if selected node is present in supported_nodes,
- menu will be enabled otherwise disabled.
- Also, hide it for system view in catalogs
- */
- var menu_enabled = function(itemData, item, data) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
- var parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(restore_supported_nodes, d._type) !== -1 &&
- is_parent_catalog(itemData, item, data)) {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
- var is_parent_catalog = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
-
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to restore
- if (_.indexOf(['catalog'], d._type) > -1)
- return false;
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- };
-
// Define the nodes on which the menus to be appear
var menus = [{
name: 'restore_object',
@@ -369,20 +317,22 @@ commonUtils
priority: 13,
label: gettext('Restore...'),
icon: 'fa fa-upload',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null,
+ pgBrowser.treeMenu, menuUtils.restoreSupportedNodes),
}];
- for (var idx = 0; idx < restore_supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.restoreSupportedNodes.length; idx++) {
menus.push({
- name: 'restore_' + restore_supported_nodes[idx],
- node: restore_supported_nodes[idx],
+ name: 'restore_' + menuUtils.restoreSupportedNodes[idx],
+ node: menuUtils.restoreSupportedNodes[idx],
module: this,
applies: ['context'],
callback: 'restore_objects',
priority: 13,
label: gettext('Restore...'),
icon: 'fa fa-upload',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null,
+ pgBrowser.treeMenu, menuUtils.restoreSupportedNodes),
});
}
@@ -391,307 +341,8 @@ commonUtils
},
// Callback to draw Backup Dialog for objects
restore_objects: function(action, treeItem) {
-
- var i = treeItem || pgBrowser.tree.selected(),
- server_data = null;
-
- while (i) {
- var node_data = pgBrowser.tree.itemData(i);
- if (node_data._type == 'server') {
- server_data = node_data;
- break;
- }
-
- if (pgBrowser.tree.hasParent(i)) {
- i = $(pgBrowser.tree.parent(i));
- } else {
- alertify.alert(
- gettext('Restore Error'),
- gettext('Please select server or child node from tree.')
- );
- break;
- }
- }
-
- if (!server_data) {
- return;
- }
-
- var module = 'paths',
- preference_name = 'pg_bin_dir',
- msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
-
- if ((server_data.type && server_data.type == 'ppas') ||
- server_data.server_type == 'ppas') {
- preference_name = 'ppas_bin_dir';
- msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
- }
-
- var preference = pgBrowser.get_preference(module, preference_name);
-
- if (preference) {
- if (!preference.value) {
- alertify.alert(gettext('Configuration required'), msg);
- return;
- }
- } else {
- alertify.alert(
- gettext('Restore Error'),
- S(gettext('Failed to load preference %s of module %s')).sprintf(preference_name, module).value()
- );
- return;
- }
-
- var title = S(gettext('Restore (%s: %s)')),
- tree = pgBrowser.tree,
- item = treeItem || tree.selected(),
- data = item && item.length == 1 && tree.itemData(item),
- node = data && data._type && pgBrowser.Nodes[data._type];
-
- if (!node)
- return;
-
- title = title.sprintf(node.label, data.label).value();
-
- if (!alertify.pg_restore) {
- // Create Dialog title on the fly with node details
- alertify.dialog('pg_restore', function factory() {
- return {
- main: function(title, item, data, node) {
- this.set('title', title);
- this.setting('pg_node', node);
- this.setting('pg_item', item);
- this.setting('pg_item_data', data);
- },
- build: function() {
- alertify.pgDialogBuild.apply(this);
- },
- setup: function() {
- return {
- buttons: [{
- text: '',
- className: 'btn btn-default pull-left fa fa-lg fa-info',
- attrs: {
- name: 'object_help',
- type: 'button',
- url: 'backup.html',
- label: gettext('Restore'),
- },
- }, {
- text: '',
- key: 112,
- className: 'btn btn-default pull-left fa fa-lg fa-question',
- attrs: {
- name: 'dialog_help',
- type: 'button',
- label: gettext('Restore'),
- url: url_for('help.static', {
- 'filename': 'restore_dialog.html',
- }),
- },
- }, {
- text: gettext('Restore'),
- key: 13,
- className: 'btn btn-primary fa fa-upload pg-alertify-button',
- restore: true,
- 'data-btn-name': 'restore',
- }, {
- text: gettext('Cancel'),
- key: 27,
- className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
- restore: false,
- 'data-btn-name': 'cancel',
- }],
- // Set options for dialog
- options: {
- title: title,
- //disable both padding and overflow control.
- padding: !1,
- overflow: !1,
- model: 0,
- resizable: true,
- maximizable: true,
- pinnable: false,
- closableByDimmer: false,
- modal: false,
- },
- };
- },
- hooks: {
- // triggered when the dialog is closed
- onclose: function() {
- if (this.view) {
- this.view.remove({
- data: true,
- internal: true,
- silent: true,
- });
- }
- },
- },
- settings: {
- pg_node: null,
- pg_item: null,
- pg_item_data: null,
- },
- prepare: function() {
-
- var self = this;
- // Disable Backup button until user provides Filename
- this.__internal.buttons[2].element.disabled = true;
- var $container = $('<div class=\'restore_dialog\'></div>');
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- var newModel = new RestoreObjectModel({
- node_data: node,
- }, {
- node_info: treeInfo,
- }),
- fields = Backform.generateViewSchema(
- treeInfo, newModel, 'create', node, treeInfo.server, true
- );
-
- var view = this.view = new Backform.Dialog({
- el: $container,
- model: newModel,
- schema: fields,
- });
-
- $(this.elements.body.childNodes[0]).addClass(
- 'alertify_tools_dialog_properties obj_properties'
- );
-
- view.render();
-
- this.elements.content.appendChild($container.get(0));
-
- view.$el.attr('tabindex', -1);
- // var dialogTabNavigator = pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
- commonUtils.findAndSetFocus(container);
-
- // Listen to model & if filename is provided then enable Backup button
- this.view.model.on('change', function() {
- if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
- this.errorModel.clear();
- self.__internal.buttons[2].element.disabled = false;
- } else {
- self.__internal.buttons[2].element.disabled = true;
- this.errorModel.set('file', gettext('Please provide filename'));
- }
- });
-
- },
- // Callback functions when click on the buttons of the Alertify dialogs
- callback: function(e) {
- // Fetch current server id
- var t = pgBrowser.tree,
- i = this.settings['pg_item'] || t.selected(),
- d = this.settings['pg_item_data'] || (
- i && i.length == 1 ? t.itemData(i) : undefined
- ),
- node = this.settings['pg_node'] || (
- d && pgBrowser.Nodes[d._type]
- );
-
- if (e.button.element.name == 'dialog_help' || e.button.element.name == 'object_help') {
- e.cancel = true;
- pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
- node, i, e.button.element.getAttribute('label'));
- return;
- }
-
- if (e.button['data-btn-name'] === 'restore') {
- if (!d)
- return;
-
- var info = node.getTreeNodeHierarchy.apply(node, [i]),
- m = this.view.model;
- // Set current node info into model
- m.set('database', info.database._label);
- if (!m.get('custom')) {
- switch (d._type) {
- case 'schema':
- m.set('schemas', [d._label]);
- break;
- case 'table':
- m.set('schemas', [info.schema._label]);
- m.set('tables', [d._label]);
- break;
- case 'function':
- m.set('schemas', [info.schema._label]);
- m.set('functions', [d._label]);
- break;
- case 'index':
- m.set('schemas', [info.schema._label]);
- m.set('indexes', [d._label]);
- break;
- case 'trigger':
- m.set('schemas', [info.schema._label]);
- m.set('triggers', [d._label]);
- break;
- case 'trigger_func':
- m.set('schemas', [info.schema._label]);
- m.set('trigger_funcs', [d._label]);
- break;
- }
- } else {
- // TODO::
- // When we will implement the object selection in the
- // import dialog, we will need to select the objects from
- // the tree selection tab.
- }
-
- var self = this,
- baseUrl = url_for('restore.create_job', {
- 'sid': info.server._id,
- }),
- args = this.view.model.toJSON();
-
- $.ajax({
- url: baseUrl,
- method: 'POST',
- data: {
- 'data': JSON.stringify(args),
- },
- success: function(res) {
- if (res.success) {
- alertify.success(
- gettext('Restore job created.'), 5
- );
- pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
- } else {
- console.warn(res);
- }
- },
- error: function(xhr) {
- try {
- var err = $.parseJSON(xhr.responseText);
- alertify.alert(
- gettext('Restore failed.'),
- err.errormsg
- );
- } catch (e) {
- console.warn(e.stack || e);
- }
- },
- });
- }
- },
- };
- });
- }
-
- alertify.pg_restore(title, item, data, node).resizeTo('65%', '60%');
+ let dialog = new restoreDialog.RestoreDialog(pgBrowser, $, alertify, RestoreObjectModel);
+ dialog.draw(action, treeItem);
},
};
return pgBrowser.Restore;
diff --git a/web/regression/javascript/backform_controls/keyboardshortcut_spec.js b/web/regression/javascript/backform_controls/keyboardshortcut_spec.js
index 02f0715c..404cc365 100644
--- a/web/regression/javascript/backform_controls/keyboardshortcut_spec.js
+++ b/web/regression/javascript/backform_controls/keyboardshortcut_spec.js
@@ -12,16 +12,16 @@ define([
'pgadmin.backform',
], function ($, Backbone, Backform) {
describe('KeyboardshortcutControl', function () {
- let field, innerFields, control, model, event;
+ let field, innerFields, control, model;
beforeEach(() => {
innerFields = [
{'name': 'key', 'type': 'keyCode', 'label': 'Key'},
{'name': 'alt_option', 'type': 'checkbox',
- 'label': 'Alt/Option'},
+ 'label': 'Alt/Option'},
{'name': 'control', 'type': 'checkbox',
- 'label': 'Ctrl'},
+ 'label': 'Ctrl'},
{'name': 'shift', 'type': 'checkbox', 'label': 'Shift'},
];
@@ -91,7 +91,7 @@ define([
'key': {
'key_code': 65,
'char': 'A',
- }
+ },
})
);
@@ -128,7 +128,7 @@ define([
model.set(field.get('name'),
$.extend(true, val, {
- 'control': false
+ 'control': false,
})
);
@@ -165,7 +165,7 @@ define([
model.set(field.get('name'),
$.extend(true, val, {
- 'shift': true
+ 'shift': true,
})
);
@@ -202,7 +202,7 @@ define([
model.set(field.get('name'),
$.extend(true, val, {
- 'alt_option': false
+ 'alt_option': false,
})
);
@@ -387,14 +387,14 @@ define([
// this should change
expect(control.$el.find('input:checkbox[name="alt_option"]')[0].checked).toBeFalsy();
expect(model.get(field.get('name'))).toEqual({
- 'control': true,
- 'shift': false,
- 'alt_option': false,
- 'key': {
- 'key_code': 73,
- 'char': 'I',
- },
- });
+ 'control': true,
+ 'shift': false,
+ 'alt_option': false,
+ 'key': {
+ 'key_code': 73,
+ 'char': 'I',
+ },
+ });
// below three should not change.
expect(control.$el.find('input:checkbox[name="shift"]')[0].checked).toBeFalsy();
@@ -431,4 +431,4 @@ define([
});
});
-});
\ No newline at end of file
+});
diff --git a/web/regression/javascript/backform_controls/keycode_spec.js b/web/regression/javascript/backform_controls/keycode_spec.js
index d71ef48c..6e57d972 100644
--- a/web/regression/javascript/backform_controls/keycode_spec.js
+++ b/web/regression/javascript/backform_controls/keycode_spec.js
@@ -18,8 +18,8 @@ define([
'key': {
'key_code': 65,
'char': 'A',
- }
- });
+ },
+ });
field = new Backform.Field({
id: 'key',
diff --git a/web/regression/javascript/backup/backup_dialog_spec.js b/web/regression/javascript/backup/backup_dialog_spec.js
new file mode 100644
index 00000000..c2da61c4
--- /dev/null
+++ b/web/regression/javascript/backup/backup_dialog_spec.js
@@ -0,0 +1,190 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {BackupDialog} from '../../../pgadmin/static/js/backup/backup_dialog';
+import {TreeFake} from '../tree/tree_fake';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+const context = describe;
+
+describe('ObjectBackupDialog', () => {
+ let backupDialog;
+ let pgBrowser;
+ let jquerySpy;
+ let alertifySpy;
+ let backupModelSpy;
+
+
+ let rootNode;
+ let serverTreeNode;
+ let databaseTreeNode;
+ let ppasServerTreeNode;
+ let noDataNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: {
+ hasId: true,
+ label: 'server',
+ getTreeNodeHierarchy: jasmine.createSpy('server.getTreeNodeHierarchy'),
+ },
+ database: {
+ hasId: true,
+ label: 'database',
+ getTreeNodeHierarchy: jasmine.createSpy('db.getTreeNodeHierarchy'),
+ },
+ schema: {
+ hasId: true,
+ label: 'schema',
+ getTreeNodeHierarchy: jasmine.createSpy('db.getTreeNodeHierarchy'),
+ },
+ },
+ };
+ pgBrowser.Nodes.server.hasId = true;
+ pgBrowser.Nodes.database.hasId = true;
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ backupModelSpy = jasmine.createSpy('backupModelSpy');
+
+ rootNode = pgBrowser.treeMenu.addNewNode('level1', {}, []);
+
+ serverTreeNode = new TreeNode('level1.1', {
+ _type: 'server',
+ _id: 10,
+ });
+ pgBrowser.treeMenu.addChild(rootNode, serverTreeNode);
+
+ databaseTreeNode = new TreeNode(
+ 'level1.1.1', {
+ _type: 'database',
+ _id: 11,
+ label: 'some_database',
+ _label: 'some_database_label',
+ }, [{id: 'level1.1.1'}]);
+ pgBrowser.treeMenu.addChild(serverTreeNode, databaseTreeNode);
+
+ ppasServerTreeNode = new TreeNode('level1.2', {
+ _type: 'server',
+ server_type: 'ppas',
+ });
+ pgBrowser.treeMenu.addChild(rootNode, ppasServerTreeNode);
+
+ const someNodeUnderneathPPASServer = new TreeNode('level3', {});
+ pgBrowser.treeMenu.addChild(ppasServerTreeNode,
+ someNodeUnderneathPPASServer);
+
+ noDataNode = new TreeNode(
+ 'level3.1', undefined);
+ pgBrowser.treeMenu.addChild(someNodeUnderneathPPASServer, noDataNode);
+ });
+
+ describe('#draw', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ alertifySpy['backup_objects'] = jasmine.createSpy('backup_objects');
+ backupDialog = new BackupDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ backupModelSpy
+ );
+
+ pgBrowser.get_preference = jasmine.createSpy('get_preferences');
+ });
+
+ context('there are no ancestors of the type server', () => {
+ it('does not create a dialog', () => {
+ pgBrowser.treeMenu.selectNode([rootNode]);
+ backupDialog.draw(null, null, null);
+ expect(alertifySpy['backup_objects']).not.toHaveBeenCalled();
+ });
+
+ it('display an alert with a Backup Error', () => {
+ backupDialog.draw(null, [rootNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Please select server or child node from the browser tree.'
+ );
+ });
+ });
+
+ context('there is an ancestor of the type server', () => {
+ context('no preference can be found', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue(undefined);
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [{id: 'level1.1.1'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference pg_bin_dir of module paths'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference ppas_bin_dir of module paths'
+ );
+ });
+ });
+ });
+
+ context('preference can be found', () => {
+ context('binary folder is not configured', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue({});
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the PostgreSQL Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the EDB Advanced Server Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+ });
+
+ context('binary folder is configured', () => {
+ let backupDialogResizeToSpy;
+ beforeEach(() => {
+ backupDialogResizeToSpy = jasmine.createSpyObj('backupDialogResizeToSpy', ['resizeTo']);
+ alertifySpy['backup_objects'].and
+ .returnValue(backupDialogResizeToSpy);
+ pgBrowser.get_preference.and.returnValue({value: '/some/path'});
+ });
+
+ it('displays the dialog', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy['backup_objects']).toHaveBeenCalledWith(true);
+ expect(backupDialogResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
+ });
+ });
+ });
+ });
+ });
+
+});
diff --git a/web/regression/javascript/backup/backup_dialog_wrapper_spec.js b/web/regression/javascript/backup/backup_dialog_wrapper_spec.js
new file mode 100644
index 00000000..73e453e5
--- /dev/null
+++ b/web/regression/javascript/backup/backup_dialog_wrapper_spec.js
@@ -0,0 +1,675 @@
+import {TreeFake} from '../tree/tree_fake';
+import {BackupDialogWrapper} from '../../../pgadmin/static/js/backup/backup_dialog_wrapper';
+import axios from 'axios/index';
+import MockAdapter from 'axios-mock-adapter';
+import {FakeModel} from '../fake_model';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+let context = describe;
+
+describe('BackupDialogWrapper', () => {
+ let jquerySpy;
+ let pgBrowser;
+ let alertifySpy;
+ let dialogModelKlassSpy;
+ let backform;
+ let generatedBackupModel;
+ let backupDialogWrapper;
+ let noDataNode;
+ let serverTreeNode;
+ let databaseTreeNode;
+ let viewSchema;
+ let backupJQueryContainerSpy;
+ let backupNodeChildNodeSpy;
+ let backupNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: {
+ hasId: true,
+ getTreeNodeHierarchy: jasmine.createSpy('getTreeNodeHierarchy'),
+ },
+ database: {
+ hasId: true,
+ },
+ },
+ keyboardNavigation: jasmine.createSpyObj('keyboardNavigation', ['getDialogTabNavigator']),
+ };
+ noDataNode = pgBrowser.treeMenu.addNewNode('level1.1', undefined, [{id: 'level1'}]);
+ serverTreeNode = pgBrowser.treeMenu.addNewNode('level2.1', {
+ _type: 'server',
+ _id: 10,
+ label: 'some-tree-label',
+ }, [{id: 'level2.1'}]);
+ databaseTreeNode = new TreeNode('database-tree-node', {
+ _type: 'database',
+ _label: 'some-database-label',
+ }, [{id: 'database-tree-node'}]);
+ pgBrowser.treeMenu.addChild(serverTreeNode, databaseTreeNode);
+
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ backupNode = {
+ __internal: {
+ buttons: [{}, {}, {
+ element: {
+ disabled: false,
+ },
+ }],
+ },
+ elements: {
+ body: {
+ childNodes: [
+ {},
+ ],
+ },
+ content: jasmine.createSpyObj('content', ['appendChild', 'attr']),
+ },
+ };
+
+ backupJQueryContainerSpy = jasmine.createSpyObj('backupJQueryContainer', ['get', 'attr']);
+ backupJQueryContainerSpy.get.and.returnValue(backupJQueryContainerSpy);
+
+ generatedBackupModel = {};
+ dialogModelKlassSpy = jasmine.createSpy('dialogModelKlass');
+ dialogModelKlassSpy.and.returnValue(generatedBackupModel);
+
+ viewSchema = {};
+ backform = jasmine.createSpyObj('backform', ['generateViewSchema', 'Dialog']);
+ backform.generateViewSchema.and.returnValue(viewSchema);
+
+ backupNodeChildNodeSpy = jasmine.createSpyObj('something', ['addClass']);
+ jquerySpy.and.callFake((selector) => {
+ if (selector === '<div class=\'backup_dialog\'></div>') {
+ return backupJQueryContainerSpy;
+ } else if (selector === backupNode.elements.body.childNodes[0]) {
+ return backupNodeChildNodeSpy;
+ }
+ });
+
+ });
+
+ describe('#prepare', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+ backupDialogWrapper = Object.assign(backupDialogWrapper, backupNode);
+ });
+
+ context('no tree element is selected', () => {
+ it('does not create a backform dialog', () => {
+ backupDialogWrapper.prepare();
+ expect(backform.Dialog).not.toHaveBeenCalled();
+ });
+
+ it('disables the button "submit button" until a filename is selected', () => {
+ backupDialogWrapper.prepare();
+ expect(backupDialogWrapper.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('selected tree node has no data', () => {
+ beforeEach(() => {
+ pgBrowser.treeMenu.selectNode(noDataNode.domNode);
+ });
+
+ it('does not create a backform dialog', () => {
+ backupDialogWrapper.prepare();
+ expect(backform.Dialog).not.toHaveBeenCalled();
+ });
+
+ it('disables the button "submit button" until a filename is selected', () => {
+ backupDialogWrapper.prepare();
+ expect(backupDialogWrapper.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('tree element is selected', () => {
+ let treeHierarchyInformation;
+ let dialogSpy;
+
+ beforeEach(() => {
+ treeHierarchyInformation = {
+ server: {
+ _type: 'server',
+ _id: 10,
+ priority: 0,
+ label: 'some-tree-label',
+ },
+ };
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ pgBrowser.Nodes['server'].getTreeNodeHierarchy.and
+ .returnValue(treeHierarchyInformation);
+ dialogSpy = jasmine.createSpyObj('newView', ['render']);
+ dialogSpy.$el = jasmine.createSpyObj('$el', ['find', 'attr']);
+ dialogSpy.model = jasmine.createSpyObj('model', ['on']);
+ dialogSpy.$el.find.and.returnValue([]);
+
+ backform.Dialog.and.returnValue(dialogSpy);
+ });
+
+ it('creates a backform dialog and displays it', () => {
+ backupDialogWrapper.prepare();
+ expect(backform.Dialog).toHaveBeenCalledWith({
+ el: backupJQueryContainerSpy,
+ model: generatedBackupModel,
+ schema: viewSchema,
+ });
+
+ expect(dialogSpy.render).toHaveBeenCalled();
+ });
+
+
+ it('add alertify classes to restore node childnode', () => {
+ backupDialogWrapper.prepare();
+ expect(backupNodeChildNodeSpy.addClass)
+ .toHaveBeenCalledWith('alertify_tools_dialog_properties obj_properties');
+ });
+
+ it('disables the button submit button until a filename is selected', () => {
+ backupDialogWrapper.prepare();
+ expect(backupNode.__internal.buttons[2].element.disabled).toBe(true);
+ });
+
+ it('generates a new backup model', () => {
+ backupDialogWrapper.prepare();
+ expect(dialogModelKlassSpy).toHaveBeenCalledWith(
+ {type: 'backup'},
+ {node_info: treeHierarchyInformation}
+ );
+ });
+
+ it('add the new dialog to the backup node HTML', () => {
+ backupDialogWrapper.prepare();
+ expect(backupNode.elements.content.appendChild).toHaveBeenCalledWith(backupJQueryContainerSpy);
+ });
+ });
+ });
+
+ describe('onButtonClicked', () => {
+ let networkMock;
+ beforeEach(() => {
+ networkMock = new MockAdapter(axios);
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ backupDialogWrapper = Object.assign(backupDialogWrapper, backupNode);
+ });
+
+ afterEach(() => {
+ networkMock.restore();
+ });
+
+ context('dialog help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.*/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ pgBrowser.showHelp = jasmine.createSpy('showHelp');
+
+ const event = {
+ button: {
+ element: {
+ name: 'dialog_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ },
+ },
+ },
+ };
+ backupDialogWrapper.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'dialog_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ );
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('object help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.*/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ pgBrowser.showHelp = jasmine.createSpy('showHelp');
+
+ const event = {
+ button: {
+ element: {
+ name: 'object_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ },
+ },
+ },
+ };
+ backupDialogWrapper.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'object_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ );
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('backup button was pressed', () => {
+ context('no tree node is selected', () => {
+ it('does not start the backup', () => {
+ let networkCalled = false;
+ networkMock.onAny(/.*/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ let event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+
+ backupDialogWrapper.callback(event);
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('tree node has no data', () => {
+ it('does not start the backup', () => {
+ pgBrowser.treeMenu.selectNode(noDataNode.domNode);
+
+ let networkCalled = false;
+ networkMock.onAny(/.*/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ let event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+
+ backupDialogWrapper.callback(event);
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('tree node has data', () => {
+ context('when dialog type is global', () => {
+ let event;
+ beforeEach(() => {
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+
+ backupDialogWrapper.view = {
+ model: new FakeModel(),
+ };
+
+ event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ });
+
+ context('when the backup job is created successfully', () => {
+ let dataSentToServer;
+ beforeEach(() => {
+ pgBrowser.Events = jasmine.createSpyObj('Events', ['trigger']);
+ alertifySpy.success = jasmine.createSpy('success');
+
+ networkMock.onPost('/backup/job/10').reply((request) => {
+ dataSentToServer = request.data;
+ return [200, {}];
+ });
+ });
+
+ it('creates a success alert box', (done) => {
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.success).toHaveBeenCalledWith(
+ 'Backup job created.',
+ 5
+ );
+ done();
+ }, 0);
+ });
+
+ it('trigger an event to background process', (done) => {
+ backupDialogWrapper.callback(event);
+
+ setTimeout(() => {
+ expect(pgBrowser.Events.trigger).toHaveBeenCalledWith(
+ 'pgadmin-bgprocess:created',
+ backupDialogWrapper
+ );
+ done();
+ }, 0);
+ });
+
+ it('send the correct paramenters to the backend', (done) => {
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(JSON.parse(dataSentToServer)).toEqual(
+ {}
+ );
+ done();
+ }, 0);
+ });
+ });
+
+ context('when creating backup job fails', () => {
+ it('creates an alert box', (done) => {
+ alertifySpy.alert = jasmine.createSpy('alert');
+ networkMock.onPost('/backup/job/10').reply(() => {
+ return [400, {
+ errormsg: 'some-error-message',
+ }];
+ });
+
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup job failed.',
+ 'some-error-message'
+ );
+ done();
+ }, 0);
+
+ });
+ });
+ });
+
+ context('when dialog type is object', () => {
+ let event;
+ beforeEach(() => {
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup_objects',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ pgBrowser.treeMenu.selectNode(databaseTreeNode.domNode);
+
+ backupDialogWrapper.view = {
+ model: new FakeModel(),
+ };
+
+ event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ });
+
+ context('when the backup job is created successfully', () => {
+ let dataSentToServer;
+ beforeEach(() => {
+ pgBrowser.Events = jasmine.createSpyObj('Events', ['trigger']);
+ alertifySpy.success = jasmine.createSpy('success');
+
+ networkMock.onPost('/backup/job/10/object').reply((request) => {
+ dataSentToServer = request.data;
+ return [200, {}];
+ });
+ });
+
+ it('creates a success alert box', (done) => {
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.success).toHaveBeenCalledWith(
+ 'Backup job created.',
+ 5
+ );
+ done();
+ }, 0);
+ });
+
+ it('trigger an event to background process', (done) => {
+ backupDialogWrapper.callback(event);
+
+ setTimeout(() => {
+ expect(pgBrowser.Events.trigger).toHaveBeenCalledWith(
+ 'pgadmin-bgprocess:created',
+ backupDialogWrapper
+ );
+ done();
+ }, 0);
+ });
+
+ it('send the correct parameters to the backend', (done) => {
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(JSON.parse(dataSentToServer)).toEqual(
+ {database: 'some-database-label'}
+ );
+ done();
+ }, 0);
+ });
+ });
+
+ context('when creating backup job fails', () => {
+ it('creates an alert box', (done) => {
+ alertifySpy.alert = jasmine.createSpy('alert');
+ networkMock.onPost('/backup/job/10/object').reply(() => {
+ return [400, {
+ errormsg: 'some-error-message',
+ }];
+ });
+
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup job failed.',
+ 'some-error-message'
+ );
+ done();
+ }, 0);
+ });
+ });
+ });
+ });
+ });
+ });
+
+ describe('#setExtraParameters', () => {
+ let selectedTreeNode;
+ let treeInfo;
+ let model;
+
+ context('when dialog type is global', () => {
+ beforeEach(() => {
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ treeInfo = {};
+ model = new FakeModel();
+ backupDialogWrapper.view = {
+ model: model,
+ };
+ });
+
+
+ it('sets nothing on the view model', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.toJSON()).toEqual({});
+ });
+ });
+
+ context('when dialog type is object', () => {
+ beforeEach(() => {
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup_objects',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ treeInfo = {
+ database: {
+ _label: 'some-database-label',
+ },
+ schema: {
+ _label: 'some-treeinfo-label',
+ },
+ };
+
+ model = new FakeModel();
+ selectedTreeNode = new TreeNode('some-selected-node',
+ {_type: 'some-type', _label: 'some-selected-label'},
+ []);
+ backupDialogWrapper.view = {
+ model: model,
+ };
+ });
+
+ it('sets the database label on the model', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ 'database': 'some-database-label',
+ });
+ });
+
+ context('when the selected is a schema type', () => {
+ beforeEach(() => {
+ selectedTreeNode = new TreeNode('some-selected-node',
+ {_type: 'schema', _label: 'some-schema-label'},
+ []);
+ });
+
+ it('sets the schema label on the model', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ 'database': 'some-database-label',
+ 'schemas': ['some-schema-label'],
+ });
+ });
+ });
+
+ context('when the selected is a table type', () => {
+ beforeEach(() => {
+ selectedTreeNode = new TreeNode('some-selected-node',
+ {_type: 'table', _label: 'some-table-label'},
+ []);
+ });
+
+ it('sets the schema label on the model', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ 'database': 'some-database-label',
+ 'tables': [['some-treeinfo-label', 'some-table-label']],
+ });
+ });
+ });
+
+ context('when the model has no ratio value', () => {
+ beforeEach(() => {
+ model.set('ratio', '');
+ });
+
+ it('sets clears the ratio value', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.get('ratio')).toBeUndefined();
+ });
+ });
+
+ context('when the model has a valid ratio value', () => {
+ beforeEach(() => {
+ model.set('ratio', '0.25');
+ });
+
+ it('sets clears the ratio value', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.get('ratio')).toEqual('0.25');
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/backup/global_server_backup_dialog_spec.js b/web/regression/javascript/backup/global_server_backup_dialog_spec.js
new file mode 100644
index 00000000..67cca9db
--- /dev/null
+++ b/web/regression/javascript/backup/global_server_backup_dialog_spec.js
@@ -0,0 +1,168 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {BackupDialog} from '../../../pgadmin/static/js/backup/backup_dialog';
+import {TreeFake} from '../tree/tree_fake';
+
+const context = describe;
+
+describe('GlobalServerBackupDialog', () => {
+ let backupDialog;
+ let pgBrowser;
+ let jquerySpy;
+ let alertifySpy;
+ let backupModelSpy;
+
+
+ let rootNode;
+ let serverTreeNode;
+ let ppasServerTreeNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: jasmine.createSpyObj('Node[server]', ['getTreeNodeHierarchy']),
+ },
+ };
+ pgBrowser.Nodes.server.hasId = true;
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ backupModelSpy = jasmine.createSpy('backupModelSpy');
+
+ rootNode = pgBrowser.treeMenu.addNewNode('level1', {}, undefined, []);
+ serverTreeNode = pgBrowser.treeMenu.addNewNode('level1.1', {
+ _type: 'server',
+ _id: 10,
+ }, undefined, ['level1']);
+ ppasServerTreeNode = pgBrowser.treeMenu.addNewNode('level1.2', {
+ _type: 'server',
+ server_type: 'ppas',
+ }, undefined, ['level1']);
+ pgBrowser.treeMenu.addNewNode('level3', {}, undefined, ['level1', 'level1.2']);
+ pgBrowser.treeMenu.addNewNode('level3.1', undefined, undefined, ['level1', 'level1.2', 'level3']);
+ });
+
+ describe('#draw', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ alertifySpy['BackupDialog_globals'] = jasmine.createSpy('BackupDialog_globals');
+ alertifySpy['BackupDialog_server'] = jasmine.createSpy('BackupDialog_server');
+ backupDialog = new BackupDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ backupModelSpy
+ );
+
+ pgBrowser.get_preference = jasmine.createSpy('get_preferences');
+ });
+
+ context('there are no ancestors of the type server', () => {
+ it('does not create a dialog', () => {
+ pgBrowser.treeMenu.selectNode([{id: 'level1'}]);
+ backupDialog.draw(null, null, null);
+ expect(alertifySpy['BackupDialog_globals']).not.toHaveBeenCalled();
+ expect(alertifySpy['BackupDialog_server']).not.toHaveBeenCalled();
+ });
+
+ it('display an alert with a Backup Error', () => {
+ backupDialog.draw(null, [rootNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Please select server or child node from the browser tree.'
+ );
+ });
+ });
+
+ context('there is an ancestor of the type server', () => {
+ context('no preference can be found', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue(undefined);
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference pg_bin_dir of module paths'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference ppas_bin_dir of module paths'
+ );
+ });
+ });
+ });
+
+ context('preference can be found', () => {
+ context('binary folder is not configured', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue({});
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the PostgreSQL Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the EDB Advanced Server Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+ });
+
+ context('binary folder is configured', () => {
+ let globalResizeToSpy;
+ let serverResizeToSpy;
+ beforeEach(() => {
+ globalResizeToSpy = jasmine.createSpyObj('globals', ['resizeTo']);
+ alertifySpy['BackupDialog_globals'].and
+ .returnValue(globalResizeToSpy);
+ serverResizeToSpy = jasmine.createSpyObj('server', ['resizeTo']);
+ alertifySpy['BackupDialog_server'].and
+ .returnValue(serverResizeToSpy);
+ pgBrowser.get_preference.and.returnValue({value: '/some/path'});
+ });
+
+ context('dialog for global backup', () => {
+ it('displays the dialog', () => {
+ backupDialog.draw(null, [serverTreeNode], {globals: true});
+ expect(alertifySpy['BackupDialog_globals']).toHaveBeenCalledWith(true);
+ expect(globalResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
+ });
+ });
+
+ context('dialog for server backup', () => {
+ it('displays the dialog', () => {
+ backupDialog.draw(null, [serverTreeNode], {server: true});
+ expect(alertifySpy['BackupDialog_server']).toHaveBeenCalledWith(true);
+ expect(serverResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/backup/menu_utils_spec.js b/web/regression/javascript/backup/menu_utils_spec.js
new file mode 100644
index 00000000..ecf8abcd
--- /dev/null
+++ b/web/regression/javascript/backup/menu_utils_spec.js
@@ -0,0 +1,55 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+
+import {menuEnabledServer} from '../../../pgadmin/static/js/backup/menu_utils';
+
+const context = describe;
+
+describe('backup.menuUtils', () => {
+ describe('#menuEnabledServer', () => {
+ context('provided node data is undefined', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer(undefined)).toBe(false);
+ });
+ });
+
+ context('provided node data is null', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer(null)).toBe(false);
+ });
+ });
+
+ context('current node type is not of the type server', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer({_type: 'schema'})).toBe(false);
+ });
+ });
+
+ context('current node type is of the type server', () => {
+ context('is connected', () => {
+ it('returns true', () => {
+ expect(menuEnabledServer({
+ _type: 'server',
+ connected: true,
+ })).toBe(true);
+ });
+ });
+ context('is not connected', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer({
+ _type: 'server',
+ connected: false,
+ })).toBe(false);
+ });
+ });
+ });
+ });
+});
+
diff --git a/web/regression/javascript/browser/modify_animation_spec.js b/web/regression/javascript/browser/modify_animation_spec.js
index b8060eed..51c882d1 100644
--- a/web/regression/javascript/browser/modify_animation_spec.js
+++ b/web/regression/javascript/browser/modify_animation_spec.js
@@ -7,8 +7,8 @@
//
//////////////////////////////////////////////////////////////
-import $ from 'jquery'
-import modifyAnimation from 'sources/modify_animation'
+import $ from 'jquery';
+import modifyAnimation from 'sources/modify_animation';
describe('modifyAnimation', function () {
@@ -16,12 +16,12 @@ describe('modifyAnimation', function () {
let dummyElement;
beforeEach(() => {
- pgBrowser = jasmine.createSpyObj('pgBrowser', ['get_preference', 'tree'])
+ pgBrowser = jasmine.createSpyObj('pgBrowser', ['get_preference', 'tree']);
pgBrowser.tree = jasmine.createSpyObj('tree', ['options']);
pgBrowser.tree.options.and.returnValue({
- show: {},
- hide: {},
- view: {},
+ show: {},
+ hide: {},
+ view: {},
});
dummyElement = document.createElement('link');
spyOn($.fn, 'find').and.returnValue($(dummyElement));
diff --git a/web/regression/javascript/browser/server_groups/servers/model_validation_spec.js b/web/regression/javascript/browser/server_groups/servers/model_validation_spec.js
index 85e51eb9..99050742 100644
--- a/web/regression/javascript/browser/server_groups/servers/model_validation_spec.js
+++ b/web/regression/javascript/browser/server_groups/servers/model_validation_spec.js
@@ -62,7 +62,7 @@ describe('Server#ModelValidation', () => {
expect(model.errorModel.set).toHaveBeenCalledWith({
host: 'Either Host name, Address or Service must be specified.',
hostaddr: 'Either Host name, Address or Service must be specified.',
- db: 'Maintenance database must be specified.'
+ db: 'Maintenance database must be specified.',
});
});
});
@@ -81,7 +81,7 @@ describe('Server#ModelValidation', () => {
hostaddr: 'Either Host name, Address or Service must be specified.',
db: 'Maintenance database must be specified.',
username: 'Username must be specified.',
- port: 'Port must be specified.'
+ port: 'Port must be specified.',
});
});
});
@@ -96,7 +96,7 @@ describe('Server#ModelValidation', () => {
hostaddr: 'Host address must be valid IPv4 or IPv6 address.',
db: 'Maintenance database must be specified.',
username: 'Username must be specified.',
- port: 'Port must be specified.'
+ port: 'Port must be specified.',
});
});
});
@@ -109,7 +109,7 @@ describe('Server#ModelValidation', () => {
expect(model.errorModel.set).toHaveBeenCalledWith({
name: 'Name must be specified.',
username: 'Username must be specified.',
- port: 'Port must be specified.'
+ port: 'Port must be specified.',
});
});
});
diff --git a/web/regression/javascript/common_keyboard_shortcuts_spec.js b/web/regression/javascript/common_keyboard_shortcuts_spec.js
index b89af5b0..e27929bf 100644
--- a/web/regression/javascript/common_keyboard_shortcuts_spec.js
+++ b/web/regression/javascript/common_keyboard_shortcuts_spec.js
@@ -10,18 +10,14 @@
import keyboardShortcuts from 'sources/keyboard_shortcuts';
describe('the keyboard shortcuts', () => {
- const F1_KEY = 112,
- EDIT_KEY = 71, // Key: G -> Grid values
- LEFT_ARROW_KEY = 37,
- RIGHT_ARROW_KEY = 39,
- MOVE_NEXT = 'right';
+ const F1_KEY = 112;
let debuggerElementSpy, event, debuggerUserShortcutSpy;
debuggerUserShortcutSpy = jasmine.createSpyObj(
'userDefinedShortcuts', [
{ 'edit_grid_keys': null },
{ 'next_panel_keys': null },
- { 'previous_panel_keys': null }
+ { 'previous_panel_keys': null },
]
);
beforeEach(() => {
diff --git a/web/regression/javascript/datagrid/get_panel_title_spec.js b/web/regression/javascript/datagrid/get_panel_title_spec.js
new file mode 100644
index 00000000..2594ec96
--- /dev/null
+++ b/web/regression/javascript/datagrid/get_panel_title_spec.js
@@ -0,0 +1,82 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {getPanelTitle} from '../../../pgadmin/static/js/datagrid/get_panel_title';
+import {TreeFake} from '../tree/tree_fake';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+const context = describe;
+
+describe('#getPanelTitle', () => {
+ let pgBrowser;
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ pgBrowser = {
+ treeMenu: tree,
+ Nodes: {
+ server: {
+ hasId: true,
+ _type: 'server',
+ },
+ database: {
+ hasId: true,
+ _type: 'database',
+ },
+ },
+ };
+ });
+
+ context('selected node does not belong to a server', () => {
+ it('returns undefined', () => {
+ const root = tree.addNewNode('level1', {_type: 'server_groups'});
+ tree.addChild(root, new TreeNode('level1.1', {_type: 'other'}));
+ tree.selectNode([{id: 'level1'}]);
+ expect(getPanelTitle(pgBrowser)).toBeUndefined();
+ });
+ });
+
+ context('selected node belong to a server', () => {
+ context('selected node does not belong to a database', () => {
+ it('returns the server label and the username', () => {
+ tree.addNewNode('level1', {
+ _type: 'server',
+ db: 'other db label',
+ user: {name: 'some user name'},
+ label: 'server label',
+ }, []);
+
+ tree.selectNode([{id: 'level1'}]);
+ expect(getPanelTitle(pgBrowser))
+ .toBe('other db label on some user name@server label');
+ });
+ });
+
+ context('selected node belongs to a database', () => {
+ it('returns the database label and the username', () => {
+ const root = tree.addNewNode('level1', {
+ _type: 'server',
+ db: 'other db label',
+ user: {name: 'some user name'},
+ label: 'server label',
+ });
+ const level1 = new TreeNode('level1.1', {
+ _type: 'database',
+ label: 'db label',
+ });
+ tree.addChild(root, level1);
+ tree.addChild(level1,
+ new TreeNode('level1.1.1', {_type: 'table'}));
+ tree.selectNode([{id: 'level1.1.1'}]);
+ expect(getPanelTitle(pgBrowser))
+ .toBe('db label on some user name@server label');
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/datagrid/show_data_spec.js b/web/regression/javascript/datagrid/show_data_spec.js
new file mode 100644
index 00000000..3e4feec9
--- /dev/null
+++ b/web/regression/javascript/datagrid/show_data_spec.js
@@ -0,0 +1,171 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {showDataGrid} from '../../../pgadmin/static/js/datagrid/show_data';
+import {TreeFake} from '../tree/tree_fake';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+const context = describe;
+
+describe('#show_data', () => {
+ let datagrid;
+ let pgBrowser;
+ let alertify;
+ beforeEach(() => {
+ alertify = jasmine.createSpyObj('alertify', ['alert']);
+ datagrid = {
+ create_transaction: jasmine.createSpy('create_transaction'),
+ };
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server_group: {
+ _type: 'server_group',
+ hasId: true,
+ },
+ server: {
+ _type: 'server',
+ hasId: true,
+ },
+ database: {
+ _type: 'database',
+ hasId: true,
+ },
+ schema: {
+ _type: 'schema',
+ hasId: true,
+ },
+ view: {
+ _type: 'view',
+ hasId: true,
+ },
+ catalog: {
+ _type: 'catalog',
+ hasId: true,
+ },
+ },
+ };
+ const parent = pgBrowser.treeMenu.addNewNode('parent', {_type: 'parent'}, []);
+ const serverGroup1 = new TreeNode('server_group1', {
+ _type: 'server_group',
+ _id: 1,
+ });
+ pgBrowser.treeMenu.addChild(parent, serverGroup1);
+
+ const server1 = new TreeNode('server1', {
+ _type: 'server',
+ label: 'server1',
+ server_type: 'pg',
+ _id: 2,
+ }, ['parent', 'server_group1']);
+ pgBrowser.treeMenu.addChild(serverGroup1, server1);
+
+ const database1 = new TreeNode('database1', {
+ _type: 'database',
+ label: 'database1',
+ _id: 3,
+ }, ['parent', 'server_group1', 'server1']);
+ pgBrowser.treeMenu.addChild(server1, database1);
+
+ const schema1 = new TreeNode('schema1', {
+ _type: 'schema',
+ label: 'schema1',
+ _id: 4,
+ });
+ pgBrowser.treeMenu.addChild(database1, schema1);
+
+ const view1 = new TreeNode('view1', {
+ _type: 'view',
+ label: 'view1',
+ _id: 5,
+ }, ['parent', 'server_group1', 'server1', 'database1']);
+ pgBrowser.treeMenu.addChild(database1, view1);
+
+ const catalog1 = new TreeNode('catalog1', {
+ _type: 'catalog',
+ label: 'catalog1',
+ _id: 6,
+ }, ['parent', 'server_group1', 'server1', 'database1']);
+ pgBrowser.treeMenu.addChild(database1, catalog1);
+ });
+
+ context('cannot find the tree node', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: '10'}]);
+ expect(datagrid.create_transaction).not.toHaveBeenCalled();
+ });
+
+ it('display alert', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: '10'}]);
+ expect(alertify.alert).toHaveBeenCalledWith(
+ 'Data Grid Error',
+ 'No object selected.'
+ );
+ });
+ });
+
+ context('current node is not underneath a server', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: 'parent'}]);
+ expect(datagrid.create_transaction).not.toHaveBeenCalled();
+ });
+ });
+
+ context('current node is not underneath a schema or view or catalog', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: 'database1'}]);
+ expect(datagrid.create_transaction).not.toHaveBeenCalled();
+ });
+ });
+
+ context('current node is underneath a schema', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {mnuid: 11}, [{id: 'schema1'}]);
+ expect(datagrid.create_transaction).toHaveBeenCalledWith(
+ '/initialize/datagrid/11/schema/1/2/3/4',
+ null,
+ 'false',
+ 'pg',
+ '',
+ 'server1 - database1 - schema1.schema1',
+ ''
+ );
+ });
+ });
+
+ context('current node is underneath a view', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {mnuid: 11}, [{id: 'view1'}]);
+ expect(datagrid.create_transaction).toHaveBeenCalledWith(
+ '/initialize/datagrid/11/view/1/2/3/5',
+ null,
+ 'false',
+ 'pg',
+ '',
+ 'server1 - database1 - view1.view1',
+ ''
+ );
+ });
+ });
+
+ context('current node is underneath a catalog', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {mnuid: 11}, [{id: 'catalog1'}]);
+ expect(datagrid.create_transaction).toHaveBeenCalledWith(
+ '/initialize/datagrid/11/catalog/1/2/3/6',
+ null,
+ 'false',
+ 'pg',
+ '',
+ 'server1 - database1 - catalog1.catalog1',
+ ''
+ );
+ });
+ });
+});
diff --git a/web/regression/javascript/datagrid/show_query_tool_spec.js b/web/regression/javascript/datagrid/show_query_tool_spec.js
new file mode 100644
index 00000000..e58112c0
--- /dev/null
+++ b/web/regression/javascript/datagrid/show_query_tool_spec.js
@@ -0,0 +1,125 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {TreeFake} from '../tree/tree_fake';
+import {showQueryTool} from '../../../pgadmin/static/js/datagrid/show_query_tool';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+const context = describe;
+
+describe('#showQueryTool', () => {
+ let queryTool;
+ let pgBrowser;
+ let alertify;
+ beforeEach(() => {
+ alertify = jasmine.createSpyObj('alertify', ['alert']);
+ queryTool = {
+ create_transaction: jasmine.createSpy('create_transaction'),
+ };
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server_group: {
+ _type: 'server_group',
+ hasId: true,
+ },
+ server: {
+ _type: 'server',
+ hasId: true,
+ },
+ database: {
+ _type: 'database',
+ hasId: true,
+ },
+ },
+ };
+ const parent = pgBrowser.treeMenu.addNewNode('parent', {_type: 'parent'});
+ const serverGroup1 = new TreeNode('server_group1', {
+ _type: 'server_group',
+ _id: 1,
+ }, ['parent']);
+ pgBrowser.treeMenu.addChild(parent, serverGroup1);
+
+ const server1 = new TreeNode('server1', {
+ _type: 'server',
+ label: 'server1',
+ server_type: 'pg',
+ _id: 2,
+ });
+ pgBrowser.treeMenu.addChild(serverGroup1, server1);
+
+ const database1 = new TreeNode('database1', {
+ _type: 'database',
+ label: 'database1',
+ _id: 3,
+ });
+ pgBrowser.treeMenu.addChild(server1, database1);
+ });
+
+ context('cannot find the tree node', () => {
+ beforeEach(() => {
+ showQueryTool(queryTool, pgBrowser, alertify, '', [{id: '10'}], 'title');
+ });
+ it('does not create a transaction', () => {
+ expect(queryTool.create_transaction).not.toHaveBeenCalled();
+ });
+
+ it('display alert', () => {
+ expect(alertify.alert).toHaveBeenCalledWith(
+ 'Query Tool Error',
+ 'No object selected.'
+ );
+ });
+ });
+
+ context('current node is not underneath a server', () => {
+ it('does not create a transaction', () => {
+ showQueryTool(queryTool, pgBrowser, alertify, '', [{id: 'parent'}], 'title');
+ expect(queryTool.create_transaction).not.toHaveBeenCalled();
+ });
+
+ it('no alert is displayed', () => {
+ expect(alertify.alert).not.toHaveBeenCalled();
+ });
+ });
+
+ context('current node is underneath a server', () => {
+ context('current node is not underneath a database', () => {
+ it('creates a transaction', () => {
+ showQueryTool(queryTool, pgBrowser, alertify, 'http://someurl', [{id: 'server1'}], 'title');
+ expect(queryTool.create_transaction).toHaveBeenCalledWith(
+ '/initialize/query_tool/1/2',
+ null,
+ 'true',
+ 'pg',
+ 'http://someurl',
+ 'title',
+ '',
+ false
+ );
+ });
+ });
+
+ context('current node is underneath a database', () => {
+ it('creates a transaction', () => {
+ showQueryTool(queryTool, pgBrowser, alertify, 'http://someurl', [{id: 'database1'}], 'title');
+ expect(queryTool.create_transaction).toHaveBeenCalledWith(
+ '/initialize/query_tool/1/2/3',
+ null,
+ 'true',
+ 'pg',
+ 'http://someurl',
+ 'title',
+ '',
+ false
+ );
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/debugger_utils_spec.js b/web/regression/javascript/debugger_utils_spec.js
index 3e6be976..65f95dde 100644
--- a/web/regression/javascript/debugger_utils_spec.js
+++ b/web/regression/javascript/debugger_utils_spec.js
@@ -16,12 +16,12 @@ describe('debuggerUtils', function () {
let tab_key = {
which: 9,
keyCode: 9,
- }
+ };
let enter_key = {
which: 13,
keyCode: 13,
- }
+ };
describe('debuggerUtils', function () {
it('returns undefined if no command is passed', function () {
@@ -31,7 +31,7 @@ describe('debuggerUtils', function () {
describe('debuggerUtils', function () {
it('should call focus on editor', function () {
- setFocusToDebuggerEditor(editor, enter_key)
+ setFocusToDebuggerEditor(editor, enter_key);
expect(editor.focus).toHaveBeenCalled();
});
});
diff --git a/web/regression/javascript/dialog_tab_navigator_spec.js b/web/regression/javascript/dialog_tab_navigator_spec.js
index d4082c87..8321e4f2 100644
--- a/web/regression/javascript/dialog_tab_navigator_spec.js
+++ b/web/regression/javascript/dialog_tab_navigator_spec.js
@@ -10,11 +10,11 @@ import dialogTabNavigator from 'sources/dialog_tab_navigator';
import $ from 'jquery';
import 'bootstrap';
- describe('dialogTabNavigator', function () {
- let dialog, tabNavigator, backward_shortcut, forward_shortcut;
+describe('dialogTabNavigator', function () {
+ let dialog, tabNavigator, backward_shortcut, forward_shortcut;
- beforeEach(() => {
- let dialogHtml =$('<div tabindex="1" class="backform-tab" role="tabpanel">'+
+ beforeEach(() => {
+ let dialogHtml =$('<div tabindex="1" class="backform-tab" role="tabpanel">'+
' <ul class="nav nav-tabs" role="tablist">'+
' <li role="presentation" class="active">'+
' <a data-toggle="tab" tabindex="-1" data-tab-index="1" href="#1" aria-controls="1"> General</a>'+
@@ -52,64 +52,64 @@ import 'bootstrap';
' </ul>'+
'</div>');
- dialog = {};
+ dialog = {};
- dialog.el = dialogHtml[0];
- dialog.$el = dialogHtml;
+ dialog.el = dialogHtml[0];
+ dialog.$el = dialogHtml;
- backward_shortcut = {
- 'alt': false,
- 'shift': true,
- 'control': true,
- 'key': {'key_code': 91, 'char': '['}
- };
+ backward_shortcut = {
+ 'alt': false,
+ 'shift': true,
+ 'control': true,
+ 'key': {'key_code': 91, 'char': '['},
+ };
- forward_shortcut = {
- 'alt': false,
- 'shift': true,
- 'control': true,
- 'key': {'key_code': 93, 'char': ']'}
- };
+ forward_shortcut = {
+ 'alt': false,
+ 'shift': true,
+ 'control': true,
+ 'key': {'key_code': 93, 'char': ']'},
+ };
- tabNavigator = new dialogTabNavigator.dialogTabNavigator(
+ tabNavigator = new dialogTabNavigator.dialogTabNavigator(
dialog, backward_shortcut, forward_shortcut);
- });
+ });
- describe('navigate', function () {
+ describe('navigate', function () {
- beforeEach(() => {
- spyOn(tabNavigator, 'navigateBackward').and.callThrough();
-
- spyOn(tabNavigator, 'navigateForward').and.callThrough();
- });
+ beforeEach(() => {
+ spyOn(tabNavigator, 'navigateBackward').and.callThrough();
- it('navigate backward', function () {
- tabNavigator.onKeyboardEvent({}, 'shift+ctrl+[');
+ spyOn(tabNavigator, 'navigateForward').and.callThrough();
+ });
- expect(tabNavigator.navigateBackward).toHaveBeenCalled();
+ it('navigate backward', function () {
+ tabNavigator.onKeyboardEvent({}, 'shift+ctrl+[');
- expect(tabNavigator.navigateForward).not.toHaveBeenCalled();
+ expect(tabNavigator.navigateBackward).toHaveBeenCalled();
- });
+ expect(tabNavigator.navigateForward).not.toHaveBeenCalled();
- it('navigate forward', function () {
- tabNavigator.onKeyboardEvent({}, 'shift+ctrl+]');
+ });
- expect(tabNavigator.navigateForward).toHaveBeenCalled();
+ it('navigate forward', function () {
+ tabNavigator.onKeyboardEvent({}, 'shift+ctrl+]');
- expect(tabNavigator.navigateBackward).not.toHaveBeenCalled();
+ expect(tabNavigator.navigateForward).toHaveBeenCalled();
- });
+ expect(tabNavigator.navigateBackward).not.toHaveBeenCalled();
- it('should not navigate', function () {
- tabNavigator.onKeyboardEvent({}, 'shift+ctrl+a');
+ });
- expect(tabNavigator.navigateForward).not.toHaveBeenCalled();
+ it('should not navigate', function () {
+ tabNavigator.onKeyboardEvent({}, 'shift+ctrl+a');
- expect(tabNavigator.navigateBackward).not.toHaveBeenCalled();
+ expect(tabNavigator.navigateForward).not.toHaveBeenCalled();
- });
+ expect(tabNavigator.navigateBackward).not.toHaveBeenCalled();
});
- });
\ No newline at end of file
+ });
+
+});
\ No newline at end of file
diff --git a/web/regression/javascript/fake_endpoints.js b/web/regression/javascript/fake_endpoints.js
index 63ab05dc..c060ba78 100644
--- a/web/regression/javascript/fake_endpoints.js
+++ b/web/regression/javascript/fake_endpoints.js
@@ -11,6 +11,12 @@ define(function () {
return {
'static': '/base/pgadmin/static/<path:filename>',
'sqleditor.poll': '/sqleditor/query_tool/poll/<path:trans_id>',
- 'sqleditor.query_tool_start': '/sqleditor/query_tool/start/<path:trans_id>'
+ 'sqleditor.query_tool_start': '/sqleditor/query_tool/start/<path:trans_id>',
+ 'backup.create_server_job': '/backup/job/<int:sid>',
+ 'backup.create_object_job': '/backup/job/<int:sid>/object',
+ 'datagrid.initialize_datagrid': '/initialize/datagrid/<int:cmd_type>/<obj_type>/<int:sgid>/<int:sid>/<int:did>/<int:obj_id>',
+ 'datagrid.initialize_query_tool': '/initialize/query_tool/<int:sgid>/<int:sid>',
+ 'datagrid.initialize_query_tool_with_did': '/initialize/query_tool/<int:sgid>/<int:sid>/<int:did>',
+ 'restore.create_job': '/restore/job/<int:sid>',
};
});
diff --git a/web/regression/javascript/fake_model.js b/web/regression/javascript/fake_model.js
new file mode 100644
index 00000000..acfaa532
--- /dev/null
+++ b/web/regression/javascript/fake_model.js
@@ -0,0 +1,21 @@
+export class FakeModel {
+ constructor() {
+ this.values = {};
+ }
+
+ set(key, value) {
+ this.values[key] = value;
+ }
+
+ get(key) {
+ return this.values[key];
+ }
+
+ unset(key) {
+ delete this.values[key];
+ }
+
+ toJSON() {
+ return Object.assign({}, this.values);
+ }
+}
diff --git a/web/regression/javascript/menu/can_create_spec.js b/web/regression/javascript/menu/can_create_spec.js
new file mode 100644
index 00000000..5ee030f2
--- /dev/null
+++ b/web/regression/javascript/menu/can_create_spec.js
@@ -0,0 +1,98 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+/////////////////////////////////////////////////////////////
+
+import {canCreate} from '../../../pgadmin/static/js/menu/can_create';
+import {TreeFake} from '../tree/tree_fake';
+
+const context = describe;
+
+describe('#canCreate', () => {
+ let ourBrowser;
+ let data;
+ let tree;
+
+ beforeEach(() => {
+ tree = new TreeFake();
+ ourBrowser = {
+ treeMenu: tree,
+ };
+
+ tree.addNewNode('level1', {}, undefined, []);
+ });
+
+ context('data is not null and check is false ', () => {
+ beforeEach(() => {
+ data = {action: 'create', check: false};
+ });
+ it('returns true', () => {
+ expect(canCreate({}, {}, {}, data)).toBe(true);
+ });
+ });
+
+ context('data is not null and check is true', () => {
+ beforeEach(() => {
+ data = {action: 'create', check: true};
+ });
+
+ context('is node with type schema', () => {
+ beforeEach(() => {
+ tree.addNewNode('level2', {_type: 'schema'}, [{id: 'level2'}], ['level1']);
+ });
+
+ it('returns true', () => {
+ expect(canCreate(ourBrowser, 'coll-table', [{id: 'level2'}], data)).toBe(true);
+ });
+ });
+
+ context('has ancestor with type schema', () => {
+ beforeEach(() => {
+ tree.addNewNode('level2', {_type: 'schema'}, undefined, ['level1']);
+ tree.addNewNode('level3', {_type: 'database'}, [{id: 'level3'}], ['level1', 'level2']);
+ });
+
+ it('returns true', () => {
+ expect(canCreate(ourBrowser, 'coll-table', [{id: 'level3'}], data)).toBe(true);
+ });
+ });
+
+ context('when type is not "coll-table"', () => {
+ beforeEach(() => {
+ tree.addNewNode('level2', {_type: 'database'}, undefined, ['level1']);
+ });
+
+ it('returns true', () => {
+ expect(canCreate(ourBrowser, 'coll-table', [{id: 'level2'}], data)).toBe(true);
+ });
+ });
+
+ context('when type is "coll-table"', () => {
+ context('when parent type is "catalog"', () => {
+ beforeEach(() => {
+ tree.addNewNode('level2', {_type: 'catalog'}, undefined, ['level1']);
+ tree.addNewNode('level3', {_type: 'coll-table'}, [{id: 'level3'}], ['level1', 'level2']);
+ });
+
+ it('returns false', () => {
+ expect(canCreate(ourBrowser, 'coll-table', [{id: 'level3'}], data)).toBe(false);
+ });
+ });
+
+ context('when parent type is not "catalog"', () => {
+ beforeEach(() => {
+ tree.addNewNode('level2', {_type: 'database'}, undefined, ['level1']);
+ tree.addNewNode('level3', {_type: 'coll-table'}, [{id: 'level3'}], ['level1', 'level2']);
+ });
+
+ it('returns false', () => {
+ expect(canCreate(ourBrowser, 'coll-table', [{id: 'level3'}], data)).toBe(true);
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/menu/menu_enabled_spec.js b/web/regression/javascript/menu/menu_enabled_spec.js
new file mode 100644
index 00000000..06967d77
--- /dev/null
+++ b/web/regression/javascript/menu/menu_enabled_spec.js
@@ -0,0 +1,131 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {TreeFake} from '../tree/tree_fake';
+import {menuEnabled} from '../../../pgadmin/static/js/menu/menu_enabled';
+
+const context = describe;
+describe('#menuEnabled', () => {
+ let ourBrowser;
+ beforeEach(() => {
+ const tree = new TreeFake();
+ ourBrowser = {
+ treeMenu: tree,
+ };
+ tree.addNewNode('level1', {}, undefined, []);
+ tree.addNewNode('level1.1', {_type: 'catalog'}, undefined, ['level1']);
+ tree.addNewNode('level1.1.1', {_type: 'database'}, undefined, ['level1', 'level1.1']);
+ tree.addNewNode('level1.2', {_type: 'bamm'}, undefined, ['level1']);
+ tree.addNewNode('level1.2.1', {
+ _type: 'database',
+ allowConn: true,
+ }, undefined, ['level1', 'level1.2']);
+ tree.addNewNode('level1.2.2', {
+ _type: 'database',
+ allowConn: false,
+ }, undefined, ['level1', 'level1.2']);
+ tree.addNewNode('level1.2.3', {
+ _type: 'table',
+ }, undefined, ['level1', 'level1.2']);
+
+ tree.addNewNode('level2', {}, undefined, []);
+ tree.addNewNode('level2.1', null, undefined, ['level2']);
+ tree.addNewNode('level2.1.1', {}, undefined, ['level2', 'level2.1']);
+ });
+
+ context('When the current node is a root node', () => {
+ it('return false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu, [], {}, [{id: 'level1'}])).toBe(false);
+ });
+ });
+
+ context('when current node does not exist', () => {
+ it('return false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu, [], {}, [{id: 'bamm'}])).toBe(false);
+ });
+ });
+
+ context('provided node data is undefined', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu, [], undefined, [{id: 'level1'}])).toBe(false);
+ });
+ });
+
+ context('provided node data is null', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu, [], null, [{id: 'level1'}])).toBe(false);
+ });
+ });
+
+ context('When the current node is not a root node', () => {
+ context('parent data does not exist', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu, [], {}, [{id: 'level2.1.1'}])).toBe(false);
+ });
+ });
+
+ context('parent as data', () => {
+ context('the current node type is in the supported node types', () => {
+ context('the parent is of the type catalog', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu,
+ ['schema'],
+ {_type: 'schema'},
+ [{id: 'level1.1.1'}]
+ )).toBe(false);
+ });
+ });
+ context('an ancestor with type catalog exists', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu,
+ ['table'],
+ {_type: 'table'},
+ [{id: 'level1.1.1.1'}]
+ )).toBe(false);
+ });
+ });
+ context('the parent is not of the type catalog', () => {
+ context('current node is of the type database', () => {
+ context('current node allows connection', () => {
+ it('returns true', () => {
+ expect(menuEnabled(ourBrowser.treeMenu,
+ ['database'],
+ {
+ _type: 'database',
+ allowConn: true,
+ }, [{id: 'level1.2.1'}])).toBe(true);
+ });
+ });
+ context('current node do not allow connection', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu,
+ ['database'], {
+ _type: 'database',
+ allowConn: false,
+ }, [{id: 'level1.2.2'}])).toBe(false);
+ });
+ });
+ });
+ context('current node is not of the type database', () => {
+ it('returns true', () => {
+ expect(menuEnabled(ourBrowser.treeMenu,
+ ['schema'], {
+ _type: 'schema',
+ }, [{id: 'level1.2.3'}])).toBe(true);
+ });
+ });
+ });
+ });
+ context('the current node type is not in the supported node types', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu, [], {_type: 'catalog'}, [{id: 'level1.1'}])).toBe(false);
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/misc/statistics/statistics_spec.js b/web/regression/javascript/misc/statistics/statistics_spec.js
index 40c0065e..e63ffad9 100644
--- a/web/regression/javascript/misc/statistics/statistics_spec.js
+++ b/web/regression/javascript/misc/statistics/statistics_spec.js
@@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////////////////
-import {nodeHasStatistics} from "../../../../pgadmin/static/js/misc/statistics/statistics";
+import {nodeHasStatistics} from '../../../../pgadmin/static/js/misc/statistics/statistics';
describe('#nodeHasStatistics', () => {
describe('when node hasStatistics is not a function', () => {
diff --git a/web/regression/javascript/restore/restore_dialog_spec.js b/web/regression/javascript/restore/restore_dialog_spec.js
new file mode 100644
index 00000000..48ef5961
--- /dev/null
+++ b/web/regression/javascript/restore/restore_dialog_spec.js
@@ -0,0 +1,181 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {TreeFake} from '../tree/tree_fake';
+import {
+ RestoreDialog,
+} from '../../../pgadmin/static/js/restore/restore_dialog';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+const context = describe;
+
+describe('RestoreDialog', () => {
+ let restoreDialog;
+ let pgBrowser;
+ let jquerySpy;
+ let alertifySpy;
+ let restoreModelSpy;
+
+
+ let rootNode;
+ let serverTreeNode;
+ let databaseTreeNode;
+ let ppasServerTreeNode;
+ let noDataNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: jasmine.createSpyObj('Node[server]', ['getTreeNodeHierarchy']),
+ database: jasmine.createSpyObj('Node[database]', ['getTreeNodeHierarchy']),
+ },
+ };
+ pgBrowser.Nodes.server.hasId = true;
+ pgBrowser.Nodes.database.hasId = true;
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ restoreModelSpy = jasmine.createSpy('restoreModelSpy');
+
+ rootNode = pgBrowser.treeMenu.addNewNode('level1', {}, [{id: 'level1'}], []);
+ serverTreeNode = new TreeNode('level1.1', {
+ _type: 'server',
+ _id: 10,
+ label: 'some-tree-label',
+ }, [{id: 'level1.1'}]);
+ pgBrowser.treeMenu.addChild(rootNode, serverTreeNode);
+
+ databaseTreeNode = new TreeNode('level1.1.1', {
+ _type: 'database',
+ _id: 10,
+ _label: 'some-database-label',
+ }, [{id: 'level1.1.1'}], ['level1', 'level1.1']);
+ pgBrowser.treeMenu.addChild(serverTreeNode, databaseTreeNode);
+
+ ppasServerTreeNode = new TreeNode('level1.2', {
+ _type: 'server',
+ server_type: 'ppas',
+ }, [{id: 'level1.2'}], ['level1']);
+ pgBrowser.treeMenu.addChild(rootNode, ppasServerTreeNode);
+
+ const level3 = new TreeNode('level3', {}, [{id: 'level3'}]);
+ pgBrowser.treeMenu.addChild(ppasServerTreeNode, level3);
+
+ noDataNode = pgBrowser.treeMenu.addNewNode('level3.1', undefined, [{id: 'level1'}]);
+ pgBrowser.treeMenu.addChild(level3, noDataNode);
+
+ });
+
+ describe('#draw', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ alertifySpy['pg_restore'] = jasmine.createSpy('pg_restore');
+ restoreDialog = new RestoreDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ restoreModelSpy
+ );
+
+ pgBrowser.get_preference = jasmine.createSpy('get_preferences');
+ });
+
+ context('there are no ancestors of the type server', () => {
+ it('does not create a dialog', () => {
+ pgBrowser.treeMenu.selectNode([{id: 'level1'}]);
+ restoreDialog.draw(null, null, null);
+ expect(alertifySpy['pg_restore']).not.toHaveBeenCalled();
+ });
+
+ it('display an alert with a Restore Error', () => {
+ restoreDialog.draw(null, [{id: 'level1'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Restore Error',
+ 'Please select server or child node from the browser tree.'
+ );
+ });
+ });
+
+ context('there is an ancestor of the type server', () => {
+ context('no preference can be found', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue(undefined);
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Restore Error"', () => {
+ restoreDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Restore Error',
+ 'Failed to load preference pg_bin_dir of module paths'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Restore Error"', () => {
+ restoreDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Restore Error',
+ 'Failed to load preference ppas_bin_dir of module paths'
+ );
+ });
+ });
+ });
+
+ context('preference can be found', () => {
+ context('binary folder is not configured', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue({});
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ restoreDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the PostgreSQL Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ restoreDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the EDB Advanced Server Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+ });
+
+ context('binary folder is configured', () => {
+ let spy;
+ beforeEach(() => {
+ spy = jasmine.createSpyObj('globals', ['resizeTo']);
+ alertifySpy['pg_restore'].and
+ .returnValue(spy);
+ pgBrowser.get_preference.and.returnValue({value: '/some/path'});
+ pgBrowser.Nodes.server.label = 'some-server-label';
+ });
+
+ it('displays the dialog', () => {
+ restoreDialog.draw(null, [{id: 'level1.1'}], {server: true});
+ expect(alertifySpy['pg_restore']).toHaveBeenCalledWith(
+ 'Restore (some-server-label: some-tree-label)',
+ [{id: 'level1.1'}],
+ serverTreeNode.getData(),
+ pgBrowser.Nodes.server
+ );
+ expect(spy.resizeTo).toHaveBeenCalledWith('65%', '60%');
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/restore/restore_dialog_wrapper_spec.js b/web/regression/javascript/restore/restore_dialog_wrapper_spec.js
new file mode 100644
index 00000000..84de8e62
--- /dev/null
+++ b/web/regression/javascript/restore/restore_dialog_wrapper_spec.js
@@ -0,0 +1,593 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {TreeFake} from '../tree/tree_fake';
+import {RestoreDialogWrapper} from '../../../pgadmin/static/js/restore/restore_dialog_wrapper';
+import MockAdapter from 'axios-mock-adapter';
+import axios from 'axios/index';
+import {FakeModel} from '../fake_model';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+let context = describe;
+
+describe('RestoreDialogWrapper', () => {
+ let jquerySpy;
+ let pgBrowser;
+ let alertifySpy;
+ let dialogModelKlassSpy;
+ let backform;
+ let generatedRestoreModel;
+ let restoreDialogWrapper;
+ let noDataNode;
+ let serverTreeNode;
+ let viewSchema;
+ let restoreJQueryContainerSpy;
+ let restoreNodeChildNodeSpy;
+ let restoreNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: {
+ hasId: true,
+ getTreeNodeHierarchy: jasmine.createSpy('getTreeNodeHierarchy'),
+ },
+ },
+ keyboardNavigation: jasmine.createSpyObj('keyboardNavigation', ['getDialogTabNavigator']),
+ };
+
+ noDataNode = pgBrowser.treeMenu.addNewNode('level1.1', undefined, [{id: 'level1'}]);
+ serverTreeNode = pgBrowser.treeMenu.addNewNode('level2.1', {
+ _type: 'server',
+ _id: 10,
+ label: 'some-tree-label',
+ }, [{id: 'level2.1'}]);
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ dialogModelKlassSpy = jasmine.createSpy('dialogModelKlass');
+ generatedRestoreModel = {};
+ viewSchema = {};
+ backform = jasmine.createSpyObj('backform', ['generateViewSchema', 'Dialog']);
+ backform.generateViewSchema.and.returnValue(viewSchema);
+ dialogModelKlassSpy.and.returnValue(generatedRestoreModel);
+ restoreJQueryContainerSpy = jasmine.createSpyObj('restoreJQueryContainer', ['get', 'attr']);
+ restoreJQueryContainerSpy.get.and.returnValue(restoreJQueryContainerSpy);
+
+ restoreNode = {
+ __internal: {
+ buttons: [
+ {}, {},
+ {
+ element: {
+ disabled: false,
+ },
+ },
+ ],
+ },
+ elements: {
+ body: {
+ childNodes: [
+ {},
+ ],
+ },
+ content: jasmine.createSpyObj('content', ['appendChild', 'attr']),
+ },
+ };
+
+
+ restoreNodeChildNodeSpy = jasmine.createSpyObj('something', ['addClass']);
+
+ jquerySpy.and.callFake((selector) => {
+ if (selector === '<div class=\'restore_dialog\'></div>') {
+ return restoreJQueryContainerSpy;
+ } else if (selector === restoreNode.elements.body.childNodes[0]) {
+ return restoreNodeChildNodeSpy;
+ }
+ });
+ });
+
+ describe('#prepare', () => {
+
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ restoreDialogWrapper = new RestoreDialogWrapper(
+ '<div class=\'restore_dialog\'></div>',
+ 'restoreDialogTitle',
+ 'restore',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+ restoreDialogWrapper = Object.assign(restoreDialogWrapper, restoreNode);
+ });
+ context('no tree element is selected', () => {
+ it('does not create a backform dialog', () => {
+ restoreDialogWrapper.prepare();
+ expect(backform.Dialog).not.toHaveBeenCalled();
+ });
+
+ it('disables the button "submit button" until a filename is selected', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreDialogWrapper.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('selected tree node has no data', () => {
+ beforeEach(() => {
+ pgBrowser.treeMenu.selectNode(noDataNode.domNode);
+ });
+
+ it('does not create a backform dialog', () => {
+ restoreDialogWrapper.prepare();
+ expect(backform.Dialog).not.toHaveBeenCalled();
+ });
+
+ it('disables the button "submit button" until a filename is selected', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreDialogWrapper.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('tree element is selected', () => {
+ let treeHierarchyInformation;
+ let dialogSpy;
+ beforeEach(() => {
+ treeHierarchyInformation = {
+ server: {
+ _type: 'server',
+ _id: 10,
+ priority: 0,
+ label: 'some-tree-label',
+ },
+ };
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ pgBrowser.Nodes['server'].getTreeNodeHierarchy.and
+ .returnValue(treeHierarchyInformation);
+ dialogSpy = jasmine.createSpyObj('newView', ['render']);
+ dialogSpy.$el = jasmine.createSpyObj('$el', ['find', 'attr']);
+ dialogSpy.model = jasmine.createSpyObj('model', ['on']);
+ dialogSpy.$el.find.and.returnValue([]);
+
+ backform.Dialog.and.returnValue(dialogSpy);
+ });
+
+ it('creates a backform dialog and displays it', () => {
+ restoreDialogWrapper.prepare();
+ expect(backform.Dialog).toHaveBeenCalledWith({
+ el: restoreJQueryContainerSpy,
+ model: generatedRestoreModel,
+ schema: viewSchema,
+ });
+
+ expect(dialogSpy.render).toHaveBeenCalled();
+ });
+
+ it('add alertify classes to restore node childnode', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreNodeChildNodeSpy.addClass)
+ .toHaveBeenCalledWith('alertify_tools_dialog_properties obj_properties');
+ });
+
+ it('disables the button submit button until a filename is selected', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreNode.__internal.buttons[2].element.disabled).toBe(true);
+ });
+
+ it('generates a new restore model', () => {
+ restoreDialogWrapper.prepare();
+ expect(dialogModelKlassSpy).toHaveBeenCalledWith(
+ {node_data: pgBrowser.Nodes['server']},
+ {node_info: treeHierarchyInformation}
+ );
+ });
+
+ it('add the new dialog to the restore node HTML', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreNode.elements.content.appendChild).toHaveBeenCalledWith(restoreJQueryContainerSpy);
+ });
+ });
+ });
+
+ describe('onButtonClicked', () => {
+ let networkMock;
+
+ beforeEach(() => {
+ pgBrowser.showHelp = jasmine.createSpy('showHelp');
+ networkMock = new MockAdapter(axios);
+ alertifySpy = jasmine.createSpyObj('alertify', ['success', 'alert']);
+ restoreDialogWrapper = new RestoreDialogWrapper(
+ '<div class=\'restore_dialog\'></div>',
+ 'restoreDialogTitle',
+ 'restore',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+ restoreDialogWrapper = Object.assign(restoreDialogWrapper, restoreNode);
+
+ });
+
+ afterEach(function () {
+ networkMock.restore();
+ });
+
+ context('dialog help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ element: {
+ name: 'dialog_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ },
+ },
+ },
+ };
+ restoreDialogWrapper.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'dialog_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ );
+ });
+
+ it('does not start the restore', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('object help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ element: {
+ name: 'object_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ },
+ },
+ },
+ };
+ restoreDialogWrapper.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'object_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ );
+ });
+
+ it('does not start the restore', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('restore button was pressed', () => {
+ let networkCalled;
+ let event;
+
+ context('no tree node is selected', () => {
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+ event = {
+ button: {
+ 'data-btn-name': 'restore',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ });
+
+ it('does not start the restore', () => {
+ restoreDialogWrapper.callback(event);
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('tree node selected has no data', () => {
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+ event = {
+ button: {
+ 'data-btn-name': 'restore',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ pgBrowser.treeMenu.selectNode(noDataNode.domNode);
+ });
+
+ it('does not start the restore', () => {
+ restoreDialogWrapper.callback(event);
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('tree node select has data', () => {
+
+ let databaseTreeNode;
+
+ beforeEach(() => {
+ databaseTreeNode = pgBrowser.treeMenu.addNewNode('level3.1', {
+ _type: 'database',
+ _id: 10,
+ _label: 'some-database-label',
+ }, [{id: 'level3.1'}]);
+ pgBrowser.treeMenu.addChild(serverTreeNode, databaseTreeNode);
+ pgBrowser.Nodes.database = {
+ hasId: true,
+ _label: 'some-database-label',
+ };
+ let fakeModel = new FakeModel();
+ fakeModel.set('some-key', 'some-value');
+ restoreDialogWrapper.view = {
+ model: fakeModel,
+ };
+ pgBrowser.treeMenu.selectNode(databaseTreeNode.domNode);
+ pgBrowser.Events = jasmine.createSpyObj('pgBrowserEventsSpy', ['trigger']);
+ event = {
+ button: {
+ 'data-btn-name': 'restore',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ });
+ context('restore job created successfully', () => {
+ let dataSentToServer;
+ beforeEach(() => {
+ networkMock.onPost('/restore/job/10').reply((request) => {
+ dataSentToServer = request.data;
+ return [200, {}];
+ });
+ });
+
+ it('create an success alert box', (done) => {
+ restoreDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.success).toHaveBeenCalledWith(
+ 'Restore job created.',
+ 5
+ );
+ done();
+ }, 0);
+ });
+
+ it('trigger background process', (done) => {
+ restoreDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(pgBrowser.Events.trigger).toHaveBeenCalledWith(
+ 'pgadmin-bgprocess:created',
+ restoreDialogWrapper
+ );
+ done();
+ }, 0);
+ });
+
+ it('send correct data to server', (done) => {
+ restoreDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(JSON.parse(dataSentToServer)).toEqual({
+ 'some-key': 'some-value',
+ 'database': 'some-database-label',
+ });
+ done();
+ }, 0);
+ });
+ });
+
+ context('error creating restore job', () => {
+ beforeEach(() => {
+ networkMock.onPost('/restore/job/10').reply(() => {
+ return [400, {}];
+ });
+ });
+
+ it('creates an alert box', (done) => {
+ restoreDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Restore job failed.',
+ undefined
+ );
+ done();
+ }, 0);
+ });
+ });
+ });
+ });
+ });
+
+ describe('setExtraParameters', () => {
+ let selectedNode;
+ let treeInfo;
+ let model;
+
+ beforeEach(() => {
+ restoreDialogWrapper = new RestoreDialogWrapper(
+ '<div class=\'restore_dialog\'></div>',
+ 'restoreDialogTitle',
+ 'restore',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ model = new FakeModel();
+ restoreDialogWrapper.view = {
+ model: model,
+ };
+ });
+
+ context('when it is a custom model', () => {
+ beforeEach(() => {
+ model.set('custom', true);
+ treeInfo = {
+ 'database': {
+ '_label': 'some-database-label',
+ },
+ };
+ });
+
+ it('only sets the database', () => {
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ 'custom': true,
+ 'database': 'some-database-label',
+ });
+ });
+ });
+
+ context('when it is not a custom model', () => {
+ beforeEach(() => {
+ model.set('custom', false);
+ treeInfo = {
+ 'database': {
+ '_label': 'some-database-label',
+ },
+ 'schema': {
+ '_label': 'some-schema-label',
+ },
+ };
+ });
+
+ context('when selected node is a schema', () => {
+ it('sets schemas on the model', () => {
+ selectedNode = new TreeNode('schema', {_type: 'schema', _label: 'some-schema-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ });
+ });
+ });
+
+ context('when selected node is a table', () => {
+ it('sets schemas and table on the model', () => {
+ selectedNode = new TreeNode('table', {_type: 'table', _label: 'some-table-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ tables: ['some-table-label'],
+ });
+ });
+ });
+
+ context('when selected node is a function', () => {
+ it('sets schemas and function on the model', () => {
+ selectedNode = new TreeNode('function', {_type: 'function', _label: 'some-function-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ functions: ['some-function-label'],
+ });
+ });
+ });
+
+ context('when selected node is an index', () => {
+ it('sets schemas and index on the model', () => {
+ selectedNode = new TreeNode('index', {_type: 'index', _label: 'some-index-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ indexes: ['some-index-label'],
+ });
+ });
+ });
+
+ context('when selected node is a trigger', () => {
+ it('sets schemas and trigger on the model', () => {
+ selectedNode = new TreeNode('trigger', {_type: 'trigger', _label: 'some-trigger-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ triggers: ['some-trigger-label'],
+ });
+ });
+ });
+
+ context('when selected node is a trigger_func', () => {
+ it('sets schemas and trigger_func on the model', () => {
+ selectedNode = new TreeNode('trigger_func', {_type: 'trigger_func', _label: 'some-trigger_func-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ trigger_funcs: ['some-trigger_func-label'],
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/schema/can_drop_child_spec.js b/web/regression/javascript/schema/can_drop_child_spec.js
new file mode 100644
index 00000000..388dffbc
--- /dev/null
+++ b/web/regression/javascript/schema/can_drop_child_spec.js
@@ -0,0 +1,82 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {canDropChild} from '../../../pgadmin/static/js/schema/can_drop_child';
+import {TreeFake} from '../tree/tree_fake';
+
+let context = describe;
+
+describe('can_drop_child', () => {
+
+ let browser;
+ let itemData;
+ let item;
+
+ beforeEach(() => {
+ browser = {
+ treeMenu: new TreeFake(),
+ };
+ item = [];
+ browser.treeMenu.addNewNode('node1', {_type: 'schema'}, [{id: 'node1'}], []);
+ browser.treeMenu.addNewNode('node1.1', {_type: 'database'}, [{id: 'node1.1'}], ['node1']);
+ browser.treeMenu.addNewNode('node2', {_type: 'catalog'}, [{id: 'node2'}], []);
+ browser.treeMenu.addNewNode('node2.1', {_type: 'table'}, [{id: 'node2.1'}], ['node2']);
+ browser.treeMenu.addNewNode('node3', {_type: 'function'}, [{id: 'node3'}], []);
+ browser.treeMenu.addNewNode('node3.1', {_type: 'procedure'}, [{id: 'node3.1'}], ['node3']);
+ });
+
+ context('when current node is of the type schema', () => {
+ beforeEach(() => {
+ itemData = {
+ _type: 'schema',
+ };
+ item = [{id: 'node1'}];
+ });
+
+ it('returns true', () => {
+ let bool = canDropChild(browser, itemData, item);
+ expect(bool).toBe(true);
+ });
+ });
+
+ context('when a parent of the current node is a schema', () => {
+ it('returns true', () => {
+ itemData = {
+ _type: 'database',
+ };
+ item = [{id: 'node1.1'}];
+ let bool = canDropChild(browser, itemData, item);
+ expect(bool).toBe(true);
+ });
+ });
+
+ context('when a parent of the current node is a catalog', () => {
+ it('returns false', () => {
+ itemData= {
+ _type: 'table',
+ };
+ item = [{id: 'node2.1'}];
+
+ let bool = canDropChild(browser, itemData, item);
+ expect(bool).toBe(false);
+ });
+ });
+
+ context('when a parent of the current node is not catalog nor schema', () => {
+ it('returns true', () => {
+ itemData = {
+ _type: 'procedure',
+ };
+ item = [{id: 'node3.1'}];
+
+ let bool = canDropChild(browser, itemData, item);
+ expect(bool).toBe(true);
+ });
+ });
+});
diff --git a/web/regression/javascript/sqleditor/calculate_query_run_time_spec.js b/web/regression/javascript/sqleditor/calculate_query_run_time_spec.js
index c1ed5c57..31c14294 100644
--- a/web/regression/javascript/sqleditor/calculate_query_run_time_spec.js
+++ b/web/regression/javascript/sqleditor/calculate_query_run_time_spec.js
@@ -29,7 +29,7 @@ describe('#calculateQueryRunTime', () => {
minutes:30,
seconds:21,
milliseconds:70}).toDate();
- expect(calculateQueryRunTime(startDate, endDate))
+ expect(calculateQueryRunTime(startDate, endDate))
.toEqual('947 msec');
});
});
@@ -52,7 +52,7 @@ describe('#calculateQueryRunTime', () => {
minutes:31,
seconds:15,
milliseconds:70}).toDate();
- expect(calculateQueryRunTime(startDate, endDate))
+ expect(calculateQueryRunTime(startDate, endDate))
.toEqual('54 secs 947 msec');
});
});
@@ -75,7 +75,7 @@ describe('#calculateQueryRunTime', () => {
minutes:40,
seconds:15,
milliseconds:70}).toDate();
- expect(calculateQueryRunTime(startDate, endDate))
+ expect(calculateQueryRunTime(startDate, endDate))
.toEqual('9 min 54 secs');
});
});
diff --git a/web/regression/javascript/sqleditor/filter_dialog_specs.js b/web/regression/javascript/sqleditor/filter_dialog_specs.js
index e13fa097..cea75e6b 100644
--- a/web/regression/javascript/sqleditor/filter_dialog_specs.js
+++ b/web/regression/javascript/sqleditor/filter_dialog_specs.js
@@ -7,18 +7,15 @@
//
//////////////////////////////////////////////////////////////////////////
import filterDialog from 'sources/sqleditor/filter_dialog';
-import filterDialogModel from 'sources/sqleditor/filter_dialog_model';
describe('filterDialog', () => {
- let sqlEditorController;
- sqlEditorController = jasmine.createSpy('sqlEditorController')
describe('filterDialog', () => {
describe('when using filter dialog', () => {
beforeEach(() => {
spyOn(filterDialog, 'dialog');
});
- it("it should be defined as function", function() {
+ it('it should be defined as function', function() {
expect(filterDialog.dialog).toBeDefined();
});
diff --git a/web/regression/javascript/sqleditor/keyboard_shortcuts_spec.js b/web/regression/javascript/sqleditor/keyboard_shortcuts_spec.js
index 18a4cc1b..06586d34 100644
--- a/web/regression/javascript/sqleditor/keyboard_shortcuts_spec.js
+++ b/web/regression/javascript/sqleditor/keyboard_shortcuts_spec.js
@@ -35,48 +35,48 @@ describe('the keyboard shortcuts', () => {
shift: false,
control: false,
key: {
- key_code: F5_KEY
- }
+ key_code: F5_KEY,
+ },
},
explain: {
alt: false,
shift: false,
control: false,
key: {
- key_code: F7_KEY
- }
+ key_code: F7_KEY,
+ },
},
explain_analyze: {
alt: false,
shift: true,
control: false,
key: {
- key_code: F7_KEY
- }
+ key_code: F7_KEY,
+ },
},
download_csv: {
alt: false,
shift: false,
control: false,
key: {
- key_code: F8_KEY
- }
+ key_code: F8_KEY,
+ },
},
move_next: {
alt: false,
shift: false,
control: false,
key: {
- key_code: null
- }
+ key_code: null,
+ },
},
move_previous: {
alt: false,
shift: false,
control: false,
key: {
- key_code: null
- }
+ key_code: null,
+ },
},
};
diff --git a/web/regression/javascript/sqleditor/query_tool_http_error_handler_spec.js b/web/regression/javascript/sqleditor/query_tool_http_error_handler_spec.js
index 21281523..bc5f50e2 100644
--- a/web/regression/javascript/sqleditor/query_tool_http_error_handler_spec.js
+++ b/web/regression/javascript/sqleditor/query_tool_http_error_handler_spec.js
@@ -9,7 +9,7 @@
import {
httpResponseRequiresNewTransaction,
- handleQueryToolAjaxError
+ handleQueryToolAjaxError,
} from '../../../pgadmin/static/js/sqleditor/query_tool_http_error_handler';
describe('#httpResponseRequiresNewTransaction', () => {
@@ -71,29 +71,29 @@ describe('#httpResponseRequiresNewTransaction', () => {
describe('#handleQueryToolAjaxError', () => {
let sqlEditorHandler,
exceptionSpy, stateToSave,
- stateParameters, checkTransaction, UserManagementMock,
+ stateParameters, checkTransaction,
pgBrowserMock;
- beforeEach(() => {
- stateToSave = 'testState';
- stateParameters = [];
- checkTransaction = false;
- sqlEditorHandler = jasmine.createSpyObj(
+ beforeEach(() => {
+ stateToSave = 'testState';
+ stateParameters = [];
+ checkTransaction = false;
+ sqlEditorHandler = jasmine.createSpyObj(
'handler', ['initTransaction', 'saveState', 'handle_connection_lost']
);
- exceptionSpy = {
- readyState: 0,
- status: 404,
- data: {
- info: 'CONNECTION_LOST',
- },
- };
- pgBrowserMock = {
- 'Browser': {
- 'UserManagement': jasmine.createSpyObj('UserManagement', ['isPgaLoginRequired', 'pgaLogin'])
- }
- };
- });
+ exceptionSpy = {
+ readyState: 0,
+ status: 404,
+ data: {
+ info: 'CONNECTION_LOST',
+ },
+ };
+ pgBrowserMock = {
+ 'Browser': {
+ 'UserManagement': jasmine.createSpyObj('UserManagement', ['isPgaLoginRequired', 'pgaLogin']),
+ },
+ };
+ });
describe('when ready state is 0', () => {
it('should return connection', () => {
@@ -149,7 +149,7 @@ describe('#handleQueryToolAjaxError', () => {
exceptionSpy.readyState = 1;
exceptionSpy.status = 503;
exceptionSpy.responseJSON = {
- 'info': 'CONNECTION_LOST'
+ 'info': 'CONNECTION_LOST',
};
pgBrowserMock.Browser.UserManagement.isPgaLoginRequired.and.returnValue(false);
checkTransaction = false;
diff --git a/web/regression/javascript/table/enable_disable_triggers_spec.js b/web/regression/javascript/table/enable_disable_triggers_spec.js
new file mode 100644
index 00000000..767c78a4
--- /dev/null
+++ b/web/regression/javascript/table/enable_disable_triggers_spec.js
@@ -0,0 +1,271 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import MockAdapter from 'axios-mock-adapter';
+import axios from 'axios/index';
+import {
+ enableTriggers,
+ disableTriggers,
+} from '../../../pgadmin/static/js/table/enable_disable_triggers';
+import {TreeFake} from '../tree/tree_fake';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+describe('#enableTriggers', () => {
+ let networkMock;
+ let tree;
+ let alertify;
+ let generateUrlSpy;
+ beforeEach(() => {
+ networkMock = new MockAdapter(axios);
+ tree = new TreeFake();
+ const server1 = tree.addNewNode('server1', {_id: 1}, ['<li>server1</li>']);
+ const database1 = tree.addNewNode('database1', {_type: 'database'}, ['<li>database1</li>']);
+ tree.addChild(server1, database1);
+
+ const schema1 = tree.addNewNode('schema1', {_type: 'schema'}, ['<li>schema1</li>']);
+ tree.addChild(database1, schema1);
+
+ const table1 = tree.addNewNode('table1', {_type: 'table'}, ['<li>table1</li>']);
+ tree.addChild(schema1, table1);
+
+ const column1 = tree.addNewNode('column1', {_type: 'column'}, ['<li>column1</li>']);
+ tree.addChild(table1, column1);
+
+ const tableNoData = tree.addNewNode('table-no-data', undefined, ['<li>table-no-data</li>']);
+ tree.addChild(schema1, tableNoData);
+
+ alertify = jasmine.createSpyObj('alertify', ['success', 'error']);
+ generateUrlSpy = jasmine.createSpy('generateUrl');
+ generateUrlSpy.and.returnValue('/some/place');
+ });
+
+ describe('no node is selected', () => {
+ it('does not send the request to the backend', (done) => {
+ networkMock.onAny('.*').reply(200, () => {
+ });
+
+ setTimeout(() => {
+ expect(enableTriggers(tree, alertify, generateUrlSpy, {})).toBe(false);
+ done();
+ }, 0);
+ });
+ });
+
+ describe('a node is selected', () => {
+ describe('node as no data', () => {
+ it('does not send the request to the backend', () => {
+ tree.selectNode([{id: 'table-no-data'}]);
+
+ networkMock.onAny('.*').reply(200, () => {
+ });
+
+ setTimeout(() => {
+ expect(enableTriggers(tree, alertify, generateUrlSpy, {})).toBe(false);
+ }, 0);
+ });
+ });
+
+ describe('node as data', () => {
+ describe('backend responds with success', () => {
+ let networkMockCalledWith;
+ beforeEach(() => {
+ networkMockCalledWith = false;
+ networkMock.onPut(/.*/).reply((configuration) => {
+ networkMockCalledWith = configuration;
+ return [200, {
+ success: 1,
+ info: 'some information',
+ }];
+ });
+ });
+
+ it('displays an alert box with success', (done) => {
+ tree.selectNode([{id: 'table1'}]);
+ enableTriggers(tree, alertify, generateUrlSpy, {});
+ setTimeout(() => {
+ expect(alertify.success).toHaveBeenCalledWith('some information');
+ done();
+ }, 0);
+ });
+
+ it('reloads the node', (done) => {
+ enableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+ setTimeout(() => {
+ expect(tree.selected()).toEqual(['<li>table1</li>']);
+ done();
+ }, 20);
+ });
+
+ it('call backend with the correct parameters', (done) => {
+ enableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+ setTimeout(() => {
+ expect(networkMockCalledWith.data).toEqual(JSON.stringify({enable: 'true'}));
+ done();
+ }, 0);
+ });
+ });
+
+ describe('backend responds with error', () => {
+ beforeEach(() => {
+ networkMock.onPut(/.*/).reply(() => {
+ return [500, {
+ success: 0,
+ errormsg: 'some error message',
+ }];
+ });
+ });
+
+ it('displays an error alert', (done) => {
+ tree.selectNode([{id: 'table1'}]);
+ enableTriggers(tree, alertify, generateUrlSpy, {});
+ setTimeout(() => {
+ expect(alertify.error).toHaveBeenCalledWith('some error message');
+ done();
+ }, 0);
+ });
+
+ it('unload the node', (done) => {
+ enableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+
+ setTimeout(() => {
+ expect(tree.findNodeByDomElement([{id: 'table1'}]).children.length).toBe(0);
+ done();
+ }, 20);
+ });
+ });
+ });
+ });
+});
+
+describe('#disableTriggers', () => {
+ let networkMock;
+ let tree;
+ let alertify;
+ let generateUrlSpy;
+ beforeEach(() => {
+ networkMock = new MockAdapter(axios);
+ tree = new TreeFake();
+ const server1 = tree.addNewNode('server1', {_id: 1}, ['<li>server1</li>']);
+ const database1 = new TreeNode('database1', {_type: 'database'}, ['<li>database1</li>']);
+ tree.addChild(server1, database1);
+
+ const schema1 = new TreeNode('schema1', {_type: 'schema'}, ['<li>schema1</li>']);
+ tree.addChild(database1, schema1);
+
+ const table1 = new TreeNode('table1', {_type: 'table'}, ['<li>table1</li>']);
+ tree.addChild(schema1, table1);
+
+ const column1 = new TreeNode('column1', {_type: 'column'}, ['<li>column1</li>']);
+ tree.addChild(table1, column1);
+
+ const tableNoData = new TreeNode('table-no-data', undefined, ['<li>table-no-data</li>']);
+ tree.addChild(schema1, tableNoData);
+
+ alertify = jasmine.createSpyObj('alertify', ['success', 'error']);
+ generateUrlSpy = jasmine.createSpy('generateUrl');
+ generateUrlSpy.and.returnValue('/some/place');
+ });
+
+ describe('no node is selected', () => {
+ it('does not send the request to the backend', (done) => {
+ networkMock.onAny('.*').reply(200, () => {
+ });
+
+ setTimeout(() => {
+ expect(disableTriggers(tree, alertify, generateUrlSpy, {})).toBe(false);
+ done();
+ }, 0);
+ });
+ });
+
+ describe('a node is selected', () => {
+ describe('node as no data', () => {
+ it('does not send the request to the backend', () => {
+ tree.selectNode([{id: 'table-no-data'}]);
+
+ networkMock.onAny('.*').reply(200, () => {
+ });
+
+ setTimeout(() => {
+ expect(disableTriggers(tree, alertify, generateUrlSpy, {})).toBe(false);
+ }, 0);
+ });
+ });
+
+ describe('node as data', () => {
+ describe('backend responds with success', () => {
+ let networkMockCalledWith;
+ beforeEach(() => {
+ networkMockCalledWith = false;
+ networkMock.onPut(/.*/).reply((configuration) => {
+ networkMockCalledWith = configuration;
+ return [200, {
+ success: 1,
+ info: 'some information',
+ }];
+ });
+ });
+
+ it('displays an alert box with success', (done) => {
+ tree.selectNode([{id: 'table1'}]);
+ disableTriggers(tree, alertify, generateUrlSpy, {});
+ setTimeout(() => {
+ expect(alertify.success).toHaveBeenCalledWith('some information');
+ done();
+ }, 0);
+ });
+
+ it('reloads the node', (done) => {
+ disableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+ setTimeout(() => {
+ expect(tree.selected()).toEqual(['<li>table1</li>']);
+ done();
+ }, 20);
+ });
+
+ it('call backend with the correct parameters', (done) => {
+ disableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+ setTimeout(() => {
+ expect(networkMockCalledWith.data).toEqual(JSON.stringify({enable: 'false'}));
+ done();
+ }, 0);
+ });
+ });
+
+ describe('backend responds with error', () => {
+ beforeEach(() => {
+ networkMock.onPut(/.*/).reply(() => {
+ return [500, {
+ success: 0,
+ errormsg: 'some error message',
+ }];
+ });
+ });
+
+ it('displays an error alert', (done) => {
+ tree.selectNode([{id: 'table1'}]);
+ disableTriggers(tree, alertify, generateUrlSpy, {});
+ setTimeout(() => {
+ expect(alertify.error).toHaveBeenCalledWith('some error message');
+ done();
+ }, 0);
+ });
+
+ it('unload the node', (done) => {
+ disableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+
+ setTimeout(() => {
+ expect(tree.findNodeByDomElement([{id: 'table1'}]).children.length).toBe(0);
+ done();
+ }, 20);
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/tree/pgadmin_tree_node_spec.js b/web/regression/javascript/tree/pgadmin_tree_node_spec.js
new file mode 100644
index 00000000..479e515c
--- /dev/null
+++ b/web/regression/javascript/tree/pgadmin_tree_node_spec.js
@@ -0,0 +1,353 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+import {
+ getTreeNodeHierarchyFromElement,
+ getTreeNodeHierarchyFromIdentifier,
+} from '../../../pgadmin/static/js/tree/pgadmin_tree_node';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+import {TreeFake} from './tree_fake';
+
+const context = describe;
+
+describe('tree#node#getTreeNodeHierarchy', () => {
+ let browser;
+ let newTree;
+ beforeEach(() => {
+ newTree = new TreeFake();
+ browser = {
+ Nodes: {
+ 'special one': {hasId: true},
+ 'child special': {hasId: true},
+ 'other type': {hasId: true},
+ 'table': {hasId: true},
+ 'partition': {hasId: true},
+ 'no id': {hasId: false},
+ },
+ };
+ browser.treeMenu = newTree;
+ });
+
+ context('getTreeNodeHierarchy is called with aciTreeNode object', () => {
+ describe('When the current node is root element', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ });
+ });
+
+ it('returns a object with the element type passed data and priority == 0', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)([{id: 'root'}]);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+
+ describe('When the current node is not of a known type', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'do not exist',
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)('root');
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node type has no id', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'no id',
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)('root');
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node is at the second level', () => {
+ beforeEach(() => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ });
+ const firstChild = new TreeNode('first child', {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ }, ['root']);
+ newTree.addChild(root, firstChild);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)([{id: 'first child'}]);
+ expect(result).toEqual({
+ 'child special': {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ 'priority': 0,
+ },
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+
+ context('When tree as "special type"', () => {
+ context('When "special type" have "other type"', () => {
+ context('When "other type" have "special type"', () => {
+ describe('When retrieving lead node', () => {
+ it('does not override previous node type data', () => {
+ const rootNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+
+ const level1 = new TreeNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'other type',
+ });
+ newTree.addChild(rootNode, level1);
+
+ newTree.addChild(level1, new TreeNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ }));
+
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)([{id: 'level 2'}]);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ 'priority': 0,
+ },
+ 'other type': {
+ 'some key': 'some value',
+ '_type': 'other type',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+ });
+ });
+ });
+
+ context('When tree has table', () => {
+ context('when table has partition', () => {
+ it('returns table with partition parameters', () => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+ const level1 = new TreeNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'table',
+ });
+ newTree.addChild(root, level1);
+ newTree.addChild(level1, new TreeNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'partition',
+ 'some other key': 'some other value',
+ }));
+
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)([{id:'level 2'}]);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ 'table': {
+ 'some key': 'expected value',
+ 'some other key': 'some other value',
+ '_type': 'partition',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+ });
+ });
+
+ context('getTreeNodeHierarchy is called with TreeNode object', () => {
+ let treeNode;
+ describe('When the current node is root element', () => {
+ beforeEach(() => {
+ treeNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+ });
+
+ it('returns a object with the element type passed data and priority == 0', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+
+ describe('When the current node is not of a known type', () => {
+ beforeEach(() => {
+ treeNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'do not exist',
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node type has no id', () => {
+ beforeEach(() => {
+ treeNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'no id',
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node is at the second level', () => {
+ beforeEach(() => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+ treeNode = new TreeNode('first child', {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ });
+ newTree.addChild(root, treeNode);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'child special': {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ 'priority': 0,
+ },
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+
+ context('When tree as "special type"', () => {
+ context('When "special type" have "other type"', () => {
+ context('When "other type" have "special type"', () => {
+ describe('When retrieving lead node', () => {
+ it('does not override previous node type data', () => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+ const level1 = new TreeNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'other type',
+ });
+ newTree.addChild(root, level1);
+ treeNode = new TreeNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ });
+ newTree.addChild(level1, treeNode);
+
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ 'priority': 0,
+ },
+ 'other type': {
+ 'some key': 'some value',
+ '_type': 'other type',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+ });
+ });
+ });
+
+ context('When tree has table', () => {
+ context('when table has partition', () => {
+ it('returns table with partition parameters', () => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ });
+ const level1 = newTree.addNewNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'table',
+ });
+ newTree.addChild(root, level1);
+ treeNode = new TreeNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'partition',
+ 'some other key': 'some other value',
+ });
+ newTree.addChild(level1, treeNode);
+
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ 'table': {
+ 'some key': 'expected value',
+ 'some other key': 'some other value',
+ '_type': 'partition',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+ });
+ });
+});
--
2.16.2
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-30 06:16 Ashesh Vashi <[email protected]>
parent: Joao De Almeida Pereira <[email protected]>
1 sibling, 1 reply; 69+ messages in thread
From: Ashesh Vashi @ 2018-04-30 06:16 UTC (permalink / raw)
To: Joao De Almeida Pereira <[email protected]>; +Cc: Dave Page <[email protected]>; Khushboo Vashi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
On Sat, Apr 28, 2018 at 3:55 AM, Joao De Almeida Pereira <
[email protected]> wrote:
> Hi Hackers,
> As you are aware we kept on working on the patch, so we are attaching to
> this email a new version of the patch.
> This new version contains all the changes in the previous one plus more
> extractions of functions and refactoring of code.
>
> The objective of this patch is to create a separation between pgAdmin and
> the ACI Tree. We are doing this because we realized that at this point in
> time we have the ACI Tree all over the code of pgAdmin. I found a very
> interesting article that really talks about this:
> https://medium.freecodecamp.org/code-dependencies-are-the-
> devil-35ed28b556d
>
> In this patch there are some visions and ideas about the location of the
> code, the way to organize it and also try to pave the future for a
> application that is stable, easy to develop on and that can be release at a
> times notice.
>
> We are investing a big chunk of our time in doing this refactoring, but
> while doing that we also try to respond to the patches sent to the mailing
> list. We would like the feedback from the community because we believe this
> is a changing point for the application. The idea is to change the way we
> develop this application, instead of only correcting a bug of developing a
> feature, with every commit we should correct the bug or develop a feature
> but leave the code a little better than we found it (Refactoring,
> refactoring, refactoring). This is hard work but that is what the users
> from pgAdmin expect from this community of developers.
>
>
> ======
>
>
>
> It is a huge patch
> 86 files changed, 5492 inserts, 1840 deletions
> and we would like to get your feedback as soon as possible, because we are
> continuing to work on it which means it is going to grow in size.
>
>
> At this point in time we still have 124 of 176 calls to the function
> itemData from ACITree.
>
> What does each patch contain:
> 0001:
> Very simple patch, we found out that the linter was not looking into all
> the javascript test files, so this patch will ensure it is
>
> 0002:
> New Tree abstraction. This patch contains the new Tree that works as an
> adaptor for ACI Tree and is going to be used on all the extractions that we
> are doing
>
> 0003:
> Code that extracts, wrap with tests and replace ACI Tree invocations.
> We start creating new pattern for the location of Javascript files and
> their structure.
> Create patterns for creation of dialogs (backup and restore)
>
Do you have some TODO left for the same?
Or, is this the final one? Because - it gives us the better understanding
during reviewing the patch.
-- Thanks, Ashesh
>
>
> Thanks
> Joao
>
>
> On Fri, Apr 27, 2018 at 5:34 AM Ashesh Vashi <
> [email protected]> wrote:
>
>> I have quite a few comments for the patch.
>> I will send them soon.
>>
>> On Fri, Apr 27, 2018, 14:45 Dave Page <[email protected]> wrote:
>>
>>> How is your work on this going Ashesh? Will you be committing today?
>>>
>>> On Wed, Apr 25, 2018 at 8:52 AM, Dave Page <[email protected]> wrote:
>>>
>>>> Ashesh; you had agreed to work on this early this week. Please ensure
>>>> you do so today.
>>>>
>>>> Thanks.
>>>>
>>>> On Tue, Apr 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi Hackers,
>>>>>
>>>>> Can someone review and merge this patch?
>>>>>
>>>>> Thanks
>>>>> Joao
>>>>>
>>>>> On Wed, Apr 18, 2018 at 10:23 AM Joao De Almeida Pereira <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hi Hackers,
>>>>>> Any other comment about this patch?
>>>>>>
>>>>>> Thanks
>>>>>> Joao
>>>>>>
>>>>>> On Tue, Apr 10, 2018 at 12:00 PM Joao De Almeida Pereira <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hello Khushboo
>>>>>>>
>>>>>>> On Mon, Apr 9, 2018 at 1:59 AM Khushboo Vashi <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hi Joao,
>>>>>>>>
>>>>>>>> I have reviewed your patch and have some suggestions.
>>>>>>>>
>>>>>>>> On Sat, Apr 7, 2018 at 12:43 AM, Joao De Almeida Pereira <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Hello Murtuza/Dave,
>>>>>>>>> Yes now the extracted functions are spread into different files.
>>>>>>>>> The intent would be to make the files as small as possible, and also to
>>>>>>>>> group and name them in a way that would be easy to understand what each
>>>>>>>>> file is doing without the need of opening it.
>>>>>>>>> As a example:
>>>>>>>>> static/js/backup will contain all the backup related
>>>>>>>>> functionality inside of this folder we can see the file:
>>>>>>>>>
>>>>>>>> menu_utils.js At this moment in time we decided to group all the
>>>>>>>>> functions that are related to the menu, but we can split that also if we
>>>>>>>>> believe it is easier to see.
>>>>>>>>>
>>>>>>>> It's really very good to see the separated code for backup module.
>>>>>>>> As we have done for backup, we would like do it for other PG utilities like
>>>>>>>> restore, maintenance etc.
>>>>>>>> Considering this, we should separate the code in a way that some of
>>>>>>>> the common functionalities can be used for other modules like menu (as you
>>>>>>>> have mentioned above), dialogue factory etc.
>>>>>>>> Also, I think these functionalities should be in their respective
>>>>>>>> static folder instead of pgadmin/static.
>>>>>>>>
>>>>>>>
>>>>>>> About the location of the files. The move of the files to
>>>>>>> pgadmin/static/js was made on purpose in order to clearly separate
>>>>>>> Javascript from python code.
>>>>>>> The rational behind it was
>>>>>>> - Create a clear separation between the backend and frontend
>>>>>>> - Having Javascript code concentrated in a single place, hopefully,
>>>>>>> will encourage to developers to look for a functionality, that is already
>>>>>>> implemented in another modules, because they are right there. (When we
>>>>>>> started this journey we realized that the 'nodes' have a big groups of code
>>>>>>> that could be shared, but because the Javascript is spread everywhere it is
>>>>>>> much harder to look for it)
>>>>>>>
>>>>>>>
>>>>>>> There are some drawbacks of this separation:
>>>>>>> - When creating a new module we will need to put the javascript in a
>>>>>>> separate location from the backend code
>>>>>>>
>>>>>>>
>>>>>>>>
>>>>>>>>
>>>>>>>>> static/js/datagrid folder contains all the datagrid related
>>>>>>>>> functionality
>>>>>>>>>
>>>>>>>> Same as backup module, this should be in it's respective static/js
>>>>>>>> folder.
>>>>>>>>
>>>>>>>>> Inside of the folder we can see the files:
>>>>>>>>> get_panel_title.js is responsible for retrieving the name of the
>>>>>>>>> panel
>>>>>>>>> show_data.js is responsible for showing the datagrid
>>>>>>>>> show_query_tool.js is responsible for showing the query tool
>>>>>>>>>
>>>>>>>>> Does this structure make sense?
>>>>>>>>> Can you give an example of a comment that you think is missing and
>>>>>>>>> that could help?
>>>>>>>>>
>>>>>>>>> As a personal note, unless the algorithm is very obscure or very
>>>>>>>>> complicated, I believe that if the code needs comments it is a signal that
>>>>>>>>> something needs to change in terms of naming, structure of the part in
>>>>>>>>> question. This being said, I am open to add some comments that might help
>>>>>>>>> people.
>>>>>>>>>
>>>>>>>> You are right, with the help of naming convention and structure of
>>>>>>>> the code, any one can get the idea about the code. But it is very useful to
>>>>>>>> understand the code
>>>>>>>> very easily with the proper comments especially when there are
>>>>>>>> multiple developers working on a single project.
>>>>>>>>
>>>>>>>> I found some of the places where it would be great to have comments.
>>>>>>>>
>>>>>>>> - treeMenu: new tree.Tree() in a browser.js
>>>>>>>> - tree.js (especially Tree class)
>>>>>>>>
>>>>>>> About the comment point I need a more clear understanding on what
>>>>>>> kind of comments you are looking for. Because when you read the function
>>>>>>> names you understand the intent, what they are doing. The parameters also
>>>>>>> explain what you need to pass into them.
>>>>>>>
>>>>>>> If what you are looking for in these comments is the reasoning being
>>>>>>> the change itself, then that should be present in the commit message.
>>>>>>> Specially because this is going to be a very big patch with a very big
>>>>>>> number of changes.
>>>>>>>
>>>>>>>>
>>>>>>>> Thanks
>>>>>>>>> Joao
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> Thanks,
>>>>>>>> Khushboo
>>>>>>>>
>>>>>>>>>
>>>>>>>>> On Fri, Apr 6, 2018 at 4:48 AM Murtuza Zabuawala <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Hi Joao,
>>>>>>>>>>
>>>>>>>>>> Patch looks good and working as expected.
>>>>>>>>>>
>>>>>>>>>> I also agree with Dave, Can we please add some comments in each
>>>>>>>>>> file which can help us to understand the flow, I'm saying because now the
>>>>>>>>>> code is segregated in so many separate files it will be hard to keep track
>>>>>>>>>> of the flow from one file to another when debugging.
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> --
>>>>>>>>>> Regards,
>>>>>>>>>> Murtuza Zabuawala
>>>>>>>>>> EnterpriseDB: http://www.enterprisedb.com
>>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> On Thu, Apr 5, 2018 at 7:08 PM, Joao De Almeida Pereira <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> Hi Khushboo,
>>>>>>>>>>> Attached you can find both patches rebased
>>>>>>>>>>>
>>>>>>>>>>> Thanks
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>
>>>>>>>>>>>> Can you please rebase the second patch?
>>>>>>>>>>>>
>>>>>>>>>>>> Thanks,
>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida Pereira <
>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>
>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>
>>>>>>>>>>>>> Attached you can find the patch that will start to decouple
>>>>>>>>>>>>> pgAdmin from ACITree library.
>>>>>>>>>>>>> This patch is intended to be merged after 3.0, because we do
>>>>>>>>>>>>> not want to cause any entropy or delay the release, but we want to start
>>>>>>>>>>>>> the discussion and show some code.
>>>>>>>>>>>>>
>>>>>>>>>>>>> This job that we started is a massive tech debt chore that
>>>>>>>>>>>>> will take some time to finalize and we would love the help of the community
>>>>>>>>>>>>> to do it.
>>>>>>>>>>>>>
>>>>>>>>>>>>> *Summary of the patch:*
>>>>>>>>>>>>> 0001 patch:
>>>>>>>>>>>>> - Creates a new tree that will allow us to create a
>>>>>>>>>>>>> separation between the application and ACI Tree
>>>>>>>>>>>>> - Creates a Fake Tree (Test double, for reference on the
>>>>>>>>>>>>> available test doubles: https://martinfowler.
>>>>>>>>>>>>> com/bliki/TestDouble.html) that can be used to inplace to
>>>>>>>>>>>>> replace the ACITree and also encapsulate the new tree behavior on our tests
>>>>>>>>>>>>> - Adds tests for all the tree functionalities
>>>>>>>>>>>>>
>>>>>>>>>>>>> 0002 patch:
>>>>>>>>>>>>> - Extracts, refactors, adds tests and remove dependency from
>>>>>>>>>>>>> ACI Tree on:
>>>>>>>>>>>>> - getTreeNodeHierarchy
>>>>>>>>>>>>> - on backup.js: menu_enabled, menu_enabled_server,
>>>>>>>>>>>>> start_backup_global_server, backup_objects
>>>>>>>>>>>>> - on datagrid.js: show_data_grid, get_panel_title,
>>>>>>>>>>>>> show_query_tool
>>>>>>>>>>>>> - Start using sprintf-js as Underscore.String is deprecating
>>>>>>>>>>>>> sprintf function
>>>>>>>>>>>>>
>>>>>>>>>>>>> This patch represents only 10 calls to ACITree.itemData out of
>>>>>>>>>>>>> 176 that are spread around our code
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> *In Depth look on the process behind the patch:*
>>>>>>>>>>>>>
>>>>>>>>>>>>> We started writing this patch with the idea that we need to
>>>>>>>>>>>>> decouple pgAdmin4 from ACITree, because ACITree is no longer supported, the
>>>>>>>>>>>>> documentation is non existent and ACITree is no longer being actively
>>>>>>>>>>>>> developed.
>>>>>>>>>>>>>
>>>>>>>>>>>>> Our process:
>>>>>>>>>>>>> 1. We "randomly" selected a function that is part of the
>>>>>>>>>>>>> ACITree. From this point we decided to replace that function with our own
>>>>>>>>>>>>> version. The function that we choose was "itemData".
>>>>>>>>>>>>> The function gives us all the "data" that a specific node of
>>>>>>>>>>>>> the tree find.
>>>>>>>>>>>>> Given in order to replace the tree we would need to have a
>>>>>>>>>>>>> function that would give us the same information. We had 2 options:
>>>>>>>>>>>>> a) Create a tree with a function called itemData
>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>> - At first view this was the simpler solution
>>>>>>>>>>>>> - Would keep the status quo
>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>> - Not a OOP approach
>>>>>>>>>>>>> - Not very flexible
>>>>>>>>>>>>> b) Create a tree that would return a node given an ID and
>>>>>>>>>>>>> then the node would be responsible for giving it's data.
>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>> - OOP Approach
>>>>>>>>>>>>> - More flexible and we do not need to bring the tree around,
>>>>>>>>>>>>> just a node
>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>> - Break the current status quo
>>>>>>>>>>>>>
>>>>>>>>>>>>> Given these 2 options we decided to go for a more OOP approach
>>>>>>>>>>>>> creating a Tree and a TreeNode classes, that in the future will be renamed
>>>>>>>>>>>>> to ACITreeWrapper and TreeNode.
>>>>>>>>>>>>>
>>>>>>>>>>>>> 2. After we decided on the starting point we searched for
>>>>>>>>>>>>> occurrences of the function "itemData" and we found out that there were 303
>>>>>>>>>>>>> occurrences of "itemData" in the code and roughly 176 calls to the function
>>>>>>>>>>>>> itself (some of the hits were variable names).
>>>>>>>>>>>>>
>>>>>>>>>>>>> 3. We selected the first file on the search and found the
>>>>>>>>>>>>> function that was responsible for calling the itemData function.
>>>>>>>>>>>>>
>>>>>>>>>>>>> 4. Extracted the function to a separate file
>>>>>>>>>>>>>
>>>>>>>>>>>>> 5. Wrap this function with tests
>>>>>>>>>>>>>
>>>>>>>>>>>>> 6. Refactor the function to ES6, give more declarative names
>>>>>>>>>>>>> to variables and break the functions into smaller chunks
>>>>>>>>>>>>>
>>>>>>>>>>>>> 7. When all the tests were passing we replaced ACITree with
>>>>>>>>>>>>> our Tree
>>>>>>>>>>>>>
>>>>>>>>>>>>> 8. We ensured that all tests were passing
>>>>>>>>>>>>>
>>>>>>>>>>>>> 9. Remove function from the original file and use the new
>>>>>>>>>>>>> function
>>>>>>>>>>>>>
>>>>>>>>>>>>> 10. Ensure everything still works
>>>>>>>>>>>>>
>>>>>>>>>>>>> 11. Find the next function and execute from step 4 until all
>>>>>>>>>>>>> the functions are replaced, refactored and tested.
>>>>>>>>>>>>>
>>>>>>>>>>>>> As you can see by the process this is a pretty huge undertake,
>>>>>>>>>>>>> because of the number of calls to the function. This is just the first step
>>>>>>>>>>>>> on the direction of completely isolating the ACITree so that we can solve
>>>>>>>>>>>>> the problem with a large number of elements on the tree.
>>>>>>>>>>>>>
>>>>>>>>>>>>> *What is on our radar that we need to address:*
>>>>>>>>>>>>> - Finish the complete decoupling of the ACITree
>>>>>>>>>>>>> - Performance of the current tree implementation
>>>>>>>>>>>>> - Tweak the naming of the Tree class to explicitly tell us
>>>>>>>>>>>>> this is to use only with ACITree.
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>
>>>>
>>>>
>>>> --
>>>> Dave Page
>>>> Blog: http://pgsnake.blogspot.com
>>>> Twitter: @pgsnake
>>>>
>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>> The Enterprise PostgreSQL Company
>>>>
>>>
>>>
>>>
>>> --
>>> Dave Page
>>> Blog: http://pgsnake.blogspot.com
>>> Twitter: @pgsnake
>>>
>>> EnterpriseDB UK: http://www.enterprisedb.com
>>> The Enterprise PostgreSQL Company
>>>
>>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-30 10:21 Anthony Emengo <[email protected]>
parent: Ashesh Vashi <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Anthony Emengo @ 2018-04-30 10:21 UTC (permalink / raw)
To: Ashesh Vashi <[email protected]>; +Cc: Joao De Almeida Pereira <[email protected]>; Dave Page <[email protected]>; Khushboo Vashi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
Hi there,
Yes, there's a lot of TODO that we intend for this effort - to be
submitted. We'd like to remove as much *direct* invocations on the ACI Tree
library, as it causes a lot of coupling to the library. It is not the final
patch, but we cannot come up with a definitive list of the things we intend
to do, at this time.
On Mon, Apr 30, 2018 at 2:16 AM, Ashesh Vashi <[email protected]
> wrote:
>
>
> On Sat, Apr 28, 2018 at 3:55 AM, Joao De Almeida Pereira <
> [email protected]> wrote:
>
>> Hi Hackers,
>> As you are aware we kept on working on the patch, so we are attaching to
>> this email a new version of the patch.
>> This new version contains all the changes in the previous one plus more
>> extractions of functions and refactoring of code.
>>
>> The objective of this patch is to create a separation between pgAdmin and
>> the ACI Tree. We are doing this because we realized that at this point in
>> time we have the ACI Tree all over the code of pgAdmin. I found a very
>> interesting article that really talks about this:
>> https://medium.freecodecamp.org/code-dependencies-are-the-de
>> vil-35ed28b556d
>>
>> In this patch there are some visions and ideas about the location of the
>> code, the way to organize it and also try to pave the future for a
>> application that is stable, easy to develop on and that can be release at a
>> times notice.
>>
>> We are investing a big chunk of our time in doing this refactoring, but
>> while doing that we also try to respond to the patches sent to the mailing
>> list. We would like the feedback from the community because we believe this
>> is a changing point for the application. The idea is to change the way we
>> develop this application, instead of only correcting a bug of developing a
>> feature, with every commit we should correct the bug or develop a feature
>> but leave the code a little better than we found it (Refactoring,
>> refactoring, refactoring). This is hard work but that is what the users
>> from pgAdmin expect from this community of developers.
>>
>>
>> ======
>>
>>
>>
>> It is a huge patch
>> 86 files changed, 5492 inserts, 1840 deletions
>> and we would like to get your feedback as soon as possible, because we
>> are continuing to work on it which means it is going to grow in size.
>>
>>
>> At this point in time we still have 124 of 176 calls to the function
>> itemData from ACITree.
>>
>> What does each patch contain:
>> 0001:
>> Very simple patch, we found out that the linter was not looking into
>> all the javascript test files, so this patch will ensure it is
>>
>> 0002:
>> New Tree abstraction. This patch contains the new Tree that works as an
>> adaptor for ACI Tree and is going to be used on all the extractions that we
>> are doing
>>
>> 0003:
>> Code that extracts, wrap with tests and replace ACI Tree invocations.
>> We start creating new pattern for the location of Javascript files and
>> their structure.
>> Create patterns for creation of dialogs (backup and restore)
>>
>
> Do you have some TODO left for the same?
> Or, is this the final one? Because - it gives us the better understanding
> during reviewing the patch.
>
> -- Thanks, Ashesh
>
>>
>>
>> Thanks
>> Joao
>>
>>
>> On Fri, Apr 27, 2018 at 5:34 AM Ashesh Vashi <
>> [email protected]> wrote:
>>
>>> I have quite a few comments for the patch.
>>> I will send them soon.
>>>
>>> On Fri, Apr 27, 2018, 14:45 Dave Page <[email protected]> wrote:
>>>
>>>> How is your work on this going Ashesh? Will you be committing today?
>>>>
>>>> On Wed, Apr 25, 2018 at 8:52 AM, Dave Page <[email protected]> wrote:
>>>>
>>>>> Ashesh; you had agreed to work on this early this week. Please ensure
>>>>> you do so today.
>>>>>
>>>>> Thanks.
>>>>>
>>>>> On Tue, Apr 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hi Hackers,
>>>>>>
>>>>>> Can someone review and merge this patch?
>>>>>>
>>>>>> Thanks
>>>>>> Joao
>>>>>>
>>>>>> On Wed, Apr 18, 2018 at 10:23 AM Joao De Almeida Pereira <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hi Hackers,
>>>>>>> Any other comment about this patch?
>>>>>>>
>>>>>>> Thanks
>>>>>>> Joao
>>>>>>>
>>>>>>> On Tue, Apr 10, 2018 at 12:00 PM Joao De Almeida Pereira <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hello Khushboo
>>>>>>>>
>>>>>>>> On Mon, Apr 9, 2018 at 1:59 AM Khushboo Vashi <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Hi Joao,
>>>>>>>>>
>>>>>>>>> I have reviewed your patch and have some suggestions.
>>>>>>>>>
>>>>>>>>> On Sat, Apr 7, 2018 at 12:43 AM, Joao De Almeida Pereira <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Hello Murtuza/Dave,
>>>>>>>>>> Yes now the extracted functions are spread into different files.
>>>>>>>>>> The intent would be to make the files as small as possible, and also to
>>>>>>>>>> group and name them in a way that would be easy to understand what each
>>>>>>>>>> file is doing without the need of opening it.
>>>>>>>>>> As a example:
>>>>>>>>>> static/js/backup will contain all the backup related
>>>>>>>>>> functionality inside of this folder we can see the file:
>>>>>>>>>>
>>>>>>>>> menu_utils.js At this moment in time we decided to group all the
>>>>>>>>>> functions that are related to the menu, but we can split that also if we
>>>>>>>>>> believe it is easier to see.
>>>>>>>>>>
>>>>>>>>> It's really very good to see the separated code for backup module.
>>>>>>>>> As we have done for backup, we would like do it for other PG utilities like
>>>>>>>>> restore, maintenance etc.
>>>>>>>>> Considering this, we should separate the code in a way that some
>>>>>>>>> of the common functionalities can be used for other modules like menu (as
>>>>>>>>> you have mentioned above), dialogue factory etc.
>>>>>>>>> Also, I think these functionalities should be in their respective
>>>>>>>>> static folder instead of pgadmin/static.
>>>>>>>>>
>>>>>>>>
>>>>>>>> About the location of the files. The move of the files to
>>>>>>>> pgadmin/static/js was made on purpose in order to clearly separate
>>>>>>>> Javascript from python code.
>>>>>>>> The rational behind it was
>>>>>>>> - Create a clear separation between the backend and frontend
>>>>>>>> - Having Javascript code concentrated in a single place, hopefully,
>>>>>>>> will encourage to developers to look for a functionality, that is already
>>>>>>>> implemented in another modules, because they are right there. (When we
>>>>>>>> started this journey we realized that the 'nodes' have a big groups of code
>>>>>>>> that could be shared, but because the Javascript is spread everywhere it is
>>>>>>>> much harder to look for it)
>>>>>>>>
>>>>>>>>
>>>>>>>> There are some drawbacks of this separation:
>>>>>>>> - When creating a new module we will need to put the javascript in
>>>>>>>> a separate location from the backend code
>>>>>>>>
>>>>>>>>
>>>>>>>>>
>>>>>>>>>
>>>>>>>>>> static/js/datagrid folder contains all the datagrid related
>>>>>>>>>> functionality
>>>>>>>>>>
>>>>>>>>> Same as backup module, this should be in it's respective
>>>>>>>>> static/js folder.
>>>>>>>>>
>>>>>>>>>> Inside of the folder we can see the files:
>>>>>>>>>> get_panel_title.js is responsible for retrieving the name of the
>>>>>>>>>> panel
>>>>>>>>>> show_data.js is responsible for showing the datagrid
>>>>>>>>>> show_query_tool.js is responsible for showing the query tool
>>>>>>>>>>
>>>>>>>>>> Does this structure make sense?
>>>>>>>>>> Can you give an example of a comment that you think is missing
>>>>>>>>>> and that could help?
>>>>>>>>>>
>>>>>>>>>> As a personal note, unless the algorithm is very obscure or very
>>>>>>>>>> complicated, I believe that if the code needs comments it is a signal that
>>>>>>>>>> something needs to change in terms of naming, structure of the part in
>>>>>>>>>> question. This being said, I am open to add some comments that might help
>>>>>>>>>> people.
>>>>>>>>>>
>>>>>>>>> You are right, with the help of naming convention and structure of
>>>>>>>>> the code, any one can get the idea about the code. But it is very useful to
>>>>>>>>> understand the code
>>>>>>>>> very easily with the proper comments especially when there are
>>>>>>>>> multiple developers working on a single project.
>>>>>>>>>
>>>>>>>>> I found some of the places where it would be great to have
>>>>>>>>> comments.
>>>>>>>>>
>>>>>>>>> - treeMenu: new tree.Tree() in a browser.js
>>>>>>>>> - tree.js (especially Tree class)
>>>>>>>>>
>>>>>>>> About the comment point I need a more clear understanding on what
>>>>>>>> kind of comments you are looking for. Because when you read the function
>>>>>>>> names you understand the intent, what they are doing. The parameters also
>>>>>>>> explain what you need to pass into them.
>>>>>>>>
>>>>>>>> If what you are looking for in these comments is the reasoning
>>>>>>>> being the change itself, then that should be present in the commit message.
>>>>>>>> Specially because this is going to be a very big patch with a very big
>>>>>>>> number of changes.
>>>>>>>>
>>>>>>>>>
>>>>>>>>> Thanks
>>>>>>>>>> Joao
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> Thanks,
>>>>>>>>> Khushboo
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> On Fri, Apr 6, 2018 at 4:48 AM Murtuza Zabuawala <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>
>>>>>>>>>>> Patch looks good and working as expected.
>>>>>>>>>>>
>>>>>>>>>>> I also agree with Dave, Can we please add some comments in each
>>>>>>>>>>> file which can help us to understand the flow, I'm saying because now the
>>>>>>>>>>> code is segregated in so many separate files it will be hard to keep track
>>>>>>>>>>> of the flow from one file to another when debugging.
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> --
>>>>>>>>>>> Regards,
>>>>>>>>>>> Murtuza Zabuawala
>>>>>>>>>>> EnterpriseDB: http://www.enterprisedb.com
>>>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> On Thu, Apr 5, 2018 at 7:08 PM, Joao De Almeida Pereira <
>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> Hi Khushboo,
>>>>>>>>>>>> Attached you can find both patches rebased
>>>>>>>>>>>>
>>>>>>>>>>>> Thanks
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>
>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>
>>>>>>>>>>>>> Can you please rebase the second patch?
>>>>>>>>>>>>>
>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida Pereira <
>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>
>>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Attached you can find the patch that will start to decouple
>>>>>>>>>>>>>> pgAdmin from ACITree library.
>>>>>>>>>>>>>> This patch is intended to be merged after 3.0, because we do
>>>>>>>>>>>>>> not want to cause any entropy or delay the release, but we want to start
>>>>>>>>>>>>>> the discussion and show some code.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> This job that we started is a massive tech debt chore that
>>>>>>>>>>>>>> will take some time to finalize and we would love the help of the community
>>>>>>>>>>>>>> to do it.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> *Summary of the patch:*
>>>>>>>>>>>>>> 0001 patch:
>>>>>>>>>>>>>> - Creates a new tree that will allow us to create a
>>>>>>>>>>>>>> separation between the application and ACI Tree
>>>>>>>>>>>>>> - Creates a Fake Tree (Test double, for reference on the
>>>>>>>>>>>>>> available test doubles: https://martinfowler.
>>>>>>>>>>>>>> com/bliki/TestDouble.html) that can be used to inplace to
>>>>>>>>>>>>>> replace the ACITree and also encapsulate the new tree behavior on our tests
>>>>>>>>>>>>>> - Adds tests for all the tree functionalities
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> 0002 patch:
>>>>>>>>>>>>>> - Extracts, refactors, adds tests and remove dependency from
>>>>>>>>>>>>>> ACI Tree on:
>>>>>>>>>>>>>> - getTreeNodeHierarchy
>>>>>>>>>>>>>> - on backup.js: menu_enabled, menu_enabled_server,
>>>>>>>>>>>>>> start_backup_global_server, backup_objects
>>>>>>>>>>>>>> - on datagrid.js: show_data_grid, get_panel_title,
>>>>>>>>>>>>>> show_query_tool
>>>>>>>>>>>>>> - Start using sprintf-js as Underscore.String is deprecating
>>>>>>>>>>>>>> sprintf function
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> This patch represents only 10 calls to ACITree.itemData out
>>>>>>>>>>>>>> of 176 that are spread around our code
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> *In Depth look on the process behind the patch:*
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> We started writing this patch with the idea that we need to
>>>>>>>>>>>>>> decouple pgAdmin4 from ACITree, because ACITree is no longer supported, the
>>>>>>>>>>>>>> documentation is non existent and ACITree is no longer being actively
>>>>>>>>>>>>>> developed.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Our process:
>>>>>>>>>>>>>> 1. We "randomly" selected a function that is part of the
>>>>>>>>>>>>>> ACITree. From this point we decided to replace that function with our own
>>>>>>>>>>>>>> version. The function that we choose was "itemData".
>>>>>>>>>>>>>> The function gives us all the "data" that a specific node of
>>>>>>>>>>>>>> the tree find.
>>>>>>>>>>>>>> Given in order to replace the tree we would need to have a
>>>>>>>>>>>>>> function that would give us the same information. We had 2 options:
>>>>>>>>>>>>>> a) Create a tree with a function called itemData
>>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>>> - At first view this was the simpler solution
>>>>>>>>>>>>>> - Would keep the status quo
>>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>>> - Not a OOP approach
>>>>>>>>>>>>>> - Not very flexible
>>>>>>>>>>>>>> b) Create a tree that would return a node given an ID and
>>>>>>>>>>>>>> then the node would be responsible for giving it's data.
>>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>>> - OOP Approach
>>>>>>>>>>>>>> - More flexible and we do not need to bring the tree around,
>>>>>>>>>>>>>> just a node
>>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>>> - Break the current status quo
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Given these 2 options we decided to go for a more OOP
>>>>>>>>>>>>>> approach creating a Tree and a TreeNode classes, that in the future will be
>>>>>>>>>>>>>> renamed to ACITreeWrapper and TreeNode.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> 2. After we decided on the starting point we searched for
>>>>>>>>>>>>>> occurrences of the function "itemData" and we found out that there were 303
>>>>>>>>>>>>>> occurrences of "itemData" in the code and roughly 176 calls to the function
>>>>>>>>>>>>>> itself (some of the hits were variable names).
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> 3. We selected the first file on the search and found the
>>>>>>>>>>>>>> function that was responsible for calling the itemData function.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> 4. Extracted the function to a separate file
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> 5. Wrap this function with tests
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> 6. Refactor the function to ES6, give more declarative names
>>>>>>>>>>>>>> to variables and break the functions into smaller chunks
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> 7. When all the tests were passing we replaced ACITree with
>>>>>>>>>>>>>> our Tree
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> 8. We ensured that all tests were passing
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> 9. Remove function from the original file and use the new
>>>>>>>>>>>>>> function
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> 10. Ensure everything still works
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> 11. Find the next function and execute from step 4 until all
>>>>>>>>>>>>>> the functions are replaced, refactored and tested.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> As you can see by the process this is a pretty huge
>>>>>>>>>>>>>> undertake, because of the number of calls to the function. This is just the
>>>>>>>>>>>>>> first step on the direction of completely isolating the ACITree so that we
>>>>>>>>>>>>>> can solve the problem with a large number of elements on the tree.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> *What is on our radar that we need to address:*
>>>>>>>>>>>>>> - Finish the complete decoupling of the ACITree
>>>>>>>>>>>>>> - Performance of the current tree implementation
>>>>>>>>>>>>>> - Tweak the naming of the Tree class to explicitly tell us
>>>>>>>>>>>>>> this is to use only with ACITree.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>
>>>>>
>>>>>
>>>>> --
>>>>> Dave Page
>>>>> Blog: http://pgsnake.blogspot.com
>>>>> Twitter: @pgsnake
>>>>>
>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>> The Enterprise PostgreSQL Company
>>>>>
>>>>
>>>>
>>>>
>>>> --
>>>> Dave Page
>>>> Blog: http://pgsnake.blogspot.com
>>>> Twitter: @pgsnake
>>>>
>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>> The Enterprise PostgreSQL Company
>>>>
>>>
>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-30 10:24 Ashesh Vashi <[email protected]>
parent: Anthony Emengo <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Ashesh Vashi @ 2018-04-30 10:24 UTC (permalink / raw)
To: Anthony Emengo <[email protected]>; +Cc: Joao De Almeida Pereira <[email protected]>; Dave Page <[email protected]>; Khushboo Vashi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
On Mon, Apr 30, 2018 at 3:51 PM, Anthony Emengo <[email protected]> wrote:
> Hi there,
>
> Yes, there's a lot of TODO that we intend for this effort - to be
> submitted. We'd like to remove as much *direct* invocations on the ACI
> Tree library, as it causes a lot of coupling to the library. It is not the
> final patch, but we cannot come up with a definitive list of the things we
> intend to do, at this time.
>
Is there any known TODO list?
So that - I can help you figure out (if any more).
-- Thanks, Ashesh
>
> On Mon, Apr 30, 2018 at 2:16 AM, Ashesh Vashi <
> [email protected]> wrote:
>
>>
>>
>> On Sat, Apr 28, 2018 at 3:55 AM, Joao De Almeida Pereira <
>> [email protected]> wrote:
>>
>>> Hi Hackers,
>>> As you are aware we kept on working on the patch, so we are attaching to
>>> this email a new version of the patch.
>>> This new version contains all the changes in the previous one plus more
>>> extractions of functions and refactoring of code.
>>>
>>> The objective of this patch is to create a separation between pgAdmin
>>> and the ACI Tree. We are doing this because we realized that at this point
>>> in time we have the ACI Tree all over the code of pgAdmin. I found a very
>>> interesting article that really talks about this:
>>> https://medium.freecodecamp.org/code-dependencies-are-the-de
>>> vil-35ed28b556d
>>>
>>> In this patch there are some visions and ideas about the location of the
>>> code, the way to organize it and also try to pave the future for a
>>> application that is stable, easy to develop on and that can be release at a
>>> times notice.
>>>
>>> We are investing a big chunk of our time in doing this refactoring, but
>>> while doing that we also try to respond to the patches sent to the mailing
>>> list. We would like the feedback from the community because we believe this
>>> is a changing point for the application. The idea is to change the way we
>>> develop this application, instead of only correcting a bug of developing a
>>> feature, with every commit we should correct the bug or develop a feature
>>> but leave the code a little better than we found it (Refactoring,
>>> refactoring, refactoring). This is hard work but that is what the users
>>> from pgAdmin expect from this community of developers.
>>>
>>>
>>> ======
>>>
>>>
>>>
>>> It is a huge patch
>>> 86 files changed, 5492 inserts, 1840 deletions
>>> and we would like to get your feedback as soon as possible, because we
>>> are continuing to work on it which means it is going to grow in size.
>>>
>>>
>>> At this point in time we still have 124 of 176 calls to the function
>>> itemData from ACITree.
>>>
>>> What does each patch contain:
>>> 0001:
>>> Very simple patch, we found out that the linter was not looking into
>>> all the javascript test files, so this patch will ensure it is
>>>
>>> 0002:
>>> New Tree abstraction. This patch contains the new Tree that works as
>>> an adaptor for ACI Tree and is going to be used on all the extractions that
>>> we are doing
>>>
>>> 0003:
>>> Code that extracts, wrap with tests and replace ACI Tree invocations.
>>> We start creating new pattern for the location of Javascript files and
>>> their structure.
>>> Create patterns for creation of dialogs (backup and restore)
>>>
>>
>> Do you have some TODO left for the same?
>> Or, is this the final one? Because - it gives us the better understanding
>> during reviewing the patch.
>>
>> -- Thanks, Ashesh
>>
>>>
>>>
>>> Thanks
>>> Joao
>>>
>>>
>>> On Fri, Apr 27, 2018 at 5:34 AM Ashesh Vashi <
>>> [email protected]> wrote:
>>>
>>>> I have quite a few comments for the patch.
>>>> I will send them soon.
>>>>
>>>> On Fri, Apr 27, 2018, 14:45 Dave Page <[email protected]> wrote:
>>>>
>>>>> How is your work on this going Ashesh? Will you be committing today?
>>>>>
>>>>> On Wed, Apr 25, 2018 at 8:52 AM, Dave Page <[email protected]> wrote:
>>>>>
>>>>>> Ashesh; you had agreed to work on this early this week. Please ensure
>>>>>> you do so today.
>>>>>>
>>>>>> Thanks.
>>>>>>
>>>>>> On Tue, Apr 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hi Hackers,
>>>>>>>
>>>>>>> Can someone review and merge this patch?
>>>>>>>
>>>>>>> Thanks
>>>>>>> Joao
>>>>>>>
>>>>>>> On Wed, Apr 18, 2018 at 10:23 AM Joao De Almeida Pereira <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hi Hackers,
>>>>>>>> Any other comment about this patch?
>>>>>>>>
>>>>>>>> Thanks
>>>>>>>> Joao
>>>>>>>>
>>>>>>>> On Tue, Apr 10, 2018 at 12:00 PM Joao De Almeida Pereira <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Hello Khushboo
>>>>>>>>>
>>>>>>>>> On Mon, Apr 9, 2018 at 1:59 AM Khushboo Vashi <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Hi Joao,
>>>>>>>>>>
>>>>>>>>>> I have reviewed your patch and have some suggestions.
>>>>>>>>>>
>>>>>>>>>> On Sat, Apr 7, 2018 at 12:43 AM, Joao De Almeida Pereira <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> Hello Murtuza/Dave,
>>>>>>>>>>> Yes now the extracted functions are spread into different files.
>>>>>>>>>>> The intent would be to make the files as small as possible, and also to
>>>>>>>>>>> group and name them in a way that would be easy to understand what each
>>>>>>>>>>> file is doing without the need of opening it.
>>>>>>>>>>> As a example:
>>>>>>>>>>> static/js/backup will contain all the backup related
>>>>>>>>>>> functionality inside of this folder we can see the file:
>>>>>>>>>>>
>>>>>>>>>> menu_utils.js At this moment in time we decided to group all the
>>>>>>>>>>> functions that are related to the menu, but we can split that also if we
>>>>>>>>>>> believe it is easier to see.
>>>>>>>>>>>
>>>>>>>>>> It's really very good to see the separated code for backup
>>>>>>>>>> module. As we have done for backup, we would like do it for other PG
>>>>>>>>>> utilities like restore, maintenance etc.
>>>>>>>>>> Considering this, we should separate the code in a way that some
>>>>>>>>>> of the common functionalities can be used for other modules like menu (as
>>>>>>>>>> you have mentioned above), dialogue factory etc.
>>>>>>>>>> Also, I think these functionalities should be in their respective
>>>>>>>>>> static folder instead of pgadmin/static.
>>>>>>>>>>
>>>>>>>>>
>>>>>>>>> About the location of the files. The move of the files to
>>>>>>>>> pgadmin/static/js was made on purpose in order to clearly separate
>>>>>>>>> Javascript from python code.
>>>>>>>>> The rational behind it was
>>>>>>>>> - Create a clear separation between the backend and frontend
>>>>>>>>> - Having Javascript code concentrated in a single place,
>>>>>>>>> hopefully, will encourage to developers to look for a functionality, that
>>>>>>>>> is already implemented in another modules, because they are right there.
>>>>>>>>> (When we started this journey we realized that the 'nodes' have a big
>>>>>>>>> groups of code that could be shared, but because the Javascript is spread
>>>>>>>>> everywhere it is much harder to look for it)
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> There are some drawbacks of this separation:
>>>>>>>>> - When creating a new module we will need to put the javascript in
>>>>>>>>> a separate location from the backend code
>>>>>>>>>
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>> static/js/datagrid folder contains all the datagrid related
>>>>>>>>>>> functionality
>>>>>>>>>>>
>>>>>>>>>> Same as backup module, this should be in it's respective
>>>>>>>>>> static/js folder.
>>>>>>>>>>
>>>>>>>>>>> Inside of the folder we can see the files:
>>>>>>>>>>> get_panel_title.js is responsible for retrieving the name of
>>>>>>>>>>> the panel
>>>>>>>>>>> show_data.js is responsible for showing the datagrid
>>>>>>>>>>> show_query_tool.js is responsible for showing the query tool
>>>>>>>>>>>
>>>>>>>>>>> Does this structure make sense?
>>>>>>>>>>> Can you give an example of a comment that you think is missing
>>>>>>>>>>> and that could help?
>>>>>>>>>>>
>>>>>>>>>>> As a personal note, unless the algorithm is very obscure or very
>>>>>>>>>>> complicated, I believe that if the code needs comments it is a signal that
>>>>>>>>>>> something needs to change in terms of naming, structure of the part in
>>>>>>>>>>> question. This being said, I am open to add some comments that might help
>>>>>>>>>>> people.
>>>>>>>>>>>
>>>>>>>>>> You are right, with the help of naming convention and structure
>>>>>>>>>> of the code, any one can get the idea about the code. But it is very useful
>>>>>>>>>> to understand the code
>>>>>>>>>> very easily with the proper comments especially when there are
>>>>>>>>>> multiple developers working on a single project.
>>>>>>>>>>
>>>>>>>>>> I found some of the places where it would be great to have
>>>>>>>>>> comments.
>>>>>>>>>>
>>>>>>>>>> - treeMenu: new tree.Tree() in a browser.js
>>>>>>>>>> - tree.js (especially Tree class)
>>>>>>>>>>
>>>>>>>>> About the comment point I need a more clear understanding on what
>>>>>>>>> kind of comments you are looking for. Because when you read the function
>>>>>>>>> names you understand the intent, what they are doing. The parameters also
>>>>>>>>> explain what you need to pass into them.
>>>>>>>>>
>>>>>>>>> If what you are looking for in these comments is the reasoning
>>>>>>>>> being the change itself, then that should be present in the commit message.
>>>>>>>>> Specially because this is going to be a very big patch with a very big
>>>>>>>>> number of changes.
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> Thanks
>>>>>>>>>>> Joao
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> Thanks,
>>>>>>>>>> Khushboo
>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> On Fri, Apr 6, 2018 at 4:48 AM Murtuza Zabuawala <
>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>
>>>>>>>>>>>> Patch looks good and working as expected.
>>>>>>>>>>>>
>>>>>>>>>>>> I also agree with Dave, Can we please add some comments in each
>>>>>>>>>>>> file which can help us to understand the flow, I'm saying because now the
>>>>>>>>>>>> code is segregated in so many separate files it will be hard to keep track
>>>>>>>>>>>> of the flow from one file to another when debugging.
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> --
>>>>>>>>>>>> Regards,
>>>>>>>>>>>> Murtuza Zabuawala
>>>>>>>>>>>> EnterpriseDB: http://www.enterprisedb.com
>>>>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> On Thu, Apr 5, 2018 at 7:08 PM, Joao De Almeida Pereira <
>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>
>>>>>>>>>>>>> Hi Khushboo,
>>>>>>>>>>>>> Attached you can find both patches rebased
>>>>>>>>>>>>>
>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>
>>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Can you please rebase the second patch?
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida Pereira <
>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Attached you can find the patch that will start to decouple
>>>>>>>>>>>>>>> pgAdmin from ACITree library.
>>>>>>>>>>>>>>> This patch is intended to be merged after 3.0, because we do
>>>>>>>>>>>>>>> not want to cause any entropy or delay the release, but we want to start
>>>>>>>>>>>>>>> the discussion and show some code.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> This job that we started is a massive tech debt chore that
>>>>>>>>>>>>>>> will take some time to finalize and we would love the help of the community
>>>>>>>>>>>>>>> to do it.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> *Summary of the patch:*
>>>>>>>>>>>>>>> 0001 patch:
>>>>>>>>>>>>>>> - Creates a new tree that will allow us to create a
>>>>>>>>>>>>>>> separation between the application and ACI Tree
>>>>>>>>>>>>>>> - Creates a Fake Tree (Test double, for reference on the
>>>>>>>>>>>>>>> available test doubles: https://martinfowler.
>>>>>>>>>>>>>>> com/bliki/TestDouble.html) that can be used to inplace to
>>>>>>>>>>>>>>> replace the ACITree and also encapsulate the new tree behavior on our tests
>>>>>>>>>>>>>>> - Adds tests for all the tree functionalities
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> 0002 patch:
>>>>>>>>>>>>>>> - Extracts, refactors, adds tests and remove dependency
>>>>>>>>>>>>>>> from ACI Tree on:
>>>>>>>>>>>>>>> - getTreeNodeHierarchy
>>>>>>>>>>>>>>> - on backup.js: menu_enabled, menu_enabled_server,
>>>>>>>>>>>>>>> start_backup_global_server, backup_objects
>>>>>>>>>>>>>>> - on datagrid.js: show_data_grid, get_panel_title,
>>>>>>>>>>>>>>> show_query_tool
>>>>>>>>>>>>>>> - Start using sprintf-js as Underscore.String is deprecating
>>>>>>>>>>>>>>> sprintf function
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> This patch represents only 10 calls to ACITree.itemData out
>>>>>>>>>>>>>>> of 176 that are spread around our code
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> *In Depth look on the process behind the patch:*
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> We started writing this patch with the idea that we need to
>>>>>>>>>>>>>>> decouple pgAdmin4 from ACITree, because ACITree is no longer supported, the
>>>>>>>>>>>>>>> documentation is non existent and ACITree is no longer being actively
>>>>>>>>>>>>>>> developed.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Our process:
>>>>>>>>>>>>>>> 1. We "randomly" selected a function that is part of the
>>>>>>>>>>>>>>> ACITree. From this point we decided to replace that function with our own
>>>>>>>>>>>>>>> version. The function that we choose was "itemData".
>>>>>>>>>>>>>>> The function gives us all the "data" that a specific node of
>>>>>>>>>>>>>>> the tree find.
>>>>>>>>>>>>>>> Given in order to replace the tree we would need to have a
>>>>>>>>>>>>>>> function that would give us the same information. We had 2 options:
>>>>>>>>>>>>>>> a) Create a tree with a function called itemData
>>>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>>>> - At first view this was the simpler solution
>>>>>>>>>>>>>>> - Would keep the status quo
>>>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>>>> - Not a OOP approach
>>>>>>>>>>>>>>> - Not very flexible
>>>>>>>>>>>>>>> b) Create a tree that would return a node given an ID and
>>>>>>>>>>>>>>> then the node would be responsible for giving it's data.
>>>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>>>> - OOP Approach
>>>>>>>>>>>>>>> - More flexible and we do not need to bring the tree
>>>>>>>>>>>>>>> around, just a node
>>>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>>>> - Break the current status quo
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Given these 2 options we decided to go for a more OOP
>>>>>>>>>>>>>>> approach creating a Tree and a TreeNode classes, that in the future will be
>>>>>>>>>>>>>>> renamed to ACITreeWrapper and TreeNode.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> 2. After we decided on the starting point we searched for
>>>>>>>>>>>>>>> occurrences of the function "itemData" and we found out that there were 303
>>>>>>>>>>>>>>> occurrences of "itemData" in the code and roughly 176 calls to the function
>>>>>>>>>>>>>>> itself (some of the hits were variable names).
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> 3. We selected the first file on the search and found the
>>>>>>>>>>>>>>> function that was responsible for calling the itemData function.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> 4. Extracted the function to a separate file
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> 5. Wrap this function with tests
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> 6. Refactor the function to ES6, give more declarative names
>>>>>>>>>>>>>>> to variables and break the functions into smaller chunks
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> 7. When all the tests were passing we replaced ACITree with
>>>>>>>>>>>>>>> our Tree
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> 8. We ensured that all tests were passing
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> 9. Remove function from the original file and use the new
>>>>>>>>>>>>>>> function
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> 10. Ensure everything still works
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> 11. Find the next function and execute from step 4 until all
>>>>>>>>>>>>>>> the functions are replaced, refactored and tested.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> As you can see by the process this is a pretty huge
>>>>>>>>>>>>>>> undertake, because of the number of calls to the function. This is just the
>>>>>>>>>>>>>>> first step on the direction of completely isolating the ACITree so that we
>>>>>>>>>>>>>>> can solve the problem with a large number of elements on the tree.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> *What is on our radar that we need to address:*
>>>>>>>>>>>>>>> - Finish the complete decoupling of the ACITree
>>>>>>>>>>>>>>> - Performance of the current tree implementation
>>>>>>>>>>>>>>> - Tweak the naming of the Tree class to explicitly tell us
>>>>>>>>>>>>>>> this is to use only with ACITree.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>
>>>>>>
>>>>>> --
>>>>>> Dave Page
>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>> Twitter: @pgsnake
>>>>>>
>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>> The Enterprise PostgreSQL Company
>>>>>>
>>>>>
>>>>>
>>>>>
>>>>> --
>>>>> Dave Page
>>>>> Blog: http://pgsnake.blogspot.com
>>>>> Twitter: @pgsnake
>>>>>
>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>> The Enterprise PostgreSQL Company
>>>>>
>>>>
>>
>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-30 14:57 Joao De Almeida Pereira <[email protected]>
parent: Ashesh Vashi <[email protected]>
0 siblings, 0 replies; 69+ messages in thread
From: Joao De Almeida Pereira @ 2018-04-30 14:57 UTC (permalink / raw)
To: Ashesh Vashi <[email protected]>; +Cc: Anthony Emengo <[email protected]>; Dave Page <[email protected]>; Khushboo Vashi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
Despite this being a work-in-progress. We've provided committable code:
We've ensured it works like before.
For right now, our strategy is looking for calls to `itemData(`. We've
noticed that these surface a lot of calls that are duplicated throughout
the code base. We'd like to reduce the amount of duplication as much as
possible for multiple reasons:
- we've noticed that it makes the code easier to reason about.
- we've noticed that it makes the code much easier to test (there's a lot
of functionality we'd like to have coverage for).
- ultimately will make the goal of decoupling the code from the aci tree
library easier.
For instance, the following lines in the picture below show up in too many
places.
[image: Screen Shot 2018-04-30 at 10.50.24 AM.png]
You can expect in the next patches to see more and more functions extracted
with the goal of decoupling the tree in mind.
Anthony && Joao
On Mon, Apr 30, 2018 at 6:25 AM Ashesh Vashi <[email protected]>
wrote:
> On Mon, Apr 30, 2018 at 3:51 PM, Anthony Emengo <[email protected]>
> wrote:
>
>> Hi there,
>>
>> Yes, there's a lot of TODO that we intend for this effort - to be
>> submitted. We'd like to remove as much *direct* invocations on the ACI
>> Tree library, as it causes a lot of coupling to the library. It is not the
>> final patch, but we cannot come up with a definitive list of the things we
>> intend to do, at this time.
>>
> Is there any known TODO list?
> So that - I can help you figure out (if any more).
>
> -- Thanks, Ashesh
>
>>
>> On Mon, Apr 30, 2018 at 2:16 AM, Ashesh Vashi <
>> [email protected]> wrote:
>>
>>>
>>>
>>> On Sat, Apr 28, 2018 at 3:55 AM, Joao De Almeida Pereira <
>>> [email protected]> wrote:
>>>
>>>> Hi Hackers,
>>>> As you are aware we kept on working on the patch, so we are attaching
>>>> to this email a new version of the patch.
>>>> This new version contains all the changes in the previous one plus more
>>>> extractions of functions and refactoring of code.
>>>>
>>>> The objective of this patch is to create a separation between pgAdmin
>>>> and the ACI Tree. We are doing this because we realized that at this point
>>>> in time we have the ACI Tree all over the code of pgAdmin. I found a very
>>>> interesting article that really talks about this:
>>>> https://medium.freecodecamp.org/code-dependencies-are-the-devil-35ed28b556d
>>>>
>>>> In this patch there are some visions and ideas about the location of
>>>> the code, the way to organize it and also try to pave the future for a
>>>> application that is stable, easy to develop on and that can be release at a
>>>> times notice.
>>>>
>>>> We are investing a big chunk of our time in doing this refactoring, but
>>>> while doing that we also try to respond to the patches sent to the mailing
>>>> list. We would like the feedback from the community because we believe this
>>>> is a changing point for the application. The idea is to change the way we
>>>> develop this application, instead of only correcting a bug of developing a
>>>> feature, with every commit we should correct the bug or develop a feature
>>>> but leave the code a little better than we found it (Refactoring,
>>>> refactoring, refactoring). This is hard work but that is what the users
>>>> from pgAdmin expect from this community of developers.
>>>>
>>>>
>>>> ======
>>>>
>>>>
>>>>
>>>> It is a huge patch
>>>> 86 files changed, 5492 inserts, 1840 deletions
>>>> and we would like to get your feedback as soon as possible, because we
>>>> are continuing to work on it which means it is going to grow in size.
>>>>
>>>>
>>>> At this point in time we still have 124 of 176 calls to the function
>>>> itemData from ACITree.
>>>>
>>>> What does each patch contain:
>>>> 0001:
>>>> Very simple patch, we found out that the linter was not looking into
>>>> all the javascript test files, so this patch will ensure it is
>>>>
>>>> 0002:
>>>> New Tree abstraction. This patch contains the new Tree that works as
>>>> an adaptor for ACI Tree and is going to be used on all the extractions that
>>>> we are doing
>>>>
>>>> 0003:
>>>> Code that extracts, wrap with tests and replace ACI Tree invocations.
>>>> We start creating new pattern for the location of Javascript files
>>>> and their structure.
>>>> Create patterns for creation of dialogs (backup and restore)
>>>>
>>>
>>> Do you have some TODO left for the same?
>>> Or, is this the final one? Because - it gives us the better
>>> understanding during reviewing the patch.
>>>
>>> -- Thanks, Ashesh
>>>
>>>>
>>>>
>>>> Thanks
>>>> Joao
>>>>
>>>>
>>>> On Fri, Apr 27, 2018 at 5:34 AM Ashesh Vashi <
>>>> [email protected]> wrote:
>>>>
>>>>> I have quite a few comments for the patch.
>>>>> I will send them soon.
>>>>>
>>>>> On Fri, Apr 27, 2018, 14:45 Dave Page <[email protected]> wrote:
>>>>>
>>>>>> How is your work on this going Ashesh? Will you be committing today?
>>>>>>
>>>>>> On Wed, Apr 25, 2018 at 8:52 AM, Dave Page <[email protected]> wrote:
>>>>>>
>>>>>>> Ashesh; you had agreed to work on this early this week. Please
>>>>>>> ensure you do so today.
>>>>>>>
>>>>>>> Thanks.
>>>>>>>
>>>>>>> On Tue, Apr 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hi Hackers,
>>>>>>>>
>>>>>>>> Can someone review and merge this patch?
>>>>>>>>
>>>>>>>> Thanks
>>>>>>>> Joao
>>>>>>>>
>>>>>>>> On Wed, Apr 18, 2018 at 10:23 AM Joao De Almeida Pereira <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Hi Hackers,
>>>>>>>>> Any other comment about this patch?
>>>>>>>>>
>>>>>>>>> Thanks
>>>>>>>>> Joao
>>>>>>>>>
>>>>>>>>> On Tue, Apr 10, 2018 at 12:00 PM Joao De Almeida Pereira <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Hello Khushboo
>>>>>>>>>>
>>>>>>>>>> On Mon, Apr 9, 2018 at 1:59 AM Khushboo Vashi <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>
>>>>>>>>>>> I have reviewed your patch and have some suggestions.
>>>>>>>>>>>
>>>>>>>>>>> On Sat, Apr 7, 2018 at 12:43 AM, Joao De Almeida Pereira <
>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> Hello Murtuza/Dave,
>>>>>>>>>>>> Yes now the extracted functions are spread into different
>>>>>>>>>>>> files. The intent would be to make the files as small as possible, and also
>>>>>>>>>>>> to group and name them in a way that would be easy to understand what each
>>>>>>>>>>>> file is doing without the need of opening it.
>>>>>>>>>>>> As a example:
>>>>>>>>>>>> static/js/backup will contain all the backup related
>>>>>>>>>>>> functionality inside of this folder we can see the file:
>>>>>>>>>>>>
>>>>>>>>>>> menu_utils.js At this moment in time we decided to group all
>>>>>>>>>>>> the functions that are related to the menu, but we can split that also if
>>>>>>>>>>>> we believe it is easier to see.
>>>>>>>>>>>>
>>>>>>>>>>> It's really very good to see the separated code for backup
>>>>>>>>>>> module. As we have done for backup, we would like do it for other PG
>>>>>>>>>>> utilities like restore, maintenance etc.
>>>>>>>>>>> Considering this, we should separate the code in a way that some
>>>>>>>>>>> of the common functionalities can be used for other modules like menu (as
>>>>>>>>>>> you have mentioned above), dialogue factory etc.
>>>>>>>>>>> Also, I think these functionalities should be in their
>>>>>>>>>>> respective static folder instead of pgadmin/static.
>>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> About the location of the files. The move of the files to
>>>>>>>>>> pgadmin/static/js was made on purpose in order to clearly separate
>>>>>>>>>> Javascript from python code.
>>>>>>>>>> The rational behind it was
>>>>>>>>>> - Create a clear separation between the backend and frontend
>>>>>>>>>> - Having Javascript code concentrated in a single place,
>>>>>>>>>> hopefully, will encourage to developers to look for a functionality, that
>>>>>>>>>> is already implemented in another modules, because they are right there.
>>>>>>>>>> (When we started this journey we realized that the 'nodes' have a big
>>>>>>>>>> groups of code that could be shared, but because the Javascript is spread
>>>>>>>>>> everywhere it is much harder to look for it)
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> There are some drawbacks of this separation:
>>>>>>>>>> - When creating a new module we will need to put the javascript
>>>>>>>>>> in a separate location from the backend code
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>> static/js/datagrid folder contains all the datagrid related
>>>>>>>>>>>> functionality
>>>>>>>>>>>>
>>>>>>>>>>> Same as backup module, this should be in it's respective
>>>>>>>>>>> static/js folder.
>>>>>>>>>>>
>>>>>>>>>>>> Inside of the folder we can see the files:
>>>>>>>>>>>> get_panel_title.js is responsible for retrieving the name of
>>>>>>>>>>>> the panel
>>>>>>>>>>>> show_data.js is responsible for showing the datagrid
>>>>>>>>>>>> show_query_tool.js is responsible for showing the query tool
>>>>>>>>>>>>
>>>>>>>>>>>> Does this structure make sense?
>>>>>>>>>>>> Can you give an example of a comment that you think is missing
>>>>>>>>>>>> and that could help?
>>>>>>>>>>>>
>>>>>>>>>>>> As a personal note, unless the algorithm is very obscure or
>>>>>>>>>>>> very complicated, I believe that if the code needs comments it is a signal
>>>>>>>>>>>> that something needs to change in terms of naming, structure of the part in
>>>>>>>>>>>> question. This being said, I am open to add some comments that might help
>>>>>>>>>>>> people.
>>>>>>>>>>>>
>>>>>>>>>>> You are right, with the help of naming convention and structure
>>>>>>>>>>> of the code, any one can get the idea about the code. But it is very useful
>>>>>>>>>>> to understand the code
>>>>>>>>>>> very easily with the proper comments especially when there are
>>>>>>>>>>> multiple developers working on a single project.
>>>>>>>>>>>
>>>>>>>>>>> I found some of the places where it would be great to have
>>>>>>>>>>> comments.
>>>>>>>>>>>
>>>>>>>>>>> - treeMenu: new tree.Tree() in a browser.js
>>>>>>>>>>> - tree.js (especially Tree class)
>>>>>>>>>>>
>>>>>>>>>> About the comment point I need a more clear understanding on what
>>>>>>>>>> kind of comments you are looking for. Because when you read the function
>>>>>>>>>> names you understand the intent, what they are doing. The parameters also
>>>>>>>>>> explain what you need to pass into them.
>>>>>>>>>>
>>>>>>>>>> If what you are looking for in these comments is the reasoning
>>>>>>>>>> being the change itself, then that should be present in the commit message.
>>>>>>>>>> Specially because this is going to be a very big patch with a very big
>>>>>>>>>> number of changes.
>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> Thanks
>>>>>>>>>>>> Joao
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> Thanks,
>>>>>>>>>>> Khushboo
>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> On Fri, Apr 6, 2018 at 4:48 AM Murtuza Zabuawala <
>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>
>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>
>>>>>>>>>>>>> Patch looks good and working as expected.
>>>>>>>>>>>>>
>>>>>>>>>>>>> I also agree with Dave, Can we please add some comments in
>>>>>>>>>>>>> each file which can help us to understand the flow, I'm saying because now
>>>>>>>>>>>>> the code is segregated in so many separate files it will be hard to keep
>>>>>>>>>>>>> track of the flow from one file to another when debugging.
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> --
>>>>>>>>>>>>> Regards,
>>>>>>>>>>>>> Murtuza Zabuawala
>>>>>>>>>>>>> EnterpriseDB: http://www.enterprisedb.com
>>>>>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> On Thu, Apr 5, 2018 at 7:08 PM, Joao De Almeida Pereira <
>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>
>>>>>>>>>>>>>> Hi Khushboo,
>>>>>>>>>>>>>> Attached you can find both patches rebased
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Can you please rebase the second patch?
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida Pereira <
>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Attached you can find the patch that will start to decouple
>>>>>>>>>>>>>>>> pgAdmin from ACITree library.
>>>>>>>>>>>>>>>> This patch is intended to be merged after 3.0, because we
>>>>>>>>>>>>>>>> do not want to cause any entropy or delay the release, but we want to start
>>>>>>>>>>>>>>>> the discussion and show some code.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> This job that we started is a massive tech debt chore that
>>>>>>>>>>>>>>>> will take some time to finalize and we would love the help of the community
>>>>>>>>>>>>>>>> to do it.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> *Summary of the patch:*
>>>>>>>>>>>>>>>> 0001 patch:
>>>>>>>>>>>>>>>> - Creates a new tree that will allow us to create a
>>>>>>>>>>>>>>>> separation between the application and ACI Tree
>>>>>>>>>>>>>>>> - Creates a Fake Tree (Test double, for reference on the
>>>>>>>>>>>>>>>> available test doubles:
>>>>>>>>>>>>>>>> https://martinfowler.com/bliki/TestDouble.html) that can
>>>>>>>>>>>>>>>> be used to inplace to replace the ACITree and also encapsulate the new tree
>>>>>>>>>>>>>>>> behavior on our tests
>>>>>>>>>>>>>>>> - Adds tests for all the tree functionalities
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> 0002 patch:
>>>>>>>>>>>>>>>> - Extracts, refactors, adds tests and remove dependency
>>>>>>>>>>>>>>>> from ACI Tree on:
>>>>>>>>>>>>>>>> - getTreeNodeHierarchy
>>>>>>>>>>>>>>>> - on backup.js: menu_enabled, menu_enabled_server,
>>>>>>>>>>>>>>>> start_backup_global_server, backup_objects
>>>>>>>>>>>>>>>> - on datagrid.js: show_data_grid, get_panel_title,
>>>>>>>>>>>>>>>> show_query_tool
>>>>>>>>>>>>>>>> - Start using sprintf-js as Underscore.String is
>>>>>>>>>>>>>>>> deprecating sprintf function
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> This patch represents only 10 calls to ACITree.itemData out
>>>>>>>>>>>>>>>> of 176 that are spread around our code
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> *In Depth look on the process behind the patch:*
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> We started writing this patch with the idea that we need to
>>>>>>>>>>>>>>>> decouple pgAdmin4 from ACITree, because ACITree is no longer supported, the
>>>>>>>>>>>>>>>> documentation is non existent and ACITree is no longer being actively
>>>>>>>>>>>>>>>> developed.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Our process:
>>>>>>>>>>>>>>>> 1. We "randomly" selected a function that is part of the
>>>>>>>>>>>>>>>> ACITree. From this point we decided to replace that function with our own
>>>>>>>>>>>>>>>> version. The function that we choose was "itemData".
>>>>>>>>>>>>>>>> The function gives us all the "data" that a specific node
>>>>>>>>>>>>>>>> of the tree find.
>>>>>>>>>>>>>>>> Given in order to replace the tree we would need to have a
>>>>>>>>>>>>>>>> function that would give us the same information. We had 2 options:
>>>>>>>>>>>>>>>> a) Create a tree with a function called itemData
>>>>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>>>>> - At first view this was the simpler solution
>>>>>>>>>>>>>>>> - Would keep the status quo
>>>>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>>>>> - Not a OOP approach
>>>>>>>>>>>>>>>> - Not very flexible
>>>>>>>>>>>>>>>> b) Create a tree that would return a node given an ID and
>>>>>>>>>>>>>>>> then the node would be responsible for giving it's data.
>>>>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>>>>> - OOP Approach
>>>>>>>>>>>>>>>> - More flexible and we do not need to bring the tree
>>>>>>>>>>>>>>>> around, just a node
>>>>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>>>>> - Break the current status quo
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Given these 2 options we decided to go for a more OOP
>>>>>>>>>>>>>>>> approach creating a Tree and a TreeNode classes, that in the future will be
>>>>>>>>>>>>>>>> renamed to ACITreeWrapper and TreeNode.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> 2. After we decided on the starting point we searched for
>>>>>>>>>>>>>>>> occurrences of the function "itemData" and we found out that there were 303
>>>>>>>>>>>>>>>> occurrences of "itemData" in the code and roughly 176 calls to the function
>>>>>>>>>>>>>>>> itself (some of the hits were variable names).
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> 3. We selected the first file on the search and found the
>>>>>>>>>>>>>>>> function that was responsible for calling the itemData function.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> 4. Extracted the function to a separate file
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> 5. Wrap this function with tests
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> 6. Refactor the function to ES6, give more declarative
>>>>>>>>>>>>>>>> names to variables and break the functions into smaller chunks
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> 7. When all the tests were passing we replaced ACITree with
>>>>>>>>>>>>>>>> our Tree
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> 8. We ensured that all tests were passing
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> 9. Remove function from the original file and use the new
>>>>>>>>>>>>>>>> function
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> 10. Ensure everything still works
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> 11. Find the next function and execute from step 4 until
>>>>>>>>>>>>>>>> all the functions are replaced, refactored and tested.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> As you can see by the process this is a pretty huge
>>>>>>>>>>>>>>>> undertake, because of the number of calls to the function. This is just the
>>>>>>>>>>>>>>>> first step on the direction of completely isolating the ACITree so that we
>>>>>>>>>>>>>>>> can solve the problem with a large number of elements on the tree.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> *What is on our radar that we need to address:*
>>>>>>>>>>>>>>>> - Finish the complete decoupling of the ACITree
>>>>>>>>>>>>>>>> - Performance of the current tree implementation
>>>>>>>>>>>>>>>> - Tweak the naming of the Tree class to explicitly tell us
>>>>>>>>>>>>>>>> this is to use only with ACITree.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>> --
>>>>>>> Dave Page
>>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>>> Twitter: @pgsnake
>>>>>>>
>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>
>>>>>>
>>>>>>
>>>>>>
>>>>>> --
>>>>>> Dave Page
>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>> Twitter: @pgsnake
>>>>>>
>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>> The Enterprise PostgreSQL Company
>>>>>>
>>>>>
>>>
>>
>
Attachments:
[image/png] Screen Shot 2018-04-30 at 10.50.24 AM.png (237.9K, 3-Screen%20Shot%202018-04-30%20at%2010.50.24%20AM.png)
download | view image
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-30 15:00 Ashesh Vashi <[email protected]>
parent: Joao De Almeida Pereira <[email protected]>
1 sibling, 1 reply; 69+ messages in thread
From: Ashesh Vashi @ 2018-04-30 15:00 UTC (permalink / raw)
To: Joao De Almeida Pereira <[email protected]>; +Cc: Dave Page <[email protected]>; Khushboo Vashi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
On Sat, Apr 28, 2018 at 3:55 AM, Joao De Almeida Pereira <
[email protected]> wrote:
> Hi Hackers,
> As you are aware we kept on working on the patch, so we are attaching to
> this email a new version of the patch.
> This new version contains all the changes in the previous one plus more
> extractions of functions and refactoring of code.
>
> The objective of this patch is to create a separation between pgAdmin and
> the ACI Tree. We are doing this because we realized that at this point in
> time we have the ACI Tree all over the code of pgAdmin. I found a very
> interesting article that really talks about this:
> https://medium.freecodecamp.org/code-dependencies-are-the-
> devil-35ed28b556d
>
> In this patch there are some visions and ideas about the location of the
> code, the way to organize it and also try to pave the future for a
> application that is stable, easy to develop on and that can be release at a
> times notice.
>
> We are investing a big chunk of our time in doing this refactoring, but
> while doing that we also try to respond to the patches sent to the mailing
> list. We would like the feedback from the community because we believe this
> is a changing point for the application. The idea is to change the way we
> develop this application, instead of only correcting a bug of developing a
> feature, with every commit we should correct the bug or develop a feature
> but leave the code a little better than we found it (Refactoring,
> refactoring, refactoring). This is hard work but that is what the users
> from pgAdmin expect from this community of developers.
>
>
> ======
>
>
>
> It is a huge patch
> 86 files changed, 5492 inserts, 1840 deletions
> and we would like to get your feedback as soon as possible, because we are
> continuing to work on it which means it is going to grow in size.
>
>
> At this point in time we still have 124 of 176 calls to the function
> itemData from ACITree.
>
> What does each patch contain:
> 0001:
> Very simple patch, we found out that the linter was not looking into all
> the javascript test files, so this patch will ensure it is
>
Committed the patch along with the regression introduced because of this
patch.
>
> 0002:
> New Tree abstraction. This patch contains the new Tree that works as an
> adaptor for ACI Tree and is going to be used on all the extractions that we
> are doing.
>
I was expecting a separate layer between the tree implementation, and
aciTree adaptor.
Please find the patch for the example.
It will separate the two layers, and easy to replace with the new
implemenation in future.
>
> 0003:
> Code that extracts, wrap with tests and replace ACI Tree invocations.
>
There are many small cases left in the patches.
Hence - I would like to know the TODO list created by you.
e.g. When we remove any of the object from the database server, we're not
yet removing the respective node from the new implementation, and its
children.
>
> We start creating new pattern for the location of Javascript files and
> their structure.
>
I would not like to see that changes in this patch.
I would like us to come up with the actual design about the hot
pluggability before going in this direction.
> Create patterns for creation of dialogs (backup and restore)
>
It's better - we don't change the directory structure at the moment.
I am not against dividing the big javascript files in small chunks, but - I
would like us to discuss first about the hot plugins design first.
-- Thanks, Ashesh
>
>
>
> Thanks
> Joao
>
>
> On Fri, Apr 27, 2018 at 5:34 AM Ashesh Vashi <
> [email protected]> wrote:
>
>> I have quite a few comments for the patch.
>> I will send them soon.
>>
>> On Fri, Apr 27, 2018, 14:45 Dave Page <[email protected]> wrote:
>>
>>> How is your work on this going Ashesh? Will you be committing today?
>>>
>>> On Wed, Apr 25, 2018 at 8:52 AM, Dave Page <[email protected]> wrote:
>>>
>>>> Ashesh; you had agreed to work on this early this week. Please ensure
>>>> you do so today.
>>>>
>>>> Thanks.
>>>>
>>>> On Tue, Apr 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi Hackers,
>>>>>
>>>>> Can someone review and merge this patch?
>>>>>
>>>>> Thanks
>>>>> Joao
>>>>>
>>>>> On Wed, Apr 18, 2018 at 10:23 AM Joao De Almeida Pereira <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hi Hackers,
>>>>>> Any other comment about this patch?
>>>>>>
>>>>>> Thanks
>>>>>> Joao
>>>>>>
>>>>>> On Tue, Apr 10, 2018 at 12:00 PM Joao De Almeida Pereira <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hello Khushboo
>>>>>>>
>>>>>>> On Mon, Apr 9, 2018 at 1:59 AM Khushboo Vashi <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hi Joao,
>>>>>>>>
>>>>>>>> I have reviewed your patch and have some suggestions.
>>>>>>>>
>>>>>>>> On Sat, Apr 7, 2018 at 12:43 AM, Joao De Almeida Pereira <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Hello Murtuza/Dave,
>>>>>>>>> Yes now the extracted functions are spread into different files.
>>>>>>>>> The intent would be to make the files as small as possible, and also to
>>>>>>>>> group and name them in a way that would be easy to understand what each
>>>>>>>>> file is doing without the need of opening it.
>>>>>>>>> As a example:
>>>>>>>>> static/js/backup will contain all the backup related
>>>>>>>>> functionality inside of this folder we can see the file:
>>>>>>>>>
>>>>>>>> menu_utils.js At this moment in time we decided to group all the
>>>>>>>>> functions that are related to the menu, but we can split that also if we
>>>>>>>>> believe it is easier to see.
>>>>>>>>>
>>>>>>>> It's really very good to see the separated code for backup module.
>>>>>>>> As we have done for backup, we would like do it for other PG utilities like
>>>>>>>> restore, maintenance etc.
>>>>>>>> Considering this, we should separate the code in a way that some of
>>>>>>>> the common functionalities can be used for other modules like menu (as you
>>>>>>>> have mentioned above), dialogue factory etc.
>>>>>>>> Also, I think these functionalities should be in their respective
>>>>>>>> static folder instead of pgadmin/static.
>>>>>>>>
>>>>>>>
>>>>>>> About the location of the files. The move of the files to
>>>>>>> pgadmin/static/js was made on purpose in order to clearly separate
>>>>>>> Javascript from python code.
>>>>>>> The rational behind it was
>>>>>>> - Create a clear separation between the backend and frontend
>>>>>>> - Having Javascript code concentrated in a single place, hopefully,
>>>>>>> will encourage to developers to look for a functionality, that is already
>>>>>>> implemented in another modules, because they are right there. (When we
>>>>>>> started this journey we realized that the 'nodes' have a big groups of code
>>>>>>> that could be shared, but because the Javascript is spread everywhere it is
>>>>>>> much harder to look for it)
>>>>>>>
>>>>>>>
>>>>>>> There are some drawbacks of this separation:
>>>>>>> - When creating a new module we will need to put the javascript in a
>>>>>>> separate location from the backend code
>>>>>>>
>>>>>>>
>>>>>>>>
>>>>>>>>
>>>>>>>>> static/js/datagrid folder contains all the datagrid related
>>>>>>>>> functionality
>>>>>>>>>
>>>>>>>> Same as backup module, this should be in it's respective static/js
>>>>>>>> folder.
>>>>>>>>
>>>>>>>>> Inside of the folder we can see the files:
>>>>>>>>> get_panel_title.js is responsible for retrieving the name of the
>>>>>>>>> panel
>>>>>>>>> show_data.js is responsible for showing the datagrid
>>>>>>>>> show_query_tool.js is responsible for showing the query tool
>>>>>>>>>
>>>>>>>>> Does this structure make sense?
>>>>>>>>> Can you give an example of a comment that you think is missing and
>>>>>>>>> that could help?
>>>>>>>>>
>>>>>>>>> As a personal note, unless the algorithm is very obscure or very
>>>>>>>>> complicated, I believe that if the code needs comments it is a signal that
>>>>>>>>> something needs to change in terms of naming, structure of the part in
>>>>>>>>> question. This being said, I am open to add some comments that might help
>>>>>>>>> people.
>>>>>>>>>
>>>>>>>> You are right, with the help of naming convention and structure of
>>>>>>>> the code, any one can get the idea about the code. But it is very useful to
>>>>>>>> understand the code
>>>>>>>> very easily with the proper comments especially when there are
>>>>>>>> multiple developers working on a single project.
>>>>>>>>
>>>>>>>> I found some of the places where it would be great to have comments.
>>>>>>>>
>>>>>>>> - treeMenu: new tree.Tree() in a browser.js
>>>>>>>> - tree.js (especially Tree class)
>>>>>>>>
>>>>>>> About the comment point I need a more clear understanding on what
>>>>>>> kind of comments you are looking for. Because when you read the function
>>>>>>> names you understand the intent, what they are doing. The parameters also
>>>>>>> explain what you need to pass into them.
>>>>>>>
>>>>>>> If what you are looking for in these comments is the reasoning being
>>>>>>> the change itself, then that should be present in the commit message.
>>>>>>> Specially because this is going to be a very big patch with a very big
>>>>>>> number of changes.
>>>>>>>
>>>>>>>>
>>>>>>>> Thanks
>>>>>>>>> Joao
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> Thanks,
>>>>>>>> Khushboo
>>>>>>>>
>>>>>>>>>
>>>>>>>>> On Fri, Apr 6, 2018 at 4:48 AM Murtuza Zabuawala <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Hi Joao,
>>>>>>>>>>
>>>>>>>>>> Patch looks good and working as expected.
>>>>>>>>>>
>>>>>>>>>> I also agree with Dave, Can we please add some comments in each
>>>>>>>>>> file which can help us to understand the flow, I'm saying because now the
>>>>>>>>>> code is segregated in so many separate files it will be hard to keep track
>>>>>>>>>> of the flow from one file to another when debugging.
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> --
>>>>>>>>>> Regards,
>>>>>>>>>> Murtuza Zabuawala
>>>>>>>>>> EnterpriseDB: http://www.enterprisedb.com
>>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> On Thu, Apr 5, 2018 at 7:08 PM, Joao De Almeida Pereira <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> Hi Khushboo,
>>>>>>>>>>> Attached you can find both patches rebased
>>>>>>>>>>>
>>>>>>>>>>> Thanks
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>
>>>>>>>>>>>> Can you please rebase the second patch?
>>>>>>>>>>>>
>>>>>>>>>>>> Thanks,
>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida Pereira <
>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>
>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>
>>>>>>>>>>>>> Attached you can find the patch that will start to decouple
>>>>>>>>>>>>> pgAdmin from ACITree library.
>>>>>>>>>>>>> This patch is intended to be merged after 3.0, because we do
>>>>>>>>>>>>> not want to cause any entropy or delay the release, but we want to start
>>>>>>>>>>>>> the discussion and show some code.
>>>>>>>>>>>>>
>>>>>>>>>>>>> This job that we started is a massive tech debt chore that
>>>>>>>>>>>>> will take some time to finalize and we would love the help of the community
>>>>>>>>>>>>> to do it.
>>>>>>>>>>>>>
>>>>>>>>>>>>> *Summary of the patch:*
>>>>>>>>>>>>> 0001 patch:
>>>>>>>>>>>>> - Creates a new tree that will allow us to create a
>>>>>>>>>>>>> separation between the application and ACI Tree
>>>>>>>>>>>>> - Creates a Fake Tree (Test double, for reference on the
>>>>>>>>>>>>> available test doubles: https://martinfowler.
>>>>>>>>>>>>> com/bliki/TestDouble.html) that can be used to inplace to
>>>>>>>>>>>>> replace the ACITree and also encapsulate the new tree behavior on our tests
>>>>>>>>>>>>> - Adds tests for all the tree functionalities
>>>>>>>>>>>>>
>>>>>>>>>>>>> 0002 patch:
>>>>>>>>>>>>> - Extracts, refactors, adds tests and remove dependency from
>>>>>>>>>>>>> ACI Tree on:
>>>>>>>>>>>>> - getTreeNodeHierarchy
>>>>>>>>>>>>> - on backup.js: menu_enabled, menu_enabled_server,
>>>>>>>>>>>>> start_backup_global_server, backup_objects
>>>>>>>>>>>>> - on datagrid.js: show_data_grid, get_panel_title,
>>>>>>>>>>>>> show_query_tool
>>>>>>>>>>>>> - Start using sprintf-js as Underscore.String is deprecating
>>>>>>>>>>>>> sprintf function
>>>>>>>>>>>>>
>>>>>>>>>>>>> This patch represents only 10 calls to ACITree.itemData out of
>>>>>>>>>>>>> 176 that are spread around our code
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> *In Depth look on the process behind the patch:*
>>>>>>>>>>>>>
>>>>>>>>>>>>> We started writing this patch with the idea that we need to
>>>>>>>>>>>>> decouple pgAdmin4 from ACITree, because ACITree is no longer supported, the
>>>>>>>>>>>>> documentation is non existent and ACITree is no longer being actively
>>>>>>>>>>>>> developed.
>>>>>>>>>>>>>
>>>>>>>>>>>>> Our process:
>>>>>>>>>>>>> 1. We "randomly" selected a function that is part of the
>>>>>>>>>>>>> ACITree. From this point we decided to replace that function with our own
>>>>>>>>>>>>> version. The function that we choose was "itemData".
>>>>>>>>>>>>> The function gives us all the "data" that a specific node of
>>>>>>>>>>>>> the tree find.
>>>>>>>>>>>>> Given in order to replace the tree we would need to have a
>>>>>>>>>>>>> function that would give us the same information. We had 2 options:
>>>>>>>>>>>>> a) Create a tree with a function called itemData
>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>> - At first view this was the simpler solution
>>>>>>>>>>>>> - Would keep the status quo
>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>> - Not a OOP approach
>>>>>>>>>>>>> - Not very flexible
>>>>>>>>>>>>> b) Create a tree that would return a node given an ID and
>>>>>>>>>>>>> then the node would be responsible for giving it's data.
>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>> - OOP Approach
>>>>>>>>>>>>> - More flexible and we do not need to bring the tree around,
>>>>>>>>>>>>> just a node
>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>> - Break the current status quo
>>>>>>>>>>>>>
>>>>>>>>>>>>> Given these 2 options we decided to go for a more OOP approach
>>>>>>>>>>>>> creating a Tree and a TreeNode classes, that in the future will be renamed
>>>>>>>>>>>>> to ACITreeWrapper and TreeNode.
>>>>>>>>>>>>>
>>>>>>>>>>>>> 2. After we decided on the starting point we searched for
>>>>>>>>>>>>> occurrences of the function "itemData" and we found out that there were 303
>>>>>>>>>>>>> occurrences of "itemData" in the code and roughly 176 calls to the function
>>>>>>>>>>>>> itself (some of the hits were variable names).
>>>>>>>>>>>>>
>>>>>>>>>>>>> 3. We selected the first file on the search and found the
>>>>>>>>>>>>> function that was responsible for calling the itemData function.
>>>>>>>>>>>>>
>>>>>>>>>>>>> 4. Extracted the function to a separate file
>>>>>>>>>>>>>
>>>>>>>>>>>>> 5. Wrap this function with tests
>>>>>>>>>>>>>
>>>>>>>>>>>>> 6. Refactor the function to ES6, give more declarative names
>>>>>>>>>>>>> to variables and break the functions into smaller chunks
>>>>>>>>>>>>>
>>>>>>>>>>>>> 7. When all the tests were passing we replaced ACITree with
>>>>>>>>>>>>> our Tree
>>>>>>>>>>>>>
>>>>>>>>>>>>> 8. We ensured that all tests were passing
>>>>>>>>>>>>>
>>>>>>>>>>>>> 9. Remove function from the original file and use the new
>>>>>>>>>>>>> function
>>>>>>>>>>>>>
>>>>>>>>>>>>> 10. Ensure everything still works
>>>>>>>>>>>>>
>>>>>>>>>>>>> 11. Find the next function and execute from step 4 until all
>>>>>>>>>>>>> the functions are replaced, refactored and tested.
>>>>>>>>>>>>>
>>>>>>>>>>>>> As you can see by the process this is a pretty huge undertake,
>>>>>>>>>>>>> because of the number of calls to the function. This is just the first step
>>>>>>>>>>>>> on the direction of completely isolating the ACITree so that we can solve
>>>>>>>>>>>>> the problem with a large number of elements on the tree.
>>>>>>>>>>>>>
>>>>>>>>>>>>> *What is on our radar that we need to address:*
>>>>>>>>>>>>> - Finish the complete decoupling of the ACITree
>>>>>>>>>>>>> - Performance of the current tree implementation
>>>>>>>>>>>>> - Tweak the naming of the Tree class to explicitly tell us
>>>>>>>>>>>>> this is to use only with ACITree.
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>
>>>>
>>>>
>>>> --
>>>> Dave Page
>>>> Blog: http://pgsnake.blogspot.com
>>>> Twitter: @pgsnake
>>>>
>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>> The Enterprise PostgreSQL Company
>>>>
>>>
>>>
>>>
>>> --
>>> Dave Page
>>> Blog: http://pgsnake.blogspot.com
>>> Twitter: @pgsnake
>>>
>>> EnterpriseDB UK: http://www.enterprisedb.com
>>> The Enterprise PostgreSQL Company
>>>
>>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-30 15:03 Ashesh Vashi <[email protected]>
parent: Ashesh Vashi <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Ashesh Vashi @ 2018-04-30 15:03 UTC (permalink / raw)
To: Joao De Almeida Pereira <[email protected]>; +Cc: Dave Page <[email protected]>; Khushboo Vashi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
On Mon, Apr 30, 2018 at 8:30 PM, Ashesh Vashi <[email protected]
> wrote:
> On Sat, Apr 28, 2018 at 3:55 AM, Joao De Almeida Pereira <
> [email protected]> wrote:
>
>> Hi Hackers,
>> As you are aware we kept on working on the patch, so we are attaching to
>> this email a new version of the patch.
>> This new version contains all the changes in the previous one plus more
>> extractions of functions and refactoring of code.
>>
>> The objective of this patch is to create a separation between pgAdmin and
>> the ACI Tree. We are doing this because we realized that at this point in
>> time we have the ACI Tree all over the code of pgAdmin. I found a very
>> interesting article that really talks about this:
>> https://medium.freecodecamp.org/code-dependencies-are-the-de
>> vil-35ed28b556d
>>
>> In this patch there are some visions and ideas about the location of the
>> code, the way to organize it and also try to pave the future for a
>> application that is stable, easy to develop on and that can be release at a
>> times notice.
>>
>> We are investing a big chunk of our time in doing this refactoring, but
>> while doing that we also try to respond to the patches sent to the mailing
>> list. We would like the feedback from the community because we believe this
>> is a changing point for the application. The idea is to change the way we
>> develop this application, instead of only correcting a bug of developing a
>> feature, with every commit we should correct the bug or develop a feature
>> but leave the code a little better than we found it (Refactoring,
>> refactoring, refactoring). This is hard work but that is what the users
>> from pgAdmin expect from this community of developers.
>>
>>
>> ======
>>
>>
>>
>> It is a huge patch
>> 86 files changed, 5492 inserts, 1840 deletions
>> and we would like to get your feedback as soon as possible, because we
>> are continuing to work on it which means it is going to grow in size.
>>
>>
>> At this point in time we still have 124 of 176 calls to the function
>> itemData from ACITree.
>>
>> What does each patch contain:
>> 0001:
>> Very simple patch, we found out that the linter was not looking into
>> all the javascript test files, so this patch will ensure it is
>>
> Committed the patch along with the regression introduced because of this
> patch.
>
>>
>> 0002:
>> New Tree abstraction. This patch contains the new Tree that works as an
>> adaptor for ACI Tree and is going to be used on all the extractions that we
>> are doing.
>>
>
> I was expecting a separate layer between the tree implementation, and
> aciTree adaptor.
> Please find the patch for the example.
>
> It will separate the two layers, and easy to replace with the new
> implemenation in future.
>
Oops forgot to attach the patch.
Please find the patch attached.
-- Thanks, Ashesh
>
>> 0003:
>> Code that extracts, wrap with tests and replace ACI Tree invocations.
>>
> There are many small cases left in the patches.
> Hence - I would like to know the TODO list created by you.
>
> e.g. When we remove any of the object from the database server, we're not
> yet removing the respective node from the new implementation, and its
> children.
>
>>
>> We start creating new pattern for the location of Javascript files and
>> their structure.
>>
> I would not like to see that changes in this patch.
> I would like us to come up with the actual design about the hot
> pluggability before going in this direction.
>
>> Create patterns for creation of dialogs (backup and restore)
>>
> It's better - we don't change the directory structure at the moment.
>
> I am not against dividing the big javascript files in small chunks, but -
> I would like us to discuss first about the hot plugins design first.
>
> -- Thanks, Ashesh
>
>>
>>
>
>>
>> Thanks
>> Joao
>>
>>
>> On Fri, Apr 27, 2018 at 5:34 AM Ashesh Vashi <
>> [email protected]> wrote:
>>
>>> I have quite a few comments for the patch.
>>> I will send them soon.
>>>
>>> On Fri, Apr 27, 2018, 14:45 Dave Page <[email protected]> wrote:
>>>
>>>> How is your work on this going Ashesh? Will you be committing today?
>>>>
>>>> On Wed, Apr 25, 2018 at 8:52 AM, Dave Page <[email protected]> wrote:
>>>>
>>>>> Ashesh; you had agreed to work on this early this week. Please ensure
>>>>> you do so today.
>>>>>
>>>>> Thanks.
>>>>>
>>>>> On Tue, Apr 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hi Hackers,
>>>>>>
>>>>>> Can someone review and merge this patch?
>>>>>>
>>>>>> Thanks
>>>>>> Joao
>>>>>>
>>>>>> On Wed, Apr 18, 2018 at 10:23 AM Joao De Almeida Pereira <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hi Hackers,
>>>>>>> Any other comment about this patch?
>>>>>>>
>>>>>>> Thanks
>>>>>>> Joao
>>>>>>>
>>>>>>> On Tue, Apr 10, 2018 at 12:00 PM Joao De Almeida Pereira <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hello Khushboo
>>>>>>>>
>>>>>>>> On Mon, Apr 9, 2018 at 1:59 AM Khushboo Vashi <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Hi Joao,
>>>>>>>>>
>>>>>>>>> I have reviewed your patch and have some suggestions.
>>>>>>>>>
>>>>>>>>> On Sat, Apr 7, 2018 at 12:43 AM, Joao De Almeida Pereira <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Hello Murtuza/Dave,
>>>>>>>>>> Yes now the extracted functions are spread into different files.
>>>>>>>>>> The intent would be to make the files as small as possible, and also to
>>>>>>>>>> group and name them in a way that would be easy to understand what each
>>>>>>>>>> file is doing without the need of opening it.
>>>>>>>>>> As a example:
>>>>>>>>>> static/js/backup will contain all the backup related
>>>>>>>>>> functionality inside of this folder we can see the file:
>>>>>>>>>>
>>>>>>>>> menu_utils.js At this moment in time we decided to group all the
>>>>>>>>>> functions that are related to the menu, but we can split that also if we
>>>>>>>>>> believe it is easier to see.
>>>>>>>>>>
>>>>>>>>> It's really very good to see the separated code for backup module.
>>>>>>>>> As we have done for backup, we would like do it for other PG utilities like
>>>>>>>>> restore, maintenance etc.
>>>>>>>>> Considering this, we should separate the code in a way that some
>>>>>>>>> of the common functionalities can be used for other modules like menu (as
>>>>>>>>> you have mentioned above), dialogue factory etc.
>>>>>>>>> Also, I think these functionalities should be in their respective
>>>>>>>>> static folder instead of pgadmin/static.
>>>>>>>>>
>>>>>>>>
>>>>>>>> About the location of the files. The move of the files to
>>>>>>>> pgadmin/static/js was made on purpose in order to clearly separate
>>>>>>>> Javascript from python code.
>>>>>>>> The rational behind it was
>>>>>>>> - Create a clear separation between the backend and frontend
>>>>>>>> - Having Javascript code concentrated in a single place, hopefully,
>>>>>>>> will encourage to developers to look for a functionality, that is already
>>>>>>>> implemented in another modules, because they are right there. (When we
>>>>>>>> started this journey we realized that the 'nodes' have a big groups of code
>>>>>>>> that could be shared, but because the Javascript is spread everywhere it is
>>>>>>>> much harder to look for it)
>>>>>>>>
>>>>>>>>
>>>>>>>> There are some drawbacks of this separation:
>>>>>>>> - When creating a new module we will need to put the javascript in
>>>>>>>> a separate location from the backend code
>>>>>>>>
>>>>>>>>
>>>>>>>>>
>>>>>>>>>
>>>>>>>>>> static/js/datagrid folder contains all the datagrid related
>>>>>>>>>> functionality
>>>>>>>>>>
>>>>>>>>> Same as backup module, this should be in it's respective
>>>>>>>>> static/js folder.
>>>>>>>>>
>>>>>>>>>> Inside of the folder we can see the files:
>>>>>>>>>> get_panel_title.js is responsible for retrieving the name of the
>>>>>>>>>> panel
>>>>>>>>>> show_data.js is responsible for showing the datagrid
>>>>>>>>>> show_query_tool.js is responsible for showing the query tool
>>>>>>>>>>
>>>>>>>>>> Does this structure make sense?
>>>>>>>>>> Can you give an example of a comment that you think is missing
>>>>>>>>>> and that could help?
>>>>>>>>>>
>>>>>>>>>> As a personal note, unless the algorithm is very obscure or very
>>>>>>>>>> complicated, I believe that if the code needs comments it is a signal that
>>>>>>>>>> something needs to change in terms of naming, structure of the part in
>>>>>>>>>> question. This being said, I am open to add some comments that might help
>>>>>>>>>> people.
>>>>>>>>>>
>>>>>>>>> You are right, with the help of naming convention and structure of
>>>>>>>>> the code, any one can get the idea about the code. But it is very useful to
>>>>>>>>> understand the code
>>>>>>>>> very easily with the proper comments especially when there are
>>>>>>>>> multiple developers working on a single project.
>>>>>>>>>
>>>>>>>>> I found some of the places where it would be great to have
>>>>>>>>> comments.
>>>>>>>>>
>>>>>>>>> - treeMenu: new tree.Tree() in a browser.js
>>>>>>>>> - tree.js (especially Tree class)
>>>>>>>>>
>>>>>>>> About the comment point I need a more clear understanding on what
>>>>>>>> kind of comments you are looking for. Because when you read the function
>>>>>>>> names you understand the intent, what they are doing. The parameters also
>>>>>>>> explain what you need to pass into them.
>>>>>>>>
>>>>>>>> If what you are looking for in these comments is the reasoning
>>>>>>>> being the change itself, then that should be present in the commit message.
>>>>>>>> Specially because this is going to be a very big patch with a very big
>>>>>>>> number of changes.
>>>>>>>>
>>>>>>>>>
>>>>>>>>> Thanks
>>>>>>>>>> Joao
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> Thanks,
>>>>>>>>> Khushboo
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> On Fri, Apr 6, 2018 at 4:48 AM Murtuza Zabuawala <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>
>>>>>>>>>>> Patch looks good and working as expected.
>>>>>>>>>>>
>>>>>>>>>>> I also agree with Dave, Can we please add some comments in each
>>>>>>>>>>> file which can help us to understand the flow, I'm saying because now the
>>>>>>>>>>> code is segregated in so many separate files it will be hard to keep track
>>>>>>>>>>> of the flow from one file to another when debugging.
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> --
>>>>>>>>>>> Regards,
>>>>>>>>>>> Murtuza Zabuawala
>>>>>>>>>>> EnterpriseDB: http://www.enterprisedb.com
>>>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> On Thu, Apr 5, 2018 at 7:08 PM, Joao De Almeida Pereira <
>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> Hi Khushboo,
>>>>>>>>>>>> Attached you can find both patches rebased
>>>>>>>>>>>>
>>>>>>>>>>>> Thanks
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>
>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>
>>>>>>>>>>>>> Can you please rebase the second patch?
>>>>>>>>>>>>>
>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida Pereira <
>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>
>>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Attached you can find the patch that will start to decouple
>>>>>>>>>>>>>> pgAdmin from ACITree library.
>>>>>>>>>>>>>> This patch is intended to be merged after 3.0, because we do
>>>>>>>>>>>>>> not want to cause any entropy or delay the release, but we want to start
>>>>>>>>>>>>>> the discussion and show some code.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> This job that we started is a massive tech debt chore that
>>>>>>>>>>>>>> will take some time to finalize and we would love the help of the community
>>>>>>>>>>>>>> to do it.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> *Summary of the patch:*
>>>>>>>>>>>>>> 0001 patch:
>>>>>>>>>>>>>> - Creates a new tree that will allow us to create a
>>>>>>>>>>>>>> separation between the application and ACI Tree
>>>>>>>>>>>>>> - Creates a Fake Tree (Test double, for reference on the
>>>>>>>>>>>>>> available test doubles: https://martinfowler.
>>>>>>>>>>>>>> com/bliki/TestDouble.html) that can be used to inplace to
>>>>>>>>>>>>>> replace the ACITree and also encapsulate the new tree behavior on our tests
>>>>>>>>>>>>>> - Adds tests for all the tree functionalities
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> 0002 patch:
>>>>>>>>>>>>>> - Extracts, refactors, adds tests and remove dependency from
>>>>>>>>>>>>>> ACI Tree on:
>>>>>>>>>>>>>> - getTreeNodeHierarchy
>>>>>>>>>>>>>> - on backup.js: menu_enabled, menu_enabled_server,
>>>>>>>>>>>>>> start_backup_global_server, backup_objects
>>>>>>>>>>>>>> - on datagrid.js: show_data_grid, get_panel_title,
>>>>>>>>>>>>>> show_query_tool
>>>>>>>>>>>>>> - Start using sprintf-js as Underscore.String is deprecating
>>>>>>>>>>>>>> sprintf function
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> This patch represents only 10 calls to ACITree.itemData out
>>>>>>>>>>>>>> of 176 that are spread around our code
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> *In Depth look on the process behind the patch:*
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> We started writing this patch with the idea that we need to
>>>>>>>>>>>>>> decouple pgAdmin4 from ACITree, because ACITree is no longer supported, the
>>>>>>>>>>>>>> documentation is non existent and ACITree is no longer being actively
>>>>>>>>>>>>>> developed.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Our process:
>>>>>>>>>>>>>> 1. We "randomly" selected a function that is part of the
>>>>>>>>>>>>>> ACITree. From this point we decided to replace that function with our own
>>>>>>>>>>>>>> version. The function that we choose was "itemData".
>>>>>>>>>>>>>> The function gives us all the "data" that a specific node of
>>>>>>>>>>>>>> the tree find.
>>>>>>>>>>>>>> Given in order to replace the tree we would need to have a
>>>>>>>>>>>>>> function that would give us the same information. We had 2 options:
>>>>>>>>>>>>>> a) Create a tree with a function called itemData
>>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>>> - At first view this was the simpler solution
>>>>>>>>>>>>>> - Would keep the status quo
>>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>>> - Not a OOP approach
>>>>>>>>>>>>>> - Not very flexible
>>>>>>>>>>>>>> b) Create a tree that would return a node given an ID and
>>>>>>>>>>>>>> then the node would be responsible for giving it's data.
>>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>>> - OOP Approach
>>>>>>>>>>>>>> - More flexible and we do not need to bring the tree around,
>>>>>>>>>>>>>> just a node
>>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>>> - Break the current status quo
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Given these 2 options we decided to go for a more OOP
>>>>>>>>>>>>>> approach creating a Tree and a TreeNode classes, that in the future will be
>>>>>>>>>>>>>> renamed to ACITreeWrapper and TreeNode.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> 2. After we decided on the starting point we searched for
>>>>>>>>>>>>>> occurrences of the function "itemData" and we found out that there were 303
>>>>>>>>>>>>>> occurrences of "itemData" in the code and roughly 176 calls to the function
>>>>>>>>>>>>>> itself (some of the hits were variable names).
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> 3. We selected the first file on the search and found the
>>>>>>>>>>>>>> function that was responsible for calling the itemData function.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> 4. Extracted the function to a separate file
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> 5. Wrap this function with tests
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> 6. Refactor the function to ES6, give more declarative names
>>>>>>>>>>>>>> to variables and break the functions into smaller chunks
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> 7. When all the tests were passing we replaced ACITree with
>>>>>>>>>>>>>> our Tree
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> 8. We ensured that all tests were passing
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> 9. Remove function from the original file and use the new
>>>>>>>>>>>>>> function
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> 10. Ensure everything still works
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> 11. Find the next function and execute from step 4 until all
>>>>>>>>>>>>>> the functions are replaced, refactored and tested.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> As you can see by the process this is a pretty huge
>>>>>>>>>>>>>> undertake, because of the number of calls to the function. This is just the
>>>>>>>>>>>>>> first step on the direction of completely isolating the ACITree so that we
>>>>>>>>>>>>>> can solve the problem with a large number of elements on the tree.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> *What is on our radar that we need to address:*
>>>>>>>>>>>>>> - Finish the complete decoupling of the ACITree
>>>>>>>>>>>>>> - Performance of the current tree implementation
>>>>>>>>>>>>>> - Tweak the naming of the Tree class to explicitly tell us
>>>>>>>>>>>>>> this is to use only with ACITree.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>
>>>>>
>>>>>
>>>>> --
>>>>> Dave Page
>>>>> Blog: http://pgsnake.blogspot.com
>>>>> Twitter: @pgsnake
>>>>>
>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>> The Enterprise PostgreSQL Company
>>>>>
>>>>
>>>>
>>>>
>>>> --
>>>> Dave Page
>>>> Blog: http://pgsnake.blogspot.com
>>>> Twitter: @pgsnake
>>>>
>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>> The Enterprise PostgreSQL Company
>>>>
>>>
>
Attachments:
[application/octet-stream] 0002-New-tree-implemenation.patch (23.7K, 3-0002-New-tree-implemenation.patch)
download | inline diff:
diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js
index 897d270..fc68089 100644
--- a/web/pgadmin/browser/static/js/browser.js
+++ b/web/pgadmin/browser/static/js/browser.js
@@ -1,4 +1,5 @@
define('pgadmin.browser', [
+ 'sources/tree/tree',
'sources/gettext', 'sources/url_for', 'require', 'jquery', 'underscore', 'underscore.string',
'bootstrap', 'sources/pgadmin', 'pgadmin.alertifyjs', 'bundled_codemirror',
'sources/check_node_visibility', 'sources/modify_animation', 'pgadmin.browser.utils', 'wcdocker',
@@ -10,6 +11,7 @@ define('pgadmin.browser', [
'sources/codemirror/addon/fold/pgadmin-sqlfoldcode',
'pgadmin.browser.keyboard',
], function(
+ tree,
gettext, url_for, require, $, _, S, Bootstrap, pgAdmin, Alertify,
codemirror, checkNodeVisibility, modifyAnimation
) {
@@ -86,6 +88,7 @@ define('pgadmin.browser', [
});
b.tree = $('#tree').aciTree('api');
+ b.treeMenu.register($('#tree'));
};
// Extend the browser class attributes
@@ -100,6 +103,7 @@ define('pgadmin.browser', [
editor:null,
// Left hand browser tree
tree:null,
+ treeMenu: new tree.Tree(),
// list of script to be loaded, when a certain type of node is loaded
// It will be used to register extensions, tools, child node scripts,
// etc.
diff --git a/web/pgadmin/static/js/tree/tree.js b/web/pgadmin/static/js/tree/tree.js
new file mode 100644
index 0000000..8048771
--- /dev/null
+++ b/web/pgadmin/static/js/tree/tree.js
@@ -0,0 +1,275 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+
+class BaseTreeNode {
+ constructor(id, data, domNode, parent) {
+ this.id = id;
+ this.data = data;
+ this.setParent(parent);
+ this.children = [];
+ this.domNode = domNode;
+ }
+
+ hasParent() {
+ return isNotNullOrUndefined(this.parentNode);
+ }
+
+ parent() {
+ return this.parentNode;
+ }
+
+ setParent(parent) {
+ this.parentNode = parent;
+ if (isNotNullOrUndefined(parent)) {
+ this.path = this.id;
+ if (isNotNullOrUndefined(parent) && isNotNullOrUndefined(parent.path)) {
+ this.path = parent.path + '.' + this.id;
+ }
+ } else {
+ this.path = null;
+ }
+ }
+
+ getData() {
+ if (this.data === undefined) {
+ return undefined;
+ } else if (this.data === null) {
+ return null;
+ }
+ return Object.assign({}, this.data);
+ }
+
+ getHtmlIdentifier() {
+ return this.domNode;
+ }
+
+ reload(tree) {
+ // Implement it in the actual implementation
+ // This is more of a shell object.
+ throw 'Not Implemented', tree;
+ }
+
+ unload(tree) {
+ this.children.forEach(function(child) {
+ child.unload(tree);
+ child.setParent(null);
+ });
+ this.children = [];
+ }
+
+ anyParent(condition) {
+ let node = this;
+
+ while (node.hasParent()) {
+ node = node.parent();
+ if (condition(node)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ anyFamilyMember(condition) {
+ if(condition(this)) {
+ return true;
+ }
+
+ return this.anyParent(condition);
+ }
+}
+
+
+class BaseTree {
+ constructor() {
+ this.rootNode = this._createNewTreeNode(undefined, {});
+ }
+
+ addNewNode(id, data, domNode, parentPath) {
+ const parent = this.findNode(parentPath);
+ return this.createOrUpdateNode(id, data, parent, domNode);
+ }
+
+ findNode(path) {
+ if (path === null || path === undefined || path.length === 0) {
+ return this.rootNode;
+ }
+ return findInTree(this.rootNode, path.join('.'));
+ }
+
+ findNodeByDomElement(/* domElement */) {
+ // TODO::
+ // Think through the new implementation - How is it going to work with real
+ // domElement?
+ //
+ // NOTE: We already have implementation for the aciTree.
+ throw 'Not Implemented';
+ }
+
+ selected() {
+ throw 'Not Implemented';
+ }
+
+ selectNode(/* node */) {
+ throw 'Not Implemented';
+ }
+
+ register(/* $treeJQuery */) {
+ throw 'Not Implemented';
+ }
+
+ _createNewTreeNode(/* id, data, domNode, parent */) {
+ throw 'Not Implemented';
+ }
+
+ createOrUpdateNode(id, data, parent, domNode) {
+ let oldNodePath = [id];
+ if(parent !== null && parent !== undefined) {
+ oldNodePath = [parent.path, id];
+ }
+ const oldNode = this.findNode(oldNodePath);
+ if (oldNode !== null) {
+ oldNode.data = Object.assign({}, data);
+ return oldNode;
+ }
+
+ const node = this._createNewTreeNode(id, data, domNode, parent);
+ if (parent === this.rootNode) {
+ node.parentNode = null;
+ }
+ if (isNotNullOrUndefined(parent)) {
+ parent.children.push(node);
+ }
+ return node;
+ }
+}
+
+
+function findInTree(rootNode, path) {
+ if (path === null) {
+ return rootNode;
+ }
+ return (function findInNode(currentNode) {
+ for (let i = 0, length = currentNode.children.length; i < length; i++) {
+ const calculatedNode = findInNode(currentNode.children[i]);
+ if (calculatedNode !== null) {
+ return calculatedNode;
+ }
+ }
+
+ if (currentNode.path === path) {
+ return currentNode;
+ } else {
+ return null;
+ }
+ })(rootNode);
+}
+
+
+class ACITreeNode extends BaseTreeNode {
+ constructor(id, data, domNode, parent) {
+ super(id, data, domNode, parent);
+ }
+
+ reload(tree) {
+ this.unload(tree);
+ tree.aciTreeApi.setInode(this.domNode);
+ tree.aciTreeApi.deselect(this.domNode);
+
+ setTimeout(() => {
+ tree.selectNode(this.domNode);
+ }, 0);
+ }
+
+ unload(tree) {
+ // Do not pass on tree object to 'super' object to avoid unloading the
+ // children recursively for performance reason.
+ //
+ // Any way - unloading using 'aciTreeApi.unload(...)' will any way unload
+ // all children.
+ super.unload(null);
+
+ if (isNotNullOrUndefined(tree) && isNotNullOrUndefined(tree.aciTreeApi)) {
+ tree.aciTreeApi.unload(this.domNode);
+ } else {
+ this.domNode = null;
+ }
+ }
+}
+
+
+function isNotNullOrUndefined(val) {
+ return (val !== undefined && val !== null);
+}
+
+
+
+class ACITree extends BaseTree {
+ constructor() {
+ super(arguments);
+ this.aciTreeApi = null;
+ }
+
+ _createNewTreeNode(id, data, domNode, parent) {
+ let node = new ACITreeNode(id, data, domNode, parent);
+ return node;
+ }
+
+ findNodeByDomElement(domElement) {
+ const path = this.translateTreeNodeIdFromACITree(domElement);
+ if(!path || !path[0]) {
+ return undefined;
+ }
+
+ return this.findNode(path);
+ }
+
+ selected() {
+ return this.aciTreeApi.selected();
+ }
+
+ selectNode(aciTreeIdentifier) {
+ this.aciTreeApi.select(aciTreeIdentifier);
+ }
+
+ register($treeJQuery) {
+ $treeJQuery.on('acitree', function (event, api, item, eventName) {
+ if (api.isItem(item)) {
+ if (eventName === 'added') {
+ const id = api.getId(item);
+ const data = api.itemData(item);
+ const parentId = this.translateTreeNodeIdFromACITree(api.parent(item));
+ this.addNewNode(id, data, item, parentId);
+ }
+ }
+ }.bind(this));
+ this.aciTreeApi = $treeJQuery.aciTree('api');
+ }
+
+ translateTreeNodeIdFromACITree(aciTreeNode) {
+ let currentTreeNode = aciTreeNode;
+ let path = [];
+ while (currentTreeNode !== null && currentTreeNode !== undefined && currentTreeNode.length > 0) {
+ path.unshift(this.aciTreeApi.getId(currentTreeNode));
+ if (this.aciTreeApi.hasParent(currentTreeNode)) {
+ currentTreeNode = this.aciTreeApi.parent(currentTreeNode);
+ } else {
+ break;
+ }
+ }
+ return path;
+ }
+
+ static test() {
+ return 'ACITree';
+ }
+}
+
+export { ACITree as Tree, ACITreeNode as TreeNode };
diff --git a/web/regression/javascript/tree/tree_fake.js b/web/regression/javascript/tree/tree_fake.js
new file mode 100644
index 0000000..b285a45
--- /dev/null
+++ b/web/regression/javascript/tree/tree_fake.js
@@ -0,0 +1,69 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {Tree} from '../../../pgadmin/static/js/tree/tree';
+
+export class TreeFake extends Tree {
+ constructor() {
+ super();
+ this.aciTreeToOurTreeTranslator = {};
+ this.aciTreeApi = jasmine.createSpyObj(
+ ['ACITreeApi'], ['setInode', 'unload', 'deselect', 'select']);
+ }
+
+ addNewNode(id, data, domNode, path) {
+ this.aciTreeToOurTreeTranslator[id] = [id];
+ if (path !== null && path !== undefined) {
+ this.aciTreeToOurTreeTranslator[id] = path.concat(id);
+ }
+ return super.addNewNode(id, data, domNode, path);
+ }
+
+ addChild(parent, child) {
+ child.setParent(parent);
+ this.aciTreeToOurTreeTranslator[child.id] = this.aciTreeToOurTreeTranslator[parent.id].concat(child.id);
+ parent.children.push(child);
+ }
+
+ hasParent(aciTreeNode) {
+ return this.translateTreeNodeIdFromACITree(aciTreeNode).length > 1;
+ }
+
+ parent(aciTreeNode) {
+ if (this.hasParent(aciTreeNode)) {
+ let path = this.translateTreeNodeIdFromACITree(aciTreeNode);
+ return [{id: this.findNode(path).parent().id}];
+ }
+
+ return null;
+ }
+
+ translateTreeNodeIdFromACITree(aciTreeNode) {
+ if(aciTreeNode === undefined || aciTreeNode[0] === undefined) {
+ return null;
+ }
+ return this.aciTreeToOurTreeTranslator[aciTreeNode[0].id];
+ }
+
+ itemData(aciTreeNode) {
+ let node = this.findNodeByDomElement(aciTreeNode);
+ if (node === undefined || node === null) {
+ return undefined;
+ }
+ return node.getData();
+ }
+
+ selected() {
+ return this.selectedNode;
+ }
+
+ selectNode(selectedNode) {
+ this.selectedNode = selectedNode;
+ }
+}
diff --git a/web/regression/javascript/tree/tree_spec.js b/web/regression/javascript/tree/tree_spec.js
new file mode 100644
index 0000000..164ceb5
--- /dev/null
+++ b/web/regression/javascript/tree/tree_spec.js
@@ -0,0 +1,421 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+import {Tree, TreeNode} from '../../../pgadmin/static/js/tree/tree';
+import {TreeFake} from './tree_fake';
+
+const context = describe;
+
+const treeTests = (treeClass, setDefaultCallBack) => {
+ let tree;
+ beforeEach(() => {
+ tree = new treeClass();
+ });
+
+ describe('#addNewNode', () => {
+ describe('when add a new root element', () => {
+ context('using [] as the parent', () => {
+ beforeEach(() => {
+ tree.addNewNode('some new node', {data: 'interesting'}, undefined, []);
+ });
+
+ it('can be retrieved', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.data).toEqual({data: 'interesting'});
+ });
+
+ it('return false for #hasParent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.hasParent()).toBe(false);
+ });
+
+ it('return null for #parent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.parent()).toBeNull();
+ });
+ });
+
+ context('using null as the parent', () => {
+ beforeEach(() => {
+ tree.addNewNode('some new node', {data: 'interesting'}, undefined, null);
+ });
+
+ it('can be retrieved', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.data).toEqual({data: 'interesting'});
+ });
+
+ it('return false for #hasParent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.hasParent()).toBe(false);
+ });
+
+ it('return null for #parent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.parent()).toBeNull();
+ });
+ });
+
+ context('using undefined as the parent', () => {
+ beforeEach(() => {
+ tree.addNewNode('some new node', {data: 'interesting'});
+ });
+
+ it('can be retrieved', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.data).toEqual({data: 'interesting'});
+ });
+
+ it('return false for #hasParent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.hasParent()).toBe(false);
+ });
+
+ it('return null for #parent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.parent()).toBeNull();
+ });
+ });
+ });
+
+ describe('when add a new element as a child', () => {
+ let parentNode;
+ beforeEach(() => {
+ parentNode = tree.addNewNode('parent node', {data: 'parent data'}, undefined, []);
+ tree.addNewNode('some new node', {data: 'interesting'}, undefined, ['parent' +
+ ' node']);
+ });
+
+ it('can be retrieved', () => {
+ const node = tree.findNode(['parent node', 'some new node']);
+ expect(node.data).toEqual({data: 'interesting'});
+ });
+
+ it('return true for #hasParent()', () => {
+ const node = tree.findNode(['parent node', 'some new node']);
+ expect(node.hasParent()).toBe(true);
+ });
+
+ it('return "parent node" object for #parent()', () => {
+ const node = tree.findNode(['parent node', 'some new node']);
+ expect(node.parent()).toEqual(parentNode);
+ });
+ });
+
+ describe('when add an element that already exists under a parent', () => {
+ beforeEach(() => {
+ tree.addNewNode('parent node', {data: 'parent data'}, undefined, []);
+ tree.addNewNode('some new node', {data: 'interesting'}, undefined, ['parent' +
+ ' node']);
+ });
+
+ it('does not add a new child', () => {
+ tree.addNewNode('some new node', {data: 'interesting 1'}, undefined, ['parent' +
+ ' node']);
+ const parentNode = tree.findNode(['parent node']);
+ expect(parentNode.children.length).toBe(1);
+ });
+
+ it('updates the existing node data', () => {
+ tree.addNewNode('some new node', {data: 'interesting 1'}, undefined, ['parent' +
+ ' node']);
+ const node = tree.findNode(['parent node', 'some new node']);
+ expect(node.data).toEqual({data: 'interesting 1'});
+ });
+ });
+ });
+
+ describe('#translateTreeNodeIdFromACITree', () => {
+ let aciTreeApi;
+ beforeEach(() => {
+ aciTreeApi = jasmine.createSpyObj('ACITreeApi', [
+ 'hasParent',
+ 'parent',
+ 'getId',
+ ]);
+
+ aciTreeApi.getId.and.callFake((node) => {
+ return node[0].id;
+ });
+ tree.aciTreeApi = aciTreeApi;
+ });
+
+ describe('When tree as a single level', () => {
+ beforeEach(() => {
+ aciTreeApi.hasParent.and.returnValue(false);
+ });
+
+ it('returns an array with the ID of the first level', () => {
+ let node = [{
+ id: 'some id',
+ }];
+ tree.addNewNode('some id', {}, undefined, []);
+
+ expect(tree.translateTreeNodeIdFromACITree(node)).toEqual(['some id']);
+ });
+ });
+
+ describe('When tree as a 2 levels', () => {
+ describe('When we try to retrieve the node in the second level', () => {
+ it('returns an array with the ID of the first level and second level', () => {
+ aciTreeApi.hasParent.and.returnValues(true, false);
+ aciTreeApi.parent.and.returnValue([{
+ id: 'parent id',
+ }]);
+ let node = [{
+ id: 'some id',
+ }];
+
+ tree.addNewNode('parent id', {}, undefined, []);
+ tree.addNewNode('some id', {}, undefined, ['parent id']);
+
+ expect(tree.translateTreeNodeIdFromACITree(node))
+ .toEqual(['parent id', 'some id']);
+ });
+ });
+ });
+ });
+
+ describe('#selected', () => {
+ context('a node in the tree is selected', () => {
+ it('returns that node object', () => {
+ let selectedNode = new TreeNode('bamm', {}, []);
+ setDefaultCallBack(tree, selectedNode);
+ expect(tree.selected()).toEqual(selectedNode);
+ });
+ });
+ });
+
+ describe('#findNodeByTreeElement', () => {
+ context('retrieve data from node not found', () => {
+ it('return undefined', () => {
+ let aciTreeApi = jasmine.createSpyObj('ACITreeApi', [
+ 'hasParent',
+ 'parent',
+ 'getId',
+ ]);
+
+ aciTreeApi.getId.and.callFake((node) => {
+ return node[0].id;
+ });
+ tree.aciTreeApi = aciTreeApi;
+ expect(tree.findNodeByDomElement(['<li>something</li>'])).toBeUndefined();
+ });
+ });
+ });
+};
+
+describe('tree tests', () => {
+ describe('TreeNode', () => {
+ describe('#hasParent', () => {
+ context('parent is null', () => {
+ it('returns false', () => {
+ let treeNode = new TreeNode('123', {}, [], null);
+ expect(treeNode.hasParent()).toBe(false);
+ });
+ });
+ context('parent is undefined', () => {
+ it('returns false', () => {
+ let treeNode = new TreeNode('123', {}, [], undefined);
+ expect(treeNode.hasParent()).toBe(false);
+ });
+ });
+ context('parent exists', () => {
+ it('returns true', () => {
+ let parentNode = new TreeNode('456', {}, []);
+ let treeNode = new TreeNode('123', {}, [], parentNode);
+ expect(treeNode.hasParent()).toBe(true);
+ });
+ });
+ });
+
+ describe('#reload', () => {
+ let tree;
+ let level2;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, [{id: 'level1'}], []);
+ level2 = tree.addNewNode('level2', {data: 'data'}, [{id: 'level2'}], ['level1']);
+ tree.addNewNode('level3', {data: 'more data'}, [{id: 'level3'}], ['level1', 'level2']);
+
+ tree.aciTreeApi = jasmine.createSpyObj(
+ 'ACITreeApi', ['setInode', 'unload', 'deselect', 'select']);
+ });
+
+ it('reloads the node and its children', () => {
+ level2.reload(tree);
+ expect(tree.findNodeByDomElement([{id: 'level2'}])).toEqual(level2);
+ });
+
+ it('does not reload the children of node', () => {
+ level2.reload(tree);
+ expect(tree.findNodeByDomElement([{id: 'level3'}])).toBeNull();
+ });
+
+ it('select the node', (done) => {
+ level2.reload(tree);
+ setTimeout(() => {
+ expect(tree.selected()).toEqual([{id: 'level2'}]);
+ done();
+ }, 20);
+ });
+
+ describe('ACITree specific', () => {
+ it('sets the current node as a Inode, changing the Icon back to +', () => {
+ level2.reload(tree);
+ expect(tree.aciTreeApi.setInode).toHaveBeenCalledWith([{id: 'level2'}]);
+ });
+
+ it('deselect the node and selects it again to trigger ACI tree' +
+ ' events', (done) => {
+ level2.reload(tree);
+ setTimeout(() => {
+ expect(tree.aciTreeApi.deselect).toHaveBeenCalledWith([{id: 'level2'}]);
+ done();
+ }, 20);
+ });
+ });
+ });
+
+ describe('#unload', () => {
+ let tree;
+ let level2;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, ['<li>level1</li>'], []);
+ level2 = tree.addNewNode('level2', {data: 'data'}, ['<li>level2</li>'], ['level1']);
+ tree.addNewNode('level3', {data: 'more data'}, ['<li>level3</li>'], ['level1', 'level2']);
+ tree.aciTreeApi = jasmine.createSpyObj('ACITreeApi', ['unload']);
+ });
+
+ it('unloads the children of the current node', () => {
+ level2.unload(tree);
+ expect(tree.findNodeByDomElement([{id: 'level2'}])).toEqual(level2);
+ expect(tree.findNodeByDomElement([{id: 'level3'}])).toBeNull();
+ });
+
+ it('calls unload on the ACI Tree', () => {
+ level2.unload(tree);
+ expect(tree.aciTreeApi.unload).toHaveBeenCalledWith(['<li>level2</li>']);
+ });
+ });
+ });
+
+ describe('Tree', () => {
+ function realTreeSelectNode(tree, selectedNode) {
+ let aciTreeApi = jasmine.createSpyObj('ACITreeApi', [
+ 'selected',
+ ]);
+ tree.aciTreeApi = aciTreeApi;
+ aciTreeApi.selected.and.returnValue(selectedNode);
+ }
+
+ treeTests(Tree, realTreeSelectNode);
+ });
+
+ describe('TreeFake', () => {
+ function fakeTreeSelectNode(tree, selectedNode) {
+ tree.selectNode(selectedNode);
+ }
+
+ treeTests(TreeFake, fakeTreeSelectNode);
+
+ describe('#hasParent', () => {
+ context('tree contains multiple levels', () => {
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, undefined, []);
+ tree.addNewNode('level2', {data: 'interesting'}, undefined, ['level1']);
+ });
+
+ context('node is at the first level', () => {
+ it('returns false', () => {
+ expect(tree.hasParent([{id: 'level1'}])).toBe(false);
+ });
+ });
+
+ context('node is at the second level', () => {
+ it('returns true', () => {
+ expect(tree.hasParent([{id: 'level2'}])).toBe(true);
+ });
+ });
+ });
+ });
+
+ describe('#parent', () => {
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, undefined, []);
+ tree.addNewNode('level2', {data: 'interesting'}, undefined, ['level1']);
+ });
+
+ context('node is the root', () => {
+ it('returns null', () => {
+ expect(tree.parent([{id: 'level1'}])).toBeNull();
+ });
+ });
+
+ context('node is not root', () => {
+ it('returns root element', () => {
+ expect(tree.parent([{id: 'level2'}])).toEqual([{id: 'level1'}]);
+ });
+ });
+ });
+
+ describe('#itemData', () => {
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, undefined, []);
+ tree.addNewNode('level2', {data: 'expected data'}, undefined, ['level1']);
+ });
+
+ context('retrieve data from the node', () => {
+ it('return the node data', () => {
+ expect(tree.itemData([{id: 'level2'}])).toEqual({
+ data: 'expected' +
+ ' data',
+ });
+ });
+ });
+
+ context('retrieve data from node not found', () => {
+ it('return undefined', () => {
+ expect(tree.itemData([{id: 'bamm'}])).toBeUndefined();
+ });
+ });
+ });
+
+ describe('#addChild', () => {
+ let root, child;
+ beforeEach(() => {
+ let tree = new TreeFake();
+ root = tree.addNewNode('root', {}, [{id: 'root'}]);
+ child = new TreeNode('node.1', {}, [{id: 'node.1'}]);
+ tree.addChild(root, child);
+ });
+
+ it('adds a new child to a node', () => {
+ expect(root.children).toEqual([child]);
+ });
+
+ it('changes the parent of the child node', () => {
+ expect(root.children[0].parentNode).toEqual(root);
+ expect(child.parentNode).toEqual(root);
+ });
+
+ it('changes the path of the child', () => {
+ expect(child.path).toEqual('root.node.1');
+ });
+ });
+ });
+});
+
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-30 15:28 Anthony Emengo <[email protected]>
parent: Ashesh Vashi <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Anthony Emengo @ 2018-04-30 15:28 UTC (permalink / raw)
To: Ashesh Vashi <[email protected]>; +Cc: Joao De Almeida Pereira <[email protected]>; Dave Page <[email protected]>; Khushboo Vashi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
>
> I was expecting a separate layer between the tree implementation, and
> aciTree adaptor.
> Please find the patch for the example.
> It will separate the two layers, and easy to replace with the new
> implementation in future.
In general, we want defer the separation of the layers for now. Even though
we might assume that this is the direction we want to go in. It's simply
too early to be making such an architectural leap. For right now, we just
know that we need the decoupling, but don't know what if we'd need the 2
layers *as implemented*. The principle we're adhering to here is the Last
Responsible Moment principle, which states that you only make the changes
that you feel is necessary for the given problems you're facing:
https://blog.codinghorror.com/the-last-responsible-moment/
I would not like to see that changes in this patch.
> I would like us to come up with the actual design about the hot
> pluggability before going in this direction.
In our point of view these 2 changes are not related. One thing is the
internal code organization of the application, other thing is allowing
third party to drop code in the application and it just work. These 2
should be talked separately, but the hot pluggability is not something that
will be address by this work we are doing right now.
Anthony && Joao
On Mon, Apr 30, 2018 at 11:03 AM, Ashesh Vashi <
[email protected]> wrote:
>
>
>
> On Mon, Apr 30, 2018 at 8:30 PM, Ashesh Vashi <
> [email protected]> wrote:
>
>> On Sat, Apr 28, 2018 at 3:55 AM, Joao De Almeida Pereira <
>> [email protected]> wrote:
>>
>>> Hi Hackers,
>>> As you are aware we kept on working on the patch, so we are attaching to
>>> this email a new version of the patch.
>>> This new version contains all the changes in the previous one plus more
>>> extractions of functions and refactoring of code.
>>>
>>> The objective of this patch is to create a separation between pgAdmin
>>> and the ACI Tree. We are doing this because we realized that at this point
>>> in time we have the ACI Tree all over the code of pgAdmin. I found a very
>>> interesting article that really talks about this:
>>> https://medium.freecodecamp.org/code-dependencies-are-the-de
>>> vil-35ed28b556d
>>>
>>> In this patch there are some visions and ideas about the location of the
>>> code, the way to organize it and also try to pave the future for a
>>> application that is stable, easy to develop on and that can be release at a
>>> times notice.
>>>
>>> We are investing a big chunk of our time in doing this refactoring, but
>>> while doing that we also try to respond to the patches sent to the mailing
>>> list. We would like the feedback from the community because we believe this
>>> is a changing point for the application. The idea is to change the way we
>>> develop this application, instead of only correcting a bug of developing a
>>> feature, with every commit we should correct the bug or develop a feature
>>> but leave the code a little better than we found it (Refactoring,
>>> refactoring, refactoring). This is hard work but that is what the users
>>> from pgAdmin expect from this community of developers.
>>>
>>>
>>> ======
>>>
>>>
>>>
>>> It is a huge patch
>>> 86 files changed, 5492 inserts, 1840 deletions
>>> and we would like to get your feedback as soon as possible, because we
>>> are continuing to work on it which means it is going to grow in size.
>>>
>>>
>>> At this point in time we still have 124 of 176 calls to the function
>>> itemData from ACITree.
>>>
>>> What does each patch contain:
>>> 0001:
>>> Very simple patch, we found out that the linter was not looking into
>>> all the javascript test files, so this patch will ensure it is
>>>
>> Committed the patch along with the regression introduced because of this
>> patch.
>>
>>>
>>> 0002:
>>> New Tree abstraction. This patch contains the new Tree that works as
>>> an adaptor for ACI Tree and is going to be used on all the extractions that
>>> we are doing.
>>>
>>
>> I was expecting a separate layer between the tree implementation, and
>> aciTree adaptor.
>> Please find the patch for the example.
>>
>> It will separate the two layers, and easy to replace with the new
>> implemenation in future.
>>
>
> Oops forgot to attach the patch.
> Please find the patch attached.
>
> -- Thanks, Ashesh
>
>>
>>> 0003:
>>> Code that extracts, wrap with tests and replace ACI Tree invocations.
>>>
>> There are many small cases left in the patches.
>> Hence - I would like to know the TODO list created by you.
>>
>> e.g. When we remove any of the object from the database server, we're not
>> yet removing the respective node from the new implementation, and its
>> children.
>>
>>>
>>> We start creating new pattern for the location of Javascript files and
>>> their structure.
>>>
>> I would not like to see that changes in this patch.
>> I would like us to come up with the actual design about the hot
>> pluggability before going in this direction.
>>
>>> Create patterns for creation of dialogs (backup and restore)
>>>
>> It's better - we don't change the directory structure at the moment.
>>
>> I am not against dividing the big javascript files in small chunks, but -
>> I would like us to discuss first about the hot plugins design first.
>>
>> -- Thanks, Ashesh
>>
>>>
>>>
>>
>>>
>>> Thanks
>>> Joao
>>>
>>>
>>> On Fri, Apr 27, 2018 at 5:34 AM Ashesh Vashi <
>>> [email protected]> wrote:
>>>
>>>> I have quite a few comments for the patch.
>>>> I will send them soon.
>>>>
>>>> On Fri, Apr 27, 2018, 14:45 Dave Page <[email protected]> wrote:
>>>>
>>>>> How is your work on this going Ashesh? Will you be committing today?
>>>>>
>>>>> On Wed, Apr 25, 2018 at 8:52 AM, Dave Page <[email protected]> wrote:
>>>>>
>>>>>> Ashesh; you had agreed to work on this early this week. Please ensure
>>>>>> you do so today.
>>>>>>
>>>>>> Thanks.
>>>>>>
>>>>>> On Tue, Apr 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hi Hackers,
>>>>>>>
>>>>>>> Can someone review and merge this patch?
>>>>>>>
>>>>>>> Thanks
>>>>>>> Joao
>>>>>>>
>>>>>>> On Wed, Apr 18, 2018 at 10:23 AM Joao De Almeida Pereira <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hi Hackers,
>>>>>>>> Any other comment about this patch?
>>>>>>>>
>>>>>>>> Thanks
>>>>>>>> Joao
>>>>>>>>
>>>>>>>> On Tue, Apr 10, 2018 at 12:00 PM Joao De Almeida Pereira <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Hello Khushboo
>>>>>>>>>
>>>>>>>>> On Mon, Apr 9, 2018 at 1:59 AM Khushboo Vashi <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Hi Joao,
>>>>>>>>>>
>>>>>>>>>> I have reviewed your patch and have some suggestions.
>>>>>>>>>>
>>>>>>>>>> On Sat, Apr 7, 2018 at 12:43 AM, Joao De Almeida Pereira <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> Hello Murtuza/Dave,
>>>>>>>>>>> Yes now the extracted functions are spread into different files.
>>>>>>>>>>> The intent would be to make the files as small as possible, and also to
>>>>>>>>>>> group and name them in a way that would be easy to understand what each
>>>>>>>>>>> file is doing without the need of opening it.
>>>>>>>>>>> As a example:
>>>>>>>>>>> static/js/backup will contain all the backup related
>>>>>>>>>>> functionality inside of this folder we can see the file:
>>>>>>>>>>>
>>>>>>>>>> menu_utils.js At this moment in time we decided to group all the
>>>>>>>>>>> functions that are related to the menu, but we can split that also if we
>>>>>>>>>>> believe it is easier to see.
>>>>>>>>>>>
>>>>>>>>>> It's really very good to see the separated code for backup
>>>>>>>>>> module. As we have done for backup, we would like do it for other PG
>>>>>>>>>> utilities like restore, maintenance etc.
>>>>>>>>>> Considering this, we should separate the code in a way that some
>>>>>>>>>> of the common functionalities can be used for other modules like menu (as
>>>>>>>>>> you have mentioned above), dialogue factory etc.
>>>>>>>>>> Also, I think these functionalities should be in their respective
>>>>>>>>>> static folder instead of pgadmin/static.
>>>>>>>>>>
>>>>>>>>>
>>>>>>>>> About the location of the files. The move of the files to
>>>>>>>>> pgadmin/static/js was made on purpose in order to clearly separate
>>>>>>>>> Javascript from python code.
>>>>>>>>> The rational behind it was
>>>>>>>>> - Create a clear separation between the backend and frontend
>>>>>>>>> - Having Javascript code concentrated in a single place,
>>>>>>>>> hopefully, will encourage to developers to look for a functionality, that
>>>>>>>>> is already implemented in another modules, because they are right there.
>>>>>>>>> (When we started this journey we realized that the 'nodes' have a big
>>>>>>>>> groups of code that could be shared, but because the Javascript is spread
>>>>>>>>> everywhere it is much harder to look for it)
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> There are some drawbacks of this separation:
>>>>>>>>> - When creating a new module we will need to put the javascript in
>>>>>>>>> a separate location from the backend code
>>>>>>>>>
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>> static/js/datagrid folder contains all the datagrid related
>>>>>>>>>>> functionality
>>>>>>>>>>>
>>>>>>>>>> Same as backup module, this should be in it's respective
>>>>>>>>>> static/js folder.
>>>>>>>>>>
>>>>>>>>>>> Inside of the folder we can see the files:
>>>>>>>>>>> get_panel_title.js is responsible for retrieving the name of
>>>>>>>>>>> the panel
>>>>>>>>>>> show_data.js is responsible for showing the datagrid
>>>>>>>>>>> show_query_tool.js is responsible for showing the query tool
>>>>>>>>>>>
>>>>>>>>>>> Does this structure make sense?
>>>>>>>>>>> Can you give an example of a comment that you think is missing
>>>>>>>>>>> and that could help?
>>>>>>>>>>>
>>>>>>>>>>> As a personal note, unless the algorithm is very obscure or very
>>>>>>>>>>> complicated, I believe that if the code needs comments it is a signal that
>>>>>>>>>>> something needs to change in terms of naming, structure of the part in
>>>>>>>>>>> question. This being said, I am open to add some comments that might help
>>>>>>>>>>> people.
>>>>>>>>>>>
>>>>>>>>>> You are right, with the help of naming convention and structure
>>>>>>>>>> of the code, any one can get the idea about the code. But it is very useful
>>>>>>>>>> to understand the code
>>>>>>>>>> very easily with the proper comments especially when there are
>>>>>>>>>> multiple developers working on a single project.
>>>>>>>>>>
>>>>>>>>>> I found some of the places where it would be great to have
>>>>>>>>>> comments.
>>>>>>>>>>
>>>>>>>>>> - treeMenu: new tree.Tree() in a browser.js
>>>>>>>>>> - tree.js (especially Tree class)
>>>>>>>>>>
>>>>>>>>> About the comment point I need a more clear understanding on what
>>>>>>>>> kind of comments you are looking for. Because when you read the function
>>>>>>>>> names you understand the intent, what they are doing. The parameters also
>>>>>>>>> explain what you need to pass into them.
>>>>>>>>>
>>>>>>>>> If what you are looking for in these comments is the reasoning
>>>>>>>>> being the change itself, then that should be present in the commit message.
>>>>>>>>> Specially because this is going to be a very big patch with a very big
>>>>>>>>> number of changes.
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> Thanks
>>>>>>>>>>> Joao
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> Thanks,
>>>>>>>>>> Khushboo
>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> On Fri, Apr 6, 2018 at 4:48 AM Murtuza Zabuawala <
>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>
>>>>>>>>>>>> Patch looks good and working as expected.
>>>>>>>>>>>>
>>>>>>>>>>>> I also agree with Dave, Can we please add some comments in each
>>>>>>>>>>>> file which can help us to understand the flow, I'm saying because now the
>>>>>>>>>>>> code is segregated in so many separate files it will be hard to keep track
>>>>>>>>>>>> of the flow from one file to another when debugging.
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> --
>>>>>>>>>>>> Regards,
>>>>>>>>>>>> Murtuza Zabuawala
>>>>>>>>>>>> EnterpriseDB: http://www.enterprisedb.com
>>>>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> On Thu, Apr 5, 2018 at 7:08 PM, Joao De Almeida Pereira <
>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>
>>>>>>>>>>>>> Hi Khushboo,
>>>>>>>>>>>>> Attached you can find both patches rebased
>>>>>>>>>>>>>
>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>
>>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Can you please rebase the second patch?
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida Pereira <
>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Attached you can find the patch that will start to decouple
>>>>>>>>>>>>>>> pgAdmin from ACITree library.
>>>>>>>>>>>>>>> This patch is intended to be merged after 3.0, because we do
>>>>>>>>>>>>>>> not want to cause any entropy or delay the release, but we want to start
>>>>>>>>>>>>>>> the discussion and show some code.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> This job that we started is a massive tech debt chore that
>>>>>>>>>>>>>>> will take some time to finalize and we would love the help of the community
>>>>>>>>>>>>>>> to do it.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> *Summary of the patch:*
>>>>>>>>>>>>>>> 0001 patch:
>>>>>>>>>>>>>>> - Creates a new tree that will allow us to create a
>>>>>>>>>>>>>>> separation between the application and ACI Tree
>>>>>>>>>>>>>>> - Creates a Fake Tree (Test double, for reference on the
>>>>>>>>>>>>>>> available test doubles: https://martinfowler.
>>>>>>>>>>>>>>> com/bliki/TestDouble.html) that can be used to inplace to
>>>>>>>>>>>>>>> replace the ACITree and also encapsulate the new tree behavior on our tests
>>>>>>>>>>>>>>> - Adds tests for all the tree functionalities
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> 0002 patch:
>>>>>>>>>>>>>>> - Extracts, refactors, adds tests and remove dependency
>>>>>>>>>>>>>>> from ACI Tree on:
>>>>>>>>>>>>>>> - getTreeNodeHierarchy
>>>>>>>>>>>>>>> - on backup.js: menu_enabled, menu_enabled_server,
>>>>>>>>>>>>>>> start_backup_global_server, backup_objects
>>>>>>>>>>>>>>> - on datagrid.js: show_data_grid, get_panel_title,
>>>>>>>>>>>>>>> show_query_tool
>>>>>>>>>>>>>>> - Start using sprintf-js as Underscore.String is deprecating
>>>>>>>>>>>>>>> sprintf function
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> This patch represents only 10 calls to ACITree.itemData out
>>>>>>>>>>>>>>> of 176 that are spread around our code
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> *In Depth look on the process behind the patch:*
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> We started writing this patch with the idea that we need to
>>>>>>>>>>>>>>> decouple pgAdmin4 from ACITree, because ACITree is no longer supported, the
>>>>>>>>>>>>>>> documentation is non existent and ACITree is no longer being actively
>>>>>>>>>>>>>>> developed.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Our process:
>>>>>>>>>>>>>>> 1. We "randomly" selected a function that is part of the
>>>>>>>>>>>>>>> ACITree. From this point we decided to replace that function with our own
>>>>>>>>>>>>>>> version. The function that we choose was "itemData".
>>>>>>>>>>>>>>> The function gives us all the "data" that a specific node of
>>>>>>>>>>>>>>> the tree find.
>>>>>>>>>>>>>>> Given in order to replace the tree we would need to have a
>>>>>>>>>>>>>>> function that would give us the same information. We had 2 options:
>>>>>>>>>>>>>>> a) Create a tree with a function called itemData
>>>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>>>> - At first view this was the simpler solution
>>>>>>>>>>>>>>> - Would keep the status quo
>>>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>>>> - Not a OOP approach
>>>>>>>>>>>>>>> - Not very flexible
>>>>>>>>>>>>>>> b) Create a tree that would return a node given an ID and
>>>>>>>>>>>>>>> then the node would be responsible for giving it's data.
>>>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>>>> - OOP Approach
>>>>>>>>>>>>>>> - More flexible and we do not need to bring the tree
>>>>>>>>>>>>>>> around, just a node
>>>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>>>> - Break the current status quo
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Given these 2 options we decided to go for a more OOP
>>>>>>>>>>>>>>> approach creating a Tree and a TreeNode classes, that in the future will be
>>>>>>>>>>>>>>> renamed to ACITreeWrapper and TreeNode.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> 2. After we decided on the starting point we searched for
>>>>>>>>>>>>>>> occurrences of the function "itemData" and we found out that there were 303
>>>>>>>>>>>>>>> occurrences of "itemData" in the code and roughly 176 calls to the function
>>>>>>>>>>>>>>> itself (some of the hits were variable names).
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> 3. We selected the first file on the search and found the
>>>>>>>>>>>>>>> function that was responsible for calling the itemData function.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> 4. Extracted the function to a separate file
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> 5. Wrap this function with tests
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> 6. Refactor the function to ES6, give more declarative names
>>>>>>>>>>>>>>> to variables and break the functions into smaller chunks
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> 7. When all the tests were passing we replaced ACITree with
>>>>>>>>>>>>>>> our Tree
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> 8. We ensured that all tests were passing
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> 9. Remove function from the original file and use the new
>>>>>>>>>>>>>>> function
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> 10. Ensure everything still works
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> 11. Find the next function and execute from step 4 until all
>>>>>>>>>>>>>>> the functions are replaced, refactored and tested.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> As you can see by the process this is a pretty huge
>>>>>>>>>>>>>>> undertake, because of the number of calls to the function. This is just the
>>>>>>>>>>>>>>> first step on the direction of completely isolating the ACITree so that we
>>>>>>>>>>>>>>> can solve the problem with a large number of elements on the tree.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> *What is on our radar that we need to address:*
>>>>>>>>>>>>>>> - Finish the complete decoupling of the ACITree
>>>>>>>>>>>>>>> - Performance of the current tree implementation
>>>>>>>>>>>>>>> - Tweak the naming of the Tree class to explicitly tell us
>>>>>>>>>>>>>>> this is to use only with ACITree.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>
>>>>>>
>>>>>> --
>>>>>> Dave Page
>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>> Twitter: @pgsnake
>>>>>>
>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>> The Enterprise PostgreSQL Company
>>>>>>
>>>>>
>>>>>
>>>>>
>>>>> --
>>>>> Dave Page
>>>>> Blog: http://pgsnake.blogspot.com
>>>>> Twitter: @pgsnake
>>>>>
>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>> The Enterprise PostgreSQL Company
>>>>>
>>>>
>>
>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-30 15:33 Ashesh Vashi <[email protected]>
parent: Anthony Emengo <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Ashesh Vashi @ 2018-04-30 15:33 UTC (permalink / raw)
To: Anthony Emengo <[email protected]>; +Cc: Joao De Almeida Pereira <[email protected]>; Dave Page <[email protected]>; Khushboo Vashi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
On Mon, Apr 30, 2018 at 8:58 PM, Anthony Emengo <[email protected]> wrote:
> I was expecting a separate layer between the tree implementation, and
>> aciTree adaptor.
>> Please find the patch for the example.
>> It will separate the two layers, and easy to replace with the new
>> implementation in future.
>
>
> In general, we want defer the separation of the layers for now. Even
> though we might assume that this is the direction we want to go in. It's
> simply too early to be making such an architectural leap. For right now, we
> just know that we need the decoupling, but don't know what if we'd need the
> 2 layers *as implemented*. The principle we're adhering to here is the
> Last Responsible Moment principle, which states that you only make the
> changes that you feel is necessary for the given problems you're facing:
> https://blog.codinghorror.com/the-last-responsible-moment/
>
> I would not like to see that changes in this patch.
>> I would like us to come up with the actual design about the hot
>> pluggability before going in this direction.
>
>
> In our point of view these 2 changes are not related. One thing is the
> internal code organization of the application, other thing is allowing
> third party to drop code in the application and it just work. These 2
> should be talked separately, but the hot pluggability is not something that
> will be address by this work we are doing right now.
>
Neither - it should be part of this change.
It should be addressed separately, and discussed.
-- Thanks, Ashesh
>
> Anthony && Joao
>
> On Mon, Apr 30, 2018 at 11:03 AM, Ashesh Vashi <
> [email protected]> wrote:
>
>>
>>
>>
>> On Mon, Apr 30, 2018 at 8:30 PM, Ashesh Vashi <
>> [email protected]> wrote:
>>
>>> On Sat, Apr 28, 2018 at 3:55 AM, Joao De Almeida Pereira <
>>> [email protected]> wrote:
>>>
>>>> Hi Hackers,
>>>> As you are aware we kept on working on the patch, so we are attaching
>>>> to this email a new version of the patch.
>>>> This new version contains all the changes in the previous one plus more
>>>> extractions of functions and refactoring of code.
>>>>
>>>> The objective of this patch is to create a separation between pgAdmin
>>>> and the ACI Tree. We are doing this because we realized that at this point
>>>> in time we have the ACI Tree all over the code of pgAdmin. I found a very
>>>> interesting article that really talks about this:
>>>> https://medium.freecodecamp.org/code-dependencies-are-the-de
>>>> vil-35ed28b556d
>>>>
>>>> In this patch there are some visions and ideas about the location of
>>>> the code, the way to organize it and also try to pave the future for a
>>>> application that is stable, easy to develop on and that can be release at a
>>>> times notice.
>>>>
>>>> We are investing a big chunk of our time in doing this refactoring, but
>>>> while doing that we also try to respond to the patches sent to the mailing
>>>> list. We would like the feedback from the community because we believe this
>>>> is a changing point for the application. The idea is to change the way we
>>>> develop this application, instead of only correcting a bug of developing a
>>>> feature, with every commit we should correct the bug or develop a feature
>>>> but leave the code a little better than we found it (Refactoring,
>>>> refactoring, refactoring). This is hard work but that is what the users
>>>> from pgAdmin expect from this community of developers.
>>>>
>>>>
>>>> ======
>>>>
>>>>
>>>>
>>>> It is a huge patch
>>>> 86 files changed, 5492 inserts, 1840 deletions
>>>> and we would like to get your feedback as soon as possible, because we
>>>> are continuing to work on it which means it is going to grow in size.
>>>>
>>>>
>>>> At this point in time we still have 124 of 176 calls to the function
>>>> itemData from ACITree.
>>>>
>>>> What does each patch contain:
>>>> 0001:
>>>> Very simple patch, we found out that the linter was not looking into
>>>> all the javascript test files, so this patch will ensure it is
>>>>
>>> Committed the patch along with the regression introduced because of this
>>> patch.
>>>
>>>>
>>>> 0002:
>>>> New Tree abstraction. This patch contains the new Tree that works as
>>>> an adaptor for ACI Tree and is going to be used on all the extractions that
>>>> we are doing.
>>>>
>>>
>>> I was expecting a separate layer between the tree implementation, and
>>> aciTree adaptor.
>>> Please find the patch for the example.
>>>
>>> It will separate the two layers, and easy to replace with the new
>>> implemenation in future.
>>>
>>
>> Oops forgot to attach the patch.
>> Please find the patch attached.
>>
>> -- Thanks, Ashesh
>>
>>>
>>>> 0003:
>>>> Code that extracts, wrap with tests and replace ACI Tree invocations.
>>>>
>>> There are many small cases left in the patches.
>>> Hence - I would like to know the TODO list created by you.
>>>
>>> e.g. When we remove any of the object from the database server, we're
>>> not yet removing the respective node from the new implementation, and its
>>> children.
>>>
>>>>
>>>> We start creating new pattern for the location of Javascript files
>>>> and their structure.
>>>>
>>> I would not like to see that changes in this patch.
>>> I would like us to come up with the actual design about the hot
>>> pluggability before going in this direction.
>>>
>>>> Create patterns for creation of dialogs (backup and restore)
>>>>
>>> It's better - we don't change the directory structure at the moment.
>>>
>>> I am not against dividing the big javascript files in small chunks, but
>>> - I would like us to discuss first about the hot plugins design first.
>>>
>>> -- Thanks, Ashesh
>>>
>>>>
>>>>
>>>
>>>>
>>>> Thanks
>>>> Joao
>>>>
>>>>
>>>> On Fri, Apr 27, 2018 at 5:34 AM Ashesh Vashi <
>>>> [email protected]> wrote:
>>>>
>>>>> I have quite a few comments for the patch.
>>>>> I will send them soon.
>>>>>
>>>>> On Fri, Apr 27, 2018, 14:45 Dave Page <[email protected]> wrote:
>>>>>
>>>>>> How is your work on this going Ashesh? Will you be committing today?
>>>>>>
>>>>>> On Wed, Apr 25, 2018 at 8:52 AM, Dave Page <[email protected]> wrote:
>>>>>>
>>>>>>> Ashesh; you had agreed to work on this early this week. Please
>>>>>>> ensure you do so today.
>>>>>>>
>>>>>>> Thanks.
>>>>>>>
>>>>>>> On Tue, Apr 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hi Hackers,
>>>>>>>>
>>>>>>>> Can someone review and merge this patch?
>>>>>>>>
>>>>>>>> Thanks
>>>>>>>> Joao
>>>>>>>>
>>>>>>>> On Wed, Apr 18, 2018 at 10:23 AM Joao De Almeida Pereira <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Hi Hackers,
>>>>>>>>> Any other comment about this patch?
>>>>>>>>>
>>>>>>>>> Thanks
>>>>>>>>> Joao
>>>>>>>>>
>>>>>>>>> On Tue, Apr 10, 2018 at 12:00 PM Joao De Almeida Pereira <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Hello Khushboo
>>>>>>>>>>
>>>>>>>>>> On Mon, Apr 9, 2018 at 1:59 AM Khushboo Vashi <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>
>>>>>>>>>>> I have reviewed your patch and have some suggestions.
>>>>>>>>>>>
>>>>>>>>>>> On Sat, Apr 7, 2018 at 12:43 AM, Joao De Almeida Pereira <
>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> Hello Murtuza/Dave,
>>>>>>>>>>>> Yes now the extracted functions are spread into different
>>>>>>>>>>>> files. The intent would be to make the files as small as possible, and also
>>>>>>>>>>>> to group and name them in a way that would be easy to understand what each
>>>>>>>>>>>> file is doing without the need of opening it.
>>>>>>>>>>>> As a example:
>>>>>>>>>>>> static/js/backup will contain all the backup related
>>>>>>>>>>>> functionality inside of this folder we can see the file:
>>>>>>>>>>>>
>>>>>>>>>>> menu_utils.js At this moment in time we decided to group all
>>>>>>>>>>>> the functions that are related to the menu, but we can split that also if
>>>>>>>>>>>> we believe it is easier to see.
>>>>>>>>>>>>
>>>>>>>>>>> It's really very good to see the separated code for backup
>>>>>>>>>>> module. As we have done for backup, we would like do it for other PG
>>>>>>>>>>> utilities like restore, maintenance etc.
>>>>>>>>>>> Considering this, we should separate the code in a way that some
>>>>>>>>>>> of the common functionalities can be used for other modules like menu (as
>>>>>>>>>>> you have mentioned above), dialogue factory etc.
>>>>>>>>>>> Also, I think these functionalities should be in their
>>>>>>>>>>> respective static folder instead of pgadmin/static.
>>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> About the location of the files. The move of the files to
>>>>>>>>>> pgadmin/static/js was made on purpose in order to clearly separate
>>>>>>>>>> Javascript from python code.
>>>>>>>>>> The rational behind it was
>>>>>>>>>> - Create a clear separation between the backend and frontend
>>>>>>>>>> - Having Javascript code concentrated in a single place,
>>>>>>>>>> hopefully, will encourage to developers to look for a functionality, that
>>>>>>>>>> is already implemented in another modules, because they are right there.
>>>>>>>>>> (When we started this journey we realized that the 'nodes' have a big
>>>>>>>>>> groups of code that could be shared, but because the Javascript is spread
>>>>>>>>>> everywhere it is much harder to look for it)
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> There are some drawbacks of this separation:
>>>>>>>>>> - When creating a new module we will need to put the javascript
>>>>>>>>>> in a separate location from the backend code
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>> static/js/datagrid folder contains all the datagrid related
>>>>>>>>>>>> functionality
>>>>>>>>>>>>
>>>>>>>>>>> Same as backup module, this should be in it's respective
>>>>>>>>>>> static/js folder.
>>>>>>>>>>>
>>>>>>>>>>>> Inside of the folder we can see the files:
>>>>>>>>>>>> get_panel_title.js is responsible for retrieving the name of
>>>>>>>>>>>> the panel
>>>>>>>>>>>> show_data.js is responsible for showing the datagrid
>>>>>>>>>>>> show_query_tool.js is responsible for showing the query tool
>>>>>>>>>>>>
>>>>>>>>>>>> Does this structure make sense?
>>>>>>>>>>>> Can you give an example of a comment that you think is missing
>>>>>>>>>>>> and that could help?
>>>>>>>>>>>>
>>>>>>>>>>>> As a personal note, unless the algorithm is very obscure or
>>>>>>>>>>>> very complicated, I believe that if the code needs comments it is a signal
>>>>>>>>>>>> that something needs to change in terms of naming, structure of the part in
>>>>>>>>>>>> question. This being said, I am open to add some comments that might help
>>>>>>>>>>>> people.
>>>>>>>>>>>>
>>>>>>>>>>> You are right, with the help of naming convention and structure
>>>>>>>>>>> of the code, any one can get the idea about the code. But it is very useful
>>>>>>>>>>> to understand the code
>>>>>>>>>>> very easily with the proper comments especially when there are
>>>>>>>>>>> multiple developers working on a single project.
>>>>>>>>>>>
>>>>>>>>>>> I found some of the places where it would be great to have
>>>>>>>>>>> comments.
>>>>>>>>>>>
>>>>>>>>>>> - treeMenu: new tree.Tree() in a browser.js
>>>>>>>>>>> - tree.js (especially Tree class)
>>>>>>>>>>>
>>>>>>>>>> About the comment point I need a more clear understanding on what
>>>>>>>>>> kind of comments you are looking for. Because when you read the function
>>>>>>>>>> names you understand the intent, what they are doing. The parameters also
>>>>>>>>>> explain what you need to pass into them.
>>>>>>>>>>
>>>>>>>>>> If what you are looking for in these comments is the reasoning
>>>>>>>>>> being the change itself, then that should be present in the commit message.
>>>>>>>>>> Specially because this is going to be a very big patch with a very big
>>>>>>>>>> number of changes.
>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> Thanks
>>>>>>>>>>>> Joao
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> Thanks,
>>>>>>>>>>> Khushboo
>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> On Fri, Apr 6, 2018 at 4:48 AM Murtuza Zabuawala <
>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>
>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>
>>>>>>>>>>>>> Patch looks good and working as expected.
>>>>>>>>>>>>>
>>>>>>>>>>>>> I also agree with Dave, Can we please add some comments in
>>>>>>>>>>>>> each file which can help us to understand the flow, I'm saying because now
>>>>>>>>>>>>> the code is segregated in so many separate files it will be hard to keep
>>>>>>>>>>>>> track of the flow from one file to another when debugging.
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> --
>>>>>>>>>>>>> Regards,
>>>>>>>>>>>>> Murtuza Zabuawala
>>>>>>>>>>>>> EnterpriseDB: http://www.enterprisedb.com
>>>>>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> On Thu, Apr 5, 2018 at 7:08 PM, Joao De Almeida Pereira <
>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>
>>>>>>>>>>>>>> Hi Khushboo,
>>>>>>>>>>>>>> Attached you can find both patches rebased
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Can you please rebase the second patch?
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida Pereira <
>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Attached you can find the patch that will start to decouple
>>>>>>>>>>>>>>>> pgAdmin from ACITree library.
>>>>>>>>>>>>>>>> This patch is intended to be merged after 3.0, because we
>>>>>>>>>>>>>>>> do not want to cause any entropy or delay the release, but we want to start
>>>>>>>>>>>>>>>> the discussion and show some code.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> This job that we started is a massive tech debt chore that
>>>>>>>>>>>>>>>> will take some time to finalize and we would love the help of the community
>>>>>>>>>>>>>>>> to do it.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> *Summary of the patch:*
>>>>>>>>>>>>>>>> 0001 patch:
>>>>>>>>>>>>>>>> - Creates a new tree that will allow us to create a
>>>>>>>>>>>>>>>> separation between the application and ACI Tree
>>>>>>>>>>>>>>>> - Creates a Fake Tree (Test double, for reference on the
>>>>>>>>>>>>>>>> available test doubles: https://martinfowler.
>>>>>>>>>>>>>>>> com/bliki/TestDouble.html) that can be used to inplace to
>>>>>>>>>>>>>>>> replace the ACITree and also encapsulate the new tree behavior on our tests
>>>>>>>>>>>>>>>> - Adds tests for all the tree functionalities
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> 0002 patch:
>>>>>>>>>>>>>>>> - Extracts, refactors, adds tests and remove dependency
>>>>>>>>>>>>>>>> from ACI Tree on:
>>>>>>>>>>>>>>>> - getTreeNodeHierarchy
>>>>>>>>>>>>>>>> - on backup.js: menu_enabled, menu_enabled_server,
>>>>>>>>>>>>>>>> start_backup_global_server, backup_objects
>>>>>>>>>>>>>>>> - on datagrid.js: show_data_grid, get_panel_title,
>>>>>>>>>>>>>>>> show_query_tool
>>>>>>>>>>>>>>>> - Start using sprintf-js as Underscore.String is
>>>>>>>>>>>>>>>> deprecating sprintf function
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> This patch represents only 10 calls to ACITree.itemData out
>>>>>>>>>>>>>>>> of 176 that are spread around our code
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> *In Depth look on the process behind the patch:*
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> We started writing this patch with the idea that we need to
>>>>>>>>>>>>>>>> decouple pgAdmin4 from ACITree, because ACITree is no longer supported, the
>>>>>>>>>>>>>>>> documentation is non existent and ACITree is no longer being actively
>>>>>>>>>>>>>>>> developed.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Our process:
>>>>>>>>>>>>>>>> 1. We "randomly" selected a function that is part of the
>>>>>>>>>>>>>>>> ACITree. From this point we decided to replace that function with our own
>>>>>>>>>>>>>>>> version. The function that we choose was "itemData".
>>>>>>>>>>>>>>>> The function gives us all the "data" that a specific node
>>>>>>>>>>>>>>>> of the tree find.
>>>>>>>>>>>>>>>> Given in order to replace the tree we would need to have a
>>>>>>>>>>>>>>>> function that would give us the same information. We had 2 options:
>>>>>>>>>>>>>>>> a) Create a tree with a function called itemData
>>>>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>>>>> - At first view this was the simpler solution
>>>>>>>>>>>>>>>> - Would keep the status quo
>>>>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>>>>> - Not a OOP approach
>>>>>>>>>>>>>>>> - Not very flexible
>>>>>>>>>>>>>>>> b) Create a tree that would return a node given an ID and
>>>>>>>>>>>>>>>> then the node would be responsible for giving it's data.
>>>>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>>>>> - OOP Approach
>>>>>>>>>>>>>>>> - More flexible and we do not need to bring the tree
>>>>>>>>>>>>>>>> around, just a node
>>>>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>>>>> - Break the current status quo
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Given these 2 options we decided to go for a more OOP
>>>>>>>>>>>>>>>> approach creating a Tree and a TreeNode classes, that in the future will be
>>>>>>>>>>>>>>>> renamed to ACITreeWrapper and TreeNode.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> 2. After we decided on the starting point we searched for
>>>>>>>>>>>>>>>> occurrences of the function "itemData" and we found out that there were 303
>>>>>>>>>>>>>>>> occurrences of "itemData" in the code and roughly 176 calls to the function
>>>>>>>>>>>>>>>> itself (some of the hits were variable names).
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> 3. We selected the first file on the search and found the
>>>>>>>>>>>>>>>> function that was responsible for calling the itemData function.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> 4. Extracted the function to a separate file
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> 5. Wrap this function with tests
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> 6. Refactor the function to ES6, give more declarative
>>>>>>>>>>>>>>>> names to variables and break the functions into smaller chunks
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> 7. When all the tests were passing we replaced ACITree with
>>>>>>>>>>>>>>>> our Tree
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> 8. We ensured that all tests were passing
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> 9. Remove function from the original file and use the new
>>>>>>>>>>>>>>>> function
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> 10. Ensure everything still works
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> 11. Find the next function and execute from step 4 until
>>>>>>>>>>>>>>>> all the functions are replaced, refactored and tested.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> As you can see by the process this is a pretty huge
>>>>>>>>>>>>>>>> undertake, because of the number of calls to the function. This is just the
>>>>>>>>>>>>>>>> first step on the direction of completely isolating the ACITree so that we
>>>>>>>>>>>>>>>> can solve the problem with a large number of elements on the tree.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> *What is on our radar that we need to address:*
>>>>>>>>>>>>>>>> - Finish the complete decoupling of the ACITree
>>>>>>>>>>>>>>>> - Performance of the current tree implementation
>>>>>>>>>>>>>>>> - Tweak the naming of the Tree class to explicitly tell us
>>>>>>>>>>>>>>>> this is to use only with ACITree.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>> --
>>>>>>> Dave Page
>>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>>> Twitter: @pgsnake
>>>>>>>
>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>
>>>>>>
>>>>>>
>>>>>>
>>>>>> --
>>>>>> Dave Page
>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>> Twitter: @pgsnake
>>>>>>
>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>> The Enterprise PostgreSQL Company
>>>>>>
>>>>>
>>>
>>
>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-30 15:37 Dave Page <[email protected]>
parent: Ashesh Vashi <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Dave Page @ 2018-04-30 15:37 UTC (permalink / raw)
To: Ashesh Vashi <[email protected]>; +Cc: Anthony Emengo <[email protected]>; Joao De Almeida Pereira <[email protected]>; Khushboo Vashi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
On Mon, Apr 30, 2018 at 4:33 PM, Ashesh Vashi <[email protected]
> wrote:
>
> On Mon, Apr 30, 2018 at 8:58 PM, Anthony Emengo <[email protected]>
> wrote:
>
>> I was expecting a separate layer between the tree implementation, and
>>> aciTree adaptor.
>>> Please find the patch for the example.
>>> It will separate the two layers, and easy to replace with the new
>>> implementation in future.
>>
>>
>> In general, we want defer the separation of the layers for now. Even
>> though we might assume that this is the direction we want to go in. It's
>> simply too early to be making such an architectural leap. For right now, we
>> just know that we need the decoupling, but don't know what if we'd need the
>> 2 layers *as implemented*. The principle we're adhering to here is the
>> Last Responsible Moment principle, which states that you only make the
>> changes that you feel is necessary for the given problems you're facing:
>> https://blog.codinghorror.com/the-last-responsible-moment/
>>
>> I would not like to see that changes in this patch.
>>> I would like us to come up with the actual design about the hot
>>> pluggability before going in this direction.
>>
>>
>> In our point of view these 2 changes are not related. One thing is the
>> internal code organization of the application, other thing is allowing
>> third party to drop code in the application and it just work. These 2
>> should be talked separately, but the hot pluggability is not something that
>> will be address by this work we are doing right now.
>>
> Neither - it should be part of this change.
> It should be addressed separately, and discussed.
>
I agree. As long as this work doesn't make the pluggability problem worse,
that problem should be considered separately.
So given Anthony's comments, are you happy with this patch?
>
> -- Thanks, Ashesh
>
>>
>> Anthony && Joao
>>
>> On Mon, Apr 30, 2018 at 11:03 AM, Ashesh Vashi <
>> [email protected]> wrote:
>>
>>>
>>>
>>>
>>> On Mon, Apr 30, 2018 at 8:30 PM, Ashesh Vashi <
>>> [email protected]> wrote:
>>>
>>>> On Sat, Apr 28, 2018 at 3:55 AM, Joao De Almeida Pereira <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi Hackers,
>>>>> As you are aware we kept on working on the patch, so we are attaching
>>>>> to this email a new version of the patch.
>>>>> This new version contains all the changes in the previous one plus
>>>>> more extractions of functions and refactoring of code.
>>>>>
>>>>> The objective of this patch is to create a separation between pgAdmin
>>>>> and the ACI Tree. We are doing this because we realized that at this point
>>>>> in time we have the ACI Tree all over the code of pgAdmin. I found a very
>>>>> interesting article that really talks about this:
>>>>> https://medium.freecodecamp.org/code-dependencies-are-the-de
>>>>> vil-35ed28b556d
>>>>>
>>>>> In this patch there are some visions and ideas about the location of
>>>>> the code, the way to organize it and also try to pave the future for a
>>>>> application that is stable, easy to develop on and that can be release at a
>>>>> times notice.
>>>>>
>>>>> We are investing a big chunk of our time in doing this refactoring,
>>>>> but while doing that we also try to respond to the patches sent to the
>>>>> mailing list. We would like the feedback from the community because we
>>>>> believe this is a changing point for the application. The idea is to change
>>>>> the way we develop this application, instead of only correcting a bug of
>>>>> developing a feature, with every commit we should correct the bug or
>>>>> develop a feature but leave the code a little better than we found it
>>>>> (Refactoring, refactoring, refactoring). This is hard work but that is what
>>>>> the users from pgAdmin expect from this community of developers.
>>>>>
>>>>>
>>>>> ======
>>>>>
>>>>>
>>>>>
>>>>> It is a huge patch
>>>>> 86 files changed, 5492 inserts, 1840
>>>>> deletions
>>>>> and we would like to get your feedback as soon as possible, because we
>>>>> are continuing to work on it which means it is going to grow in size.
>>>>>
>>>>>
>>>>> At this point in time we still have 124 of 176 calls to the function
>>>>> itemData from ACITree.
>>>>>
>>>>> What does each patch contain:
>>>>> 0001:
>>>>> Very simple patch, we found out that the linter was not looking into
>>>>> all the javascript test files, so this patch will ensure it is
>>>>>
>>>> Committed the patch along with the regression introduced because of
>>>> this patch.
>>>>
>>>>>
>>>>> 0002:
>>>>> New Tree abstraction. This patch contains the new Tree that works as
>>>>> an adaptor for ACI Tree and is going to be used on all the extractions that
>>>>> we are doing.
>>>>>
>>>>
>>>> I was expecting a separate layer between the tree implementation, and
>>>> aciTree adaptor.
>>>> Please find the patch for the example.
>>>>
>>>> It will separate the two layers, and easy to replace with the new
>>>> implemenation in future.
>>>>
>>>
>>> Oops forgot to attach the patch.
>>> Please find the patch attached.
>>>
>>> -- Thanks, Ashesh
>>>
>>>>
>>>>> 0003:
>>>>> Code that extracts, wrap with tests and replace ACI Tree invocations.
>>>>>
>>>> There are many small cases left in the patches.
>>>> Hence - I would like to know the TODO list created by you.
>>>>
>>>> e.g. When we remove any of the object from the database server, we're
>>>> not yet removing the respective node from the new implementation, and its
>>>> children.
>>>>
>>>>>
>>>>> We start creating new pattern for the location of Javascript files
>>>>> and their structure.
>>>>>
>>>> I would not like to see that changes in this patch.
>>>> I would like us to come up with the actual design about the hot
>>>> pluggability before going in this direction.
>>>>
>>>>> Create patterns for creation of dialogs (backup and restore)
>>>>>
>>>> It's better - we don't change the directory structure at the moment.
>>>>
>>>> I am not against dividing the big javascript files in small chunks, but
>>>> - I would like us to discuss first about the hot plugins design first.
>>>>
>>>> -- Thanks, Ashesh
>>>>
>>>>>
>>>>>
>>>>
>>>>>
>>>>> Thanks
>>>>> Joao
>>>>>
>>>>>
>>>>> On Fri, Apr 27, 2018 at 5:34 AM Ashesh Vashi <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> I have quite a few comments for the patch.
>>>>>> I will send them soon.
>>>>>>
>>>>>> On Fri, Apr 27, 2018, 14:45 Dave Page <[email protected]> wrote:
>>>>>>
>>>>>>> How is your work on this going Ashesh? Will you be committing today?
>>>>>>>
>>>>>>> On Wed, Apr 25, 2018 at 8:52 AM, Dave Page <[email protected]>
>>>>>>> wrote:
>>>>>>>
>>>>>>>> Ashesh; you had agreed to work on this early this week. Please
>>>>>>>> ensure you do so today.
>>>>>>>>
>>>>>>>> Thanks.
>>>>>>>>
>>>>>>>> On Tue, Apr 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Hi Hackers,
>>>>>>>>>
>>>>>>>>> Can someone review and merge this patch?
>>>>>>>>>
>>>>>>>>> Thanks
>>>>>>>>> Joao
>>>>>>>>>
>>>>>>>>> On Wed, Apr 18, 2018 at 10:23 AM Joao De Almeida Pereira <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Hi Hackers,
>>>>>>>>>> Any other comment about this patch?
>>>>>>>>>>
>>>>>>>>>> Thanks
>>>>>>>>>> Joao
>>>>>>>>>>
>>>>>>>>>> On Tue, Apr 10, 2018 at 12:00 PM Joao De Almeida Pereira <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> Hello Khushboo
>>>>>>>>>>>
>>>>>>>>>>> On Mon, Apr 9, 2018 at 1:59 AM Khushboo Vashi <
>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>
>>>>>>>>>>>> I have reviewed your patch and have some suggestions.
>>>>>>>>>>>>
>>>>>>>>>>>> On Sat, Apr 7, 2018 at 12:43 AM, Joao De Almeida Pereira <
>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>
>>>>>>>>>>>>> Hello Murtuza/Dave,
>>>>>>>>>>>>> Yes now the extracted functions are spread into different
>>>>>>>>>>>>> files. The intent would be to make the files as small as possible, and also
>>>>>>>>>>>>> to group and name them in a way that would be easy to understand what each
>>>>>>>>>>>>> file is doing without the need of opening it.
>>>>>>>>>>>>> As a example:
>>>>>>>>>>>>> static/js/backup will contain all the backup related
>>>>>>>>>>>>> functionality inside of this folder we can see the file:
>>>>>>>>>>>>>
>>>>>>>>>>>> menu_utils.js At this moment in time we decided to group all
>>>>>>>>>>>>> the functions that are related to the menu, but we can split that also if
>>>>>>>>>>>>> we believe it is easier to see.
>>>>>>>>>>>>>
>>>>>>>>>>>> It's really very good to see the separated code for backup
>>>>>>>>>>>> module. As we have done for backup, we would like do it for other PG
>>>>>>>>>>>> utilities like restore, maintenance etc.
>>>>>>>>>>>> Considering this, we should separate the code in a way that
>>>>>>>>>>>> some of the common functionalities can be used for other modules like menu
>>>>>>>>>>>> (as you have mentioned above), dialogue factory etc.
>>>>>>>>>>>> Also, I think these functionalities should be in their
>>>>>>>>>>>> respective static folder instead of pgadmin/static.
>>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> About the location of the files. The move of the files to
>>>>>>>>>>> pgadmin/static/js was made on purpose in order to clearly separate
>>>>>>>>>>> Javascript from python code.
>>>>>>>>>>> The rational behind it was
>>>>>>>>>>> - Create a clear separation between the backend and frontend
>>>>>>>>>>> - Having Javascript code concentrated in a single place,
>>>>>>>>>>> hopefully, will encourage to developers to look for a functionality, that
>>>>>>>>>>> is already implemented in another modules, because they are right there.
>>>>>>>>>>> (When we started this journey we realized that the 'nodes' have a big
>>>>>>>>>>> groups of code that could be shared, but because the Javascript is spread
>>>>>>>>>>> everywhere it is much harder to look for it)
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> There are some drawbacks of this separation:
>>>>>>>>>>> - When creating a new module we will need to put the javascript
>>>>>>>>>>> in a separate location from the backend code
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>> static/js/datagrid folder contains all the datagrid related
>>>>>>>>>>>>> functionality
>>>>>>>>>>>>>
>>>>>>>>>>>> Same as backup module, this should be in it's respective
>>>>>>>>>>>> static/js folder.
>>>>>>>>>>>>
>>>>>>>>>>>>> Inside of the folder we can see the files:
>>>>>>>>>>>>> get_panel_title.js is responsible for retrieving the name of
>>>>>>>>>>>>> the panel
>>>>>>>>>>>>> show_data.js is responsible for showing the datagrid
>>>>>>>>>>>>> show_query_tool.js is responsible for showing the query tool
>>>>>>>>>>>>>
>>>>>>>>>>>>> Does this structure make sense?
>>>>>>>>>>>>> Can you give an example of a comment that you think is missing
>>>>>>>>>>>>> and that could help?
>>>>>>>>>>>>>
>>>>>>>>>>>>> As a personal note, unless the algorithm is very obscure or
>>>>>>>>>>>>> very complicated, I believe that if the code needs comments it is a signal
>>>>>>>>>>>>> that something needs to change in terms of naming, structure of the part in
>>>>>>>>>>>>> question. This being said, I am open to add some comments that might help
>>>>>>>>>>>>> people.
>>>>>>>>>>>>>
>>>>>>>>>>>> You are right, with the help of naming convention and structure
>>>>>>>>>>>> of the code, any one can get the idea about the code. But it is very useful
>>>>>>>>>>>> to understand the code
>>>>>>>>>>>> very easily with the proper comments especially when there are
>>>>>>>>>>>> multiple developers working on a single project.
>>>>>>>>>>>>
>>>>>>>>>>>> I found some of the places where it would be great to have
>>>>>>>>>>>> comments.
>>>>>>>>>>>>
>>>>>>>>>>>> - treeMenu: new tree.Tree() in a browser.js
>>>>>>>>>>>> - tree.js (especially Tree class)
>>>>>>>>>>>>
>>>>>>>>>>> About the comment point I need a more clear understanding on
>>>>>>>>>>> what kind of comments you are looking for. Because when you read the
>>>>>>>>>>> function names you understand the intent, what they are doing. The
>>>>>>>>>>> parameters also explain what you need to pass into them.
>>>>>>>>>>>
>>>>>>>>>>> If what you are looking for in these comments is the reasoning
>>>>>>>>>>> being the change itself, then that should be present in the commit message.
>>>>>>>>>>> Specially because this is going to be a very big patch with a very big
>>>>>>>>>>> number of changes.
>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> Thanks
>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> On Fri, Apr 6, 2018 at 4:48 AM Murtuza Zabuawala <
>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>
>>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Patch looks good and working as expected.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> I also agree with Dave, Can we please add some comments in
>>>>>>>>>>>>>> each file which can help us to understand the flow, I'm saying because now
>>>>>>>>>>>>>> the code is segregated in so many separate files it will be hard to keep
>>>>>>>>>>>>>> track of the flow from one file to another when debugging.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> --
>>>>>>>>>>>>>> Regards,
>>>>>>>>>>>>>> Murtuza Zabuawala
>>>>>>>>>>>>>> EnterpriseDB: http://www.enterprisedb.com
>>>>>>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> On Thu, Apr 5, 2018 at 7:08 PM, Joao De Almeida Pereira <
>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Hi Khushboo,
>>>>>>>>>>>>>>> Attached you can find both patches rebased
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Can you please rebase the second patch?
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida Pereira <
>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Attached you can find the patch that will start to
>>>>>>>>>>>>>>>>> decouple pgAdmin from ACITree library.
>>>>>>>>>>>>>>>>> This patch is intended to be merged after 3.0, because we
>>>>>>>>>>>>>>>>> do not want to cause any entropy or delay the release, but we want to start
>>>>>>>>>>>>>>>>> the discussion and show some code.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> This job that we started is a massive tech debt chore that
>>>>>>>>>>>>>>>>> will take some time to finalize and we would love the help of the community
>>>>>>>>>>>>>>>>> to do it.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> *Summary of the patch:*
>>>>>>>>>>>>>>>>> 0001 patch:
>>>>>>>>>>>>>>>>> - Creates a new tree that will allow us to create a
>>>>>>>>>>>>>>>>> separation between the application and ACI Tree
>>>>>>>>>>>>>>>>> - Creates a Fake Tree (Test double, for reference on the
>>>>>>>>>>>>>>>>> available test doubles: https://martinfowler.
>>>>>>>>>>>>>>>>> com/bliki/TestDouble.html) that can be used to inplace to
>>>>>>>>>>>>>>>>> replace the ACITree and also encapsulate the new tree behavior on our tests
>>>>>>>>>>>>>>>>> - Adds tests for all the tree functionalities
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> 0002 patch:
>>>>>>>>>>>>>>>>> - Extracts, refactors, adds tests and remove dependency
>>>>>>>>>>>>>>>>> from ACI Tree on:
>>>>>>>>>>>>>>>>> - getTreeNodeHierarchy
>>>>>>>>>>>>>>>>> - on backup.js: menu_enabled, menu_enabled_server,
>>>>>>>>>>>>>>>>> start_backup_global_server, backup_objects
>>>>>>>>>>>>>>>>> - on datagrid.js: show_data_grid, get_panel_title,
>>>>>>>>>>>>>>>>> show_query_tool
>>>>>>>>>>>>>>>>> - Start using sprintf-js as Underscore.String is
>>>>>>>>>>>>>>>>> deprecating sprintf function
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> This patch represents only 10 calls to ACITree.itemData
>>>>>>>>>>>>>>>>> out of 176 that are spread around our code
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> *In Depth look on the process behind the patch:*
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> We started writing this patch with the idea that we need
>>>>>>>>>>>>>>>>> to decouple pgAdmin4 from ACITree, because ACITree is no longer supported,
>>>>>>>>>>>>>>>>> the documentation is non existent and ACITree is no longer being actively
>>>>>>>>>>>>>>>>> developed.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Our process:
>>>>>>>>>>>>>>>>> 1. We "randomly" selected a function that is part of the
>>>>>>>>>>>>>>>>> ACITree. From this point we decided to replace that function with our own
>>>>>>>>>>>>>>>>> version. The function that we choose was "itemData".
>>>>>>>>>>>>>>>>> The function gives us all the "data" that a specific node
>>>>>>>>>>>>>>>>> of the tree find.
>>>>>>>>>>>>>>>>> Given in order to replace the tree we would need to have a
>>>>>>>>>>>>>>>>> function that would give us the same information. We had 2 options:
>>>>>>>>>>>>>>>>> a) Create a tree with a function called itemData
>>>>>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>>>>>> - At first view this was the simpler solution
>>>>>>>>>>>>>>>>> - Would keep the status quo
>>>>>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>>>>>> - Not a OOP approach
>>>>>>>>>>>>>>>>> - Not very flexible
>>>>>>>>>>>>>>>>> b) Create a tree that would return a node given an ID
>>>>>>>>>>>>>>>>> and then the node would be responsible for giving it's data.
>>>>>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>>>>>> - OOP Approach
>>>>>>>>>>>>>>>>> - More flexible and we do not need to bring the tree
>>>>>>>>>>>>>>>>> around, just a node
>>>>>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>>>>>> - Break the current status quo
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Given these 2 options we decided to go for a more OOP
>>>>>>>>>>>>>>>>> approach creating a Tree and a TreeNode classes, that in the future will be
>>>>>>>>>>>>>>>>> renamed to ACITreeWrapper and TreeNode.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> 2. After we decided on the starting point we searched for
>>>>>>>>>>>>>>>>> occurrences of the function "itemData" and we found out that there were 303
>>>>>>>>>>>>>>>>> occurrences of "itemData" in the code and roughly 176 calls to the function
>>>>>>>>>>>>>>>>> itself (some of the hits were variable names).
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> 3. We selected the first file on the search and found the
>>>>>>>>>>>>>>>>> function that was responsible for calling the itemData function.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> 4. Extracted the function to a separate file
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> 5. Wrap this function with tests
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> 6. Refactor the function to ES6, give more declarative
>>>>>>>>>>>>>>>>> names to variables and break the functions into smaller chunks
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> 7. When all the tests were passing we replaced ACITree
>>>>>>>>>>>>>>>>> with our Tree
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> 8. We ensured that all tests were passing
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> 9. Remove function from the original file and use the new
>>>>>>>>>>>>>>>>> function
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> 10. Ensure everything still works
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> 11. Find the next function and execute from step 4 until
>>>>>>>>>>>>>>>>> all the functions are replaced, refactored and tested.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> As you can see by the process this is a pretty huge
>>>>>>>>>>>>>>>>> undertake, because of the number of calls to the function. This is just the
>>>>>>>>>>>>>>>>> first step on the direction of completely isolating the ACITree so that we
>>>>>>>>>>>>>>>>> can solve the problem with a large number of elements on the tree.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> *What is on our radar that we need to address:*
>>>>>>>>>>>>>>>>> - Finish the complete decoupling of the ACITree
>>>>>>>>>>>>>>>>> - Performance of the current tree implementation
>>>>>>>>>>>>>>>>> - Tweak the naming of the Tree class to explicitly tell
>>>>>>>>>>>>>>>>> us this is to use only with ACITree.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>
>>>>>>>>
>>>>>>>> --
>>>>>>>> Dave Page
>>>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>>>> Twitter: @pgsnake
>>>>>>>>
>>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>> --
>>>>>>> Dave Page
>>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>>> Twitter: @pgsnake
>>>>>>>
>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>
>>>>>>
>>>>
>>>
>>
>
--
Dave Page
Blog: http://pgsnake.blogspot.com
Twitter: @pgsnake
EnterpriseDB UK: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-30 15:45 Ashesh Vashi <[email protected]>
parent: Dave Page <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Ashesh Vashi @ 2018-04-30 15:45 UTC (permalink / raw)
To: Dave Page <[email protected]>; +Cc: Anthony Emengo <[email protected]>; Joao De Almeida Pereira <[email protected]>; Khushboo Vashi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
On Mon, Apr 30, 2018 at 9:07 PM, Dave Page <[email protected]> wrote:
>
>
> On Mon, Apr 30, 2018 at 4:33 PM, Ashesh Vashi <
> [email protected]> wrote:
>
>>
>> On Mon, Apr 30, 2018 at 8:58 PM, Anthony Emengo <[email protected]>
>> wrote:
>>
>>> I was expecting a separate layer between the tree implementation, and
>>>> aciTree adaptor.
>>>> Please find the patch for the example.
>>>> It will separate the two layers, and easy to replace with the new
>>>> implementation in future.
>>>
>>>
>>> In general, we want defer the separation of the layers for now. Even
>>> though we might assume that this is the direction we want to go in. It's
>>> simply too early to be making such an architectural leap. For right now, we
>>> just know that we need the decoupling, but don't know what if we'd need the
>>> 2 layers *as implemented*. The principle we're adhering to here is the
>>> Last Responsible Moment principle, which states that you only make the
>>> changes that you feel is necessary for the given problems you're facing:
>>> https://blog.codinghorror.com/the-last-responsible-moment/
>>>
>>> I would not like to see that changes in this patch.
>>>> I would like us to come up with the actual design about the hot
>>>> pluggability before going in this direction.
>>>
>>>
>>> In our point of view these 2 changes are not related. One thing is the
>>> internal code organization of the application, other thing is allowing
>>> third party to drop code in the application and it just work. These 2
>>> should be talked separately, but the hot pluggability is not something that
>>> will be address by this work we are doing right now.
>>>
>> Neither - it should be part of this change.
>> It should be addressed separately, and discussed.
>>
>
> I agree. As long as this work doesn't make the pluggability problem worse,
> that problem should be considered separately.
>
> So given Anthony's comments, are you happy with this patch?
>
I liked the design so far.
But - as Khushboo mentioned ealier - it is missing at some places.
I had to read through the code to understand the execution flow for some.
And, there is still a lot missing bits, and pieces to consider for commit
in the repo.
-- Thanks, Ashesh
>
>
>>
>> -- Thanks, Ashesh
>>
>>>
>>> Anthony && Joao
>>>
>>> On Mon, Apr 30, 2018 at 11:03 AM, Ashesh Vashi <
>>> [email protected]> wrote:
>>>
>>>>
>>>>
>>>>
>>>> On Mon, Apr 30, 2018 at 8:30 PM, Ashesh Vashi <
>>>> [email protected]> wrote:
>>>>
>>>>> On Sat, Apr 28, 2018 at 3:55 AM, Joao De Almeida Pereira <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hi Hackers,
>>>>>> As you are aware we kept on working on the patch, so we are attaching
>>>>>> to this email a new version of the patch.
>>>>>> This new version contains all the changes in the previous one plus
>>>>>> more extractions of functions and refactoring of code.
>>>>>>
>>>>>> The objective of this patch is to create a separation between pgAdmin
>>>>>> and the ACI Tree. We are doing this because we realized that at this point
>>>>>> in time we have the ACI Tree all over the code of pgAdmin. I found a very
>>>>>> interesting article that really talks about this:
>>>>>> https://medium.freecodecamp.org/code-dependencies-are-the-de
>>>>>> vil-35ed28b556d
>>>>>>
>>>>>> In this patch there are some visions and ideas about the location of
>>>>>> the code, the way to organize it and also try to pave the future for a
>>>>>> application that is stable, easy to develop on and that can be release at a
>>>>>> times notice.
>>>>>>
>>>>>> We are investing a big chunk of our time in doing this refactoring,
>>>>>> but while doing that we also try to respond to the patches sent to the
>>>>>> mailing list. We would like the feedback from the community because we
>>>>>> believe this is a changing point for the application. The idea is to change
>>>>>> the way we develop this application, instead of only correcting a bug of
>>>>>> developing a feature, with every commit we should correct the bug or
>>>>>> develop a feature but leave the code a little better than we found it
>>>>>> (Refactoring, refactoring, refactoring). This is hard work but that is what
>>>>>> the users from pgAdmin expect from this community of developers.
>>>>>>
>>>>>>
>>>>>> ======
>>>>>>
>>>>>>
>>>>>>
>>>>>> It is a huge patch
>>>>>> 86 files changed, 5492 inserts, 1840
>>>>>> deletions
>>>>>> and we would like to get your feedback as soon as possible, because
>>>>>> we are continuing to work on it which means it is going to grow in size.
>>>>>>
>>>>>>
>>>>>> At this point in time we still have 124 of 176 calls to the function
>>>>>> itemData from ACITree.
>>>>>>
>>>>>> What does each patch contain:
>>>>>> 0001:
>>>>>> Very simple patch, we found out that the linter was not looking
>>>>>> into all the javascript test files, so this patch will ensure it is
>>>>>>
>>>>> Committed the patch along with the regression introduced because of
>>>>> this patch.
>>>>>
>>>>>>
>>>>>> 0002:
>>>>>> New Tree abstraction. This patch contains the new Tree that works
>>>>>> as an adaptor for ACI Tree and is going to be used on all the extractions
>>>>>> that we are doing.
>>>>>>
>>>>>
>>>>> I was expecting a separate layer between the tree implementation, and
>>>>> aciTree adaptor.
>>>>> Please find the patch for the example.
>>>>>
>>>>> It will separate the two layers, and easy to replace with the new
>>>>> implemenation in future.
>>>>>
>>>>
>>>> Oops forgot to attach the patch.
>>>> Please find the patch attached.
>>>>
>>>> -- Thanks, Ashesh
>>>>
>>>>>
>>>>>> 0003:
>>>>>> Code that extracts, wrap with tests and replace ACI Tree
>>>>>> invocations.
>>>>>>
>>>>> There are many small cases left in the patches.
>>>>> Hence - I would like to know the TODO list created by you.
>>>>>
>>>>> e.g. When we remove any of the object from the database server, we're
>>>>> not yet removing the respective node from the new implementation, and its
>>>>> children.
>>>>>
>>>>>>
>>>>>> We start creating new pattern for the location of Javascript files
>>>>>> and their structure.
>>>>>>
>>>>> I would not like to see that changes in this patch.
>>>>> I would like us to come up with the actual design about the hot
>>>>> pluggability before going in this direction.
>>>>>
>>>>>> Create patterns for creation of dialogs (backup and restore)
>>>>>>
>>>>> It's better - we don't change the directory structure at the moment.
>>>>>
>>>>> I am not against dividing the big javascript files in small chunks,
>>>>> but - I would like us to discuss first about the hot plugins design first.
>>>>>
>>>>> -- Thanks, Ashesh
>>>>>
>>>>>>
>>>>>>
>>>>>
>>>>>>
>>>>>> Thanks
>>>>>> Joao
>>>>>>
>>>>>>
>>>>>> On Fri, Apr 27, 2018 at 5:34 AM Ashesh Vashi <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> I have quite a few comments for the patch.
>>>>>>> I will send them soon.
>>>>>>>
>>>>>>> On Fri, Apr 27, 2018, 14:45 Dave Page <[email protected]> wrote:
>>>>>>>
>>>>>>>> How is your work on this going Ashesh? Will you be committing today?
>>>>>>>>
>>>>>>>> On Wed, Apr 25, 2018 at 8:52 AM, Dave Page <[email protected]>
>>>>>>>> wrote:
>>>>>>>>
>>>>>>>>> Ashesh; you had agreed to work on this early this week. Please
>>>>>>>>> ensure you do so today.
>>>>>>>>>
>>>>>>>>> Thanks.
>>>>>>>>>
>>>>>>>>> On Tue, Apr 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Hi Hackers,
>>>>>>>>>>
>>>>>>>>>> Can someone review and merge this patch?
>>>>>>>>>>
>>>>>>>>>> Thanks
>>>>>>>>>> Joao
>>>>>>>>>>
>>>>>>>>>> On Wed, Apr 18, 2018 at 10:23 AM Joao De Almeida Pereira <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>> Any other comment about this patch?
>>>>>>>>>>>
>>>>>>>>>>> Thanks
>>>>>>>>>>> Joao
>>>>>>>>>>>
>>>>>>>>>>> On Tue, Apr 10, 2018 at 12:00 PM Joao De Almeida Pereira <
>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> Hello Khushboo
>>>>>>>>>>>>
>>>>>>>>>>>> On Mon, Apr 9, 2018 at 1:59 AM Khushboo Vashi <
>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>
>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>
>>>>>>>>>>>>> I have reviewed your patch and have some suggestions.
>>>>>>>>>>>>>
>>>>>>>>>>>>> On Sat, Apr 7, 2018 at 12:43 AM, Joao De Almeida Pereira <
>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>
>>>>>>>>>>>>>> Hello Murtuza/Dave,
>>>>>>>>>>>>>> Yes now the extracted functions are spread into different
>>>>>>>>>>>>>> files. The intent would be to make the files as small as possible, and also
>>>>>>>>>>>>>> to group and name them in a way that would be easy to understand what each
>>>>>>>>>>>>>> file is doing without the need of opening it.
>>>>>>>>>>>>>> As a example:
>>>>>>>>>>>>>> static/js/backup will contain all the backup related
>>>>>>>>>>>>>> functionality inside of this folder we can see the file:
>>>>>>>>>>>>>>
>>>>>>>>>>>>> menu_utils.js At this moment in time we decided to group all
>>>>>>>>>>>>>> the functions that are related to the menu, but we can split that also if
>>>>>>>>>>>>>> we believe it is easier to see.
>>>>>>>>>>>>>>
>>>>>>>>>>>>> It's really very good to see the separated code for backup
>>>>>>>>>>>>> module. As we have done for backup, we would like do it for other PG
>>>>>>>>>>>>> utilities like restore, maintenance etc.
>>>>>>>>>>>>> Considering this, we should separate the code in a way that
>>>>>>>>>>>>> some of the common functionalities can be used for other modules like menu
>>>>>>>>>>>>> (as you have mentioned above), dialogue factory etc.
>>>>>>>>>>>>> Also, I think these functionalities should be in their
>>>>>>>>>>>>> respective static folder instead of pgadmin/static.
>>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> About the location of the files. The move of the files to
>>>>>>>>>>>> pgadmin/static/js was made on purpose in order to clearly separate
>>>>>>>>>>>> Javascript from python code.
>>>>>>>>>>>> The rational behind it was
>>>>>>>>>>>> - Create a clear separation between the backend and frontend
>>>>>>>>>>>> - Having Javascript code concentrated in a single place,
>>>>>>>>>>>> hopefully, will encourage to developers to look for a functionality, that
>>>>>>>>>>>> is already implemented in another modules, because they are right there.
>>>>>>>>>>>> (When we started this journey we realized that the 'nodes' have a big
>>>>>>>>>>>> groups of code that could be shared, but because the Javascript is spread
>>>>>>>>>>>> everywhere it is much harder to look for it)
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> There are some drawbacks of this separation:
>>>>>>>>>>>> - When creating a new module we will need to put the javascript
>>>>>>>>>>>> in a separate location from the backend code
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>>> static/js/datagrid folder contains all the datagrid related
>>>>>>>>>>>>>> functionality
>>>>>>>>>>>>>>
>>>>>>>>>>>>> Same as backup module, this should be in it's respective
>>>>>>>>>>>>> static/js folder.
>>>>>>>>>>>>>
>>>>>>>>>>>>>> Inside of the folder we can see the files:
>>>>>>>>>>>>>> get_panel_title.js is responsible for retrieving the name of
>>>>>>>>>>>>>> the panel
>>>>>>>>>>>>>> show_data.js is responsible for showing the datagrid
>>>>>>>>>>>>>> show_query_tool.js is responsible for showing the query tool
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Does this structure make sense?
>>>>>>>>>>>>>> Can you give an example of a comment that you think is
>>>>>>>>>>>>>> missing and that could help?
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> As a personal note, unless the algorithm is very obscure or
>>>>>>>>>>>>>> very complicated, I believe that if the code needs comments it is a signal
>>>>>>>>>>>>>> that something needs to change in terms of naming, structure of the part in
>>>>>>>>>>>>>> question. This being said, I am open to add some comments that might help
>>>>>>>>>>>>>> people.
>>>>>>>>>>>>>>
>>>>>>>>>>>>> You are right, with the help of naming convention and
>>>>>>>>>>>>> structure of the code, any one can get the idea about the code. But it is
>>>>>>>>>>>>> very useful to understand the code
>>>>>>>>>>>>> very easily with the proper comments especially when there are
>>>>>>>>>>>>> multiple developers working on a single project.
>>>>>>>>>>>>>
>>>>>>>>>>>>> I found some of the places where it would be great to have
>>>>>>>>>>>>> comments.
>>>>>>>>>>>>>
>>>>>>>>>>>>> - treeMenu: new tree.Tree() in a browser.js
>>>>>>>>>>>>> - tree.js (especially Tree class)
>>>>>>>>>>>>>
>>>>>>>>>>>> About the comment point I need a more clear understanding on
>>>>>>>>>>>> what kind of comments you are looking for. Because when you read the
>>>>>>>>>>>> function names you understand the intent, what they are doing. The
>>>>>>>>>>>> parameters also explain what you need to pass into them.
>>>>>>>>>>>>
>>>>>>>>>>>> If what you are looking for in these comments is the reasoning
>>>>>>>>>>>> being the change itself, then that should be present in the commit message.
>>>>>>>>>>>> Specially because this is going to be a very big patch with a very big
>>>>>>>>>>>> number of changes.
>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> On Fri, Apr 6, 2018 at 4:48 AM Murtuza Zabuawala <
>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Patch looks good and working as expected.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> I also agree with Dave, Can we please add some comments in
>>>>>>>>>>>>>>> each file which can help us to understand the flow, I'm saying because now
>>>>>>>>>>>>>>> the code is segregated in so many separate files it will be hard to keep
>>>>>>>>>>>>>>> track of the flow from one file to another when debugging.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> --
>>>>>>>>>>>>>>> Regards,
>>>>>>>>>>>>>>> Murtuza Zabuawala
>>>>>>>>>>>>>>> EnterpriseDB: http://www.enterprisedb.com
>>>>>>>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> On Thu, Apr 5, 2018 at 7:08 PM, Joao De Almeida Pereira <
>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Hi Khushboo,
>>>>>>>>>>>>>>>> Attached you can find both patches rebased
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Can you please rebase the second patch?
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida Pereira <
>>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> Attached you can find the patch that will start to
>>>>>>>>>>>>>>>>>> decouple pgAdmin from ACITree library.
>>>>>>>>>>>>>>>>>> This patch is intended to be merged after 3.0, because we
>>>>>>>>>>>>>>>>>> do not want to cause any entropy or delay the release, but we want to start
>>>>>>>>>>>>>>>>>> the discussion and show some code.
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> This job that we started is a massive tech debt chore
>>>>>>>>>>>>>>>>>> that will take some time to finalize and we would love the help of the
>>>>>>>>>>>>>>>>>> community to do it.
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> *Summary of the patch:*
>>>>>>>>>>>>>>>>>> 0001 patch:
>>>>>>>>>>>>>>>>>> - Creates a new tree that will allow us to create a
>>>>>>>>>>>>>>>>>> separation between the application and ACI Tree
>>>>>>>>>>>>>>>>>> - Creates a Fake Tree (Test double, for reference on the
>>>>>>>>>>>>>>>>>> available test doubles: https://martinfowler.
>>>>>>>>>>>>>>>>>> com/bliki/TestDouble.html) that can be used to inplace
>>>>>>>>>>>>>>>>>> to replace the ACITree and also encapsulate the new tree behavior on our
>>>>>>>>>>>>>>>>>> tests
>>>>>>>>>>>>>>>>>> - Adds tests for all the tree functionalities
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> 0002 patch:
>>>>>>>>>>>>>>>>>> - Extracts, refactors, adds tests and remove dependency
>>>>>>>>>>>>>>>>>> from ACI Tree on:
>>>>>>>>>>>>>>>>>> - getTreeNodeHierarchy
>>>>>>>>>>>>>>>>>> - on backup.js: menu_enabled, menu_enabled_server,
>>>>>>>>>>>>>>>>>> start_backup_global_server, backup_objects
>>>>>>>>>>>>>>>>>> - on datagrid.js: show_data_grid, get_panel_title,
>>>>>>>>>>>>>>>>>> show_query_tool
>>>>>>>>>>>>>>>>>> - Start using sprintf-js as Underscore.String is
>>>>>>>>>>>>>>>>>> deprecating sprintf function
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> This patch represents only 10 calls to ACITree.itemData
>>>>>>>>>>>>>>>>>> out of 176 that are spread around our code
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> *In Depth look on the process behind the patch:*
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> We started writing this patch with the idea that we need
>>>>>>>>>>>>>>>>>> to decouple pgAdmin4 from ACITree, because ACITree is no longer supported,
>>>>>>>>>>>>>>>>>> the documentation is non existent and ACITree is no longer being actively
>>>>>>>>>>>>>>>>>> developed.
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> Our process:
>>>>>>>>>>>>>>>>>> 1. We "randomly" selected a function that is part of the
>>>>>>>>>>>>>>>>>> ACITree. From this point we decided to replace that function with our own
>>>>>>>>>>>>>>>>>> version. The function that we choose was "itemData".
>>>>>>>>>>>>>>>>>> The function gives us all the "data" that a specific node
>>>>>>>>>>>>>>>>>> of the tree find.
>>>>>>>>>>>>>>>>>> Given in order to replace the tree we would need to have
>>>>>>>>>>>>>>>>>> a function that would give us the same information. We had 2 options:
>>>>>>>>>>>>>>>>>> a) Create a tree with a function called itemData
>>>>>>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>>>>>>> - At first view this was the simpler solution
>>>>>>>>>>>>>>>>>> - Would keep the status quo
>>>>>>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>>>>>>> - Not a OOP approach
>>>>>>>>>>>>>>>>>> - Not very flexible
>>>>>>>>>>>>>>>>>> b) Create a tree that would return a node given an ID
>>>>>>>>>>>>>>>>>> and then the node would be responsible for giving it's data.
>>>>>>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>>>>>>> - OOP Approach
>>>>>>>>>>>>>>>>>> - More flexible and we do not need to bring the tree
>>>>>>>>>>>>>>>>>> around, just a node
>>>>>>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>>>>>>> - Break the current status quo
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> Given these 2 options we decided to go for a more OOP
>>>>>>>>>>>>>>>>>> approach creating a Tree and a TreeNode classes, that in the future will be
>>>>>>>>>>>>>>>>>> renamed to ACITreeWrapper and TreeNode.
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> 2. After we decided on the starting point we searched for
>>>>>>>>>>>>>>>>>> occurrences of the function "itemData" and we found out that there were 303
>>>>>>>>>>>>>>>>>> occurrences of "itemData" in the code and roughly 176 calls to the function
>>>>>>>>>>>>>>>>>> itself (some of the hits were variable names).
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> 3. We selected the first file on the search and found the
>>>>>>>>>>>>>>>>>> function that was responsible for calling the itemData function.
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> 4. Extracted the function to a separate file
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> 5. Wrap this function with tests
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> 6. Refactor the function to ES6, give more declarative
>>>>>>>>>>>>>>>>>> names to variables and break the functions into smaller chunks
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> 7. When all the tests were passing we replaced ACITree
>>>>>>>>>>>>>>>>>> with our Tree
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> 8. We ensured that all tests were passing
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> 9. Remove function from the original file and use the new
>>>>>>>>>>>>>>>>>> function
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> 10. Ensure everything still works
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> 11. Find the next function and execute from step 4 until
>>>>>>>>>>>>>>>>>> all the functions are replaced, refactored and tested.
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> As you can see by the process this is a pretty huge
>>>>>>>>>>>>>>>>>> undertake, because of the number of calls to the function. This is just the
>>>>>>>>>>>>>>>>>> first step on the direction of completely isolating the ACITree so that we
>>>>>>>>>>>>>>>>>> can solve the problem with a large number of elements on the tree.
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> *What is on our radar that we need to address:*
>>>>>>>>>>>>>>>>>> - Finish the complete decoupling of the ACITree
>>>>>>>>>>>>>>>>>> - Performance of the current tree implementation
>>>>>>>>>>>>>>>>>> - Tweak the naming of the Tree class to explicitly tell
>>>>>>>>>>>>>>>>>> us this is to use only with ACITree.
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> --
>>>>>>>>> Dave Page
>>>>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>>>>> Twitter: @pgsnake
>>>>>>>>>
>>>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>
>>>>>>>>
>>>>>>>>
>>>>>>>>
>>>>>>>> --
>>>>>>>> Dave Page
>>>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>>>> Twitter: @pgsnake
>>>>>>>>
>>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>
>>>>>>>
>>>>>
>>>>
>>>
>>
>
>
> --
> Dave Page
> Blog: http://pgsnake.blogspot.com
> Twitter: @pgsnake
>
> EnterpriseDB UK: http://www.enterprisedb.com
> The Enterprise PostgreSQL Company
>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-04-30 17:57 Joao De Almeida Pereira <[email protected]>
parent: Ashesh Vashi <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Joao De Almeida Pereira @ 2018-04-30 17:57 UTC (permalink / raw)
To: Ashesh Vashi <[email protected]>; +Cc: Dave Page <[email protected]>; Anthony Emengo <[email protected]>; Khushboo Vashi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
You've lost us with the last reply. We'd love to know what we'd have to do
to get this patch committed. You mention that "some things are missing at
some places" and that "there are missing bits". Please note that this is
not a complete solution that we've offered so far. But only one step in a
grander effort to effect a more cleaner, more maintainable, more testable,
code base.
Thanks
Joao and Anthony.
On Mon, Apr 30, 2018 at 11:45 AM Ashesh Vashi <[email protected]>
wrote:
> On Mon, Apr 30, 2018 at 9:07 PM, Dave Page <[email protected]> wrote:
>
>>
>>
>> On Mon, Apr 30, 2018 at 4:33 PM, Ashesh Vashi <
>> [email protected]> wrote:
>>
>>>
>>> On Mon, Apr 30, 2018 at 8:58 PM, Anthony Emengo <[email protected]>
>>> wrote:
>>>
>>>> I was expecting a separate layer between the tree implementation, and
>>>>> aciTree adaptor.
>>>>> Please find the patch for the example.
>>>>> It will separate the two layers, and easy to replace with the new
>>>>> implementation in future.
>>>>
>>>>
>>>> In general, we want defer the separation of the layers for now. Even
>>>> though we might assume that this is the direction we want to go in. It's
>>>> simply too early to be making such an architectural leap. For right now, we
>>>> just know that we need the decoupling, but don't know what if we'd need the
>>>> 2 layers *as implemented*. The principle we're adhering to here is the
>>>> Last Responsible Moment principle, which states that you only make the
>>>> changes that you feel is necessary for the given problems you're facing:
>>>> https://blog.codinghorror.com/the-last-responsible-moment/
>>>>
>>>> I would not like to see that changes in this patch.
>>>>> I would like us to come up with the actual design about the hot
>>>>> pluggability before going in this direction.
>>>>
>>>>
>>>> In our point of view these 2 changes are not related. One thing is the
>>>> internal code organization of the application, other thing is allowing
>>>> third party to drop code in the application and it just work. These 2
>>>> should be talked separately, but the hot pluggability is not something that
>>>> will be address by this work we are doing right now.
>>>>
>>> Neither - it should be part of this change.
>>> It should be addressed separately, and discussed.
>>>
>>
>> I agree. As long as this work doesn't make the pluggability problem
>> worse, that problem should be considered separately.
>>
>> So given Anthony's comments, are you happy with this patch?
>>
> I liked the design so far.
> But - as Khushboo mentioned ealier - it is missing at some places.
> I had to read through the code to understand the execution flow for some.
>
> And, there is still a lot missing bits, and pieces to consider for commit
> in the repo.
>
> -- Thanks, Ashesh
>
>>
>>
>>>
>>> -- Thanks, Ashesh
>>>
>>>>
>>>> Anthony && Joao
>>>>
>>>> On Mon, Apr 30, 2018 at 11:03 AM, Ashesh Vashi <
>>>> [email protected]> wrote:
>>>>
>>>>>
>>>>>
>>>>>
>>>>> On Mon, Apr 30, 2018 at 8:30 PM, Ashesh Vashi <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> On Sat, Apr 28, 2018 at 3:55 AM, Joao De Almeida Pereira <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hi Hackers,
>>>>>>> As you are aware we kept on working on the patch, so we are
>>>>>>> attaching to this email a new version of the patch.
>>>>>>> This new version contains all the changes in the previous one plus
>>>>>>> more extractions of functions and refactoring of code.
>>>>>>>
>>>>>>> The objective of this patch is to create a separation between
>>>>>>> pgAdmin and the ACI Tree. We are doing this because we realized that at
>>>>>>> this point in time we have the ACI Tree all over the code of pgAdmin. I
>>>>>>> found a very interesting article that really talks about this:
>>>>>>> https://medium.freecodecamp.org/code-dependencies-are-the-devil-35ed28b556d
>>>>>>>
>>>>>>> In this patch there are some visions and ideas about the location of
>>>>>>> the code, the way to organize it and also try to pave the future for a
>>>>>>> application that is stable, easy to develop on and that can be release at a
>>>>>>> times notice.
>>>>>>>
>>>>>>> We are investing a big chunk of our time in doing this refactoring,
>>>>>>> but while doing that we also try to respond to the patches sent to the
>>>>>>> mailing list. We would like the feedback from the community because we
>>>>>>> believe this is a changing point for the application. The idea is to change
>>>>>>> the way we develop this application, instead of only correcting a bug of
>>>>>>> developing a feature, with every commit we should correct the bug or
>>>>>>> develop a feature but leave the code a little better than we found it
>>>>>>> (Refactoring, refactoring, refactoring). This is hard work but that is what
>>>>>>> the users from pgAdmin expect from this community of developers.
>>>>>>>
>>>>>>>
>>>>>>> ======
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>> It is a huge patch
>>>>>>> 86 files changed, 5492 inserts, 1840
>>>>>>> deletions
>>>>>>> and we would like to get your feedback as soon as possible, because
>>>>>>> we are continuing to work on it which means it is going to grow in size.
>>>>>>>
>>>>>>>
>>>>>>> At this point in time we still have 124 of 176 calls to the function
>>>>>>> itemData from ACITree.
>>>>>>>
>>>>>>> What does each patch contain:
>>>>>>> 0001:
>>>>>>> Very simple patch, we found out that the linter was not looking
>>>>>>> into all the javascript test files, so this patch will ensure it is
>>>>>>>
>>>>>> Committed the patch along with the regression introduced because of
>>>>>> this patch.
>>>>>>
>>>>>>>
>>>>>>> 0002:
>>>>>>> New Tree abstraction. This patch contains the new Tree that works
>>>>>>> as an adaptor for ACI Tree and is going to be used on all the extractions
>>>>>>> that we are doing.
>>>>>>>
>>>>>>
>>>>>> I was expecting a separate layer between the tree implementation, and
>>>>>> aciTree adaptor.
>>>>>> Please find the patch for the example.
>>>>>>
>>>>>> It will separate the two layers, and easy to replace with the new
>>>>>> implemenation in future.
>>>>>>
>>>>>
>>>>> Oops forgot to attach the patch.
>>>>> Please find the patch attached.
>>>>>
>>>>> -- Thanks, Ashesh
>>>>>
>>>>>>
>>>>>>> 0003:
>>>>>>> Code that extracts, wrap with tests and replace ACI Tree
>>>>>>> invocations.
>>>>>>>
>>>>>> There are many small cases left in the patches.
>>>>>> Hence - I would like to know the TODO list created by you.
>>>>>>
>>>>>> e.g. When we remove any of the object from the database server, we're
>>>>>> not yet removing the respective node from the new implementation, and its
>>>>>> children.
>>>>>>
>>>>>>>
>>>>>>> We start creating new pattern for the location of Javascript files
>>>>>>> and their structure.
>>>>>>>
>>>>>> I would not like to see that changes in this patch.
>>>>>> I would like us to come up with the actual design about the hot
>>>>>> pluggability before going in this direction.
>>>>>>
>>>>>>> Create patterns for creation of dialogs (backup and restore)
>>>>>>>
>>>>>> It's better - we don't change the directory structure at the moment.
>>>>>>
>>>>>> I am not against dividing the big javascript files in small chunks,
>>>>>> but - I would like us to discuss first about the hot plugins design first.
>>>>>>
>>>>>> -- Thanks, Ashesh
>>>>>>
>>>>>>>
>>>>>>>
>>>>>>
>>>>>>>
>>>>>>> Thanks
>>>>>>> Joao
>>>>>>>
>>>>>>>
>>>>>>> On Fri, Apr 27, 2018 at 5:34 AM Ashesh Vashi <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> I have quite a few comments for the patch.
>>>>>>>> I will send them soon.
>>>>>>>>
>>>>>>>> On Fri, Apr 27, 2018, 14:45 Dave Page <[email protected]> wrote:
>>>>>>>>
>>>>>>>>> How is your work on this going Ashesh? Will you be committing
>>>>>>>>> today?
>>>>>>>>>
>>>>>>>>> On Wed, Apr 25, 2018 at 8:52 AM, Dave Page <[email protected]>
>>>>>>>>> wrote:
>>>>>>>>>
>>>>>>>>>> Ashesh; you had agreed to work on this early this week. Please
>>>>>>>>>> ensure you do so today.
>>>>>>>>>>
>>>>>>>>>> Thanks.
>>>>>>>>>>
>>>>>>>>>> On Tue, Apr 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>
>>>>>>>>>>> Can someone review and merge this patch?
>>>>>>>>>>>
>>>>>>>>>>> Thanks
>>>>>>>>>>> Joao
>>>>>>>>>>>
>>>>>>>>>>> On Wed, Apr 18, 2018 at 10:23 AM Joao De Almeida Pereira <
>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>> Any other comment about this patch?
>>>>>>>>>>>>
>>>>>>>>>>>> Thanks
>>>>>>>>>>>> Joao
>>>>>>>>>>>>
>>>>>>>>>>>> On Tue, Apr 10, 2018 at 12:00 PM Joao De Almeida Pereira <
>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>
>>>>>>>>>>>>> Hello Khushboo
>>>>>>>>>>>>>
>>>>>>>>>>>>> On Mon, Apr 9, 2018 at 1:59 AM Khushboo Vashi <
>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>
>>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> I have reviewed your patch and have some suggestions.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> On Sat, Apr 7, 2018 at 12:43 AM, Joao De Almeida Pereira <
>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Hello Murtuza/Dave,
>>>>>>>>>>>>>>> Yes now the extracted functions are spread into different
>>>>>>>>>>>>>>> files. The intent would be to make the files as small as possible, and also
>>>>>>>>>>>>>>> to group and name them in a way that would be easy to understand what each
>>>>>>>>>>>>>>> file is doing without the need of opening it.
>>>>>>>>>>>>>>> As a example:
>>>>>>>>>>>>>>> static/js/backup will contain all the backup related
>>>>>>>>>>>>>>> functionality inside of this folder we can see the file:
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>> menu_utils.js At this moment in time we decided to group all
>>>>>>>>>>>>>>> the functions that are related to the menu, but we can split that also if
>>>>>>>>>>>>>>> we believe it is easier to see.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>> It's really very good to see the separated code for backup
>>>>>>>>>>>>>> module. As we have done for backup, we would like do it for other PG
>>>>>>>>>>>>>> utilities like restore, maintenance etc.
>>>>>>>>>>>>>> Considering this, we should separate the code in a way that
>>>>>>>>>>>>>> some of the common functionalities can be used for other modules like menu
>>>>>>>>>>>>>> (as you have mentioned above), dialogue factory etc.
>>>>>>>>>>>>>> Also, I think these functionalities should be in their
>>>>>>>>>>>>>> respective static folder instead of pgadmin/static.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> About the location of the files. The move of the files to
>>>>>>>>>>>>> pgadmin/static/js was made on purpose in order to clearly separate
>>>>>>>>>>>>> Javascript from python code.
>>>>>>>>>>>>> The rational behind it was
>>>>>>>>>>>>> - Create a clear separation between the backend and frontend
>>>>>>>>>>>>> - Having Javascript code concentrated in a single place,
>>>>>>>>>>>>> hopefully, will encourage to developers to look for a functionality, that
>>>>>>>>>>>>> is already implemented in another modules, because they are right there.
>>>>>>>>>>>>> (When we started this journey we realized that the 'nodes' have a big
>>>>>>>>>>>>> groups of code that could be shared, but because the Javascript is spread
>>>>>>>>>>>>> everywhere it is much harder to look for it)
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> There are some drawbacks of this separation:
>>>>>>>>>>>>> - When creating a new module we will need to put the
>>>>>>>>>>>>> javascript in a separate location from the backend code
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> static/js/datagrid folder contains all the datagrid related
>>>>>>>>>>>>>>> functionality
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Same as backup module, this should be in it's respective
>>>>>>>>>>>>>> static/js folder.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Inside of the folder we can see the files:
>>>>>>>>>>>>>>> get_panel_title.js is responsible for retrieving the name
>>>>>>>>>>>>>>> of the panel
>>>>>>>>>>>>>>> show_data.js is responsible for showing the datagrid
>>>>>>>>>>>>>>> show_query_tool.js is responsible for showing the query tool
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Does this structure make sense?
>>>>>>>>>>>>>>> Can you give an example of a comment that you think is
>>>>>>>>>>>>>>> missing and that could help?
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> As a personal note, unless the algorithm is very obscure or
>>>>>>>>>>>>>>> very complicated, I believe that if the code needs comments it is a signal
>>>>>>>>>>>>>>> that something needs to change in terms of naming, structure of the part in
>>>>>>>>>>>>>>> question. This being said, I am open to add some comments that might help
>>>>>>>>>>>>>>> people.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>> You are right, with the help of naming convention and
>>>>>>>>>>>>>> structure of the code, any one can get the idea about the code. But it is
>>>>>>>>>>>>>> very useful to understand the code
>>>>>>>>>>>>>> very easily with the proper comments especially when there
>>>>>>>>>>>>>> are multiple developers working on a single project.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> I found some of the places where it would be great to have
>>>>>>>>>>>>>> comments.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> - treeMenu: new tree.Tree() in a browser.js
>>>>>>>>>>>>>> - tree.js (especially Tree class)
>>>>>>>>>>>>>>
>>>>>>>>>>>>> About the comment point I need a more clear understanding on
>>>>>>>>>>>>> what kind of comments you are looking for. Because when you read the
>>>>>>>>>>>>> function names you understand the intent, what they are doing. The
>>>>>>>>>>>>> parameters also explain what you need to pass into them.
>>>>>>>>>>>>>
>>>>>>>>>>>>> If what you are looking for in these comments is the reasoning
>>>>>>>>>>>>> being the change itself, then that should be present in the commit message.
>>>>>>>>>>>>> Specially because this is going to be a very big patch with a very big
>>>>>>>>>>>>> number of changes.
>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> On Fri, Apr 6, 2018 at 4:48 AM Murtuza Zabuawala <
>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Patch looks good and working as expected.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> I also agree with Dave, Can we please add some comments in
>>>>>>>>>>>>>>>> each file which can help us to understand the flow, I'm saying because now
>>>>>>>>>>>>>>>> the code is segregated in so many separate files it will be hard to keep
>>>>>>>>>>>>>>>> track of the flow from one file to another when debugging.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> --
>>>>>>>>>>>>>>>> Regards,
>>>>>>>>>>>>>>>> Murtuza Zabuawala
>>>>>>>>>>>>>>>> EnterpriseDB: http://www.enterprisedb.com
>>>>>>>>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> On Thu, Apr 5, 2018 at 7:08 PM, Joao De Almeida Pereira <
>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Hi Khushboo,
>>>>>>>>>>>>>>>>> Attached you can find both patches rebased
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
>>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> Can you please rebase the second patch?
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida Pereira
>>>>>>>>>>>>>>>>>> <[email protected]> wrote:
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> Attached you can find the patch that will start to
>>>>>>>>>>>>>>>>>>> decouple pgAdmin from ACITree library.
>>>>>>>>>>>>>>>>>>> This patch is intended to be merged after 3.0, because
>>>>>>>>>>>>>>>>>>> we do not want to cause any entropy or delay the release, but we want to
>>>>>>>>>>>>>>>>>>> start the discussion and show some code.
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> This job that we started is a massive tech debt chore
>>>>>>>>>>>>>>>>>>> that will take some time to finalize and we would love the help of the
>>>>>>>>>>>>>>>>>>> community to do it.
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> *Summary of the patch:*
>>>>>>>>>>>>>>>>>>> 0001 patch:
>>>>>>>>>>>>>>>>>>> - Creates a new tree that will allow us to create a
>>>>>>>>>>>>>>>>>>> separation between the application and ACI Tree
>>>>>>>>>>>>>>>>>>> - Creates a Fake Tree (Test double, for reference on
>>>>>>>>>>>>>>>>>>> the available test doubles:
>>>>>>>>>>>>>>>>>>> https://martinfowler.com/bliki/TestDouble.html) that
>>>>>>>>>>>>>>>>>>> can be used to inplace to replace the ACITree and also encapsulate the new
>>>>>>>>>>>>>>>>>>> tree behavior on our tests
>>>>>>>>>>>>>>>>>>> - Adds tests for all the tree functionalities
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> 0002 patch:
>>>>>>>>>>>>>>>>>>> - Extracts, refactors, adds tests and remove dependency
>>>>>>>>>>>>>>>>>>> from ACI Tree on:
>>>>>>>>>>>>>>>>>>> - getTreeNodeHierarchy
>>>>>>>>>>>>>>>>>>> - on backup.js: menu_enabled, menu_enabled_server,
>>>>>>>>>>>>>>>>>>> start_backup_global_server, backup_objects
>>>>>>>>>>>>>>>>>>> - on datagrid.js: show_data_grid, get_panel_title,
>>>>>>>>>>>>>>>>>>> show_query_tool
>>>>>>>>>>>>>>>>>>> - Start using sprintf-js as Underscore.String is
>>>>>>>>>>>>>>>>>>> deprecating sprintf function
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> This patch represents only 10 calls to ACITree.itemData
>>>>>>>>>>>>>>>>>>> out of 176 that are spread around our code
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> *In Depth look on the process behind the patch:*
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> We started writing this patch with the idea that we need
>>>>>>>>>>>>>>>>>>> to decouple pgAdmin4 from ACITree, because ACITree is no longer supported,
>>>>>>>>>>>>>>>>>>> the documentation is non existent and ACITree is no longer being actively
>>>>>>>>>>>>>>>>>>> developed.
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> Our process:
>>>>>>>>>>>>>>>>>>> 1. We "randomly" selected a function that is part of the
>>>>>>>>>>>>>>>>>>> ACITree. From this point we decided to replace that function with our own
>>>>>>>>>>>>>>>>>>> version. The function that we choose was "itemData".
>>>>>>>>>>>>>>>>>>> The function gives us all the "data" that a specific
>>>>>>>>>>>>>>>>>>> node of the tree find.
>>>>>>>>>>>>>>>>>>> Given in order to replace the tree we would need to have
>>>>>>>>>>>>>>>>>>> a function that would give us the same information. We had 2 options:
>>>>>>>>>>>>>>>>>>> a) Create a tree with a function called itemData
>>>>>>>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>>>>>>>> - At first view this was the simpler solution
>>>>>>>>>>>>>>>>>>> - Would keep the status quo
>>>>>>>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>>>>>>>> - Not a OOP approach
>>>>>>>>>>>>>>>>>>> - Not very flexible
>>>>>>>>>>>>>>>>>>> b) Create a tree that would return a node given an ID
>>>>>>>>>>>>>>>>>>> and then the node would be responsible for giving it's data.
>>>>>>>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>>>>>>>> - OOP Approach
>>>>>>>>>>>>>>>>>>> - More flexible and we do not need to bring the tree
>>>>>>>>>>>>>>>>>>> around, just a node
>>>>>>>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>>>>>>>> - Break the current status quo
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> Given these 2 options we decided to go for a more OOP
>>>>>>>>>>>>>>>>>>> approach creating a Tree and a TreeNode classes, that in the future will be
>>>>>>>>>>>>>>>>>>> renamed to ACITreeWrapper and TreeNode.
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> 2. After we decided on the starting point we searched
>>>>>>>>>>>>>>>>>>> for occurrences of the function "itemData" and we found out that there were
>>>>>>>>>>>>>>>>>>> 303 occurrences of "itemData" in the code and roughly 176 calls to the
>>>>>>>>>>>>>>>>>>> function itself (some of the hits were variable names).
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> 3. We selected the first file on the search and found
>>>>>>>>>>>>>>>>>>> the function that was responsible for calling the itemData function.
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> 4. Extracted the function to a separate file
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> 5. Wrap this function with tests
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> 6. Refactor the function to ES6, give more declarative
>>>>>>>>>>>>>>>>>>> names to variables and break the functions into smaller chunks
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> 7. When all the tests were passing we replaced ACITree
>>>>>>>>>>>>>>>>>>> with our Tree
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> 8. We ensured that all tests were passing
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> 9. Remove function from the original file and use the
>>>>>>>>>>>>>>>>>>> new function
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> 10. Ensure everything still works
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> 11. Find the next function and execute from step 4 until
>>>>>>>>>>>>>>>>>>> all the functions are replaced, refactored and tested.
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> As you can see by the process this is a pretty huge
>>>>>>>>>>>>>>>>>>> undertake, because of the number of calls to the function. This is just the
>>>>>>>>>>>>>>>>>>> first step on the direction of completely isolating the ACITree so that we
>>>>>>>>>>>>>>>>>>> can solve the problem with a large number of elements on the tree.
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> *What is on our radar that we need to address:*
>>>>>>>>>>>>>>>>>>> - Finish the complete decoupling of the ACITree
>>>>>>>>>>>>>>>>>>> - Performance of the current tree implementation
>>>>>>>>>>>>>>>>>>> - Tweak the naming of the Tree class to explicitly tell
>>>>>>>>>>>>>>>>>>> us this is to use only with ACITree.
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> --
>>>>>>>>>> Dave Page
>>>>>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>>>>>> Twitter: @pgsnake
>>>>>>>>>>
>>>>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>>
>>>>>>>>>
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> --
>>>>>>>>> Dave Page
>>>>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>>>>> Twitter: @pgsnake
>>>>>>>>>
>>>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>
>>>>>>>>
>>>>>>
>>>>>
>>>>
>>>
>>
>>
>> --
>> Dave Page
>> Blog: http://pgsnake.blogspot.com
>> Twitter: @pgsnake
>>
>> EnterpriseDB UK: http://www.enterprisedb.com
>> The Enterprise PostgreSQL Company
>>
>
>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-02 05:30 Ashesh Vashi <[email protected]>
parent: Joao De Almeida Pereira <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Ashesh Vashi @ 2018-05-02 05:30 UTC (permalink / raw)
To: Joao De Almeida Pereira <[email protected]>; +Cc: Dave Page <[email protected]>; Anthony Emengo <[email protected]>; Khushboo Vashi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
On Mon, Apr 30, 2018 at 11:27 PM, Joao De Almeida Pereira <
[email protected]> wrote:
> You've lost us with the last reply. We'd love to know what we'd have to do
> to get this patch committed. You mention that "some things are missing at
> some places" and that "there are missing bits".
>
I've already mentioned few of them in the earlier mails about them.
> Please note that this is not a complete solution that we've offered so far.
>
I know and, hence - asked for your understanding of the code (and, asked
for the TODOs.)
> But only one step in a grander effort to effect a more cleaner, more
> maintainable, more testable, code base.
>
Yes - we all want cleaner, maintainable, and testable code base.
As per my understading comments are required for better understanding, and
makes the code more maintainable.
But - if we're going to change everything in one go, it is difficult to
understand the changes.
Let's avoid mix match multiple things in a single patch. And, concerntrate
one functionality at a time.
-- Thanks, Ashesh
>
> Thanks
> Joao and Anthony.
>
> On Mon, Apr 30, 2018 at 11:45 AM Ashesh Vashi <
> [email protected]> wrote:
>
>> On Mon, Apr 30, 2018 at 9:07 PM, Dave Page <[email protected]> wrote:
>>
>>>
>>>
>>> On Mon, Apr 30, 2018 at 4:33 PM, Ashesh Vashi <
>>> [email protected]> wrote:
>>>
>>>>
>>>> On Mon, Apr 30, 2018 at 8:58 PM, Anthony Emengo <[email protected]>
>>>> wrote:
>>>>
>>>>> I was expecting a separate layer between the tree implementation, and
>>>>>> aciTree adaptor.
>>>>>> Please find the patch for the example.
>>>>>> It will separate the two layers, and easy to replace with the new
>>>>>> implementation in future.
>>>>>
>>>>>
>>>>> In general, we want defer the separation of the layers for now. Even
>>>>> though we might assume that this is the direction we want to go in. It's
>>>>> simply too early to be making such an architectural leap. For right now, we
>>>>> just know that we need the decoupling, but don't know what if we'd need the
>>>>> 2 layers *as implemented*. The principle we're adhering to here is
>>>>> the Last Responsible Moment principle, which states that you only make the
>>>>> changes that you feel is necessary for the given problems you're facing:
>>>>> https://blog.codinghorror.com/the-last-responsible-moment/
>>>>>
>>>>> I would not like to see that changes in this patch.
>>>>>> I would like us to come up with the actual design about the hot
>>>>>> pluggability before going in this direction.
>>>>>
>>>>>
>>>>> In our point of view these 2 changes are not related. One thing is the
>>>>> internal code organization of the application, other thing is allowing
>>>>> third party to drop code in the application and it just work. These 2
>>>>> should be talked separately, but the hot pluggability is not something that
>>>>> will be address by this work we are doing right now.
>>>>>
>>>> Neither - it should be part of this change.
>>>> It should be addressed separately, and discussed.
>>>>
>>>
>>> I agree. As long as this work doesn't make the pluggability problem
>>> worse, that problem should be considered separately.
>>>
>>> So given Anthony's comments, are you happy with this patch?
>>>
>> I liked the design so far.
>> But - as Khushboo mentioned ealier - it is missing at some places.
>> I had to read through the code to understand the execution flow for some.
>>
>> And, there is still a lot missing bits, and pieces to consider for commit
>> in the repo.
>>
>> -- Thanks, Ashesh
>>
>>>
>>>
>>>>
>>>> -- Thanks, Ashesh
>>>>
>>>>>
>>>>> Anthony && Joao
>>>>>
>>>>> On Mon, Apr 30, 2018 at 11:03 AM, Ashesh Vashi <
>>>>> [email protected]> wrote:
>>>>>
>>>>>>
>>>>>>
>>>>>>
>>>>>> On Mon, Apr 30, 2018 at 8:30 PM, Ashesh Vashi <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> On Sat, Apr 28, 2018 at 3:55 AM, Joao De Almeida Pereira <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hi Hackers,
>>>>>>>> As you are aware we kept on working on the patch, so we are
>>>>>>>> attaching to this email a new version of the patch.
>>>>>>>> This new version contains all the changes in the previous one plus
>>>>>>>> more extractions of functions and refactoring of code.
>>>>>>>>
>>>>>>>> The objective of this patch is to create a separation between
>>>>>>>> pgAdmin and the ACI Tree. We are doing this because we realized that at
>>>>>>>> this point in time we have the ACI Tree all over the code of pgAdmin. I
>>>>>>>> found a very interesting article that really talks about this:
>>>>>>>> https://medium.freecodecamp.org/code-dependencies-are-the-
>>>>>>>> devil-35ed28b556d
>>>>>>>>
>>>>>>>> In this patch there are some visions and ideas about the location
>>>>>>>> of the code, the way to organize it and also try to pave the future for a
>>>>>>>> application that is stable, easy to develop on and that can be release at a
>>>>>>>> times notice.
>>>>>>>>
>>>>>>>> We are investing a big chunk of our time in doing this refactoring,
>>>>>>>> but while doing that we also try to respond to the patches sent to the
>>>>>>>> mailing list. We would like the feedback from the community because we
>>>>>>>> believe this is a changing point for the application. The idea is to change
>>>>>>>> the way we develop this application, instead of only correcting a bug of
>>>>>>>> developing a feature, with every commit we should correct the bug or
>>>>>>>> develop a feature but leave the code a little better than we found it
>>>>>>>> (Refactoring, refactoring, refactoring). This is hard work but that is what
>>>>>>>> the users from pgAdmin expect from this community of developers.
>>>>>>>>
>>>>>>>>
>>>>>>>> ======
>>>>>>>>
>>>>>>>>
>>>>>>>>
>>>>>>>> It is a huge patch
>>>>>>>> 86 files changed, 5492 inserts, 1840
>>>>>>>> deletions
>>>>>>>> and we would like to get your feedback as soon as possible, because
>>>>>>>> we are continuing to work on it which means it is going to grow in size.
>>>>>>>>
>>>>>>>>
>>>>>>>> At this point in time we still have 124 of 176 calls to the
>>>>>>>> function itemData from ACITree.
>>>>>>>>
>>>>>>>> What does each patch contain:
>>>>>>>> 0001:
>>>>>>>> Very simple patch, we found out that the linter was not looking
>>>>>>>> into all the javascript test files, so this patch will ensure it is
>>>>>>>>
>>>>>>> Committed the patch along with the regression introduced because of
>>>>>>> this patch.
>>>>>>>
>>>>>>>>
>>>>>>>> 0002:
>>>>>>>> New Tree abstraction. This patch contains the new Tree that works
>>>>>>>> as an adaptor for ACI Tree and is going to be used on all the extractions
>>>>>>>> that we are doing.
>>>>>>>>
>>>>>>>
>>>>>>> I was expecting a separate layer between the tree implementation,
>>>>>>> and aciTree adaptor.
>>>>>>> Please find the patch for the example.
>>>>>>>
>>>>>>> It will separate the two layers, and easy to replace with the new
>>>>>>> implemenation in future.
>>>>>>>
>>>>>>
>>>>>> Oops forgot to attach the patch.
>>>>>> Please find the patch attached.
>>>>>>
>>>>>> -- Thanks, Ashesh
>>>>>>
>>>>>>>
>>>>>>>> 0003:
>>>>>>>> Code that extracts, wrap with tests and replace ACI Tree
>>>>>>>> invocations.
>>>>>>>>
>>>>>>> There are many small cases left in the patches.
>>>>>>> Hence - I would like to know the TODO list created by you.
>>>>>>>
>>>>>>> e.g. When we remove any of the object from the database server,
>>>>>>> we're not yet removing the respective node from the new implementation, and
>>>>>>> its children.
>>>>>>>
>>>>>>>>
>>>>>>>> We start creating new pattern for the location of Javascript
>>>>>>>> files and their structure.
>>>>>>>>
>>>>>>> I would not like to see that changes in this patch.
>>>>>>> I would like us to come up with the actual design about the hot
>>>>>>> pluggability before going in this direction.
>>>>>>>
>>>>>>>> Create patterns for creation of dialogs (backup and restore)
>>>>>>>>
>>>>>>> It's better - we don't change the directory structure at the moment.
>>>>>>>
>>>>>>> I am not against dividing the big javascript files in small chunks,
>>>>>>> but - I would like us to discuss first about the hot plugins design first.
>>>>>>>
>>>>>>> -- Thanks, Ashesh
>>>>>>>
>>>>>>>>
>>>>>>>>
>>>>>>>
>>>>>>>>
>>>>>>>> Thanks
>>>>>>>> Joao
>>>>>>>>
>>>>>>>>
>>>>>>>> On Fri, Apr 27, 2018 at 5:34 AM Ashesh Vashi <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> I have quite a few comments for the patch.
>>>>>>>>> I will send them soon.
>>>>>>>>>
>>>>>>>>> On Fri, Apr 27, 2018, 14:45 Dave Page <[email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> How is your work on this going Ashesh? Will you be committing
>>>>>>>>>> today?
>>>>>>>>>>
>>>>>>>>>> On Wed, Apr 25, 2018 at 8:52 AM, Dave Page <[email protected]>
>>>>>>>>>> wrote:
>>>>>>>>>>
>>>>>>>>>>> Ashesh; you had agreed to work on this early this week. Please
>>>>>>>>>>> ensure you do so today.
>>>>>>>>>>>
>>>>>>>>>>> Thanks.
>>>>>>>>>>>
>>>>>>>>>>> On Tue, Apr 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>
>>>>>>>>>>>> Can someone review and merge this patch?
>>>>>>>>>>>>
>>>>>>>>>>>> Thanks
>>>>>>>>>>>> Joao
>>>>>>>>>>>>
>>>>>>>>>>>> On Wed, Apr 18, 2018 at 10:23 AM Joao De Almeida Pereira <
>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>
>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>> Any other comment about this patch?
>>>>>>>>>>>>>
>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>
>>>>>>>>>>>>> On Tue, Apr 10, 2018 at 12:00 PM Joao De Almeida Pereira <
>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>
>>>>>>>>>>>>>> Hello Khushboo
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> On Mon, Apr 9, 2018 at 1:59 AM Khushboo Vashi <
>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> I have reviewed your patch and have some suggestions.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> On Sat, Apr 7, 2018 at 12:43 AM, Joao De Almeida Pereira <
>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Hello Murtuza/Dave,
>>>>>>>>>>>>>>>> Yes now the extracted functions are spread into different
>>>>>>>>>>>>>>>> files. The intent would be to make the files as small as possible, and also
>>>>>>>>>>>>>>>> to group and name them in a way that would be easy to understand what each
>>>>>>>>>>>>>>>> file is doing without the need of opening it.
>>>>>>>>>>>>>>>> As a example:
>>>>>>>>>>>>>>>> static/js/backup will contain all the backup related
>>>>>>>>>>>>>>>> functionality inside of this folder we can see the file:
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> menu_utils.js At this moment in time we decided to group
>>>>>>>>>>>>>>>> all the functions that are related to the menu, but we can split that also
>>>>>>>>>>>>>>>> if we believe it is easier to see.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> It's really very good to see the separated code for backup
>>>>>>>>>>>>>>> module. As we have done for backup, we would like do it for other PG
>>>>>>>>>>>>>>> utilities like restore, maintenance etc.
>>>>>>>>>>>>>>> Considering this, we should separate the code in a way that
>>>>>>>>>>>>>>> some of the common functionalities can be used for other modules like menu
>>>>>>>>>>>>>>> (as you have mentioned above), dialogue factory etc.
>>>>>>>>>>>>>>> Also, I think these functionalities should be in their
>>>>>>>>>>>>>>> respective static folder instead of pgadmin/static.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> About the location of the files. The move of the files to
>>>>>>>>>>>>>> pgadmin/static/js was made on purpose in order to clearly separate
>>>>>>>>>>>>>> Javascript from python code.
>>>>>>>>>>>>>> The rational behind it was
>>>>>>>>>>>>>> - Create a clear separation between the backend and frontend
>>>>>>>>>>>>>> - Having Javascript code concentrated in a single place,
>>>>>>>>>>>>>> hopefully, will encourage to developers to look for a functionality, that
>>>>>>>>>>>>>> is already implemented in another modules, because they are right there.
>>>>>>>>>>>>>> (When we started this journey we realized that the 'nodes' have a big
>>>>>>>>>>>>>> groups of code that could be shared, but because the Javascript is spread
>>>>>>>>>>>>>> everywhere it is much harder to look for it)
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> There are some drawbacks of this separation:
>>>>>>>>>>>>>> - When creating a new module we will need to put the
>>>>>>>>>>>>>> javascript in a separate location from the backend code
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> static/js/datagrid folder contains all the datagrid
>>>>>>>>>>>>>>>> related functionality
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Same as backup module, this should be in it's respective
>>>>>>>>>>>>>>> static/js folder.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Inside of the folder we can see the files:
>>>>>>>>>>>>>>>> get_panel_title.js is responsible for retrieving the name
>>>>>>>>>>>>>>>> of the panel
>>>>>>>>>>>>>>>> show_data.js is responsible for showing the datagrid
>>>>>>>>>>>>>>>> show_query_tool.js is responsible for showing the query
>>>>>>>>>>>>>>>> tool
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Does this structure make sense?
>>>>>>>>>>>>>>>> Can you give an example of a comment that you think is
>>>>>>>>>>>>>>>> missing and that could help?
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> As a personal note, unless the algorithm is very obscure or
>>>>>>>>>>>>>>>> very complicated, I believe that if the code needs comments it is a signal
>>>>>>>>>>>>>>>> that something needs to change in terms of naming, structure of the part in
>>>>>>>>>>>>>>>> question. This being said, I am open to add some comments that might help
>>>>>>>>>>>>>>>> people.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> You are right, with the help of naming convention and
>>>>>>>>>>>>>>> structure of the code, any one can get the idea about the code. But it is
>>>>>>>>>>>>>>> very useful to understand the code
>>>>>>>>>>>>>>> very easily with the proper comments especially when there
>>>>>>>>>>>>>>> are multiple developers working on a single project.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> I found some of the places where it would be great to have
>>>>>>>>>>>>>>> comments.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> - treeMenu: new tree.Tree() in a browser.js
>>>>>>>>>>>>>>> - tree.js (especially Tree class)
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>> About the comment point I need a more clear understanding on
>>>>>>>>>>>>>> what kind of comments you are looking for. Because when you read the
>>>>>>>>>>>>>> function names you understand the intent, what they are doing. The
>>>>>>>>>>>>>> parameters also explain what you need to pass into them.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> If what you are looking for in these comments is the
>>>>>>>>>>>>>> reasoning being the change itself, then that should be present in the
>>>>>>>>>>>>>> commit message. Specially because this is going to be a very big patch with
>>>>>>>>>>>>>> a very big number of changes.
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> On Fri, Apr 6, 2018 at 4:48 AM Murtuza Zabuawala <
>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Patch looks good and working as expected.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> I also agree with Dave, Can we please add some comments in
>>>>>>>>>>>>>>>>> each file which can help us to understand the flow, I'm saying because now
>>>>>>>>>>>>>>>>> the code is segregated in so many separate files it will be hard to keep
>>>>>>>>>>>>>>>>> track of the flow from one file to another when debugging.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> --
>>>>>>>>>>>>>>>>> Regards,
>>>>>>>>>>>>>>>>> Murtuza Zabuawala
>>>>>>>>>>>>>>>>> EnterpriseDB: http://www.enterprisedb.com
>>>>>>>>>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> On Thu, Apr 5, 2018 at 7:08 PM, Joao De Almeida Pereira <
>>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> Hi Khushboo,
>>>>>>>>>>>>>>>>>> Attached you can find both patches rebased
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
>>>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> Can you please rebase the second patch?
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida Pereira
>>>>>>>>>>>>>>>>>>> <[email protected]> wrote:
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> Attached you can find the patch that will start to
>>>>>>>>>>>>>>>>>>>> decouple pgAdmin from ACITree library.
>>>>>>>>>>>>>>>>>>>> This patch is intended to be merged after 3.0, because
>>>>>>>>>>>>>>>>>>>> we do not want to cause any entropy or delay the release, but we want to
>>>>>>>>>>>>>>>>>>>> start the discussion and show some code.
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> This job that we started is a massive tech debt chore
>>>>>>>>>>>>>>>>>>>> that will take some time to finalize and we would love the help of the
>>>>>>>>>>>>>>>>>>>> community to do it.
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> *Summary of the patch:*
>>>>>>>>>>>>>>>>>>>> 0001 patch:
>>>>>>>>>>>>>>>>>>>> - Creates a new tree that will allow us to create a
>>>>>>>>>>>>>>>>>>>> separation between the application and ACI Tree
>>>>>>>>>>>>>>>>>>>> - Creates a Fake Tree (Test double, for reference on
>>>>>>>>>>>>>>>>>>>> the available test doubles: https://martinfowler.
>>>>>>>>>>>>>>>>>>>> com/bliki/TestDouble.html) that can be used to inplace
>>>>>>>>>>>>>>>>>>>> to replace the ACITree and also encapsulate the new tree behavior on our
>>>>>>>>>>>>>>>>>>>> tests
>>>>>>>>>>>>>>>>>>>> - Adds tests for all the tree functionalities
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> 0002 patch:
>>>>>>>>>>>>>>>>>>>> - Extracts, refactors, adds tests and remove
>>>>>>>>>>>>>>>>>>>> dependency from ACI Tree on:
>>>>>>>>>>>>>>>>>>>> - getTreeNodeHierarchy
>>>>>>>>>>>>>>>>>>>> - on backup.js: menu_enabled, menu_enabled_server,
>>>>>>>>>>>>>>>>>>>> start_backup_global_server, backup_objects
>>>>>>>>>>>>>>>>>>>> - on datagrid.js: show_data_grid, get_panel_title,
>>>>>>>>>>>>>>>>>>>> show_query_tool
>>>>>>>>>>>>>>>>>>>> - Start using sprintf-js as Underscore.String is
>>>>>>>>>>>>>>>>>>>> deprecating sprintf function
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> This patch represents only 10 calls to ACITree.itemData
>>>>>>>>>>>>>>>>>>>> out of 176 that are spread around our code
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> *In Depth look on the process behind the patch:*
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> We started writing this patch with the idea that we
>>>>>>>>>>>>>>>>>>>> need to decouple pgAdmin4 from ACITree, because ACITree is no longer
>>>>>>>>>>>>>>>>>>>> supported, the documentation is non existent and ACITree is no longer being
>>>>>>>>>>>>>>>>>>>> actively developed.
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> Our process:
>>>>>>>>>>>>>>>>>>>> 1. We "randomly" selected a function that is part of
>>>>>>>>>>>>>>>>>>>> the ACITree. From this point we decided to replace that function with our
>>>>>>>>>>>>>>>>>>>> own version. The function that we choose was "itemData".
>>>>>>>>>>>>>>>>>>>> The function gives us all the "data" that a specific
>>>>>>>>>>>>>>>>>>>> node of the tree find.
>>>>>>>>>>>>>>>>>>>> Given in order to replace the tree we would need to
>>>>>>>>>>>>>>>>>>>> have a function that would give us the same information. We had 2 options:
>>>>>>>>>>>>>>>>>>>> a) Create a tree with a function called itemData
>>>>>>>>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>>>>>>>>> - At first view this was the simpler solution
>>>>>>>>>>>>>>>>>>>> - Would keep the status quo
>>>>>>>>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>>>>>>>>> - Not a OOP approach
>>>>>>>>>>>>>>>>>>>> - Not very flexible
>>>>>>>>>>>>>>>>>>>> b) Create a tree that would return a node given an ID
>>>>>>>>>>>>>>>>>>>> and then the node would be responsible for giving it's data.
>>>>>>>>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>>>>>>>>> - OOP Approach
>>>>>>>>>>>>>>>>>>>> - More flexible and we do not need to bring the tree
>>>>>>>>>>>>>>>>>>>> around, just a node
>>>>>>>>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>>>>>>>>> - Break the current status quo
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> Given these 2 options we decided to go for a more OOP
>>>>>>>>>>>>>>>>>>>> approach creating a Tree and a TreeNode classes, that in the future will be
>>>>>>>>>>>>>>>>>>>> renamed to ACITreeWrapper and TreeNode.
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> 2. After we decided on the starting point we searched
>>>>>>>>>>>>>>>>>>>> for occurrences of the function "itemData" and we found out that there were
>>>>>>>>>>>>>>>>>>>> 303 occurrences of "itemData" in the code and roughly 176 calls to the
>>>>>>>>>>>>>>>>>>>> function itself (some of the hits were variable names).
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> 3. We selected the first file on the search and found
>>>>>>>>>>>>>>>>>>>> the function that was responsible for calling the itemData function.
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> 4. Extracted the function to a separate file
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> 5. Wrap this function with tests
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> 6. Refactor the function to ES6, give more declarative
>>>>>>>>>>>>>>>>>>>> names to variables and break the functions into smaller chunks
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> 7. When all the tests were passing we replaced ACITree
>>>>>>>>>>>>>>>>>>>> with our Tree
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> 8. We ensured that all tests were passing
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> 9. Remove function from the original file and use the
>>>>>>>>>>>>>>>>>>>> new function
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> 10. Ensure everything still works
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> 11. Find the next function and execute from step 4
>>>>>>>>>>>>>>>>>>>> until all the functions are replaced, refactored and tested.
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> As you can see by the process this is a pretty huge
>>>>>>>>>>>>>>>>>>>> undertake, because of the number of calls to the function. This is just the
>>>>>>>>>>>>>>>>>>>> first step on the direction of completely isolating the ACITree so that we
>>>>>>>>>>>>>>>>>>>> can solve the problem with a large number of elements on the tree.
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> *What is on our radar that we need to address:*
>>>>>>>>>>>>>>>>>>>> - Finish the complete decoupling of the ACITree
>>>>>>>>>>>>>>>>>>>> - Performance of the current tree implementation
>>>>>>>>>>>>>>>>>>>> - Tweak the naming of the Tree class to explicitly
>>>>>>>>>>>>>>>>>>>> tell us this is to use only with ACITree.
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> --
>>>>>>>>>>> Dave Page
>>>>>>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>>>>>>> Twitter: @pgsnake
>>>>>>>>>>>
>>>>>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> --
>>>>>>>>>> Dave Page
>>>>>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>>>>>> Twitter: @pgsnake
>>>>>>>>>>
>>>>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>>
>>>>>>>>>
>>>>>>>
>>>>>>
>>>>>
>>>>
>>>
>>>
>>> --
>>> Dave Page
>>> Blog: http://pgsnake.blogspot.com
>>> Twitter: @pgsnake
>>>
>>> EnterpriseDB UK: http://www.enterprisedb.com
>>> The Enterprise PostgreSQL Company
>>>
>>
>>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-02 08:08 Dave Page <[email protected]>
parent: Ashesh Vashi <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Dave Page @ 2018-05-02 08:08 UTC (permalink / raw)
To: Ashesh Vashi <[email protected]>; +Cc: Joao De Almeida Pereira <[email protected]>; Anthony Emengo <[email protected]>; Khushboo Vashi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
On Wed, May 2, 2018 at 6:30 AM, Ashesh Vashi <[email protected]>
wrote:
> On Mon, Apr 30, 2018 at 11:27 PM, Joao De Almeida Pereira <
> [email protected]> wrote:
>
>> You've lost us with the last reply. We'd love to know what we'd have to
>> do to get this patch committed. You mention that "some things are missing
>> at some places" and that "there are missing bits".
>>
> I've already mentioned few of them in the earlier mails about them.
>
>> Please note that this is not a complete solution that we've offered so
>> far.
>>
> I know and, hence - asked for your understanding of the code (and, asked
> for the TODOs.)
>
>> But only one step in a grander effort to effect a more cleaner, more
>> maintainable, more testable, code base.
>>
> Yes - we all want cleaner, maintainable, and testable code base.
> As per my understading comments are required for better understanding, and
> makes the code more maintainable.
>
> But - if we're going to change everything in one go, it is difficult to
> understand the changes.
> Let's avoid mix match multiple things in a single patch. And, concerntrate
> one functionality at a time.
>
So if I understand you correctly, your concerns are:
- The changes should cover specific units of code at once.
- There should be high-level comments to help guide new or less experienced
developers.
The first I agree with - and I think (if I understand correctly, so do Joao
and Anthony). That's why they've concentrated on itemData first.
The second I also agree with. It's one thing being able to follow a simple
function without comments, but that doesn't give you an easy to understand
high level view of how it all ties together without non-trivial amounts of
work. I'll be the first to admit that we're not the best at that either,
but we do try, and I'd like us to get better.
>
> -- Thanks, Ashesh
>
>>
>> Thanks
>> Joao and Anthony.
>>
>> On Mon, Apr 30, 2018 at 11:45 AM Ashesh Vashi <
>> [email protected]> wrote:
>>
>>> On Mon, Apr 30, 2018 at 9:07 PM, Dave Page <[email protected]> wrote:
>>>
>>>>
>>>>
>>>> On Mon, Apr 30, 2018 at 4:33 PM, Ashesh Vashi <
>>>> [email protected]> wrote:
>>>>
>>>>>
>>>>> On Mon, Apr 30, 2018 at 8:58 PM, Anthony Emengo <[email protected]>
>>>>> wrote:
>>>>>
>>>>>> I was expecting a separate layer between the tree implementation, and
>>>>>>> aciTree adaptor.
>>>>>>> Please find the patch for the example.
>>>>>>> It will separate the two layers, and easy to replace with the new
>>>>>>> implementation in future.
>>>>>>
>>>>>>
>>>>>> In general, we want defer the separation of the layers for now. Even
>>>>>> though we might assume that this is the direction we want to go in. It's
>>>>>> simply too early to be making such an architectural leap. For right now, we
>>>>>> just know that we need the decoupling, but don't know what if we'd need the
>>>>>> 2 layers *as implemented*. The principle we're adhering to here is
>>>>>> the Last Responsible Moment principle, which states that you only make the
>>>>>> changes that you feel is necessary for the given problems you're facing:
>>>>>> https://blog.codinghorror.com/the-last-responsible-moment/
>>>>>>
>>>>>> I would not like to see that changes in this patch.
>>>>>>> I would like us to come up with the actual design about the hot
>>>>>>> pluggability before going in this direction.
>>>>>>
>>>>>>
>>>>>> In our point of view these 2 changes are not related. One thing is
>>>>>> the internal code organization of the application, other thing is allowing
>>>>>> third party to drop code in the application and it just work. These 2
>>>>>> should be talked separately, but the hot pluggability is not something that
>>>>>> will be address by this work we are doing right now.
>>>>>>
>>>>> Neither - it should be part of this change.
>>>>> It should be addressed separately, and discussed.
>>>>>
>>>>
>>>> I agree. As long as this work doesn't make the pluggability problem
>>>> worse, that problem should be considered separately.
>>>>
>>>> So given Anthony's comments, are you happy with this patch?
>>>>
>>> I liked the design so far.
>>> But - as Khushboo mentioned ealier - it is missing at some places.
>>> I had to read through the code to understand the execution flow for some.
>>>
>>> And, there is still a lot missing bits, and pieces to consider for
>>> commit in the repo.
>>>
>>> -- Thanks, Ashesh
>>>
>>>>
>>>>
>>>>>
>>>>> -- Thanks, Ashesh
>>>>>
>>>>>>
>>>>>> Anthony && Joao
>>>>>>
>>>>>> On Mon, Apr 30, 2018 at 11:03 AM, Ashesh Vashi <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>> On Mon, Apr 30, 2018 at 8:30 PM, Ashesh Vashi <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> On Sat, Apr 28, 2018 at 3:55 AM, Joao De Almeida Pereira <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Hi Hackers,
>>>>>>>>> As you are aware we kept on working on the patch, so we are
>>>>>>>>> attaching to this email a new version of the patch.
>>>>>>>>> This new version contains all the changes in the previous one plus
>>>>>>>>> more extractions of functions and refactoring of code.
>>>>>>>>>
>>>>>>>>> The objective of this patch is to create a separation between
>>>>>>>>> pgAdmin and the ACI Tree. We are doing this because we realized that at
>>>>>>>>> this point in time we have the ACI Tree all over the code of pgAdmin. I
>>>>>>>>> found a very interesting article that really talks about this:
>>>>>>>>> https://medium.freecodecamp.org/code-dependencies-are-the-de
>>>>>>>>> vil-35ed28b556d
>>>>>>>>>
>>>>>>>>> In this patch there are some visions and ideas about the location
>>>>>>>>> of the code, the way to organize it and also try to pave the future for a
>>>>>>>>> application that is stable, easy to develop on and that can be release at a
>>>>>>>>> times notice.
>>>>>>>>>
>>>>>>>>> We are investing a big chunk of our time in doing this
>>>>>>>>> refactoring, but while doing that we also try to respond to the patches
>>>>>>>>> sent to the mailing list. We would like the feedback from the community
>>>>>>>>> because we believe this is a changing point for the application. The idea
>>>>>>>>> is to change the way we develop this application, instead of only
>>>>>>>>> correcting a bug of developing a feature, with every commit we should
>>>>>>>>> correct the bug or develop a feature but leave the code a little better
>>>>>>>>> than we found it (Refactoring, refactoring, refactoring). This is hard work
>>>>>>>>> but that is what the users from pgAdmin expect from this community of
>>>>>>>>> developers.
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> ======
>>>>>>>>>
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> It is a huge patch
>>>>>>>>> 86 files changed, 5492 inserts, 1840
>>>>>>>>> deletions
>>>>>>>>> and we would like to get your feedback as soon as possible,
>>>>>>>>> because we are continuing to work on it which means it is going to grow in
>>>>>>>>> size.
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> At this point in time we still have 124 of 176 calls to the
>>>>>>>>> function itemData from ACITree.
>>>>>>>>>
>>>>>>>>> What does each patch contain:
>>>>>>>>> 0001:
>>>>>>>>> Very simple patch, we found out that the linter was not looking
>>>>>>>>> into all the javascript test files, so this patch will ensure it is
>>>>>>>>>
>>>>>>>> Committed the patch along with the regression introduced because of
>>>>>>>> this patch.
>>>>>>>>
>>>>>>>>>
>>>>>>>>> 0002:
>>>>>>>>> New Tree abstraction. This patch contains the new Tree that
>>>>>>>>> works as an adaptor for ACI Tree and is going to be used on all the
>>>>>>>>> extractions that we are doing.
>>>>>>>>>
>>>>>>>>
>>>>>>>> I was expecting a separate layer between the tree implementation,
>>>>>>>> and aciTree adaptor.
>>>>>>>> Please find the patch for the example.
>>>>>>>>
>>>>>>>> It will separate the two layers, and easy to replace with the new
>>>>>>>> implemenation in future.
>>>>>>>>
>>>>>>>
>>>>>>> Oops forgot to attach the patch.
>>>>>>> Please find the patch attached.
>>>>>>>
>>>>>>> -- Thanks, Ashesh
>>>>>>>
>>>>>>>>
>>>>>>>>> 0003:
>>>>>>>>> Code that extracts, wrap with tests and replace ACI Tree
>>>>>>>>> invocations.
>>>>>>>>>
>>>>>>>> There are many small cases left in the patches.
>>>>>>>> Hence - I would like to know the TODO list created by you.
>>>>>>>>
>>>>>>>> e.g. When we remove any of the object from the database server,
>>>>>>>> we're not yet removing the respective node from the new implementation, and
>>>>>>>> its children.
>>>>>>>>
>>>>>>>>>
>>>>>>>>> We start creating new pattern for the location of Javascript
>>>>>>>>> files and their structure.
>>>>>>>>>
>>>>>>>> I would not like to see that changes in this patch.
>>>>>>>> I would like us to come up with the actual design about the hot
>>>>>>>> pluggability before going in this direction.
>>>>>>>>
>>>>>>>>> Create patterns for creation of dialogs (backup and restore)
>>>>>>>>>
>>>>>>>> It's better - we don't change the directory structure at the moment.
>>>>>>>>
>>>>>>>> I am not against dividing the big javascript files in small chunks,
>>>>>>>> but - I would like us to discuss first about the hot plugins design first.
>>>>>>>>
>>>>>>>> -- Thanks, Ashesh
>>>>>>>>
>>>>>>>>>
>>>>>>>>>
>>>>>>>>
>>>>>>>>>
>>>>>>>>> Thanks
>>>>>>>>> Joao
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> On Fri, Apr 27, 2018 at 5:34 AM Ashesh Vashi <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> I have quite a few comments for the patch.
>>>>>>>>>> I will send them soon.
>>>>>>>>>>
>>>>>>>>>> On Fri, Apr 27, 2018, 14:45 Dave Page <[email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> How is your work on this going Ashesh? Will you be committing
>>>>>>>>>>> today?
>>>>>>>>>>>
>>>>>>>>>>> On Wed, Apr 25, 2018 at 8:52 AM, Dave Page <[email protected]>
>>>>>>>>>>> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> Ashesh; you had agreed to work on this early this week. Please
>>>>>>>>>>>> ensure you do so today.
>>>>>>>>>>>>
>>>>>>>>>>>> Thanks.
>>>>>>>>>>>>
>>>>>>>>>>>> On Tue, Apr 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>
>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>
>>>>>>>>>>>>> Can someone review and merge this patch?
>>>>>>>>>>>>>
>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>
>>>>>>>>>>>>> On Wed, Apr 18, 2018 at 10:23 AM Joao De Almeida Pereira <
>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>
>>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>> Any other comment about this patch?
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> On Tue, Apr 10, 2018 at 12:00 PM Joao De Almeida Pereira <
>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Hello Khushboo
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> On Mon, Apr 9, 2018 at 1:59 AM Khushboo Vashi <
>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> I have reviewed your patch and have some suggestions.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> On Sat, Apr 7, 2018 at 12:43 AM, Joao De Almeida Pereira <
>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Hello Murtuza/Dave,
>>>>>>>>>>>>>>>>> Yes now the extracted functions are spread into different
>>>>>>>>>>>>>>>>> files. The intent would be to make the files as small as possible, and also
>>>>>>>>>>>>>>>>> to group and name them in a way that would be easy to understand what each
>>>>>>>>>>>>>>>>> file is doing without the need of opening it.
>>>>>>>>>>>>>>>>> As a example:
>>>>>>>>>>>>>>>>> static/js/backup will contain all the backup related
>>>>>>>>>>>>>>>>> functionality inside of this folder we can see the file:
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> menu_utils.js At this moment in time we decided to group
>>>>>>>>>>>>>>>>> all the functions that are related to the menu, but we can split that also
>>>>>>>>>>>>>>>>> if we believe it is easier to see.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> It's really very good to see the separated code for backup
>>>>>>>>>>>>>>>> module. As we have done for backup, we would like do it for other PG
>>>>>>>>>>>>>>>> utilities like restore, maintenance etc.
>>>>>>>>>>>>>>>> Considering this, we should separate the code in a way that
>>>>>>>>>>>>>>>> some of the common functionalities can be used for other modules like menu
>>>>>>>>>>>>>>>> (as you have mentioned above), dialogue factory etc.
>>>>>>>>>>>>>>>> Also, I think these functionalities should be in their
>>>>>>>>>>>>>>>> respective static folder instead of pgadmin/static.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> About the location of the files. The move of the files to
>>>>>>>>>>>>>>> pgadmin/static/js was made on purpose in order to clearly separate
>>>>>>>>>>>>>>> Javascript from python code.
>>>>>>>>>>>>>>> The rational behind it was
>>>>>>>>>>>>>>> - Create a clear separation between the backend and frontend
>>>>>>>>>>>>>>> - Having Javascript code concentrated in a single place,
>>>>>>>>>>>>>>> hopefully, will encourage to developers to look for a functionality, that
>>>>>>>>>>>>>>> is already implemented in another modules, because they are right there.
>>>>>>>>>>>>>>> (When we started this journey we realized that the 'nodes' have a big
>>>>>>>>>>>>>>> groups of code that could be shared, but because the Javascript is spread
>>>>>>>>>>>>>>> everywhere it is much harder to look for it)
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> There are some drawbacks of this separation:
>>>>>>>>>>>>>>> - When creating a new module we will need to put the
>>>>>>>>>>>>>>> javascript in a separate location from the backend code
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> static/js/datagrid folder contains all the datagrid
>>>>>>>>>>>>>>>>> related functionality
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Same as backup module, this should be in it's respective
>>>>>>>>>>>>>>>> static/js folder.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Inside of the folder we can see the files:
>>>>>>>>>>>>>>>>> get_panel_title.js is responsible for retrieving the name
>>>>>>>>>>>>>>>>> of the panel
>>>>>>>>>>>>>>>>> show_data.js is responsible for showing the datagrid
>>>>>>>>>>>>>>>>> show_query_tool.js is responsible for showing the query
>>>>>>>>>>>>>>>>> tool
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Does this structure make sense?
>>>>>>>>>>>>>>>>> Can you give an example of a comment that you think is
>>>>>>>>>>>>>>>>> missing and that could help?
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> As a personal note, unless the algorithm is very obscure
>>>>>>>>>>>>>>>>> or very complicated, I believe that if the code needs comments it is a
>>>>>>>>>>>>>>>>> signal that something needs to change in terms of naming, structure of the
>>>>>>>>>>>>>>>>> part in question. This being said, I am open to add some comments that
>>>>>>>>>>>>>>>>> might help people.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> You are right, with the help of naming convention and
>>>>>>>>>>>>>>>> structure of the code, any one can get the idea about the code. But it is
>>>>>>>>>>>>>>>> very useful to understand the code
>>>>>>>>>>>>>>>> very easily with the proper comments especially when there
>>>>>>>>>>>>>>>> are multiple developers working on a single project.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> I found some of the places where it would be great to have
>>>>>>>>>>>>>>>> comments.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> - treeMenu: new tree.Tree() in a browser.js
>>>>>>>>>>>>>>>> - tree.js (especially Tree class)
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> About the comment point I need a more clear understanding on
>>>>>>>>>>>>>>> what kind of comments you are looking for. Because when you read the
>>>>>>>>>>>>>>> function names you understand the intent, what they are doing. The
>>>>>>>>>>>>>>> parameters also explain what you need to pass into them.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> If what you are looking for in these comments is the
>>>>>>>>>>>>>>> reasoning being the change itself, then that should be present in the
>>>>>>>>>>>>>>> commit message. Specially because this is going to be a very big patch with
>>>>>>>>>>>>>>> a very big number of changes.
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> On Fri, Apr 6, 2018 at 4:48 AM Murtuza Zabuawala <
>>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> Patch looks good and working as expected.
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> I also agree with Dave, Can we please add some comments
>>>>>>>>>>>>>>>>>> in each file which can help us to understand the flow, I'm saying because
>>>>>>>>>>>>>>>>>> now the code is segregated in so many separate files it will be hard to
>>>>>>>>>>>>>>>>>> keep track of the flow from one file to another when debugging.
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> --
>>>>>>>>>>>>>>>>>> Regards,
>>>>>>>>>>>>>>>>>> Murtuza Zabuawala
>>>>>>>>>>>>>>>>>> EnterpriseDB: http://www.enterprisedb.com
>>>>>>>>>>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> On Thu, Apr 5, 2018 at 7:08 PM, Joao De Almeida Pereira <
>>>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> Hi Khushboo,
>>>>>>>>>>>>>>>>>>> Attached you can find both patches rebased
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
>>>>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> Can you please rebase the second patch?
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida
>>>>>>>>>>>>>>>>>>>> Pereira <[email protected]> wrote:
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> Attached you can find the patch that will start to
>>>>>>>>>>>>>>>>>>>>> decouple pgAdmin from ACITree library.
>>>>>>>>>>>>>>>>>>>>> This patch is intended to be merged after 3.0, because
>>>>>>>>>>>>>>>>>>>>> we do not want to cause any entropy or delay the release, but we want to
>>>>>>>>>>>>>>>>>>>>> start the discussion and show some code.
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> This job that we started is a massive tech debt chore
>>>>>>>>>>>>>>>>>>>>> that will take some time to finalize and we would love the help of the
>>>>>>>>>>>>>>>>>>>>> community to do it.
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> *Summary of the patch:*
>>>>>>>>>>>>>>>>>>>>> 0001 patch:
>>>>>>>>>>>>>>>>>>>>> - Creates a new tree that will allow us to create a
>>>>>>>>>>>>>>>>>>>>> separation between the application and ACI Tree
>>>>>>>>>>>>>>>>>>>>> - Creates a Fake Tree (Test double, for reference on
>>>>>>>>>>>>>>>>>>>>> the available test doubles: https://martinfowler.
>>>>>>>>>>>>>>>>>>>>> com/bliki/TestDouble.html) that can be used to
>>>>>>>>>>>>>>>>>>>>> inplace to replace the ACITree and also encapsulate the new tree behavior
>>>>>>>>>>>>>>>>>>>>> on our tests
>>>>>>>>>>>>>>>>>>>>> - Adds tests for all the tree functionalities
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> 0002 patch:
>>>>>>>>>>>>>>>>>>>>> - Extracts, refactors, adds tests and remove
>>>>>>>>>>>>>>>>>>>>> dependency from ACI Tree on:
>>>>>>>>>>>>>>>>>>>>> - getTreeNodeHierarchy
>>>>>>>>>>>>>>>>>>>>> - on backup.js: menu_enabled, menu_enabled_server,
>>>>>>>>>>>>>>>>>>>>> start_backup_global_server, backup_objects
>>>>>>>>>>>>>>>>>>>>> - on datagrid.js: show_data_grid, get_panel_title,
>>>>>>>>>>>>>>>>>>>>> show_query_tool
>>>>>>>>>>>>>>>>>>>>> - Start using sprintf-js as Underscore.String is
>>>>>>>>>>>>>>>>>>>>> deprecating sprintf function
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> This patch represents only 10 calls to
>>>>>>>>>>>>>>>>>>>>> ACITree.itemData out of 176 that are spread around our code
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> *In Depth look on the process behind the patch:*
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> We started writing this patch with the idea that we
>>>>>>>>>>>>>>>>>>>>> need to decouple pgAdmin4 from ACITree, because ACITree is no longer
>>>>>>>>>>>>>>>>>>>>> supported, the documentation is non existent and ACITree is no longer being
>>>>>>>>>>>>>>>>>>>>> actively developed.
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> Our process:
>>>>>>>>>>>>>>>>>>>>> 1. We "randomly" selected a function that is part of
>>>>>>>>>>>>>>>>>>>>> the ACITree. From this point we decided to replace that function with our
>>>>>>>>>>>>>>>>>>>>> own version. The function that we choose was "itemData".
>>>>>>>>>>>>>>>>>>>>> The function gives us all the "data" that a specific
>>>>>>>>>>>>>>>>>>>>> node of the tree find.
>>>>>>>>>>>>>>>>>>>>> Given in order to replace the tree we would need to
>>>>>>>>>>>>>>>>>>>>> have a function that would give us the same information. We had 2 options:
>>>>>>>>>>>>>>>>>>>>> a) Create a tree with a function called itemData
>>>>>>>>>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>>>>>>>>>> - At first view this was the simpler solution
>>>>>>>>>>>>>>>>>>>>> - Would keep the status quo
>>>>>>>>>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>>>>>>>>>> - Not a OOP approach
>>>>>>>>>>>>>>>>>>>>> - Not very flexible
>>>>>>>>>>>>>>>>>>>>> b) Create a tree that would return a node given an
>>>>>>>>>>>>>>>>>>>>> ID and then the node would be responsible for giving it's data.
>>>>>>>>>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>>>>>>>>>> - OOP Approach
>>>>>>>>>>>>>>>>>>>>> - More flexible and we do not need to bring the tree
>>>>>>>>>>>>>>>>>>>>> around, just a node
>>>>>>>>>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>>>>>>>>>> - Break the current status quo
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> Given these 2 options we decided to go for a more OOP
>>>>>>>>>>>>>>>>>>>>> approach creating a Tree and a TreeNode classes, that in the future will be
>>>>>>>>>>>>>>>>>>>>> renamed to ACITreeWrapper and TreeNode.
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> 2. After we decided on the starting point we searched
>>>>>>>>>>>>>>>>>>>>> for occurrences of the function "itemData" and we found out that there were
>>>>>>>>>>>>>>>>>>>>> 303 occurrences of "itemData" in the code and roughly 176 calls to the
>>>>>>>>>>>>>>>>>>>>> function itself (some of the hits were variable names).
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> 3. We selected the first file on the search and found
>>>>>>>>>>>>>>>>>>>>> the function that was responsible for calling the itemData function.
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> 4. Extracted the function to a separate file
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> 5. Wrap this function with tests
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> 6. Refactor the function to ES6, give more declarative
>>>>>>>>>>>>>>>>>>>>> names to variables and break the functions into smaller chunks
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> 7. When all the tests were passing we replaced ACITree
>>>>>>>>>>>>>>>>>>>>> with our Tree
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> 8. We ensured that all tests were passing
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> 9. Remove function from the original file and use the
>>>>>>>>>>>>>>>>>>>>> new function
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> 10. Ensure everything still works
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> 11. Find the next function and execute from step 4
>>>>>>>>>>>>>>>>>>>>> until all the functions are replaced, refactored and tested.
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> As you can see by the process this is a pretty huge
>>>>>>>>>>>>>>>>>>>>> undertake, because of the number of calls to the function. This is just the
>>>>>>>>>>>>>>>>>>>>> first step on the direction of completely isolating the ACITree so that we
>>>>>>>>>>>>>>>>>>>>> can solve the problem with a large number of elements on the tree.
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> *What is on our radar that we need to address:*
>>>>>>>>>>>>>>>>>>>>> - Finish the complete decoupling of the ACITree
>>>>>>>>>>>>>>>>>>>>> - Performance of the current tree implementation
>>>>>>>>>>>>>>>>>>>>> - Tweak the naming of the Tree class to explicitly
>>>>>>>>>>>>>>>>>>>>> tell us this is to use only with ACITree.
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> --
>>>>>>>>>>>> Dave Page
>>>>>>>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>>>>>>>> Twitter: @pgsnake
>>>>>>>>>>>>
>>>>>>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>> --
>>>>>>>>>>> Dave Page
>>>>>>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>>>>>>> Twitter: @pgsnake
>>>>>>>>>>>
>>>>>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>
>>>>>>>
>>>>>>
>>>>>
>>>>
>>>>
>>>> --
>>>> Dave Page
>>>> Blog: http://pgsnake.blogspot.com
>>>> Twitter: @pgsnake
>>>>
>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>> The Enterprise PostgreSQL Company
>>>>
>>>
>>>
>
--
Dave Page
Blog: http://pgsnake.blogspot.com
Twitter: @pgsnake
EnterpriseDB UK: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-02 14:54 Joao De Almeida Pereira <[email protected]>
parent: Dave Page <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Joao De Almeida Pereira @ 2018-05-02 14:54 UTC (permalink / raw)
To: Dave Page <[email protected]>; +Cc: Ashesh Vashi <[email protected]>; Anthony Emengo <[email protected]>; Khushboo Vashi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
HI Hackers,
The next image depicts what we have been talking in the previous email
"TODO List". On the left side you can see the master branch utilizations of
the function itemData, and on the left side what master will look like when
you merge this patch.
[image: Screenshot from 2018-05-02 10-12-11.png]
As you can see this is a huge job that should be committed in smaller chunk
as soon as they are completed or else we will lose all traceability of the
changes and it will become harder and harder to merge and review any patch.
We can split this patch into small patchs, one for each unit(Unit can be
considered an application feature like handling tables or server
information). This will make the patches smaller and easier to
review/merge.
But - if we're going to change everything in one go, it is difficult to
> understand the changes.
> Let's avoid mix match multiple things in a single patch. And, concerntrate
> one functionality at a time.
You are talking about the change in the locations of the file + removal of
the refactor the Tree integration, correct? If so you are right, but we
have to consider the following in order to refactor the Tree we need to
extract the functions from the place where they are so that they can be
wrapped with tests, only after that the Refactor should happen. By doing
this we will have to extract them to separate file(s). This leaves us with
2 options, one is to put these files next to the files we extract the
functions from the other is to try to create more intuitive structure for
the code. We hoped to go with the second options, because we feel that this
will make the application easier to understand and to develop on.
What is preventing us from merging this patch?
Do you believe we didn't explain the intent of this patch well?
Any information that you can provide to us that would help us understand
what is required to merge this patch would be welcomed. Because after one
month of back and forth with this patch it is time to give it some closure.
Thanks
Joao & Victoria
On Wed, May 2, 2018 at 4:08 AM Dave Page <[email protected]> wrote:
>
>
> On Wed, May 2, 2018 at 6:30 AM, Ashesh Vashi <
> [email protected]> wrote:
>
>> On Mon, Apr 30, 2018 at 11:27 PM, Joao De Almeida Pereira <
>> [email protected]> wrote:
>>
>>> You've lost us with the last reply. We'd love to know what we'd have to
>>> do to get this patch committed. You mention that "some things are missing
>>> at some places" and that "there are missing bits".
>>>
>> I've already mentioned few of them in the earlier mails about them.
>>
>>> Please note that this is not a complete solution that we've offered so
>>> far.
>>>
>> I know and, hence - asked for your understanding of the code (and, asked
>> for the TODOs.)
>>
>>> But only one step in a grander effort to effect a more cleaner, more
>>> maintainable, more testable, code base.
>>>
>> Yes - we all want cleaner, maintainable, and testable code base.
>> As per my understading comments are required for better understanding,
>> and makes the code more maintainable.
>>
>> But - if we're going to change everything in one go, it is difficult to
>> understand the changes.
>> Let's avoid mix match multiple things in a single patch. And,
>> concerntrate one functionality at a time.
>>
>
> So if I understand you correctly, your concerns are:
>
> - The changes should cover specific units of code at once.
>
> - There should be high-level comments to help guide new or less
> experienced developers.
>
> The first I agree with - and I think (if I understand correctly, so do
> Joao and Anthony). That's why they've concentrated on itemData first.
>
> The second I also agree with. It's one thing being able to follow a simple
> function without comments, but that doesn't give you an easy to understand
> high level view of how it all ties together without non-trivial amounts of
> work. I'll be the first to admit that we're not the best at that either,
> but we do try, and I'd like us to get better.
>
>
>>
>> -- Thanks, Ashesh
>>
>>>
>>> Thanks
>>> Joao and Anthony.
>>>
>>> On Mon, Apr 30, 2018 at 11:45 AM Ashesh Vashi <
>>> [email protected]> wrote:
>>>
>>>> On Mon, Apr 30, 2018 at 9:07 PM, Dave Page <[email protected]> wrote:
>>>>
>>>>>
>>>>>
>>>>> On Mon, Apr 30, 2018 at 4:33 PM, Ashesh Vashi <
>>>>> [email protected]> wrote:
>>>>>
>>>>>>
>>>>>> On Mon, Apr 30, 2018 at 8:58 PM, Anthony Emengo <[email protected]>
>>>>>> wrote:
>>>>>>
>>>>>>> I was expecting a separate layer between the tree implementation,
>>>>>>>> and aciTree adaptor.
>>>>>>>> Please find the patch for the example.
>>>>>>>> It will separate the two layers, and easy to replace with the new
>>>>>>>> implementation in future.
>>>>>>>
>>>>>>>
>>>>>>> In general, we want defer the separation of the layers for now. Even
>>>>>>> though we might assume that this is the direction we want to go in. It's
>>>>>>> simply too early to be making such an architectural leap. For right now, we
>>>>>>> just know that we need the decoupling, but don't know what if we'd need the
>>>>>>> 2 layers *as implemented*. The principle we're adhering to here is
>>>>>>> the Last Responsible Moment principle, which states that you only make the
>>>>>>> changes that you feel is necessary for the given problems you're facing:
>>>>>>> https://blog.codinghorror.com/the-last-responsible-moment/
>>>>>>>
>>>>>>> I would not like to see that changes in this patch.
>>>>>>>> I would like us to come up with the actual design about the hot
>>>>>>>> pluggability before going in this direction.
>>>>>>>
>>>>>>>
>>>>>>> In our point of view these 2 changes are not related. One thing is
>>>>>>> the internal code organization of the application, other thing is allowing
>>>>>>> third party to drop code in the application and it just work. These 2
>>>>>>> should be talked separately, but the hot pluggability is not something that
>>>>>>> will be address by this work we are doing right now.
>>>>>>>
>>>>>> Neither - it should be part of this change.
>>>>>> It should be addressed separately, and discussed.
>>>>>>
>>>>>
>>>>> I agree. As long as this work doesn't make the pluggability problem
>>>>> worse, that problem should be considered separately.
>>>>>
>>>>> So given Anthony's comments, are you happy with this patch?
>>>>>
>>>> I liked the design so far.
>>>> But - as Khushboo mentioned ealier - it is missing at some places.
>>>> I had to read through the code to understand the execution flow for
>>>> some.
>>>>
>>>> And, there is still a lot missing bits, and pieces to consider for
>>>> commit in the repo.
>>>>
>>>> -- Thanks, Ashesh
>>>>
>>>>>
>>>>>
>>>>>>
>>>>>> -- Thanks, Ashesh
>>>>>>
>>>>>>>
>>>>>>> Anthony && Joao
>>>>>>>
>>>>>>> On Mon, Apr 30, 2018 at 11:03 AM, Ashesh Vashi <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>>
>>>>>>>>
>>>>>>>>
>>>>>>>> On Mon, Apr 30, 2018 at 8:30 PM, Ashesh Vashi <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> On Sat, Apr 28, 2018 at 3:55 AM, Joao De Almeida Pereira <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Hi Hackers,
>>>>>>>>>> As you are aware we kept on working on the patch, so we are
>>>>>>>>>> attaching to this email a new version of the patch.
>>>>>>>>>> This new version contains all the changes in the previous one
>>>>>>>>>> plus more extractions of functions and refactoring of code.
>>>>>>>>>>
>>>>>>>>>> The objective of this patch is to create a separation between
>>>>>>>>>> pgAdmin and the ACI Tree. We are doing this because we realized that at
>>>>>>>>>> this point in time we have the ACI Tree all over the code of pgAdmin. I
>>>>>>>>>> found a very interesting article that really talks about this:
>>>>>>>>>> https://medium.freecodecamp.org/code-dependencies-are-the-devil-35ed28b556d
>>>>>>>>>>
>>>>>>>>>> In this patch there are some visions and ideas about the location
>>>>>>>>>> of the code, the way to organize it and also try to pave the future for a
>>>>>>>>>> application that is stable, easy to develop on and that can be release at a
>>>>>>>>>> times notice.
>>>>>>>>>>
>>>>>>>>>> We are investing a big chunk of our time in doing this
>>>>>>>>>> refactoring, but while doing that we also try to respond to the patches
>>>>>>>>>> sent to the mailing list. We would like the feedback from the community
>>>>>>>>>> because we believe this is a changing point for the application. The idea
>>>>>>>>>> is to change the way we develop this application, instead of only
>>>>>>>>>> correcting a bug of developing a feature, with every commit we should
>>>>>>>>>> correct the bug or develop a feature but leave the code a little better
>>>>>>>>>> than we found it (Refactoring, refactoring, refactoring). This is hard work
>>>>>>>>>> but that is what the users from pgAdmin expect from this community of
>>>>>>>>>> developers.
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> ======
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> It is a huge patch
>>>>>>>>>> 86 files changed, 5492 inserts, 1840
>>>>>>>>>> deletions
>>>>>>>>>> and we would like to get your feedback as soon as possible,
>>>>>>>>>> because we are continuing to work on it which means it is going to grow in
>>>>>>>>>> size.
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> At this point in time we still have 124 of 176 calls to the
>>>>>>>>>> function itemData from ACITree.
>>>>>>>>>>
>>>>>>>>>> What does each patch contain:
>>>>>>>>>> 0001:
>>>>>>>>>> Very simple patch, we found out that the linter was not looking
>>>>>>>>>> into all the javascript test files, so this patch will ensure it is
>>>>>>>>>>
>>>>>>>>> Committed the patch along with the regression introduced because
>>>>>>>>> of this patch.
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> 0002:
>>>>>>>>>> New Tree abstraction. This patch contains the new Tree that
>>>>>>>>>> works as an adaptor for ACI Tree and is going to be used on all the
>>>>>>>>>> extractions that we are doing.
>>>>>>>>>>
>>>>>>>>>
>>>>>>>>> I was expecting a separate layer between the tree implementation,
>>>>>>>>> and aciTree adaptor.
>>>>>>>>> Please find the patch for the example.
>>>>>>>>>
>>>>>>>>> It will separate the two layers, and easy to replace with the new
>>>>>>>>> implemenation in future.
>>>>>>>>>
>>>>>>>>
>>>>>>>> Oops forgot to attach the patch.
>>>>>>>> Please find the patch attached.
>>>>>>>>
>>>>>>>> -- Thanks, Ashesh
>>>>>>>>
>>>>>>>>>
>>>>>>>>>> 0003:
>>>>>>>>>> Code that extracts, wrap with tests and replace ACI Tree
>>>>>>>>>> invocations.
>>>>>>>>>>
>>>>>>>>> There are many small cases left in the patches.
>>>>>>>>> Hence - I would like to know the TODO list created by you.
>>>>>>>>>
>>>>>>>>> e.g. When we remove any of the object from the database server,
>>>>>>>>> we're not yet removing the respective node from the new implementation, and
>>>>>>>>> its children.
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> We start creating new pattern for the location of Javascript
>>>>>>>>>> files and their structure.
>>>>>>>>>>
>>>>>>>>> I would not like to see that changes in this patch.
>>>>>>>>> I would like us to come up with the actual design about the hot
>>>>>>>>> pluggability before going in this direction.
>>>>>>>>>
>>>>>>>>>> Create patterns for creation of dialogs (backup and restore)
>>>>>>>>>>
>>>>>>>>> It's better - we don't change the directory structure at the
>>>>>>>>> moment.
>>>>>>>>>
>>>>>>>>> I am not against dividing the big javascript files in small
>>>>>>>>> chunks, but - I would like us to discuss first about the hot plugins design
>>>>>>>>> first.
>>>>>>>>>
>>>>>>>>> -- Thanks, Ashesh
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> Thanks
>>>>>>>>>> Joao
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> On Fri, Apr 27, 2018 at 5:34 AM Ashesh Vashi <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> I have quite a few comments for the patch.
>>>>>>>>>>> I will send them soon.
>>>>>>>>>>>
>>>>>>>>>>> On Fri, Apr 27, 2018, 14:45 Dave Page <[email protected]> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> How is your work on this going Ashesh? Will you be committing
>>>>>>>>>>>> today?
>>>>>>>>>>>>
>>>>>>>>>>>> On Wed, Apr 25, 2018 at 8:52 AM, Dave Page <[email protected]>
>>>>>>>>>>>> wrote:
>>>>>>>>>>>>
>>>>>>>>>>>>> Ashesh; you had agreed to work on this early this week. Please
>>>>>>>>>>>>> ensure you do so today.
>>>>>>>>>>>>>
>>>>>>>>>>>>> Thanks.
>>>>>>>>>>>>>
>>>>>>>>>>>>> On Tue, Apr 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>
>>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Can someone review and merge this patch?
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>>
>>>>>>>>>>>>>> On Wed, Apr 18, 2018 at 10:23 AM Joao De Almeida Pereira <
>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>>> Any other comment about this patch?
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>> On Tue, Apr 10, 2018 at 12:00 PM Joao De Almeida Pereira <
>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> Hello Khushboo
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> On Mon, Apr 9, 2018 at 1:59 AM Khushboo Vashi <
>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> I have reviewed your patch and have some suggestions.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> On Sat, Apr 7, 2018 at 12:43 AM, Joao De Almeida Pereira <
>>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> Hello Murtuza/Dave,
>>>>>>>>>>>>>>>>>> Yes now the extracted functions are spread into different
>>>>>>>>>>>>>>>>>> files. The intent would be to make the files as small as possible, and also
>>>>>>>>>>>>>>>>>> to group and name them in a way that would be easy to understand what each
>>>>>>>>>>>>>>>>>> file is doing without the need of opening it.
>>>>>>>>>>>>>>>>>> As a example:
>>>>>>>>>>>>>>>>>> static/js/backup will contain all the backup related
>>>>>>>>>>>>>>>>>> functionality inside of this folder we can see the file:
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> menu_utils.js At this moment in time we decided to group
>>>>>>>>>>>>>>>>>> all the functions that are related to the menu, but we can split that also
>>>>>>>>>>>>>>>>>> if we believe it is easier to see.
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> It's really very good to see the separated code for backup
>>>>>>>>>>>>>>>>> module. As we have done for backup, we would like do it for other PG
>>>>>>>>>>>>>>>>> utilities like restore, maintenance etc.
>>>>>>>>>>>>>>>>> Considering this, we should separate the code in a way
>>>>>>>>>>>>>>>>> that some of the common functionalities can be used for other modules like
>>>>>>>>>>>>>>>>> menu (as you have mentioned above), dialogue factory etc.
>>>>>>>>>>>>>>>>> Also, I think these functionalities should be in their
>>>>>>>>>>>>>>>>> respective static folder instead of pgadmin/static.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> About the location of the files. The move of the files to
>>>>>>>>>>>>>>>> pgadmin/static/js was made on purpose in order to clearly separate
>>>>>>>>>>>>>>>> Javascript from python code.
>>>>>>>>>>>>>>>> The rational behind it was
>>>>>>>>>>>>>>>> - Create a clear separation between the backend and frontend
>>>>>>>>>>>>>>>> - Having Javascript code concentrated in a single place,
>>>>>>>>>>>>>>>> hopefully, will encourage to developers to look for a functionality, that
>>>>>>>>>>>>>>>> is already implemented in another modules, because they are right there.
>>>>>>>>>>>>>>>> (When we started this journey we realized that the 'nodes' have a big
>>>>>>>>>>>>>>>> groups of code that could be shared, but because the Javascript is spread
>>>>>>>>>>>>>>>> everywhere it is much harder to look for it)
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> There are some drawbacks of this separation:
>>>>>>>>>>>>>>>> - When creating a new module we will need to put the
>>>>>>>>>>>>>>>> javascript in a separate location from the backend code
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> static/js/datagrid folder contains all the datagrid
>>>>>>>>>>>>>>>>>> related functionality
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Same as backup module, this should be in it's respective
>>>>>>>>>>>>>>>>> static/js folder.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> Inside of the folder we can see the files:
>>>>>>>>>>>>>>>>>> get_panel_title.js is responsible for retrieving the
>>>>>>>>>>>>>>>>>> name of the panel
>>>>>>>>>>>>>>>>>> show_data.js is responsible for showing the datagrid
>>>>>>>>>>>>>>>>>> show_query_tool.js is responsible for showing the query
>>>>>>>>>>>>>>>>>> tool
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> Does this structure make sense?
>>>>>>>>>>>>>>>>>> Can you give an example of a comment that you think is
>>>>>>>>>>>>>>>>>> missing and that could help?
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> As a personal note, unless the algorithm is very obscure
>>>>>>>>>>>>>>>>>> or very complicated, I believe that if the code needs comments it is a
>>>>>>>>>>>>>>>>>> signal that something needs to change in terms of naming, structure of the
>>>>>>>>>>>>>>>>>> part in question. This being said, I am open to add some comments that
>>>>>>>>>>>>>>>>>> might help people.
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> You are right, with the help of naming convention and
>>>>>>>>>>>>>>>>> structure of the code, any one can get the idea about the code. But it is
>>>>>>>>>>>>>>>>> very useful to understand the code
>>>>>>>>>>>>>>>>> very easily with the proper comments especially when there
>>>>>>>>>>>>>>>>> are multiple developers working on a single project.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> I found some of the places where it would be great to have
>>>>>>>>>>>>>>>>> comments.
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> - treeMenu: new tree.Tree() in a browser.js
>>>>>>>>>>>>>>>>> - tree.js (especially Tree class)
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> About the comment point I need a more clear understanding
>>>>>>>>>>>>>>>> on what kind of comments you are looking for. Because when you read the
>>>>>>>>>>>>>>>> function names you understand the intent, what they are doing. The
>>>>>>>>>>>>>>>> parameters also explain what you need to pass into them.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>> If what you are looking for in these comments is the
>>>>>>>>>>>>>>>> reasoning being the change itself, then that should be present in the
>>>>>>>>>>>>>>>> commit message. Specially because this is going to be a very big patch with
>>>>>>>>>>>>>>>> a very big number of changes.
>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>> On Fri, Apr 6, 2018 at 4:48 AM Murtuza Zabuawala <
>>>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> Patch looks good and working as expected.
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> I also agree with Dave, Can we please add some comments
>>>>>>>>>>>>>>>>>>> in each file which can help us to understand the flow, I'm saying because
>>>>>>>>>>>>>>>>>>> now the code is segregated in so many separate files it will be hard to
>>>>>>>>>>>>>>>>>>> keep track of the flow from one file to another when debugging.
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> --
>>>>>>>>>>>>>>>>>>> Regards,
>>>>>>>>>>>>>>>>>>> Murtuza Zabuawala
>>>>>>>>>>>>>>>>>>> EnterpriseDB: http://www.enterprisedb.com
>>>>>>>>>>>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>> On Thu, Apr 5, 2018 at 7:08 PM, Joao De Almeida Pereira
>>>>>>>>>>>>>>>>>>> <[email protected]> wrote:
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> Hi Khushboo,
>>>>>>>>>>>>>>>>>>>> Attached you can find both patches rebased
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>> On Thu, Apr 5, 2018 at 6:31 AM Khushboo Vashi <
>>>>>>>>>>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> Hi Joao,
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> Can you please rebase the second patch?
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> Thanks,
>>>>>>>>>>>>>>>>>>>>> Khushboo
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>> On Tue, Apr 3, 2018 at 12:15 AM, Joao De Almeida
>>>>>>>>>>>>>>>>>>>>> Pereira <[email protected]> wrote:
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>> Hi Hackers,
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>> Attached you can find the patch that will start to
>>>>>>>>>>>>>>>>>>>>>> decouple pgAdmin from ACITree library.
>>>>>>>>>>>>>>>>>>>>>> This patch is intended to be merged after 3.0,
>>>>>>>>>>>>>>>>>>>>>> because we do not want to cause any entropy or delay the release, but we
>>>>>>>>>>>>>>>>>>>>>> want to start the discussion and show some code.
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>> This job that we started is a massive tech debt chore
>>>>>>>>>>>>>>>>>>>>>> that will take some time to finalize and we would love the help of the
>>>>>>>>>>>>>>>>>>>>>> community to do it.
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>> *Summary of the patch:*
>>>>>>>>>>>>>>>>>>>>>> 0001 patch:
>>>>>>>>>>>>>>>>>>>>>> - Creates a new tree that will allow us to create a
>>>>>>>>>>>>>>>>>>>>>> separation between the application and ACI Tree
>>>>>>>>>>>>>>>>>>>>>> - Creates a Fake Tree (Test double, for reference on
>>>>>>>>>>>>>>>>>>>>>> the available test doubles:
>>>>>>>>>>>>>>>>>>>>>> https://martinfowler.com/bliki/TestDouble.html) that
>>>>>>>>>>>>>>>>>>>>>> can be used to inplace to replace the ACITree and also encapsulate the new
>>>>>>>>>>>>>>>>>>>>>> tree behavior on our tests
>>>>>>>>>>>>>>>>>>>>>> - Adds tests for all the tree functionalities
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>> 0002 patch:
>>>>>>>>>>>>>>>>>>>>>> - Extracts, refactors, adds tests and remove
>>>>>>>>>>>>>>>>>>>>>> dependency from ACI Tree on:
>>>>>>>>>>>>>>>>>>>>>> - getTreeNodeHierarchy
>>>>>>>>>>>>>>>>>>>>>> - on backup.js: menu_enabled, menu_enabled_server,
>>>>>>>>>>>>>>>>>>>>>> start_backup_global_server, backup_objects
>>>>>>>>>>>>>>>>>>>>>> - on datagrid.js: show_data_grid, get_panel_title,
>>>>>>>>>>>>>>>>>>>>>> show_query_tool
>>>>>>>>>>>>>>>>>>>>>> - Start using sprintf-js as Underscore.String is
>>>>>>>>>>>>>>>>>>>>>> deprecating sprintf function
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>> This patch represents only 10 calls to
>>>>>>>>>>>>>>>>>>>>>> ACITree.itemData out of 176 that are spread around our code
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>> *In Depth look on the process behind the patch:*
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>> We started writing this patch with the idea that we
>>>>>>>>>>>>>>>>>>>>>> need to decouple pgAdmin4 from ACITree, because ACITree is no longer
>>>>>>>>>>>>>>>>>>>>>> supported, the documentation is non existent and ACITree is no longer being
>>>>>>>>>>>>>>>>>>>>>> actively developed.
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>> Our process:
>>>>>>>>>>>>>>>>>>>>>> 1. We "randomly" selected a function that is part of
>>>>>>>>>>>>>>>>>>>>>> the ACITree. From this point we decided to replace that function with our
>>>>>>>>>>>>>>>>>>>>>> own version. The function that we choose was "itemData".
>>>>>>>>>>>>>>>>>>>>>> The function gives us all the "data" that a specific
>>>>>>>>>>>>>>>>>>>>>> node of the tree find.
>>>>>>>>>>>>>>>>>>>>>> Given in order to replace the tree we would need to
>>>>>>>>>>>>>>>>>>>>>> have a function that would give us the same information. We had 2 options:
>>>>>>>>>>>>>>>>>>>>>> a) Create a tree with a function called itemData
>>>>>>>>>>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>>>>>>>>>>> - At first view this was the simpler solution
>>>>>>>>>>>>>>>>>>>>>> - Would keep the status quo
>>>>>>>>>>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>>>>>>>>>>> - Not a OOP approach
>>>>>>>>>>>>>>>>>>>>>> - Not very flexible
>>>>>>>>>>>>>>>>>>>>>> b) Create a tree that would return a node given an
>>>>>>>>>>>>>>>>>>>>>> ID and then the node would be responsible for giving it's data.
>>>>>>>>>>>>>>>>>>>>>> Pros:
>>>>>>>>>>>>>>>>>>>>>> - OOP Approach
>>>>>>>>>>>>>>>>>>>>>> - More flexible and we do not need to bring the tree
>>>>>>>>>>>>>>>>>>>>>> around, just a node
>>>>>>>>>>>>>>>>>>>>>> Cons:
>>>>>>>>>>>>>>>>>>>>>> - Break the current status quo
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>> Given these 2 options we decided to go for a more OOP
>>>>>>>>>>>>>>>>>>>>>> approach creating a Tree and a TreeNode classes, that in the future will be
>>>>>>>>>>>>>>>>>>>>>> renamed to ACITreeWrapper and TreeNode.
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>> 2. After we decided on the starting point we searched
>>>>>>>>>>>>>>>>>>>>>> for occurrences of the function "itemData" and we found out that there were
>>>>>>>>>>>>>>>>>>>>>> 303 occurrences of "itemData" in the code and roughly 176 calls to the
>>>>>>>>>>>>>>>>>>>>>> function itself (some of the hits were variable names).
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>> 3. We selected the first file on the search and found
>>>>>>>>>>>>>>>>>>>>>> the function that was responsible for calling the itemData function.
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>> 4. Extracted the function to a separate file
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>> 5. Wrap this function with tests
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>> 6. Refactor the function to ES6, give more
>>>>>>>>>>>>>>>>>>>>>> declarative names to variables and break the functions into smaller chunks
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>> 7. When all the tests were passing we replaced
>>>>>>>>>>>>>>>>>>>>>> ACITree with our Tree
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>> 8. We ensured that all tests were passing
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>> 9. Remove function from the original file and use the
>>>>>>>>>>>>>>>>>>>>>> new function
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>> 10. Ensure everything still works
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>> 11. Find the next function and execute from step 4
>>>>>>>>>>>>>>>>>>>>>> until all the functions are replaced, refactored and tested.
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>> As you can see by the process this is a pretty huge
>>>>>>>>>>>>>>>>>>>>>> undertake, because of the number of calls to the function. This is just the
>>>>>>>>>>>>>>>>>>>>>> first step on the direction of completely isolating the ACITree so that we
>>>>>>>>>>>>>>>>>>>>>> can solve the problem with a large number of elements on the tree.
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>> *What is on our radar that we need to address:*
>>>>>>>>>>>>>>>>>>>>>> - Finish the complete decoupling of the ACITree
>>>>>>>>>>>>>>>>>>>>>> - Performance of the current tree implementation
>>>>>>>>>>>>>>>>>>>>>> - Tweak the naming of the Tree class to explicitly
>>>>>>>>>>>>>>>>>>>>>> tell us this is to use only with ACITree.
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>> Thanks
>>>>>>>>>>>>>>>>>>>>>> Joao
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>>
>>>>>>>>>>>>> --
>>>>>>>>>>>>> Dave Page
>>>>>>>>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>>>>>>>>> Twitter: @pgsnake
>>>>>>>>>>>>>
>>>>>>>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>> --
>>>>>>>>>>>> Dave Page
>>>>>>>>>>>> Blog: http://pgsnake.blogspot.com
>>>>>>>>>>>> Twitter: @pgsnake
>>>>>>>>>>>>
>>>>>>>>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>>>>>>>>> The Enterprise PostgreSQL Company
>>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>
>>>>>>>>
>>>>>>>
>>>>>>
>>>>>
>>>>>
>>>>> --
>>>>> Dave Page
>>>>> Blog: http://pgsnake.blogspot.com
>>>>> Twitter: @pgsnake
>>>>>
>>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>>> The Enterprise PostgreSQL Company
>>>>>
>>>>
>>>>
>>
>
>
> --
> Dave Page
> Blog: http://pgsnake.blogspot.com
> Twitter: @pgsnake
>
> EnterpriseDB UK: http://www.enterprisedb.com
> The Enterprise PostgreSQL Company
>
Attachments:
[image/png] Screenshot from 2018-05-02 10-12-11.png (301.6K, 3-Screenshot%20from%202018-05-02%2010-12-11.png)
download | view image
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-04 22:01 Joao De Almeida Pereira <[email protected]>
parent: Joao De Almeida Pereira <[email protected]>
0 siblings, 3 replies; 69+ messages in thread
From: Joao De Almeida Pereira @ 2018-05-04 22:01 UTC (permalink / raw)
To: Dave Page <[email protected]>; +Cc: Ashesh Vashi <[email protected]>; Anthony Emengo <[email protected]>; Khushboo Vashi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
Hi Hackers,
*Be prepared this is going to be a big email.*
Objectives of this patch:
1. Start the work to split ACI from pgAdmin code
2. Add more tests around the front end components
3. Start the discussion on application architecture
1. Start the work to split ACI from pgAdmin code Why
This journey started on our first attempt to store state of the current
tree, so that when a user accessed again pgAdmin
there would be no need to reopen all the nodes and make the flow better.
Another problem users brought to us was related
to the UI behavior when the number of objects was too big.
In order to do implement this features we would have to play around with
aciTree or some functions from our code that
called and leveraged the aciTree functionalities. To do this we needed to
have some confidence that our changes would
implement correctly the new feature and it would not break the current
functionality.
The process that we used was first extract the functions that interacted
with the tree, wrap them with
tests and then refactor them in some way. While doing this work we realized
that the tree started spreading roots
throughout the majority of the application.
How
Options:
1. Rewrite the front end
2. One bang replace ACI Tree without change on the application
3. Create an abstraction layer between ACI Tree and the application
1. Rewrite the front end
Pros Const
Achieve decoupling by recreating the frontend Cost is too high
Do not rely on deprecated or unmaintained libraries Turn around is too long
One shot change 2. One bang replace ACI Tree
Pros Const
Achieve decoupling by recreating the frontend Cost is too high
Does not achieve the decoupling unless we change the application
Need to create an adaptor
No garantee of success
One shot change 3. Create an abstraction layer between ACI Tree and the
application
Pros Const
Achieve decoupling of the application and the tree 90% of the time on Code
archaeology
Lower cost
Ability to change or keep ACI Tree, decision TBD
Can be done in a iterative way
We decided that options 3 looked more attractive specially because we could
do it in a iterative way.
The next image depicts the current state of the interactions between the
application and the ACI and the place we want
to be in.
[image: Screen Shot 2018-05-04 at 4.06.17 PM.png]
for retrieving of data from the ACI Tree that then is used in different
ways.
Because we are doing this in a iterative process we need to keep the
current tree working and create our adaptor next
to it. In order to do that we created a new property on PGBrowser class
called treeMenu and when ACI Tree receives
the node information we also populate the new Tree with all the needed
information.
This approach allow us not to worry, for now, with data retrieval, URL
system and other issues that should be addressed
in the future when the adaptor is done.
This first patch tries to deal with the low hanging fruit, functions that
are triggered by events from ACI Tree.
Cases like registering to events and triggering events need to be handled
in the future as well.
2. Add more tests around the front end components Why
Go Fast Forever: https://builttoadapt.io/why-tdd-489fdcdda05e
I think this is a very good summary of why do we need tests in our
applications.
3. Start the discussion on application architecture
Why should we care about location of files inside a our application?
Why is this way better the another?
These are 2 good questions that have very lengthy answers. Trying to more
or less summarize the answers we care about
the location of the files, because we want our application to communicate
intent and there are always pros and cons
on all the decisions that we make.
At this point the application structure follows our menu, this approach
eventually make is easier to follow the code
but at the same time if the menu changes down the line, will we change the
structure of our folders?
The proposal that we do with the last diff of this patch is to change to a
structure that slices vertically the
application. This way we can understand intent behind the code and more
easily find what we are looking for.
In the current structure if you want to see the tables code you need to go
to
pgAdmin/browser/server_groups/servers/databases/schemas/tables/ this is a
huge path to remember and to get to. What
do we win with this? If we open pgAdmin we know which nodes to click in
order to get to tables. But for development
every time that you are looking for a specific functionality you need to
run the application, navigate the menu so that
you know where you can find the code. This doesn’t sound very appealing.
What if our structure would look like this:
- web
- tables
- controller
- get_nodes.py
- get_sql.py
- __init__.py
- frontend
- component
- ddl_component.js
- services
- table-service.js
- schemas
- servers
- ....
This would saves us time because all the information that we need is what
are we working on and everything is right there.
Menu driven structure Intent Driven Structure
*Pros:* *Pros:*
Already in place Explicitly shows features
Self contained features Self contained features
Support for drop in features Support for drop in features
*Cons:* *Cons:*
Follows the menu, and it could change Need to change current code
Hard to find features Some additional plumbing might be needed
Drop in features need to be placed in a specific location according to the
menu location
What are your thought about this architecture?
Around minute 7 of this video <https://www.youtube.com/watch?v=hALFGQNeEnU;
Uncle Bob shows an application written
in rails to talk about architecture. It is a long KeyNote if you are
curious I would advise you to see the full video.
His approach to architecture of the application is pretty interesting.
------------------------------
Patches 0001 Change the order of the shims on the shim file
Simple change that orders the shims inside the shim file
0002 New tree implementation
This is the first version of our Tree implementation. At this point is a
very simple tree without no abstractions and
with code that eventually is not very performant, but this is only the
first iteration and we are trying to follow the
Last Responsible Moment Principle
<https://medium.com/@aidanjcasey/guiding-principles-for-an-evolutionary-software-architecture-b6dc2cb...;
.
What can you find in this patch:
- Creation of PGBrowser.treeMenu
- Initial version of the Tree Adaptor web/pgadmin/static/js/tree/tree.js
- TreeFake test double <https://martinfowler.com/bliki/TestDouble.html;
that can replace the Tree for testing purposes
- Tests. As an interesting asside because Fake’s need to behave like the
real object you will noticed that there are
tests for this type of double and they the same as of the real object.
0003 Extract, Test and Refactor Methods
This is the patch that contains the change that we talked a in the
objectives. It touches in a subset of the places
where itemData function is called. To have a sense of progress the
following image depicts, on the left all places
where this functions can be found in the code and on the right the places
where it still remains after this patch.
But this patch is not only related to the ACI Tree, it also creates some
abstractions from code that we found repeated.
Some examples of this are the dialogs for Backup and Restore, where the
majority of the logic was common, so we created
in web/pgadmin/static/js/alertify/ 3 different objects:
A Factory DialogFactory that is responsible for creating new Dialog and
these dialogs will use the DialogWrapper.
This is also a good example of the Last Responsible Moment Principle
<https://medium.com/@aidanjcasey/guiding-principles-for-an-evolutionary-software-architecture-b6dc2cb...;
,
in Dialog there are still some functions that are used in a Recover and
Backup scenario. When we have more examples
we will be able to, if we want, to extract that into another function.
While doing some code refactoring we found out that the Right Click Menu as
some checks that can be centralized. Examples
of these are checks for Enable/Disable of an option of the display of the
Create/Edit/Drop option in some menus.
Check web/pgadmin/static/js/menu/ for more information. This simplified
code like:
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create domain
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-domain' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-domain', item, data);
}
This refactor was made in a couple of places throughout the code.
Another refactor that had to be done was the creation of the functions
getTreeNodeHierarchyFromElement and
getTreeNodeHierarchyFromIdentifier that replace the old getTreeNodeHierarchy.
This implementation was done
because for the time being we have 2 different types of trees and calls to
them.
The rest of the changes resulted from our process of refactoring that we
are following:
1. Find a location where itemData is used
2. Extract that into a function outside of the current code
3. Wrap it with tests
4. When we have enough confidence on the tests, we refactor the function
to use the new Tree
5. We refactor the function to become more readable
6. Start over
0004 Architecture
The proposed structure is our vision for the application, this is not the
state we will be in when this patch lands.
For now the only change we are doing is move the javascript into
static/js/FEATURE_NAME. This is a first step in the
direction that we would like to see the product heading.
Even if we decide to keep using the same architecture the move of
Javascript to a centralized place will
not break any plugability as webpack can retrieve all the Javascript from
any place in the code.
Have a nice weekend
Joao
Attachments:
[image/png] Screenshot from 2018-05-02 10-12-11.png (301.6K, 3-Screenshot%20from%202018-05-02%2010-12-11.png)
download | view image
[image/png] Screen Shot 2018-05-04 at 4.06.17 PM.png (104.4K, 4-Screen%20Shot%202018-05-04%20at%204.06.17%20PM.png)
download | view image
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-04 22:03 Joao De Almeida Pereira <[email protected]>
parent: Joao De Almeida Pereira <[email protected]>
2 siblings, 1 reply; 69+ messages in thread
From: Joao De Almeida Pereira @ 2018-05-04 22:03 UTC (permalink / raw)
To: Dave Page <[email protected]>; +Cc: Ashesh Vashi <[email protected]>; Anthony Emengo <[email protected]>; Khushboo Vashi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
And now the patches......
On Fri, May 4, 2018 at 6:01 PM Joao De Almeida Pereira <
[email protected]> wrote:
> Hi Hackers,
>
> *Be prepared this is going to be a big email.*
> Objectives of this patch:
>
> 1. Start the work to split ACI from pgAdmin code
> 2. Add more tests around the front end components
> 3. Start the discussion on application architecture
>
> 1. Start the work to split ACI from pgAdmin code Why
>
> This journey started on our first attempt to store state of the current
> tree, so that when a user accessed again pgAdmin
> there would be no need to reopen all the nodes and make the flow better.
> Another problem users brought to us was related
> to the UI behavior when the number of objects was too big.
>
> In order to do implement this features we would have to play around with
> aciTree or some functions from our code that
> called and leveraged the aciTree functionalities. To do this we needed to
> have some confidence that our changes would
> implement correctly the new feature and it would not break the current
> functionality.
> The process that we used was first extract the functions that interacted
> with the tree, wrap them with
> tests and then refactor them in some way. While doing this work we
> realized that the tree started spreading roots
> throughout the majority of the application.
> How
>
> Options:
>
> 1. Rewrite the front end
> 2. One bang replace ACI Tree without change on the application
> 3. Create an abstraction layer between ACI Tree and the application
>
> 1. Rewrite the front end
> Pros Const
> Achieve decoupling by recreating the frontend Cost is too high
> Do not rely on deprecated or unmaintained libraries Turn around is too
> long
> One shot change 2. One bang replace ACI Tree
> Pros Const
> Achieve decoupling by recreating the frontend Cost is too high
> Does not achieve the decoupling unless we change the application
> Need to create an adaptor
> No garantee of success
> One shot change 3. Create an abstraction layer between ACI Tree and the
> application
> Pros Const
> Achieve decoupling of the application and the tree 90% of the time on
> Code archaeology
> Lower cost
> Ability to change or keep ACI Tree, decision TBD
> Can be done in a iterative way
>
> We decided that options 3 looked more attractive specially because we
> could do it in a iterative way.
>
> The next image depicts the current state of the interactions between the
> application and the ACI and the place we want
> to be in.
>
> [image: Screen Shot 2018-05-04 at 4.06.17 PM.png]
>
> for retrieving of data from the ACI Tree that then is used in different
> ways.
>
> Because we are doing this in a iterative process we need to keep the
> current tree working and create our adaptor next
> to it. In order to do that we created a new property on PGBrowser class
> called treeMenu and when ACI Tree receives
> the node information we also populate the new Tree with all the needed
> information.
>
> This approach allow us not to worry, for now, with data retrieval, URL
> system and other issues that should be addressed
> in the future when the adaptor is done.
>
> This first patch tries to deal with the low hanging fruit, functions that
> are triggered by events from ACI Tree.
> Cases like registering to events and triggering events need to be handled
> in the future as well.
> 2. Add more tests around the front end components Why
>
> Go Fast Forever: https://builttoadapt.io/why-tdd-489fdcdda05e
> I think this is a very good summary of why do we need tests in our
> applications.
> 3. Start the discussion on application architecture
>
> Why should we care about location of files inside a our application?
>
> Why is this way better the another?
>
> These are 2 good questions that have very lengthy answers. Trying to more
> or less summarize the answers we care about
> the location of the files, because we want our application to communicate
> intent and there are always pros and cons
> on all the decisions that we make.
>
> At this point the application structure follows our menu, this approach
> eventually make is easier to follow the code
> but at the same time if the menu changes down the line, will we change the
> structure of our folders?
>
> The proposal that we do with the last diff of this patch is to change to a
> structure that slices vertically the
> application. This way we can understand intent behind the code and more
> easily find what we are looking for.
>
> In the current structure if you want to see the tables code you need to go
> to
> pgAdmin/browser/server_groups/servers/databases/schemas/tables/ this is a
> huge path to remember and to get to. What
> do we win with this? If we open pgAdmin we know which nodes to click in
> order to get to tables. But for development
> every time that you are looking for a specific functionality you need to
> run the application, navigate the menu so that
> you know where you can find the code. This doesn’t sound very appealing.
>
> What if our structure would look like this:
>
> - web
> - tables
> - controller
> - get_nodes.py
> - get_sql.py
> - __init__.py
> - frontend
> - component
> - ddl_component.js
> - services
> - table-service.js
> - schemas
> - servers
> - ....
>
> This would saves us time because all the information that we need is what
> are we working on and everything is right there.
> Menu driven structure Intent Driven Structure
> *Pros:* *Pros:*
> Already in place Explicitly shows features
> Self contained features Self contained features
> Support for drop in features Support for drop in features
> *Cons:* *Cons:*
> Follows the menu, and it could change Need to change current code
> Hard to find features Some additional plumbing might be needed
> Drop in features need to be placed in a specific location according to the
> menu location
>
> What are your thought about this architecture?
>
> Around minute 7 of this video
> <https://www.youtube.com/watch?v=hALFGQNeEnU; Uncle Bob shows an
> application written
> in rails to talk about architecture. It is a long KeyNote if you are
> curious I would advise you to see the full video.
> His approach to architecture of the application is pretty interesting.
> ------------------------------
> Patches 0001 Change the order of the shims on the shim file
>
> Simple change that orders the shims inside the shim file
> 0002 New tree implementation
>
> This is the first version of our Tree implementation. At this point is a
> very simple tree without no abstractions and
> with code that eventually is not very performant, but this is only the
> first iteration and we are trying to follow the
> Last Responsible Moment Principle
> <https://medium.com/@aidanjcasey/guiding-principles-for-an-evolutionary-software-architecture-b6dc2cb...;
> .
>
> What can you find in this patch:
>
> - Creation of PGBrowser.treeMenu
> - Initial version of the Tree Adaptor
> web/pgadmin/static/js/tree/tree.js
> - TreeFake test double <https://martinfowler.com/bliki/TestDouble.html;
> that can replace the Tree for testing purposes
> - Tests. As an interesting asside because Fake’s need to behave like
> the real object you will noticed that there are
> tests for this type of double and they the same as of the real object.
>
> 0003 Extract, Test and Refactor Methods
>
> This is the patch that contains the change that we talked a in the
> objectives. It touches in a subset of the places
> where itemData function is called. To have a sense of progress the
> following image depicts, on the left all places
> where this functions can be found in the code and on the right the places
> where it still remains after this patch.
>
> But this patch is not only related to the ACI Tree, it also creates some
> abstractions from code that we found repeated.
> Some examples of this are the dialogs for Backup and Restore, where the
> majority of the logic was common, so we created
> in web/pgadmin/static/js/alertify/ 3 different objects:
> A Factory DialogFactory that is responsible for creating new Dialog and
> these dialogs will use the DialogWrapper.
>
> This is also a good example of the Last Responsible Moment Principle
> <https://medium.com/@aidanjcasey/guiding-principles-for-an-evolutionary-software-architecture-b6dc2cb...;
> ,
> in Dialog there are still some functions that are used in a Recover and
> Backup scenario. When we have more examples
> we will be able to, if we want, to extract that into another function.
>
> While doing some code refactoring we found out that the Right Click Menu
> as some checks that can be centralized. Examples
> of these are checks for Enable/Disable of an option of the display of the
> Create/Edit/Drop option in some menus.
> Check web/pgadmin/static/js/menu/ for more information. This simplified
> code like:
>
> canCreate: function(itemData, item, data) {
> - //If check is false then , we will allow create menu
> - if (data && data.check == false)
> - return true;
> -
> - var t = pgBrowser.tree, i = item, d = itemData;
> - // To iterate over tree to check parent node
> - while (i) {
> - // If it is schema then allow user to create domain
> - if (_.indexOf(['schema'], d._type) > -1)
> - return true;
> -
> - if ('coll-domain' == d._type) {
> - //Check if we are not child of catalog
> - var prev_i = t.hasParent(i) ? t.parent(i) : null,
> - prev_d = prev_i ? t.itemData(prev_i) : null;
> - if( prev_d._type == 'catalog') {
> - return false;
> - } else {
> - return true;
> - }
> - }
> - i = t.hasParent(i) ? t.parent(i) : null;
> - d = i ? t.itemData(i) : null;
> - }
> - // by default we do not want to allow create menu
> - return true;
> + return canCreate.canCreate(pgBrowser, 'coll-domain', item, data);
> }
>
> This refactor was made in a couple of places throughout the code.
>
> Another refactor that had to be done was the creation of the functions
> getTreeNodeHierarchyFromElement and
> getTreeNodeHierarchyFromIdentifier that replace the old
> getTreeNodeHierarchy. This implementation was done
> because for the time being we have 2 different types of trees and calls to
> them.
>
> The rest of the changes resulted from our process of refactoring that we
> are following:
>
> 1. Find a location where itemData is used
> 2. Extract that into a function outside of the current code
> 3. Wrap it with tests
> 4. When we have enough confidence on the tests, we refactor the
> function to use the new Tree
> 5. We refactor the function to become more readable
> 6. Start over
>
> 0004 Architecture
>
> The proposed structure is our vision for the application, this is not the
> state we will be in when this patch lands.
> For now the only change we are doing is move the javascript into
> static/js/FEATURE_NAME. This is a first step in the
> direction that we would like to see the product heading.
>
> Even if we decide to keep using the same architecture the move of
> Javascript to a centralized place will
> not break any plugability as webpack can retrieve all the Javascript from
> any place in the code.
>
> Have a nice weekend
> Joao
>
>
>
Attachments:
[image/png] Screen Shot 2018-05-04 at 4.06.17 PM.png (104.4K, 3-Screen%20Shot%202018-05-04%20at%204.06.17%20PM.png)
download | view image
[application/octet-stream] 0001-Change-the-order-of-the-shims-on-the-shim-file.patch (22.4K, 4-0001-Change-the-order-of-the-shims-on-the-shim-file.patch)
download | inline diff:
diff --git a/web/webpack.shim.js b/web/webpack.shim.js
index 1b6442a6..487d73cb 100644
--- a/web/webpack.shim.js
+++ b/web/webpack.shim.js
@@ -129,6 +129,7 @@ var webpackShimConfig = {
'sources/gettext': path.join(__dirname, './pgadmin/static/js/gettext'),
'sources/utils': path.join(__dirname, './pgadmin/static/js/utils'),
'babel-polyfill': path.join(__dirname, './node_modules/babel-polyfill/dist/polyfill'),
+ 'tools': path.join(__dirname, './pgadmin/tools/'),
// Vendor JS
'jquery': path.join(__dirname, './node_modules/jquery/dist/jquery'),
@@ -168,105 +169,105 @@ var webpackShimConfig = {
'pgadmin.backform': path.join(__dirname, './pgadmin/static/js/backform.pgadmin'),
'pgadmin.backgrid': path.join(__dirname, './pgadmin/static/js/backgrid.pgadmin'),
- 'pgadmin.browser.panel': path.join(__dirname, './pgadmin/browser/static/js/panel'),
- 'pgadmin.node.language': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/languages/static/js/language'),
- 'pgadmin.node.mview': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview'),
- 'pgadmin.tools.maintenance': path.join(__dirname, './pgadmin/tools/maintenance/static/js/maintenance'),
- 'pgadmin.node.pga_jobstep': path.join(__dirname, './pgadmin/browser/server_groups/servers/pgagent/steps/static/js/pga_jobstep'),
- 'pgadmin.sqleditor': path.join(__dirname, './pgadmin/tools/sqleditor/static/js/sqleditor'),
- 'pgadmin.node.foreign_server': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/foreign_servers/static/js/foreign_server'),
- 'pgadmin.node.domain': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain'),
- 'pgadmin.tools.user_management': path.join(__dirname, './pgadmin/tools/user_management/static/js/user_management'),
+ 'pgadmin.about': path.join(__dirname, './pgadmin/about/static/js/about'),
'pgadmin.browser': path.join(__dirname, './pgadmin/browser/static/js/browser'),
+ 'pgadmin.browser.bgprocess': path.join(__dirname, './pgadmin/misc/bgprocess/static/js/bgprocess'),
+ 'pgadmin.browser.collection': path.join(__dirname, './pgadmin/browser/static/js/collection'),
+ 'pgadmin.browser.datamodel': path.join(__dirname, './pgadmin/browser/static/js/datamodel'),
+ 'pgadmin.browser.endpoints': '/browser/js/endpoints',
+ 'pgadmin.browser.error': path.join(__dirname, './pgadmin/browser/static/js/error'),
+ 'pgadmin.browser.frame': path.join(__dirname, './pgadmin/browser/static/js/frame'),
+ 'pgadmin.browser.keyboard': path.join(__dirname, './pgadmin/browser/static/js/keyboard'),
+ 'pgadmin.browser.menu': path.join(__dirname, './pgadmin/browser/static/js/menu'),
+ 'pgadmin.browser.messages': '/browser/js/messages',
+ 'pgadmin.browser.node': path.join(__dirname, './pgadmin/browser/static/js/node'),
+ 'pgadmin.browser.node.ui': path.join(__dirname, './pgadmin/browser/static/js/node.ui'),
+ 'pgadmin.browser.object_depends': path.join(__dirname, './pgadmin/misc/depends/static/js/depends'),
+ 'pgadmin.browser.object_sql': path.join(__dirname, './pgadmin/misc/sql/static/js/sql'),
'pgadmin.browser.object_statistics': path.join(__dirname, './pgadmin/misc/statistics/static/js/statistics'),
- 'pgadmin.node.constraints': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints'),
+ 'pgadmin.browser.panel': path.join(__dirname, './pgadmin/browser/static/js/panel'),
+ 'pgadmin.browser.server.privilege': path.join(__dirname, './pgadmin/browser/server_groups/servers/static/js/privilege'),
+ 'pgadmin.browser.server.variable': path.join(__dirname, './pgadmin/browser/server_groups/servers/static/js/variable'),
'pgadmin.browser.table.partition.utils': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/partition.utils'),
+ 'pgadmin.browser.utils': '/browser/js/utils',
+ 'pgadmin.browser.wizard': path.join(__dirname, './pgadmin/browser/static/js/wizard'),
+ 'pgadmin.dashboard': path.join(__dirname, './pgadmin/dashboard/static/js/dashboard'),
+ 'pgadmin.datagrid': path.join(__dirname, './pgadmin/tools/datagrid/static/js/datagrid'),
+ 'pgadmin.file_manager': path.join(__dirname, './pgadmin/misc/file_manager/static/js/file_manager'),
+ 'pgadmin.file_utility': path.join(__dirname, './pgadmin/misc/file_manager/static/js/utility'),
+ 'pgadmin.misc.explain': path.join(__dirname, './pgadmin/misc/static/explain/js/explain'),
+ 'pgadmin.node.cast': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/casts/static/js/cast'),
'pgadmin.node.catalog': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/static/js/catalog'),
- 'pgadmin.node.role': path.join(__dirname, './pgadmin/browser/server_groups/servers/roles/static/js/role'),
+ 'pgadmin.node.catalog_object': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/catalog_objects/static/js/catalog_object'),
+ 'pgadmin.node.catalog_object_column': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/catalog_objects/columns/static/js/catalog_object_column'),
'pgadmin.node.check_constraint': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint'),
- 'pgadmin.node.user_mapping': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/foreign_servers/user_mapping/static/js/user_mapping'),
- 'pgadmin.browser.messages': '/browser/js/messages',
'pgadmin.node.collation': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation'),
- 'pgadmin.browser.endpoints': '/browser/js/endpoints',
- 'pgadmin.node.table': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table'),
- 'pgadmin.node.tablespace': path.join(__dirname, './pgadmin/browser/server_groups/servers/tablespaces/static/js/tablespace'),
- 'pgadmin.node.server': path.join(__dirname, './pgadmin/browser/server_groups/servers/static/js/server'),
- 'pgadmin.node.package': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/packages/static/js/package'),
- 'pgadmin.browser.menu': path.join(__dirname, './pgadmin/browser/static/js/menu'),
- 'pgadmin.browser.node.ui': path.join(__dirname, './pgadmin/browser/static/js/node.ui'),
- 'pgadmin.node.rule': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule'),
- 'pgadmin.node.server_group': path.join(__dirname, './pgadmin/browser/server_groups/static/js/server_group'),
- 'pgadmin.tools.backup': path.join(__dirname, './pgadmin/tools/backup/static/js/backup'),
- 'pgadmin.node.fts_configuration': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration'),
+ 'pgadmin.node.column': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column'),
+ 'pgadmin.node.constraints': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints'),
+ 'pgadmin.node.database': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/static/js/database'),
+ 'pgadmin.node.domain': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain'),
+ 'pgadmin.node.domain_constraints': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/domains/domain_constraints/static/js/domain_constraints'),
'pgadmin.node.event_trigger': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/event_triggers/static/js/event_trigger'),
- 'pgadmin.browser.bgprocess': path.join(__dirname, './pgadmin/misc/bgprocess/static/js/bgprocess'),
- 'pgadmin.node.pga_job': path.join(__dirname, './pgadmin/browser/server_groups/servers/pgagent/static/js/pga_job'),
- 'pgadmin.node.function': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function'),
- 'pgadmin.file_utility': path.join(__dirname, './pgadmin/misc/file_manager/static/js/utility'),
- 'pgadmin.browser.object_depends': path.join(__dirname, './pgadmin/misc/depends/static/js/depends'),
'pgadmin.node.edbfunc': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/packages/edbfuncs/static/js/edbfunc'),
- 'pgadmin.node.sequence': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence'),
'pgadmin.node.edbproc': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/packages/edbfuncs/static/js/edbproc'),
- 'pgadmin.browser.object_sql': path.join(__dirname, './pgadmin/misc/sql/static/js/sql'),
- 'pgadmin.preferences': path.join(__dirname, './pgadmin/preferences/static/js/preferences'),
- 'pgadmin.node.domain_constraints': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/domains/domain_constraints/static/js/domain_constraints'),
- 'slick.pgadmin.formatters': path.join(__dirname, './pgadmin/tools/../static/js/slickgrid/formatters'),
- 'pgadmin.node.catalog_object': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/catalog_objects/static/js/catalog_object'),
- 'pgadmin.dashboard': path.join(__dirname, './pgadmin/dashboard/static/js/dashboard'),
- 'pgadmin.node.foreign_data_wrapper': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/static/js/foreign_data_wrapper'),
+ 'pgadmin.node.edbvar': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/packages/edbvars/static/js/edbvar'),
+ 'pgadmin.node.exclusion_constraint': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint'),
+ 'pgadmin.node.extension': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/extensions/static/js/extension'),
'pgadmin.node.external_table': path.join(__dirname, './pgadmin/static/js/browser/server_groups/servers/databases/external_tables/index'),
- 'pgadmin.node.external_tables': path.join(__dirname, './pgadmin/static/js/browser/server_groups/servers/databases/external_tables/index'),
+ 'pgadmin.node.foreign_data_wrapper': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/static/js/foreign_data_wrapper'),
'pgadmin.node.foreign_key': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key'),
- 'pgadmin.browser.server.variable': path.join(__dirname, './pgadmin/browser/server_groups/servers/static/js/variable'),
- 'pgadmin.tools.grant_wizard': path.join(__dirname, './pgadmin/tools/grant_wizard/static/js/grant_wizard'),
- 'pgadmin.browser.datamodel': path.join(__dirname, './pgadmin/browser/static/js/datamodel'),
- 'pgadmin.tools.restore': path.join(__dirname, './pgadmin/tools/restore/static/js/restore'),
+ 'pgadmin.node.foreign_server': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/foreign_servers/static/js/foreign_server'),
+ 'pgadmin.node.foreign_table': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table'),
+ 'pgadmin.node.fts_configuration': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration'),
+ 'pgadmin.node.fts_dictionary': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary'),
+ 'pgadmin.node.fts_parser': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser'),
+ 'pgadmin.node.fts_template': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template'),
+ 'pgadmin.node.function': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function'),
+ 'pgadmin.node.index': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index'),
+ 'pgadmin.node.language': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/languages/static/js/language'),
+ 'pgadmin.node.mview': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview'),
+ 'pgadmin.node.package': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/packages/static/js/package'),
+ 'pgadmin.node.partition': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition'),
+ 'pgadmin.node.pga_job': path.join(__dirname, './pgadmin/browser/server_groups/servers/pgagent/static/js/pga_job'),
+ 'pgadmin.node.pga_jobstep': path.join(__dirname, './pgadmin/browser/server_groups/servers/pgagent/steps/static/js/pga_jobstep'),
+ 'pgadmin.node.pga_schedule': path.join(__dirname, './pgadmin/browser/server_groups/servers/pgagent/schedules/static/js/pga_schedule'),
+ 'pgadmin.node.primary_key': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key'),
'pgadmin.node.procedure': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/procedure'),
- 'pgadmin.browser.server.privilege': path.join(__dirname, './pgadmin/browser/server_groups/servers/static/js/privilege'),
'pgadmin.node.resource_group': path.join(__dirname, './pgadmin/browser/server_groups/servers/resource_groups/static/js/resource_group'),
- 'pgadmin.node.exclusion_constraint': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint'),
- 'pgadmin.node.primary_key': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key'),
- 'pgadmin.tools.debugger.direct': path.join(__dirname, './pgadmin/tools/debugger/static/js/direct'),
- 'pgadmin.settings': path.join(__dirname, './pgadmin/settings/static/js/settings'),
+ 'pgadmin.node.role': path.join(__dirname, './pgadmin/browser/server_groups/servers/roles/static/js/role'),
+ 'pgadmin.node.rule': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule'),
'pgadmin.node.schema': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema'),
- 'pgadmin.browser.error': path.join(__dirname, './pgadmin/browser/static/js/error'),
- 'pgadmin.tools.import_export': path.join(__dirname, './pgadmin/tools/import_export/static/js/import_export'),
- 'pgadmin.node.view': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view'),
- 'pgadmin.tools.debugger.ui': path.join(__dirname, './pgadmin/tools/debugger/static/js/debugger_ui'),
- 'pgadmin.tools.debugger.utils': path.join(__dirname, './pgadmin/tools/debugger/static/js/debugger_utils'),
- 'pgadmin.node.pga_schedule': path.join(__dirname, './pgadmin/browser/server_groups/servers/pgagent/schedules/static/js/pga_schedule'),
- 'pgadmin.node.catalog_object_column': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/catalog_objects/columns/static/js/catalog_object_column'),
- 'pgadmin.browser.collection': path.join(__dirname, './pgadmin/browser/static/js/collection'),
- 'pgadmin.browser.node': path.join(__dirname, './pgadmin/browser/static/js/node'),
- 'pgadmin.misc.explain': path.join(__dirname, './pgadmin/misc/static/explain/js/explain'),
+ 'pgadmin.node.schema.dir': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/static/js/'),
+ 'pgadmin.node.sequence': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence'),
+ 'pgadmin.node.server': path.join(__dirname, './pgadmin/browser/server_groups/servers/static/js/server'),
+ 'pgadmin.node.server_group': path.join(__dirname, './pgadmin/browser/server_groups/static/js/server_group'),
'pgadmin.node.synonym': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/synonyms/static/js/synonym'),
- 'pgadmin.node.extension': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/extensions/static/js/extension'),
- 'pgadmin.node.unique_constraint': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint'),
- 'pgadmin.node.database': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/static/js/database'),
- 'pgadmin.node.edbvar': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/packages/edbvars/static/js/edbvar'),
+ 'pgadmin.node.table': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table'),
+ 'pgadmin.node.tablespace': path.join(__dirname, './pgadmin/browser/server_groups/servers/tablespaces/static/js/tablespace'),
'pgadmin.node.trigger': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger'),
- 'pgadmin.browser.wizard': path.join(__dirname, './pgadmin/browser/static/js/wizard'),
- 'pgadmin.server.supported_servers': '/browser/server/supported_servers',
- 'pgadmin.node.partition': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition'),
- 'pgadmin.node.fts_template': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template'),
- 'pgadmin.node.cast': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/casts/static/js/cast'),
- 'pgadmin.tools.debugger.controller': path.join(__dirname, './pgadmin/tools/debugger/static/js/debugger'),
- 'pgadmin.node.fts_parser': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser'),
- 'pgadmin.browser.utils': '/browser/js/utils',
- 'pgadmin.about': path.join(__dirname, './pgadmin/about/static/js/about'),
- 'slick.pgadmin.editors': path.join(__dirname, './pgadmin/tools/../static/js/slickgrid/editors'),
- 'pgadmin.node.column': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column'),
- 'pgadmin.node.foreign_table': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table'),
- 'pgadmin.node.fts_dictionary': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary'),
- 'pgadmin.datagrid': path.join(__dirname, './pgadmin/tools/datagrid/static/js/datagrid'),
'pgadmin.node.trigger_function': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function'),
- 'pgadmin.user_management.current_user': '/user_management/current_user',
- 'pgadmin.node.index': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index'),
- 'pgadmin.browser.frame': path.join(__dirname, './pgadmin/browser/static/js/frame'),
'pgadmin.node.type': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type'),
- 'pgadmin.file_manager': path.join(__dirname, './pgadmin/misc/file_manager/static/js/file_manager'),
- 'pgadmin.browser.keyboard': path.join(__dirname, './pgadmin/browser/static/js/keyboard'),
+ 'pgadmin.node.unique_constraint': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint'),
+ 'pgadmin.node.user_mapping': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/foreign_servers/user_mapping/static/js/user_mapping'),
+ 'pgadmin.node.view': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view'),
+ 'pgadmin.preferences': path.join(__dirname, './pgadmin/preferences/static/js/preferences'),
+ 'pgadmin.settings': path.join(__dirname, './pgadmin/settings/static/js/settings'),
+ 'pgadmin.server.supported_servers': '/browser/server/supported_servers',
+ 'pgadmin.sqleditor': path.join(__dirname, './pgadmin/tools/sqleditor/static/js/sqleditor'),
'pgadmin.tables.js': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/'),
+ 'pgadmin.tools.backup': path.join(__dirname, './pgadmin/tools/backup/static/js/backup'),
+ 'pgadmin.tools.debugger.controller': path.join(__dirname, './pgadmin/tools/debugger/static/js/debugger'),
+ 'pgadmin.tools.debugger.direct': path.join(__dirname, './pgadmin/tools/debugger/static/js/direct'),
+ 'pgadmin.tools.debugger.ui': path.join(__dirname, './pgadmin/tools/debugger/static/js/debugger_ui'),
+ 'pgadmin.tools.debugger.utils': path.join(__dirname, './pgadmin/tools/debugger/static/js/debugger_utils'),
+ 'pgadmin.tools.grant_wizard': path.join(__dirname, './pgadmin/tools/grant_wizard/static/js/grant_wizard'),
+ 'pgadmin.tools.import_export': path.join(__dirname, './pgadmin/tools/import_export/static/js/import_export'),
+ 'pgadmin.tools.maintenance': path.join(__dirname, './pgadmin/tools/maintenance/static/js/maintenance'),
+ 'pgadmin.tools.restore': path.join(__dirname, './pgadmin/tools/restore/static/js/restore'),
+ 'pgadmin.tools.user_management': path.join(__dirname, './pgadmin/tools/user_management/static/js/user_management'),
+ 'pgadmin.user_management.current_user': '/user_management/current_user',
+ 'slick.pgadmin.editors': path.join(__dirname, './pgadmin/tools/../static/js/slickgrid/editors'),
+ 'slick.pgadmin.formatters': path.join(__dirname, './pgadmin/tools/../static/js/slickgrid/formatters'),
},
externals: [
'pgadmin.user_management.current_user',
--
2.16.2
[application/octet-stream] 0002-New-tree-implementation.patch (22.9K, 5-0002-New-tree-implementation.patch)
download | inline diff:
diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js
index 897d2708..fc68089f 100644
--- a/web/pgadmin/browser/static/js/browser.js
+++ b/web/pgadmin/browser/static/js/browser.js
@@ -1,4 +1,5 @@
define('pgadmin.browser', [
+ 'sources/tree/tree',
'sources/gettext', 'sources/url_for', 'require', 'jquery', 'underscore', 'underscore.string',
'bootstrap', 'sources/pgadmin', 'pgadmin.alertifyjs', 'bundled_codemirror',
'sources/check_node_visibility', 'sources/modify_animation', 'pgadmin.browser.utils', 'wcdocker',
@@ -10,6 +11,7 @@ define('pgadmin.browser', [
'sources/codemirror/addon/fold/pgadmin-sqlfoldcode',
'pgadmin.browser.keyboard',
], function(
+ tree,
gettext, url_for, require, $, _, S, Bootstrap, pgAdmin, Alertify,
codemirror, checkNodeVisibility, modifyAnimation
) {
@@ -86,6 +88,7 @@ define('pgadmin.browser', [
});
b.tree = $('#tree').aciTree('api');
+ b.treeMenu.register($('#tree'));
};
// Extend the browser class attributes
@@ -100,6 +103,7 @@ define('pgadmin.browser', [
editor:null,
// Left hand browser tree
tree:null,
+ treeMenu: new tree.Tree(),
// list of script to be loaded, when a certain type of node is loaded
// It will be used to register extensions, tools, child node scripts,
// etc.
diff --git a/web/pgadmin/static/js/tree/tree.js b/web/pgadmin/static/js/tree/tree.js
new file mode 100644
index 00000000..974ee8de
--- /dev/null
+++ b/web/pgadmin/static/js/tree/tree.js
@@ -0,0 +1,214 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+export class TreeNode {
+ constructor(id, data, domNode, parent) {
+ this.id = id;
+ this.data = data;
+ this.setParent(parent);
+ this.children = [];
+ this.domNode = domNode;
+ }
+
+ hasParent() {
+ return this.parentNode !== null && this.parentNode !== undefined;
+ }
+
+ parent() {
+ return this.parentNode;
+ }
+
+ setParent(parent) {
+ this.parentNode = parent;
+ this.path = this.id;
+ if (parent !== null && parent !== undefined && parent.path !== undefined) {
+ this.path = parent.path + '.' + this.id;
+ }
+ }
+
+ getData() {
+ if (this.data === undefined) {
+ return undefined;
+ } else if (this.data === null) {
+ return null;
+ }
+ return Object.assign({}, this.data);
+ }
+
+ getHtmlIdentifier() {
+ return this.domNode;
+ }
+
+ reload(tree) {
+ this.unload(tree);
+ tree.aciTreeApi.setInode(this.domNode);
+ tree.aciTreeApi.deselect(this.domNode);
+ setTimeout(() => {
+ tree.selectNode(this.domNode);
+ }, 0);
+ }
+
+ unload(tree) {
+ this.children = [];
+ tree.aciTreeApi.unload(this.domNode);
+ }
+
+ anyParent(condition) {
+ let node = this;
+
+ while (node.hasParent()) {
+ node = node.parent();
+ if (condition(node)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Given a condition returns true if the current node
+ * or any of the parent nodes condition result is true
+ */
+ anyFamilyMember(condition) {
+ if(condition(this)) {
+ return true;
+ }
+
+ return this.anyParent(condition);
+ }
+}
+
+export class Tree {
+ constructor() {
+ this.rootNode = new TreeNode(undefined, {});
+ this.aciTreeApi = undefined;
+ }
+
+ addNewNode(id, data, domNode, parentPath) {
+ const parent = this.findNode(parentPath);
+ return this.createOrUpdateNode(id, data, parent, domNode);
+ }
+
+ findNode(path) {
+ if (path === null || path === undefined || path.length === 0) {
+ return this.rootNode;
+ }
+ return findInTree(this.rootNode, path.join('.'));
+ }
+
+ findNodeByDomElement(domElement) {
+ const path = this.translateTreeNodeIdFromACITree(domElement);
+ if(!path || !path[0]) {
+ return undefined;
+ }
+
+ return this.findNode(path);
+ }
+
+ selected() {
+ return this.aciTreeApi.selected();
+ }
+
+ selectNode(aciTreeIdentifier) {
+ this.aciTreeApi.select(aciTreeIdentifier);
+ }
+
+ createOrUpdateNode(id, data, parent, domNode) {
+ let oldNodePath = [id];
+ if(parent !== null && parent !== undefined) {
+ oldNodePath = [parent.path, id];
+ }
+ const oldNode = this.findNode(oldNodePath);
+ if (oldNode !== null) {
+ oldNode.data = Object.assign({}, data);
+ return oldNode;
+ }
+
+ const node = new TreeNode(id, data, domNode, parent);
+ if (parent === this.rootNode) {
+ node.parentNode = null;
+ }
+ parent.children.push(node);
+ return node;
+ }
+
+ /**
+ * Given the JQuery object that contains the ACI Tree
+ * this method is responsible for registering this tree class
+ * to listen to all the events that happen in the ACI Tree.
+ *
+ * At this point in time the only event that we care about is
+ * the addition of a new node.
+ * The function will create a new tree node to store the information
+ * that exist in the ACI for it.
+ */
+ register($treeJQuery) {
+ $treeJQuery.on('acitree', function (event, api, item, eventName) {
+ if (api.isItem(item)) {
+ if (eventName === 'added') {
+ const id = api.getId(item);
+ const data = api.itemData(item);
+ const parentId = this.translateTreeNodeIdFromACITree(api.parent(item));
+ this.addNewNode(id, data, item, parentId);
+ }
+ }
+ }.bind(this));
+ this.aciTreeApi = $treeJQuery.aciTree('api');
+ }
+
+ /**
+ * As the name hints this functions works as a layer in between ACI and
+ * the adaptor. Given a ACITree JQuery node find the location of it in the
+ * Tree and then returns and array with the path to to the Tree Node in
+ * question
+ *
+ * This is not optimized and will always go through the full tree
+ */
+ translateTreeNodeIdFromACITree(aciTreeNode) {
+ let currentTreeNode = aciTreeNode;
+ let path = [];
+ while (currentTreeNode !== null && currentTreeNode !== undefined && currentTreeNode.length > 0) {
+ path.unshift(this.aciTreeApi.getId(currentTreeNode));
+ if (this.aciTreeApi.hasParent(currentTreeNode)) {
+ currentTreeNode = this.aciTreeApi.parent(currentTreeNode);
+ } else {
+ break;
+ }
+ }
+ return path;
+ }
+}
+
+export let tree = new Tree();
+
+/**
+ * Given an initial node and a path, it will navigate through
+ * the new tree to find the node that matches the path
+ */
+function findInTree(rootNode, path) {
+ if (path === null) {
+ return rootNode;
+ }
+
+ return (function findInNode(currentNode) {
+ for (let i = 0, length = currentNode.children.length; i < length; i++) {
+ const calculatedNode = findInNode(currentNode.children[i]);
+ if (calculatedNode !== null) {
+ return calculatedNode;
+ }
+ }
+
+ if (currentNode.path === path) {
+ return currentNode;
+ } else {
+ return null;
+ }
+ })(rootNode);
+}
diff --git a/web/regression/javascript/tree/tree_fake.js b/web/regression/javascript/tree/tree_fake.js
new file mode 100644
index 00000000..b285a45f
--- /dev/null
+++ b/web/regression/javascript/tree/tree_fake.js
@@ -0,0 +1,69 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {Tree} from '../../../pgadmin/static/js/tree/tree';
+
+export class TreeFake extends Tree {
+ constructor() {
+ super();
+ this.aciTreeToOurTreeTranslator = {};
+ this.aciTreeApi = jasmine.createSpyObj(
+ ['ACITreeApi'], ['setInode', 'unload', 'deselect', 'select']);
+ }
+
+ addNewNode(id, data, domNode, path) {
+ this.aciTreeToOurTreeTranslator[id] = [id];
+ if (path !== null && path !== undefined) {
+ this.aciTreeToOurTreeTranslator[id] = path.concat(id);
+ }
+ return super.addNewNode(id, data, domNode, path);
+ }
+
+ addChild(parent, child) {
+ child.setParent(parent);
+ this.aciTreeToOurTreeTranslator[child.id] = this.aciTreeToOurTreeTranslator[parent.id].concat(child.id);
+ parent.children.push(child);
+ }
+
+ hasParent(aciTreeNode) {
+ return this.translateTreeNodeIdFromACITree(aciTreeNode).length > 1;
+ }
+
+ parent(aciTreeNode) {
+ if (this.hasParent(aciTreeNode)) {
+ let path = this.translateTreeNodeIdFromACITree(aciTreeNode);
+ return [{id: this.findNode(path).parent().id}];
+ }
+
+ return null;
+ }
+
+ translateTreeNodeIdFromACITree(aciTreeNode) {
+ if(aciTreeNode === undefined || aciTreeNode[0] === undefined) {
+ return null;
+ }
+ return this.aciTreeToOurTreeTranslator[aciTreeNode[0].id];
+ }
+
+ itemData(aciTreeNode) {
+ let node = this.findNodeByDomElement(aciTreeNode);
+ if (node === undefined || node === null) {
+ return undefined;
+ }
+ return node.getData();
+ }
+
+ selected() {
+ return this.selectedNode;
+ }
+
+ selectNode(selectedNode) {
+ this.selectedNode = selectedNode;
+ }
+}
diff --git a/web/regression/javascript/tree/tree_spec.js b/web/regression/javascript/tree/tree_spec.js
new file mode 100644
index 00000000..164ceb5b
--- /dev/null
+++ b/web/regression/javascript/tree/tree_spec.js
@@ -0,0 +1,421 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+import {Tree, TreeNode} from '../../../pgadmin/static/js/tree/tree';
+import {TreeFake} from './tree_fake';
+
+const context = describe;
+
+const treeTests = (treeClass, setDefaultCallBack) => {
+ let tree;
+ beforeEach(() => {
+ tree = new treeClass();
+ });
+
+ describe('#addNewNode', () => {
+ describe('when add a new root element', () => {
+ context('using [] as the parent', () => {
+ beforeEach(() => {
+ tree.addNewNode('some new node', {data: 'interesting'}, undefined, []);
+ });
+
+ it('can be retrieved', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.data).toEqual({data: 'interesting'});
+ });
+
+ it('return false for #hasParent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.hasParent()).toBe(false);
+ });
+
+ it('return null for #parent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.parent()).toBeNull();
+ });
+ });
+
+ context('using null as the parent', () => {
+ beforeEach(() => {
+ tree.addNewNode('some new node', {data: 'interesting'}, undefined, null);
+ });
+
+ it('can be retrieved', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.data).toEqual({data: 'interesting'});
+ });
+
+ it('return false for #hasParent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.hasParent()).toBe(false);
+ });
+
+ it('return null for #parent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.parent()).toBeNull();
+ });
+ });
+
+ context('using undefined as the parent', () => {
+ beforeEach(() => {
+ tree.addNewNode('some new node', {data: 'interesting'});
+ });
+
+ it('can be retrieved', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.data).toEqual({data: 'interesting'});
+ });
+
+ it('return false for #hasParent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.hasParent()).toBe(false);
+ });
+
+ it('return null for #parent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.parent()).toBeNull();
+ });
+ });
+ });
+
+ describe('when add a new element as a child', () => {
+ let parentNode;
+ beforeEach(() => {
+ parentNode = tree.addNewNode('parent node', {data: 'parent data'}, undefined, []);
+ tree.addNewNode('some new node', {data: 'interesting'}, undefined, ['parent' +
+ ' node']);
+ });
+
+ it('can be retrieved', () => {
+ const node = tree.findNode(['parent node', 'some new node']);
+ expect(node.data).toEqual({data: 'interesting'});
+ });
+
+ it('return true for #hasParent()', () => {
+ const node = tree.findNode(['parent node', 'some new node']);
+ expect(node.hasParent()).toBe(true);
+ });
+
+ it('return "parent node" object for #parent()', () => {
+ const node = tree.findNode(['parent node', 'some new node']);
+ expect(node.parent()).toEqual(parentNode);
+ });
+ });
+
+ describe('when add an element that already exists under a parent', () => {
+ beforeEach(() => {
+ tree.addNewNode('parent node', {data: 'parent data'}, undefined, []);
+ tree.addNewNode('some new node', {data: 'interesting'}, undefined, ['parent' +
+ ' node']);
+ });
+
+ it('does not add a new child', () => {
+ tree.addNewNode('some new node', {data: 'interesting 1'}, undefined, ['parent' +
+ ' node']);
+ const parentNode = tree.findNode(['parent node']);
+ expect(parentNode.children.length).toBe(1);
+ });
+
+ it('updates the existing node data', () => {
+ tree.addNewNode('some new node', {data: 'interesting 1'}, undefined, ['parent' +
+ ' node']);
+ const node = tree.findNode(['parent node', 'some new node']);
+ expect(node.data).toEqual({data: 'interesting 1'});
+ });
+ });
+ });
+
+ describe('#translateTreeNodeIdFromACITree', () => {
+ let aciTreeApi;
+ beforeEach(() => {
+ aciTreeApi = jasmine.createSpyObj('ACITreeApi', [
+ 'hasParent',
+ 'parent',
+ 'getId',
+ ]);
+
+ aciTreeApi.getId.and.callFake((node) => {
+ return node[0].id;
+ });
+ tree.aciTreeApi = aciTreeApi;
+ });
+
+ describe('When tree as a single level', () => {
+ beforeEach(() => {
+ aciTreeApi.hasParent.and.returnValue(false);
+ });
+
+ it('returns an array with the ID of the first level', () => {
+ let node = [{
+ id: 'some id',
+ }];
+ tree.addNewNode('some id', {}, undefined, []);
+
+ expect(tree.translateTreeNodeIdFromACITree(node)).toEqual(['some id']);
+ });
+ });
+
+ describe('When tree as a 2 levels', () => {
+ describe('When we try to retrieve the node in the second level', () => {
+ it('returns an array with the ID of the first level and second level', () => {
+ aciTreeApi.hasParent.and.returnValues(true, false);
+ aciTreeApi.parent.and.returnValue([{
+ id: 'parent id',
+ }]);
+ let node = [{
+ id: 'some id',
+ }];
+
+ tree.addNewNode('parent id', {}, undefined, []);
+ tree.addNewNode('some id', {}, undefined, ['parent id']);
+
+ expect(tree.translateTreeNodeIdFromACITree(node))
+ .toEqual(['parent id', 'some id']);
+ });
+ });
+ });
+ });
+
+ describe('#selected', () => {
+ context('a node in the tree is selected', () => {
+ it('returns that node object', () => {
+ let selectedNode = new TreeNode('bamm', {}, []);
+ setDefaultCallBack(tree, selectedNode);
+ expect(tree.selected()).toEqual(selectedNode);
+ });
+ });
+ });
+
+ describe('#findNodeByTreeElement', () => {
+ context('retrieve data from node not found', () => {
+ it('return undefined', () => {
+ let aciTreeApi = jasmine.createSpyObj('ACITreeApi', [
+ 'hasParent',
+ 'parent',
+ 'getId',
+ ]);
+
+ aciTreeApi.getId.and.callFake((node) => {
+ return node[0].id;
+ });
+ tree.aciTreeApi = aciTreeApi;
+ expect(tree.findNodeByDomElement(['<li>something</li>'])).toBeUndefined();
+ });
+ });
+ });
+};
+
+describe('tree tests', () => {
+ describe('TreeNode', () => {
+ describe('#hasParent', () => {
+ context('parent is null', () => {
+ it('returns false', () => {
+ let treeNode = new TreeNode('123', {}, [], null);
+ expect(treeNode.hasParent()).toBe(false);
+ });
+ });
+ context('parent is undefined', () => {
+ it('returns false', () => {
+ let treeNode = new TreeNode('123', {}, [], undefined);
+ expect(treeNode.hasParent()).toBe(false);
+ });
+ });
+ context('parent exists', () => {
+ it('returns true', () => {
+ let parentNode = new TreeNode('456', {}, []);
+ let treeNode = new TreeNode('123', {}, [], parentNode);
+ expect(treeNode.hasParent()).toBe(true);
+ });
+ });
+ });
+
+ describe('#reload', () => {
+ let tree;
+ let level2;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, [{id: 'level1'}], []);
+ level2 = tree.addNewNode('level2', {data: 'data'}, [{id: 'level2'}], ['level1']);
+ tree.addNewNode('level3', {data: 'more data'}, [{id: 'level3'}], ['level1', 'level2']);
+
+ tree.aciTreeApi = jasmine.createSpyObj(
+ 'ACITreeApi', ['setInode', 'unload', 'deselect', 'select']);
+ });
+
+ it('reloads the node and its children', () => {
+ level2.reload(tree);
+ expect(tree.findNodeByDomElement([{id: 'level2'}])).toEqual(level2);
+ });
+
+ it('does not reload the children of node', () => {
+ level2.reload(tree);
+ expect(tree.findNodeByDomElement([{id: 'level3'}])).toBeNull();
+ });
+
+ it('select the node', (done) => {
+ level2.reload(tree);
+ setTimeout(() => {
+ expect(tree.selected()).toEqual([{id: 'level2'}]);
+ done();
+ }, 20);
+ });
+
+ describe('ACITree specific', () => {
+ it('sets the current node as a Inode, changing the Icon back to +', () => {
+ level2.reload(tree);
+ expect(tree.aciTreeApi.setInode).toHaveBeenCalledWith([{id: 'level2'}]);
+ });
+
+ it('deselect the node and selects it again to trigger ACI tree' +
+ ' events', (done) => {
+ level2.reload(tree);
+ setTimeout(() => {
+ expect(tree.aciTreeApi.deselect).toHaveBeenCalledWith([{id: 'level2'}]);
+ done();
+ }, 20);
+ });
+ });
+ });
+
+ describe('#unload', () => {
+ let tree;
+ let level2;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, ['<li>level1</li>'], []);
+ level2 = tree.addNewNode('level2', {data: 'data'}, ['<li>level2</li>'], ['level1']);
+ tree.addNewNode('level3', {data: 'more data'}, ['<li>level3</li>'], ['level1', 'level2']);
+ tree.aciTreeApi = jasmine.createSpyObj('ACITreeApi', ['unload']);
+ });
+
+ it('unloads the children of the current node', () => {
+ level2.unload(tree);
+ expect(tree.findNodeByDomElement([{id: 'level2'}])).toEqual(level2);
+ expect(tree.findNodeByDomElement([{id: 'level3'}])).toBeNull();
+ });
+
+ it('calls unload on the ACI Tree', () => {
+ level2.unload(tree);
+ expect(tree.aciTreeApi.unload).toHaveBeenCalledWith(['<li>level2</li>']);
+ });
+ });
+ });
+
+ describe('Tree', () => {
+ function realTreeSelectNode(tree, selectedNode) {
+ let aciTreeApi = jasmine.createSpyObj('ACITreeApi', [
+ 'selected',
+ ]);
+ tree.aciTreeApi = aciTreeApi;
+ aciTreeApi.selected.and.returnValue(selectedNode);
+ }
+
+ treeTests(Tree, realTreeSelectNode);
+ });
+
+ describe('TreeFake', () => {
+ function fakeTreeSelectNode(tree, selectedNode) {
+ tree.selectNode(selectedNode);
+ }
+
+ treeTests(TreeFake, fakeTreeSelectNode);
+
+ describe('#hasParent', () => {
+ context('tree contains multiple levels', () => {
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, undefined, []);
+ tree.addNewNode('level2', {data: 'interesting'}, undefined, ['level1']);
+ });
+
+ context('node is at the first level', () => {
+ it('returns false', () => {
+ expect(tree.hasParent([{id: 'level1'}])).toBe(false);
+ });
+ });
+
+ context('node is at the second level', () => {
+ it('returns true', () => {
+ expect(tree.hasParent([{id: 'level2'}])).toBe(true);
+ });
+ });
+ });
+ });
+
+ describe('#parent', () => {
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, undefined, []);
+ tree.addNewNode('level2', {data: 'interesting'}, undefined, ['level1']);
+ });
+
+ context('node is the root', () => {
+ it('returns null', () => {
+ expect(tree.parent([{id: 'level1'}])).toBeNull();
+ });
+ });
+
+ context('node is not root', () => {
+ it('returns root element', () => {
+ expect(tree.parent([{id: 'level2'}])).toEqual([{id: 'level1'}]);
+ });
+ });
+ });
+
+ describe('#itemData', () => {
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, undefined, []);
+ tree.addNewNode('level2', {data: 'expected data'}, undefined, ['level1']);
+ });
+
+ context('retrieve data from the node', () => {
+ it('return the node data', () => {
+ expect(tree.itemData([{id: 'level2'}])).toEqual({
+ data: 'expected' +
+ ' data',
+ });
+ });
+ });
+
+ context('retrieve data from node not found', () => {
+ it('return undefined', () => {
+ expect(tree.itemData([{id: 'bamm'}])).toBeUndefined();
+ });
+ });
+ });
+
+ describe('#addChild', () => {
+ let root, child;
+ beforeEach(() => {
+ let tree = new TreeFake();
+ root = tree.addNewNode('root', {}, [{id: 'root'}]);
+ child = new TreeNode('node.1', {}, [{id: 'node.1'}]);
+ tree.addChild(root, child);
+ });
+
+ it('adds a new child to a node', () => {
+ expect(root.children).toEqual([child]);
+ });
+
+ it('changes the parent of the child node', () => {
+ expect(root.children[0].parentNode).toEqual(root);
+ expect(child.parentNode).toEqual(root);
+ });
+
+ it('changes the path of the child', () => {
+ expect(child.path).toEqual('root.node.1');
+ });
+ });
+ });
+});
+
--
2.16.2
[application/octet-stream] 0004-Architectural-change.patch (23.3K, 6-0004-Architectural-change.patch)
download | inline diff:
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
index a09610ad..6d003578 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
@@ -1,7 +1,7 @@
define('pgadmin.node.schema', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid',
- 'pgadmin.node.schema.dir/can_drop_child',
+ 'sources/schema/can_drop_child',
'pgadmin.browser.collection', 'pgadmin.browser.server.privilege',
], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, Backgrid,
canDropChild) {
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
index 08bb47b7..812d88c4 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
@@ -1,9 +1,9 @@
define('pgadmin.node.table', [
- 'pgadmin.tables.js/enable_disable_triggers',
+ 'sources/table/enable_disable_triggers',
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.backgrid',
- 'pgadmin.tables.js/show_advanced_tab',
+ 'sources/table/show_advanced_tab',
'sources/menu/can_create',
'pgadmin.browser.collection', 'pgadmin.node.column',
diff --git a/web/pgadmin/static/js/alertify/dialog_factory.js b/web/pgadmin/static/js/alertify/dialog_factory.js
index 500140b8..806f8687 100644
--- a/web/pgadmin/static/js/alertify/dialog_factory.js
+++ b/web/pgadmin/static/js/alertify/dialog_factory.js
@@ -7,8 +7,8 @@
//
//////////////////////////////////////////////////////////////
-import * as BackupDialog from '../../../tools/backup/static/js/backup_dialog_wrapper';
-import {RestoreDialogWrapper} from '../../../tools/restore/static/js/restore_dialog_wrapper';
+import * as BackupDialog from '../backup/backup_dialog_wrapper';
+import {RestoreDialogWrapper} from '../restore/restore_dialog_wrapper';
export class DialogFactory {
constructor(pgBrowser, $,
diff --git a/web/pgadmin/tools/backup/static/js/backup_dialog.js b/web/pgadmin/static/js/backup/backup_dialog.js
similarity index 90%
rename from web/pgadmin/tools/backup/static/js/backup_dialog.js
rename to web/pgadmin/static/js/backup/backup_dialog.js
index e603fcf9..aa4461f5 100644
--- a/web/pgadmin/tools/backup/static/js/backup_dialog.js
+++ b/web/pgadmin/static/js/backup/backup_dialog.js
@@ -7,9 +7,9 @@
//
//////////////////////////////////////////////////////////////
-import gettext from '../../../../static/js/gettext';
-import Backform from '../../../../static/js/backform.pgadmin';
-import {Dialog} from '../../../../static/js/alertify/dialog';
+import gettext from '../gettext';
+import Backform from '../backform.pgadmin';
+import {Dialog} from '../alertify/dialog';
export class BackupDialog extends Dialog {
constructor(pgBrowser, $, alertify, BackupModel, backform = Backform) {
diff --git a/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js b/web/pgadmin/static/js/backup/backup_dialog_wrapper.js
similarity index 96%
rename from web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js
rename to web/pgadmin/static/js/backup/backup_dialog_wrapper.js
index 2cebe3d1..7b6250f7 100644
--- a/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js
+++ b/web/pgadmin/static/js/backup/backup_dialog_wrapper.js
@@ -7,12 +7,12 @@
//
//////////////////////////////////////////////////////////////
-import {getTreeNodeHierarchyFromElement} from '../../../../static/js/tree/pgadmin_tree_node';
+import {getTreeNodeHierarchyFromElement} from '../tree/pgadmin_tree_node';
import axios from 'axios/index';
-import gettext from '../../../../static/js/gettext';
-import url_for from '../../../../static/js/url_for';
+import gettext from '../gettext';
+import url_for from '../url_for';
import _ from 'underscore';
-import {DialogWrapper} from '../../../../static/js/alertify/dialog_wrapper';
+import {DialogWrapper} from '../alertify/dialog_wrapper';
export class BackupDialogWrapper extends DialogWrapper {
constructor(dialogContainerSelector, dialogTitle, typeOfDialog,
diff --git a/web/pgadmin/tools/backup/static/js/menu_utils.js b/web/pgadmin/static/js/backup/menu_utils.js
similarity index 89%
rename from web/pgadmin/tools/backup/static/js/menu_utils.js
rename to web/pgadmin/static/js/backup/menu_utils.js
index fdf400af..d3c1b143 100644
--- a/web/pgadmin/tools/backup/static/js/menu_utils.js
+++ b/web/pgadmin/static/js/backup/menu_utils.js
@@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
-import {isProvidedDataValid} from '../../../../static/js/menu/menu_enabled';
+import {isProvidedDataValid} from '../menu/menu_enabled';
export const backupSupportedNodes = [
'database', 'schema', 'table', 'partition',
diff --git a/web/pgadmin/tools/datagrid/static/js/get_panel_title.js b/web/pgadmin/static/js/datagrid/get_panel_title.js
similarity index 90%
rename from web/pgadmin/tools/datagrid/static/js/get_panel_title.js
rename to web/pgadmin/static/js/datagrid/get_panel_title.js
index 64b3a3d2..f5a5664d 100644
--- a/web/pgadmin/tools/datagrid/static/js/get_panel_title.js
+++ b/web/pgadmin/static/js/datagrid/get_panel_title.js
@@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
-import {getTreeNodeHierarchyFromIdentifier} from '../../../../static/js/tree/pgadmin_tree_node';
+import {getTreeNodeHierarchyFromIdentifier} from '../tree/pgadmin_tree_node';
function getDatabaseLabel(parentData) {
return parentData.database ? parentData.database.label
diff --git a/web/pgadmin/tools/datagrid/static/js/show_data.js b/web/pgadmin/static/js/datagrid/show_data.js
similarity index 92%
rename from web/pgadmin/tools/datagrid/static/js/show_data.js
rename to web/pgadmin/static/js/datagrid/show_data.js
index 373c97cd..b6d7c52b 100644
--- a/web/pgadmin/tools/datagrid/static/js/show_data.js
+++ b/web/pgadmin/static/js/datagrid/show_data.js
@@ -6,9 +6,9 @@
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
-import gettext from '../../../../static/js/gettext';
-import url_for from '../../../../static/js/url_for';
-import {getTreeNodeHierarchyFromIdentifier} from '../../../../static/js/tree/pgadmin_tree_node';
+import gettext from '../gettext';
+import url_for from '../url_for';
+import {getTreeNodeHierarchyFromIdentifier} from '../tree/pgadmin_tree_node';
export function showDataGrid(
datagrid,
diff --git a/web/pgadmin/tools/datagrid/static/js/show_query_tool.js b/web/pgadmin/static/js/datagrid/show_query_tool.js
similarity index 88%
rename from web/pgadmin/tools/datagrid/static/js/show_query_tool.js
rename to web/pgadmin/static/js/datagrid/show_query_tool.js
index 0eb12b8b..a1b4874c 100644
--- a/web/pgadmin/tools/datagrid/static/js/show_query_tool.js
+++ b/web/pgadmin/static/js/datagrid/show_query_tool.js
@@ -7,9 +7,9 @@
//
//////////////////////////////////////////////////////////////
-import gettext from '../../../../static/js/gettext';
-import url_for from '../../../../static/js/url_for';
-import {getTreeNodeHierarchyFromIdentifier} from '../../../../static/js/tree/pgadmin_tree_node';
+import gettext from '../gettext';
+import url_for from '../url_for';
+import {getTreeNodeHierarchyFromIdentifier} from '../tree/pgadmin_tree_node';
function hasDatabaseInformation(parentData) {
return parentData.database;
diff --git a/web/pgadmin/tools/grant_wizard/static/js/menu_utils.js b/web/pgadmin/static/js/grant/wizard/menu_utils.js
similarity index 100%
rename from web/pgadmin/tools/grant_wizard/static/js/menu_utils.js
rename to web/pgadmin/static/js/grant/wizard/menu_utils.js
diff --git a/web/pgadmin/tools/maintenance/static/js/menu_utils.js b/web/pgadmin/static/js/maintenance/menu_utils.js
similarity index 100%
rename from web/pgadmin/tools/maintenance/static/js/menu_utils.js
rename to web/pgadmin/static/js/maintenance/menu_utils.js
diff --git a/web/pgadmin/tools/restore/static/js/menu_utils.js b/web/pgadmin/static/js/restore/menu_utils.js
similarity index 100%
rename from web/pgadmin/tools/restore/static/js/menu_utils.js
rename to web/pgadmin/static/js/restore/menu_utils.js
diff --git a/web/pgadmin/tools/restore/static/js/restore_dialog.js b/web/pgadmin/static/js/restore/restore_dialog.js
similarity index 88%
rename from web/pgadmin/tools/restore/static/js/restore_dialog.js
rename to web/pgadmin/static/js/restore/restore_dialog.js
index 66fea3ba..237b8412 100644
--- a/web/pgadmin/tools/restore/static/js/restore_dialog.js
+++ b/web/pgadmin/static/js/restore/restore_dialog.js
@@ -7,10 +7,10 @@
//
//////////////////////////////////////////////////////////////
-import gettext from '../../../../static/js/gettext';
+import gettext from '../gettext';
import {sprintf} from 'sprintf-js';
-import Backform from '../../../../static/js/backform.pgadmin';
-import {Dialog} from '../../../../static/js/alertify/dialog';
+import Backform from '../backform.pgadmin';
+import {Dialog} from '../alertify/dialog';
export class RestoreDialog extends Dialog {
constructor(pgBrowser, $, alertify, RestoreModel, backform = Backform) {
diff --git a/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js b/web/pgadmin/static/js/restore/restore_dialog_wrapper.js
similarity index 96%
rename from web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js
rename to web/pgadmin/static/js/restore/restore_dialog_wrapper.js
index 845da7a3..13d781fb 100644
--- a/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js
+++ b/web/pgadmin/static/js/restore/restore_dialog_wrapper.js
@@ -1,9 +1,9 @@
-import {getTreeNodeHierarchyFromElement} from '../../../../static/js/tree/pgadmin_tree_node';
+import {getTreeNodeHierarchyFromElement} from '../tree/pgadmin_tree_node';
import axios from 'axios/index';
import _ from 'underscore';
-import gettext from '../../../../static/js/gettext';
-import url_for from '../../../../static/js/url_for';
-import {DialogWrapper} from '../../../../static/js/alertify/dialog_wrapper';
+import gettext from '../gettext';
+import url_for from '../url_for';
+import {DialogWrapper} from '../alertify/dialog_wrapper';
export class RestoreDialogWrapper extends DialogWrapper {
constructor(dialogContainerSelector, dialogTitle, typeOfDialog,
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child.js b/web/pgadmin/static/js/schema/can_drop_child.js
similarity index 100%
rename from web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child.js
rename to web/pgadmin/static/js/schema/can_drop_child.js
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers.js b/web/pgadmin/static/js/table/enable_disable_triggers.js
similarity index 100%
rename from web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers.js
rename to web/pgadmin/static/js/table/enable_disable_triggers.js
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/show_advanced_tab.js b/web/pgadmin/static/js/table/show_advanced_tab.js
similarity index 100%
rename from web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/show_advanced_tab.js
rename to web/pgadmin/static/js/table/show_advanced_tab.js
diff --git a/web/pgadmin/tools/backup/static/js/backup.js b/web/pgadmin/tools/backup/static/js/backup.js
index d74ed6dd..b20b7e6c 100644
--- a/web/pgadmin/tools/backup/static/js/backup.js
+++ b/web/pgadmin/tools/backup/static/js/backup.js
@@ -3,8 +3,8 @@ define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'pgadmin.alertifyjs', 'backbone', 'pgadmin.backgrid',
'pgadmin.backform', 'pgadmin.browser', 'sources/utils',
- 'tools/backup/static/js/menu_utils',
- 'tools/backup/static/js/backup_dialog',
+ 'sources/backup/menu_utils',
+ 'sources/backup/backup_dialog',
'sources/menu/menu_enabled',
], function(
gettext, url_for, $, _, S, alertify, Backbone, Backgrid, Backform, pgBrowser,
diff --git a/web/pgadmin/tools/datagrid/static/js/datagrid.js b/web/pgadmin/tools/datagrid/static/js/datagrid.js
index 520a9ce5..676f1901 100644
--- a/web/pgadmin/tools/datagrid/static/js/datagrid.js
+++ b/web/pgadmin/tools/datagrid/static/js/datagrid.js
@@ -2,9 +2,9 @@ define('pgadmin.datagrid', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'pgadmin.alertifyjs', 'sources/pgadmin', 'bundled_codemirror',
'sources/sqleditor_utils', 'backbone',
- 'tools/datagrid/static/js/show_data',
- 'tools/datagrid/static/js/get_panel_title',
- 'tools/datagrid/static/js/show_query_tool',
+ 'sources/datagrid/show_data',
+ 'sources/datagrid/get_panel_title',
+ 'sources/datagrid/show_query_tool',
'wcdocker',
], function(
gettext, url_for, $, _, alertify, pgAdmin, codemirror, sqlEditorUtils,
diff --git a/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js b/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js
index cf234f1d..56eb378a 100644
--- a/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js
+++ b/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js
@@ -3,7 +3,7 @@ define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'pgadmin.alertifyjs', 'pgadmin.backgrid', 'pgadmin.backform',
'pgadmin.browser', 'pgadmin.browser.node',
- 'tools/grant_wizard/static/js/menu_utils',
+ 'sources/grant/wizard/menu_utils',
'sources/menu/menu_enabled',
'backgrid.select.all',
diff --git a/web/pgadmin/tools/maintenance/static/js/maintenance.js b/web/pgadmin/tools/maintenance/static/js/maintenance.js
index f32ce5c4..c9976d53 100644
--- a/web/pgadmin/tools/maintenance/static/js/maintenance.js
+++ b/web/pgadmin/tools/maintenance/static/js/maintenance.js
@@ -2,7 +2,7 @@ define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'pgadmin.alertifyjs', 'sources/pgadmin', 'pgadmin.browser', 'backbone',
'backgrid', 'backform', 'sources/utils',
- 'tools/maintenance/static/js/menu_utils',
+ 'sources/maintenance/menu_utils',
'sources/menu/menu_enabled',
'pgadmin.backform', 'pgadmin.backgrid',
'pgadmin.browser.node.ui',
diff --git a/web/pgadmin/tools/restore/static/js/restore.js b/web/pgadmin/tools/restore/static/js/restore.js
index eaa7a15e..b5d6913a 100644
--- a/web/pgadmin/tools/restore/static/js/restore.js
+++ b/web/pgadmin/tools/restore/static/js/restore.js
@@ -2,9 +2,9 @@ define('tools.restore', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'underscore.string', 'pgadmin.alertifyjs', 'pgadmin.browser',
'pgadmin.backgrid', 'pgadmin.backform', 'sources/utils',
- 'tools/restore/static/js/menu_utils',
+ 'sources/restore/menu_utils',
'sources/menu/menu_enabled',
- 'tools/restore/static/js/restore_dialog',
+ 'sources/restore/restore_dialog',
], function(
gettext, url_for, $, _, Backbone, S, alertify, pgBrowser, Backgrid, Backform,
commonUtils, menuUtils, menuEnabled, restoreDialog
diff --git a/web/regression/javascript/backup/backup_dialog_spec.js b/web/regression/javascript/backup/backup_dialog_spec.js
index 85e5e60b..c2da61c4 100644
--- a/web/regression/javascript/backup/backup_dialog_spec.js
+++ b/web/regression/javascript/backup/backup_dialog_spec.js
@@ -6,7 +6,7 @@
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
-import {BackupDialog} from '../../../pgadmin/tools/backup/static/js/backup_dialog';
+import {BackupDialog} from '../../../pgadmin/static/js/backup/backup_dialog';
import {TreeFake} from '../tree/tree_fake';
import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
diff --git a/web/regression/javascript/backup/backup_dialog_wrapper_spec.js b/web/regression/javascript/backup/backup_dialog_wrapper_spec.js
index 58705318..73e453e5 100644
--- a/web/regression/javascript/backup/backup_dialog_wrapper_spec.js
+++ b/web/regression/javascript/backup/backup_dialog_wrapper_spec.js
@@ -1,5 +1,5 @@
import {TreeFake} from '../tree/tree_fake';
-import {BackupDialogWrapper} from '../../../pgadmin/tools/backup/static/js/backup_dialog_wrapper';
+import {BackupDialogWrapper} from '../../../pgadmin/static/js/backup/backup_dialog_wrapper';
import axios from 'axios/index';
import MockAdapter from 'axios-mock-adapter';
import {FakeModel} from '../fake_model';
diff --git a/web/regression/javascript/backup/global_server_backup_dialog_spec.js b/web/regression/javascript/backup/global_server_backup_dialog_spec.js
index 86df672e..67cca9db 100644
--- a/web/regression/javascript/backup/global_server_backup_dialog_spec.js
+++ b/web/regression/javascript/backup/global_server_backup_dialog_spec.js
@@ -6,7 +6,7 @@
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
-import {BackupDialog} from '../../../pgadmin/tools/backup/static/js/backup_dialog';
+import {BackupDialog} from '../../../pgadmin/static/js/backup/backup_dialog';
import {TreeFake} from '../tree/tree_fake';
const context = describe;
diff --git a/web/regression/javascript/backup/menu_utils_spec.js b/web/regression/javascript/backup/menu_utils_spec.js
index 9435d699..ecf8abcd 100644
--- a/web/regression/javascript/backup/menu_utils_spec.js
+++ b/web/regression/javascript/backup/menu_utils_spec.js
@@ -8,7 +8,7 @@
//////////////////////////////////////////////////////////////
-import {menuEnabledServer} from '../../../pgadmin/tools/backup/static/js/menu_utils';
+import {menuEnabledServer} from '../../../pgadmin/static/js/backup/menu_utils';
const context = describe;
diff --git a/web/regression/javascript/browser/server_groups/servers/databases/schemas/tables/show_advanced_tab_spec.js b/web/regression/javascript/browser/server_groups/servers/databases/schemas/tables/show_advanced_tab_spec.js
index f9559a41..4b3811aa 100644
--- a/web/regression/javascript/browser/server_groups/servers/databases/schemas/tables/show_advanced_tab_spec.js
+++ b/web/regression/javascript/browser/server_groups/servers/databases/schemas/tables/show_advanced_tab_spec.js
@@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
-import {show_advanced_tab} from '../../../../../../../../pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/show_advanced_tab';
+import {show_advanced_tab} from '../../../../../../../../pgadmin/static/js/table/show_advanced_tab';
describe('#show_advanced_tab', () => {
let tableModel;
diff --git a/web/regression/javascript/datagrid/get_panel_title_spec.js b/web/regression/javascript/datagrid/get_panel_title_spec.js
index 8a344a84..2594ec96 100644
--- a/web/regression/javascript/datagrid/get_panel_title_spec.js
+++ b/web/regression/javascript/datagrid/get_panel_title_spec.js
@@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
-import {getPanelTitle} from '../../../pgadmin/tools/datagrid/static/js/get_panel_title';
+import {getPanelTitle} from '../../../pgadmin/static/js/datagrid/get_panel_title';
import {TreeFake} from '../tree/tree_fake';
import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
diff --git a/web/regression/javascript/datagrid/show_data_spec.js b/web/regression/javascript/datagrid/show_data_spec.js
index 80d25eb3..3e4feec9 100644
--- a/web/regression/javascript/datagrid/show_data_spec.js
+++ b/web/regression/javascript/datagrid/show_data_spec.js
@@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
-import {showDataGrid} from '../../../pgadmin/tools/datagrid/static/js/show_data';
+import {showDataGrid} from '../../../pgadmin/static/js/datagrid/show_data';
import {TreeFake} from '../tree/tree_fake';
import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
diff --git a/web/regression/javascript/datagrid/show_query_tool_spec.js b/web/regression/javascript/datagrid/show_query_tool_spec.js
index 66bd37ce..e58112c0 100644
--- a/web/regression/javascript/datagrid/show_query_tool_spec.js
+++ b/web/regression/javascript/datagrid/show_query_tool_spec.js
@@ -8,7 +8,7 @@
//////////////////////////////////////////////////////////////
import {TreeFake} from '../tree/tree_fake';
-import {showQueryTool} from '../../../pgadmin/tools/datagrid/static/js/show_query_tool';
+import {showQueryTool} from '../../../pgadmin/static/js/datagrid/show_query_tool';
import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
const context = describe;
diff --git a/web/regression/javascript/restore/restore_dialog_spec.js b/web/regression/javascript/restore/restore_dialog_spec.js
index c1335eeb..48ef5961 100644
--- a/web/regression/javascript/restore/restore_dialog_spec.js
+++ b/web/regression/javascript/restore/restore_dialog_spec.js
@@ -9,7 +9,7 @@
import {TreeFake} from '../tree/tree_fake';
import {
RestoreDialog,
-} from '../../../pgadmin/tools/restore/static/js/restore_dialog';
+} from '../../../pgadmin/static/js/restore/restore_dialog';
import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
const context = describe;
diff --git a/web/regression/javascript/restore/restore_dialog_wrapper_spec.js b/web/regression/javascript/restore/restore_dialog_wrapper_spec.js
index c2a31d55..84de8e62 100644
--- a/web/regression/javascript/restore/restore_dialog_wrapper_spec.js
+++ b/web/regression/javascript/restore/restore_dialog_wrapper_spec.js
@@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
import {TreeFake} from '../tree/tree_fake';
-import {RestoreDialogWrapper} from '../../../pgadmin/tools/restore/static/js/restore_dialog_wrapper';
+import {RestoreDialogWrapper} from '../../../pgadmin/static/js/restore/restore_dialog_wrapper';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios/index';
import {FakeModel} from '../fake_model';
diff --git a/web/regression/javascript/schema/can_drop_child_spec.js b/web/regression/javascript/schema/can_drop_child_spec.js
index 4403b274..388dffbc 100644
--- a/web/regression/javascript/schema/can_drop_child_spec.js
+++ b/web/regression/javascript/schema/can_drop_child_spec.js
@@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
-import {canDropChild} from '../../../pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child';
+import {canDropChild} from '../../../pgadmin/static/js/schema/can_drop_child';
import {TreeFake} from '../tree/tree_fake';
let context = describe;
diff --git a/web/regression/javascript/table/enable_disable_triggers_spec.js b/web/regression/javascript/table/enable_disable_triggers_spec.js
index 7bdd284e..767c78a4 100644
--- a/web/regression/javascript/table/enable_disable_triggers_spec.js
+++ b/web/regression/javascript/table/enable_disable_triggers_spec.js
@@ -12,7 +12,7 @@ import axios from 'axios/index';
import {
enableTriggers,
disableTriggers,
-} from '../../../pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers';
+} from '../../../pgadmin/static/js/table/enable_disable_triggers';
import {TreeFake} from '../tree/tree_fake';
import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
--
2.16.2
[application/octet-stream] 0003-Extract-test-and-refactor-methods.patch (258.9K, 7-0003-Extract-test-and-refactor-methods.patch)
download | inline diff:
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.js
index 9015d8d2..5a3a4bc0 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.js
@@ -1,8 +1,9 @@
define('pgadmin.node.collation', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
+ 'sources/menu/can_create',
'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser) {
+], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, canCreate) {
if (!pgBrowser.Nodes['coll-collation']) {
pgAdmin.Browser.Nodes['coll-collation'] =
@@ -223,32 +224,7 @@ define('pgadmin.node.collation', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create collation
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-collation' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-collation', item, data);
},
});
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.js
index 403ca471..1a38b083 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.js
@@ -2,9 +2,11 @@
define('pgadmin.node.domain', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid',
+ 'sources/menu/can_create',
'pgadmin.browser.collection',
], function(
- gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid
+ gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid,
+ canCreate
) {
// Define Domain Collection Node
@@ -297,32 +299,7 @@ define('pgadmin.node.domain', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create domain
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-domain' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-domain', item, data);
},
isDisabled: function(m){
if (!m.isNew()) {
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table.js
index 160db83f..0e820343 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table.js
@@ -2,9 +2,11 @@
define('pgadmin.node.foreign_table', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid',
+ 'sources/menu/can_create',
'pgadmin.browser.collection',
], function(
- gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid
+ gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid,
+ canCreate
) {
if (!pgBrowser.Nodes['coll-foreign_table']) {
@@ -660,32 +662,7 @@ define('pgadmin.node.foreign_table', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create foreign table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-foreign_table' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-foreign_table', item, data);
},
});
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.js
index 89806681..f2fe85be 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.js
@@ -1,9 +1,11 @@
define('pgadmin.node.fts_configuration', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid',
+ 'sources/menu/can_create',
'pgadmin.browser.collection',
], function(
- gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid
+ gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid,
+ canCreate
) {
// Model for tokens control
@@ -578,32 +580,7 @@ define('pgadmin.node.fts_configuration', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create fts configuration
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-fts_configuration' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-fts_configuration', item, data);
},
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary.js
index ed83feb1..f1a330f7 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary.js
@@ -1,8 +1,10 @@
define('pgadmin.node.fts_dictionary', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
+ 'sources/menu/can_create',
'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform) {
+], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform,
+ canCreate) {
// Extend the browser's node model class to create a option/value pair
var OptionLabelModel = pgAdmin.Browser.Node.Model.extend({
@@ -187,32 +189,7 @@ define('pgadmin.node.fts_dictionary', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create fts dictionary
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-fts_dictionary' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-fts_dictionary', item, data);
},
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser.js
index 92c0786e..daea35de 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser.js
@@ -1,7 +1,9 @@
define('pgadmin.node.fts_parser', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
- 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser) {
+ 'sources/pgadmin', 'pgadmin.browser',
+ 'sources/menu/can_create',
+ 'pgadmin.browser.collection',
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, canCreate) {
// Extend the collection class for fts parser
if (!pgBrowser.Nodes['coll-fts_parser']) {
@@ -200,32 +202,7 @@ define('pgadmin.node.fts_parser', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create fts parser
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-fts_parser' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-fts_parser', item, data);
},
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template.js
index 606a57a6..0c008c83 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template.js
@@ -1,7 +1,9 @@
define('pgadmin.node.fts_template', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
- 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser) {
+ 'sources/pgadmin', 'pgadmin.browser',
+ 'sources/menu/can_create',
+ 'pgadmin.browser.collection',
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, canCreate) {
// Extend the collection class for fts template
if (!pgBrowser.Nodes['coll-fts_template']) {
@@ -140,32 +142,7 @@ define('pgadmin.node.fts_template', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create fts fts_template
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-fts_template' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-fts_template', item, data);
},
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function.js
index 6e405165..40636446 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function.js
@@ -2,8 +2,10 @@
define('pgadmin.node.function', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
+ 'sources/menu/can_create',
'pgadmin.browser.collection', 'pgadmin.browser.server.privilege',
-], function(gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform) {
+], function(gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform,
+ canCreate) {
if (!pgBrowser.Nodes['coll-function']) {
pgBrowser.Nodes['coll-function'] =
@@ -439,32 +441,7 @@ define('pgadmin.node.function', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create Function
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-function' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-function', item, data);
},
});
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js
index aeb8271b..11b587ed 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js
@@ -2,8 +2,9 @@
define('pgadmin.node.trigger_function', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
+ 'sources/menu/can_create',
'pgadmin.browser.collection', 'pgadmin.browser.server.privilege',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform) {
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, canCreate) {
if (!pgBrowser.Nodes['coll-trigger_function']) {
pgBrowser.Nodes['coll-trigger_function'] =
@@ -358,32 +359,7 @@ define('pgadmin.node.trigger_function', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create Function
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-trigger_function' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-trigger_function', item, data);
},
});
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence.js
index 57c95acd..0a59efb0 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence.js
@@ -1,8 +1,10 @@
define('pgadmin.node.sequence', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
+ 'sources/menu/can_create',
'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform) {
+], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform,
+ canCreate) {
// Extend the browser's collection class for sequence collection
if (!pgBrowser.Nodes['coll-sequence']) {
@@ -61,32 +63,7 @@ define('pgadmin.node.sequence', [
canDrop: pgBrowser.Nodes['schema'].canChildDrop,
canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create collation
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-sequence' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-sequence', item, data);
},
// Define the model for sequence node.
model: pgBrowser.Node.Model.extend({
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child.js
new file mode 100644
index 00000000..84e25b5c
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child.js
@@ -0,0 +1,18 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export function canDropChild(pgBrowser, itemData, item) {
+ let node = pgBrowser.treeMenu.findNodeByDomElement(item);
+
+ if (node.anyParent((parent) => parent.getData()._type === 'catalog')) {
+ return false;
+ }
+
+ return true;
+}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
index a7fd4c7c..a09610ad 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
@@ -1,8 +1,10 @@
define('pgadmin.node.schema', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid',
+ 'pgadmin.node.schema.dir/can_drop_child',
'pgadmin.browser.collection', 'pgadmin.browser.server.privilege',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, Backgrid) {
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, Backgrid,
+canDropChild) {
// VacuumSettings Collection to display all settings parameters as Grid
Backform.VacuumCollectionControl =
@@ -428,51 +430,12 @@ define('pgadmin.node.schema', [
// This function will checks whether we can allow user to
// drop object or not based on location within schema & catalog
canChildDrop: function(itemData, item) {
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create collation
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if(prev_d && prev_d._type == 'catalog') {
- return false;
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canDropChild.canDropChild(pgBrowser, itemData, item);
},
});
pgBrowser.tableChildTreeNodeHierarchy = function(i) {
- var idx = 0,
- res = {},
- t = pgBrowser.tree;
-
- do {
- var d = t.itemData(i);
- if (
- d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId
- ) {
- if (d._type === 'partition' || d._type === 'table') {
- if (!('table' in res)) {
- res['table'] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- } else {
- res[d._type] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- } while (i);
-
- return res;
+ return this.getTreeNodeHierarchy(i);
};
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js
index 72b6eb0c..1fdacf52 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js
@@ -88,7 +88,6 @@ define('pgadmin.node.column', [
if (!pgBrowser.Nodes['column']) {
pgBrowser.Nodes['column'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table', 'view', 'mview'],
collection_type: ['coll-table', 'coll-view', 'coll-mview'],
type: 'column',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js
index bf7ef556..b1cb2518 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js
@@ -8,7 +8,6 @@ define('pgadmin.node.check_constraint', [
// Check Constraint Node
if (!pgBrowser.Nodes['check_constraint']) {
pgAdmin.Browser.Nodes['check_constraint'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'check_constraint',
label: gettext('Check'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js
index 0bbf66a1..adccf2e9 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js
@@ -605,7 +605,6 @@ define('pgadmin.node.exclusion_constraint', [
// Extend the browser's node class for exclusion constraint node
if (!pgBrowser.Nodes['exclusion_constraint']) {
pgAdmin.Browser.Nodes['exclusion_constraint'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'exclusion_constraint',
label: gettext('Exclusion constraint'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js
index 4997d175..788cecfc 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js
@@ -603,7 +603,6 @@ define('pgadmin.node.foreign_key', [
// Extend the browser's node class for foreign key node
if (!pgBrowser.Nodes['foreign_key']) {
pgAdmin.Browser.Nodes['foreign_key'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'foreign_key',
label: gettext('Foreign key'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js
index d3a6cff4..0ad0f054 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js
@@ -20,7 +20,6 @@ define('pgadmin.node.primary_key', [
parent_type: ['table','partition'],
canDrop: true,
canDropCascade: true,
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
Init: function() {
/* Avoid multiple registration of menus */
if (this.initialized)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js
index 769185d6..18d3ca33 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js
@@ -20,7 +20,6 @@ define('pgadmin.node.unique_constraint', [
parent_type: ['table','partition'],
canDrop: true,
canDropCascade: true,
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
Init: function() {
/* Avoid multiple registration of menus */
if (this.initialized)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js
index cb242cd2..9c0e24fe 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js
@@ -12,14 +12,12 @@ define('pgadmin.node.constraints', [
node: 'constraints',
label: gettext('Constraints'),
type: 'coll-constraints',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
columns: ['name', 'comment'],
});
}
if (!pgBrowser.Nodes['constraints']) {
pgAdmin.Browser.Nodes['constraints'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'constraints',
label: gettext('Constraints'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js
index ec2b4da1..2b687c1a 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js
@@ -13,7 +13,6 @@ define('pgadmin.node.index', [
node: 'index',
label: gettext('Indexes'),
type: 'coll-index',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
sqlAlterHelp: 'sql-alterindex.html',
sqlCreateHelp: 'sql-createindex.html',
dialogHelp: url_for('help.static', {'filename': 'index_dialog.html'}),
@@ -215,7 +214,6 @@ define('pgadmin.node.index', [
if (!pgBrowser.Nodes['index']) {
pgAdmin.Browser.Nodes['index'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table', 'view', 'mview', 'partition'],
collection_type: ['coll-table', 'coll-view'],
sqlAlterHelp: 'sql-alterindex.html',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js
index fd53f743..17a0e6ce 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js
@@ -1,11 +1,15 @@
define([
+ 'sources/tree/pgadmin_tree_node',
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.backgrid',
+ 'sources/menu/can_create',
'pgadmin.browser.collection', 'pgadmin.browser.table.partition.utils',
],
function(
- gettext, url_for, $, _, S, pgAdmin, pgBrowser, Alertify, Backform, Backgrid
+ pgadminTreeNode,
+ gettext, url_for, $, _, S, pgAdmin, pgBrowser, Alertify, Backform, Backgrid,
+ canCreate
) {
if (!pgBrowser.Nodes['coll-partition']) {
@@ -13,7 +17,6 @@ function(
pgAdmin.Browser.Collection.extend({
node: 'partition',
label: gettext('Partitions'),
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'coll-partition',
columns: [
'name', 'schema', 'partition_value', 'is_partitioned', 'description',
@@ -80,36 +83,6 @@ function(
},
]);
},
- getTreeNodeHierarchy: function(i) {
- var idx = 0,
- res = {},
- t = pgBrowser.tree;
-
- do {
- var d = t.itemData(i);
- if (
- d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId
- ) {
- if (d._type == 'partition' && 'partition' in res) {
- if (!('table' in res)) {
- res['table'] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- } else if (d._type == 'table') {
- if (!('table' in res)) {
- res['table'] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- } else {
- res[d._type] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- } while (i);
-
- return res;
- },
generate_url: function(item, type, d, with_id, info) {
if (_.indexOf([
'stats', 'statistics', 'dependency', 'dependent', 'reset',
@@ -1219,32 +1192,7 @@ function(
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-table' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null;
- var prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-table', item, data);
},
// Check to whether table has disable trigger(s)
canCreate_with_trigger_enable: function(itemData, item, data) {
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js
index 3af61754..354909f3 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js
@@ -16,7 +16,6 @@ define('pgadmin.node.rule', [
node: 'rule',
label: gettext('Rules'),
type: 'coll-rule',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
columns: ['name', 'owner', 'comment'],
});
}
@@ -35,7 +34,6 @@ define('pgadmin.node.rule', [
*/
if (!pgBrowser.Nodes['rule']) {
pgAdmin.Browser.Nodes['rule'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table','view', 'partition'],
type: 'rule',
sqlAlterHelp: 'sql-alterrule.html',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers.js
new file mode 100644
index 00000000..2d792043
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers.js
@@ -0,0 +1,52 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import axios from 'axios';
+
+export function disableTriggers(tree, alertify, generateUrl, args) {
+ return setTriggers(tree, alertify, generateUrl, args, {enable: 'false' });
+}
+export function enableTriggers(tree, alertify, generateUrl, args) {
+ return setTriggers(tree, alertify, generateUrl, args, {enable: 'true' });
+}
+
+function setTriggers(tree, alertify, generateUrl, args, params) {
+ const treeNode = retrieveTreeNode(args, tree);
+
+ if (!treeNode || treeNode.getData() === null || treeNode.getData() === undefined)
+ return false;
+
+ axios.put(
+ generateUrl(treeNode.getHtmlIdentifier(), 'set_trigger', treeNode.getData(), true),
+ params
+ )
+ .then((res) => {
+ if (res.data.success === 1) {
+ alertify.success(res.data.info);
+ treeNode.reload(tree);
+ }
+ })
+ .catch((xhr) => {
+ try {
+ const err = xhr.response.data;
+ if (err.success === 0) {
+ alertify.error(err.errormsg);
+ }
+ } catch (e) {
+ console.warn(e.stack || e);
+ }
+ treeNode.unload(tree);
+ });
+}
+
+function retrieveTreeNode(args, tree) {
+ const input = args || {};
+ const domElementIdentifier = input.item || tree.selected();
+ return tree.findNodeByDomElement(domElementIdentifier);
+}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
index 647d1bfe..08bb47b7 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
@@ -1,13 +1,17 @@
define('pgadmin.node.table', [
+ 'pgadmin.tables.js/enable_disable_triggers',
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.backgrid',
'pgadmin.tables.js/show_advanced_tab',
+ 'sources/menu/can_create',
+
'pgadmin.browser.collection', 'pgadmin.node.column',
'pgadmin.node.constraints', 'pgadmin.browser.table.partition.utils',
], function(
+ tableFunctions,
gettext, url_for, $, _, S, pgAdmin, pgBrowser, Alertify, Backform, Backgrid,
- ShowAdvancedTab
+ ShowAdvancedTab, canCreate
) {
if (!pgBrowser.Nodes['coll-table']) {
@@ -26,7 +30,6 @@ define('pgadmin.node.table', [
if (!pgBrowser.Nodes['table']) {
pgBrowser.Nodes['table'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'table',
label: gettext('Table'),
collection_type: 'coll-table',
@@ -118,53 +121,21 @@ define('pgadmin.node.table', [
callbacks: {
/* Enable trigger(s) on table */
enable_triggers_on_table: function(args) {
- var params = {'enable': true };
- this.callbacks.set_triggers.apply(this, [args, params]);
+ tableFunctions.enableTriggers(
+ pgBrowser.treeMenu,
+ Alertify,
+ this.generate_url.bind(this),
+ args
+ );
},
/* Disable trigger(s) on table */
disable_triggers_on_table: function(args) {
- var params = {'enable': false };
- this.callbacks.set_triggers.apply(this, [args, params]);
- },
- set_triggers: function(args, params) {
- // This function will send request to enable or
- // disable triggers on table level
- var input = args || {},
- obj = this,
- t = pgBrowser.tree,
- i = input.item || t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined;
- if (!d)
- return false;
-
- $.ajax({
- url: obj.generate_url(i, 'set_trigger' , d, true),
- type:'PUT',
- data: params,
- dataType: 'json',
- success: function(res) {
- if (res.success == 1) {
- Alertify.success(res.info);
- t.unload(i);
- t.setInode(i);
- t.deselect(i);
- setTimeout(function() {
- t.select(i);
- }, 10);
- }
- },
- error: function(xhr) {
- try {
- var err = $.parseJSON(xhr.responseText);
- if (err.success == 0) {
- Alertify.error(err.errormsg);
- }
- } catch (e) {
- console.warn(e.stack || e);
- }
- t.unload(i);
- },
- });
+ tableFunctions.disableTriggers(
+ pgBrowser.treeMenu,
+ Alertify,
+ this.generate_url.bind(this),
+ args
+ );
},
/* Truncate table */
truncate_table: function(args) {
@@ -1329,32 +1300,7 @@ define('pgadmin.node.table', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-table' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-table', item, data);
},
// Check to whether table has disable trigger(s)
canCreate_with_trigger_enable: function(itemData, item, data) {
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js
index fbe7659e..9a1ce2e0 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js
@@ -29,14 +29,12 @@ define('pgadmin.node.trigger', [
node: 'trigger',
label: gettext('Triggers'),
type: 'coll-trigger',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
columns: ['name', 'description'],
});
}
if (!pgBrowser.Nodes['trigger']) {
pgAdmin.Browser.Nodes['trigger'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table', 'view', 'partition'],
collection_type: ['coll-table', 'coll-view'],
type: 'trigger',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.js
index c1c24861..91ce615f 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.js
@@ -1,8 +1,9 @@
define('pgadmin.node.type', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
- 'pgadmin.backgrid', 'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, Backgrid) {
+ 'pgadmin.backgrid', 'sources/menu/can_create', 'pgadmin.browser.collection',
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, Backgrid,
+ canCreate) {
if (!pgBrowser.Nodes['coll-type']) {
pgBrowser.Nodes['coll-type'] =
@@ -912,32 +913,7 @@ define('pgadmin.node.type', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-type' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-type', item, data);
},
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.js
index 795bbfcf..15a517b8 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.js
@@ -1,8 +1,11 @@
define('pgadmin.node.mview', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.alertifyjs', 'pgadmin.browser',
- 'pgadmin.backform', 'pgadmin.browser.server.privilege',
-], function(gettext, url_for, $, _, pgAdmin, Alertify, pgBrowser, Backform) {
+ 'pgadmin.backform',
+ 'sources/menu/can_create',
+ 'pgadmin.browser.server.privilege',
+], function(gettext, url_for, $, _, pgAdmin, Alertify, pgBrowser, Backform,
+ canCreate) {
/**
Create and add a view collection into nodes
@@ -241,37 +244,7 @@ define('pgadmin.node.mview', [
and hide for system view in catalogs.
*/
canCreate: function(itemData, item, data) {
-
- // If check is false then, we will allow create menu
- if (data && data.check === false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
-
- // To iterate over tree to check parent node
- while (i) {
-
- // If it is schema then allow user to create view
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-mview' == d._type) {
-
- // Check if we are not child of view
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
-
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-mview', item, data);
},
refresh_mview: function(args) {
var input = args || {},
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js
index 5755a509..e4258188 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js
@@ -1,9 +1,12 @@
define('pgadmin.node.view', [
'sources/gettext',
'sources/url_for', 'jquery', 'underscore', 'sources/pgadmin',
- 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.browser.server.privilege',
+ 'pgadmin.browser', 'pgadmin.backform',
+ 'sources/menu/can_create',
+ 'pgadmin.browser.server.privilege',
'pgadmin.node.rule',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform) {
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform,
+ canCreate) {
/**
Create and add a view collection into nodes
@@ -203,38 +206,7 @@ define('pgadmin.node.view', [
and hide for system view in catalogs.
*/
canCreate: function(itemData, item, data) {
-
- // If check is false then, we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
-
- // To iterate over tree to check parent node
- while (i) {
-
- // If it is schema then allow user to create view
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-view' == d._type) {
-
- // Check if we are not child of view
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
-
- // by default we do not want to allow create menu
- return true;
-
+ return canCreate.canCreate(pgBrowser, 'coll-view', item, data);
},
});
}
diff --git a/web/pgadmin/browser/static/js/node.js b/web/pgadmin/browser/static/js/node.js
index ad582483..3baf554a 100644
--- a/web/pgadmin/browser/static/js/node.js
+++ b/web/pgadmin/browser/static/js/node.js
@@ -1,9 +1,12 @@
define('pgadmin.browser.node', [
+ 'sources/tree/pgadmin_tree_node',
'sources/gettext', 'jquery', 'underscore', 'underscore.string', 'sources/pgadmin',
'pgadmin.browser.menu', 'backbone', 'pgadmin.alertifyjs', 'pgadmin.browser.datamodel',
'backform', 'sources/browser/generate_url', 'sources/utils', 'pgadmin.browser.utils',
'pgadmin.backform',
-], function(gettext, $, _, S, pgAdmin, Menu, Backbone, Alertify, pgBrowser, Backform, generateUrl, commonUtils) {
+], function(
+ pgadminTreeNode,
+ gettext, $, _, S, pgAdmin, Menu, Backbone, Alertify, pgBrowser, Backform, generateUrl, commonUtils) {
var wcDocker = window.wcDocker,
keyCode = {
@@ -1566,7 +1569,6 @@ define('pgadmin.browser.node', [
* depends, statistics
*/
generate_url: function(item, type, d, with_id, info) {
-
var opURL = {
'create': 'obj',
'drop': 'obj',
@@ -1608,24 +1610,7 @@ define('pgadmin.browser.node', [
Collection: pgBrowser.DataCollection,
// Base class for Node Data Model
Model: pgBrowser.DataModel,
- getTreeNodeHierarchy: function(i) {
- var idx = 0,
- res = {},
- t = pgBrowser.tree,
- d;
- do {
- d = t.itemData(i);
- if (d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId) {
- res[d._type] = _.extend({}, d, {
- 'priority': idx,
- });
- idx -= 1;
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- } while (i);
-
- return res;
- },
+ getTreeNodeHierarchy: pgadminTreeNode.getTreeNodeHierarchyFromIdentifier.bind(pgBrowser),
cache: function(url, node_info, level, data) {
var cached = this.cached = this.cached || {},
hash = url,
diff --git a/web/pgadmin/static/js/alertify/dialog.js b/web/pgadmin/static/js/alertify/dialog.js
new file mode 100644
index 00000000..7b309944
--- /dev/null
+++ b/web/pgadmin/static/js/alertify/dialog.js
@@ -0,0 +1,114 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../gettext';
+import {sprintf} from 'sprintf-js';
+import {DialogFactory} from './dialog_factory';
+import Backform from '../backform.pgadmin';
+
+/**
+ * This class can be extended to create new dialog boxes.
+ * Examples of this can be found in:
+ * `web/pgadmin/static/js/backup/backup_dialog.js`
+ *
+ * Do not forget to add the new Dialog type to the `DialogFactory`
+ */
+export class Dialog {
+ constructor(errorAlertTitle,
+ dialogContainerSelector,
+ pgBrowser, $, alertify, DialogModel,
+ backform = Backform) {
+ this.errorAlertTitle = errorAlertTitle;
+ this.alertify = alertify;
+ this.pgBrowser = pgBrowser;
+ this.jquery = $;
+ this.dialogModel = DialogModel;
+ this.backform = backform;
+ this.dialogContainerSelector = dialogContainerSelector;
+ }
+
+ retrieveAncestorOfTypeServer(item) {
+ let serverInformation = null;
+ let aciTreeItem = item || this.pgBrowser.treeMenu.selected();
+ let treeNode = this.pgBrowser.treeMenu.findNodeByDomElement(aciTreeItem);
+
+ while (treeNode) {
+ const node_data = treeNode.getData();
+ if (node_data._type === 'server') {
+ serverInformation = node_data;
+ break;
+ }
+
+ if (treeNode.hasParent()) {
+ treeNode = treeNode.parent();
+ } else {
+ this.alertify.alert(
+ gettext(this.errorAlertTitle),
+ gettext('Please select server or child node from the browser tree.')
+ );
+ break;
+ }
+ }
+ return serverInformation;
+ }
+
+ hasBinariesConfiguration(serverInformation) {
+ const module = 'paths';
+ let preference_name = 'pg_bin_dir';
+ let msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
+
+ if ((serverInformation.type && serverInformation.type === 'ppas') ||
+ serverInformation.server_type === 'ppas') {
+ preference_name = 'ppas_bin_dir';
+ msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
+ }
+ const preference = this.pgBrowser.get_preference(module, preference_name);
+
+ if (preference) {
+ if (!preference.value) {
+ this.alertify.alert(gettext('Configuration required'), msg);
+ return false;
+ }
+ } else {
+ this.alertify.alert(
+ gettext(this.errorAlertTitle),
+ sprintf(gettext('Failed to load preference %s of module %s'), preference_name, module)
+ );
+ return false;
+ }
+ return true;
+ }
+
+ dialogName() {
+ return undefined;
+ }
+
+ createOrGetDialog(dialogTitle, typeOfDialog) {
+ const dialogName = this.dialogName(typeOfDialog);
+
+ if (!this.alertify[dialogName]) {
+ const self = this;
+ this.alertify.dialog(dialogName, function factory() {
+ return self.dialogFactory(dialogTitle, typeOfDialog);
+ });
+ }
+ return this.alertify[dialogName];
+ }
+
+ dialogFactory(dialogTitle, typeOfDialog) {
+ const factory = new DialogFactory(
+ this.pgBrowser,
+ this.jquery,
+ this.alertify,
+ this.dialogModel,
+ this.backform,
+ this.dialogContainerSelector);
+ return factory.create(dialogTitle, typeOfDialog);
+ }
+}
diff --git a/web/pgadmin/static/js/alertify/dialog_factory.js b/web/pgadmin/static/js/alertify/dialog_factory.js
new file mode 100644
index 00000000..500140b8
--- /dev/null
+++ b/web/pgadmin/static/js/alertify/dialog_factory.js
@@ -0,0 +1,52 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import * as BackupDialog from '../../../tools/backup/static/js/backup_dialog_wrapper';
+import {RestoreDialogWrapper} from '../../../tools/restore/static/js/restore_dialog_wrapper';
+
+export class DialogFactory {
+ constructor(pgBrowser, $,
+ alertify, DialogModel,
+ backform, dialogContainerSelector) {
+ this.pgBrowser = pgBrowser;
+ this.jquery = $;
+ this.alertify = alertify;
+ this.dialogModel = DialogModel;
+ this.backform = backform;
+ this.dialogContainerSelector = dialogContainerSelector;
+ }
+
+ create(dialogTitle, typeOfDialog) {
+ if (typeOfDialog === 'restore') {
+ return this.createRestoreDialog(dialogTitle, typeOfDialog);
+ } else {
+ return this.createBackupDialog(dialogTitle, typeOfDialog);
+ }
+ }
+
+ createRestoreDialog(dialogTitle, typeOfDialog) {
+ return new RestoreDialogWrapper(
+ this.dialogContainerSelector, dialogTitle, typeOfDialog,
+ this.jquery,
+ this.pgBrowser,
+ this.alertify,
+ this.dialogModel,
+ this.backform);
+ }
+
+ createBackupDialog(dialogTitle, typeOfDialog) {
+ return new BackupDialog.BackupDialogWrapper(
+ this.dialogContainerSelector, dialogTitle, typeOfDialog,
+ this.jquery,
+ this.pgBrowser,
+ this.alertify,
+ this.dialogModel,
+ this.backform);
+ }
+}
diff --git a/web/pgadmin/static/js/alertify/dialog_wrapper.js b/web/pgadmin/static/js/alertify/dialog_wrapper.js
new file mode 100644
index 00000000..b5ff8204
--- /dev/null
+++ b/web/pgadmin/static/js/alertify/dialog_wrapper.js
@@ -0,0 +1,57 @@
+import * as commonUtils from '../utils';
+
+export class DialogWrapper {
+ constructor(
+ dialogContainerSelector, dialogTitle, jquery, pgBrowser,
+ alertify, dialogModel, backform) {
+ this.hooks = {
+ onclose: function () {
+ if (this.view) {
+ this.view.remove({
+ data: true,
+ internal: true,
+ silent: true,
+ });
+ }
+ },
+ };
+ this.dialogContainerSelector = dialogContainerSelector;
+ this.dialogTitle = dialogTitle;
+ this.jquery = jquery;
+ this.pgBrowser = pgBrowser;
+ this.alertify = alertify;
+ this.dialogModel = dialogModel;
+ this.backform = backform;
+ }
+
+ build() {
+ this.alertify.pgDialogBuild.apply(this);
+ }
+
+ wasHelpButtonPressed(e) {
+ return e.button.element.name === 'dialog_help'
+ || e.button.element.name === 'object_help';
+ }
+
+ getSelectedNodeData(selectedTreeNode) {
+ if (!this.isNodeSelected(selectedTreeNode)) {
+ return undefined;
+ }
+ const treeNodeData = selectedTreeNode.getData();
+ if (treeNodeData) {
+ return treeNodeData;
+ }
+ return undefined;
+ }
+
+ focusOnDialog(dialog) {
+ dialog.$el.attr('tabindex', -1);
+ this.pgBrowser.keyboardNavigation.getDialogTabNavigator(dialog);
+ const container = dialog.$el.find('.tab-content:first > .tab-pane.active:first');
+ commonUtils.findAndSetFocus(container);
+ }
+
+ isNodeSelected(selectedTreeNode) {
+ return selectedTreeNode;
+ }
+}
diff --git a/web/pgadmin/static/js/menu/can_create.js b/web/pgadmin/static/js/menu/can_create.js
new file mode 100644
index 00000000..476be437
--- /dev/null
+++ b/web/pgadmin/static/js/menu/can_create.js
@@ -0,0 +1,37 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+/////////////////////////////////////////////////////////////
+
+export function canCreate(pgBrowser, childOfCatalogType, item, data) {
+ //If check is false then , we will allow create menu
+ if (data && data.check === false) {
+ return true;
+ }
+
+ let node = pgBrowser.treeMenu.findNodeByDomElement(item);
+
+ if (node.anyFamilyMember(parentCatalogOfTableChild.bind(null, childOfCatalogType))) {
+ return false;
+ }
+
+ return true;
+}
+
+function parentCatalogOfTableChild(arg, node) {
+ if (arg === node.getData()._type) {
+ if (node.hasParent()) {
+
+ let parent = node.parent();
+ if ('catalog' === parent.getData()._type) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
diff --git a/web/pgadmin/static/js/menu/menu_enabled.js b/web/pgadmin/static/js/menu/menu_enabled.js
new file mode 100644
index 00000000..05ff0f4a
--- /dev/null
+++ b/web/pgadmin/static/js/menu/menu_enabled.js
@@ -0,0 +1,52 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+function isNodeTypeSupported(backupSupportedNodes, nodeDataType, treeNode) {
+ return _.indexOf(backupSupportedNodes, nodeDataType) !== -1
+ && ancestorWithTypeCatalogDoesNotExists(treeNode);
+}
+
+export function isProvidedDataValid(treeNodeData) {
+ return !_.isUndefined(treeNodeData) && !_.isNull(treeNodeData);
+}
+
+function doesNodeHaveMenu(treeNodeData) {
+ return (treeNodeData._type === 'database' && treeNodeData.allowConn)
+ || treeNodeData._type !== 'database';
+}
+
+function ancestorWithTypeCatalogDoesNotExists(treeNode) {
+ let currentNode = treeNode;
+
+ while(currentNode.hasParent() && treeNode.parent().getData() !== null) {
+ if(currentNode.parent().getData()._type === 'catalog') {
+ return false;
+ }
+
+ currentNode = currentNode.parent();
+ }
+
+ return true;
+}
+
+export function menuEnabled(tree, backupSupportedNodes, treeNodeData, domTreeNode) {
+ let treeNode = tree.findNodeByDomElement(domTreeNode);
+ if (!treeNode) {
+ return false;
+ }
+
+ if (isProvidedDataValid(treeNodeData)) {
+ return isNodeTypeSupported(backupSupportedNodes, treeNodeData._type, treeNode)
+ && doesNodeHaveMenu(treeNodeData);
+ } else {
+ return false;
+ }
+}
+
+
diff --git a/web/pgadmin/static/js/tree/pgadmin_tree_node.js b/web/pgadmin/static/js/tree/pgadmin_tree_node.js
new file mode 100644
index 00000000..4faa58f3
--- /dev/null
+++ b/web/pgadmin/static/js/tree/pgadmin_tree_node.js
@@ -0,0 +1,69 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+/**
+ * This method received pgBrowser and new TreeNode object
+ *
+ * This method retrieves all the data that exists in the tree node and in
+ * `pgBrowser.Nodes` for all the parent node of the provided node.
+ *
+ * The 2 condition to get the information from pgBrowser.Nodes are:
+ * 1 - the variable _type of the tree node
+ * 2 - the presence of hasId in the pgBrowser.Nodes for the specific node
+ *
+ * Number 2 is used to ignore coll-* nodes as they do not add any useful
+ * information
+ */
+export function getTreeNodeHierarchyFromElement(pgBrowser, treeNode) {
+ return getTreeNodeHierarchy.call(pgBrowser, treeNode);
+}
+
+/**
+ * This method received an ACI Tree JQuery node
+ *
+ * This method retrieves all the data that exists in the tree node and in
+ * `pgBrowser.Nodes` for all the parent node of the provided node.
+ *
+ * The 2 condition to get the information from pgBrowser.Nodes are:
+ * 1 - the variable _type of the tree node
+ * 2 - the presence of hasId in the pgBrowser.Nodes for the specific node
+ *
+ * Number 2 is used to ignore coll-* nodes as they do not add any useful
+ * information
+ */
+export function getTreeNodeHierarchyFromIdentifier(aciTreeNodeIdentifier) {
+ let identifier = this.treeMenu.translateTreeNodeIdFromACITree(aciTreeNodeIdentifier);
+ let currentNode = this.treeMenu.findNode(identifier);
+ return getTreeNodeHierarchy.call(this, currentNode);
+}
+
+export function getTreeNodeHierarchy(currentNode) {
+ let idx = 0;
+ let result = {};
+
+ do {
+ const currentNodeData = currentNode.getData();
+ if (currentNodeData._type in this.Nodes && this.Nodes[currentNodeData._type].hasId) {
+ const nodeType = mapType(currentNodeData._type);
+ if (result[nodeType] === undefined) {
+ result[nodeType] = _.extend({}, currentNodeData, {
+ 'priority': idx,
+ });
+ idx -= 1;
+ }
+ }
+ currentNode = currentNode.hasParent() ? currentNode.parent() : null;
+ } while (currentNode);
+
+ return result;
+}
+
+function mapType(type) {
+ return type === 'partition' ? 'table' : type;
+}
diff --git a/web/pgadmin/tools/backup/static/js/backup.js b/web/pgadmin/tools/backup/static/js/backup.js
index ddbfaee4..d74ed6dd 100644
--- a/web/pgadmin/tools/backup/static/js/backup.js
+++ b/web/pgadmin/tools/backup/static/js/backup.js
@@ -3,9 +3,12 @@ define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'pgadmin.alertifyjs', 'backbone', 'pgadmin.backgrid',
'pgadmin.backform', 'pgadmin.browser', 'sources/utils',
+ 'tools/backup/static/js/menu_utils',
+ 'tools/backup/static/js/backup_dialog',
+ 'sources/menu/menu_enabled',
], function(
gettext, url_for, $, _, S, alertify, Backbone, Backgrid, Backform, pgBrowser,
-commonUtils
+commonUtils, menuUtils, globalBackupDialog, menuEnabled
) {
// if module is already initialized, refer to that.
@@ -394,48 +397,6 @@ commonUtils
this.initialized = true;
- // Define list of nodes on which backup context menu option appears
- var backup_supported_nodes = [
- 'database', 'schema', 'table', 'partition',
- ];
-
- /**
- Enable/disable backup menu in tools based
- on node selected
- if selected node is present in supported_nodes,
- menu will be enabled otherwise disabled.
- Also, hide it for system view in catalogs
- */
- var menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData,
- parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
-
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(backup_supported_nodes, d._type) !== -1 &&
- parent_data._type != 'catalog') {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
- var menu_enabled_server = function(itemData) {
- // If server node selected && connected
- if (!_.isUndefined(itemData) && !_.isNull(itemData))
- return (('server' === itemData._type) && itemData.connected);
- else
- return false;
- };
-
// Define the nodes on which the menus to be appear
var menus = [{
name: 'backup_global',
@@ -445,7 +406,7 @@ commonUtils
priority: 12,
label: gettext('Backup Globals...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_server',
module: this,
@@ -454,7 +415,7 @@ commonUtils
priority: 12,
label: gettext('Backup Server...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_global_ctx',
module: this,
@@ -464,7 +425,7 @@ commonUtils
priority: 12,
label: gettext('Backup Globals...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_server_ctx',
module: this,
@@ -474,7 +435,7 @@ commonUtils
priority: 12,
label: gettext('Backup Server...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_object',
module: this,
@@ -483,20 +444,22 @@ commonUtils
priority: 11,
label: gettext('Backup...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null,
+ pgBrowser.treeMenu, menuUtils.backupSupportedNodes),
}];
- for (var idx = 0; idx < backup_supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.backupSupportedNodes.length; idx++) {
menus.push({
- name: 'backup_' + backup_supported_nodes[idx],
- node: backup_supported_nodes[idx],
+ name: 'backup_' + menuUtils.backupSupportedNodes[idx],
+ node: menuUtils.backupSupportedNodes[idx],
module: this,
applies: ['context'],
callback: 'backup_objects',
priority: 11,
label: gettext('Backup...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null,
+ pgBrowser.treeMenu, menuUtils.backupSupportedNodes),
});
}
@@ -521,531 +484,25 @@ commonUtils
},
// Callback to draw Backup Dialog for globals/server
- start_backup_global_server: function(action, item, params) {
- var i = item || pgBrowser.tree.selected(),
- server_data = null;
-
- while (i) {
- var node_data = pgBrowser.tree.itemData(i);
- if (node_data._type == 'server') {
- server_data = node_data;
- break;
- }
-
- if (pgBrowser.tree.hasParent(i)) {
- i = $(pgBrowser.tree.parent(i));
- } else {
- alertify.alert(
- gettext('Backup Error'),
- gettext('Please select server or child node from the browser tree.')
- );
- break;
- }
- }
-
- if (!server_data) {
- return;
- }
-
- var module = 'paths',
- preference_name = 'pg_bin_dir',
- msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
-
- if ((server_data.type && server_data.type == 'ppas') ||
- server_data.server_type == 'ppas') {
- preference_name = 'ppas_bin_dir';
- msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
- }
-
- var preference = pgBrowser.get_preference(module, preference_name);
-
- if (preference) {
- if (!preference.value) {
- alertify.alert(gettext('Configuration required'), msg);
- return;
- }
- } else {
- alertify.alert(
- gettext('Backup Error'),
- S(gettext('Failed to load preference %s of module %s')).sprintf(preference_name, module).value()
- );
- return;
- }
-
- var of_type = undefined;
-
- // Set Notes according to type of backup
- if (!_.isUndefined(params['globals']) && params['globals']) {
- of_type = 'globals';
- } else {
- of_type = 'server';
- }
-
- var DialogName = 'BackupDialog_' + of_type,
- DialogTitle = ((of_type == 'globals') ?
- gettext('Backup Globals...') :
- gettext('Backup Server...'));
-
- if (!alertify[DialogName]) {
- alertify.dialog(DialogName, function factory() {
- return {
- main: function(title) {
- this.set('title', title);
- },
- build: function() {
- alertify.pgDialogBuild.apply(this);
- },
- setup: function() {
- return {
- buttons: [{
- text: '',
- className: 'btn btn-default pull-left fa fa-lg fa-info',
- attrs: {
- name: 'object_help',
- type: 'button',
- url: 'backup.html',
- label: gettext('Backup'),
- },
- }, {
- text: '',
- key: 112,
- className: 'btn btn-default pull-left fa fa-lg fa-question',
- attrs: {
- name: 'dialog_help',
- type: 'button',
- label: gettext('Backup'),
- url: url_for('help.static', {
- 'filename': 'backup_dialog.html',
- }),
- },
- }, {
- text: gettext('Backup'),
- key: 13,
- className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
- 'data-btn-name': 'backup',
- }, {
- text: gettext('Cancel'),
- key: 27,
- className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
- 'data-btn-name': 'cancel',
- }],
- // Set options for dialog
- options: {
- title: DialogTitle,
- //disable both padding and overflow control.
- padding: !1,
- overflow: !1,
- model: 0,
- resizable: true,
- maximizable: true,
- pinnable: false,
- closableByDimmer: false,
- modal: false,
- },
- };
- },
- hooks: {
- // Triggered when the dialog is closed
- onclose: function() {
- if (this.view) {
- // clear our backform model/view
- this.view.remove({
- data: true,
- internal: true,
- silent: true,
- });
- }
- },
- },
- prepare: function() {
- var self = this;
- // Disable Backup button until user provides Filename
- this.__internal.buttons[2].element.disabled = true;
-
- var $container = $('<div class=\'backup_dialog\'></div>');
- // Find current/selected node
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (!d)
- return;
- // Create treeInfo
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
- // Instance of backbone model
- var newModel = new BackupModel({
- type: of_type,
- }, {
- node_info: treeInfo,
- }),
- fields = Backform.generateViewSchema(
- treeInfo, newModel, 'create', node, treeInfo.server, true
- );
-
- var view = this.view = new Backform.Dialog({
- el: $container,
- model: newModel,
- schema: fields,
- });
- // Add our class to alertify
- $(this.elements.body.childNodes[0]).addClass(
- 'alertify_tools_dialog_properties obj_properties'
- );
- // Render dialog
- view.render();
-
- this.elements.content.appendChild($container.get(0));
-
- var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
- commonUtils.findAndSetFocus(container);
-
- // Listen to model & if filename is provided then enable Backup button
- this.view.model.on('change', function() {
- if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
- this.errorModel.clear();
- self.__internal.buttons[2].element.disabled = false;
- } else {
- self.__internal.buttons[2].element.disabled = true;
- this.errorModel.set('file', gettext('Please provide a filename'));
- }
- });
- },
- // Callback functions when click on the buttons of the Alertify dialogs
- callback: function(e) {
- // Fetch current server id
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (e.button.element.name == 'dialog_help' || e.button.element.name == 'object_help') {
- e.cancel = true;
- pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
- node, i, e.button.element.getAttribute('label'));
- return;
- }
-
- if (e.button['data-btn-name'] === 'backup') {
-
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- var self = this,
- baseUrl = url_for('backup.create_server_job', {
- 'sid': treeInfo.server._id,
- }),
- args = this.view.model.toJSON();
-
- $.ajax({
- url: baseUrl,
- method: 'POST',
- data: {
- 'data': JSON.stringify(args),
- },
- success: function(res) {
- if (res.success) {
- alertify.success(gettext('Backup job created.'), 5);
- pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
- } else {
- console.warn(res);
- }
- },
- error: function(xhr) {
- try {
- var err = $.parseJSON(xhr.responseText);
- alertify.alert(
- gettext('Backup job failed.'),
- err.errormsg
- );
- } catch (e) {
- console.warn(e.stack || e);
- }
- },
- });
- }
- },
- };
- });
- }
- alertify[DialogName](true).resizeTo('60%', '50%');
+ start_backup_global_server: function(action, treeItem, params) {
+ let dialog = new globalBackupDialog.BackupDialog(
+ pgBrowser,
+ $,
+ alertify,
+ BackupModel
+ );
+ dialog.draw(action, treeItem, params);
},
// Callback to draw Backup Dialog for objects
backup_objects: function(action, treeItem) {
-
- var i = treeItem || pgBrowser.tree.selected(),
- server_data = null;
-
- while (i) {
- var node_data = pgBrowser.tree.itemData(i);
- if (node_data._type == 'server') {
- server_data = node_data;
- break;
- }
-
- if (pgBrowser.tree.hasParent(i)) {
- i = $(pgBrowser.tree.parent(i));
- } else {
- alertify.alert(
- gettext('Backup Error'),
- gettext('Please select server or child node from tree.')
- );
- break;
- }
- }
-
- if (!server_data) {
- return;
- }
-
- var module = 'paths',
- preference_name = 'pg_bin_dir',
- msg = gettext('Please set binary path for PostgreSQL Server from preferences.');
-
- if ((server_data.type && server_data.type == 'ppas') ||
- server_data.server_type == 'ppas') {
- preference_name = 'ppas_bin_dir';
- msg = gettext('Please set binary path for EDB Postgres Advanced Server from preferences.');
- }
-
- var preference = pgBrowser.get_preference(module, preference_name);
-
- if (preference) {
- if (!preference.value) {
- alertify.alert(gettext('Configuration required'), msg);
- return;
- }
- } else {
- alertify.alert(
- gettext('Backup Error'),
- S(gettext('Failed to load preference %s of module %s')).sprintf(preference_name, module).value()
- );
- return;
- }
-
- var title = S(gettext('Backup (%s: %s)')),
- tree = pgBrowser.tree,
- item = treeItem || tree.selected(),
- data = item && item.length == 1 && tree.itemData(item),
- node = data && data._type && pgBrowser.Nodes[data._type];
-
- if (!node)
- return;
-
- title = title.sprintf(node.label, data.label).value();
-
- if (!alertify.backup_objects) {
- // Create Dialog title on the fly with node details
- alertify.dialog('backup_objects', function factory() {
- return {
- main: function(title) {
- this.set('title', title);
- },
- build: function() {
- alertify.pgDialogBuild.apply(this);
- },
- setup: function() {
- return {
- buttons: [{
- text: '',
- className: 'btn btn-default pull-left fa fa-lg fa-info',
- attrs: {
- name: 'object_help',
- type: 'button',
- url: 'backup.html',
- label: gettext('Backup'),
- },
- }, {
- text: '',
- key: 112,
- className: 'btn btn-default pull-left fa fa-lg fa-question',
- attrs: {
- name: 'dialog_help',
- type: 'button',
- label: gettext('Backup'),
- url: url_for('help.static', {
- 'filename': 'backup_dialog.html',
- }),
- },
- }, {
- text: gettext('Backup'),
- key: 13,
- className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
- 'data-btn-name': 'backup',
- }, {
- text: gettext('Cancel'),
- key: 27,
- className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
- 'data-btn-name': 'cancel',
- }],
- // Set options for dialog
- options: {
- title: title,
- //disable both padding and overflow control.
- padding: !1,
- overflow: !1,
- model: 0,
- resizable: true,
- maximizable: true,
- pinnable: false,
- closableByDimmer: false,
- modal: false,
- },
- };
- },
- hooks: {
- // triggered when the dialog is closed
- onclose: function() {
- if (this.view) {
- this.view.remove({
- data: true,
- internal: true,
- silent: true,
- });
- }
- },
- },
- prepare: function() {
- var self = this;
- // Disable Backup button until user provides Filename
- this.__internal.buttons[2].element.disabled = true;
- var $container = $('<div class=\'backup_dialog\'></div>');
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- var newModel = new BackupObjectModel({}, {
- node_info: treeInfo,
- }),
- fields = Backform.generateViewSchema(
- treeInfo, newModel, 'create', node, treeInfo.server, true
- );
-
- var view = this.view = new Backform.Dialog({
- el: $container,
- model: newModel,
- schema: fields,
- });
-
- $(this.elements.body.childNodes[0]).addClass(
- 'alertify_tools_dialog_properties obj_properties'
- );
-
- view.render();
-
- this.elements.content.appendChild($container.get(0));
-
- if(view) {
- view.$el.attr('tabindex', -1);
- // var dialogTabNavigator = pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
- commonUtils.findAndSetFocus(container);
- }
- // Listen to model & if filename is provided then enable Backup button
- this.view.model.on('change', function() {
- if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
- this.errorModel.clear();
- self.__internal.buttons[2].element.disabled = false;
- } else {
- self.__internal.buttons[2].element.disabled = true;
- this.errorModel.set('file', gettext('Please provide filename'));
- }
- });
-
- },
- // Callback functions when click on the buttons of the Alertify dialogs
- callback: function(e) {
- // Fetch current server id
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (e.button.element.name == 'dialog_help' || e.button.element.name == 'object_help') {
- e.cancel = true;
- pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
- node, i, e.button.element.getAttribute('label'));
- return;
- }
-
- if (e.button['data-btn-name'] === 'backup') {
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- // Set current database into model
- this.view.model.set('database', treeInfo.database._label);
-
- // We will remove once object tree is implemented
- // If selected node is Schema then add it in model
- if (d._type == 'schema') {
- var schemas = [];
- schemas.push(d._label);
- this.view.model.set('schemas', schemas);
- }
- // If selected node is Table then add it in model along with
- // its schema
- if (d._type == 'table') {
- this.view.model.set(
- 'tables', [
- [treeInfo.schema._label, d._label],
- ]
- );
- }
-
- // Remove ratio attribute from model if it has empty string.
- // The valid value can be between 0 to 9.
- if (_.isEmpty(this.view.model.get('ratio'))) {
- this.view.model.unset('ratio');
- }
-
- var self = this,
- baseUrl = url_for('backup.create_object_job', {
- 'sid': treeInfo.server._id,
- }),
- args = this.view.model.toJSON();
-
- $.ajax({
- url: baseUrl,
- method: 'POST',
- data: {
- 'data': JSON.stringify(args),
- },
- success: function(res) {
- if (res.success) {
- alertify.success(gettext('Backup job created.'), 5);
- pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
- }
- },
- error: function(xhr) {
- try {
- var err = $.parseJSON(xhr.responseText);
- alertify.alert(
- gettext('Backup job failed.'),
- err.errormsg
- );
- } catch (e) {
- console.warn(e.stack || e);
- }
- },
- });
- }
- },
- };
- });
- }
- alertify.backup_objects(title).resizeTo('65%', '60%');
+ let dialog = new globalBackupDialog.BackupDialog(
+ pgBrowser,
+ $,
+ alertify,
+ BackupObjectModel
+ );
+ dialog.draw(action, treeItem, null);
},
};
return pgBrowser.Backup;
diff --git a/web/pgadmin/tools/backup/static/js/backup_dialog.js b/web/pgadmin/tools/backup/static/js/backup_dialog.js
new file mode 100644
index 00000000..e603fcf9
--- /dev/null
+++ b/web/pgadmin/tools/backup/static/js/backup_dialog.js
@@ -0,0 +1,65 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../../../../static/js/gettext';
+import Backform from '../../../../static/js/backform.pgadmin';
+import {Dialog} from '../../../../static/js/alertify/dialog';
+
+export class BackupDialog extends Dialog {
+ constructor(pgBrowser, $, alertify, BackupModel, backform = Backform) {
+ super('Backup Error',
+ '<div class=\'backup_dialog\'></div>',
+ pgBrowser, $, alertify, BackupModel, backform);
+ }
+
+ draw(action, aciTreeItem, params) {
+ const serverInformation = this.retrieveAncestorOfTypeServer(aciTreeItem);
+
+ if (!serverInformation) {
+ return;
+ }
+
+ if (!this.hasBinariesConfiguration(serverInformation)) {
+ return;
+ }
+
+ const typeOfDialog = BackupDialog.typeOfDialog(params);
+
+ const dialog = this.createOrGetDialog(BackupDialog.dialogTitle(typeOfDialog),
+ typeOfDialog);
+ dialog(true).resizeTo('60%', '50%');
+ }
+
+ static typeOfDialog(params) {
+ if(params === null) {
+ return 'backup_objects';
+ }
+ let typeOfDialog = 'server';
+ if (!_.isUndefined(params['globals']) && params['globals']) {
+ typeOfDialog = 'globals';
+ }
+ return typeOfDialog;
+ }
+
+ static dialogTitle(typeOfDialog) {
+ if(typeOfDialog === 'backup_objects') {
+ return null;
+ }
+ return ((typeOfDialog === 'globals') ?
+ gettext('Backup Globals...') :
+ gettext('Backup Server...'));
+ }
+
+ dialogName(typeOfDialog) {
+ if(typeOfDialog === 'backup_objects') {
+ return typeOfDialog;
+ }
+ return 'BackupDialog_' + typeOfDialog;
+ }
+}
diff --git a/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js b/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js
new file mode 100644
index 00000000..2cebe3d1
--- /dev/null
+++ b/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js
@@ -0,0 +1,258 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {getTreeNodeHierarchyFromElement} from '../../../../static/js/tree/pgadmin_tree_node';
+import axios from 'axios/index';
+import gettext from '../../../../static/js/gettext';
+import url_for from '../../../../static/js/url_for';
+import _ from 'underscore';
+import {DialogWrapper} from '../../../../static/js/alertify/dialog_wrapper';
+
+export class BackupDialogWrapper extends DialogWrapper {
+ constructor(dialogContainerSelector, dialogTitle, typeOfDialog,
+ jquery, pgBrowser, alertify, dialogModel, backform) {
+ super(dialogContainerSelector, dialogTitle, jquery,
+ pgBrowser, alertify, dialogModel, backform);
+ this.typeOfDialog = typeOfDialog;
+ }
+
+ main(title) {
+ this.set('title', title);
+ }
+
+ setup() {
+ return {
+ buttons: [{
+ text: '',
+ className: 'btn btn-default pull-left fa fa-lg fa-info',
+ attrs: {
+ name: 'object_help',
+ type: 'button',
+ url: 'backup.html',
+ label: gettext('Backup'),
+ },
+ }, {
+ text: '',
+ key: 112,
+ className: 'btn btn-default pull-left fa fa-lg fa-question',
+ attrs: {
+ name: 'dialog_help',
+ type: 'button',
+ label: gettext('Backup'),
+ url: url_for('help.static', {
+ 'filename': 'backup_dialog.html',
+ }),
+ },
+ }, {
+ text: gettext('Backup'),
+ key: 13,
+ className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
+ 'data-btn-name': 'backup',
+ }, {
+ text: gettext('Cancel'),
+ key: 27,
+ className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
+ 'data-btn-name': 'cancel',
+ }],
+ // Set options for dialog
+ options: {
+ title: this.dialogTitle,
+ //disable both padding and overflow control.
+ padding: !1,
+ overflow: !1,
+ model: 0,
+ resizable: true,
+ maximizable: true,
+ pinnable: false,
+ closableByDimmer: false,
+ modal: false,
+ },
+ };
+ }
+
+ prepare() {
+ this.disableBackupButton();
+
+ const $container = this.jquery(this.dialogContainerSelector);
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = this.getSelectedNodeData(selectedTreeNode);
+ if (!selectedTreeNodeData) {
+ return;
+ }
+
+ const node = this.pgBrowser.Nodes[selectedTreeNodeData._type];
+ if (this.dialogTitle === null) {
+ const title = `Backup (${node.label}: ${selectedTreeNodeData.label})`;
+ this.main(title);
+ }
+
+ const treeInfo = getTreeNodeHierarchyFromElement(this.pgBrowser, selectedTreeNode);
+ const dialog = this.createDialog(node, treeInfo, this.typeOfDialog, $container);
+ this.addAlertifyClassToBackupNodeChildNodes();
+ dialog.render();
+
+ this.elements.content.appendChild($container.get(0));
+
+ this.focusOnDialog(dialog);
+ this.setListenersForFilenameChanges();
+ }
+
+ callback(event) {
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = this.getSelectedNodeData(selectedTreeNode);
+ const node = selectedTreeNodeData && this.pgBrowser.Nodes[selectedTreeNodeData._type];
+
+ if (this.wasHelpButtonPressed(event)) {
+ event.cancel = true;
+ this.pgBrowser.showHelp(
+ event.button.element.name,
+ event.button.element.getAttribute('url'),
+ node,
+ selectedTreeNode,
+ event.button.element.getAttribute('label')
+ );
+ return;
+ }
+
+ if (this.wasBackupButtonPressed(event)) {
+
+ if (!selectedTreeNodeData)
+ return;
+
+ const serverIdentifier = this.retrieveServerIdentifier(node, selectedTreeNode);
+
+ const dialog = this;
+ let urlShortcut = 'backup.create_server_job';
+ if (this.typeOfDialog === 'backup_objects') {
+ urlShortcut = 'backup.create_object_job';
+ }
+ const baseUrl = url_for(urlShortcut, {
+ 'sid': serverIdentifier,
+ });
+
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.pgBrowser,
+ selectedTreeNode
+ );
+
+ this.setExtraParameters(selectedTreeNode, treeInfo);
+
+ let service = axios.create({});
+ service.post(
+ baseUrl,
+ this.view.model.toJSON()
+ ).then(function () {
+ dialog.alertify.success(gettext('Backup job created.'), 5);
+ dialog.pgBrowser.Events.trigger('pgadmin-bgprocess:created', dialog);
+ }).catch(function (error) {
+ try {
+ const err = error.response.data;
+ dialog.alertify.alert(
+ gettext('Backup job failed.'),
+ err.errormsg
+ );
+ } catch (e) {
+ console.warn(e.stack || e);
+ }
+ });
+ }
+ }
+
+ addAlertifyClassToBackupNodeChildNodes() {
+ this.jquery(this.elements.body.childNodes[0]).addClass(
+ 'alertify_tools_dialog_properties obj_properties'
+ );
+ }
+
+ getSelectedNode() {
+ const tree = this.pgBrowser.treeMenu;
+ const selectedNode = tree.selected();
+ if (selectedNode) {
+ return tree.findNodeByDomElement(selectedNode);
+ } else {
+ return undefined;
+ }
+ }
+
+ disableBackupButton() {
+ this.__internal.buttons[2].element.disabled = true;
+ }
+
+ enableBackupButton() {
+ this.__internal.buttons[2].element.disabled = false;
+ }
+
+ createDialog(node, treeInfo, typeOfDialog, $container) {
+ let attributes = {};
+ if (typeOfDialog !== 'backup_objects') {
+ attributes['type'] = typeOfDialog;
+ }
+ // Instance of backbone model
+ const newModel = new this.dialogModel(attributes, {
+ node_info: treeInfo,
+ });
+ const fields = this.backform.generateViewSchema(
+ treeInfo, newModel, 'create', node, treeInfo.server, true
+ );
+
+ return this.view = new this.backform.Dialog({
+ el: $container,
+ model: newModel,
+ schema: fields,
+ });
+ }
+
+ retrieveServerIdentifier(node, selectedTreeNode) {
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.pgBrowser,
+ selectedTreeNode
+ );
+ return treeInfo.server._id;
+ }
+
+ setListenersForFilenameChanges() {
+ const self = this;
+
+ this.view.model.on('change', function () {
+ if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
+ this.errorModel.clear();
+ self.enableBackupButton();
+ } else {
+ self.disableBackupButton();
+ this.errorModel.set('file', gettext('Please provide a filename'));
+ }
+ });
+ }
+
+ setExtraParameters(selectedTreeNode, treeInfo) {
+ if (this.typeOfDialog === 'backup_objects') {
+
+ this.view.model.set('database', treeInfo.database._label);
+
+ const nodeData = selectedTreeNode.getData();
+ if (nodeData._type === 'schema') {
+ this.view.model.set('schemas', [nodeData._label]);
+ }
+
+ if (nodeData._type === 'table') {
+ this.view.model.set('tables', [
+ [treeInfo.schema._label, nodeData._label],
+ ]);
+ }
+
+ if (_.isEmpty(this.view.model.get('ratio'))) {
+ this.view.model.unset('ratio');
+ }
+ }
+ }
+
+ wasBackupButtonPressed(event) {
+ return event.button['data-btn-name'] === 'backup';
+ }
+}
diff --git a/web/pgadmin/tools/backup/static/js/menu_utils.js b/web/pgadmin/tools/backup/static/js/menu_utils.js
new file mode 100644
index 00000000..fdf400af
--- /dev/null
+++ b/web/pgadmin/tools/backup/static/js/menu_utils.js
@@ -0,0 +1,23 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {isProvidedDataValid} from '../../../../static/js/menu/menu_enabled';
+
+export const backupSupportedNodes = [
+ 'database', 'schema', 'table', 'partition',
+];
+
+function isNodeAServerAndConnected(treeNodeData) {
+ return (('server' === treeNodeData._type) && treeNodeData.connected);
+}
+
+export function menuEnabledServer(treeNodeData) {
+ return isProvidedDataValid(treeNodeData)
+ && isNodeAServerAndConnected(treeNodeData);
+}
diff --git a/web/pgadmin/tools/datagrid/static/js/datagrid.js b/web/pgadmin/tools/datagrid/static/js/datagrid.js
index b0ed60f6..520a9ce5 100644
--- a/web/pgadmin/tools/datagrid/static/js/datagrid.js
+++ b/web/pgadmin/tools/datagrid/static/js/datagrid.js
@@ -1,10 +1,14 @@
define('pgadmin.datagrid', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'pgadmin.alertifyjs', 'sources/pgadmin', 'bundled_codemirror',
- 'sources/sqleditor_utils', 'backbone', 'wcdocker',
+ 'sources/sqleditor_utils', 'backbone',
+ 'tools/datagrid/static/js/show_data',
+ 'tools/datagrid/static/js/get_panel_title',
+ 'tools/datagrid/static/js/show_query_tool',
+ 'wcdocker',
], function(
gettext, url_for, $, _, alertify, pgAdmin, codemirror, sqlEditorUtils,
- Backbone
+ Backbone, showData, panelTitle, showQueryTool
) {
// Some scripts do export their object in the window only.
// Generally the one, which do no have AMD support.
@@ -161,55 +165,7 @@ define('pgadmin.datagrid', [
// This is a callback function to show data when user click on menu item.
show_data_grid: function(data, i) {
- var self = this,
- d = pgAdmin.Browser.tree.itemData(i);
- if (d === undefined) {
- alertify.alert(
- gettext('Data Grid Error'),
- gettext('No object selected.')
- );
- return;
- }
-
- // Get the parent data from the tree node hierarchy.
- var node = pgBrowser.Nodes[d._type],
- parentData = node.getTreeNodeHierarchy(i);
-
- // If server, database or schema is undefined then return from the function.
- if (parentData.server === undefined || parentData.database === undefined) {
- return;
- }
- // If schema, view, catalog object all are undefined then return from the function.
- if (parentData.schema === undefined && parentData.view === undefined &&
- parentData.catalog === undefined) {
- return;
- }
-
- var nsp_name = '';
-
- if (parentData.schema != undefined) {
- nsp_name = parentData.schema.label;
- }
- else if (parentData.view != undefined) {
- nsp_name = parentData.view.label;
- }
- else if (parentData.catalog != undefined) {
- nsp_name = parentData.catalog.label;
- }
- var url_params = {
- 'cmd_type': data.mnuid,
- 'obj_type': d._type,
- 'sgid': parentData.server_group._id,
- 'sid': parentData.server._id,
- 'did': parentData.database._id,
- 'obj_id': d._id,
- };
-
- var baseUrl = url_for('datagrid.initialize_datagrid', url_params);
- var grid_title = parentData.server.label + ' - ' + parentData.database.label + ' - '
- + nsp_name + '.' + d.label;
-
- self.create_transaction(baseUrl, null, 'false', parentData.server.server_type, '', grid_title, '');
+ showData.showDataGrid(this, pgBrowser, alertify, data, i);
},
// This is a callback function to show filtered data when user click on menu item.
@@ -384,63 +340,12 @@ define('pgadmin.datagrid', [
},
get_panel_title: function() {
- // Get the parent data from the tree node hierarchy.
- var tree = pgAdmin.Browser.tree,
- selected_item = tree.selected(),
- item_data = tree.itemData(selected_item);
-
- var node = pgBrowser.Nodes[item_data._type],
- parentData = node.getTreeNodeHierarchy(selected_item);
-
- // If server, database is undefined then return from the function.
- if (parentData.server === undefined) {
- return;
- }
- // If Database is not available then use default db
- var db_label = parentData.database ? parentData.database.label
- : parentData.server.db;
-
- var grid_title = db_label + ' on ' + parentData.server.user.name + '@' +
- parentData.server.label;
- return grid_title;
+ return panelTitle.getPanelTitle(pgBrowser);
},
// This is a callback function to show query tool when user click on menu item.
- show_query_tool: function(url, i, panel_title) {
- var sURL = url || '',
- d = pgAdmin.Browser.tree.itemData(i);
-
- panel_title = panel_title || '';
- if (d === undefined) {
- alertify.alert(
- gettext('Query Tool Error'),
- gettext('No object selected.')
- );
- return;
- }
-
- // Get the parent data from the tree node hierarchy.
- var node = pgBrowser.Nodes[d._type],
- parentData = node.getTreeNodeHierarchy(i);
-
- // If server, database is undefined then return from the function.
- if (parentData.server === undefined) {
- return;
- }
-
- var url_params = {
- 'sgid': parentData.server_group._id,
- 'sid': parentData.server._id,
- },
- url_endpoint = 'datagrid.initialize_query_tool';
- // If database not present then use Maintenance database
- // We will handle this at server side
- if (parentData.database) {
- url_params['did'] = parentData.database._id;
- url_endpoint = 'datagrid.initialize_query_tool_with_did';
- }
- var baseUrl = url_for(url_endpoint, url_params);
-
- this.create_transaction(baseUrl, null, 'true', parentData.server.server_type, sURL, panel_title, '', false);
+ show_query_tool: function(url, aciTreeIdentifier, panelTitle) {
+ showQueryTool.showQueryTool(this, pgBrowser, alertify, url,
+ aciTreeIdentifier, panelTitle);
},
create_transaction: function(baseUrl, target, is_query_tool, server_type, sURL, panel_title, sql_filter, recreate) {
var self = this;
diff --git a/web/pgadmin/tools/datagrid/static/js/get_panel_title.js b/web/pgadmin/tools/datagrid/static/js/get_panel_title.js
new file mode 100644
index 00000000..64b3a3d2
--- /dev/null
+++ b/web/pgadmin/tools/datagrid/static/js/get_panel_title.js
@@ -0,0 +1,33 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {getTreeNodeHierarchyFromIdentifier} from '../../../../static/js/tree/pgadmin_tree_node';
+
+function getDatabaseLabel(parentData) {
+ return parentData.database ? parentData.database.label
+ : parentData.server.db;
+}
+
+function isServerInformationAvailable(parentData) {
+ return parentData.server === undefined;
+}
+
+export function getPanelTitle(pgBrowser) {
+ const selected_item = pgBrowser.treeMenu.selected();
+
+ const parentData = getTreeNodeHierarchyFromIdentifier
+ .call(pgBrowser, selected_item);
+ if (isServerInformationAvailable(parentData)) {
+ return;
+ }
+
+ const db_label = getDatabaseLabel(parentData);
+
+ return `${db_label} on ${parentData.server.user.name}@${parentData.server.label}`;
+}
diff --git a/web/pgadmin/tools/datagrid/static/js/show_data.js b/web/pgadmin/tools/datagrid/static/js/show_data.js
new file mode 100644
index 00000000..373c97cd
--- /dev/null
+++ b/web/pgadmin/tools/datagrid/static/js/show_data.js
@@ -0,0 +1,92 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import gettext from '../../../../static/js/gettext';
+import url_for from '../../../../static/js/url_for';
+import {getTreeNodeHierarchyFromIdentifier} from '../../../../static/js/tree/pgadmin_tree_node';
+
+export function showDataGrid(
+ datagrid,
+ pgBrowser,
+ alertify,
+ connectionData,
+ aciTreeIdentifier
+) {
+ const node = pgBrowser.treeMenu.findNodeByDomElement(aciTreeIdentifier);
+ if (node === undefined || !node.getData()) {
+ alertify.alert(
+ gettext('Data Grid Error'),
+ gettext('No object selected.')
+ );
+ return;
+ }
+
+ const parentData = getTreeNodeHierarchyFromIdentifier.call(
+ pgBrowser,
+ aciTreeIdentifier
+ );
+
+ if (hasServerOrDatabaseConfiguration(parentData)
+ || !hasSchemaOrCatalogOrViewInformation(parentData)) {
+ return;
+ }
+
+ let namespaceName = retrieveNameSpaceName(parentData);
+ const baseUrl = generateUrl(connectionData, node.getData(), parentData);
+ const grid_title = generateDatagridTitle(parentData, namespaceName, node.getData());
+
+ datagrid.create_transaction(
+ baseUrl,
+ null,
+ 'false',
+ parentData.server.server_type,
+ '',
+ grid_title,
+ ''
+ );
+}
+
+
+function retrieveNameSpaceName(parentData) {
+ if (parentData.schema !== undefined) {
+ return parentData.schema.label;
+ }
+ else if (parentData.view !== undefined) {
+ return parentData.view.label;
+ }
+ else if (parentData.catalog !== undefined) {
+ return parentData.catalog.label;
+ }
+ return '';
+}
+
+function generateUrl(connectionData, nodeData, parentData) {
+ const url_params = {
+ 'cmd_type': connectionData.mnuid,
+ 'obj_type': nodeData._type,
+ 'sgid': parentData.server_group._id,
+ 'sid': parentData.server._id,
+ 'did': parentData.database._id,
+ 'obj_id': nodeData._id,
+ };
+
+ return url_for('datagrid.initialize_datagrid', url_params);
+}
+
+function hasServerOrDatabaseConfiguration(parentData) {
+ return parentData.server === undefined || parentData.database === undefined;
+}
+
+function hasSchemaOrCatalogOrViewInformation(parentData) {
+ return parentData.schema !== undefined || parentData.view !== undefined ||
+ parentData.catalog !== undefined;
+}
+
+function generateDatagridTitle(parentData, namespaceName, nodeData) {
+ return `${parentData.server.label} - ${parentData.database.label} - ${namespaceName}.${nodeData.label}`;
+}
diff --git a/web/pgadmin/tools/datagrid/static/js/show_query_tool.js b/web/pgadmin/tools/datagrid/static/js/show_query_tool.js
new file mode 100644
index 00000000..0eb12b8b
--- /dev/null
+++ b/web/pgadmin/tools/datagrid/static/js/show_query_tool.js
@@ -0,0 +1,63 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../../../../static/js/gettext';
+import url_for from '../../../../static/js/url_for';
+import {getTreeNodeHierarchyFromIdentifier} from '../../../../static/js/tree/pgadmin_tree_node';
+
+function hasDatabaseInformation(parentData) {
+ return parentData.database;
+}
+
+function generateUrl(parentData) {
+ let url_endpoint = 'datagrid.initialize_query_tool';
+ let url_params = {
+ 'sgid': parentData.server_group._id,
+ 'sid': parentData.server._id,
+ };
+
+ if (hasDatabaseInformation(parentData)) {
+ url_params['did'] = parentData.database._id;
+ url_endpoint = 'datagrid.initialize_query_tool_with_did';
+ }
+
+ return url_for(url_endpoint, url_params);
+}
+
+function hasServerInformations(parentData) {
+ return parentData.server === undefined;
+}
+
+export function showQueryTool(datagrid, pgBrowser, alertify, url,
+ aciTreeIdentifier, panelTitle) {
+ const sURL = url || '';
+ const queryToolTitle = panelTitle || '';
+
+ const currentNode = pgBrowser.treeMenu.findNodeByDomElement(aciTreeIdentifier);
+ if (currentNode === undefined) {
+ alertify.alert(
+ gettext('Query Tool Error'),
+ gettext('No object selected.')
+ );
+ return;
+ }
+
+ const parentData = getTreeNodeHierarchyFromIdentifier.call(
+ pgBrowser, aciTreeIdentifier);
+
+ if (hasServerInformations(parentData)) {
+ return;
+ }
+
+ const baseUrl = generateUrl(parentData);
+
+ datagrid.create_transaction(
+ baseUrl, null, 'true',
+ parentData.server.server_type, sURL, queryToolTitle, '', false);
+}
diff --git a/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js b/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js
index 750887ec..cf234f1d 100644
--- a/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js
+++ b/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js
@@ -2,12 +2,16 @@
define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'pgadmin.alertifyjs', 'pgadmin.backgrid', 'pgadmin.backform',
- 'pgadmin.browser', 'pgadmin.browser.node', 'backgrid.select.all',
+ 'pgadmin.browser', 'pgadmin.browser.node',
+ 'tools/grant_wizard/static/js/menu_utils',
+ 'sources/menu/menu_enabled',
+
+ 'backgrid.select.all',
'backgrid.filter', 'pgadmin.browser.server.privilege',
'pgadmin.browser.wizard',
], function(
gettext, url_for, $, _, Backbone, Alertify, Backgrid, Backform, pgBrowser,
- pgNode
+ pgNode, menuUtils, menuEnabled
) {
// if module is already initialized, refer to that.
@@ -143,41 +147,6 @@ define([
this.initialized = true;
- // Define list of nodes on which grant wizard context menu option appears
- var supported_nodes = [
- 'schema', 'coll-function', 'coll-sequence',
- 'coll-table', 'coll-view', 'coll-procedure',
- 'coll-mview', 'database', 'coll-trigger_function',
- ],
-
- /**
- Enable/disable grantwizard menu in tools based
- on node selected
- if selected node is present in supported_nodes,
- menu will be enabled otherwise disabled.
- Also, hide it for system view in catalogs
- */
- menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
- var parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(supported_nodes, d._type) !== -1 &&
- parent_data._type != 'catalog') {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
// Define the nodes on which the menus to be appear
var menus = [{
name: 'grant_wizard_schema',
@@ -187,21 +156,23 @@ define([
priority: 14,
label: gettext('Grant Wizard...'),
icon: 'fa fa-unlock-alt',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null,
+ pgBrowser.treeMenu, menuUtils.supportedNodes),
}];
// Add supported menus into the menus list
- for (var idx = 0; idx < supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.supportedNodes.length; idx++) {
menus.push({
- name: 'grant_wizard_schema_context_' + supported_nodes[idx],
- node: supported_nodes[idx],
+ name: 'grant_wizard_schema_context_' + menuUtils.supportedNodes[idx],
+ node: menuUtils.supportedNodes[idx],
module: this,
applies: ['context'],
callback: 'start_grant_wizard',
priority: 14,
label: gettext('Grant Wizard...'),
icon: 'fa fa-unlock-alt',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null,
+ pgBrowser.treeMenu, menuUtils.supportedNodes),
});
}
pgBrowser.add_menus(menus);
diff --git a/web/pgadmin/tools/grant_wizard/static/js/menu_utils.js b/web/pgadmin/tools/grant_wizard/static/js/menu_utils.js
new file mode 100644
index 00000000..b56ce3cd
--- /dev/null
+++ b/web/pgadmin/tools/grant_wizard/static/js/menu_utils.js
@@ -0,0 +1,16 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export const supportedNodes = [
+ 'schema', 'coll-function', 'coll-sequence',
+ 'coll-table', 'coll-view', 'coll-procedure',
+ 'coll-mview', 'database', 'coll-trigger_function',
+];
+
+
diff --git a/web/pgadmin/tools/import_export/static/js/import_export.js b/web/pgadmin/tools/import_export/static/js/import_export.js
index 3058f122..bbb045cd 100644
--- a/web/pgadmin/tools/import_export/static/js/import_export.js
+++ b/web/pgadmin/tools/import_export/static/js/import_export.js
@@ -1,10 +1,13 @@
define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'underscore.string', 'pgadmin.alertifyjs',
'sources/pgadmin', 'pgadmin.browser', 'backbone', 'backgrid', 'backform',
- 'sources/utils', 'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser.node.ui',
+ 'sources/utils',
+ 'sources/menu/menu_enabled',
+
+ 'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser.node.ui',
], function(
gettext, url_for, $, _, S, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid,
-Backform, commonUtils
+Backform, commonUtils, menuEnabled
) {
pgAdmin = pgAdmin || window.pgAdmin || {};
@@ -383,25 +386,6 @@ Backform, commonUtils
this.initialized = true;
- /*
- * Enable/disable import menu in tools based on node selected. Import
- * menu will be enabled only when user select table node.
- */
- var menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
- var parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data))
- return (
- (_.indexOf(['table'], d._type) !== -1 &&
- parent_data._type != 'catalog') ? true : false
- );
- else
- return false;
- };
-
// Initialize the context menu to display the import options when user open the context menu for table
pgBrowser.add_menus([{
name: 'import',
@@ -413,7 +397,7 @@ Backform, commonUtils
priority: 10,
label: gettext('Import/Export...'),
icon: 'fa fa-shopping-cart',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null, pgBrowser.treeMenu, ['table']),
}]);
},
diff --git a/web/pgadmin/tools/maintenance/static/js/maintenance.js b/web/pgadmin/tools/maintenance/static/js/maintenance.js
index 81e45944..f32ce5c4 100644
--- a/web/pgadmin/tools/maintenance/static/js/maintenance.js
+++ b/web/pgadmin/tools/maintenance/static/js/maintenance.js
@@ -2,11 +2,14 @@ define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'pgadmin.alertifyjs', 'sources/pgadmin', 'pgadmin.browser', 'backbone',
'backgrid', 'backform', 'sources/utils',
+ 'tools/maintenance/static/js/menu_utils',
+ 'sources/menu/menu_enabled',
'pgadmin.backform', 'pgadmin.backgrid',
'pgadmin.browser.node.ui',
], function(
gettext, url_for, $, _, S, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid,
- Backform, commonUtils
+ Backform, commonUtils,
+ menuUtils, menuEnabled
) {
pgAdmin = pgAdmin || window.pgAdmin || {};
@@ -168,36 +171,6 @@ define([
this.initialized = true;
- var maintenance_supported_nodes = [
- 'database', 'table', 'primary_key',
- 'unique_constraint', 'index', 'partition',
- ];
-
- /**
- Enable/disable Maintenance menu in tools based on node selected.
- Maintenance menu will be enabled only when user select table and database node.
- */
- var menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
- var parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(maintenance_supported_nodes, d._type) !== -1 &&
- parent_data._type != 'catalog') {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
var menus = [{
name: 'maintenance',
module: this,
@@ -206,21 +179,23 @@ define([
priority: 10,
label: gettext('Maintenance...'),
icon: 'fa fa-wrench',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null,
+ pgBrowser.treeMenu, menuUtils.maintenanceSupportedNodes),
}];
// Add supported menus into the menus list
- for (var idx = 0; idx < maintenance_supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.maintenanceSupportedNodes.length; idx++) {
menus.push({
- name: 'maintenance_context_' + maintenance_supported_nodes[idx],
- node: maintenance_supported_nodes[idx],
+ name: 'maintenance_context_' + menuUtils.maintenanceSupportedNodes[idx],
+ node: menuUtils.maintenanceSupportedNodes[idx],
module: this,
applies: ['context'],
callback: 'callback_maintenance',
priority: 10,
label: gettext('Maintenance...'),
icon: 'fa fa-wrench',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null,
+ pgBrowser.treeMenu, menuUtils.maintenanceSupportedNodes),
});
}
pgBrowser.add_menus(menus);
diff --git a/web/pgadmin/tools/maintenance/static/js/menu_utils.js b/web/pgadmin/tools/maintenance/static/js/menu_utils.js
new file mode 100644
index 00000000..8cde1baa
--- /dev/null
+++ b/web/pgadmin/tools/maintenance/static/js/menu_utils.js
@@ -0,0 +1,13 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export const maintenanceSupportedNodes = [
+ 'database', 'table', 'primary_key',
+ 'unique_constraint', 'index', 'partition',
+];
diff --git a/web/pgadmin/tools/restore/static/js/menu_utils.js b/web/pgadmin/tools/restore/static/js/menu_utils.js
new file mode 100644
index 00000000..2d35c951
--- /dev/null
+++ b/web/pgadmin/tools/restore/static/js/menu_utils.js
@@ -0,0 +1,18 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export const restoreSupportedNodes = [
+ 'database',
+ 'schema',
+ 'table',
+ 'function',
+ 'trigger',
+ 'index',
+ 'partition',
+];
diff --git a/web/pgadmin/tools/restore/static/js/restore.js b/web/pgadmin/tools/restore/static/js/restore.js
index 5c082a9f..eaa7a15e 100644
--- a/web/pgadmin/tools/restore/static/js/restore.js
+++ b/web/pgadmin/tools/restore/static/js/restore.js
@@ -1,11 +1,13 @@
-// Restore dialog
define('tools.restore', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'underscore.string', 'pgadmin.alertifyjs', 'pgadmin.browser',
'pgadmin.backgrid', 'pgadmin.backform', 'sources/utils',
+ 'tools/restore/static/js/menu_utils',
+ 'sources/menu/menu_enabled',
+ 'tools/restore/static/js/restore_dialog',
], function(
gettext, url_for, $, _, Backbone, S, alertify, pgBrowser, Backgrid, Backform,
-commonUtils
+commonUtils, menuUtils, menuEnabled, restoreDialog
) {
// if module is already initialized, refer to that.
@@ -307,59 +309,6 @@ commonUtils
this.initialized = true;
- // Define list of nodes on which restore context menu option appears
- var restore_supported_nodes = [
- 'database', 'schema',
- 'table', 'function',
- 'trigger', 'index',
- 'partition',
- ];
-
- /**
- Enable/disable restore menu in tools based
- on node selected
- if selected node is present in supported_nodes,
- menu will be enabled otherwise disabled.
- Also, hide it for system view in catalogs
- */
- var menu_enabled = function(itemData, item, data) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
- var parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(restore_supported_nodes, d._type) !== -1 &&
- is_parent_catalog(itemData, item, data)) {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
- var is_parent_catalog = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
-
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to restore
- if (_.indexOf(['catalog'], d._type) > -1)
- return false;
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- };
-
// Define the nodes on which the menus to be appear
var menus = [{
name: 'restore_object',
@@ -369,20 +318,22 @@ commonUtils
priority: 13,
label: gettext('Restore...'),
icon: 'fa fa-upload',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null,
+ pgBrowser.treeMenu, menuUtils.restoreSupportedNodes),
}];
- for (var idx = 0; idx < restore_supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.restoreSupportedNodes.length; idx++) {
menus.push({
- name: 'restore_' + restore_supported_nodes[idx],
- node: restore_supported_nodes[idx],
+ name: 'restore_' + menuUtils.restoreSupportedNodes[idx],
+ node: menuUtils.restoreSupportedNodes[idx],
module: this,
applies: ['context'],
callback: 'restore_objects',
priority: 13,
label: gettext('Restore...'),
icon: 'fa fa-upload',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null,
+ pgBrowser.treeMenu, menuUtils.restoreSupportedNodes),
});
}
@@ -391,307 +342,8 @@ commonUtils
},
// Callback to draw Backup Dialog for objects
restore_objects: function(action, treeItem) {
-
- var i = treeItem || pgBrowser.tree.selected(),
- server_data = null;
-
- while (i) {
- var node_data = pgBrowser.tree.itemData(i);
- if (node_data._type == 'server') {
- server_data = node_data;
- break;
- }
-
- if (pgBrowser.tree.hasParent(i)) {
- i = $(pgBrowser.tree.parent(i));
- } else {
- alertify.alert(
- gettext('Restore Error'),
- gettext('Please select server or child node from tree.')
- );
- break;
- }
- }
-
- if (!server_data) {
- return;
- }
-
- var module = 'paths',
- preference_name = 'pg_bin_dir',
- msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
-
- if ((server_data.type && server_data.type == 'ppas') ||
- server_data.server_type == 'ppas') {
- preference_name = 'ppas_bin_dir';
- msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
- }
-
- var preference = pgBrowser.get_preference(module, preference_name);
-
- if (preference) {
- if (!preference.value) {
- alertify.alert(gettext('Configuration required'), msg);
- return;
- }
- } else {
- alertify.alert(
- gettext('Restore Error'),
- S(gettext('Failed to load preference %s of module %s')).sprintf(preference_name, module).value()
- );
- return;
- }
-
- var title = S(gettext('Restore (%s: %s)')),
- tree = pgBrowser.tree,
- item = treeItem || tree.selected(),
- data = item && item.length == 1 && tree.itemData(item),
- node = data && data._type && pgBrowser.Nodes[data._type];
-
- if (!node)
- return;
-
- title = title.sprintf(node.label, data.label).value();
-
- if (!alertify.pg_restore) {
- // Create Dialog title on the fly with node details
- alertify.dialog('pg_restore', function factory() {
- return {
- main: function(title, item, data, node) {
- this.set('title', title);
- this.setting('pg_node', node);
- this.setting('pg_item', item);
- this.setting('pg_item_data', data);
- },
- build: function() {
- alertify.pgDialogBuild.apply(this);
- },
- setup: function() {
- return {
- buttons: [{
- text: '',
- className: 'btn btn-default pull-left fa fa-lg fa-info',
- attrs: {
- name: 'object_help',
- type: 'button',
- url: 'backup.html',
- label: gettext('Restore'),
- },
- }, {
- text: '',
- key: 112,
- className: 'btn btn-default pull-left fa fa-lg fa-question',
- attrs: {
- name: 'dialog_help',
- type: 'button',
- label: gettext('Restore'),
- url: url_for('help.static', {
- 'filename': 'restore_dialog.html',
- }),
- },
- }, {
- text: gettext('Restore'),
- key: 13,
- className: 'btn btn-primary fa fa-upload pg-alertify-button',
- restore: true,
- 'data-btn-name': 'restore',
- }, {
- text: gettext('Cancel'),
- key: 27,
- className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
- restore: false,
- 'data-btn-name': 'cancel',
- }],
- // Set options for dialog
- options: {
- title: title,
- //disable both padding and overflow control.
- padding: !1,
- overflow: !1,
- model: 0,
- resizable: true,
- maximizable: true,
- pinnable: false,
- closableByDimmer: false,
- modal: false,
- },
- };
- },
- hooks: {
- // triggered when the dialog is closed
- onclose: function() {
- if (this.view) {
- this.view.remove({
- data: true,
- internal: true,
- silent: true,
- });
- }
- },
- },
- settings: {
- pg_node: null,
- pg_item: null,
- pg_item_data: null,
- },
- prepare: function() {
-
- var self = this;
- // Disable Backup button until user provides Filename
- this.__internal.buttons[2].element.disabled = true;
- var $container = $('<div class=\'restore_dialog\'></div>');
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- var newModel = new RestoreObjectModel({
- node_data: node,
- }, {
- node_info: treeInfo,
- }),
- fields = Backform.generateViewSchema(
- treeInfo, newModel, 'create', node, treeInfo.server, true
- );
-
- var view = this.view = new Backform.Dialog({
- el: $container,
- model: newModel,
- schema: fields,
- });
-
- $(this.elements.body.childNodes[0]).addClass(
- 'alertify_tools_dialog_properties obj_properties'
- );
-
- view.render();
-
- this.elements.content.appendChild($container.get(0));
-
- view.$el.attr('tabindex', -1);
- // var dialogTabNavigator = pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
- commonUtils.findAndSetFocus(container);
-
- // Listen to model & if filename is provided then enable Backup button
- this.view.model.on('change', function() {
- if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
- this.errorModel.clear();
- self.__internal.buttons[2].element.disabled = false;
- } else {
- self.__internal.buttons[2].element.disabled = true;
- this.errorModel.set('file', gettext('Please provide filename'));
- }
- });
-
- },
- // Callback functions when click on the buttons of the Alertify dialogs
- callback: function(e) {
- // Fetch current server id
- var t = pgBrowser.tree,
- i = this.settings['pg_item'] || t.selected(),
- d = this.settings['pg_item_data'] || (
- i && i.length == 1 ? t.itemData(i) : undefined
- ),
- node = this.settings['pg_node'] || (
- d && pgBrowser.Nodes[d._type]
- );
-
- if (e.button.element.name == 'dialog_help' || e.button.element.name == 'object_help') {
- e.cancel = true;
- pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
- node, i, e.button.element.getAttribute('label'));
- return;
- }
-
- if (e.button['data-btn-name'] === 'restore') {
- if (!d)
- return;
-
- var info = node.getTreeNodeHierarchy.apply(node, [i]),
- m = this.view.model;
- // Set current node info into model
- m.set('database', info.database._label);
- if (!m.get('custom')) {
- switch (d._type) {
- case 'schema':
- m.set('schemas', [d._label]);
- break;
- case 'table':
- m.set('schemas', [info.schema._label]);
- m.set('tables', [d._label]);
- break;
- case 'function':
- m.set('schemas', [info.schema._label]);
- m.set('functions', [d._label]);
- break;
- case 'index':
- m.set('schemas', [info.schema._label]);
- m.set('indexes', [d._label]);
- break;
- case 'trigger':
- m.set('schemas', [info.schema._label]);
- m.set('triggers', [d._label]);
- break;
- case 'trigger_func':
- m.set('schemas', [info.schema._label]);
- m.set('trigger_funcs', [d._label]);
- break;
- }
- } else {
- // TODO::
- // When we will implement the object selection in the
- // import dialog, we will need to select the objects from
- // the tree selection tab.
- }
-
- var self = this,
- baseUrl = url_for('restore.create_job', {
- 'sid': info.server._id,
- }),
- args = this.view.model.toJSON();
-
- $.ajax({
- url: baseUrl,
- method: 'POST',
- data: {
- 'data': JSON.stringify(args),
- },
- success: function(res) {
- if (res.success) {
- alertify.success(
- gettext('Restore job created.'), 5
- );
- pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
- } else {
- console.warn(res);
- }
- },
- error: function(xhr) {
- try {
- var err = $.parseJSON(xhr.responseText);
- alertify.alert(
- gettext('Restore failed.'),
- err.errormsg
- );
- } catch (e) {
- console.warn(e.stack || e);
- }
- },
- });
- }
- },
- };
- });
- }
-
- alertify.pg_restore(title, item, data, node).resizeTo('65%', '60%');
+ let dialog = new restoreDialog.RestoreDialog(pgBrowser, $, alertify, RestoreObjectModel);
+ dialog.draw(action, treeItem);
},
};
return pgBrowser.Restore;
diff --git a/web/pgadmin/tools/restore/static/js/restore_dialog.js b/web/pgadmin/tools/restore/static/js/restore_dialog.js
new file mode 100644
index 00000000..66fea3ba
--- /dev/null
+++ b/web/pgadmin/tools/restore/static/js/restore_dialog.js
@@ -0,0 +1,53 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../../../../static/js/gettext';
+import {sprintf} from 'sprintf-js';
+import Backform from '../../../../static/js/backform.pgadmin';
+import {Dialog} from '../../../../static/js/alertify/dialog';
+
+export class RestoreDialog extends Dialog {
+ constructor(pgBrowser, $, alertify, RestoreModel, backform = Backform) {
+ super('Restore Error',
+ '<div class=\'restore_dialog\'></div>',
+ pgBrowser, $, alertify, RestoreModel, backform);
+ }
+
+ draw(action, aciTreeItem) {
+
+ const serverInformation = this.retrieveAncestorOfTypeServer(aciTreeItem);
+
+ if (!serverInformation) {
+ return;
+ }
+
+ if (!this.hasBinariesConfiguration(serverInformation)) {
+ return;
+ }
+
+ let aciTreeItem1 = aciTreeItem || this.pgBrowser.treeMenu.selected();
+ let item = this.pgBrowser.treeMenu.findNodeByDomElement(aciTreeItem1);
+ const data = item.getData();
+ const node = this.pgBrowser.Nodes[data._type];
+
+ if (!node)
+ return;
+
+ let title = sprintf(gettext('Restore (%s: %s)'), node.label, data.label);
+
+ this.createOrGetDialog(title, 'restore');
+
+ this.alertify.pg_restore(title, aciTreeItem1, data, node).resizeTo('65%', '60%');
+ }
+
+ dialogName() {
+ return 'pg_restore';
+ }
+}
+
diff --git a/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js b/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js
new file mode 100644
index 00000000..845da7a3
--- /dev/null
+++ b/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js
@@ -0,0 +1,255 @@
+import {getTreeNodeHierarchyFromElement} from '../../../../static/js/tree/pgadmin_tree_node';
+import axios from 'axios/index';
+import _ from 'underscore';
+import gettext from '../../../../static/js/gettext';
+import url_for from '../../../../static/js/url_for';
+import {DialogWrapper} from '../../../../static/js/alertify/dialog_wrapper';
+
+export class RestoreDialogWrapper extends DialogWrapper {
+ constructor(dialogContainerSelector, dialogTitle, typeOfDialog,
+ jquery, pgBrowser, alertify, dialogModel, backform) {
+ super(dialogContainerSelector, dialogTitle, jquery,
+ pgBrowser, alertify, dialogModel, backform);
+ }
+
+ main(title, item, data, node) {
+ this.set('title', title);
+ this.setting('pg_node', node);
+ this.setting('pg_item', item);
+ this.setting('pg_item_data', data);
+ }
+
+ setup() {
+ return {
+ buttons: [{
+ text: '',
+ className: 'btn btn-default pull-left fa fa-lg fa-info',
+ attrs: {
+ name: 'object_help',
+ type: 'button',
+ url: 'backup.html',
+ label: gettext('Restore'),
+ },
+ }, {
+ text: '',
+ key: 112,
+ className: 'btn btn-default pull-left fa fa-lg fa-question',
+ attrs: {
+ name: 'dialog_help',
+ type: 'button',
+ label: gettext('Restore'),
+ url: url_for('help.static', {
+ 'filename': 'restore_dialog.html',
+ }),
+ },
+ }, {
+ text: gettext('Restore'),
+ key: 13,
+ className: 'btn btn-primary fa fa-upload pg-alertify-button',
+ restore: true,
+ 'data-btn-name': 'restore',
+ }, {
+ text: gettext('Cancel'),
+ key: 27,
+ className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
+ restore: false,
+ 'data-btn-name': 'cancel',
+ }],
+ // Set options for dialog
+ options: {
+ title: this.dialogTitle,
+ //disable both padding and overflow control.
+ padding: !1,
+ overflow: !1,
+ model: 0,
+ resizable: true,
+ maximizable: true,
+ pinnable: false,
+ closableByDimmer: false,
+ modal: false,
+ },
+ };
+ }
+
+ prepare() {
+ this.disableRestoreButton();
+
+ const $container = this.jquery(this.dialogContainerSelector);
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = this.getSelectedNodeData(selectedTreeNode);
+ if (!selectedTreeNodeData) {
+ return;
+ }
+
+ const node = this.pgBrowser.Nodes[selectedTreeNodeData._type];
+
+ const treeInfo = getTreeNodeHierarchyFromElement(this.pgBrowser, selectedTreeNode);
+ const dialog = this.createDialog(node, treeInfo, $container);
+ this.addAlertifyClassToRestoreNodeChildNodes();
+ dialog.render();
+
+ this.elements.content.appendChild($container.get(0));
+
+ this.focusOnDialog(dialog);
+ this.setListenersForFilenameChanges();
+ }
+
+ callback(event) {
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = this.getSelectedNodeData(selectedTreeNode);
+ const node = selectedTreeNodeData && this.pgBrowser.Nodes[selectedTreeNodeData._type];
+
+ if (this.wasHelpButtonPressed(event)) {
+ event.cancel = true;
+ this.pgBrowser.showHelp(
+ event.button.element.name,
+ event.button.element.getAttribute('url'),
+ node,
+ selectedTreeNode,
+ event.button.element.getAttribute('label')
+ );
+ return;
+ }
+
+ if (this.wasRestoreButtonPressed(event)) {
+
+ if (!selectedTreeNodeData)
+ return;
+
+ const serverIdentifier = this.retrieveServerIdentifier(node, selectedTreeNode);
+
+ const dialogWrapper = this;
+ let urlShortcut = 'restore.create_job';
+
+ const baseUrl = url_for(urlShortcut, {
+ 'sid': serverIdentifier,
+ });
+
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.pgBrowser,
+ selectedTreeNode
+ );
+
+ this.setExtraParameters(selectedTreeNode, treeInfo);
+
+ let service = axios.create({});
+ service.post(
+ baseUrl,
+ this.view.model.toJSON()
+ ).then(function () {
+ dialogWrapper.alertify.success(gettext('Restore job created.'), 5);
+ dialogWrapper.pgBrowser.Events.trigger('pgadmin-bgprocess:created', dialogWrapper);
+ }).catch(function (error) {
+ try {
+ const err = error.response.data;
+ dialogWrapper.alertify.alert(
+ gettext('Restore job failed.'),
+ err.errormsg
+ );
+ } catch (e) {
+ console.warn(e.stack || e);
+ }
+ });
+ }
+ }
+
+ addAlertifyClassToRestoreNodeChildNodes() {
+ this.jquery(this.elements.body.childNodes[0]).addClass(
+ 'alertify_tools_dialog_properties obj_properties'
+ );
+ }
+
+ getSelectedNode() {
+ const tree = this.pgBrowser.treeMenu;
+ const selectedNode = tree.selected();
+ if (selectedNode) {
+ return tree.findNodeByDomElement(selectedNode);
+ } else {
+ return undefined;
+ }
+ }
+
+ disableRestoreButton() {
+ this.__internal.buttons[2].element.disabled = true;
+ }
+
+ enableRestoreButton() {
+ this.__internal.buttons[2].element.disabled = false;
+ }
+
+ createDialog(node, treeInfo, $container) {
+ const newModel = new this.dialogModel({
+ node_data: node,
+ }, {
+ node_info: treeInfo,
+ });
+ const fields = this.backform.generateViewSchema(
+ treeInfo, newModel, 'create', node, treeInfo.server, true
+ );
+
+ return this.view = new this.backform.Dialog({
+ el: $container,
+ model: newModel,
+ schema: fields,
+ });
+ }
+
+ retrieveServerIdentifier(node, selectedTreeNode) {
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.pgBrowser,
+ selectedTreeNode
+ );
+ return treeInfo.server._id;
+ }
+
+ setListenersForFilenameChanges() {
+ const self = this;
+
+ this.view.model.on('change', function () {
+ if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
+ this.errorModel.clear();
+ self.enableRestoreButton();
+ } else {
+ self.disableRestoreButton();
+ this.errorModel.set('file', gettext('Please provide a filename'));
+ }
+ });
+ }
+
+ setExtraParameters(selectedTreeNode, treeInfo) {
+ this.view.model.set('database', treeInfo.database._label);
+ if (!this.view.model.get('custom')) {
+ const nodeData = selectedTreeNode.getData();
+
+ switch (nodeData._type) {
+ case 'schema':
+ this.view.model.set('schemas', [nodeData._label]);
+ break;
+ case 'table':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('tables', [nodeData._label]);
+ break;
+ case 'function':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('functions', [nodeData._label]);
+ break;
+ case 'index':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('indexes', [nodeData._label]);
+ break;
+ case 'trigger':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('triggers', [nodeData._label]);
+ break;
+ case 'trigger_func':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('trigger_funcs', [nodeData._label]);
+ break;
+ }
+ }
+ }
+
+ wasRestoreButtonPressed(event) {
+ return event.button['data-btn-name'] === 'restore';
+ }
+}
diff --git a/web/regression/javascript/backup/backup_dialog_spec.js b/web/regression/javascript/backup/backup_dialog_spec.js
new file mode 100644
index 00000000..85e5e60b
--- /dev/null
+++ b/web/regression/javascript/backup/backup_dialog_spec.js
@@ -0,0 +1,190 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {BackupDialog} from '../../../pgadmin/tools/backup/static/js/backup_dialog';
+import {TreeFake} from '../tree/tree_fake';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+const context = describe;
+
+describe('ObjectBackupDialog', () => {
+ let backupDialog;
+ let pgBrowser;
+ let jquerySpy;
+ let alertifySpy;
+ let backupModelSpy;
+
+
+ let rootNode;
+ let serverTreeNode;
+ let databaseTreeNode;
+ let ppasServerTreeNode;
+ let noDataNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: {
+ hasId: true,
+ label: 'server',
+ getTreeNodeHierarchy: jasmine.createSpy('server.getTreeNodeHierarchy'),
+ },
+ database: {
+ hasId: true,
+ label: 'database',
+ getTreeNodeHierarchy: jasmine.createSpy('db.getTreeNodeHierarchy'),
+ },
+ schema: {
+ hasId: true,
+ label: 'schema',
+ getTreeNodeHierarchy: jasmine.createSpy('db.getTreeNodeHierarchy'),
+ },
+ },
+ };
+ pgBrowser.Nodes.server.hasId = true;
+ pgBrowser.Nodes.database.hasId = true;
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ backupModelSpy = jasmine.createSpy('backupModelSpy');
+
+ rootNode = pgBrowser.treeMenu.addNewNode('level1', {}, []);
+
+ serverTreeNode = new TreeNode('level1.1', {
+ _type: 'server',
+ _id: 10,
+ });
+ pgBrowser.treeMenu.addChild(rootNode, serverTreeNode);
+
+ databaseTreeNode = new TreeNode(
+ 'level1.1.1', {
+ _type: 'database',
+ _id: 11,
+ label: 'some_database',
+ _label: 'some_database_label',
+ }, [{id: 'level1.1.1'}]);
+ pgBrowser.treeMenu.addChild(serverTreeNode, databaseTreeNode);
+
+ ppasServerTreeNode = new TreeNode('level1.2', {
+ _type: 'server',
+ server_type: 'ppas',
+ });
+ pgBrowser.treeMenu.addChild(rootNode, ppasServerTreeNode);
+
+ const someNodeUnderneathPPASServer = new TreeNode('level3', {});
+ pgBrowser.treeMenu.addChild(ppasServerTreeNode,
+ someNodeUnderneathPPASServer);
+
+ noDataNode = new TreeNode(
+ 'level3.1', undefined);
+ pgBrowser.treeMenu.addChild(someNodeUnderneathPPASServer, noDataNode);
+ });
+
+ describe('#draw', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ alertifySpy['backup_objects'] = jasmine.createSpy('backup_objects');
+ backupDialog = new BackupDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ backupModelSpy
+ );
+
+ pgBrowser.get_preference = jasmine.createSpy('get_preferences');
+ });
+
+ context('there are no ancestors of the type server', () => {
+ it('does not create a dialog', () => {
+ pgBrowser.treeMenu.selectNode([rootNode]);
+ backupDialog.draw(null, null, null);
+ expect(alertifySpy['backup_objects']).not.toHaveBeenCalled();
+ });
+
+ it('display an alert with a Backup Error', () => {
+ backupDialog.draw(null, [rootNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Please select server or child node from the browser tree.'
+ );
+ });
+ });
+
+ context('there is an ancestor of the type server', () => {
+ context('no preference can be found', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue(undefined);
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [{id: 'level1.1.1'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference pg_bin_dir of module paths'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference ppas_bin_dir of module paths'
+ );
+ });
+ });
+ });
+
+ context('preference can be found', () => {
+ context('binary folder is not configured', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue({});
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the PostgreSQL Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the EDB Advanced Server Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+ });
+
+ context('binary folder is configured', () => {
+ let backupDialogResizeToSpy;
+ beforeEach(() => {
+ backupDialogResizeToSpy = jasmine.createSpyObj('backupDialogResizeToSpy', ['resizeTo']);
+ alertifySpy['backup_objects'].and
+ .returnValue(backupDialogResizeToSpy);
+ pgBrowser.get_preference.and.returnValue({value: '/some/path'});
+ });
+
+ it('displays the dialog', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy['backup_objects']).toHaveBeenCalledWith(true);
+ expect(backupDialogResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
+ });
+ });
+ });
+ });
+ });
+
+});
diff --git a/web/regression/javascript/backup/backup_dialog_wrapper_spec.js b/web/regression/javascript/backup/backup_dialog_wrapper_spec.js
new file mode 100644
index 00000000..58705318
--- /dev/null
+++ b/web/regression/javascript/backup/backup_dialog_wrapper_spec.js
@@ -0,0 +1,675 @@
+import {TreeFake} from '../tree/tree_fake';
+import {BackupDialogWrapper} from '../../../pgadmin/tools/backup/static/js/backup_dialog_wrapper';
+import axios from 'axios/index';
+import MockAdapter from 'axios-mock-adapter';
+import {FakeModel} from '../fake_model';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+let context = describe;
+
+describe('BackupDialogWrapper', () => {
+ let jquerySpy;
+ let pgBrowser;
+ let alertifySpy;
+ let dialogModelKlassSpy;
+ let backform;
+ let generatedBackupModel;
+ let backupDialogWrapper;
+ let noDataNode;
+ let serverTreeNode;
+ let databaseTreeNode;
+ let viewSchema;
+ let backupJQueryContainerSpy;
+ let backupNodeChildNodeSpy;
+ let backupNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: {
+ hasId: true,
+ getTreeNodeHierarchy: jasmine.createSpy('getTreeNodeHierarchy'),
+ },
+ database: {
+ hasId: true,
+ },
+ },
+ keyboardNavigation: jasmine.createSpyObj('keyboardNavigation', ['getDialogTabNavigator']),
+ };
+ noDataNode = pgBrowser.treeMenu.addNewNode('level1.1', undefined, [{id: 'level1'}]);
+ serverTreeNode = pgBrowser.treeMenu.addNewNode('level2.1', {
+ _type: 'server',
+ _id: 10,
+ label: 'some-tree-label',
+ }, [{id: 'level2.1'}]);
+ databaseTreeNode = new TreeNode('database-tree-node', {
+ _type: 'database',
+ _label: 'some-database-label',
+ }, [{id: 'database-tree-node'}]);
+ pgBrowser.treeMenu.addChild(serverTreeNode, databaseTreeNode);
+
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ backupNode = {
+ __internal: {
+ buttons: [{}, {}, {
+ element: {
+ disabled: false,
+ },
+ }],
+ },
+ elements: {
+ body: {
+ childNodes: [
+ {},
+ ],
+ },
+ content: jasmine.createSpyObj('content', ['appendChild', 'attr']),
+ },
+ };
+
+ backupJQueryContainerSpy = jasmine.createSpyObj('backupJQueryContainer', ['get', 'attr']);
+ backupJQueryContainerSpy.get.and.returnValue(backupJQueryContainerSpy);
+
+ generatedBackupModel = {};
+ dialogModelKlassSpy = jasmine.createSpy('dialogModelKlass');
+ dialogModelKlassSpy.and.returnValue(generatedBackupModel);
+
+ viewSchema = {};
+ backform = jasmine.createSpyObj('backform', ['generateViewSchema', 'Dialog']);
+ backform.generateViewSchema.and.returnValue(viewSchema);
+
+ backupNodeChildNodeSpy = jasmine.createSpyObj('something', ['addClass']);
+ jquerySpy.and.callFake((selector) => {
+ if (selector === '<div class=\'backup_dialog\'></div>') {
+ return backupJQueryContainerSpy;
+ } else if (selector === backupNode.elements.body.childNodes[0]) {
+ return backupNodeChildNodeSpy;
+ }
+ });
+
+ });
+
+ describe('#prepare', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+ backupDialogWrapper = Object.assign(backupDialogWrapper, backupNode);
+ });
+
+ context('no tree element is selected', () => {
+ it('does not create a backform dialog', () => {
+ backupDialogWrapper.prepare();
+ expect(backform.Dialog).not.toHaveBeenCalled();
+ });
+
+ it('disables the button "submit button" until a filename is selected', () => {
+ backupDialogWrapper.prepare();
+ expect(backupDialogWrapper.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('selected tree node has no data', () => {
+ beforeEach(() => {
+ pgBrowser.treeMenu.selectNode(noDataNode.domNode);
+ });
+
+ it('does not create a backform dialog', () => {
+ backupDialogWrapper.prepare();
+ expect(backform.Dialog).not.toHaveBeenCalled();
+ });
+
+ it('disables the button "submit button" until a filename is selected', () => {
+ backupDialogWrapper.prepare();
+ expect(backupDialogWrapper.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('tree element is selected', () => {
+ let treeHierarchyInformation;
+ let dialogSpy;
+
+ beforeEach(() => {
+ treeHierarchyInformation = {
+ server: {
+ _type: 'server',
+ _id: 10,
+ priority: 0,
+ label: 'some-tree-label',
+ },
+ };
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ pgBrowser.Nodes['server'].getTreeNodeHierarchy.and
+ .returnValue(treeHierarchyInformation);
+ dialogSpy = jasmine.createSpyObj('newView', ['render']);
+ dialogSpy.$el = jasmine.createSpyObj('$el', ['find', 'attr']);
+ dialogSpy.model = jasmine.createSpyObj('model', ['on']);
+ dialogSpy.$el.find.and.returnValue([]);
+
+ backform.Dialog.and.returnValue(dialogSpy);
+ });
+
+ it('creates a backform dialog and displays it', () => {
+ backupDialogWrapper.prepare();
+ expect(backform.Dialog).toHaveBeenCalledWith({
+ el: backupJQueryContainerSpy,
+ model: generatedBackupModel,
+ schema: viewSchema,
+ });
+
+ expect(dialogSpy.render).toHaveBeenCalled();
+ });
+
+
+ it('add alertify classes to restore node childnode', () => {
+ backupDialogWrapper.prepare();
+ expect(backupNodeChildNodeSpy.addClass)
+ .toHaveBeenCalledWith('alertify_tools_dialog_properties obj_properties');
+ });
+
+ it('disables the button submit button until a filename is selected', () => {
+ backupDialogWrapper.prepare();
+ expect(backupNode.__internal.buttons[2].element.disabled).toBe(true);
+ });
+
+ it('generates a new backup model', () => {
+ backupDialogWrapper.prepare();
+ expect(dialogModelKlassSpy).toHaveBeenCalledWith(
+ {type: 'backup'},
+ {node_info: treeHierarchyInformation}
+ );
+ });
+
+ it('add the new dialog to the backup node HTML', () => {
+ backupDialogWrapper.prepare();
+ expect(backupNode.elements.content.appendChild).toHaveBeenCalledWith(backupJQueryContainerSpy);
+ });
+ });
+ });
+
+ describe('onButtonClicked', () => {
+ let networkMock;
+ beforeEach(() => {
+ networkMock = new MockAdapter(axios);
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ backupDialogWrapper = Object.assign(backupDialogWrapper, backupNode);
+ });
+
+ afterEach(() => {
+ networkMock.restore();
+ });
+
+ context('dialog help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.*/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ pgBrowser.showHelp = jasmine.createSpy('showHelp');
+
+ const event = {
+ button: {
+ element: {
+ name: 'dialog_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ },
+ },
+ },
+ };
+ backupDialogWrapper.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'dialog_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ );
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('object help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.*/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ pgBrowser.showHelp = jasmine.createSpy('showHelp');
+
+ const event = {
+ button: {
+ element: {
+ name: 'object_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ },
+ },
+ },
+ };
+ backupDialogWrapper.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'object_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ );
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('backup button was pressed', () => {
+ context('no tree node is selected', () => {
+ it('does not start the backup', () => {
+ let networkCalled = false;
+ networkMock.onAny(/.*/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ let event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+
+ backupDialogWrapper.callback(event);
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('tree node has no data', () => {
+ it('does not start the backup', () => {
+ pgBrowser.treeMenu.selectNode(noDataNode.domNode);
+
+ let networkCalled = false;
+ networkMock.onAny(/.*/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ let event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+
+ backupDialogWrapper.callback(event);
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('tree node has data', () => {
+ context('when dialog type is global', () => {
+ let event;
+ beforeEach(() => {
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+
+ backupDialogWrapper.view = {
+ model: new FakeModel(),
+ };
+
+ event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ });
+
+ context('when the backup job is created successfully', () => {
+ let dataSentToServer;
+ beforeEach(() => {
+ pgBrowser.Events = jasmine.createSpyObj('Events', ['trigger']);
+ alertifySpy.success = jasmine.createSpy('success');
+
+ networkMock.onPost('/backup/job/10').reply((request) => {
+ dataSentToServer = request.data;
+ return [200, {}];
+ });
+ });
+
+ it('creates a success alert box', (done) => {
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.success).toHaveBeenCalledWith(
+ 'Backup job created.',
+ 5
+ );
+ done();
+ }, 0);
+ });
+
+ it('trigger an event to background process', (done) => {
+ backupDialogWrapper.callback(event);
+
+ setTimeout(() => {
+ expect(pgBrowser.Events.trigger).toHaveBeenCalledWith(
+ 'pgadmin-bgprocess:created',
+ backupDialogWrapper
+ );
+ done();
+ }, 0);
+ });
+
+ it('send the correct paramenters to the backend', (done) => {
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(JSON.parse(dataSentToServer)).toEqual(
+ {}
+ );
+ done();
+ }, 0);
+ });
+ });
+
+ context('when creating backup job fails', () => {
+ it('creates an alert box', (done) => {
+ alertifySpy.alert = jasmine.createSpy('alert');
+ networkMock.onPost('/backup/job/10').reply(() => {
+ return [400, {
+ errormsg: 'some-error-message',
+ }];
+ });
+
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup job failed.',
+ 'some-error-message'
+ );
+ done();
+ }, 0);
+
+ });
+ });
+ });
+
+ context('when dialog type is object', () => {
+ let event;
+ beforeEach(() => {
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup_objects',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ pgBrowser.treeMenu.selectNode(databaseTreeNode.domNode);
+
+ backupDialogWrapper.view = {
+ model: new FakeModel(),
+ };
+
+ event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ });
+
+ context('when the backup job is created successfully', () => {
+ let dataSentToServer;
+ beforeEach(() => {
+ pgBrowser.Events = jasmine.createSpyObj('Events', ['trigger']);
+ alertifySpy.success = jasmine.createSpy('success');
+
+ networkMock.onPost('/backup/job/10/object').reply((request) => {
+ dataSentToServer = request.data;
+ return [200, {}];
+ });
+ });
+
+ it('creates a success alert box', (done) => {
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.success).toHaveBeenCalledWith(
+ 'Backup job created.',
+ 5
+ );
+ done();
+ }, 0);
+ });
+
+ it('trigger an event to background process', (done) => {
+ backupDialogWrapper.callback(event);
+
+ setTimeout(() => {
+ expect(pgBrowser.Events.trigger).toHaveBeenCalledWith(
+ 'pgadmin-bgprocess:created',
+ backupDialogWrapper
+ );
+ done();
+ }, 0);
+ });
+
+ it('send the correct parameters to the backend', (done) => {
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(JSON.parse(dataSentToServer)).toEqual(
+ {database: 'some-database-label'}
+ );
+ done();
+ }, 0);
+ });
+ });
+
+ context('when creating backup job fails', () => {
+ it('creates an alert box', (done) => {
+ alertifySpy.alert = jasmine.createSpy('alert');
+ networkMock.onPost('/backup/job/10/object').reply(() => {
+ return [400, {
+ errormsg: 'some-error-message',
+ }];
+ });
+
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup job failed.',
+ 'some-error-message'
+ );
+ done();
+ }, 0);
+ });
+ });
+ });
+ });
+ });
+ });
+
+ describe('#setExtraParameters', () => {
+ let selectedTreeNode;
+ let treeInfo;
+ let model;
+
+ context('when dialog type is global', () => {
+ beforeEach(() => {
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ treeInfo = {};
+ model = new FakeModel();
+ backupDialogWrapper.view = {
+ model: model,
+ };
+ });
+
+
+ it('sets nothing on the view model', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.toJSON()).toEqual({});
+ });
+ });
+
+ context('when dialog type is object', () => {
+ beforeEach(() => {
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup_objects',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ treeInfo = {
+ database: {
+ _label: 'some-database-label',
+ },
+ schema: {
+ _label: 'some-treeinfo-label',
+ },
+ };
+
+ model = new FakeModel();
+ selectedTreeNode = new TreeNode('some-selected-node',
+ {_type: 'some-type', _label: 'some-selected-label'},
+ []);
+ backupDialogWrapper.view = {
+ model: model,
+ };
+ });
+
+ it('sets the database label on the model', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ 'database': 'some-database-label',
+ });
+ });
+
+ context('when the selected is a schema type', () => {
+ beforeEach(() => {
+ selectedTreeNode = new TreeNode('some-selected-node',
+ {_type: 'schema', _label: 'some-schema-label'},
+ []);
+ });
+
+ it('sets the schema label on the model', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ 'database': 'some-database-label',
+ 'schemas': ['some-schema-label'],
+ });
+ });
+ });
+
+ context('when the selected is a table type', () => {
+ beforeEach(() => {
+ selectedTreeNode = new TreeNode('some-selected-node',
+ {_type: 'table', _label: 'some-table-label'},
+ []);
+ });
+
+ it('sets the schema label on the model', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ 'database': 'some-database-label',
+ 'tables': [['some-treeinfo-label', 'some-table-label']],
+ });
+ });
+ });
+
+ context('when the model has no ratio value', () => {
+ beforeEach(() => {
+ model.set('ratio', '');
+ });
+
+ it('sets clears the ratio value', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.get('ratio')).toBeUndefined();
+ });
+ });
+
+ context('when the model has a valid ratio value', () => {
+ beforeEach(() => {
+ model.set('ratio', '0.25');
+ });
+
+ it('sets clears the ratio value', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.get('ratio')).toEqual('0.25');
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/backup/global_server_backup_dialog_spec.js b/web/regression/javascript/backup/global_server_backup_dialog_spec.js
new file mode 100644
index 00000000..86df672e
--- /dev/null
+++ b/web/regression/javascript/backup/global_server_backup_dialog_spec.js
@@ -0,0 +1,168 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {BackupDialog} from '../../../pgadmin/tools/backup/static/js/backup_dialog';
+import {TreeFake} from '../tree/tree_fake';
+
+const context = describe;
+
+describe('GlobalServerBackupDialog', () => {
+ let backupDialog;
+ let pgBrowser;
+ let jquerySpy;
+ let alertifySpy;
+ let backupModelSpy;
+
+
+ let rootNode;
+ let serverTreeNode;
+ let ppasServerTreeNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: jasmine.createSpyObj('Node[server]', ['getTreeNodeHierarchy']),
+ },
+ };
+ pgBrowser.Nodes.server.hasId = true;
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ backupModelSpy = jasmine.createSpy('backupModelSpy');
+
+ rootNode = pgBrowser.treeMenu.addNewNode('level1', {}, undefined, []);
+ serverTreeNode = pgBrowser.treeMenu.addNewNode('level1.1', {
+ _type: 'server',
+ _id: 10,
+ }, undefined, ['level1']);
+ ppasServerTreeNode = pgBrowser.treeMenu.addNewNode('level1.2', {
+ _type: 'server',
+ server_type: 'ppas',
+ }, undefined, ['level1']);
+ pgBrowser.treeMenu.addNewNode('level3', {}, undefined, ['level1', 'level1.2']);
+ pgBrowser.treeMenu.addNewNode('level3.1', undefined, undefined, ['level1', 'level1.2', 'level3']);
+ });
+
+ describe('#draw', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ alertifySpy['BackupDialog_globals'] = jasmine.createSpy('BackupDialog_globals');
+ alertifySpy['BackupDialog_server'] = jasmine.createSpy('BackupDialog_server');
+ backupDialog = new BackupDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ backupModelSpy
+ );
+
+ pgBrowser.get_preference = jasmine.createSpy('get_preferences');
+ });
+
+ context('there are no ancestors of the type server', () => {
+ it('does not create a dialog', () => {
+ pgBrowser.treeMenu.selectNode([{id: 'level1'}]);
+ backupDialog.draw(null, null, null);
+ expect(alertifySpy['BackupDialog_globals']).not.toHaveBeenCalled();
+ expect(alertifySpy['BackupDialog_server']).not.toHaveBeenCalled();
+ });
+
+ it('display an alert with a Backup Error', () => {
+ backupDialog.draw(null, [rootNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Please select server or child node from the browser tree.'
+ );
+ });
+ });
+
+ context('there is an ancestor of the type server', () => {
+ context('no preference can be found', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue(undefined);
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference pg_bin_dir of module paths'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference ppas_bin_dir of module paths'
+ );
+ });
+ });
+ });
+
+ context('preference can be found', () => {
+ context('binary folder is not configured', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue({});
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the PostgreSQL Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the EDB Advanced Server Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+ });
+
+ context('binary folder is configured', () => {
+ let globalResizeToSpy;
+ let serverResizeToSpy;
+ beforeEach(() => {
+ globalResizeToSpy = jasmine.createSpyObj('globals', ['resizeTo']);
+ alertifySpy['BackupDialog_globals'].and
+ .returnValue(globalResizeToSpy);
+ serverResizeToSpy = jasmine.createSpyObj('server', ['resizeTo']);
+ alertifySpy['BackupDialog_server'].and
+ .returnValue(serverResizeToSpy);
+ pgBrowser.get_preference.and.returnValue({value: '/some/path'});
+ });
+
+ context('dialog for global backup', () => {
+ it('displays the dialog', () => {
+ backupDialog.draw(null, [serverTreeNode], {globals: true});
+ expect(alertifySpy['BackupDialog_globals']).toHaveBeenCalledWith(true);
+ expect(globalResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
+ });
+ });
+
+ context('dialog for server backup', () => {
+ it('displays the dialog', () => {
+ backupDialog.draw(null, [serverTreeNode], {server: true});
+ expect(alertifySpy['BackupDialog_server']).toHaveBeenCalledWith(true);
+ expect(serverResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/backup/menu_utils_spec.js b/web/regression/javascript/backup/menu_utils_spec.js
new file mode 100644
index 00000000..9435d699
--- /dev/null
+++ b/web/regression/javascript/backup/menu_utils_spec.js
@@ -0,0 +1,55 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+
+import {menuEnabledServer} from '../../../pgadmin/tools/backup/static/js/menu_utils';
+
+const context = describe;
+
+describe('backup.menuUtils', () => {
+ describe('#menuEnabledServer', () => {
+ context('provided node data is undefined', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer(undefined)).toBe(false);
+ });
+ });
+
+ context('provided node data is null', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer(null)).toBe(false);
+ });
+ });
+
+ context('current node type is not of the type server', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer({_type: 'schema'})).toBe(false);
+ });
+ });
+
+ context('current node type is of the type server', () => {
+ context('is connected', () => {
+ it('returns true', () => {
+ expect(menuEnabledServer({
+ _type: 'server',
+ connected: true,
+ })).toBe(true);
+ });
+ });
+ context('is not connected', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer({
+ _type: 'server',
+ connected: false,
+ })).toBe(false);
+ });
+ });
+ });
+ });
+});
+
diff --git a/web/regression/javascript/common_keyboard_shortcuts_spec.js b/web/regression/javascript/common_keyboard_shortcuts_spec.js
index 9ea31efd..e27929bf 100644
--- a/web/regression/javascript/common_keyboard_shortcuts_spec.js
+++ b/web/regression/javascript/common_keyboard_shortcuts_spec.js
@@ -11,10 +11,6 @@ import keyboardShortcuts from 'sources/keyboard_shortcuts';
describe('the keyboard shortcuts', () => {
const F1_KEY = 112;
- // const EDIT_KEY = 71; // Key: G -> Grid values
- // const LEFT_ARROW_KEY = 37;
- // const RIGHT_ARROW_KEY = 39;
- // const MOVE_NEXT = 'right';
let debuggerElementSpy, event, debuggerUserShortcutSpy;
debuggerUserShortcutSpy = jasmine.createSpyObj(
diff --git a/web/regression/javascript/datagrid/get_panel_title_spec.js b/web/regression/javascript/datagrid/get_panel_title_spec.js
new file mode 100644
index 00000000..8a344a84
--- /dev/null
+++ b/web/regression/javascript/datagrid/get_panel_title_spec.js
@@ -0,0 +1,82 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {getPanelTitle} from '../../../pgadmin/tools/datagrid/static/js/get_panel_title';
+import {TreeFake} from '../tree/tree_fake';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+const context = describe;
+
+describe('#getPanelTitle', () => {
+ let pgBrowser;
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ pgBrowser = {
+ treeMenu: tree,
+ Nodes: {
+ server: {
+ hasId: true,
+ _type: 'server',
+ },
+ database: {
+ hasId: true,
+ _type: 'database',
+ },
+ },
+ };
+ });
+
+ context('selected node does not belong to a server', () => {
+ it('returns undefined', () => {
+ const root = tree.addNewNode('level1', {_type: 'server_groups'});
+ tree.addChild(root, new TreeNode('level1.1', {_type: 'other'}));
+ tree.selectNode([{id: 'level1'}]);
+ expect(getPanelTitle(pgBrowser)).toBeUndefined();
+ });
+ });
+
+ context('selected node belong to a server', () => {
+ context('selected node does not belong to a database', () => {
+ it('returns the server label and the username', () => {
+ tree.addNewNode('level1', {
+ _type: 'server',
+ db: 'other db label',
+ user: {name: 'some user name'},
+ label: 'server label',
+ }, []);
+
+ tree.selectNode([{id: 'level1'}]);
+ expect(getPanelTitle(pgBrowser))
+ .toBe('other db label on some user name@server label');
+ });
+ });
+
+ context('selected node belongs to a database', () => {
+ it('returns the database label and the username', () => {
+ const root = tree.addNewNode('level1', {
+ _type: 'server',
+ db: 'other db label',
+ user: {name: 'some user name'},
+ label: 'server label',
+ });
+ const level1 = new TreeNode('level1.1', {
+ _type: 'database',
+ label: 'db label',
+ });
+ tree.addChild(root, level1);
+ tree.addChild(level1,
+ new TreeNode('level1.1.1', {_type: 'table'}));
+ tree.selectNode([{id: 'level1.1.1'}]);
+ expect(getPanelTitle(pgBrowser))
+ .toBe('db label on some user name@server label');
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/datagrid/show_data_spec.js b/web/regression/javascript/datagrid/show_data_spec.js
new file mode 100644
index 00000000..80d25eb3
--- /dev/null
+++ b/web/regression/javascript/datagrid/show_data_spec.js
@@ -0,0 +1,171 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {showDataGrid} from '../../../pgadmin/tools/datagrid/static/js/show_data';
+import {TreeFake} from '../tree/tree_fake';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+const context = describe;
+
+describe('#show_data', () => {
+ let datagrid;
+ let pgBrowser;
+ let alertify;
+ beforeEach(() => {
+ alertify = jasmine.createSpyObj('alertify', ['alert']);
+ datagrid = {
+ create_transaction: jasmine.createSpy('create_transaction'),
+ };
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server_group: {
+ _type: 'server_group',
+ hasId: true,
+ },
+ server: {
+ _type: 'server',
+ hasId: true,
+ },
+ database: {
+ _type: 'database',
+ hasId: true,
+ },
+ schema: {
+ _type: 'schema',
+ hasId: true,
+ },
+ view: {
+ _type: 'view',
+ hasId: true,
+ },
+ catalog: {
+ _type: 'catalog',
+ hasId: true,
+ },
+ },
+ };
+ const parent = pgBrowser.treeMenu.addNewNode('parent', {_type: 'parent'}, []);
+ const serverGroup1 = new TreeNode('server_group1', {
+ _type: 'server_group',
+ _id: 1,
+ });
+ pgBrowser.treeMenu.addChild(parent, serverGroup1);
+
+ const server1 = new TreeNode('server1', {
+ _type: 'server',
+ label: 'server1',
+ server_type: 'pg',
+ _id: 2,
+ }, ['parent', 'server_group1']);
+ pgBrowser.treeMenu.addChild(serverGroup1, server1);
+
+ const database1 = new TreeNode('database1', {
+ _type: 'database',
+ label: 'database1',
+ _id: 3,
+ }, ['parent', 'server_group1', 'server1']);
+ pgBrowser.treeMenu.addChild(server1, database1);
+
+ const schema1 = new TreeNode('schema1', {
+ _type: 'schema',
+ label: 'schema1',
+ _id: 4,
+ });
+ pgBrowser.treeMenu.addChild(database1, schema1);
+
+ const view1 = new TreeNode('view1', {
+ _type: 'view',
+ label: 'view1',
+ _id: 5,
+ }, ['parent', 'server_group1', 'server1', 'database1']);
+ pgBrowser.treeMenu.addChild(database1, view1);
+
+ const catalog1 = new TreeNode('catalog1', {
+ _type: 'catalog',
+ label: 'catalog1',
+ _id: 6,
+ }, ['parent', 'server_group1', 'server1', 'database1']);
+ pgBrowser.treeMenu.addChild(database1, catalog1);
+ });
+
+ context('cannot find the tree node', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: '10'}]);
+ expect(datagrid.create_transaction).not.toHaveBeenCalled();
+ });
+
+ it('display alert', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: '10'}]);
+ expect(alertify.alert).toHaveBeenCalledWith(
+ 'Data Grid Error',
+ 'No object selected.'
+ );
+ });
+ });
+
+ context('current node is not underneath a server', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: 'parent'}]);
+ expect(datagrid.create_transaction).not.toHaveBeenCalled();
+ });
+ });
+
+ context('current node is not underneath a schema or view or catalog', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: 'database1'}]);
+ expect(datagrid.create_transaction).not.toHaveBeenCalled();
+ });
+ });
+
+ context('current node is underneath a schema', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {mnuid: 11}, [{id: 'schema1'}]);
+ expect(datagrid.create_transaction).toHaveBeenCalledWith(
+ '/initialize/datagrid/11/schema/1/2/3/4',
+ null,
+ 'false',
+ 'pg',
+ '',
+ 'server1 - database1 - schema1.schema1',
+ ''
+ );
+ });
+ });
+
+ context('current node is underneath a view', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {mnuid: 11}, [{id: 'view1'}]);
+ expect(datagrid.create_transaction).toHaveBeenCalledWith(
+ '/initialize/datagrid/11/view/1/2/3/5',
+ null,
+ 'false',
+ 'pg',
+ '',
+ 'server1 - database1 - view1.view1',
+ ''
+ );
+ });
+ });
+
+ context('current node is underneath a catalog', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {mnuid: 11}, [{id: 'catalog1'}]);
+ expect(datagrid.create_transaction).toHaveBeenCalledWith(
+ '/initialize/datagrid/11/catalog/1/2/3/6',
+ null,
+ 'false',
+ 'pg',
+ '',
+ 'server1 - database1 - catalog1.catalog1',
+ ''
+ );
+ });
+ });
+});
diff --git a/web/regression/javascript/datagrid/show_query_tool_spec.js b/web/regression/javascript/datagrid/show_query_tool_spec.js
new file mode 100644
index 00000000..66bd37ce
--- /dev/null
+++ b/web/regression/javascript/datagrid/show_query_tool_spec.js
@@ -0,0 +1,125 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {TreeFake} from '../tree/tree_fake';
+import {showQueryTool} from '../../../pgadmin/tools/datagrid/static/js/show_query_tool';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+const context = describe;
+
+describe('#showQueryTool', () => {
+ let queryTool;
+ let pgBrowser;
+ let alertify;
+ beforeEach(() => {
+ alertify = jasmine.createSpyObj('alertify', ['alert']);
+ queryTool = {
+ create_transaction: jasmine.createSpy('create_transaction'),
+ };
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server_group: {
+ _type: 'server_group',
+ hasId: true,
+ },
+ server: {
+ _type: 'server',
+ hasId: true,
+ },
+ database: {
+ _type: 'database',
+ hasId: true,
+ },
+ },
+ };
+ const parent = pgBrowser.treeMenu.addNewNode('parent', {_type: 'parent'});
+ const serverGroup1 = new TreeNode('server_group1', {
+ _type: 'server_group',
+ _id: 1,
+ }, ['parent']);
+ pgBrowser.treeMenu.addChild(parent, serverGroup1);
+
+ const server1 = new TreeNode('server1', {
+ _type: 'server',
+ label: 'server1',
+ server_type: 'pg',
+ _id: 2,
+ });
+ pgBrowser.treeMenu.addChild(serverGroup1, server1);
+
+ const database1 = new TreeNode('database1', {
+ _type: 'database',
+ label: 'database1',
+ _id: 3,
+ });
+ pgBrowser.treeMenu.addChild(server1, database1);
+ });
+
+ context('cannot find the tree node', () => {
+ beforeEach(() => {
+ showQueryTool(queryTool, pgBrowser, alertify, '', [{id: '10'}], 'title');
+ });
+ it('does not create a transaction', () => {
+ expect(queryTool.create_transaction).not.toHaveBeenCalled();
+ });
+
+ it('display alert', () => {
+ expect(alertify.alert).toHaveBeenCalledWith(
+ 'Query Tool Error',
+ 'No object selected.'
+ );
+ });
+ });
+
+ context('current node is not underneath a server', () => {
+ it('does not create a transaction', () => {
+ showQueryTool(queryTool, pgBrowser, alertify, '', [{id: 'parent'}], 'title');
+ expect(queryTool.create_transaction).not.toHaveBeenCalled();
+ });
+
+ it('no alert is displayed', () => {
+ expect(alertify.alert).not.toHaveBeenCalled();
+ });
+ });
+
+ context('current node is underneath a server', () => {
+ context('current node is not underneath a database', () => {
+ it('creates a transaction', () => {
+ showQueryTool(queryTool, pgBrowser, alertify, 'http://someurl', [{id: 'server1'}], 'title');
+ expect(queryTool.create_transaction).toHaveBeenCalledWith(
+ '/initialize/query_tool/1/2',
+ null,
+ 'true',
+ 'pg',
+ 'http://someurl',
+ 'title',
+ '',
+ false
+ );
+ });
+ });
+
+ context('current node is underneath a database', () => {
+ it('creates a transaction', () => {
+ showQueryTool(queryTool, pgBrowser, alertify, 'http://someurl', [{id: 'database1'}], 'title');
+ expect(queryTool.create_transaction).toHaveBeenCalledWith(
+ '/initialize/query_tool/1/2/3',
+ null,
+ 'true',
+ 'pg',
+ 'http://someurl',
+ 'title',
+ '',
+ false
+ );
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/fake_endpoints.js b/web/regression/javascript/fake_endpoints.js
index 54b86a94..c060ba78 100644
--- a/web/regression/javascript/fake_endpoints.js
+++ b/web/regression/javascript/fake_endpoints.js
@@ -12,5 +12,11 @@ define(function () {
'static': '/base/pgadmin/static/<path:filename>',
'sqleditor.poll': '/sqleditor/query_tool/poll/<path:trans_id>',
'sqleditor.query_tool_start': '/sqleditor/query_tool/start/<path:trans_id>',
+ 'backup.create_server_job': '/backup/job/<int:sid>',
+ 'backup.create_object_job': '/backup/job/<int:sid>/object',
+ 'datagrid.initialize_datagrid': '/initialize/datagrid/<int:cmd_type>/<obj_type>/<int:sgid>/<int:sid>/<int:did>/<int:obj_id>',
+ 'datagrid.initialize_query_tool': '/initialize/query_tool/<int:sgid>/<int:sid>',
+ 'datagrid.initialize_query_tool_with_did': '/initialize/query_tool/<int:sgid>/<int:sid>/<int:did>',
+ 'restore.create_job': '/restore/job/<int:sid>',
};
});
diff --git a/web/regression/javascript/fake_model.js b/web/regression/javascript/fake_model.js
new file mode 100644
index 00000000..acfaa532
--- /dev/null
+++ b/web/regression/javascript/fake_model.js
@@ -0,0 +1,21 @@
+export class FakeModel {
+ constructor() {
+ this.values = {};
+ }
+
+ set(key, value) {
+ this.values[key] = value;
+ }
+
+ get(key) {
+ return this.values[key];
+ }
+
+ unset(key) {
+ delete this.values[key];
+ }
+
+ toJSON() {
+ return Object.assign({}, this.values);
+ }
+}
diff --git a/web/regression/javascript/menu/can_create_spec.js b/web/regression/javascript/menu/can_create_spec.js
new file mode 100644
index 00000000..6423960f
--- /dev/null
+++ b/web/regression/javascript/menu/can_create_spec.js
@@ -0,0 +1,160 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+/////////////////////////////////////////////////////////////
+
+import {canCreate} from '../../../pgadmin/static/js/menu/can_create';
+import {TreeFake} from '../tree/tree_fake';
+
+const context = describe;
+
+describe('#canCreate', () => {
+ let ourBrowser;
+ let data;
+ let tree;
+
+ context('data is not null and check is false ', () => {
+ beforeEach(() => {
+ data = {action: 'create', check: false};
+ });
+ it('returns true', () => {
+ expect(canCreate({}, {}, {}, data)).toBe(true);
+ });
+ });
+
+ context('data is not null and check is true', () => {
+ beforeEach(() => {
+ data = {action: 'create', check: true};
+ });
+
+ context('is node with type schema', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [
+ {
+ id: 'level2',
+ data: {_type: 'schema'},
+ },
+ ],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ ourBrowser = {treeMenu: tree};
+ });
+
+ it('returns true', () => {
+ expect(canCreate(ourBrowser, 'coll-table', [{id: 'level2'}], data)).toBe(true);
+ });
+ });
+
+ context('has ancestor with type schema', () => {
+ beforeEach(() => {
+
+ let hierarchy = {
+ id: 'root',
+ children: [
+ {
+ id: 'level2',
+ data: {_type: 'schema'},
+ children: [
+ {
+ id: 'level3',
+ data: {_type: 'database'},
+ },
+ ],
+ },
+ ],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ ourBrowser = {treeMenu: tree};
+ });
+
+ it('returns true', () => {
+ expect(canCreate(ourBrowser, 'coll-table', [{id: 'level3'}], data)).toBe(true);
+ });
+ });
+
+ context('when type is not "coll-table"', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [
+ {
+ id: 'level2',
+ data: {_type: 'database'},
+ },
+ ],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ ourBrowser = {treeMenu: tree};
+ });
+
+ it('returns true', () => {
+ expect(canCreate(ourBrowser, 'coll-table', [{id: 'level2'}], data)).toBe(true);
+ });
+ });
+
+ context('when type is "coll-table"', () => {
+ context('when parent type is "catalog"', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [
+ {
+ id: 'level2',
+ data: {_type: 'catalog'},
+ children: [
+ {
+ id: 'level3',
+ data: {_type: 'coll-table'},
+ },
+ ],
+ },
+ ],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ ourBrowser = {treeMenu: tree};
+ });
+
+ it('returns false', () => {
+ expect(canCreate(ourBrowser, 'coll-table', [{id: 'level3'}], data)).toBe(false);
+ });
+ });
+
+ context('when parent type is not "catalog"', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [
+ {
+ id: 'level2',
+ data: {_type: 'database'},
+ children: [
+ {
+ id: 'level3',
+ data: {_type: 'coll-table'},
+ },
+ ],
+ },
+ ],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ ourBrowser = {treeMenu: tree};
+ });
+
+ it('returns false', () => {
+ expect(canCreate(ourBrowser, 'coll-table', [{id: 'level3'}], data)).toBe(true);
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/menu/menu_enabled_spec.js b/web/regression/javascript/menu/menu_enabled_spec.js
new file mode 100644
index 00000000..06967d77
--- /dev/null
+++ b/web/regression/javascript/menu/menu_enabled_spec.js
@@ -0,0 +1,131 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {TreeFake} from '../tree/tree_fake';
+import {menuEnabled} from '../../../pgadmin/static/js/menu/menu_enabled';
+
+const context = describe;
+describe('#menuEnabled', () => {
+ let ourBrowser;
+ beforeEach(() => {
+ const tree = new TreeFake();
+ ourBrowser = {
+ treeMenu: tree,
+ };
+ tree.addNewNode('level1', {}, undefined, []);
+ tree.addNewNode('level1.1', {_type: 'catalog'}, undefined, ['level1']);
+ tree.addNewNode('level1.1.1', {_type: 'database'}, undefined, ['level1', 'level1.1']);
+ tree.addNewNode('level1.2', {_type: 'bamm'}, undefined, ['level1']);
+ tree.addNewNode('level1.2.1', {
+ _type: 'database',
+ allowConn: true,
+ }, undefined, ['level1', 'level1.2']);
+ tree.addNewNode('level1.2.2', {
+ _type: 'database',
+ allowConn: false,
+ }, undefined, ['level1', 'level1.2']);
+ tree.addNewNode('level1.2.3', {
+ _type: 'table',
+ }, undefined, ['level1', 'level1.2']);
+
+ tree.addNewNode('level2', {}, undefined, []);
+ tree.addNewNode('level2.1', null, undefined, ['level2']);
+ tree.addNewNode('level2.1.1', {}, undefined, ['level2', 'level2.1']);
+ });
+
+ context('When the current node is a root node', () => {
+ it('return false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu, [], {}, [{id: 'level1'}])).toBe(false);
+ });
+ });
+
+ context('when current node does not exist', () => {
+ it('return false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu, [], {}, [{id: 'bamm'}])).toBe(false);
+ });
+ });
+
+ context('provided node data is undefined', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu, [], undefined, [{id: 'level1'}])).toBe(false);
+ });
+ });
+
+ context('provided node data is null', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu, [], null, [{id: 'level1'}])).toBe(false);
+ });
+ });
+
+ context('When the current node is not a root node', () => {
+ context('parent data does not exist', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu, [], {}, [{id: 'level2.1.1'}])).toBe(false);
+ });
+ });
+
+ context('parent as data', () => {
+ context('the current node type is in the supported node types', () => {
+ context('the parent is of the type catalog', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu,
+ ['schema'],
+ {_type: 'schema'},
+ [{id: 'level1.1.1'}]
+ )).toBe(false);
+ });
+ });
+ context('an ancestor with type catalog exists', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu,
+ ['table'],
+ {_type: 'table'},
+ [{id: 'level1.1.1.1'}]
+ )).toBe(false);
+ });
+ });
+ context('the parent is not of the type catalog', () => {
+ context('current node is of the type database', () => {
+ context('current node allows connection', () => {
+ it('returns true', () => {
+ expect(menuEnabled(ourBrowser.treeMenu,
+ ['database'],
+ {
+ _type: 'database',
+ allowConn: true,
+ }, [{id: 'level1.2.1'}])).toBe(true);
+ });
+ });
+ context('current node do not allow connection', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu,
+ ['database'], {
+ _type: 'database',
+ allowConn: false,
+ }, [{id: 'level1.2.2'}])).toBe(false);
+ });
+ });
+ });
+ context('current node is not of the type database', () => {
+ it('returns true', () => {
+ expect(menuEnabled(ourBrowser.treeMenu,
+ ['schema'], {
+ _type: 'schema',
+ }, [{id: 'level1.2.3'}])).toBe(true);
+ });
+ });
+ });
+ });
+ context('the current node type is not in the supported node types', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu, [], {_type: 'catalog'}, [{id: 'level1.1'}])).toBe(false);
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/restore/restore_dialog_spec.js b/web/regression/javascript/restore/restore_dialog_spec.js
new file mode 100644
index 00000000..c1335eeb
--- /dev/null
+++ b/web/regression/javascript/restore/restore_dialog_spec.js
@@ -0,0 +1,181 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {TreeFake} from '../tree/tree_fake';
+import {
+ RestoreDialog,
+} from '../../../pgadmin/tools/restore/static/js/restore_dialog';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+const context = describe;
+
+describe('RestoreDialog', () => {
+ let restoreDialog;
+ let pgBrowser;
+ let jquerySpy;
+ let alertifySpy;
+ let restoreModelSpy;
+
+
+ let rootNode;
+ let serverTreeNode;
+ let databaseTreeNode;
+ let ppasServerTreeNode;
+ let noDataNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: jasmine.createSpyObj('Node[server]', ['getTreeNodeHierarchy']),
+ database: jasmine.createSpyObj('Node[database]', ['getTreeNodeHierarchy']),
+ },
+ };
+ pgBrowser.Nodes.server.hasId = true;
+ pgBrowser.Nodes.database.hasId = true;
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ restoreModelSpy = jasmine.createSpy('restoreModelSpy');
+
+ rootNode = pgBrowser.treeMenu.addNewNode('level1', {}, [{id: 'level1'}], []);
+ serverTreeNode = new TreeNode('level1.1', {
+ _type: 'server',
+ _id: 10,
+ label: 'some-tree-label',
+ }, [{id: 'level1.1'}]);
+ pgBrowser.treeMenu.addChild(rootNode, serverTreeNode);
+
+ databaseTreeNode = new TreeNode('level1.1.1', {
+ _type: 'database',
+ _id: 10,
+ _label: 'some-database-label',
+ }, [{id: 'level1.1.1'}], ['level1', 'level1.1']);
+ pgBrowser.treeMenu.addChild(serverTreeNode, databaseTreeNode);
+
+ ppasServerTreeNode = new TreeNode('level1.2', {
+ _type: 'server',
+ server_type: 'ppas',
+ }, [{id: 'level1.2'}], ['level1']);
+ pgBrowser.treeMenu.addChild(rootNode, ppasServerTreeNode);
+
+ const level3 = new TreeNode('level3', {}, [{id: 'level3'}]);
+ pgBrowser.treeMenu.addChild(ppasServerTreeNode, level3);
+
+ noDataNode = pgBrowser.treeMenu.addNewNode('level3.1', undefined, [{id: 'level1'}]);
+ pgBrowser.treeMenu.addChild(level3, noDataNode);
+
+ });
+
+ describe('#draw', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ alertifySpy['pg_restore'] = jasmine.createSpy('pg_restore');
+ restoreDialog = new RestoreDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ restoreModelSpy
+ );
+
+ pgBrowser.get_preference = jasmine.createSpy('get_preferences');
+ });
+
+ context('there are no ancestors of the type server', () => {
+ it('does not create a dialog', () => {
+ pgBrowser.treeMenu.selectNode([{id: 'level1'}]);
+ restoreDialog.draw(null, null, null);
+ expect(alertifySpy['pg_restore']).not.toHaveBeenCalled();
+ });
+
+ it('display an alert with a Restore Error', () => {
+ restoreDialog.draw(null, [{id: 'level1'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Restore Error',
+ 'Please select server or child node from the browser tree.'
+ );
+ });
+ });
+
+ context('there is an ancestor of the type server', () => {
+ context('no preference can be found', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue(undefined);
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Restore Error"', () => {
+ restoreDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Restore Error',
+ 'Failed to load preference pg_bin_dir of module paths'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Restore Error"', () => {
+ restoreDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Restore Error',
+ 'Failed to load preference ppas_bin_dir of module paths'
+ );
+ });
+ });
+ });
+
+ context('preference can be found', () => {
+ context('binary folder is not configured', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue({});
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ restoreDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the PostgreSQL Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ restoreDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the EDB Advanced Server Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+ });
+
+ context('binary folder is configured', () => {
+ let spy;
+ beforeEach(() => {
+ spy = jasmine.createSpyObj('globals', ['resizeTo']);
+ alertifySpy['pg_restore'].and
+ .returnValue(spy);
+ pgBrowser.get_preference.and.returnValue({value: '/some/path'});
+ pgBrowser.Nodes.server.label = 'some-server-label';
+ });
+
+ it('displays the dialog', () => {
+ restoreDialog.draw(null, [{id: 'level1.1'}], {server: true});
+ expect(alertifySpy['pg_restore']).toHaveBeenCalledWith(
+ 'Restore (some-server-label: some-tree-label)',
+ [{id: 'level1.1'}],
+ serverTreeNode.getData(),
+ pgBrowser.Nodes.server
+ );
+ expect(spy.resizeTo).toHaveBeenCalledWith('65%', '60%');
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/restore/restore_dialog_wrapper_spec.js b/web/regression/javascript/restore/restore_dialog_wrapper_spec.js
new file mode 100644
index 00000000..c2a31d55
--- /dev/null
+++ b/web/regression/javascript/restore/restore_dialog_wrapper_spec.js
@@ -0,0 +1,593 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {TreeFake} from '../tree/tree_fake';
+import {RestoreDialogWrapper} from '../../../pgadmin/tools/restore/static/js/restore_dialog_wrapper';
+import MockAdapter from 'axios-mock-adapter';
+import axios from 'axios/index';
+import {FakeModel} from '../fake_model';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+let context = describe;
+
+describe('RestoreDialogWrapper', () => {
+ let jquerySpy;
+ let pgBrowser;
+ let alertifySpy;
+ let dialogModelKlassSpy;
+ let backform;
+ let generatedRestoreModel;
+ let restoreDialogWrapper;
+ let noDataNode;
+ let serverTreeNode;
+ let viewSchema;
+ let restoreJQueryContainerSpy;
+ let restoreNodeChildNodeSpy;
+ let restoreNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: {
+ hasId: true,
+ getTreeNodeHierarchy: jasmine.createSpy('getTreeNodeHierarchy'),
+ },
+ },
+ keyboardNavigation: jasmine.createSpyObj('keyboardNavigation', ['getDialogTabNavigator']),
+ };
+
+ noDataNode = pgBrowser.treeMenu.addNewNode('level1.1', undefined, [{id: 'level1'}]);
+ serverTreeNode = pgBrowser.treeMenu.addNewNode('level2.1', {
+ _type: 'server',
+ _id: 10,
+ label: 'some-tree-label',
+ }, [{id: 'level2.1'}]);
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ dialogModelKlassSpy = jasmine.createSpy('dialogModelKlass');
+ generatedRestoreModel = {};
+ viewSchema = {};
+ backform = jasmine.createSpyObj('backform', ['generateViewSchema', 'Dialog']);
+ backform.generateViewSchema.and.returnValue(viewSchema);
+ dialogModelKlassSpy.and.returnValue(generatedRestoreModel);
+ restoreJQueryContainerSpy = jasmine.createSpyObj('restoreJQueryContainer', ['get', 'attr']);
+ restoreJQueryContainerSpy.get.and.returnValue(restoreJQueryContainerSpy);
+
+ restoreNode = {
+ __internal: {
+ buttons: [
+ {}, {},
+ {
+ element: {
+ disabled: false,
+ },
+ },
+ ],
+ },
+ elements: {
+ body: {
+ childNodes: [
+ {},
+ ],
+ },
+ content: jasmine.createSpyObj('content', ['appendChild', 'attr']),
+ },
+ };
+
+
+ restoreNodeChildNodeSpy = jasmine.createSpyObj('something', ['addClass']);
+
+ jquerySpy.and.callFake((selector) => {
+ if (selector === '<div class=\'restore_dialog\'></div>') {
+ return restoreJQueryContainerSpy;
+ } else if (selector === restoreNode.elements.body.childNodes[0]) {
+ return restoreNodeChildNodeSpy;
+ }
+ });
+ });
+
+ describe('#prepare', () => {
+
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ restoreDialogWrapper = new RestoreDialogWrapper(
+ '<div class=\'restore_dialog\'></div>',
+ 'restoreDialogTitle',
+ 'restore',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+ restoreDialogWrapper = Object.assign(restoreDialogWrapper, restoreNode);
+ });
+ context('no tree element is selected', () => {
+ it('does not create a backform dialog', () => {
+ restoreDialogWrapper.prepare();
+ expect(backform.Dialog).not.toHaveBeenCalled();
+ });
+
+ it('disables the button "submit button" until a filename is selected', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreDialogWrapper.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('selected tree node has no data', () => {
+ beforeEach(() => {
+ pgBrowser.treeMenu.selectNode(noDataNode.domNode);
+ });
+
+ it('does not create a backform dialog', () => {
+ restoreDialogWrapper.prepare();
+ expect(backform.Dialog).not.toHaveBeenCalled();
+ });
+
+ it('disables the button "submit button" until a filename is selected', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreDialogWrapper.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('tree element is selected', () => {
+ let treeHierarchyInformation;
+ let dialogSpy;
+ beforeEach(() => {
+ treeHierarchyInformation = {
+ server: {
+ _type: 'server',
+ _id: 10,
+ priority: 0,
+ label: 'some-tree-label',
+ },
+ };
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ pgBrowser.Nodes['server'].getTreeNodeHierarchy.and
+ .returnValue(treeHierarchyInformation);
+ dialogSpy = jasmine.createSpyObj('newView', ['render']);
+ dialogSpy.$el = jasmine.createSpyObj('$el', ['find', 'attr']);
+ dialogSpy.model = jasmine.createSpyObj('model', ['on']);
+ dialogSpy.$el.find.and.returnValue([]);
+
+ backform.Dialog.and.returnValue(dialogSpy);
+ });
+
+ it('creates a backform dialog and displays it', () => {
+ restoreDialogWrapper.prepare();
+ expect(backform.Dialog).toHaveBeenCalledWith({
+ el: restoreJQueryContainerSpy,
+ model: generatedRestoreModel,
+ schema: viewSchema,
+ });
+
+ expect(dialogSpy.render).toHaveBeenCalled();
+ });
+
+ it('add alertify classes to restore node childnode', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreNodeChildNodeSpy.addClass)
+ .toHaveBeenCalledWith('alertify_tools_dialog_properties obj_properties');
+ });
+
+ it('disables the button submit button until a filename is selected', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreNode.__internal.buttons[2].element.disabled).toBe(true);
+ });
+
+ it('generates a new restore model', () => {
+ restoreDialogWrapper.prepare();
+ expect(dialogModelKlassSpy).toHaveBeenCalledWith(
+ {node_data: pgBrowser.Nodes['server']},
+ {node_info: treeHierarchyInformation}
+ );
+ });
+
+ it('add the new dialog to the restore node HTML', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreNode.elements.content.appendChild).toHaveBeenCalledWith(restoreJQueryContainerSpy);
+ });
+ });
+ });
+
+ describe('onButtonClicked', () => {
+ let networkMock;
+
+ beforeEach(() => {
+ pgBrowser.showHelp = jasmine.createSpy('showHelp');
+ networkMock = new MockAdapter(axios);
+ alertifySpy = jasmine.createSpyObj('alertify', ['success', 'alert']);
+ restoreDialogWrapper = new RestoreDialogWrapper(
+ '<div class=\'restore_dialog\'></div>',
+ 'restoreDialogTitle',
+ 'restore',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+ restoreDialogWrapper = Object.assign(restoreDialogWrapper, restoreNode);
+
+ });
+
+ afterEach(function () {
+ networkMock.restore();
+ });
+
+ context('dialog help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ element: {
+ name: 'dialog_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ },
+ },
+ },
+ };
+ restoreDialogWrapper.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'dialog_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ );
+ });
+
+ it('does not start the restore', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('object help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ element: {
+ name: 'object_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ },
+ },
+ },
+ };
+ restoreDialogWrapper.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'object_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ );
+ });
+
+ it('does not start the restore', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('restore button was pressed', () => {
+ let networkCalled;
+ let event;
+
+ context('no tree node is selected', () => {
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+ event = {
+ button: {
+ 'data-btn-name': 'restore',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ });
+
+ it('does not start the restore', () => {
+ restoreDialogWrapper.callback(event);
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('tree node selected has no data', () => {
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+ event = {
+ button: {
+ 'data-btn-name': 'restore',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ pgBrowser.treeMenu.selectNode(noDataNode.domNode);
+ });
+
+ it('does not start the restore', () => {
+ restoreDialogWrapper.callback(event);
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('tree node select has data', () => {
+
+ let databaseTreeNode;
+
+ beforeEach(() => {
+ databaseTreeNode = pgBrowser.treeMenu.addNewNode('level3.1', {
+ _type: 'database',
+ _id: 10,
+ _label: 'some-database-label',
+ }, [{id: 'level3.1'}]);
+ pgBrowser.treeMenu.addChild(serverTreeNode, databaseTreeNode);
+ pgBrowser.Nodes.database = {
+ hasId: true,
+ _label: 'some-database-label',
+ };
+ let fakeModel = new FakeModel();
+ fakeModel.set('some-key', 'some-value');
+ restoreDialogWrapper.view = {
+ model: fakeModel,
+ };
+ pgBrowser.treeMenu.selectNode(databaseTreeNode.domNode);
+ pgBrowser.Events = jasmine.createSpyObj('pgBrowserEventsSpy', ['trigger']);
+ event = {
+ button: {
+ 'data-btn-name': 'restore',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ });
+ context('restore job created successfully', () => {
+ let dataSentToServer;
+ beforeEach(() => {
+ networkMock.onPost('/restore/job/10').reply((request) => {
+ dataSentToServer = request.data;
+ return [200, {}];
+ });
+ });
+
+ it('create an success alert box', (done) => {
+ restoreDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.success).toHaveBeenCalledWith(
+ 'Restore job created.',
+ 5
+ );
+ done();
+ }, 0);
+ });
+
+ it('trigger background process', (done) => {
+ restoreDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(pgBrowser.Events.trigger).toHaveBeenCalledWith(
+ 'pgadmin-bgprocess:created',
+ restoreDialogWrapper
+ );
+ done();
+ }, 0);
+ });
+
+ it('send correct data to server', (done) => {
+ restoreDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(JSON.parse(dataSentToServer)).toEqual({
+ 'some-key': 'some-value',
+ 'database': 'some-database-label',
+ });
+ done();
+ }, 0);
+ });
+ });
+
+ context('error creating restore job', () => {
+ beforeEach(() => {
+ networkMock.onPost('/restore/job/10').reply(() => {
+ return [400, {}];
+ });
+ });
+
+ it('creates an alert box', (done) => {
+ restoreDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Restore job failed.',
+ undefined
+ );
+ done();
+ }, 0);
+ });
+ });
+ });
+ });
+ });
+
+ describe('setExtraParameters', () => {
+ let selectedNode;
+ let treeInfo;
+ let model;
+
+ beforeEach(() => {
+ restoreDialogWrapper = new RestoreDialogWrapper(
+ '<div class=\'restore_dialog\'></div>',
+ 'restoreDialogTitle',
+ 'restore',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ model = new FakeModel();
+ restoreDialogWrapper.view = {
+ model: model,
+ };
+ });
+
+ context('when it is a custom model', () => {
+ beforeEach(() => {
+ model.set('custom', true);
+ treeInfo = {
+ 'database': {
+ '_label': 'some-database-label',
+ },
+ };
+ });
+
+ it('only sets the database', () => {
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ 'custom': true,
+ 'database': 'some-database-label',
+ });
+ });
+ });
+
+ context('when it is not a custom model', () => {
+ beforeEach(() => {
+ model.set('custom', false);
+ treeInfo = {
+ 'database': {
+ '_label': 'some-database-label',
+ },
+ 'schema': {
+ '_label': 'some-schema-label',
+ },
+ };
+ });
+
+ context('when selected node is a schema', () => {
+ it('sets schemas on the model', () => {
+ selectedNode = new TreeNode('schema', {_type: 'schema', _label: 'some-schema-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ });
+ });
+ });
+
+ context('when selected node is a table', () => {
+ it('sets schemas and table on the model', () => {
+ selectedNode = new TreeNode('table', {_type: 'table', _label: 'some-table-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ tables: ['some-table-label'],
+ });
+ });
+ });
+
+ context('when selected node is a function', () => {
+ it('sets schemas and function on the model', () => {
+ selectedNode = new TreeNode('function', {_type: 'function', _label: 'some-function-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ functions: ['some-function-label'],
+ });
+ });
+ });
+
+ context('when selected node is an index', () => {
+ it('sets schemas and index on the model', () => {
+ selectedNode = new TreeNode('index', {_type: 'index', _label: 'some-index-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ indexes: ['some-index-label'],
+ });
+ });
+ });
+
+ context('when selected node is a trigger', () => {
+ it('sets schemas and trigger on the model', () => {
+ selectedNode = new TreeNode('trigger', {_type: 'trigger', _label: 'some-trigger-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ triggers: ['some-trigger-label'],
+ });
+ });
+ });
+
+ context('when selected node is a trigger_func', () => {
+ it('sets schemas and trigger_func on the model', () => {
+ selectedNode = new TreeNode('trigger_func', {_type: 'trigger_func', _label: 'some-trigger_func-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ trigger_funcs: ['some-trigger_func-label'],
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/schema/can_drop_child_spec.js b/web/regression/javascript/schema/can_drop_child_spec.js
new file mode 100644
index 00000000..4403b274
--- /dev/null
+++ b/web/regression/javascript/schema/can_drop_child_spec.js
@@ -0,0 +1,82 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {canDropChild} from '../../../pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child';
+import {TreeFake} from '../tree/tree_fake';
+
+let context = describe;
+
+describe('can_drop_child', () => {
+
+ let browser;
+ let itemData;
+ let item;
+
+ beforeEach(() => {
+ browser = {
+ treeMenu: new TreeFake(),
+ };
+ item = [];
+ browser.treeMenu.addNewNode('node1', {_type: 'schema'}, [{id: 'node1'}], []);
+ browser.treeMenu.addNewNode('node1.1', {_type: 'database'}, [{id: 'node1.1'}], ['node1']);
+ browser.treeMenu.addNewNode('node2', {_type: 'catalog'}, [{id: 'node2'}], []);
+ browser.treeMenu.addNewNode('node2.1', {_type: 'table'}, [{id: 'node2.1'}], ['node2']);
+ browser.treeMenu.addNewNode('node3', {_type: 'function'}, [{id: 'node3'}], []);
+ browser.treeMenu.addNewNode('node3.1', {_type: 'procedure'}, [{id: 'node3.1'}], ['node3']);
+ });
+
+ context('when current node is of the type schema', () => {
+ beforeEach(() => {
+ itemData = {
+ _type: 'schema',
+ };
+ item = [{id: 'node1'}];
+ });
+
+ it('returns true', () => {
+ let bool = canDropChild(browser, itemData, item);
+ expect(bool).toBe(true);
+ });
+ });
+
+ context('when a parent of the current node is a schema', () => {
+ it('returns true', () => {
+ itemData = {
+ _type: 'database',
+ };
+ item = [{id: 'node1.1'}];
+ let bool = canDropChild(browser, itemData, item);
+ expect(bool).toBe(true);
+ });
+ });
+
+ context('when a parent of the current node is a catalog', () => {
+ it('returns false', () => {
+ itemData= {
+ _type: 'table',
+ };
+ item = [{id: 'node2.1'}];
+
+ let bool = canDropChild(browser, itemData, item);
+ expect(bool).toBe(false);
+ });
+ });
+
+ context('when a parent of the current node is not catalog nor schema', () => {
+ it('returns true', () => {
+ itemData = {
+ _type: 'procedure',
+ };
+ item = [{id: 'node3.1'}];
+
+ let bool = canDropChild(browser, itemData, item);
+ expect(bool).toBe(true);
+ });
+ });
+});
diff --git a/web/regression/javascript/sqleditor/filter_dialog_specs.js b/web/regression/javascript/sqleditor/filter_dialog_specs.js
index ed77dff5..cea75e6b 100644
--- a/web/regression/javascript/sqleditor/filter_dialog_specs.js
+++ b/web/regression/javascript/sqleditor/filter_dialog_specs.js
@@ -7,10 +7,8 @@
//
//////////////////////////////////////////////////////////////////////////
import filterDialog from 'sources/sqleditor/filter_dialog';
-// import filterDialogModel from 'sources/sqleditor/filter_dialog_model';
describe('filterDialog', () => {
- jasmine.createSpy('sqlEditorController');
describe('filterDialog', () => {
describe('when using filter dialog', () => {
beforeEach(() => {
diff --git a/web/regression/javascript/table/enable_disable_triggers_spec.js b/web/regression/javascript/table/enable_disable_triggers_spec.js
new file mode 100644
index 00000000..7bdd284e
--- /dev/null
+++ b/web/regression/javascript/table/enable_disable_triggers_spec.js
@@ -0,0 +1,271 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import MockAdapter from 'axios-mock-adapter';
+import axios from 'axios/index';
+import {
+ enableTriggers,
+ disableTriggers,
+} from '../../../pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers';
+import {TreeFake} from '../tree/tree_fake';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+describe('#enableTriggers', () => {
+ let networkMock;
+ let tree;
+ let alertify;
+ let generateUrlSpy;
+ beforeEach(() => {
+ networkMock = new MockAdapter(axios);
+ tree = new TreeFake();
+ const server1 = tree.addNewNode('server1', {_id: 1}, ['<li>server1</li>']);
+ const database1 = tree.addNewNode('database1', {_type: 'database'}, ['<li>database1</li>']);
+ tree.addChild(server1, database1);
+
+ const schema1 = tree.addNewNode('schema1', {_type: 'schema'}, ['<li>schema1</li>']);
+ tree.addChild(database1, schema1);
+
+ const table1 = tree.addNewNode('table1', {_type: 'table'}, ['<li>table1</li>']);
+ tree.addChild(schema1, table1);
+
+ const column1 = tree.addNewNode('column1', {_type: 'column'}, ['<li>column1</li>']);
+ tree.addChild(table1, column1);
+
+ const tableNoData = tree.addNewNode('table-no-data', undefined, ['<li>table-no-data</li>']);
+ tree.addChild(schema1, tableNoData);
+
+ alertify = jasmine.createSpyObj('alertify', ['success', 'error']);
+ generateUrlSpy = jasmine.createSpy('generateUrl');
+ generateUrlSpy.and.returnValue('/some/place');
+ });
+
+ describe('no node is selected', () => {
+ it('does not send the request to the backend', (done) => {
+ networkMock.onAny('.*').reply(200, () => {
+ });
+
+ setTimeout(() => {
+ expect(enableTriggers(tree, alertify, generateUrlSpy, {})).toBe(false);
+ done();
+ }, 0);
+ });
+ });
+
+ describe('a node is selected', () => {
+ describe('node as no data', () => {
+ it('does not send the request to the backend', () => {
+ tree.selectNode([{id: 'table-no-data'}]);
+
+ networkMock.onAny('.*').reply(200, () => {
+ });
+
+ setTimeout(() => {
+ expect(enableTriggers(tree, alertify, generateUrlSpy, {})).toBe(false);
+ }, 0);
+ });
+ });
+
+ describe('node as data', () => {
+ describe('backend responds with success', () => {
+ let networkMockCalledWith;
+ beforeEach(() => {
+ networkMockCalledWith = false;
+ networkMock.onPut(/.*/).reply((configuration) => {
+ networkMockCalledWith = configuration;
+ return [200, {
+ success: 1,
+ info: 'some information',
+ }];
+ });
+ });
+
+ it('displays an alert box with success', (done) => {
+ tree.selectNode([{id: 'table1'}]);
+ enableTriggers(tree, alertify, generateUrlSpy, {});
+ setTimeout(() => {
+ expect(alertify.success).toHaveBeenCalledWith('some information');
+ done();
+ }, 0);
+ });
+
+ it('reloads the node', (done) => {
+ enableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+ setTimeout(() => {
+ expect(tree.selected()).toEqual(['<li>table1</li>']);
+ done();
+ }, 20);
+ });
+
+ it('call backend with the correct parameters', (done) => {
+ enableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+ setTimeout(() => {
+ expect(networkMockCalledWith.data).toEqual(JSON.stringify({enable: 'true'}));
+ done();
+ }, 0);
+ });
+ });
+
+ describe('backend responds with error', () => {
+ beforeEach(() => {
+ networkMock.onPut(/.*/).reply(() => {
+ return [500, {
+ success: 0,
+ errormsg: 'some error message',
+ }];
+ });
+ });
+
+ it('displays an error alert', (done) => {
+ tree.selectNode([{id: 'table1'}]);
+ enableTriggers(tree, alertify, generateUrlSpy, {});
+ setTimeout(() => {
+ expect(alertify.error).toHaveBeenCalledWith('some error message');
+ done();
+ }, 0);
+ });
+
+ it('unload the node', (done) => {
+ enableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+
+ setTimeout(() => {
+ expect(tree.findNodeByDomElement([{id: 'table1'}]).children.length).toBe(0);
+ done();
+ }, 20);
+ });
+ });
+ });
+ });
+});
+
+describe('#disableTriggers', () => {
+ let networkMock;
+ let tree;
+ let alertify;
+ let generateUrlSpy;
+ beforeEach(() => {
+ networkMock = new MockAdapter(axios);
+ tree = new TreeFake();
+ const server1 = tree.addNewNode('server1', {_id: 1}, ['<li>server1</li>']);
+ const database1 = new TreeNode('database1', {_type: 'database'}, ['<li>database1</li>']);
+ tree.addChild(server1, database1);
+
+ const schema1 = new TreeNode('schema1', {_type: 'schema'}, ['<li>schema1</li>']);
+ tree.addChild(database1, schema1);
+
+ const table1 = new TreeNode('table1', {_type: 'table'}, ['<li>table1</li>']);
+ tree.addChild(schema1, table1);
+
+ const column1 = new TreeNode('column1', {_type: 'column'}, ['<li>column1</li>']);
+ tree.addChild(table1, column1);
+
+ const tableNoData = new TreeNode('table-no-data', undefined, ['<li>table-no-data</li>']);
+ tree.addChild(schema1, tableNoData);
+
+ alertify = jasmine.createSpyObj('alertify', ['success', 'error']);
+ generateUrlSpy = jasmine.createSpy('generateUrl');
+ generateUrlSpy.and.returnValue('/some/place');
+ });
+
+ describe('no node is selected', () => {
+ it('does not send the request to the backend', (done) => {
+ networkMock.onAny('.*').reply(200, () => {
+ });
+
+ setTimeout(() => {
+ expect(disableTriggers(tree, alertify, generateUrlSpy, {})).toBe(false);
+ done();
+ }, 0);
+ });
+ });
+
+ describe('a node is selected', () => {
+ describe('node as no data', () => {
+ it('does not send the request to the backend', () => {
+ tree.selectNode([{id: 'table-no-data'}]);
+
+ networkMock.onAny('.*').reply(200, () => {
+ });
+
+ setTimeout(() => {
+ expect(disableTriggers(tree, alertify, generateUrlSpy, {})).toBe(false);
+ }, 0);
+ });
+ });
+
+ describe('node as data', () => {
+ describe('backend responds with success', () => {
+ let networkMockCalledWith;
+ beforeEach(() => {
+ networkMockCalledWith = false;
+ networkMock.onPut(/.*/).reply((configuration) => {
+ networkMockCalledWith = configuration;
+ return [200, {
+ success: 1,
+ info: 'some information',
+ }];
+ });
+ });
+
+ it('displays an alert box with success', (done) => {
+ tree.selectNode([{id: 'table1'}]);
+ disableTriggers(tree, alertify, generateUrlSpy, {});
+ setTimeout(() => {
+ expect(alertify.success).toHaveBeenCalledWith('some information');
+ done();
+ }, 0);
+ });
+
+ it('reloads the node', (done) => {
+ disableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+ setTimeout(() => {
+ expect(tree.selected()).toEqual(['<li>table1</li>']);
+ done();
+ }, 20);
+ });
+
+ it('call backend with the correct parameters', (done) => {
+ disableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+ setTimeout(() => {
+ expect(networkMockCalledWith.data).toEqual(JSON.stringify({enable: 'false'}));
+ done();
+ }, 0);
+ });
+ });
+
+ describe('backend responds with error', () => {
+ beforeEach(() => {
+ networkMock.onPut(/.*/).reply(() => {
+ return [500, {
+ success: 0,
+ errormsg: 'some error message',
+ }];
+ });
+ });
+
+ it('displays an error alert', (done) => {
+ tree.selectNode([{id: 'table1'}]);
+ disableTriggers(tree, alertify, generateUrlSpy, {});
+ setTimeout(() => {
+ expect(alertify.error).toHaveBeenCalledWith('some error message');
+ done();
+ }, 0);
+ });
+
+ it('unload the node', (done) => {
+ disableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+
+ setTimeout(() => {
+ expect(tree.findNodeByDomElement([{id: 'table1'}]).children.length).toBe(0);
+ done();
+ }, 20);
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/tree/pgadmin_tree_node_spec.js b/web/regression/javascript/tree/pgadmin_tree_node_spec.js
new file mode 100644
index 00000000..479e515c
--- /dev/null
+++ b/web/regression/javascript/tree/pgadmin_tree_node_spec.js
@@ -0,0 +1,353 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+import {
+ getTreeNodeHierarchyFromElement,
+ getTreeNodeHierarchyFromIdentifier,
+} from '../../../pgadmin/static/js/tree/pgadmin_tree_node';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+import {TreeFake} from './tree_fake';
+
+const context = describe;
+
+describe('tree#node#getTreeNodeHierarchy', () => {
+ let browser;
+ let newTree;
+ beforeEach(() => {
+ newTree = new TreeFake();
+ browser = {
+ Nodes: {
+ 'special one': {hasId: true},
+ 'child special': {hasId: true},
+ 'other type': {hasId: true},
+ 'table': {hasId: true},
+ 'partition': {hasId: true},
+ 'no id': {hasId: false},
+ },
+ };
+ browser.treeMenu = newTree;
+ });
+
+ context('getTreeNodeHierarchy is called with aciTreeNode object', () => {
+ describe('When the current node is root element', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ });
+ });
+
+ it('returns a object with the element type passed data and priority == 0', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)([{id: 'root'}]);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+
+ describe('When the current node is not of a known type', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'do not exist',
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)('root');
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node type has no id', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'no id',
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)('root');
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node is at the second level', () => {
+ beforeEach(() => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ });
+ const firstChild = new TreeNode('first child', {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ }, ['root']);
+ newTree.addChild(root, firstChild);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)([{id: 'first child'}]);
+ expect(result).toEqual({
+ 'child special': {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ 'priority': 0,
+ },
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+
+ context('When tree as "special type"', () => {
+ context('When "special type" have "other type"', () => {
+ context('When "other type" have "special type"', () => {
+ describe('When retrieving lead node', () => {
+ it('does not override previous node type data', () => {
+ const rootNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+
+ const level1 = new TreeNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'other type',
+ });
+ newTree.addChild(rootNode, level1);
+
+ newTree.addChild(level1, new TreeNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ }));
+
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)([{id: 'level 2'}]);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ 'priority': 0,
+ },
+ 'other type': {
+ 'some key': 'some value',
+ '_type': 'other type',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+ });
+ });
+ });
+
+ context('When tree has table', () => {
+ context('when table has partition', () => {
+ it('returns table with partition parameters', () => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+ const level1 = new TreeNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'table',
+ });
+ newTree.addChild(root, level1);
+ newTree.addChild(level1, new TreeNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'partition',
+ 'some other key': 'some other value',
+ }));
+
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)([{id:'level 2'}]);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ 'table': {
+ 'some key': 'expected value',
+ 'some other key': 'some other value',
+ '_type': 'partition',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+ });
+ });
+
+ context('getTreeNodeHierarchy is called with TreeNode object', () => {
+ let treeNode;
+ describe('When the current node is root element', () => {
+ beforeEach(() => {
+ treeNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+ });
+
+ it('returns a object with the element type passed data and priority == 0', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+
+ describe('When the current node is not of a known type', () => {
+ beforeEach(() => {
+ treeNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'do not exist',
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node type has no id', () => {
+ beforeEach(() => {
+ treeNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'no id',
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node is at the second level', () => {
+ beforeEach(() => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+ treeNode = new TreeNode('first child', {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ });
+ newTree.addChild(root, treeNode);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'child special': {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ 'priority': 0,
+ },
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+
+ context('When tree as "special type"', () => {
+ context('When "special type" have "other type"', () => {
+ context('When "other type" have "special type"', () => {
+ describe('When retrieving lead node', () => {
+ it('does not override previous node type data', () => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+ const level1 = new TreeNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'other type',
+ });
+ newTree.addChild(root, level1);
+ treeNode = new TreeNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ });
+ newTree.addChild(level1, treeNode);
+
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ 'priority': 0,
+ },
+ 'other type': {
+ 'some key': 'some value',
+ '_type': 'other type',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+ });
+ });
+ });
+
+ context('When tree has table', () => {
+ context('when table has partition', () => {
+ it('returns table with partition parameters', () => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ });
+ const level1 = newTree.addNewNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'table',
+ });
+ newTree.addChild(root, level1);
+ treeNode = new TreeNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'partition',
+ 'some other key': 'some other value',
+ });
+ newTree.addChild(level1, treeNode);
+
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ 'table': {
+ 'some key': 'expected value',
+ 'some other key': 'some other value',
+ '_type': 'partition',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/tree/tree_fake.js b/web/regression/javascript/tree/tree_fake.js
index b285a45f..e03d71fb 100644
--- a/web/regression/javascript/tree/tree_fake.js
+++ b/web/regression/javascript/tree/tree_fake.js
@@ -10,6 +10,32 @@
import {Tree} from '../../../pgadmin/static/js/tree/tree';
export class TreeFake extends Tree {
+ static build(structure) {
+ let tree = new TreeFake();
+ let rootNode = tree.rootNode;
+
+ if (structure.children !== undefined) {
+ structure.children.forEach((child) => {
+ TreeFake.recursivelyAddNodes(tree, child, rootNode);
+ });
+ }
+
+ return tree;
+ }
+
+ static recursivelyAddNodes(tree, newNode, parent) {
+ let id = newNode.id;
+ let data = newNode.data ? newNode.data : {};
+ let domNode = newNode.domNode ? newNode.domNode : [{id: id}];
+ tree.addNewNode(id, data, domNode, tree.translateTreeNodeIdFromACITree([parent]));
+
+ if (newNode.children !== undefined) {
+ newNode.children.forEach((child) => {
+ TreeFake.recursivelyAddNodes(tree, child, newNode);
+ });
+ }
+ }
+
constructor() {
super();
this.aciTreeToOurTreeTranslator = {};
@@ -45,7 +71,7 @@ export class TreeFake extends Tree {
}
translateTreeNodeIdFromACITree(aciTreeNode) {
- if(aciTreeNode === undefined || aciTreeNode[0] === undefined) {
+ if (aciTreeNode === undefined || aciTreeNode[0] === undefined) {
return null;
}
return this.aciTreeToOurTreeTranslator[aciTreeNode[0].id];
--
2.16.2
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-07 19:01 Joao De Almeida Pereira <[email protected]>
parent: Joao De Almeida Pereira <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Joao De Almeida Pereira @ 2018-05-07 19:01 UTC (permalink / raw)
To: Dave Page <[email protected]>; +Cc: Ashesh Vashi <[email protected]>; Anthony Emengo <[email protected]>; Khushboo Vashi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
Hi Hackers,
We noticed that some of the latest changes created some conflicts with this
patch. So you can find attached the patch rebased on the new master.
Thanks
Victoria && Joao
On Fri, May 4, 2018 at 6:03 PM Joao De Almeida Pereira <
[email protected]> wrote:
> And now the patches......
>
>
> On Fri, May 4, 2018 at 6:01 PM Joao De Almeida Pereira <
> [email protected]> wrote:
>
>> Hi Hackers,
>>
>> *Be prepared this is going to be a big email.*
>> Objectives of this patch:
>>
>> 1. Start the work to split ACI from pgAdmin code
>> 2. Add more tests around the front end components
>> 3. Start the discussion on application architecture
>>
>> 1. Start the work to split ACI from pgAdmin code Why
>>
>> This journey started on our first attempt to store state of the current
>> tree, so that when a user accessed again pgAdmin
>> there would be no need to reopen all the nodes and make the flow better.
>> Another problem users brought to us was related
>> to the UI behavior when the number of objects was too big.
>>
>> In order to do implement this features we would have to play around with
>> aciTree or some functions from our code that
>> called and leveraged the aciTree functionalities. To do this we needed to
>> have some confidence that our changes would
>> implement correctly the new feature and it would not break the current
>> functionality.
>> The process that we used was first extract the functions that interacted
>> with the tree, wrap them with
>> tests and then refactor them in some way. While doing this work we
>> realized that the tree started spreading roots
>> throughout the majority of the application.
>> How
>>
>> Options:
>>
>> 1. Rewrite the front end
>> 2. One bang replace ACI Tree without change on the application
>> 3. Create an abstraction layer between ACI Tree and the application
>>
>> 1. Rewrite the front end
>> Pros Const
>> Achieve decoupling by recreating the frontend Cost is too high
>> Do not rely on deprecated or unmaintained libraries Turn around is too
>> long
>> One shot change 2. One bang replace ACI Tree
>> Pros Const
>> Achieve decoupling by recreating the frontend Cost is too high
>> Does not achieve the decoupling unless we change the application
>> Need to create an adaptor
>> No garantee of success
>> One shot change 3. Create an abstraction layer between ACI Tree and the
>> application
>> Pros Const
>> Achieve decoupling of the application and the tree 90% of the time on
>> Code archaeology
>> Lower cost
>> Ability to change or keep ACI Tree, decision TBD
>> Can be done in a iterative way
>>
>> We decided that options 3 looked more attractive specially because we
>> could do it in a iterative way.
>>
>> The next image depicts the current state of the interactions between the
>> application and the ACI and the place we want
>> to be in.
>>
>> [image: Screen Shot 2018-05-04 at 4.06.17 PM.png]
>>
>> for retrieving of data from the ACI Tree that then is used in different
>> ways.
>>
>> Because we are doing this in a iterative process we need to keep the
>> current tree working and create our adaptor next
>> to it. In order to do that we created a new property on PGBrowser class
>> called treeMenu and when ACI Tree receives
>> the node information we also populate the new Tree with all the needed
>> information.
>>
>> This approach allow us not to worry, for now, with data retrieval, URL
>> system and other issues that should be addressed
>> in the future when the adaptor is done.
>>
>> This first patch tries to deal with the low hanging fruit, functions that
>> are triggered by events from ACI Tree.
>> Cases like registering to events and triggering events need to be handled
>> in the future as well.
>> 2. Add more tests around the front end components Why
>>
>> Go Fast Forever: https://builttoadapt.io/why-tdd-489fdcdda05e
>> I think this is a very good summary of why do we need tests in our
>> applications.
>> 3. Start the discussion on application architecture
>>
>> Why should we care about location of files inside a our application?
>>
>> Why is this way better the another?
>>
>> These are 2 good questions that have very lengthy answers. Trying to more
>> or less summarize the answers we care about
>> the location of the files, because we want our application to communicate
>> intent and there are always pros and cons
>> on all the decisions that we make.
>>
>> At this point the application structure follows our menu, this approach
>> eventually make is easier to follow the code
>> but at the same time if the menu changes down the line, will we change
>> the structure of our folders?
>>
>> The proposal that we do with the last diff of this patch is to change to
>> a structure that slices vertically the
>> application. This way we can understand intent behind the code and more
>> easily find what we are looking for.
>>
>> In the current structure if you want to see the tables code you need to
>> go to
>> pgAdmin/browser/server_groups/servers/databases/schemas/tables/ this is
>> a huge path to remember and to get to. What
>> do we win with this? If we open pgAdmin we know which nodes to click in
>> order to get to tables. But for development
>> every time that you are looking for a specific functionality you need to
>> run the application, navigate the menu so that
>> you know where you can find the code. This doesn’t sound very appealing.
>>
>> What if our structure would look like this:
>>
>> - web
>> - tables
>> - controller
>> - get_nodes.py
>> - get_sql.py
>> - __init__.py
>> - frontend
>> - component
>> - ddl_component.js
>> - services
>> - table-service.js
>> - schemas
>> - servers
>> - ....
>>
>> This would saves us time because all the information that we need is what
>> are we working on and everything is right there.
>> Menu driven structure Intent Driven Structure
>> *Pros:* *Pros:*
>> Already in place Explicitly shows features
>> Self contained features Self contained features
>> Support for drop in features Support for drop in features
>> *Cons:* *Cons:*
>> Follows the menu, and it could change Need to change current code
>> Hard to find features Some additional plumbing might be needed
>> Drop in features need to be placed in a specific location according to
>> the menu location
>>
>> What are your thought about this architecture?
>>
>> Around minute 7 of this video
>> <https://www.youtube.com/watch?v=hALFGQNeEnU; Uncle Bob shows an
>> application written
>> in rails to talk about architecture. It is a long KeyNote if you are
>> curious I would advise you to see the full video.
>> His approach to architecture of the application is pretty interesting.
>> ------------------------------
>> Patches 0001 Change the order of the shims on the shim file
>>
>> Simple change that orders the shims inside the shim file
>> 0002 New tree implementation
>>
>> This is the first version of our Tree implementation. At this point is a
>> very simple tree without no abstractions and
>> with code that eventually is not very performant, but this is only the
>> first iteration and we are trying to follow the
>> Last Responsible Moment Principle
>> <https://medium.com/@aidanjcasey/guiding-principles-for-an-evolutionary-software-architecture-b6dc2cb...;
>> .
>>
>> What can you find in this patch:
>>
>> - Creation of PGBrowser.treeMenu
>> - Initial version of the Tree Adaptor
>> web/pgadmin/static/js/tree/tree.js
>> - TreeFake test double
>> <https://martinfowler.com/bliki/TestDouble.html; that can replace the
>> Tree for testing purposes
>> - Tests. As an interesting asside because Fake’s need to behave like
>> the real object you will noticed that there are
>> tests for this type of double and they the same as of the real object.
>>
>> 0003 Extract, Test and Refactor Methods
>>
>> This is the patch that contains the change that we talked a in the
>> objectives. It touches in a subset of the places
>> where itemData function is called. To have a sense of progress the
>> following image depicts, on the left all places
>> where this functions can be found in the code and on the right the places
>> where it still remains after this patch.
>>
>> But this patch is not only related to the ACI Tree, it also creates some
>> abstractions from code that we found repeated.
>> Some examples of this are the dialogs for Backup and Restore, where the
>> majority of the logic was common, so we created
>> in web/pgadmin/static/js/alertify/ 3 different objects:
>> A Factory DialogFactory that is responsible for creating new Dialog and
>> these dialogs will use the DialogWrapper.
>>
>> This is also a good example of the Last Responsible Moment Principle
>> <https://medium.com/@aidanjcasey/guiding-principles-for-an-evolutionary-software-architecture-b6dc2cb...;
>> ,
>> in Dialog there are still some functions that are used in a Recover and
>> Backup scenario. When we have more examples
>> we will be able to, if we want, to extract that into another function.
>>
>> While doing some code refactoring we found out that the Right Click Menu
>> as some checks that can be centralized. Examples
>> of these are checks for Enable/Disable of an option of the display of the
>> Create/Edit/Drop option in some menus.
>> Check web/pgadmin/static/js/menu/ for more information. This simplified
>> code like:
>>
>> canCreate: function(itemData, item, data) {
>> - //If check is false then , we will allow create menu
>> - if (data && data.check == false)
>> - return true;
>> -
>> - var t = pgBrowser.tree, i = item, d = itemData;
>> - // To iterate over tree to check parent node
>> - while (i) {
>> - // If it is schema then allow user to create domain
>> - if (_.indexOf(['schema'], d._type) > -1)
>> - return true;
>> -
>> - if ('coll-domain' == d._type) {
>> - //Check if we are not child of catalog
>> - var prev_i = t.hasParent(i) ? t.parent(i) : null,
>> - prev_d = prev_i ? t.itemData(prev_i) : null;
>> - if( prev_d._type == 'catalog') {
>> - return false;
>> - } else {
>> - return true;
>> - }
>> - }
>> - i = t.hasParent(i) ? t.parent(i) : null;
>> - d = i ? t.itemData(i) : null;
>> - }
>> - // by default we do not want to allow create menu
>> - return true;
>> + return canCreate.canCreate(pgBrowser, 'coll-domain', item, data);
>> }
>>
>> This refactor was made in a couple of places throughout the code.
>>
>> Another refactor that had to be done was the creation of the functions
>> getTreeNodeHierarchyFromElement and
>> getTreeNodeHierarchyFromIdentifier that replace the old
>> getTreeNodeHierarchy. This implementation was done
>> because for the time being we have 2 different types of trees and calls
>> to them.
>>
>> The rest of the changes resulted from our process of refactoring that we
>> are following:
>>
>> 1. Find a location where itemData is used
>> 2. Extract that into a function outside of the current code
>> 3. Wrap it with tests
>> 4. When we have enough confidence on the tests, we refactor the
>> function to use the new Tree
>> 5. We refactor the function to become more readable
>> 6. Start over
>>
>> 0004 Architecture
>>
>> The proposed structure is our vision for the application, this is not the
>> state we will be in when this patch lands.
>> For now the only change we are doing is move the javascript into
>> static/js/FEATURE_NAME. This is a first step in the
>> direction that we would like to see the product heading.
>>
>> Even if we decide to keep using the same architecture the move of
>> Javascript to a centralized place will
>> not break any plugability as webpack can retrieve all the Javascript from
>> any place in the code.
>>
>> Have a nice weekend
>> Joao
>>
>>
>>
Attachments:
[image/png] Screen Shot 2018-05-04 at 4.06.17 PM.png (104.4K, 3-Screen%20Shot%202018-05-04%20at%204.06.17%20PM.png)
download | view image
[application/octet-stream] 0004-Architectural-change.patch (23.3K, 4-0004-Architectural-change.patch)
download | inline diff:
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
index a09610ad..6d003578 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
@@ -1,7 +1,7 @@
define('pgadmin.node.schema', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid',
- 'pgadmin.node.schema.dir/can_drop_child',
+ 'sources/schema/can_drop_child',
'pgadmin.browser.collection', 'pgadmin.browser.server.privilege',
], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, Backgrid,
canDropChild) {
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
index 9a021745..4b06d375 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
@@ -1,9 +1,9 @@
define('pgadmin.node.table', [
- 'pgadmin.tables.js/enable_disable_triggers',
+ 'sources/table/enable_disable_triggers',
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.backgrid',
- 'pgadmin.tables.js/show_advanced_tab',
+ 'sources/table/show_advanced_tab',
'sources/menu/can_create',
'pgadmin.browser.collection', 'pgadmin.node.column',
diff --git a/web/pgadmin/static/js/alertify/dialog_factory.js b/web/pgadmin/static/js/alertify/dialog_factory.js
index 500140b8..806f8687 100644
--- a/web/pgadmin/static/js/alertify/dialog_factory.js
+++ b/web/pgadmin/static/js/alertify/dialog_factory.js
@@ -7,8 +7,8 @@
//
//////////////////////////////////////////////////////////////
-import * as BackupDialog from '../../../tools/backup/static/js/backup_dialog_wrapper';
-import {RestoreDialogWrapper} from '../../../tools/restore/static/js/restore_dialog_wrapper';
+import * as BackupDialog from '../backup/backup_dialog_wrapper';
+import {RestoreDialogWrapper} from '../restore/restore_dialog_wrapper';
export class DialogFactory {
constructor(pgBrowser, $,
diff --git a/web/pgadmin/tools/backup/static/js/backup_dialog.js b/web/pgadmin/static/js/backup/backup_dialog.js
similarity index 90%
rename from web/pgadmin/tools/backup/static/js/backup_dialog.js
rename to web/pgadmin/static/js/backup/backup_dialog.js
index c74c376c..190367ca 100644
--- a/web/pgadmin/tools/backup/static/js/backup_dialog.js
+++ b/web/pgadmin/static/js/backup/backup_dialog.js
@@ -7,9 +7,9 @@
//
//////////////////////////////////////////////////////////////
-import gettext from '../../../../static/js/gettext';
-import Backform from '../../../../static/js/backform.pgadmin';
-import {Dialog} from '../../../../static/js/alertify/dialog';
+import gettext from '../gettext';
+import Backform from '../backform.pgadmin';
+import {Dialog} from '../alertify/dialog';
export class BackupDialog extends Dialog {
constructor(pgBrowser, $, alertify, BackupModel, backform = Backform) {
diff --git a/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js b/web/pgadmin/static/js/backup/backup_dialog_wrapper.js
similarity index 96%
rename from web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js
rename to web/pgadmin/static/js/backup/backup_dialog_wrapper.js
index 2cebe3d1..7b6250f7 100644
--- a/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js
+++ b/web/pgadmin/static/js/backup/backup_dialog_wrapper.js
@@ -7,12 +7,12 @@
//
//////////////////////////////////////////////////////////////
-import {getTreeNodeHierarchyFromElement} from '../../../../static/js/tree/pgadmin_tree_node';
+import {getTreeNodeHierarchyFromElement} from '../tree/pgadmin_tree_node';
import axios from 'axios/index';
-import gettext from '../../../../static/js/gettext';
-import url_for from '../../../../static/js/url_for';
+import gettext from '../gettext';
+import url_for from '../url_for';
import _ from 'underscore';
-import {DialogWrapper} from '../../../../static/js/alertify/dialog_wrapper';
+import {DialogWrapper} from '../alertify/dialog_wrapper';
export class BackupDialogWrapper extends DialogWrapper {
constructor(dialogContainerSelector, dialogTitle, typeOfDialog,
diff --git a/web/pgadmin/tools/backup/static/js/menu_utils.js b/web/pgadmin/static/js/backup/menu_utils.js
similarity index 89%
rename from web/pgadmin/tools/backup/static/js/menu_utils.js
rename to web/pgadmin/static/js/backup/menu_utils.js
index fdf400af..d3c1b143 100644
--- a/web/pgadmin/tools/backup/static/js/menu_utils.js
+++ b/web/pgadmin/static/js/backup/menu_utils.js
@@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
-import {isProvidedDataValid} from '../../../../static/js/menu/menu_enabled';
+import {isProvidedDataValid} from '../menu/menu_enabled';
export const backupSupportedNodes = [
'database', 'schema', 'table', 'partition',
diff --git a/web/pgadmin/tools/datagrid/static/js/get_panel_title.js b/web/pgadmin/static/js/datagrid/get_panel_title.js
similarity index 90%
rename from web/pgadmin/tools/datagrid/static/js/get_panel_title.js
rename to web/pgadmin/static/js/datagrid/get_panel_title.js
index 64b3a3d2..f5a5664d 100644
--- a/web/pgadmin/tools/datagrid/static/js/get_panel_title.js
+++ b/web/pgadmin/static/js/datagrid/get_panel_title.js
@@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
-import {getTreeNodeHierarchyFromIdentifier} from '../../../../static/js/tree/pgadmin_tree_node';
+import {getTreeNodeHierarchyFromIdentifier} from '../tree/pgadmin_tree_node';
function getDatabaseLabel(parentData) {
return parentData.database ? parentData.database.label
diff --git a/web/pgadmin/tools/datagrid/static/js/show_data.js b/web/pgadmin/static/js/datagrid/show_data.js
similarity index 92%
rename from web/pgadmin/tools/datagrid/static/js/show_data.js
rename to web/pgadmin/static/js/datagrid/show_data.js
index 373c97cd..b6d7c52b 100644
--- a/web/pgadmin/tools/datagrid/static/js/show_data.js
+++ b/web/pgadmin/static/js/datagrid/show_data.js
@@ -6,9 +6,9 @@
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
-import gettext from '../../../../static/js/gettext';
-import url_for from '../../../../static/js/url_for';
-import {getTreeNodeHierarchyFromIdentifier} from '../../../../static/js/tree/pgadmin_tree_node';
+import gettext from '../gettext';
+import url_for from '../url_for';
+import {getTreeNodeHierarchyFromIdentifier} from '../tree/pgadmin_tree_node';
export function showDataGrid(
datagrid,
diff --git a/web/pgadmin/tools/datagrid/static/js/show_query_tool.js b/web/pgadmin/static/js/datagrid/show_query_tool.js
similarity index 88%
rename from web/pgadmin/tools/datagrid/static/js/show_query_tool.js
rename to web/pgadmin/static/js/datagrid/show_query_tool.js
index 0eb12b8b..a1b4874c 100644
--- a/web/pgadmin/tools/datagrid/static/js/show_query_tool.js
+++ b/web/pgadmin/static/js/datagrid/show_query_tool.js
@@ -7,9 +7,9 @@
//
//////////////////////////////////////////////////////////////
-import gettext from '../../../../static/js/gettext';
-import url_for from '../../../../static/js/url_for';
-import {getTreeNodeHierarchyFromIdentifier} from '../../../../static/js/tree/pgadmin_tree_node';
+import gettext from '../gettext';
+import url_for from '../url_for';
+import {getTreeNodeHierarchyFromIdentifier} from '../tree/pgadmin_tree_node';
function hasDatabaseInformation(parentData) {
return parentData.database;
diff --git a/web/pgadmin/tools/grant_wizard/static/js/menu_utils.js b/web/pgadmin/static/js/grant/wizard/menu_utils.js
similarity index 100%
rename from web/pgadmin/tools/grant_wizard/static/js/menu_utils.js
rename to web/pgadmin/static/js/grant/wizard/menu_utils.js
diff --git a/web/pgadmin/tools/maintenance/static/js/menu_utils.js b/web/pgadmin/static/js/maintenance/menu_utils.js
similarity index 100%
rename from web/pgadmin/tools/maintenance/static/js/menu_utils.js
rename to web/pgadmin/static/js/maintenance/menu_utils.js
diff --git a/web/pgadmin/tools/restore/static/js/menu_utils.js b/web/pgadmin/static/js/restore/menu_utils.js
similarity index 100%
rename from web/pgadmin/tools/restore/static/js/menu_utils.js
rename to web/pgadmin/static/js/restore/menu_utils.js
diff --git a/web/pgadmin/tools/restore/static/js/restore_dialog.js b/web/pgadmin/static/js/restore/restore_dialog.js
similarity index 88%
rename from web/pgadmin/tools/restore/static/js/restore_dialog.js
rename to web/pgadmin/static/js/restore/restore_dialog.js
index 4884d901..72f89045 100644
--- a/web/pgadmin/tools/restore/static/js/restore_dialog.js
+++ b/web/pgadmin/static/js/restore/restore_dialog.js
@@ -7,10 +7,10 @@
//
//////////////////////////////////////////////////////////////
-import gettext from '../../../../static/js/gettext';
+import gettext from '../gettext';
import {sprintf} from 'sprintf-js';
-import Backform from '../../../../static/js/backform.pgadmin';
-import {Dialog} from '../../../../static/js/alertify/dialog';
+import Backform from '../backform.pgadmin';
+import {Dialog} from '../alertify/dialog';
export class RestoreDialog extends Dialog {
constructor(pgBrowser, $, alertify, RestoreModel, backform = Backform) {
diff --git a/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js b/web/pgadmin/static/js/restore/restore_dialog_wrapper.js
similarity index 96%
rename from web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js
rename to web/pgadmin/static/js/restore/restore_dialog_wrapper.js
index 845da7a3..13d781fb 100644
--- a/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js
+++ b/web/pgadmin/static/js/restore/restore_dialog_wrapper.js
@@ -1,9 +1,9 @@
-import {getTreeNodeHierarchyFromElement} from '../../../../static/js/tree/pgadmin_tree_node';
+import {getTreeNodeHierarchyFromElement} from '../tree/pgadmin_tree_node';
import axios from 'axios/index';
import _ from 'underscore';
-import gettext from '../../../../static/js/gettext';
-import url_for from '../../../../static/js/url_for';
-import {DialogWrapper} from '../../../../static/js/alertify/dialog_wrapper';
+import gettext from '../gettext';
+import url_for from '../url_for';
+import {DialogWrapper} from '../alertify/dialog_wrapper';
export class RestoreDialogWrapper extends DialogWrapper {
constructor(dialogContainerSelector, dialogTitle, typeOfDialog,
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child.js b/web/pgadmin/static/js/schema/can_drop_child.js
similarity index 100%
rename from web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child.js
rename to web/pgadmin/static/js/schema/can_drop_child.js
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers.js b/web/pgadmin/static/js/table/enable_disable_triggers.js
similarity index 100%
rename from web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers.js
rename to web/pgadmin/static/js/table/enable_disable_triggers.js
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/show_advanced_tab.js b/web/pgadmin/static/js/table/show_advanced_tab.js
similarity index 100%
rename from web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/show_advanced_tab.js
rename to web/pgadmin/static/js/table/show_advanced_tab.js
diff --git a/web/pgadmin/tools/backup/static/js/backup.js b/web/pgadmin/tools/backup/static/js/backup.js
index d74ed6dd..b20b7e6c 100644
--- a/web/pgadmin/tools/backup/static/js/backup.js
+++ b/web/pgadmin/tools/backup/static/js/backup.js
@@ -3,8 +3,8 @@ define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'pgadmin.alertifyjs', 'backbone', 'pgadmin.backgrid',
'pgadmin.backform', 'pgadmin.browser', 'sources/utils',
- 'tools/backup/static/js/menu_utils',
- 'tools/backup/static/js/backup_dialog',
+ 'sources/backup/menu_utils',
+ 'sources/backup/backup_dialog',
'sources/menu/menu_enabled',
], function(
gettext, url_for, $, _, S, alertify, Backbone, Backgrid, Backform, pgBrowser,
diff --git a/web/pgadmin/tools/datagrid/static/js/datagrid.js b/web/pgadmin/tools/datagrid/static/js/datagrid.js
index 520a9ce5..676f1901 100644
--- a/web/pgadmin/tools/datagrid/static/js/datagrid.js
+++ b/web/pgadmin/tools/datagrid/static/js/datagrid.js
@@ -2,9 +2,9 @@ define('pgadmin.datagrid', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'pgadmin.alertifyjs', 'sources/pgadmin', 'bundled_codemirror',
'sources/sqleditor_utils', 'backbone',
- 'tools/datagrid/static/js/show_data',
- 'tools/datagrid/static/js/get_panel_title',
- 'tools/datagrid/static/js/show_query_tool',
+ 'sources/datagrid/show_data',
+ 'sources/datagrid/get_panel_title',
+ 'sources/datagrid/show_query_tool',
'wcdocker',
], function(
gettext, url_for, $, _, alertify, pgAdmin, codemirror, sqlEditorUtils,
diff --git a/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js b/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js
index cf234f1d..56eb378a 100644
--- a/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js
+++ b/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js
@@ -3,7 +3,7 @@ define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'pgadmin.alertifyjs', 'pgadmin.backgrid', 'pgadmin.backform',
'pgadmin.browser', 'pgadmin.browser.node',
- 'tools/grant_wizard/static/js/menu_utils',
+ 'sources/grant/wizard/menu_utils',
'sources/menu/menu_enabled',
'backgrid.select.all',
diff --git a/web/pgadmin/tools/maintenance/static/js/maintenance.js b/web/pgadmin/tools/maintenance/static/js/maintenance.js
index 9092eb73..e6aa69e3 100644
--- a/web/pgadmin/tools/maintenance/static/js/maintenance.js
+++ b/web/pgadmin/tools/maintenance/static/js/maintenance.js
@@ -2,7 +2,7 @@ define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'pgadmin.alertifyjs', 'sources/pgadmin', 'pgadmin.browser', 'backbone',
'backgrid', 'backform', 'sources/utils',
- 'tools/maintenance/static/js/menu_utils',
+ 'sources/maintenance/menu_utils',
'sources/menu/menu_enabled',
'pgadmin.backform', 'pgadmin.backgrid',
'pgadmin.browser.node.ui',
diff --git a/web/pgadmin/tools/restore/static/js/restore.js b/web/pgadmin/tools/restore/static/js/restore.js
index eaa7a15e..b5d6913a 100644
--- a/web/pgadmin/tools/restore/static/js/restore.js
+++ b/web/pgadmin/tools/restore/static/js/restore.js
@@ -2,9 +2,9 @@ define('tools.restore', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'underscore.string', 'pgadmin.alertifyjs', 'pgadmin.browser',
'pgadmin.backgrid', 'pgadmin.backform', 'sources/utils',
- 'tools/restore/static/js/menu_utils',
+ 'sources/restore/menu_utils',
'sources/menu/menu_enabled',
- 'tools/restore/static/js/restore_dialog',
+ 'sources/restore/restore_dialog',
], function(
gettext, url_for, $, _, Backbone, S, alertify, pgBrowser, Backgrid, Backform,
commonUtils, menuUtils, menuEnabled, restoreDialog
diff --git a/web/regression/javascript/backup/backup_dialog_spec.js b/web/regression/javascript/backup/backup_dialog_spec.js
index 2655059d..aae18eeb 100644
--- a/web/regression/javascript/backup/backup_dialog_spec.js
+++ b/web/regression/javascript/backup/backup_dialog_spec.js
@@ -6,7 +6,7 @@
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
-import {BackupDialog} from '../../../pgadmin/tools/backup/static/js/backup_dialog';
+import {BackupDialog} from '../../../pgadmin/static/js/backup/backup_dialog';
import {TreeFake} from '../tree/tree_fake';
const context = describe;
diff --git a/web/regression/javascript/backup/backup_dialog_wrapper_spec.js b/web/regression/javascript/backup/backup_dialog_wrapper_spec.js
index 58705318..73e453e5 100644
--- a/web/regression/javascript/backup/backup_dialog_wrapper_spec.js
+++ b/web/regression/javascript/backup/backup_dialog_wrapper_spec.js
@@ -1,5 +1,5 @@
import {TreeFake} from '../tree/tree_fake';
-import {BackupDialogWrapper} from '../../../pgadmin/tools/backup/static/js/backup_dialog_wrapper';
+import {BackupDialogWrapper} from '../../../pgadmin/static/js/backup/backup_dialog_wrapper';
import axios from 'axios/index';
import MockAdapter from 'axios-mock-adapter';
import {FakeModel} from '../fake_model';
diff --git a/web/regression/javascript/backup/global_server_backup_dialog_spec.js b/web/regression/javascript/backup/global_server_backup_dialog_spec.js
index 86df672e..67cca9db 100644
--- a/web/regression/javascript/backup/global_server_backup_dialog_spec.js
+++ b/web/regression/javascript/backup/global_server_backup_dialog_spec.js
@@ -6,7 +6,7 @@
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
-import {BackupDialog} from '../../../pgadmin/tools/backup/static/js/backup_dialog';
+import {BackupDialog} from '../../../pgadmin/static/js/backup/backup_dialog';
import {TreeFake} from '../tree/tree_fake';
const context = describe;
diff --git a/web/regression/javascript/backup/menu_utils_spec.js b/web/regression/javascript/backup/menu_utils_spec.js
index 9435d699..ecf8abcd 100644
--- a/web/regression/javascript/backup/menu_utils_spec.js
+++ b/web/regression/javascript/backup/menu_utils_spec.js
@@ -8,7 +8,7 @@
//////////////////////////////////////////////////////////////
-import {menuEnabledServer} from '../../../pgadmin/tools/backup/static/js/menu_utils';
+import {menuEnabledServer} from '../../../pgadmin/static/js/backup/menu_utils';
const context = describe;
diff --git a/web/regression/javascript/browser/server_groups/servers/databases/schemas/tables/show_advanced_tab_spec.js b/web/regression/javascript/browser/server_groups/servers/databases/schemas/tables/show_advanced_tab_spec.js
index f9559a41..4b3811aa 100644
--- a/web/regression/javascript/browser/server_groups/servers/databases/schemas/tables/show_advanced_tab_spec.js
+++ b/web/regression/javascript/browser/server_groups/servers/databases/schemas/tables/show_advanced_tab_spec.js
@@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
-import {show_advanced_tab} from '../../../../../../../../pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/show_advanced_tab';
+import {show_advanced_tab} from '../../../../../../../../pgadmin/static/js/table/show_advanced_tab';
describe('#show_advanced_tab', () => {
let tableModel;
diff --git a/web/regression/javascript/datagrid/get_panel_title_spec.js b/web/regression/javascript/datagrid/get_panel_title_spec.js
index 8a344a84..2594ec96 100644
--- a/web/regression/javascript/datagrid/get_panel_title_spec.js
+++ b/web/regression/javascript/datagrid/get_panel_title_spec.js
@@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
-import {getPanelTitle} from '../../../pgadmin/tools/datagrid/static/js/get_panel_title';
+import {getPanelTitle} from '../../../pgadmin/static/js/datagrid/get_panel_title';
import {TreeFake} from '../tree/tree_fake';
import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
diff --git a/web/regression/javascript/datagrid/show_data_spec.js b/web/regression/javascript/datagrid/show_data_spec.js
index 80d25eb3..3e4feec9 100644
--- a/web/regression/javascript/datagrid/show_data_spec.js
+++ b/web/regression/javascript/datagrid/show_data_spec.js
@@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
-import {showDataGrid} from '../../../pgadmin/tools/datagrid/static/js/show_data';
+import {showDataGrid} from '../../../pgadmin/static/js/datagrid/show_data';
import {TreeFake} from '../tree/tree_fake';
import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
diff --git a/web/regression/javascript/datagrid/show_query_tool_spec.js b/web/regression/javascript/datagrid/show_query_tool_spec.js
index 66bd37ce..e58112c0 100644
--- a/web/regression/javascript/datagrid/show_query_tool_spec.js
+++ b/web/regression/javascript/datagrid/show_query_tool_spec.js
@@ -8,7 +8,7 @@
//////////////////////////////////////////////////////////////
import {TreeFake} from '../tree/tree_fake';
-import {showQueryTool} from '../../../pgadmin/tools/datagrid/static/js/show_query_tool';
+import {showQueryTool} from '../../../pgadmin/static/js/datagrid/show_query_tool';
import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
const context = describe;
diff --git a/web/regression/javascript/restore/restore_dialog_spec.js b/web/regression/javascript/restore/restore_dialog_spec.js
index 156f56bb..b4fc1358 100644
--- a/web/regression/javascript/restore/restore_dialog_spec.js
+++ b/web/regression/javascript/restore/restore_dialog_spec.js
@@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
import {TreeFake} from '../tree/tree_fake';
-import {RestoreDialog} from '../../../pgadmin/tools/restore/static/js/restore_dialog';
+import {RestoreDialog} from '../../../pgadmin/static/js/restore/restore_dialog';
const context = describe;
diff --git a/web/regression/javascript/restore/restore_dialog_wrapper_spec.js b/web/regression/javascript/restore/restore_dialog_wrapper_spec.js
index c2a31d55..84de8e62 100644
--- a/web/regression/javascript/restore/restore_dialog_wrapper_spec.js
+++ b/web/regression/javascript/restore/restore_dialog_wrapper_spec.js
@@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
import {TreeFake} from '../tree/tree_fake';
-import {RestoreDialogWrapper} from '../../../pgadmin/tools/restore/static/js/restore_dialog_wrapper';
+import {RestoreDialogWrapper} from '../../../pgadmin/static/js/restore/restore_dialog_wrapper';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios/index';
import {FakeModel} from '../fake_model';
diff --git a/web/regression/javascript/schema/can_drop_child_spec.js b/web/regression/javascript/schema/can_drop_child_spec.js
index 4403b274..388dffbc 100644
--- a/web/regression/javascript/schema/can_drop_child_spec.js
+++ b/web/regression/javascript/schema/can_drop_child_spec.js
@@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
-import {canDropChild} from '../../../pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child';
+import {canDropChild} from '../../../pgadmin/static/js/schema/can_drop_child';
import {TreeFake} from '../tree/tree_fake';
let context = describe;
diff --git a/web/regression/javascript/table/enable_disable_triggers_spec.js b/web/regression/javascript/table/enable_disable_triggers_spec.js
index 7bdd284e..767c78a4 100644
--- a/web/regression/javascript/table/enable_disable_triggers_spec.js
+++ b/web/regression/javascript/table/enable_disable_triggers_spec.js
@@ -12,7 +12,7 @@ import axios from 'axios/index';
import {
enableTriggers,
disableTriggers,
-} from '../../../pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers';
+} from '../../../pgadmin/static/js/table/enable_disable_triggers';
import {TreeFake} from '../tree/tree_fake';
import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
--
2.16.2
[application/octet-stream] 0003-Extract-test-and-refactor-methods.patch (261.4K, 5-0003-Extract-test-and-refactor-methods.patch)
download | inline diff:
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.js
index 9015d8d2..5a3a4bc0 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.js
@@ -1,8 +1,9 @@
define('pgadmin.node.collation', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
+ 'sources/menu/can_create',
'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser) {
+], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, canCreate) {
if (!pgBrowser.Nodes['coll-collation']) {
pgAdmin.Browser.Nodes['coll-collation'] =
@@ -223,32 +224,7 @@ define('pgadmin.node.collation', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create collation
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-collation' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-collation', item, data);
},
});
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.js
index 403ca471..1a38b083 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.js
@@ -2,9 +2,11 @@
define('pgadmin.node.domain', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid',
+ 'sources/menu/can_create',
'pgadmin.browser.collection',
], function(
- gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid
+ gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid,
+ canCreate
) {
// Define Domain Collection Node
@@ -297,32 +299,7 @@ define('pgadmin.node.domain', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create domain
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-domain' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-domain', item, data);
},
isDisabled: function(m){
if (!m.isNew()) {
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table.js
index 160db83f..0e820343 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table.js
@@ -2,9 +2,11 @@
define('pgadmin.node.foreign_table', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid',
+ 'sources/menu/can_create',
'pgadmin.browser.collection',
], function(
- gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid
+ gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid,
+ canCreate
) {
if (!pgBrowser.Nodes['coll-foreign_table']) {
@@ -660,32 +662,7 @@ define('pgadmin.node.foreign_table', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create foreign table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-foreign_table' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-foreign_table', item, data);
},
});
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.js
index 89806681..f2fe85be 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.js
@@ -1,9 +1,11 @@
define('pgadmin.node.fts_configuration', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid',
+ 'sources/menu/can_create',
'pgadmin.browser.collection',
], function(
- gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid
+ gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid,
+ canCreate
) {
// Model for tokens control
@@ -578,32 +580,7 @@ define('pgadmin.node.fts_configuration', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create fts configuration
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-fts_configuration' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-fts_configuration', item, data);
},
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary.js
index ed83feb1..f1a330f7 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary.js
@@ -1,8 +1,10 @@
define('pgadmin.node.fts_dictionary', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
+ 'sources/menu/can_create',
'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform) {
+], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform,
+ canCreate) {
// Extend the browser's node model class to create a option/value pair
var OptionLabelModel = pgAdmin.Browser.Node.Model.extend({
@@ -187,32 +189,7 @@ define('pgadmin.node.fts_dictionary', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create fts dictionary
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-fts_dictionary' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-fts_dictionary', item, data);
},
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser.js
index 92c0786e..daea35de 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser.js
@@ -1,7 +1,9 @@
define('pgadmin.node.fts_parser', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
- 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser) {
+ 'sources/pgadmin', 'pgadmin.browser',
+ 'sources/menu/can_create',
+ 'pgadmin.browser.collection',
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, canCreate) {
// Extend the collection class for fts parser
if (!pgBrowser.Nodes['coll-fts_parser']) {
@@ -200,32 +202,7 @@ define('pgadmin.node.fts_parser', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create fts parser
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-fts_parser' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-fts_parser', item, data);
},
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template.js
index 606a57a6..0c008c83 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template.js
@@ -1,7 +1,9 @@
define('pgadmin.node.fts_template', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
- 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser) {
+ 'sources/pgadmin', 'pgadmin.browser',
+ 'sources/menu/can_create',
+ 'pgadmin.browser.collection',
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, canCreate) {
// Extend the collection class for fts template
if (!pgBrowser.Nodes['coll-fts_template']) {
@@ -140,32 +142,7 @@ define('pgadmin.node.fts_template', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create fts fts_template
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-fts_template' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-fts_template', item, data);
},
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function.js
index 6e405165..40636446 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function.js
@@ -2,8 +2,10 @@
define('pgadmin.node.function', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
+ 'sources/menu/can_create',
'pgadmin.browser.collection', 'pgadmin.browser.server.privilege',
-], function(gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform) {
+], function(gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform,
+ canCreate) {
if (!pgBrowser.Nodes['coll-function']) {
pgBrowser.Nodes['coll-function'] =
@@ -439,32 +441,7 @@ define('pgadmin.node.function', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create Function
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-function' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-function', item, data);
},
});
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js
index aeb8271b..11b587ed 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js
@@ -2,8 +2,9 @@
define('pgadmin.node.trigger_function', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
+ 'sources/menu/can_create',
'pgadmin.browser.collection', 'pgadmin.browser.server.privilege',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform) {
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, canCreate) {
if (!pgBrowser.Nodes['coll-trigger_function']) {
pgBrowser.Nodes['coll-trigger_function'] =
@@ -358,32 +359,7 @@ define('pgadmin.node.trigger_function', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create Function
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-trigger_function' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-trigger_function', item, data);
},
});
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence.js
index 57c95acd..0a59efb0 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence.js
@@ -1,8 +1,10 @@
define('pgadmin.node.sequence', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
+ 'sources/menu/can_create',
'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform) {
+], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform,
+ canCreate) {
// Extend the browser's collection class for sequence collection
if (!pgBrowser.Nodes['coll-sequence']) {
@@ -61,32 +63,7 @@ define('pgadmin.node.sequence', [
canDrop: pgBrowser.Nodes['schema'].canChildDrop,
canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create collation
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-sequence' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-sequence', item, data);
},
// Define the model for sequence node.
model: pgBrowser.Node.Model.extend({
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child.js
new file mode 100644
index 00000000..84e25b5c
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child.js
@@ -0,0 +1,18 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export function canDropChild(pgBrowser, itemData, item) {
+ let node = pgBrowser.treeMenu.findNodeByDomElement(item);
+
+ if (node.anyParent((parent) => parent.getData()._type === 'catalog')) {
+ return false;
+ }
+
+ return true;
+}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
index a7fd4c7c..a09610ad 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
@@ -1,8 +1,10 @@
define('pgadmin.node.schema', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid',
+ 'pgadmin.node.schema.dir/can_drop_child',
'pgadmin.browser.collection', 'pgadmin.browser.server.privilege',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, Backgrid) {
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, Backgrid,
+canDropChild) {
// VacuumSettings Collection to display all settings parameters as Grid
Backform.VacuumCollectionControl =
@@ -428,51 +430,12 @@ define('pgadmin.node.schema', [
// This function will checks whether we can allow user to
// drop object or not based on location within schema & catalog
canChildDrop: function(itemData, item) {
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create collation
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if(prev_d && prev_d._type == 'catalog') {
- return false;
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canDropChild.canDropChild(pgBrowser, itemData, item);
},
});
pgBrowser.tableChildTreeNodeHierarchy = function(i) {
- var idx = 0,
- res = {},
- t = pgBrowser.tree;
-
- do {
- var d = t.itemData(i);
- if (
- d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId
- ) {
- if (d._type === 'partition' || d._type === 'table') {
- if (!('table' in res)) {
- res['table'] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- } else {
- res[d._type] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- } while (i);
-
- return res;
+ return this.getTreeNodeHierarchy(i);
};
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js
index 72b6eb0c..1fdacf52 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js
@@ -88,7 +88,6 @@ define('pgadmin.node.column', [
if (!pgBrowser.Nodes['column']) {
pgBrowser.Nodes['column'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table', 'view', 'mview'],
collection_type: ['coll-table', 'coll-view', 'coll-mview'],
type: 'column',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js
index 857cf4c4..ab28a86b 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js
@@ -8,7 +8,6 @@ define('pgadmin.node.check_constraint', [
// Check Constraint Node
if (!pgBrowser.Nodes['check_constraint']) {
pgAdmin.Browser.Nodes['check_constraint'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'check_constraint',
label: gettext('Check'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js
index 0bbf66a1..adccf2e9 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js
@@ -605,7 +605,6 @@ define('pgadmin.node.exclusion_constraint', [
// Extend the browser's node class for exclusion constraint node
if (!pgBrowser.Nodes['exclusion_constraint']) {
pgAdmin.Browser.Nodes['exclusion_constraint'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'exclusion_constraint',
label: gettext('Exclusion constraint'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js
index 3c4b89f3..9899df92 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js
@@ -603,7 +603,6 @@ define('pgadmin.node.foreign_key', [
// Extend the browser's node class for foreign key node
if (!pgBrowser.Nodes['foreign_key']) {
pgAdmin.Browser.Nodes['foreign_key'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'foreign_key',
label: gettext('Foreign key'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js
index d3a6cff4..0ad0f054 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js
@@ -20,7 +20,6 @@ define('pgadmin.node.primary_key', [
parent_type: ['table','partition'],
canDrop: true,
canDropCascade: true,
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
Init: function() {
/* Avoid multiple registration of menus */
if (this.initialized)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js
index 769185d6..18d3ca33 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js
@@ -20,7 +20,6 @@ define('pgadmin.node.unique_constraint', [
parent_type: ['table','partition'],
canDrop: true,
canDropCascade: true,
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
Init: function() {
/* Avoid multiple registration of menus */
if (this.initialized)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js
index cb242cd2..9c0e24fe 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js
@@ -12,14 +12,12 @@ define('pgadmin.node.constraints', [
node: 'constraints',
label: gettext('Constraints'),
type: 'coll-constraints',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
columns: ['name', 'comment'],
});
}
if (!pgBrowser.Nodes['constraints']) {
pgAdmin.Browser.Nodes['constraints'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'constraints',
label: gettext('Constraints'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js
index ec2b4da1..2b687c1a 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js
@@ -13,7 +13,6 @@ define('pgadmin.node.index', [
node: 'index',
label: gettext('Indexes'),
type: 'coll-index',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
sqlAlterHelp: 'sql-alterindex.html',
sqlCreateHelp: 'sql-createindex.html',
dialogHelp: url_for('help.static', {'filename': 'index_dialog.html'}),
@@ -215,7 +214,6 @@ define('pgadmin.node.index', [
if (!pgBrowser.Nodes['index']) {
pgAdmin.Browser.Nodes['index'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table', 'view', 'mview', 'partition'],
collection_type: ['coll-table', 'coll-view'],
sqlAlterHelp: 'sql-alterindex.html',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js
index d807304e..c7a7819a 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js
@@ -1,11 +1,15 @@
define([
+ 'sources/tree/pgadmin_tree_node',
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.backgrid',
+ 'sources/menu/can_create',
'pgadmin.browser.collection', 'pgadmin.browser.table.partition.utils',
],
function(
- gettext, url_for, $, _, S, pgAdmin, pgBrowser, Alertify, Backform, Backgrid
+ pgadminTreeNode,
+ gettext, url_for, $, _, S, pgAdmin, pgBrowser, Alertify, Backform, Backgrid,
+ canCreate
) {
if (!pgBrowser.Nodes['coll-partition']) {
@@ -13,7 +17,6 @@ function(
pgAdmin.Browser.Collection.extend({
node: 'partition',
label: gettext('Partitions'),
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'coll-partition',
columns: [
'name', 'schema', 'partition_value', 'is_partitioned', 'description',
@@ -80,36 +83,6 @@ function(
},
]);
},
- getTreeNodeHierarchy: function(i) {
- var idx = 0,
- res = {},
- t = pgBrowser.tree;
-
- do {
- var d = t.itemData(i);
- if (
- d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId
- ) {
- if (d._type == 'partition' && 'partition' in res) {
- if (!('table' in res)) {
- res['table'] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- } else if (d._type == 'table') {
- if (!('table' in res)) {
- res['table'] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- } else {
- res[d._type] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- } while (i);
-
- return res;
- },
generate_url: function(item, type, d, with_id, info) {
if (_.indexOf([
'stats', 'statistics', 'dependency', 'dependent', 'reset',
@@ -1190,32 +1163,7 @@ function(
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-table' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null;
- var prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-table', item, data);
},
// Check to whether table has disable trigger(s)
canCreate_with_trigger_enable: function(itemData, item, data) {
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js
index 3af61754..354909f3 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js
@@ -16,7 +16,6 @@ define('pgadmin.node.rule', [
node: 'rule',
label: gettext('Rules'),
type: 'coll-rule',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
columns: ['name', 'owner', 'comment'],
});
}
@@ -35,7 +34,6 @@ define('pgadmin.node.rule', [
*/
if (!pgBrowser.Nodes['rule']) {
pgAdmin.Browser.Nodes['rule'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table','view', 'partition'],
type: 'rule',
sqlAlterHelp: 'sql-alterrule.html',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers.js
new file mode 100644
index 00000000..2d792043
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers.js
@@ -0,0 +1,52 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import axios from 'axios';
+
+export function disableTriggers(tree, alertify, generateUrl, args) {
+ return setTriggers(tree, alertify, generateUrl, args, {enable: 'false' });
+}
+export function enableTriggers(tree, alertify, generateUrl, args) {
+ return setTriggers(tree, alertify, generateUrl, args, {enable: 'true' });
+}
+
+function setTriggers(tree, alertify, generateUrl, args, params) {
+ const treeNode = retrieveTreeNode(args, tree);
+
+ if (!treeNode || treeNode.getData() === null || treeNode.getData() === undefined)
+ return false;
+
+ axios.put(
+ generateUrl(treeNode.getHtmlIdentifier(), 'set_trigger', treeNode.getData(), true),
+ params
+ )
+ .then((res) => {
+ if (res.data.success === 1) {
+ alertify.success(res.data.info);
+ treeNode.reload(tree);
+ }
+ })
+ .catch((xhr) => {
+ try {
+ const err = xhr.response.data;
+ if (err.success === 0) {
+ alertify.error(err.errormsg);
+ }
+ } catch (e) {
+ console.warn(e.stack || e);
+ }
+ treeNode.unload(tree);
+ });
+}
+
+function retrieveTreeNode(args, tree) {
+ const input = args || {};
+ const domElementIdentifier = input.item || tree.selected();
+ return tree.findNodeByDomElement(domElementIdentifier);
+}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
index d440bf04..9a021745 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
@@ -1,13 +1,17 @@
define('pgadmin.node.table', [
+ 'pgadmin.tables.js/enable_disable_triggers',
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.backgrid',
'pgadmin.tables.js/show_advanced_tab',
+ 'sources/menu/can_create',
+
'pgadmin.browser.collection', 'pgadmin.node.column',
'pgadmin.node.constraints', 'pgadmin.browser.table.partition.utils',
], function(
+ tableFunctions,
gettext, url_for, $, _, S, pgAdmin, pgBrowser, Alertify, Backform, Backgrid,
- ShowAdvancedTab
+ ShowAdvancedTab, canCreate
) {
if (!pgBrowser.Nodes['coll-table']) {
@@ -26,7 +30,6 @@ define('pgadmin.node.table', [
if (!pgBrowser.Nodes['table']) {
pgBrowser.Nodes['table'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'table',
label: gettext('Table'),
collection_type: 'coll-table',
@@ -118,46 +121,21 @@ define('pgadmin.node.table', [
callbacks: {
/* Enable trigger(s) on table */
enable_triggers_on_table: function(args) {
- var params = {'enable': true };
- this.callbacks.set_triggers.apply(this, [args, params]);
+ tableFunctions.enableTriggers(
+ pgBrowser.treeMenu,
+ Alertify,
+ this.generate_url.bind(this),
+ args
+ );
},
/* Disable trigger(s) on table */
disable_triggers_on_table: function(args) {
- var params = {'enable': false };
- this.callbacks.set_triggers.apply(this, [args, params]);
- },
- set_triggers: function(args, params) {
- // This function will send request to enable or
- // disable triggers on table level
- var input = args || {},
- obj = this,
- t = pgBrowser.tree,
- i = input.item || t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined;
- if (!d)
- return false;
-
- $.ajax({
- url: obj.generate_url(i, 'set_trigger' , d, true),
- type:'PUT',
- data: params,
- dataType: 'json',
- success: function(res) {
- if (res.success == 1) {
- Alertify.success(res.info);
- t.unload(i);
- t.setInode(i);
- t.deselect(i);
- setTimeout(function() {
- t.select(i);
- }, 10);
- }
- },
- error: function(xhr, status, error) {
- Alertify.pgRespErrorNotify(xhr, error);
- t.unload(i);
- },
- });
+ tableFunctions.disableTriggers(
+ pgBrowser.treeMenu,
+ Alertify,
+ this.generate_url.bind(this),
+ args
+ );
},
/* Truncate table */
truncate_table: function(args) {
@@ -1300,32 +1278,7 @@ define('pgadmin.node.table', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-table' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-table', item, data);
},
// Check to whether table has disable trigger(s)
canCreate_with_trigger_enable: function(itemData, item, data) {
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js
index a2c27188..4c25e3ea 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js
@@ -29,14 +29,12 @@ define('pgadmin.node.trigger', [
node: 'trigger',
label: gettext('Triggers'),
type: 'coll-trigger',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
columns: ['name', 'description'],
});
}
if (!pgBrowser.Nodes['trigger']) {
pgAdmin.Browser.Nodes['trigger'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table', 'view', 'partition'],
collection_type: ['coll-table', 'coll-view'],
type: 'trigger',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.js
index c1c24861..91ce615f 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.js
@@ -1,8 +1,9 @@
define('pgadmin.node.type', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
- 'pgadmin.backgrid', 'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, Backgrid) {
+ 'pgadmin.backgrid', 'sources/menu/can_create', 'pgadmin.browser.collection',
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, Backgrid,
+ canCreate) {
if (!pgBrowser.Nodes['coll-type']) {
pgBrowser.Nodes['coll-type'] =
@@ -912,32 +913,7 @@ define('pgadmin.node.type', [
},
}),
canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-type' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-type', item, data);
},
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.js
index 073ef5cb..5791cdcd 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.js
@@ -1,8 +1,11 @@
define('pgadmin.node.mview', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.alertifyjs', 'pgadmin.browser',
- 'pgadmin.backform', 'pgadmin.browser.server.privilege',
-], function(gettext, url_for, $, _, pgAdmin, Alertify, pgBrowser, Backform) {
+ 'pgadmin.backform',
+ 'sources/menu/can_create',
+ 'pgadmin.browser.server.privilege',
+], function(gettext, url_for, $, _, pgAdmin, Alertify, pgBrowser, Backform,
+ canCreate) {
/**
Create and add a view collection into nodes
@@ -241,37 +244,7 @@ define('pgadmin.node.mview', [
and hide for system view in catalogs.
*/
canCreate: function(itemData, item, data) {
-
- // If check is false then, we will allow create menu
- if (data && data.check === false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
-
- // To iterate over tree to check parent node
- while (i) {
-
- // If it is schema then allow user to create view
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-mview' == d._type) {
-
- // Check if we are not child of view
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
-
- // by default we do not want to allow create menu
- return true;
+ return canCreate.canCreate(pgBrowser, 'coll-mview', item, data);
},
refresh_mview: function(args) {
var input = args || {},
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js
index 5755a509..e4258188 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js
@@ -1,9 +1,12 @@
define('pgadmin.node.view', [
'sources/gettext',
'sources/url_for', 'jquery', 'underscore', 'sources/pgadmin',
- 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.browser.server.privilege',
+ 'pgadmin.browser', 'pgadmin.backform',
+ 'sources/menu/can_create',
+ 'pgadmin.browser.server.privilege',
'pgadmin.node.rule',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform) {
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform,
+ canCreate) {
/**
Create and add a view collection into nodes
@@ -203,38 +206,7 @@ define('pgadmin.node.view', [
and hide for system view in catalogs.
*/
canCreate: function(itemData, item, data) {
-
- // If check is false then, we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
-
- // To iterate over tree to check parent node
- while (i) {
-
- // If it is schema then allow user to create view
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-view' == d._type) {
-
- // Check if we are not child of view
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
-
- // by default we do not want to allow create menu
- return true;
-
+ return canCreate.canCreate(pgBrowser, 'coll-view', item, data);
},
});
}
diff --git a/web/pgadmin/browser/static/js/node.js b/web/pgadmin/browser/static/js/node.js
index ad582483..3baf554a 100644
--- a/web/pgadmin/browser/static/js/node.js
+++ b/web/pgadmin/browser/static/js/node.js
@@ -1,9 +1,12 @@
define('pgadmin.browser.node', [
+ 'sources/tree/pgadmin_tree_node',
'sources/gettext', 'jquery', 'underscore', 'underscore.string', 'sources/pgadmin',
'pgadmin.browser.menu', 'backbone', 'pgadmin.alertifyjs', 'pgadmin.browser.datamodel',
'backform', 'sources/browser/generate_url', 'sources/utils', 'pgadmin.browser.utils',
'pgadmin.backform',
-], function(gettext, $, _, S, pgAdmin, Menu, Backbone, Alertify, pgBrowser, Backform, generateUrl, commonUtils) {
+], function(
+ pgadminTreeNode,
+ gettext, $, _, S, pgAdmin, Menu, Backbone, Alertify, pgBrowser, Backform, generateUrl, commonUtils) {
var wcDocker = window.wcDocker,
keyCode = {
@@ -1566,7 +1569,6 @@ define('pgadmin.browser.node', [
* depends, statistics
*/
generate_url: function(item, type, d, with_id, info) {
-
var opURL = {
'create': 'obj',
'drop': 'obj',
@@ -1608,24 +1610,7 @@ define('pgadmin.browser.node', [
Collection: pgBrowser.DataCollection,
// Base class for Node Data Model
Model: pgBrowser.DataModel,
- getTreeNodeHierarchy: function(i) {
- var idx = 0,
- res = {},
- t = pgBrowser.tree,
- d;
- do {
- d = t.itemData(i);
- if (d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId) {
- res[d._type] = _.extend({}, d, {
- 'priority': idx,
- });
- idx -= 1;
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- } while (i);
-
- return res;
- },
+ getTreeNodeHierarchy: pgadminTreeNode.getTreeNodeHierarchyFromIdentifier.bind(pgBrowser),
cache: function(url, node_info, level, data) {
var cached = this.cached = this.cached || {},
hash = url,
diff --git a/web/pgadmin/static/js/alertify/dialog.js b/web/pgadmin/static/js/alertify/dialog.js
new file mode 100644
index 00000000..5a8646f9
--- /dev/null
+++ b/web/pgadmin/static/js/alertify/dialog.js
@@ -0,0 +1,129 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../gettext';
+import {sprintf} from 'sprintf-js';
+import {DialogFactory} from './dialog_factory';
+import Backform from '../backform.pgadmin';
+import {getTreeNodeHierarchyFromIdentifier} from '../tree/pgadmin_tree_node';
+
+/**
+ * This class can be extended to create new dialog boxes.
+ * Examples of this can be found in:
+ * `web/pgadmin/static/js/backup/backup_dialog.js`
+ *
+ * Do not forget to add the new Dialog type to the `DialogFactory`
+ */
+export class Dialog {
+ constructor(errorAlertTitle,
+ dialogContainerSelector,
+ pgBrowser, $, alertify, DialogModel,
+ backform = Backform) {
+ this.errorAlertTitle = errorAlertTitle;
+ this.alertify = alertify;
+ this.pgBrowser = pgBrowser;
+ this.jquery = $;
+ this.dialogModel = DialogModel;
+ this.backform = backform;
+ this.dialogContainerSelector = dialogContainerSelector;
+ }
+
+ retrieveAncestorOfTypeServer(item) {
+ let serverInformation = null;
+ let aciTreeItem = item || this.pgBrowser.treeMenu.selected();
+ let treeNode = this.pgBrowser.treeMenu.findNodeByDomElement(aciTreeItem);
+
+ while (treeNode) {
+ const node_data = treeNode.getData();
+ if (node_data._type === 'server') {
+ serverInformation = node_data;
+ break;
+ }
+
+ if (treeNode.hasParent()) {
+ treeNode = treeNode.parent();
+ } else {
+ this.alertify.alert(
+ gettext(this.errorAlertTitle),
+ gettext('Please select server or child node from the browser tree.')
+ );
+ break;
+ }
+ }
+ return serverInformation;
+ }
+
+ hasBinariesConfiguration(serverInformation) {
+ const module = 'paths';
+ let preference_name = 'pg_bin_dir';
+ let msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
+
+ if ((serverInformation.type && serverInformation.type === 'ppas') ||
+ serverInformation.server_type === 'ppas') {
+ preference_name = 'ppas_bin_dir';
+ msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
+ }
+ const preference = this.pgBrowser.get_preference(module, preference_name);
+
+ if (preference) {
+ if (!preference.value) {
+ this.alertify.alert(gettext('Configuration required'), msg);
+ return false;
+ }
+ } else {
+ this.alertify.alert(
+ gettext(this.errorAlertTitle),
+ sprintf(gettext('Failed to load preference %s of module %s'), preference_name, module)
+ );
+ return false;
+ }
+ return true;
+ }
+
+ dialogName() {
+ return undefined;
+ }
+
+ createOrGetDialog(dialogTitle, typeOfDialog) {
+ const dialogName = this.dialogName(typeOfDialog);
+
+ if (!this.alertify[dialogName]) {
+ const self = this;
+ this.alertify.dialog(dialogName, function factory() {
+ return self.dialogFactory(dialogTitle, typeOfDialog);
+ });
+ }
+ return this.alertify[dialogName];
+ }
+
+ dialogFactory(dialogTitle, typeOfDialog) {
+ const factory = new DialogFactory(
+ this.pgBrowser,
+ this.jquery,
+ this.alertify,
+ this.dialogModel,
+ this.backform,
+ this.dialogContainerSelector);
+ return factory.create(dialogTitle, typeOfDialog);
+ }
+
+ canExecuteOnCurrentDatabase(aciTreeItem) {
+ const treeInfo = getTreeNodeHierarchyFromIdentifier.apply(this.pgBrowser, [aciTreeItem]);
+
+ if (treeInfo.database && treeInfo.database._label.indexOf('=') >= 0) {
+ this.alertify.alert(
+ gettext(this.errorAlertTitle),
+ gettext('Databases with = symbols in the name cannot be backed up or restored using this utility.')
+ );
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/web/pgadmin/static/js/alertify/dialog_factory.js b/web/pgadmin/static/js/alertify/dialog_factory.js
new file mode 100644
index 00000000..500140b8
--- /dev/null
+++ b/web/pgadmin/static/js/alertify/dialog_factory.js
@@ -0,0 +1,52 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import * as BackupDialog from '../../../tools/backup/static/js/backup_dialog_wrapper';
+import {RestoreDialogWrapper} from '../../../tools/restore/static/js/restore_dialog_wrapper';
+
+export class DialogFactory {
+ constructor(pgBrowser, $,
+ alertify, DialogModel,
+ backform, dialogContainerSelector) {
+ this.pgBrowser = pgBrowser;
+ this.jquery = $;
+ this.alertify = alertify;
+ this.dialogModel = DialogModel;
+ this.backform = backform;
+ this.dialogContainerSelector = dialogContainerSelector;
+ }
+
+ create(dialogTitle, typeOfDialog) {
+ if (typeOfDialog === 'restore') {
+ return this.createRestoreDialog(dialogTitle, typeOfDialog);
+ } else {
+ return this.createBackupDialog(dialogTitle, typeOfDialog);
+ }
+ }
+
+ createRestoreDialog(dialogTitle, typeOfDialog) {
+ return new RestoreDialogWrapper(
+ this.dialogContainerSelector, dialogTitle, typeOfDialog,
+ this.jquery,
+ this.pgBrowser,
+ this.alertify,
+ this.dialogModel,
+ this.backform);
+ }
+
+ createBackupDialog(dialogTitle, typeOfDialog) {
+ return new BackupDialog.BackupDialogWrapper(
+ this.dialogContainerSelector, dialogTitle, typeOfDialog,
+ this.jquery,
+ this.pgBrowser,
+ this.alertify,
+ this.dialogModel,
+ this.backform);
+ }
+}
diff --git a/web/pgadmin/static/js/alertify/dialog_wrapper.js b/web/pgadmin/static/js/alertify/dialog_wrapper.js
new file mode 100644
index 00000000..b5ff8204
--- /dev/null
+++ b/web/pgadmin/static/js/alertify/dialog_wrapper.js
@@ -0,0 +1,57 @@
+import * as commonUtils from '../utils';
+
+export class DialogWrapper {
+ constructor(
+ dialogContainerSelector, dialogTitle, jquery, pgBrowser,
+ alertify, dialogModel, backform) {
+ this.hooks = {
+ onclose: function () {
+ if (this.view) {
+ this.view.remove({
+ data: true,
+ internal: true,
+ silent: true,
+ });
+ }
+ },
+ };
+ this.dialogContainerSelector = dialogContainerSelector;
+ this.dialogTitle = dialogTitle;
+ this.jquery = jquery;
+ this.pgBrowser = pgBrowser;
+ this.alertify = alertify;
+ this.dialogModel = dialogModel;
+ this.backform = backform;
+ }
+
+ build() {
+ this.alertify.pgDialogBuild.apply(this);
+ }
+
+ wasHelpButtonPressed(e) {
+ return e.button.element.name === 'dialog_help'
+ || e.button.element.name === 'object_help';
+ }
+
+ getSelectedNodeData(selectedTreeNode) {
+ if (!this.isNodeSelected(selectedTreeNode)) {
+ return undefined;
+ }
+ const treeNodeData = selectedTreeNode.getData();
+ if (treeNodeData) {
+ return treeNodeData;
+ }
+ return undefined;
+ }
+
+ focusOnDialog(dialog) {
+ dialog.$el.attr('tabindex', -1);
+ this.pgBrowser.keyboardNavigation.getDialogTabNavigator(dialog);
+ const container = dialog.$el.find('.tab-content:first > .tab-pane.active:first');
+ commonUtils.findAndSetFocus(container);
+ }
+
+ isNodeSelected(selectedTreeNode) {
+ return selectedTreeNode;
+ }
+}
diff --git a/web/pgadmin/static/js/menu/can_create.js b/web/pgadmin/static/js/menu/can_create.js
new file mode 100644
index 00000000..476be437
--- /dev/null
+++ b/web/pgadmin/static/js/menu/can_create.js
@@ -0,0 +1,37 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+/////////////////////////////////////////////////////////////
+
+export function canCreate(pgBrowser, childOfCatalogType, item, data) {
+ //If check is false then , we will allow create menu
+ if (data && data.check === false) {
+ return true;
+ }
+
+ let node = pgBrowser.treeMenu.findNodeByDomElement(item);
+
+ if (node.anyFamilyMember(parentCatalogOfTableChild.bind(null, childOfCatalogType))) {
+ return false;
+ }
+
+ return true;
+}
+
+function parentCatalogOfTableChild(arg, node) {
+ if (arg === node.getData()._type) {
+ if (node.hasParent()) {
+
+ let parent = node.parent();
+ if ('catalog' === parent.getData()._type) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
diff --git a/web/pgadmin/static/js/menu/menu_enabled.js b/web/pgadmin/static/js/menu/menu_enabled.js
new file mode 100644
index 00000000..05ff0f4a
--- /dev/null
+++ b/web/pgadmin/static/js/menu/menu_enabled.js
@@ -0,0 +1,52 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+function isNodeTypeSupported(backupSupportedNodes, nodeDataType, treeNode) {
+ return _.indexOf(backupSupportedNodes, nodeDataType) !== -1
+ && ancestorWithTypeCatalogDoesNotExists(treeNode);
+}
+
+export function isProvidedDataValid(treeNodeData) {
+ return !_.isUndefined(treeNodeData) && !_.isNull(treeNodeData);
+}
+
+function doesNodeHaveMenu(treeNodeData) {
+ return (treeNodeData._type === 'database' && treeNodeData.allowConn)
+ || treeNodeData._type !== 'database';
+}
+
+function ancestorWithTypeCatalogDoesNotExists(treeNode) {
+ let currentNode = treeNode;
+
+ while(currentNode.hasParent() && treeNode.parent().getData() !== null) {
+ if(currentNode.parent().getData()._type === 'catalog') {
+ return false;
+ }
+
+ currentNode = currentNode.parent();
+ }
+
+ return true;
+}
+
+export function menuEnabled(tree, backupSupportedNodes, treeNodeData, domTreeNode) {
+ let treeNode = tree.findNodeByDomElement(domTreeNode);
+ if (!treeNode) {
+ return false;
+ }
+
+ if (isProvidedDataValid(treeNodeData)) {
+ return isNodeTypeSupported(backupSupportedNodes, treeNodeData._type, treeNode)
+ && doesNodeHaveMenu(treeNodeData);
+ } else {
+ return false;
+ }
+}
+
+
diff --git a/web/pgadmin/static/js/tree/pgadmin_tree_node.js b/web/pgadmin/static/js/tree/pgadmin_tree_node.js
new file mode 100644
index 00000000..00f10d24
--- /dev/null
+++ b/web/pgadmin/static/js/tree/pgadmin_tree_node.js
@@ -0,0 +1,72 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+/**
+ * This method received pgBrowser and new TreeNode object
+ *
+ * This method retrieves all the data that exists in the tree node and in
+ * `pgBrowser.Nodes` for all the parent node of the provided node.
+ *
+ * The 2 condition to get the information from pgBrowser.Nodes are:
+ * 1 - the variable _type of the tree node
+ * 2 - the presence of hasId in the pgBrowser.Nodes for the specific node
+ *
+ * Number 2 is used to ignore coll-* nodes as they do not add any useful
+ * information
+ */
+export function getTreeNodeHierarchyFromElement(pgBrowser, treeNode) {
+ return getTreeNodeHierarchy.call(pgBrowser, treeNode);
+}
+
+/**
+ * This method received an ACI Tree JQuery node
+ *
+ * NOTE: this function need to be called on pgBrowser instance.
+ * getTreeNodeHierarchyFromIdentifier.apply(pgBrowser, [aciTreeNodeIdentifier])
+ *
+ * This method retrieves all the data that exists in the tree node and in
+ * `pgBrowser.Nodes` for all the parent node of the provided node.
+ *
+ * The 2 condition to get the information from pgBrowser.Nodes are:
+ * 1 - the variable _type of the tree node
+ * 2 - the presence of hasId in the pgBrowser.Nodes for the specific node
+ *
+ * Number 2 is used to ignore coll-* nodes as they do not add any useful
+ * information
+ */
+export function getTreeNodeHierarchyFromIdentifier(aciTreeNodeIdentifier) {
+ let identifier = this.treeMenu.translateTreeNodeIdFromACITree(aciTreeNodeIdentifier);
+ let currentNode = this.treeMenu.findNode(identifier);
+ return getTreeNodeHierarchy.call(this, currentNode);
+}
+
+export function getTreeNodeHierarchy(currentNode) {
+ let idx = 0;
+ let result = {};
+
+ do {
+ const currentNodeData = currentNode.getData();
+ if (currentNodeData._type in this.Nodes && this.Nodes[currentNodeData._type].hasId) {
+ const nodeType = mapType(currentNodeData._type);
+ if (result[nodeType] === undefined) {
+ result[nodeType] = _.extend({}, currentNodeData, {
+ 'priority': idx,
+ });
+ idx -= 1;
+ }
+ }
+ currentNode = currentNode.hasParent() ? currentNode.parent() : null;
+ } while (currentNode);
+
+ return result;
+}
+
+function mapType(type) {
+ return type === 'partition' ? 'table' : type;
+}
diff --git a/web/pgadmin/tools/backup/static/js/backup.js b/web/pgadmin/tools/backup/static/js/backup.js
index 1b0b3628..d74ed6dd 100644
--- a/web/pgadmin/tools/backup/static/js/backup.js
+++ b/web/pgadmin/tools/backup/static/js/backup.js
@@ -3,9 +3,12 @@ define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'pgadmin.alertifyjs', 'backbone', 'pgadmin.backgrid',
'pgadmin.backform', 'pgadmin.browser', 'sources/utils',
+ 'tools/backup/static/js/menu_utils',
+ 'tools/backup/static/js/backup_dialog',
+ 'sources/menu/menu_enabled',
], function(
gettext, url_for, $, _, S, alertify, Backbone, Backgrid, Backform, pgBrowser,
-commonUtils
+commonUtils, menuUtils, globalBackupDialog, menuEnabled
) {
// if module is already initialized, refer to that.
@@ -394,48 +397,6 @@ commonUtils
this.initialized = true;
- // Define list of nodes on which backup context menu option appears
- var backup_supported_nodes = [
- 'database', 'schema', 'table', 'partition',
- ];
-
- /**
- Enable/disable backup menu in tools based
- on node selected
- if selected node is present in supported_nodes,
- menu will be enabled otherwise disabled.
- Also, hide it for system view in catalogs
- */
- var menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData,
- parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
-
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(backup_supported_nodes, d._type) !== -1 &&
- parent_data._type != 'catalog') {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
- var menu_enabled_server = function(itemData) {
- // If server node selected && connected
- if (!_.isUndefined(itemData) && !_.isNull(itemData))
- return (('server' === itemData._type) && itemData.connected);
- else
- return false;
- };
-
// Define the nodes on which the menus to be appear
var menus = [{
name: 'backup_global',
@@ -445,7 +406,7 @@ commonUtils
priority: 12,
label: gettext('Backup Globals...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_server',
module: this,
@@ -454,7 +415,7 @@ commonUtils
priority: 12,
label: gettext('Backup Server...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_global_ctx',
module: this,
@@ -464,7 +425,7 @@ commonUtils
priority: 12,
label: gettext('Backup Globals...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_server_ctx',
module: this,
@@ -474,7 +435,7 @@ commonUtils
priority: 12,
label: gettext('Backup Server...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_object',
module: this,
@@ -483,20 +444,22 @@ commonUtils
priority: 11,
label: gettext('Backup...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null,
+ pgBrowser.treeMenu, menuUtils.backupSupportedNodes),
}];
- for (var idx = 0; idx < backup_supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.backupSupportedNodes.length; idx++) {
menus.push({
- name: 'backup_' + backup_supported_nodes[idx],
- node: backup_supported_nodes[idx],
+ name: 'backup_' + menuUtils.backupSupportedNodes[idx],
+ node: menuUtils.backupSupportedNodes[idx],
module: this,
applies: ['context'],
callback: 'backup_objects',
priority: 11,
label: gettext('Backup...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null,
+ pgBrowser.treeMenu, menuUtils.backupSupportedNodes),
});
}
@@ -521,542 +484,25 @@ commonUtils
},
// Callback to draw Backup Dialog for globals/server
- start_backup_global_server: function(action, item, params) {
- var i = item || pgBrowser.tree.selected(),
- server_data = null;
-
- while (i) {
- var node_data = pgBrowser.tree.itemData(i);
- if (node_data._type == 'server') {
- server_data = node_data;
- break;
- }
-
- if (pgBrowser.tree.hasParent(i)) {
- i = $(pgBrowser.tree.parent(i));
- } else {
- alertify.alert(
- gettext('Backup Error'),
- gettext('Please select server or child node from the browser tree.')
- );
- break;
- }
- }
-
- if (!server_data) {
- return;
- }
-
- var module = 'paths',
- preference_name = 'pg_bin_dir',
- msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
-
- if ((server_data.type && server_data.type == 'ppas') ||
- server_data.server_type == 'ppas') {
- preference_name = 'ppas_bin_dir';
- msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
- }
-
- var preference = pgBrowser.get_preference(module, preference_name);
-
- if (preference) {
- if (!preference.value) {
- alertify.alert(gettext('Configuration required'), msg);
- return;
- }
- } else {
- alertify.alert(
- gettext('Backup Error'),
- S(gettext('Failed to load preference %s of module %s')).sprintf(preference_name, module).value()
- );
- return;
- }
-
- var of_type = undefined;
-
- // Set Notes according to type of backup
- if (!_.isUndefined(params['globals']) && params['globals']) {
- of_type = 'globals';
- } else {
- of_type = 'server';
- }
-
- var DialogName = 'BackupDialog_' + of_type,
- DialogTitle = ((of_type == 'globals') ?
- gettext('Backup Globals...') :
- gettext('Backup Server...'));
-
- if (!alertify[DialogName]) {
- alertify.dialog(DialogName, function factory() {
- return {
- main: function(title) {
- this.set('title', title);
- },
- build: function() {
- alertify.pgDialogBuild.apply(this);
- },
- setup: function() {
- return {
- buttons: [{
- text: '',
- className: 'btn btn-default pull-left fa fa-lg fa-info',
- attrs: {
- name: 'object_help',
- type: 'button',
- url: 'backup.html',
- label: gettext('Backup'),
- },
- }, {
- text: '',
- key: 112,
- className: 'btn btn-default pull-left fa fa-lg fa-question',
- attrs: {
- name: 'dialog_help',
- type: 'button',
- label: gettext('Backup'),
- url: url_for('help.static', {
- 'filename': 'backup_dialog.html',
- }),
- },
- }, {
- text: gettext('Backup'),
- key: 13,
- className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
- 'data-btn-name': 'backup',
- }, {
- text: gettext('Cancel'),
- key: 27,
- className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
- 'data-btn-name': 'cancel',
- }],
- // Set options for dialog
- options: {
- title: DialogTitle,
- //disable both padding and overflow control.
- padding: !1,
- overflow: !1,
- model: 0,
- resizable: true,
- maximizable: true,
- pinnable: false,
- closableByDimmer: false,
- modal: false,
- },
- };
- },
- hooks: {
- // Triggered when the dialog is closed
- onclose: function() {
- if (this.view) {
- // clear our backform model/view
- this.view.remove({
- data: true,
- internal: true,
- silent: true,
- });
- }
- },
- },
- prepare: function() {
- var self = this;
- // Disable Backup button until user provides Filename
- this.__internal.buttons[2].element.disabled = true;
-
- var $container = $('<div class=\'backup_dialog\'></div>');
- // Find current/selected node
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (!d)
- return;
- // Create treeInfo
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
- // Instance of backbone model
- var newModel = new BackupModel({
- type: of_type,
- }, {
- node_info: treeInfo,
- }),
- fields = Backform.generateViewSchema(
- treeInfo, newModel, 'create', node, treeInfo.server, true
- );
-
- var view = this.view = new Backform.Dialog({
- el: $container,
- model: newModel,
- schema: fields,
- });
- // Add our class to alertify
- $(this.elements.body.childNodes[0]).addClass(
- 'alertify_tools_dialog_properties obj_properties'
- );
- // Render dialog
- view.render();
-
- this.elements.content.appendChild($container.get(0));
-
- var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
- commonUtils.findAndSetFocus(container);
-
- // Listen to model & if filename is provided then enable Backup button
- this.view.model.on('change', function() {
- if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
- this.errorModel.clear();
- self.__internal.buttons[2].element.disabled = false;
- } else {
- self.__internal.buttons[2].element.disabled = true;
- this.errorModel.set('file', gettext('Please provide a filename'));
- }
- });
- },
- // Callback functions when click on the buttons of the Alertify dialogs
- callback: function(e) {
- // Fetch current server id
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (e.button.element.name == 'dialog_help' || e.button.element.name == 'object_help') {
- e.cancel = true;
- pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
- node, i, e.button.element.getAttribute('label'));
- return;
- }
-
- if (e.button['data-btn-name'] === 'backup') {
-
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- var self = this,
- baseUrl = url_for('backup.create_server_job', {
- 'sid': treeInfo.server._id,
- }),
- args = this.view.model.toJSON();
-
- $.ajax({
- url: baseUrl,
- method: 'POST',
- data: {
- 'data': JSON.stringify(args),
- },
- success: function(res) {
- if (res.success) {
- alertify.success(gettext('Backup job created.'), 5);
- pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
- } else {
- console.warn(res);
- }
- },
- error: function(xhr) {
- try {
- var err = $.parseJSON(xhr.responseText);
- alertify.alert(
- gettext('Backup job failed.'),
- err.errormsg
- );
- } catch (e) {
- console.warn(e.stack || e);
- }
- },
- });
- }
- },
- };
- });
- }
- alertify[DialogName](true).resizeTo('60%', '50%');
+ start_backup_global_server: function(action, treeItem, params) {
+ let dialog = new globalBackupDialog.BackupDialog(
+ pgBrowser,
+ $,
+ alertify,
+ BackupModel
+ );
+ dialog.draw(action, treeItem, params);
},
// Callback to draw Backup Dialog for objects
backup_objects: function(action, treeItem) {
-
- var i = treeItem || pgBrowser.tree.selected(),
- server_data = null;
-
- while (i) {
- var node_data = pgBrowser.tree.itemData(i);
- if (node_data._type == 'server') {
- server_data = node_data;
- break;
- }
-
- if (pgBrowser.tree.hasParent(i)) {
- i = $(pgBrowser.tree.parent(i));
- } else {
- alertify.alert(
- gettext('Backup Error'),
- gettext('Please select server or child node from tree.')
- );
- break;
- }
- }
-
- if (!server_data) {
- return;
- }
-
- var module = 'paths',
- preference_name = 'pg_bin_dir',
- msg = gettext('Please set binary path for PostgreSQL Server from preferences.');
-
- if ((server_data.type && server_data.type == 'ppas') ||
- server_data.server_type == 'ppas') {
- preference_name = 'ppas_bin_dir';
- msg = gettext('Please set binary path for EDB Postgres Advanced Server from preferences.');
- }
-
- var preference = pgBrowser.get_preference(module, preference_name);
-
- if (preference) {
- if (!preference.value) {
- alertify.alert(gettext('Configuration required'), msg);
- return;
- }
- } else {
- alertify.alert(
- gettext('Backup Error'),
- S(gettext('Failed to load preference %s of module %s')).sprintf(preference_name, module).value()
- );
- return;
- }
-
- var title = S(gettext('Backup (%s: %s)')),
- tree = pgBrowser.tree,
- item = treeItem || tree.selected(),
- data = item && item.length == 1 && tree.itemData(item),
- node = data && data._type && pgBrowser.Nodes[data._type];
-
- if (!node)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [item]);
-
- if (treeInfo.database._label.indexOf('=') >= 0) {
- alertify.alert(
- gettext('Backup error'),
- gettext('Backup job creation failed. '+
- 'Databases with = symbols in the name cannot be backed up using this utility.')
- );
- return;
- }
-
- title = title.sprintf(node.label, data.label).value();
-
- if (!alertify.backup_objects) {
- // Create Dialog title on the fly with node details
- alertify.dialog('backup_objects', function factory() {
- return {
- main: function(title) {
- this.set('title', title);
- },
- build: function() {
- alertify.pgDialogBuild.apply(this);
- },
- setup: function() {
- return {
- buttons: [{
- text: '',
- className: 'btn btn-default pull-left fa fa-lg fa-info',
- attrs: {
- name: 'object_help',
- type: 'button',
- url: 'backup.html',
- label: gettext('Backup'),
- },
- }, {
- text: '',
- key: 112,
- className: 'btn btn-default pull-left fa fa-lg fa-question',
- attrs: {
- name: 'dialog_help',
- type: 'button',
- label: gettext('Backup'),
- url: url_for('help.static', {
- 'filename': 'backup_dialog.html',
- }),
- },
- }, {
- text: gettext('Backup'),
- key: 13,
- className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
- 'data-btn-name': 'backup',
- }, {
- text: gettext('Cancel'),
- key: 27,
- className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
- 'data-btn-name': 'cancel',
- }],
- // Set options for dialog
- options: {
- title: title,
- //disable both padding and overflow control.
- padding: !1,
- overflow: !1,
- model: 0,
- resizable: true,
- maximizable: true,
- pinnable: false,
- closableByDimmer: false,
- modal: false,
- },
- };
- },
- hooks: {
- // triggered when the dialog is closed
- onclose: function() {
- if (this.view) {
- this.view.remove({
- data: true,
- internal: true,
- silent: true,
- });
- }
- },
- },
- prepare: function() {
- var self = this;
- // Disable Backup button until user provides Filename
- this.__internal.buttons[2].element.disabled = true;
- var $container = $('<div class=\'backup_dialog\'></div>');
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- var newModel = new BackupObjectModel({}, {
- node_info: treeInfo,
- }),
- fields = Backform.generateViewSchema(
- treeInfo, newModel, 'create', node, treeInfo.server, true
- );
-
- var view = this.view = new Backform.Dialog({
- el: $container,
- model: newModel,
- schema: fields,
- });
-
- $(this.elements.body.childNodes[0]).addClass(
- 'alertify_tools_dialog_properties obj_properties'
- );
-
- view.render();
-
- this.elements.content.appendChild($container.get(0));
-
- if(view) {
- view.$el.attr('tabindex', -1);
- // var dialogTabNavigator = pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
- commonUtils.findAndSetFocus(container);
- }
- // Listen to model & if filename is provided then enable Backup button
- this.view.model.on('change', function() {
- if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
- this.errorModel.clear();
- self.__internal.buttons[2].element.disabled = false;
- } else {
- self.__internal.buttons[2].element.disabled = true;
- this.errorModel.set('file', gettext('Please provide filename'));
- }
- });
-
- },
- // Callback functions when click on the buttons of the Alertify dialogs
- callback: function(e) {
- // Fetch current server id
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (e.button.element.name == 'dialog_help' || e.button.element.name == 'object_help') {
- e.cancel = true;
- pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
- node, i, e.button.element.getAttribute('label'));
- return;
- }
-
- if (e.button['data-btn-name'] === 'backup') {
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- // Set current database into model
- this.view.model.set('database', treeInfo.database._label);
-
- // We will remove once object tree is implemented
- // If selected node is Schema then add it in model
- if (d._type == 'schema') {
- var schemas = [];
- schemas.push(d._label);
- this.view.model.set('schemas', schemas);
- }
- // If selected node is Table then add it in model along with
- // its schema
- if (d._type == 'table') {
- this.view.model.set(
- 'tables', [
- [treeInfo.schema._label, d._label],
- ]
- );
- }
-
- // Remove ratio attribute from model if it has empty string.
- // The valid value can be between 0 to 9.
- if (_.isEmpty(this.view.model.get('ratio'))) {
- this.view.model.unset('ratio');
- }
-
- var self = this,
- baseUrl = url_for('backup.create_object_job', {
- 'sid': treeInfo.server._id,
- }),
- args = this.view.model.toJSON();
-
- $.ajax({
- url: baseUrl,
- method: 'POST',
- data: {
- 'data': JSON.stringify(args),
- },
- success: function(res) {
- if (res.success) {
- alertify.success(gettext('Backup job created.'), 5);
- pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
- }
- },
- error: function(xhr) {
- try {
- var err = $.parseJSON(xhr.responseText);
- alertify.alert(
- gettext('Backup job failed.'),
- err.errormsg
- );
- } catch (e) {
- console.warn(e.stack || e);
- }
- },
- });
- }
- },
- };
- });
- }
- alertify.backup_objects(title).resizeTo('65%', '60%');
+ let dialog = new globalBackupDialog.BackupDialog(
+ pgBrowser,
+ $,
+ alertify,
+ BackupObjectModel
+ );
+ dialog.draw(action, treeItem, null);
},
};
return pgBrowser.Backup;
diff --git a/web/pgadmin/tools/backup/static/js/backup_dialog.js b/web/pgadmin/tools/backup/static/js/backup_dialog.js
new file mode 100644
index 00000000..c74c376c
--- /dev/null
+++ b/web/pgadmin/tools/backup/static/js/backup_dialog.js
@@ -0,0 +1,69 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../../../../static/js/gettext';
+import Backform from '../../../../static/js/backform.pgadmin';
+import {Dialog} from '../../../../static/js/alertify/dialog';
+
+export class BackupDialog extends Dialog {
+ constructor(pgBrowser, $, alertify, BackupModel, backform = Backform) {
+ super('Backup Error',
+ '<div class=\'backup_dialog\'></div>',
+ pgBrowser, $, alertify, BackupModel, backform);
+ }
+
+ draw(action, aciTreeItem, params) {
+ const serverInformation = this.retrieveAncestorOfTypeServer(aciTreeItem);
+
+ if (!serverInformation) {
+ return;
+ }
+
+ if (!this.hasBinariesConfiguration(serverInformation)) {
+ return;
+ }
+
+ const typeOfDialog = BackupDialog.typeOfDialog(params);
+
+ if (!this.canExecuteOnCurrentDatabase(aciTreeItem)) {
+ return;
+ }
+
+ const dialog = this.createOrGetDialog(BackupDialog.dialogTitle(typeOfDialog),
+ typeOfDialog);
+ dialog(true).resizeTo('60%', '50%');
+ }
+
+ static typeOfDialog(params) {
+ if (params === null) {
+ return 'backup_objects';
+ }
+ let typeOfDialog = 'server';
+ if (!_.isUndefined(params['globals']) && params['globals']) {
+ typeOfDialog = 'globals';
+ }
+ return typeOfDialog;
+ }
+
+ static dialogTitle(typeOfDialog) {
+ if (typeOfDialog === 'backup_objects') {
+ return null;
+ }
+ return ((typeOfDialog === 'globals') ?
+ gettext('Backup Globals...') :
+ gettext('Backup Server...'));
+ }
+
+ dialogName(typeOfDialog) {
+ if (typeOfDialog === 'backup_objects') {
+ return typeOfDialog;
+ }
+ return 'BackupDialog_' + typeOfDialog;
+ }
+}
diff --git a/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js b/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js
new file mode 100644
index 00000000..2cebe3d1
--- /dev/null
+++ b/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js
@@ -0,0 +1,258 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {getTreeNodeHierarchyFromElement} from '../../../../static/js/tree/pgadmin_tree_node';
+import axios from 'axios/index';
+import gettext from '../../../../static/js/gettext';
+import url_for from '../../../../static/js/url_for';
+import _ from 'underscore';
+import {DialogWrapper} from '../../../../static/js/alertify/dialog_wrapper';
+
+export class BackupDialogWrapper extends DialogWrapper {
+ constructor(dialogContainerSelector, dialogTitle, typeOfDialog,
+ jquery, pgBrowser, alertify, dialogModel, backform) {
+ super(dialogContainerSelector, dialogTitle, jquery,
+ pgBrowser, alertify, dialogModel, backform);
+ this.typeOfDialog = typeOfDialog;
+ }
+
+ main(title) {
+ this.set('title', title);
+ }
+
+ setup() {
+ return {
+ buttons: [{
+ text: '',
+ className: 'btn btn-default pull-left fa fa-lg fa-info',
+ attrs: {
+ name: 'object_help',
+ type: 'button',
+ url: 'backup.html',
+ label: gettext('Backup'),
+ },
+ }, {
+ text: '',
+ key: 112,
+ className: 'btn btn-default pull-left fa fa-lg fa-question',
+ attrs: {
+ name: 'dialog_help',
+ type: 'button',
+ label: gettext('Backup'),
+ url: url_for('help.static', {
+ 'filename': 'backup_dialog.html',
+ }),
+ },
+ }, {
+ text: gettext('Backup'),
+ key: 13,
+ className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
+ 'data-btn-name': 'backup',
+ }, {
+ text: gettext('Cancel'),
+ key: 27,
+ className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
+ 'data-btn-name': 'cancel',
+ }],
+ // Set options for dialog
+ options: {
+ title: this.dialogTitle,
+ //disable both padding and overflow control.
+ padding: !1,
+ overflow: !1,
+ model: 0,
+ resizable: true,
+ maximizable: true,
+ pinnable: false,
+ closableByDimmer: false,
+ modal: false,
+ },
+ };
+ }
+
+ prepare() {
+ this.disableBackupButton();
+
+ const $container = this.jquery(this.dialogContainerSelector);
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = this.getSelectedNodeData(selectedTreeNode);
+ if (!selectedTreeNodeData) {
+ return;
+ }
+
+ const node = this.pgBrowser.Nodes[selectedTreeNodeData._type];
+ if (this.dialogTitle === null) {
+ const title = `Backup (${node.label}: ${selectedTreeNodeData.label})`;
+ this.main(title);
+ }
+
+ const treeInfo = getTreeNodeHierarchyFromElement(this.pgBrowser, selectedTreeNode);
+ const dialog = this.createDialog(node, treeInfo, this.typeOfDialog, $container);
+ this.addAlertifyClassToBackupNodeChildNodes();
+ dialog.render();
+
+ this.elements.content.appendChild($container.get(0));
+
+ this.focusOnDialog(dialog);
+ this.setListenersForFilenameChanges();
+ }
+
+ callback(event) {
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = this.getSelectedNodeData(selectedTreeNode);
+ const node = selectedTreeNodeData && this.pgBrowser.Nodes[selectedTreeNodeData._type];
+
+ if (this.wasHelpButtonPressed(event)) {
+ event.cancel = true;
+ this.pgBrowser.showHelp(
+ event.button.element.name,
+ event.button.element.getAttribute('url'),
+ node,
+ selectedTreeNode,
+ event.button.element.getAttribute('label')
+ );
+ return;
+ }
+
+ if (this.wasBackupButtonPressed(event)) {
+
+ if (!selectedTreeNodeData)
+ return;
+
+ const serverIdentifier = this.retrieveServerIdentifier(node, selectedTreeNode);
+
+ const dialog = this;
+ let urlShortcut = 'backup.create_server_job';
+ if (this.typeOfDialog === 'backup_objects') {
+ urlShortcut = 'backup.create_object_job';
+ }
+ const baseUrl = url_for(urlShortcut, {
+ 'sid': serverIdentifier,
+ });
+
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.pgBrowser,
+ selectedTreeNode
+ );
+
+ this.setExtraParameters(selectedTreeNode, treeInfo);
+
+ let service = axios.create({});
+ service.post(
+ baseUrl,
+ this.view.model.toJSON()
+ ).then(function () {
+ dialog.alertify.success(gettext('Backup job created.'), 5);
+ dialog.pgBrowser.Events.trigger('pgadmin-bgprocess:created', dialog);
+ }).catch(function (error) {
+ try {
+ const err = error.response.data;
+ dialog.alertify.alert(
+ gettext('Backup job failed.'),
+ err.errormsg
+ );
+ } catch (e) {
+ console.warn(e.stack || e);
+ }
+ });
+ }
+ }
+
+ addAlertifyClassToBackupNodeChildNodes() {
+ this.jquery(this.elements.body.childNodes[0]).addClass(
+ 'alertify_tools_dialog_properties obj_properties'
+ );
+ }
+
+ getSelectedNode() {
+ const tree = this.pgBrowser.treeMenu;
+ const selectedNode = tree.selected();
+ if (selectedNode) {
+ return tree.findNodeByDomElement(selectedNode);
+ } else {
+ return undefined;
+ }
+ }
+
+ disableBackupButton() {
+ this.__internal.buttons[2].element.disabled = true;
+ }
+
+ enableBackupButton() {
+ this.__internal.buttons[2].element.disabled = false;
+ }
+
+ createDialog(node, treeInfo, typeOfDialog, $container) {
+ let attributes = {};
+ if (typeOfDialog !== 'backup_objects') {
+ attributes['type'] = typeOfDialog;
+ }
+ // Instance of backbone model
+ const newModel = new this.dialogModel(attributes, {
+ node_info: treeInfo,
+ });
+ const fields = this.backform.generateViewSchema(
+ treeInfo, newModel, 'create', node, treeInfo.server, true
+ );
+
+ return this.view = new this.backform.Dialog({
+ el: $container,
+ model: newModel,
+ schema: fields,
+ });
+ }
+
+ retrieveServerIdentifier(node, selectedTreeNode) {
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.pgBrowser,
+ selectedTreeNode
+ );
+ return treeInfo.server._id;
+ }
+
+ setListenersForFilenameChanges() {
+ const self = this;
+
+ this.view.model.on('change', function () {
+ if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
+ this.errorModel.clear();
+ self.enableBackupButton();
+ } else {
+ self.disableBackupButton();
+ this.errorModel.set('file', gettext('Please provide a filename'));
+ }
+ });
+ }
+
+ setExtraParameters(selectedTreeNode, treeInfo) {
+ if (this.typeOfDialog === 'backup_objects') {
+
+ this.view.model.set('database', treeInfo.database._label);
+
+ const nodeData = selectedTreeNode.getData();
+ if (nodeData._type === 'schema') {
+ this.view.model.set('schemas', [nodeData._label]);
+ }
+
+ if (nodeData._type === 'table') {
+ this.view.model.set('tables', [
+ [treeInfo.schema._label, nodeData._label],
+ ]);
+ }
+
+ if (_.isEmpty(this.view.model.get('ratio'))) {
+ this.view.model.unset('ratio');
+ }
+ }
+ }
+
+ wasBackupButtonPressed(event) {
+ return event.button['data-btn-name'] === 'backup';
+ }
+}
diff --git a/web/pgadmin/tools/backup/static/js/menu_utils.js b/web/pgadmin/tools/backup/static/js/menu_utils.js
new file mode 100644
index 00000000..fdf400af
--- /dev/null
+++ b/web/pgadmin/tools/backup/static/js/menu_utils.js
@@ -0,0 +1,23 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {isProvidedDataValid} from '../../../../static/js/menu/menu_enabled';
+
+export const backupSupportedNodes = [
+ 'database', 'schema', 'table', 'partition',
+];
+
+function isNodeAServerAndConnected(treeNodeData) {
+ return (('server' === treeNodeData._type) && treeNodeData.connected);
+}
+
+export function menuEnabledServer(treeNodeData) {
+ return isProvidedDataValid(treeNodeData)
+ && isNodeAServerAndConnected(treeNodeData);
+}
diff --git a/web/pgadmin/tools/datagrid/static/js/datagrid.js b/web/pgadmin/tools/datagrid/static/js/datagrid.js
index b0ed60f6..520a9ce5 100644
--- a/web/pgadmin/tools/datagrid/static/js/datagrid.js
+++ b/web/pgadmin/tools/datagrid/static/js/datagrid.js
@@ -1,10 +1,14 @@
define('pgadmin.datagrid', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'pgadmin.alertifyjs', 'sources/pgadmin', 'bundled_codemirror',
- 'sources/sqleditor_utils', 'backbone', 'wcdocker',
+ 'sources/sqleditor_utils', 'backbone',
+ 'tools/datagrid/static/js/show_data',
+ 'tools/datagrid/static/js/get_panel_title',
+ 'tools/datagrid/static/js/show_query_tool',
+ 'wcdocker',
], function(
gettext, url_for, $, _, alertify, pgAdmin, codemirror, sqlEditorUtils,
- Backbone
+ Backbone, showData, panelTitle, showQueryTool
) {
// Some scripts do export their object in the window only.
// Generally the one, which do no have AMD support.
@@ -161,55 +165,7 @@ define('pgadmin.datagrid', [
// This is a callback function to show data when user click on menu item.
show_data_grid: function(data, i) {
- var self = this,
- d = pgAdmin.Browser.tree.itemData(i);
- if (d === undefined) {
- alertify.alert(
- gettext('Data Grid Error'),
- gettext('No object selected.')
- );
- return;
- }
-
- // Get the parent data from the tree node hierarchy.
- var node = pgBrowser.Nodes[d._type],
- parentData = node.getTreeNodeHierarchy(i);
-
- // If server, database or schema is undefined then return from the function.
- if (parentData.server === undefined || parentData.database === undefined) {
- return;
- }
- // If schema, view, catalog object all are undefined then return from the function.
- if (parentData.schema === undefined && parentData.view === undefined &&
- parentData.catalog === undefined) {
- return;
- }
-
- var nsp_name = '';
-
- if (parentData.schema != undefined) {
- nsp_name = parentData.schema.label;
- }
- else if (parentData.view != undefined) {
- nsp_name = parentData.view.label;
- }
- else if (parentData.catalog != undefined) {
- nsp_name = parentData.catalog.label;
- }
- var url_params = {
- 'cmd_type': data.mnuid,
- 'obj_type': d._type,
- 'sgid': parentData.server_group._id,
- 'sid': parentData.server._id,
- 'did': parentData.database._id,
- 'obj_id': d._id,
- };
-
- var baseUrl = url_for('datagrid.initialize_datagrid', url_params);
- var grid_title = parentData.server.label + ' - ' + parentData.database.label + ' - '
- + nsp_name + '.' + d.label;
-
- self.create_transaction(baseUrl, null, 'false', parentData.server.server_type, '', grid_title, '');
+ showData.showDataGrid(this, pgBrowser, alertify, data, i);
},
// This is a callback function to show filtered data when user click on menu item.
@@ -384,63 +340,12 @@ define('pgadmin.datagrid', [
},
get_panel_title: function() {
- // Get the parent data from the tree node hierarchy.
- var tree = pgAdmin.Browser.tree,
- selected_item = tree.selected(),
- item_data = tree.itemData(selected_item);
-
- var node = pgBrowser.Nodes[item_data._type],
- parentData = node.getTreeNodeHierarchy(selected_item);
-
- // If server, database is undefined then return from the function.
- if (parentData.server === undefined) {
- return;
- }
- // If Database is not available then use default db
- var db_label = parentData.database ? parentData.database.label
- : parentData.server.db;
-
- var grid_title = db_label + ' on ' + parentData.server.user.name + '@' +
- parentData.server.label;
- return grid_title;
+ return panelTitle.getPanelTitle(pgBrowser);
},
// This is a callback function to show query tool when user click on menu item.
- show_query_tool: function(url, i, panel_title) {
- var sURL = url || '',
- d = pgAdmin.Browser.tree.itemData(i);
-
- panel_title = panel_title || '';
- if (d === undefined) {
- alertify.alert(
- gettext('Query Tool Error'),
- gettext('No object selected.')
- );
- return;
- }
-
- // Get the parent data from the tree node hierarchy.
- var node = pgBrowser.Nodes[d._type],
- parentData = node.getTreeNodeHierarchy(i);
-
- // If server, database is undefined then return from the function.
- if (parentData.server === undefined) {
- return;
- }
-
- var url_params = {
- 'sgid': parentData.server_group._id,
- 'sid': parentData.server._id,
- },
- url_endpoint = 'datagrid.initialize_query_tool';
- // If database not present then use Maintenance database
- // We will handle this at server side
- if (parentData.database) {
- url_params['did'] = parentData.database._id;
- url_endpoint = 'datagrid.initialize_query_tool_with_did';
- }
- var baseUrl = url_for(url_endpoint, url_params);
-
- this.create_transaction(baseUrl, null, 'true', parentData.server.server_type, sURL, panel_title, '', false);
+ show_query_tool: function(url, aciTreeIdentifier, panelTitle) {
+ showQueryTool.showQueryTool(this, pgBrowser, alertify, url,
+ aciTreeIdentifier, panelTitle);
},
create_transaction: function(baseUrl, target, is_query_tool, server_type, sURL, panel_title, sql_filter, recreate) {
var self = this;
diff --git a/web/pgadmin/tools/datagrid/static/js/get_panel_title.js b/web/pgadmin/tools/datagrid/static/js/get_panel_title.js
new file mode 100644
index 00000000..64b3a3d2
--- /dev/null
+++ b/web/pgadmin/tools/datagrid/static/js/get_panel_title.js
@@ -0,0 +1,33 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {getTreeNodeHierarchyFromIdentifier} from '../../../../static/js/tree/pgadmin_tree_node';
+
+function getDatabaseLabel(parentData) {
+ return parentData.database ? parentData.database.label
+ : parentData.server.db;
+}
+
+function isServerInformationAvailable(parentData) {
+ return parentData.server === undefined;
+}
+
+export function getPanelTitle(pgBrowser) {
+ const selected_item = pgBrowser.treeMenu.selected();
+
+ const parentData = getTreeNodeHierarchyFromIdentifier
+ .call(pgBrowser, selected_item);
+ if (isServerInformationAvailable(parentData)) {
+ return;
+ }
+
+ const db_label = getDatabaseLabel(parentData);
+
+ return `${db_label} on ${parentData.server.user.name}@${parentData.server.label}`;
+}
diff --git a/web/pgadmin/tools/datagrid/static/js/show_data.js b/web/pgadmin/tools/datagrid/static/js/show_data.js
new file mode 100644
index 00000000..373c97cd
--- /dev/null
+++ b/web/pgadmin/tools/datagrid/static/js/show_data.js
@@ -0,0 +1,92 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import gettext from '../../../../static/js/gettext';
+import url_for from '../../../../static/js/url_for';
+import {getTreeNodeHierarchyFromIdentifier} from '../../../../static/js/tree/pgadmin_tree_node';
+
+export function showDataGrid(
+ datagrid,
+ pgBrowser,
+ alertify,
+ connectionData,
+ aciTreeIdentifier
+) {
+ const node = pgBrowser.treeMenu.findNodeByDomElement(aciTreeIdentifier);
+ if (node === undefined || !node.getData()) {
+ alertify.alert(
+ gettext('Data Grid Error'),
+ gettext('No object selected.')
+ );
+ return;
+ }
+
+ const parentData = getTreeNodeHierarchyFromIdentifier.call(
+ pgBrowser,
+ aciTreeIdentifier
+ );
+
+ if (hasServerOrDatabaseConfiguration(parentData)
+ || !hasSchemaOrCatalogOrViewInformation(parentData)) {
+ return;
+ }
+
+ let namespaceName = retrieveNameSpaceName(parentData);
+ const baseUrl = generateUrl(connectionData, node.getData(), parentData);
+ const grid_title = generateDatagridTitle(parentData, namespaceName, node.getData());
+
+ datagrid.create_transaction(
+ baseUrl,
+ null,
+ 'false',
+ parentData.server.server_type,
+ '',
+ grid_title,
+ ''
+ );
+}
+
+
+function retrieveNameSpaceName(parentData) {
+ if (parentData.schema !== undefined) {
+ return parentData.schema.label;
+ }
+ else if (parentData.view !== undefined) {
+ return parentData.view.label;
+ }
+ else if (parentData.catalog !== undefined) {
+ return parentData.catalog.label;
+ }
+ return '';
+}
+
+function generateUrl(connectionData, nodeData, parentData) {
+ const url_params = {
+ 'cmd_type': connectionData.mnuid,
+ 'obj_type': nodeData._type,
+ 'sgid': parentData.server_group._id,
+ 'sid': parentData.server._id,
+ 'did': parentData.database._id,
+ 'obj_id': nodeData._id,
+ };
+
+ return url_for('datagrid.initialize_datagrid', url_params);
+}
+
+function hasServerOrDatabaseConfiguration(parentData) {
+ return parentData.server === undefined || parentData.database === undefined;
+}
+
+function hasSchemaOrCatalogOrViewInformation(parentData) {
+ return parentData.schema !== undefined || parentData.view !== undefined ||
+ parentData.catalog !== undefined;
+}
+
+function generateDatagridTitle(parentData, namespaceName, nodeData) {
+ return `${parentData.server.label} - ${parentData.database.label} - ${namespaceName}.${nodeData.label}`;
+}
diff --git a/web/pgadmin/tools/datagrid/static/js/show_query_tool.js b/web/pgadmin/tools/datagrid/static/js/show_query_tool.js
new file mode 100644
index 00000000..0eb12b8b
--- /dev/null
+++ b/web/pgadmin/tools/datagrid/static/js/show_query_tool.js
@@ -0,0 +1,63 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../../../../static/js/gettext';
+import url_for from '../../../../static/js/url_for';
+import {getTreeNodeHierarchyFromIdentifier} from '../../../../static/js/tree/pgadmin_tree_node';
+
+function hasDatabaseInformation(parentData) {
+ return parentData.database;
+}
+
+function generateUrl(parentData) {
+ let url_endpoint = 'datagrid.initialize_query_tool';
+ let url_params = {
+ 'sgid': parentData.server_group._id,
+ 'sid': parentData.server._id,
+ };
+
+ if (hasDatabaseInformation(parentData)) {
+ url_params['did'] = parentData.database._id;
+ url_endpoint = 'datagrid.initialize_query_tool_with_did';
+ }
+
+ return url_for(url_endpoint, url_params);
+}
+
+function hasServerInformations(parentData) {
+ return parentData.server === undefined;
+}
+
+export function showQueryTool(datagrid, pgBrowser, alertify, url,
+ aciTreeIdentifier, panelTitle) {
+ const sURL = url || '';
+ const queryToolTitle = panelTitle || '';
+
+ const currentNode = pgBrowser.treeMenu.findNodeByDomElement(aciTreeIdentifier);
+ if (currentNode === undefined) {
+ alertify.alert(
+ gettext('Query Tool Error'),
+ gettext('No object selected.')
+ );
+ return;
+ }
+
+ const parentData = getTreeNodeHierarchyFromIdentifier.call(
+ pgBrowser, aciTreeIdentifier);
+
+ if (hasServerInformations(parentData)) {
+ return;
+ }
+
+ const baseUrl = generateUrl(parentData);
+
+ datagrid.create_transaction(
+ baseUrl, null, 'true',
+ parentData.server.server_type, sURL, queryToolTitle, '', false);
+}
diff --git a/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js b/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js
index 750887ec..cf234f1d 100644
--- a/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js
+++ b/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js
@@ -2,12 +2,16 @@
define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'pgadmin.alertifyjs', 'pgadmin.backgrid', 'pgadmin.backform',
- 'pgadmin.browser', 'pgadmin.browser.node', 'backgrid.select.all',
+ 'pgadmin.browser', 'pgadmin.browser.node',
+ 'tools/grant_wizard/static/js/menu_utils',
+ 'sources/menu/menu_enabled',
+
+ 'backgrid.select.all',
'backgrid.filter', 'pgadmin.browser.server.privilege',
'pgadmin.browser.wizard',
], function(
gettext, url_for, $, _, Backbone, Alertify, Backgrid, Backform, pgBrowser,
- pgNode
+ pgNode, menuUtils, menuEnabled
) {
// if module is already initialized, refer to that.
@@ -143,41 +147,6 @@ define([
this.initialized = true;
- // Define list of nodes on which grant wizard context menu option appears
- var supported_nodes = [
- 'schema', 'coll-function', 'coll-sequence',
- 'coll-table', 'coll-view', 'coll-procedure',
- 'coll-mview', 'database', 'coll-trigger_function',
- ],
-
- /**
- Enable/disable grantwizard menu in tools based
- on node selected
- if selected node is present in supported_nodes,
- menu will be enabled otherwise disabled.
- Also, hide it for system view in catalogs
- */
- menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
- var parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(supported_nodes, d._type) !== -1 &&
- parent_data._type != 'catalog') {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
// Define the nodes on which the menus to be appear
var menus = [{
name: 'grant_wizard_schema',
@@ -187,21 +156,23 @@ define([
priority: 14,
label: gettext('Grant Wizard...'),
icon: 'fa fa-unlock-alt',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null,
+ pgBrowser.treeMenu, menuUtils.supportedNodes),
}];
// Add supported menus into the menus list
- for (var idx = 0; idx < supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.supportedNodes.length; idx++) {
menus.push({
- name: 'grant_wizard_schema_context_' + supported_nodes[idx],
- node: supported_nodes[idx],
+ name: 'grant_wizard_schema_context_' + menuUtils.supportedNodes[idx],
+ node: menuUtils.supportedNodes[idx],
module: this,
applies: ['context'],
callback: 'start_grant_wizard',
priority: 14,
label: gettext('Grant Wizard...'),
icon: 'fa fa-unlock-alt',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null,
+ pgBrowser.treeMenu, menuUtils.supportedNodes),
});
}
pgBrowser.add_menus(menus);
diff --git a/web/pgadmin/tools/grant_wizard/static/js/menu_utils.js b/web/pgadmin/tools/grant_wizard/static/js/menu_utils.js
new file mode 100644
index 00000000..b56ce3cd
--- /dev/null
+++ b/web/pgadmin/tools/grant_wizard/static/js/menu_utils.js
@@ -0,0 +1,16 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export const supportedNodes = [
+ 'schema', 'coll-function', 'coll-sequence',
+ 'coll-table', 'coll-view', 'coll-procedure',
+ 'coll-mview', 'database', 'coll-trigger_function',
+];
+
+
diff --git a/web/pgadmin/tools/import_export/static/js/import_export.js b/web/pgadmin/tools/import_export/static/js/import_export.js
index 3058f122..bbb045cd 100644
--- a/web/pgadmin/tools/import_export/static/js/import_export.js
+++ b/web/pgadmin/tools/import_export/static/js/import_export.js
@@ -1,10 +1,13 @@
define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'underscore.string', 'pgadmin.alertifyjs',
'sources/pgadmin', 'pgadmin.browser', 'backbone', 'backgrid', 'backform',
- 'sources/utils', 'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser.node.ui',
+ 'sources/utils',
+ 'sources/menu/menu_enabled',
+
+ 'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser.node.ui',
], function(
gettext, url_for, $, _, S, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid,
-Backform, commonUtils
+Backform, commonUtils, menuEnabled
) {
pgAdmin = pgAdmin || window.pgAdmin || {};
@@ -383,25 +386,6 @@ Backform, commonUtils
this.initialized = true;
- /*
- * Enable/disable import menu in tools based on node selected. Import
- * menu will be enabled only when user select table node.
- */
- var menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
- var parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data))
- return (
- (_.indexOf(['table'], d._type) !== -1 &&
- parent_data._type != 'catalog') ? true : false
- );
- else
- return false;
- };
-
// Initialize the context menu to display the import options when user open the context menu for table
pgBrowser.add_menus([{
name: 'import',
@@ -413,7 +397,7 @@ Backform, commonUtils
priority: 10,
label: gettext('Import/Export...'),
icon: 'fa fa-shopping-cart',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null, pgBrowser.treeMenu, ['table']),
}]);
},
diff --git a/web/pgadmin/tools/maintenance/static/js/maintenance.js b/web/pgadmin/tools/maintenance/static/js/maintenance.js
index f2102602..9092eb73 100644
--- a/web/pgadmin/tools/maintenance/static/js/maintenance.js
+++ b/web/pgadmin/tools/maintenance/static/js/maintenance.js
@@ -2,11 +2,14 @@ define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'pgadmin.alertifyjs', 'sources/pgadmin', 'pgadmin.browser', 'backbone',
'backgrid', 'backform', 'sources/utils',
+ 'tools/maintenance/static/js/menu_utils',
+ 'sources/menu/menu_enabled',
'pgadmin.backform', 'pgadmin.backgrid',
'pgadmin.browser.node.ui',
], function(
gettext, url_for, $, _, S, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid,
- Backform, commonUtils
+ Backform, commonUtils,
+ menuUtils, menuEnabled
) {
pgAdmin = pgAdmin || window.pgAdmin || {};
@@ -168,36 +171,6 @@ define([
this.initialized = true;
- var maintenance_supported_nodes = [
- 'database', 'table', 'primary_key',
- 'unique_constraint', 'index', 'partition',
- ];
-
- /**
- Enable/disable Maintenance menu in tools based on node selected.
- Maintenance menu will be enabled only when user select table and database node.
- */
- var menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
- var parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(maintenance_supported_nodes, d._type) !== -1 &&
- parent_data._type != 'catalog') {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
var menus = [{
name: 'maintenance',
module: this,
@@ -206,21 +179,23 @@ define([
priority: 10,
label: gettext('Maintenance...'),
icon: 'fa fa-wrench',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null,
+ pgBrowser.treeMenu, menuUtils.maintenanceSupportedNodes),
}];
// Add supported menus into the menus list
- for (var idx = 0; idx < maintenance_supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.maintenanceSupportedNodes.length; idx++) {
menus.push({
- name: 'maintenance_context_' + maintenance_supported_nodes[idx],
- node: maintenance_supported_nodes[idx],
+ name: 'maintenance_context_' + menuUtils.maintenanceSupportedNodes[idx],
+ node: menuUtils.maintenanceSupportedNodes[idx],
module: this,
applies: ['context'],
callback: 'callback_maintenance',
priority: 10,
label: gettext('Maintenance...'),
icon: 'fa fa-wrench',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null,
+ pgBrowser.treeMenu, menuUtils.maintenanceSupportedNodes),
});
}
pgBrowser.add_menus(menus);
diff --git a/web/pgadmin/tools/maintenance/static/js/menu_utils.js b/web/pgadmin/tools/maintenance/static/js/menu_utils.js
new file mode 100644
index 00000000..8cde1baa
--- /dev/null
+++ b/web/pgadmin/tools/maintenance/static/js/menu_utils.js
@@ -0,0 +1,13 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export const maintenanceSupportedNodes = [
+ 'database', 'table', 'primary_key',
+ 'unique_constraint', 'index', 'partition',
+];
diff --git a/web/pgadmin/tools/restore/static/js/menu_utils.js b/web/pgadmin/tools/restore/static/js/menu_utils.js
new file mode 100644
index 00000000..2d35c951
--- /dev/null
+++ b/web/pgadmin/tools/restore/static/js/menu_utils.js
@@ -0,0 +1,18 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export const restoreSupportedNodes = [
+ 'database',
+ 'schema',
+ 'table',
+ 'function',
+ 'trigger',
+ 'index',
+ 'partition',
+];
diff --git a/web/pgadmin/tools/restore/static/js/restore.js b/web/pgadmin/tools/restore/static/js/restore.js
index 585b9729..eaa7a15e 100644
--- a/web/pgadmin/tools/restore/static/js/restore.js
+++ b/web/pgadmin/tools/restore/static/js/restore.js
@@ -1,11 +1,13 @@
-// Restore dialog
define('tools.restore', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'underscore.string', 'pgadmin.alertifyjs', 'pgadmin.browser',
'pgadmin.backgrid', 'pgadmin.backform', 'sources/utils',
+ 'tools/restore/static/js/menu_utils',
+ 'sources/menu/menu_enabled',
+ 'tools/restore/static/js/restore_dialog',
], function(
gettext, url_for, $, _, Backbone, S, alertify, pgBrowser, Backgrid, Backform,
-commonUtils
+commonUtils, menuUtils, menuEnabled, restoreDialog
) {
// if module is already initialized, refer to that.
@@ -307,59 +309,6 @@ commonUtils
this.initialized = true;
- // Define list of nodes on which restore context menu option appears
- var restore_supported_nodes = [
- 'database', 'schema',
- 'table', 'function',
- 'trigger', 'index',
- 'partition',
- ];
-
- /**
- Enable/disable restore menu in tools based
- on node selected
- if selected node is present in supported_nodes,
- menu will be enabled otherwise disabled.
- Also, hide it for system view in catalogs
- */
- var menu_enabled = function(itemData, item, data) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
- var parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(restore_supported_nodes, d._type) !== -1 &&
- is_parent_catalog(itemData, item, data)) {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
- var is_parent_catalog = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
-
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to restore
- if (_.indexOf(['catalog'], d._type) > -1)
- return false;
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- };
-
// Define the nodes on which the menus to be appear
var menus = [{
name: 'restore_object',
@@ -369,20 +318,22 @@ commonUtils
priority: 13,
label: gettext('Restore...'),
icon: 'fa fa-upload',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null,
+ pgBrowser.treeMenu, menuUtils.restoreSupportedNodes),
}];
- for (var idx = 0; idx < restore_supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.restoreSupportedNodes.length; idx++) {
menus.push({
- name: 'restore_' + restore_supported_nodes[idx],
- node: restore_supported_nodes[idx],
+ name: 'restore_' + menuUtils.restoreSupportedNodes[idx],
+ node: menuUtils.restoreSupportedNodes[idx],
module: this,
applies: ['context'],
callback: 'restore_objects',
priority: 13,
label: gettext('Restore...'),
icon: 'fa fa-upload',
- enable: menu_enabled,
+ enable: menuEnabled.menuEnabled.bind(null,
+ pgBrowser.treeMenu, menuUtils.restoreSupportedNodes),
});
}
@@ -391,318 +342,8 @@ commonUtils
},
// Callback to draw Backup Dialog for objects
restore_objects: function(action, treeItem) {
-
- var i = treeItem || pgBrowser.tree.selected(),
- server_data = null;
-
- while (i) {
- var node_data = pgBrowser.tree.itemData(i);
- if (node_data._type == 'server') {
- server_data = node_data;
- break;
- }
-
- if (pgBrowser.tree.hasParent(i)) {
- i = $(pgBrowser.tree.parent(i));
- } else {
- alertify.alert(
- gettext('Restore Error'),
- gettext('Please select server or child node from tree.')
- );
- break;
- }
- }
-
- if (!server_data) {
- return;
- }
-
- var module = 'paths',
- preference_name = 'pg_bin_dir',
- msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
-
- if ((server_data.type && server_data.type == 'ppas') ||
- server_data.server_type == 'ppas') {
- preference_name = 'ppas_bin_dir';
- msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
- }
-
- var preference = pgBrowser.get_preference(module, preference_name);
-
- if (preference) {
- if (!preference.value) {
- alertify.alert(gettext('Configuration required'), msg);
- return;
- }
- } else {
- alertify.alert(
- gettext('Restore Error'),
- S(gettext('Failed to load preference %s of module %s')).sprintf(preference_name, module).value()
- );
- return;
- }
-
- var title = S(gettext('Restore (%s: %s)')),
- tree = pgBrowser.tree,
- item = treeItem || tree.selected(),
- data = item && item.length == 1 && tree.itemData(item),
- node = data && data._type && pgBrowser.Nodes[data._type];
-
- if (!node)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [item]);
-
- if (treeInfo.database._label.indexOf('=') >= 0) {
- alertify.alert(
- gettext('Restore error'),
- gettext('Restore job creation failed. '+
- 'Databases with = symbols in the name cannot be restored using this utility.')
- );
- return;
- }
-
- title = title.sprintf(node.label, data.label).value();
-
- if (!alertify.pg_restore) {
- // Create Dialog title on the fly with node details
- alertify.dialog('pg_restore', function factory() {
- return {
- main: function(title, item, data, node) {
- this.set('title', title);
- this.setting('pg_node', node);
- this.setting('pg_item', item);
- this.setting('pg_item_data', data);
- },
- build: function() {
- alertify.pgDialogBuild.apply(this);
- },
- setup: function() {
- return {
- buttons: [{
- text: '',
- className: 'btn btn-default pull-left fa fa-lg fa-info',
- attrs: {
- name: 'object_help',
- type: 'button',
- url: 'backup.html',
- label: gettext('Restore'),
- },
- }, {
- text: '',
- key: 112,
- className: 'btn btn-default pull-left fa fa-lg fa-question',
- attrs: {
- name: 'dialog_help',
- type: 'button',
- label: gettext('Restore'),
- url: url_for('help.static', {
- 'filename': 'restore_dialog.html',
- }),
- },
- }, {
- text: gettext('Restore'),
- key: 13,
- className: 'btn btn-primary fa fa-upload pg-alertify-button',
- restore: true,
- 'data-btn-name': 'restore',
- }, {
- text: gettext('Cancel'),
- key: 27,
- className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
- restore: false,
- 'data-btn-name': 'cancel',
- }],
- // Set options for dialog
- options: {
- title: title,
- //disable both padding and overflow control.
- padding: !1,
- overflow: !1,
- model: 0,
- resizable: true,
- maximizable: true,
- pinnable: false,
- closableByDimmer: false,
- modal: false,
- },
- };
- },
- hooks: {
- // triggered when the dialog is closed
- onclose: function() {
- if (this.view) {
- this.view.remove({
- data: true,
- internal: true,
- silent: true,
- });
- }
- },
- },
- settings: {
- pg_node: null,
- pg_item: null,
- pg_item_data: null,
- },
- prepare: function() {
-
- var self = this;
- // Disable Backup button until user provides Filename
- this.__internal.buttons[2].element.disabled = true;
- var $container = $('<div class=\'restore_dialog\'></div>');
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- var newModel = new RestoreObjectModel({
- node_data: node,
- }, {
- node_info: treeInfo,
- }),
- fields = Backform.generateViewSchema(
- treeInfo, newModel, 'create', node, treeInfo.server, true
- );
-
- var view = this.view = new Backform.Dialog({
- el: $container,
- model: newModel,
- schema: fields,
- });
-
- $(this.elements.body.childNodes[0]).addClass(
- 'alertify_tools_dialog_properties obj_properties'
- );
-
- view.render();
-
- this.elements.content.appendChild($container.get(0));
-
- view.$el.attr('tabindex', -1);
- // var dialogTabNavigator = pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
- commonUtils.findAndSetFocus(container);
-
- // Listen to model & if filename is provided then enable Backup button
- this.view.model.on('change', function() {
- if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
- this.errorModel.clear();
- self.__internal.buttons[2].element.disabled = false;
- } else {
- self.__internal.buttons[2].element.disabled = true;
- this.errorModel.set('file', gettext('Please provide filename'));
- }
- });
-
- },
- // Callback functions when click on the buttons of the Alertify dialogs
- callback: function(e) {
- // Fetch current server id
- var t = pgBrowser.tree,
- i = this.settings['pg_item'] || t.selected(),
- d = this.settings['pg_item_data'] || (
- i && i.length == 1 ? t.itemData(i) : undefined
- ),
- node = this.settings['pg_node'] || (
- d && pgBrowser.Nodes[d._type]
- );
-
- if (e.button.element.name == 'dialog_help' || e.button.element.name == 'object_help') {
- e.cancel = true;
- pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
- node, i, e.button.element.getAttribute('label'));
- return;
- }
-
- if (e.button['data-btn-name'] === 'restore') {
- if (!d)
- return;
-
- var info = node.getTreeNodeHierarchy.apply(node, [i]),
- m = this.view.model;
- // Set current node info into model
- m.set('database', info.database._label);
- if (!m.get('custom')) {
- switch (d._type) {
- case 'schema':
- m.set('schemas', [d._label]);
- break;
- case 'table':
- m.set('schemas', [info.schema._label]);
- m.set('tables', [d._label]);
- break;
- case 'function':
- m.set('schemas', [info.schema._label]);
- m.set('functions', [d._label]);
- break;
- case 'index':
- m.set('schemas', [info.schema._label]);
- m.set('indexes', [d._label]);
- break;
- case 'trigger':
- m.set('schemas', [info.schema._label]);
- m.set('triggers', [d._label]);
- break;
- case 'trigger_func':
- m.set('schemas', [info.schema._label]);
- m.set('trigger_funcs', [d._label]);
- break;
- }
- } else {
- // TODO::
- // When we will implement the object selection in the
- // import dialog, we will need to select the objects from
- // the tree selection tab.
- }
-
- var self = this,
- baseUrl = url_for('restore.create_job', {
- 'sid': info.server._id,
- }),
- args = this.view.model.toJSON();
-
- $.ajax({
- url: baseUrl,
- method: 'POST',
- data: {
- 'data': JSON.stringify(args),
- },
- success: function(res) {
- if (res.success) {
- alertify.success(
- gettext('Restore job created.'), 5
- );
- pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
- } else {
- console.warn(res);
- }
- },
- error: function(xhr) {
- try {
- var err = $.parseJSON(xhr.responseText);
- alertify.alert(
- gettext('Restore failed.'),
- err.errormsg
- );
- } catch (e) {
- console.warn(e.stack || e);
- }
- },
- });
- }
- },
- };
- });
- }
-
- alertify.pg_restore(title, item, data, node).resizeTo('65%', '60%');
+ let dialog = new restoreDialog.RestoreDialog(pgBrowser, $, alertify, RestoreObjectModel);
+ dialog.draw(action, treeItem);
},
};
return pgBrowser.Restore;
diff --git a/web/pgadmin/tools/restore/static/js/restore_dialog.js b/web/pgadmin/tools/restore/static/js/restore_dialog.js
new file mode 100644
index 00000000..4884d901
--- /dev/null
+++ b/web/pgadmin/tools/restore/static/js/restore_dialog.js
@@ -0,0 +1,57 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../../../../static/js/gettext';
+import {sprintf} from 'sprintf-js';
+import Backform from '../../../../static/js/backform.pgadmin';
+import {Dialog} from '../../../../static/js/alertify/dialog';
+
+export class RestoreDialog extends Dialog {
+ constructor(pgBrowser, $, alertify, RestoreModel, backform = Backform) {
+ super('Restore Error',
+ '<div class=\'restore_dialog\'></div>',
+ pgBrowser, $, alertify, RestoreModel, backform);
+ }
+
+ draw(action, aciTreeItem) {
+
+ const serverInformation = this.retrieveAncestorOfTypeServer(aciTreeItem);
+
+ if (!serverInformation) {
+ return;
+ }
+
+ if (!this.hasBinariesConfiguration(serverInformation)) {
+ return;
+ }
+
+ if (!this.canExecuteOnCurrentDatabase(aciTreeItem)) {
+ return;
+ }
+
+ let aciTreeItem1 = aciTreeItem || this.pgBrowser.treeMenu.selected();
+ let item = this.pgBrowser.treeMenu.findNodeByDomElement(aciTreeItem1);
+ const data = item.getData();
+ const node = this.pgBrowser.Nodes[data._type];
+
+ if (!node)
+ return;
+
+ let title = sprintf(gettext('Restore (%s: %s)'), node.label, data.label);
+
+ this.createOrGetDialog(title, 'restore');
+
+ this.alertify.pg_restore(title, aciTreeItem1, data, node).resizeTo('65%', '60%');
+ }
+
+ dialogName() {
+ return 'pg_restore';
+ }
+}
+
diff --git a/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js b/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js
new file mode 100644
index 00000000..845da7a3
--- /dev/null
+++ b/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js
@@ -0,0 +1,255 @@
+import {getTreeNodeHierarchyFromElement} from '../../../../static/js/tree/pgadmin_tree_node';
+import axios from 'axios/index';
+import _ from 'underscore';
+import gettext from '../../../../static/js/gettext';
+import url_for from '../../../../static/js/url_for';
+import {DialogWrapper} from '../../../../static/js/alertify/dialog_wrapper';
+
+export class RestoreDialogWrapper extends DialogWrapper {
+ constructor(dialogContainerSelector, dialogTitle, typeOfDialog,
+ jquery, pgBrowser, alertify, dialogModel, backform) {
+ super(dialogContainerSelector, dialogTitle, jquery,
+ pgBrowser, alertify, dialogModel, backform);
+ }
+
+ main(title, item, data, node) {
+ this.set('title', title);
+ this.setting('pg_node', node);
+ this.setting('pg_item', item);
+ this.setting('pg_item_data', data);
+ }
+
+ setup() {
+ return {
+ buttons: [{
+ text: '',
+ className: 'btn btn-default pull-left fa fa-lg fa-info',
+ attrs: {
+ name: 'object_help',
+ type: 'button',
+ url: 'backup.html',
+ label: gettext('Restore'),
+ },
+ }, {
+ text: '',
+ key: 112,
+ className: 'btn btn-default pull-left fa fa-lg fa-question',
+ attrs: {
+ name: 'dialog_help',
+ type: 'button',
+ label: gettext('Restore'),
+ url: url_for('help.static', {
+ 'filename': 'restore_dialog.html',
+ }),
+ },
+ }, {
+ text: gettext('Restore'),
+ key: 13,
+ className: 'btn btn-primary fa fa-upload pg-alertify-button',
+ restore: true,
+ 'data-btn-name': 'restore',
+ }, {
+ text: gettext('Cancel'),
+ key: 27,
+ className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
+ restore: false,
+ 'data-btn-name': 'cancel',
+ }],
+ // Set options for dialog
+ options: {
+ title: this.dialogTitle,
+ //disable both padding and overflow control.
+ padding: !1,
+ overflow: !1,
+ model: 0,
+ resizable: true,
+ maximizable: true,
+ pinnable: false,
+ closableByDimmer: false,
+ modal: false,
+ },
+ };
+ }
+
+ prepare() {
+ this.disableRestoreButton();
+
+ const $container = this.jquery(this.dialogContainerSelector);
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = this.getSelectedNodeData(selectedTreeNode);
+ if (!selectedTreeNodeData) {
+ return;
+ }
+
+ const node = this.pgBrowser.Nodes[selectedTreeNodeData._type];
+
+ const treeInfo = getTreeNodeHierarchyFromElement(this.pgBrowser, selectedTreeNode);
+ const dialog = this.createDialog(node, treeInfo, $container);
+ this.addAlertifyClassToRestoreNodeChildNodes();
+ dialog.render();
+
+ this.elements.content.appendChild($container.get(0));
+
+ this.focusOnDialog(dialog);
+ this.setListenersForFilenameChanges();
+ }
+
+ callback(event) {
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = this.getSelectedNodeData(selectedTreeNode);
+ const node = selectedTreeNodeData && this.pgBrowser.Nodes[selectedTreeNodeData._type];
+
+ if (this.wasHelpButtonPressed(event)) {
+ event.cancel = true;
+ this.pgBrowser.showHelp(
+ event.button.element.name,
+ event.button.element.getAttribute('url'),
+ node,
+ selectedTreeNode,
+ event.button.element.getAttribute('label')
+ );
+ return;
+ }
+
+ if (this.wasRestoreButtonPressed(event)) {
+
+ if (!selectedTreeNodeData)
+ return;
+
+ const serverIdentifier = this.retrieveServerIdentifier(node, selectedTreeNode);
+
+ const dialogWrapper = this;
+ let urlShortcut = 'restore.create_job';
+
+ const baseUrl = url_for(urlShortcut, {
+ 'sid': serverIdentifier,
+ });
+
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.pgBrowser,
+ selectedTreeNode
+ );
+
+ this.setExtraParameters(selectedTreeNode, treeInfo);
+
+ let service = axios.create({});
+ service.post(
+ baseUrl,
+ this.view.model.toJSON()
+ ).then(function () {
+ dialogWrapper.alertify.success(gettext('Restore job created.'), 5);
+ dialogWrapper.pgBrowser.Events.trigger('pgadmin-bgprocess:created', dialogWrapper);
+ }).catch(function (error) {
+ try {
+ const err = error.response.data;
+ dialogWrapper.alertify.alert(
+ gettext('Restore job failed.'),
+ err.errormsg
+ );
+ } catch (e) {
+ console.warn(e.stack || e);
+ }
+ });
+ }
+ }
+
+ addAlertifyClassToRestoreNodeChildNodes() {
+ this.jquery(this.elements.body.childNodes[0]).addClass(
+ 'alertify_tools_dialog_properties obj_properties'
+ );
+ }
+
+ getSelectedNode() {
+ const tree = this.pgBrowser.treeMenu;
+ const selectedNode = tree.selected();
+ if (selectedNode) {
+ return tree.findNodeByDomElement(selectedNode);
+ } else {
+ return undefined;
+ }
+ }
+
+ disableRestoreButton() {
+ this.__internal.buttons[2].element.disabled = true;
+ }
+
+ enableRestoreButton() {
+ this.__internal.buttons[2].element.disabled = false;
+ }
+
+ createDialog(node, treeInfo, $container) {
+ const newModel = new this.dialogModel({
+ node_data: node,
+ }, {
+ node_info: treeInfo,
+ });
+ const fields = this.backform.generateViewSchema(
+ treeInfo, newModel, 'create', node, treeInfo.server, true
+ );
+
+ return this.view = new this.backform.Dialog({
+ el: $container,
+ model: newModel,
+ schema: fields,
+ });
+ }
+
+ retrieveServerIdentifier(node, selectedTreeNode) {
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.pgBrowser,
+ selectedTreeNode
+ );
+ return treeInfo.server._id;
+ }
+
+ setListenersForFilenameChanges() {
+ const self = this;
+
+ this.view.model.on('change', function () {
+ if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
+ this.errorModel.clear();
+ self.enableRestoreButton();
+ } else {
+ self.disableRestoreButton();
+ this.errorModel.set('file', gettext('Please provide a filename'));
+ }
+ });
+ }
+
+ setExtraParameters(selectedTreeNode, treeInfo) {
+ this.view.model.set('database', treeInfo.database._label);
+ if (!this.view.model.get('custom')) {
+ const nodeData = selectedTreeNode.getData();
+
+ switch (nodeData._type) {
+ case 'schema':
+ this.view.model.set('schemas', [nodeData._label]);
+ break;
+ case 'table':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('tables', [nodeData._label]);
+ break;
+ case 'function':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('functions', [nodeData._label]);
+ break;
+ case 'index':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('indexes', [nodeData._label]);
+ break;
+ case 'trigger':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('triggers', [nodeData._label]);
+ break;
+ case 'trigger_func':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('trigger_funcs', [nodeData._label]);
+ break;
+ }
+ }
+ }
+
+ wasRestoreButtonPressed(event) {
+ return event.button['data-btn-name'] === 'restore';
+ }
+}
diff --git a/web/regression/javascript/backup/backup_dialog_spec.js b/web/regression/javascript/backup/backup_dialog_spec.js
new file mode 100644
index 00000000..2655059d
--- /dev/null
+++ b/web/regression/javascript/backup/backup_dialog_spec.js
@@ -0,0 +1,205 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {BackupDialog} from '../../../pgadmin/tools/backup/static/js/backup_dialog';
+import {TreeFake} from '../tree/tree_fake';
+
+const context = describe;
+
+describe('BackupDialog', () => {
+ let backupDialog;
+ let pgBrowser;
+ let jquerySpy;
+ let alertifySpy;
+ let backupModelSpy;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: {
+ hasId: true,
+ label: 'server',
+ getTreeNodeHierarchy: jasmine.createSpy('server.getTreeNodeHierarchy'),
+ },
+ database: {
+ hasId: true,
+ label: 'database',
+ getTreeNodeHierarchy: jasmine.createSpy('db.getTreeNodeHierarchy'),
+ },
+ schema: {
+ hasId: true,
+ label: 'schema',
+ getTreeNodeHierarchy: jasmine.createSpy('db.getTreeNodeHierarchy'),
+ },
+ },
+ };
+ pgBrowser.Nodes.server.hasId = true;
+ pgBrowser.Nodes.database.hasId = true;
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ backupModelSpy = jasmine.createSpy('backupModelSpy');
+
+ const hierarchy = {
+ children: [
+ {
+ id: 'root',
+ children: [
+ {
+ id: 'serverTreeNode',
+ data: {
+ _id: 10,
+ _type: 'server',
+ },
+ children: [
+ {
+ id: 'some_database',
+ data: {
+ _type: 'database',
+ _id: 11,
+ label: 'some_database',
+ _label: 'some_database_label',
+ },
+ }, {
+ id: 'database_with_equal_in_name',
+ data: {
+ _type: 'database',
+ label: 'some_database',
+ _label: '=some_database_label',
+ },
+ },
+ ],
+ },
+ {
+ id: 'ppasServer',
+ data: {
+ _type: 'server',
+ server_type: 'ppas',
+ children: [
+ {id: 'someNodeUnderneathPPASServer'},
+ ],
+ },
+ },
+ ],
+ },
+ ],
+ };
+
+ pgBrowser.treeMenu = TreeFake.build(hierarchy);
+ });
+
+ describe('#draw', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ alertifySpy['backup_objects'] = jasmine.createSpy('backup_objects');
+ backupDialog = new BackupDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ backupModelSpy
+ );
+
+ pgBrowser.get_preference = jasmine.createSpy('get_preferences');
+ });
+
+ context('there are no ancestors of the type server', () => {
+ it('does not create a dialog', () => {
+ pgBrowser.treeMenu.selectNode([{id: 'root'}]);
+ backupDialog.draw(null, null, null);
+ expect(alertifySpy['backup_objects']).not.toHaveBeenCalled();
+ });
+
+ it('display an alert with a Backup Error', () => {
+ backupDialog.draw(null, [{id: 'root'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Please select server or child node from the browser tree.'
+ );
+ });
+ });
+
+ context('there is an ancestor of the type server', () => {
+ context('no preference can be found', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue(undefined);
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [{id: 'some_database'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference pg_bin_dir of module paths'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [{id: 'ppasServer'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference ppas_bin_dir of module paths'
+ );
+ });
+ });
+ });
+
+ context('preference can be found', () => {
+ context('binary folder is not configured', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue({});
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [{id: 'serverTreeNode'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the PostgreSQL Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [{id: 'ppasServer'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the EDB Advanced Server Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+ });
+
+ context('binary folder is configured', () => {
+ let backupDialogResizeToSpy;
+ beforeEach(() => {
+ backupDialogResizeToSpy = jasmine.createSpyObj('backupDialogResizeToSpy', ['resizeTo']);
+ alertifySpy['backup_objects'].and
+ .returnValue(backupDialogResizeToSpy);
+ pgBrowser.get_preference.and.returnValue({value: '/some/path'});
+ });
+
+ it('displays the dialog', () => {
+ backupDialog.draw(null, [{id: 'serverTreeNode'}], null);
+ expect(alertifySpy['backup_objects']).toHaveBeenCalledWith(true);
+ expect(backupDialogResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
+ });
+
+ context('database label contain "="', () => {
+ it('should create alert dialog with backup error', () => {
+ backupDialog.draw(null, [{id: 'database_with_equal_in_name'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith('Backup Error',
+ 'Databases with = symbols in the name cannot be backed up or restored using this utility.');
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/backup/backup_dialog_wrapper_spec.js b/web/regression/javascript/backup/backup_dialog_wrapper_spec.js
new file mode 100644
index 00000000..58705318
--- /dev/null
+++ b/web/regression/javascript/backup/backup_dialog_wrapper_spec.js
@@ -0,0 +1,675 @@
+import {TreeFake} from '../tree/tree_fake';
+import {BackupDialogWrapper} from '../../../pgadmin/tools/backup/static/js/backup_dialog_wrapper';
+import axios from 'axios/index';
+import MockAdapter from 'axios-mock-adapter';
+import {FakeModel} from '../fake_model';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+let context = describe;
+
+describe('BackupDialogWrapper', () => {
+ let jquerySpy;
+ let pgBrowser;
+ let alertifySpy;
+ let dialogModelKlassSpy;
+ let backform;
+ let generatedBackupModel;
+ let backupDialogWrapper;
+ let noDataNode;
+ let serverTreeNode;
+ let databaseTreeNode;
+ let viewSchema;
+ let backupJQueryContainerSpy;
+ let backupNodeChildNodeSpy;
+ let backupNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: {
+ hasId: true,
+ getTreeNodeHierarchy: jasmine.createSpy('getTreeNodeHierarchy'),
+ },
+ database: {
+ hasId: true,
+ },
+ },
+ keyboardNavigation: jasmine.createSpyObj('keyboardNavigation', ['getDialogTabNavigator']),
+ };
+ noDataNode = pgBrowser.treeMenu.addNewNode('level1.1', undefined, [{id: 'level1'}]);
+ serverTreeNode = pgBrowser.treeMenu.addNewNode('level2.1', {
+ _type: 'server',
+ _id: 10,
+ label: 'some-tree-label',
+ }, [{id: 'level2.1'}]);
+ databaseTreeNode = new TreeNode('database-tree-node', {
+ _type: 'database',
+ _label: 'some-database-label',
+ }, [{id: 'database-tree-node'}]);
+ pgBrowser.treeMenu.addChild(serverTreeNode, databaseTreeNode);
+
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ backupNode = {
+ __internal: {
+ buttons: [{}, {}, {
+ element: {
+ disabled: false,
+ },
+ }],
+ },
+ elements: {
+ body: {
+ childNodes: [
+ {},
+ ],
+ },
+ content: jasmine.createSpyObj('content', ['appendChild', 'attr']),
+ },
+ };
+
+ backupJQueryContainerSpy = jasmine.createSpyObj('backupJQueryContainer', ['get', 'attr']);
+ backupJQueryContainerSpy.get.and.returnValue(backupJQueryContainerSpy);
+
+ generatedBackupModel = {};
+ dialogModelKlassSpy = jasmine.createSpy('dialogModelKlass');
+ dialogModelKlassSpy.and.returnValue(generatedBackupModel);
+
+ viewSchema = {};
+ backform = jasmine.createSpyObj('backform', ['generateViewSchema', 'Dialog']);
+ backform.generateViewSchema.and.returnValue(viewSchema);
+
+ backupNodeChildNodeSpy = jasmine.createSpyObj('something', ['addClass']);
+ jquerySpy.and.callFake((selector) => {
+ if (selector === '<div class=\'backup_dialog\'></div>') {
+ return backupJQueryContainerSpy;
+ } else if (selector === backupNode.elements.body.childNodes[0]) {
+ return backupNodeChildNodeSpy;
+ }
+ });
+
+ });
+
+ describe('#prepare', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+ backupDialogWrapper = Object.assign(backupDialogWrapper, backupNode);
+ });
+
+ context('no tree element is selected', () => {
+ it('does not create a backform dialog', () => {
+ backupDialogWrapper.prepare();
+ expect(backform.Dialog).not.toHaveBeenCalled();
+ });
+
+ it('disables the button "submit button" until a filename is selected', () => {
+ backupDialogWrapper.prepare();
+ expect(backupDialogWrapper.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('selected tree node has no data', () => {
+ beforeEach(() => {
+ pgBrowser.treeMenu.selectNode(noDataNode.domNode);
+ });
+
+ it('does not create a backform dialog', () => {
+ backupDialogWrapper.prepare();
+ expect(backform.Dialog).not.toHaveBeenCalled();
+ });
+
+ it('disables the button "submit button" until a filename is selected', () => {
+ backupDialogWrapper.prepare();
+ expect(backupDialogWrapper.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('tree element is selected', () => {
+ let treeHierarchyInformation;
+ let dialogSpy;
+
+ beforeEach(() => {
+ treeHierarchyInformation = {
+ server: {
+ _type: 'server',
+ _id: 10,
+ priority: 0,
+ label: 'some-tree-label',
+ },
+ };
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ pgBrowser.Nodes['server'].getTreeNodeHierarchy.and
+ .returnValue(treeHierarchyInformation);
+ dialogSpy = jasmine.createSpyObj('newView', ['render']);
+ dialogSpy.$el = jasmine.createSpyObj('$el', ['find', 'attr']);
+ dialogSpy.model = jasmine.createSpyObj('model', ['on']);
+ dialogSpy.$el.find.and.returnValue([]);
+
+ backform.Dialog.and.returnValue(dialogSpy);
+ });
+
+ it('creates a backform dialog and displays it', () => {
+ backupDialogWrapper.prepare();
+ expect(backform.Dialog).toHaveBeenCalledWith({
+ el: backupJQueryContainerSpy,
+ model: generatedBackupModel,
+ schema: viewSchema,
+ });
+
+ expect(dialogSpy.render).toHaveBeenCalled();
+ });
+
+
+ it('add alertify classes to restore node childnode', () => {
+ backupDialogWrapper.prepare();
+ expect(backupNodeChildNodeSpy.addClass)
+ .toHaveBeenCalledWith('alertify_tools_dialog_properties obj_properties');
+ });
+
+ it('disables the button submit button until a filename is selected', () => {
+ backupDialogWrapper.prepare();
+ expect(backupNode.__internal.buttons[2].element.disabled).toBe(true);
+ });
+
+ it('generates a new backup model', () => {
+ backupDialogWrapper.prepare();
+ expect(dialogModelKlassSpy).toHaveBeenCalledWith(
+ {type: 'backup'},
+ {node_info: treeHierarchyInformation}
+ );
+ });
+
+ it('add the new dialog to the backup node HTML', () => {
+ backupDialogWrapper.prepare();
+ expect(backupNode.elements.content.appendChild).toHaveBeenCalledWith(backupJQueryContainerSpy);
+ });
+ });
+ });
+
+ describe('onButtonClicked', () => {
+ let networkMock;
+ beforeEach(() => {
+ networkMock = new MockAdapter(axios);
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ backupDialogWrapper = Object.assign(backupDialogWrapper, backupNode);
+ });
+
+ afterEach(() => {
+ networkMock.restore();
+ });
+
+ context('dialog help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.*/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ pgBrowser.showHelp = jasmine.createSpy('showHelp');
+
+ const event = {
+ button: {
+ element: {
+ name: 'dialog_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ },
+ },
+ },
+ };
+ backupDialogWrapper.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'dialog_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ );
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('object help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.*/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ pgBrowser.showHelp = jasmine.createSpy('showHelp');
+
+ const event = {
+ button: {
+ element: {
+ name: 'object_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ },
+ },
+ },
+ };
+ backupDialogWrapper.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'object_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ );
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('backup button was pressed', () => {
+ context('no tree node is selected', () => {
+ it('does not start the backup', () => {
+ let networkCalled = false;
+ networkMock.onAny(/.*/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ let event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+
+ backupDialogWrapper.callback(event);
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('tree node has no data', () => {
+ it('does not start the backup', () => {
+ pgBrowser.treeMenu.selectNode(noDataNode.domNode);
+
+ let networkCalled = false;
+ networkMock.onAny(/.*/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ let event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+
+ backupDialogWrapper.callback(event);
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('tree node has data', () => {
+ context('when dialog type is global', () => {
+ let event;
+ beforeEach(() => {
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+
+ backupDialogWrapper.view = {
+ model: new FakeModel(),
+ };
+
+ event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ });
+
+ context('when the backup job is created successfully', () => {
+ let dataSentToServer;
+ beforeEach(() => {
+ pgBrowser.Events = jasmine.createSpyObj('Events', ['trigger']);
+ alertifySpy.success = jasmine.createSpy('success');
+
+ networkMock.onPost('/backup/job/10').reply((request) => {
+ dataSentToServer = request.data;
+ return [200, {}];
+ });
+ });
+
+ it('creates a success alert box', (done) => {
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.success).toHaveBeenCalledWith(
+ 'Backup job created.',
+ 5
+ );
+ done();
+ }, 0);
+ });
+
+ it('trigger an event to background process', (done) => {
+ backupDialogWrapper.callback(event);
+
+ setTimeout(() => {
+ expect(pgBrowser.Events.trigger).toHaveBeenCalledWith(
+ 'pgadmin-bgprocess:created',
+ backupDialogWrapper
+ );
+ done();
+ }, 0);
+ });
+
+ it('send the correct paramenters to the backend', (done) => {
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(JSON.parse(dataSentToServer)).toEqual(
+ {}
+ );
+ done();
+ }, 0);
+ });
+ });
+
+ context('when creating backup job fails', () => {
+ it('creates an alert box', (done) => {
+ alertifySpy.alert = jasmine.createSpy('alert');
+ networkMock.onPost('/backup/job/10').reply(() => {
+ return [400, {
+ errormsg: 'some-error-message',
+ }];
+ });
+
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup job failed.',
+ 'some-error-message'
+ );
+ done();
+ }, 0);
+
+ });
+ });
+ });
+
+ context('when dialog type is object', () => {
+ let event;
+ beforeEach(() => {
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup_objects',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ pgBrowser.treeMenu.selectNode(databaseTreeNode.domNode);
+
+ backupDialogWrapper.view = {
+ model: new FakeModel(),
+ };
+
+ event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ });
+
+ context('when the backup job is created successfully', () => {
+ let dataSentToServer;
+ beforeEach(() => {
+ pgBrowser.Events = jasmine.createSpyObj('Events', ['trigger']);
+ alertifySpy.success = jasmine.createSpy('success');
+
+ networkMock.onPost('/backup/job/10/object').reply((request) => {
+ dataSentToServer = request.data;
+ return [200, {}];
+ });
+ });
+
+ it('creates a success alert box', (done) => {
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.success).toHaveBeenCalledWith(
+ 'Backup job created.',
+ 5
+ );
+ done();
+ }, 0);
+ });
+
+ it('trigger an event to background process', (done) => {
+ backupDialogWrapper.callback(event);
+
+ setTimeout(() => {
+ expect(pgBrowser.Events.trigger).toHaveBeenCalledWith(
+ 'pgadmin-bgprocess:created',
+ backupDialogWrapper
+ );
+ done();
+ }, 0);
+ });
+
+ it('send the correct parameters to the backend', (done) => {
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(JSON.parse(dataSentToServer)).toEqual(
+ {database: 'some-database-label'}
+ );
+ done();
+ }, 0);
+ });
+ });
+
+ context('when creating backup job fails', () => {
+ it('creates an alert box', (done) => {
+ alertifySpy.alert = jasmine.createSpy('alert');
+ networkMock.onPost('/backup/job/10/object').reply(() => {
+ return [400, {
+ errormsg: 'some-error-message',
+ }];
+ });
+
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup job failed.',
+ 'some-error-message'
+ );
+ done();
+ }, 0);
+ });
+ });
+ });
+ });
+ });
+ });
+
+ describe('#setExtraParameters', () => {
+ let selectedTreeNode;
+ let treeInfo;
+ let model;
+
+ context('when dialog type is global', () => {
+ beforeEach(() => {
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ treeInfo = {};
+ model = new FakeModel();
+ backupDialogWrapper.view = {
+ model: model,
+ };
+ });
+
+
+ it('sets nothing on the view model', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.toJSON()).toEqual({});
+ });
+ });
+
+ context('when dialog type is object', () => {
+ beforeEach(() => {
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup_objects',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ treeInfo = {
+ database: {
+ _label: 'some-database-label',
+ },
+ schema: {
+ _label: 'some-treeinfo-label',
+ },
+ };
+
+ model = new FakeModel();
+ selectedTreeNode = new TreeNode('some-selected-node',
+ {_type: 'some-type', _label: 'some-selected-label'},
+ []);
+ backupDialogWrapper.view = {
+ model: model,
+ };
+ });
+
+ it('sets the database label on the model', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ 'database': 'some-database-label',
+ });
+ });
+
+ context('when the selected is a schema type', () => {
+ beforeEach(() => {
+ selectedTreeNode = new TreeNode('some-selected-node',
+ {_type: 'schema', _label: 'some-schema-label'},
+ []);
+ });
+
+ it('sets the schema label on the model', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ 'database': 'some-database-label',
+ 'schemas': ['some-schema-label'],
+ });
+ });
+ });
+
+ context('when the selected is a table type', () => {
+ beforeEach(() => {
+ selectedTreeNode = new TreeNode('some-selected-node',
+ {_type: 'table', _label: 'some-table-label'},
+ []);
+ });
+
+ it('sets the schema label on the model', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ 'database': 'some-database-label',
+ 'tables': [['some-treeinfo-label', 'some-table-label']],
+ });
+ });
+ });
+
+ context('when the model has no ratio value', () => {
+ beforeEach(() => {
+ model.set('ratio', '');
+ });
+
+ it('sets clears the ratio value', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.get('ratio')).toBeUndefined();
+ });
+ });
+
+ context('when the model has a valid ratio value', () => {
+ beforeEach(() => {
+ model.set('ratio', '0.25');
+ });
+
+ it('sets clears the ratio value', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.get('ratio')).toEqual('0.25');
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/backup/global_server_backup_dialog_spec.js b/web/regression/javascript/backup/global_server_backup_dialog_spec.js
new file mode 100644
index 00000000..86df672e
--- /dev/null
+++ b/web/regression/javascript/backup/global_server_backup_dialog_spec.js
@@ -0,0 +1,168 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {BackupDialog} from '../../../pgadmin/tools/backup/static/js/backup_dialog';
+import {TreeFake} from '../tree/tree_fake';
+
+const context = describe;
+
+describe('GlobalServerBackupDialog', () => {
+ let backupDialog;
+ let pgBrowser;
+ let jquerySpy;
+ let alertifySpy;
+ let backupModelSpy;
+
+
+ let rootNode;
+ let serverTreeNode;
+ let ppasServerTreeNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: jasmine.createSpyObj('Node[server]', ['getTreeNodeHierarchy']),
+ },
+ };
+ pgBrowser.Nodes.server.hasId = true;
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ backupModelSpy = jasmine.createSpy('backupModelSpy');
+
+ rootNode = pgBrowser.treeMenu.addNewNode('level1', {}, undefined, []);
+ serverTreeNode = pgBrowser.treeMenu.addNewNode('level1.1', {
+ _type: 'server',
+ _id: 10,
+ }, undefined, ['level1']);
+ ppasServerTreeNode = pgBrowser.treeMenu.addNewNode('level1.2', {
+ _type: 'server',
+ server_type: 'ppas',
+ }, undefined, ['level1']);
+ pgBrowser.treeMenu.addNewNode('level3', {}, undefined, ['level1', 'level1.2']);
+ pgBrowser.treeMenu.addNewNode('level3.1', undefined, undefined, ['level1', 'level1.2', 'level3']);
+ });
+
+ describe('#draw', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ alertifySpy['BackupDialog_globals'] = jasmine.createSpy('BackupDialog_globals');
+ alertifySpy['BackupDialog_server'] = jasmine.createSpy('BackupDialog_server');
+ backupDialog = new BackupDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ backupModelSpy
+ );
+
+ pgBrowser.get_preference = jasmine.createSpy('get_preferences');
+ });
+
+ context('there are no ancestors of the type server', () => {
+ it('does not create a dialog', () => {
+ pgBrowser.treeMenu.selectNode([{id: 'level1'}]);
+ backupDialog.draw(null, null, null);
+ expect(alertifySpy['BackupDialog_globals']).not.toHaveBeenCalled();
+ expect(alertifySpy['BackupDialog_server']).not.toHaveBeenCalled();
+ });
+
+ it('display an alert with a Backup Error', () => {
+ backupDialog.draw(null, [rootNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Please select server or child node from the browser tree.'
+ );
+ });
+ });
+
+ context('there is an ancestor of the type server', () => {
+ context('no preference can be found', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue(undefined);
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference pg_bin_dir of module paths'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference ppas_bin_dir of module paths'
+ );
+ });
+ });
+ });
+
+ context('preference can be found', () => {
+ context('binary folder is not configured', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue({});
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the PostgreSQL Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the EDB Advanced Server Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+ });
+
+ context('binary folder is configured', () => {
+ let globalResizeToSpy;
+ let serverResizeToSpy;
+ beforeEach(() => {
+ globalResizeToSpy = jasmine.createSpyObj('globals', ['resizeTo']);
+ alertifySpy['BackupDialog_globals'].and
+ .returnValue(globalResizeToSpy);
+ serverResizeToSpy = jasmine.createSpyObj('server', ['resizeTo']);
+ alertifySpy['BackupDialog_server'].and
+ .returnValue(serverResizeToSpy);
+ pgBrowser.get_preference.and.returnValue({value: '/some/path'});
+ });
+
+ context('dialog for global backup', () => {
+ it('displays the dialog', () => {
+ backupDialog.draw(null, [serverTreeNode], {globals: true});
+ expect(alertifySpy['BackupDialog_globals']).toHaveBeenCalledWith(true);
+ expect(globalResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
+ });
+ });
+
+ context('dialog for server backup', () => {
+ it('displays the dialog', () => {
+ backupDialog.draw(null, [serverTreeNode], {server: true});
+ expect(alertifySpy['BackupDialog_server']).toHaveBeenCalledWith(true);
+ expect(serverResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/backup/menu_utils_spec.js b/web/regression/javascript/backup/menu_utils_spec.js
new file mode 100644
index 00000000..9435d699
--- /dev/null
+++ b/web/regression/javascript/backup/menu_utils_spec.js
@@ -0,0 +1,55 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+
+import {menuEnabledServer} from '../../../pgadmin/tools/backup/static/js/menu_utils';
+
+const context = describe;
+
+describe('backup.menuUtils', () => {
+ describe('#menuEnabledServer', () => {
+ context('provided node data is undefined', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer(undefined)).toBe(false);
+ });
+ });
+
+ context('provided node data is null', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer(null)).toBe(false);
+ });
+ });
+
+ context('current node type is not of the type server', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer({_type: 'schema'})).toBe(false);
+ });
+ });
+
+ context('current node type is of the type server', () => {
+ context('is connected', () => {
+ it('returns true', () => {
+ expect(menuEnabledServer({
+ _type: 'server',
+ connected: true,
+ })).toBe(true);
+ });
+ });
+ context('is not connected', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer({
+ _type: 'server',
+ connected: false,
+ })).toBe(false);
+ });
+ });
+ });
+ });
+});
+
diff --git a/web/regression/javascript/common_keyboard_shortcuts_spec.js b/web/regression/javascript/common_keyboard_shortcuts_spec.js
index 9ea31efd..e27929bf 100644
--- a/web/regression/javascript/common_keyboard_shortcuts_spec.js
+++ b/web/regression/javascript/common_keyboard_shortcuts_spec.js
@@ -11,10 +11,6 @@ import keyboardShortcuts from 'sources/keyboard_shortcuts';
describe('the keyboard shortcuts', () => {
const F1_KEY = 112;
- // const EDIT_KEY = 71; // Key: G -> Grid values
- // const LEFT_ARROW_KEY = 37;
- // const RIGHT_ARROW_KEY = 39;
- // const MOVE_NEXT = 'right';
let debuggerElementSpy, event, debuggerUserShortcutSpy;
debuggerUserShortcutSpy = jasmine.createSpyObj(
diff --git a/web/regression/javascript/datagrid/get_panel_title_spec.js b/web/regression/javascript/datagrid/get_panel_title_spec.js
new file mode 100644
index 00000000..8a344a84
--- /dev/null
+++ b/web/regression/javascript/datagrid/get_panel_title_spec.js
@@ -0,0 +1,82 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {getPanelTitle} from '../../../pgadmin/tools/datagrid/static/js/get_panel_title';
+import {TreeFake} from '../tree/tree_fake';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+const context = describe;
+
+describe('#getPanelTitle', () => {
+ let pgBrowser;
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ pgBrowser = {
+ treeMenu: tree,
+ Nodes: {
+ server: {
+ hasId: true,
+ _type: 'server',
+ },
+ database: {
+ hasId: true,
+ _type: 'database',
+ },
+ },
+ };
+ });
+
+ context('selected node does not belong to a server', () => {
+ it('returns undefined', () => {
+ const root = tree.addNewNode('level1', {_type: 'server_groups'});
+ tree.addChild(root, new TreeNode('level1.1', {_type: 'other'}));
+ tree.selectNode([{id: 'level1'}]);
+ expect(getPanelTitle(pgBrowser)).toBeUndefined();
+ });
+ });
+
+ context('selected node belong to a server', () => {
+ context('selected node does not belong to a database', () => {
+ it('returns the server label and the username', () => {
+ tree.addNewNode('level1', {
+ _type: 'server',
+ db: 'other db label',
+ user: {name: 'some user name'},
+ label: 'server label',
+ }, []);
+
+ tree.selectNode([{id: 'level1'}]);
+ expect(getPanelTitle(pgBrowser))
+ .toBe('other db label on some user name@server label');
+ });
+ });
+
+ context('selected node belongs to a database', () => {
+ it('returns the database label and the username', () => {
+ const root = tree.addNewNode('level1', {
+ _type: 'server',
+ db: 'other db label',
+ user: {name: 'some user name'},
+ label: 'server label',
+ });
+ const level1 = new TreeNode('level1.1', {
+ _type: 'database',
+ label: 'db label',
+ });
+ tree.addChild(root, level1);
+ tree.addChild(level1,
+ new TreeNode('level1.1.1', {_type: 'table'}));
+ tree.selectNode([{id: 'level1.1.1'}]);
+ expect(getPanelTitle(pgBrowser))
+ .toBe('db label on some user name@server label');
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/datagrid/show_data_spec.js b/web/regression/javascript/datagrid/show_data_spec.js
new file mode 100644
index 00000000..80d25eb3
--- /dev/null
+++ b/web/regression/javascript/datagrid/show_data_spec.js
@@ -0,0 +1,171 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {showDataGrid} from '../../../pgadmin/tools/datagrid/static/js/show_data';
+import {TreeFake} from '../tree/tree_fake';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+const context = describe;
+
+describe('#show_data', () => {
+ let datagrid;
+ let pgBrowser;
+ let alertify;
+ beforeEach(() => {
+ alertify = jasmine.createSpyObj('alertify', ['alert']);
+ datagrid = {
+ create_transaction: jasmine.createSpy('create_transaction'),
+ };
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server_group: {
+ _type: 'server_group',
+ hasId: true,
+ },
+ server: {
+ _type: 'server',
+ hasId: true,
+ },
+ database: {
+ _type: 'database',
+ hasId: true,
+ },
+ schema: {
+ _type: 'schema',
+ hasId: true,
+ },
+ view: {
+ _type: 'view',
+ hasId: true,
+ },
+ catalog: {
+ _type: 'catalog',
+ hasId: true,
+ },
+ },
+ };
+ const parent = pgBrowser.treeMenu.addNewNode('parent', {_type: 'parent'}, []);
+ const serverGroup1 = new TreeNode('server_group1', {
+ _type: 'server_group',
+ _id: 1,
+ });
+ pgBrowser.treeMenu.addChild(parent, serverGroup1);
+
+ const server1 = new TreeNode('server1', {
+ _type: 'server',
+ label: 'server1',
+ server_type: 'pg',
+ _id: 2,
+ }, ['parent', 'server_group1']);
+ pgBrowser.treeMenu.addChild(serverGroup1, server1);
+
+ const database1 = new TreeNode('database1', {
+ _type: 'database',
+ label: 'database1',
+ _id: 3,
+ }, ['parent', 'server_group1', 'server1']);
+ pgBrowser.treeMenu.addChild(server1, database1);
+
+ const schema1 = new TreeNode('schema1', {
+ _type: 'schema',
+ label: 'schema1',
+ _id: 4,
+ });
+ pgBrowser.treeMenu.addChild(database1, schema1);
+
+ const view1 = new TreeNode('view1', {
+ _type: 'view',
+ label: 'view1',
+ _id: 5,
+ }, ['parent', 'server_group1', 'server1', 'database1']);
+ pgBrowser.treeMenu.addChild(database1, view1);
+
+ const catalog1 = new TreeNode('catalog1', {
+ _type: 'catalog',
+ label: 'catalog1',
+ _id: 6,
+ }, ['parent', 'server_group1', 'server1', 'database1']);
+ pgBrowser.treeMenu.addChild(database1, catalog1);
+ });
+
+ context('cannot find the tree node', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: '10'}]);
+ expect(datagrid.create_transaction).not.toHaveBeenCalled();
+ });
+
+ it('display alert', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: '10'}]);
+ expect(alertify.alert).toHaveBeenCalledWith(
+ 'Data Grid Error',
+ 'No object selected.'
+ );
+ });
+ });
+
+ context('current node is not underneath a server', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: 'parent'}]);
+ expect(datagrid.create_transaction).not.toHaveBeenCalled();
+ });
+ });
+
+ context('current node is not underneath a schema or view or catalog', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: 'database1'}]);
+ expect(datagrid.create_transaction).not.toHaveBeenCalled();
+ });
+ });
+
+ context('current node is underneath a schema', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {mnuid: 11}, [{id: 'schema1'}]);
+ expect(datagrid.create_transaction).toHaveBeenCalledWith(
+ '/initialize/datagrid/11/schema/1/2/3/4',
+ null,
+ 'false',
+ 'pg',
+ '',
+ 'server1 - database1 - schema1.schema1',
+ ''
+ );
+ });
+ });
+
+ context('current node is underneath a view', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {mnuid: 11}, [{id: 'view1'}]);
+ expect(datagrid.create_transaction).toHaveBeenCalledWith(
+ '/initialize/datagrid/11/view/1/2/3/5',
+ null,
+ 'false',
+ 'pg',
+ '',
+ 'server1 - database1 - view1.view1',
+ ''
+ );
+ });
+ });
+
+ context('current node is underneath a catalog', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {mnuid: 11}, [{id: 'catalog1'}]);
+ expect(datagrid.create_transaction).toHaveBeenCalledWith(
+ '/initialize/datagrid/11/catalog/1/2/3/6',
+ null,
+ 'false',
+ 'pg',
+ '',
+ 'server1 - database1 - catalog1.catalog1',
+ ''
+ );
+ });
+ });
+});
diff --git a/web/regression/javascript/datagrid/show_query_tool_spec.js b/web/regression/javascript/datagrid/show_query_tool_spec.js
new file mode 100644
index 00000000..66bd37ce
--- /dev/null
+++ b/web/regression/javascript/datagrid/show_query_tool_spec.js
@@ -0,0 +1,125 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {TreeFake} from '../tree/tree_fake';
+import {showQueryTool} from '../../../pgadmin/tools/datagrid/static/js/show_query_tool';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+const context = describe;
+
+describe('#showQueryTool', () => {
+ let queryTool;
+ let pgBrowser;
+ let alertify;
+ beforeEach(() => {
+ alertify = jasmine.createSpyObj('alertify', ['alert']);
+ queryTool = {
+ create_transaction: jasmine.createSpy('create_transaction'),
+ };
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server_group: {
+ _type: 'server_group',
+ hasId: true,
+ },
+ server: {
+ _type: 'server',
+ hasId: true,
+ },
+ database: {
+ _type: 'database',
+ hasId: true,
+ },
+ },
+ };
+ const parent = pgBrowser.treeMenu.addNewNode('parent', {_type: 'parent'});
+ const serverGroup1 = new TreeNode('server_group1', {
+ _type: 'server_group',
+ _id: 1,
+ }, ['parent']);
+ pgBrowser.treeMenu.addChild(parent, serverGroup1);
+
+ const server1 = new TreeNode('server1', {
+ _type: 'server',
+ label: 'server1',
+ server_type: 'pg',
+ _id: 2,
+ });
+ pgBrowser.treeMenu.addChild(serverGroup1, server1);
+
+ const database1 = new TreeNode('database1', {
+ _type: 'database',
+ label: 'database1',
+ _id: 3,
+ });
+ pgBrowser.treeMenu.addChild(server1, database1);
+ });
+
+ context('cannot find the tree node', () => {
+ beforeEach(() => {
+ showQueryTool(queryTool, pgBrowser, alertify, '', [{id: '10'}], 'title');
+ });
+ it('does not create a transaction', () => {
+ expect(queryTool.create_transaction).not.toHaveBeenCalled();
+ });
+
+ it('display alert', () => {
+ expect(alertify.alert).toHaveBeenCalledWith(
+ 'Query Tool Error',
+ 'No object selected.'
+ );
+ });
+ });
+
+ context('current node is not underneath a server', () => {
+ it('does not create a transaction', () => {
+ showQueryTool(queryTool, pgBrowser, alertify, '', [{id: 'parent'}], 'title');
+ expect(queryTool.create_transaction).not.toHaveBeenCalled();
+ });
+
+ it('no alert is displayed', () => {
+ expect(alertify.alert).not.toHaveBeenCalled();
+ });
+ });
+
+ context('current node is underneath a server', () => {
+ context('current node is not underneath a database', () => {
+ it('creates a transaction', () => {
+ showQueryTool(queryTool, pgBrowser, alertify, 'http://someurl', [{id: 'server1'}], 'title');
+ expect(queryTool.create_transaction).toHaveBeenCalledWith(
+ '/initialize/query_tool/1/2',
+ null,
+ 'true',
+ 'pg',
+ 'http://someurl',
+ 'title',
+ '',
+ false
+ );
+ });
+ });
+
+ context('current node is underneath a database', () => {
+ it('creates a transaction', () => {
+ showQueryTool(queryTool, pgBrowser, alertify, 'http://someurl', [{id: 'database1'}], 'title');
+ expect(queryTool.create_transaction).toHaveBeenCalledWith(
+ '/initialize/query_tool/1/2/3',
+ null,
+ 'true',
+ 'pg',
+ 'http://someurl',
+ 'title',
+ '',
+ false
+ );
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/fake_endpoints.js b/web/regression/javascript/fake_endpoints.js
index 54b86a94..c060ba78 100644
--- a/web/regression/javascript/fake_endpoints.js
+++ b/web/regression/javascript/fake_endpoints.js
@@ -12,5 +12,11 @@ define(function () {
'static': '/base/pgadmin/static/<path:filename>',
'sqleditor.poll': '/sqleditor/query_tool/poll/<path:trans_id>',
'sqleditor.query_tool_start': '/sqleditor/query_tool/start/<path:trans_id>',
+ 'backup.create_server_job': '/backup/job/<int:sid>',
+ 'backup.create_object_job': '/backup/job/<int:sid>/object',
+ 'datagrid.initialize_datagrid': '/initialize/datagrid/<int:cmd_type>/<obj_type>/<int:sgid>/<int:sid>/<int:did>/<int:obj_id>',
+ 'datagrid.initialize_query_tool': '/initialize/query_tool/<int:sgid>/<int:sid>',
+ 'datagrid.initialize_query_tool_with_did': '/initialize/query_tool/<int:sgid>/<int:sid>/<int:did>',
+ 'restore.create_job': '/restore/job/<int:sid>',
};
});
diff --git a/web/regression/javascript/fake_model.js b/web/regression/javascript/fake_model.js
new file mode 100644
index 00000000..acfaa532
--- /dev/null
+++ b/web/regression/javascript/fake_model.js
@@ -0,0 +1,21 @@
+export class FakeModel {
+ constructor() {
+ this.values = {};
+ }
+
+ set(key, value) {
+ this.values[key] = value;
+ }
+
+ get(key) {
+ return this.values[key];
+ }
+
+ unset(key) {
+ delete this.values[key];
+ }
+
+ toJSON() {
+ return Object.assign({}, this.values);
+ }
+}
diff --git a/web/regression/javascript/menu/can_create_spec.js b/web/regression/javascript/menu/can_create_spec.js
new file mode 100644
index 00000000..6423960f
--- /dev/null
+++ b/web/regression/javascript/menu/can_create_spec.js
@@ -0,0 +1,160 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+/////////////////////////////////////////////////////////////
+
+import {canCreate} from '../../../pgadmin/static/js/menu/can_create';
+import {TreeFake} from '../tree/tree_fake';
+
+const context = describe;
+
+describe('#canCreate', () => {
+ let ourBrowser;
+ let data;
+ let tree;
+
+ context('data is not null and check is false ', () => {
+ beforeEach(() => {
+ data = {action: 'create', check: false};
+ });
+ it('returns true', () => {
+ expect(canCreate({}, {}, {}, data)).toBe(true);
+ });
+ });
+
+ context('data is not null and check is true', () => {
+ beforeEach(() => {
+ data = {action: 'create', check: true};
+ });
+
+ context('is node with type schema', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [
+ {
+ id: 'level2',
+ data: {_type: 'schema'},
+ },
+ ],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ ourBrowser = {treeMenu: tree};
+ });
+
+ it('returns true', () => {
+ expect(canCreate(ourBrowser, 'coll-table', [{id: 'level2'}], data)).toBe(true);
+ });
+ });
+
+ context('has ancestor with type schema', () => {
+ beforeEach(() => {
+
+ let hierarchy = {
+ id: 'root',
+ children: [
+ {
+ id: 'level2',
+ data: {_type: 'schema'},
+ children: [
+ {
+ id: 'level3',
+ data: {_type: 'database'},
+ },
+ ],
+ },
+ ],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ ourBrowser = {treeMenu: tree};
+ });
+
+ it('returns true', () => {
+ expect(canCreate(ourBrowser, 'coll-table', [{id: 'level3'}], data)).toBe(true);
+ });
+ });
+
+ context('when type is not "coll-table"', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [
+ {
+ id: 'level2',
+ data: {_type: 'database'},
+ },
+ ],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ ourBrowser = {treeMenu: tree};
+ });
+
+ it('returns true', () => {
+ expect(canCreate(ourBrowser, 'coll-table', [{id: 'level2'}], data)).toBe(true);
+ });
+ });
+
+ context('when type is "coll-table"', () => {
+ context('when parent type is "catalog"', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [
+ {
+ id: 'level2',
+ data: {_type: 'catalog'},
+ children: [
+ {
+ id: 'level3',
+ data: {_type: 'coll-table'},
+ },
+ ],
+ },
+ ],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ ourBrowser = {treeMenu: tree};
+ });
+
+ it('returns false', () => {
+ expect(canCreate(ourBrowser, 'coll-table', [{id: 'level3'}], data)).toBe(false);
+ });
+ });
+
+ context('when parent type is not "catalog"', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [
+ {
+ id: 'level2',
+ data: {_type: 'database'},
+ children: [
+ {
+ id: 'level3',
+ data: {_type: 'coll-table'},
+ },
+ ],
+ },
+ ],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ ourBrowser = {treeMenu: tree};
+ });
+
+ it('returns false', () => {
+ expect(canCreate(ourBrowser, 'coll-table', [{id: 'level3'}], data)).toBe(true);
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/menu/menu_enabled_spec.js b/web/regression/javascript/menu/menu_enabled_spec.js
new file mode 100644
index 00000000..06967d77
--- /dev/null
+++ b/web/regression/javascript/menu/menu_enabled_spec.js
@@ -0,0 +1,131 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {TreeFake} from '../tree/tree_fake';
+import {menuEnabled} from '../../../pgadmin/static/js/menu/menu_enabled';
+
+const context = describe;
+describe('#menuEnabled', () => {
+ let ourBrowser;
+ beforeEach(() => {
+ const tree = new TreeFake();
+ ourBrowser = {
+ treeMenu: tree,
+ };
+ tree.addNewNode('level1', {}, undefined, []);
+ tree.addNewNode('level1.1', {_type: 'catalog'}, undefined, ['level1']);
+ tree.addNewNode('level1.1.1', {_type: 'database'}, undefined, ['level1', 'level1.1']);
+ tree.addNewNode('level1.2', {_type: 'bamm'}, undefined, ['level1']);
+ tree.addNewNode('level1.2.1', {
+ _type: 'database',
+ allowConn: true,
+ }, undefined, ['level1', 'level1.2']);
+ tree.addNewNode('level1.2.2', {
+ _type: 'database',
+ allowConn: false,
+ }, undefined, ['level1', 'level1.2']);
+ tree.addNewNode('level1.2.3', {
+ _type: 'table',
+ }, undefined, ['level1', 'level1.2']);
+
+ tree.addNewNode('level2', {}, undefined, []);
+ tree.addNewNode('level2.1', null, undefined, ['level2']);
+ tree.addNewNode('level2.1.1', {}, undefined, ['level2', 'level2.1']);
+ });
+
+ context('When the current node is a root node', () => {
+ it('return false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu, [], {}, [{id: 'level1'}])).toBe(false);
+ });
+ });
+
+ context('when current node does not exist', () => {
+ it('return false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu, [], {}, [{id: 'bamm'}])).toBe(false);
+ });
+ });
+
+ context('provided node data is undefined', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu, [], undefined, [{id: 'level1'}])).toBe(false);
+ });
+ });
+
+ context('provided node data is null', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu, [], null, [{id: 'level1'}])).toBe(false);
+ });
+ });
+
+ context('When the current node is not a root node', () => {
+ context('parent data does not exist', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu, [], {}, [{id: 'level2.1.1'}])).toBe(false);
+ });
+ });
+
+ context('parent as data', () => {
+ context('the current node type is in the supported node types', () => {
+ context('the parent is of the type catalog', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu,
+ ['schema'],
+ {_type: 'schema'},
+ [{id: 'level1.1.1'}]
+ )).toBe(false);
+ });
+ });
+ context('an ancestor with type catalog exists', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu,
+ ['table'],
+ {_type: 'table'},
+ [{id: 'level1.1.1.1'}]
+ )).toBe(false);
+ });
+ });
+ context('the parent is not of the type catalog', () => {
+ context('current node is of the type database', () => {
+ context('current node allows connection', () => {
+ it('returns true', () => {
+ expect(menuEnabled(ourBrowser.treeMenu,
+ ['database'],
+ {
+ _type: 'database',
+ allowConn: true,
+ }, [{id: 'level1.2.1'}])).toBe(true);
+ });
+ });
+ context('current node do not allow connection', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu,
+ ['database'], {
+ _type: 'database',
+ allowConn: false,
+ }, [{id: 'level1.2.2'}])).toBe(false);
+ });
+ });
+ });
+ context('current node is not of the type database', () => {
+ it('returns true', () => {
+ expect(menuEnabled(ourBrowser.treeMenu,
+ ['schema'], {
+ _type: 'schema',
+ }, [{id: 'level1.2.3'}])).toBe(true);
+ });
+ });
+ });
+ });
+ context('the current node type is not in the supported node types', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu, [], {_type: 'catalog'}, [{id: 'level1.1'}])).toBe(false);
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/restore/restore_dialog_spec.js b/web/regression/javascript/restore/restore_dialog_spec.js
new file mode 100644
index 00000000..156f56bb
--- /dev/null
+++ b/web/regression/javascript/restore/restore_dialog_spec.js
@@ -0,0 +1,203 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {TreeFake} from '../tree/tree_fake';
+import {RestoreDialog} from '../../../pgadmin/tools/restore/static/js/restore_dialog';
+
+const context = describe;
+
+describe('RestoreDialog', () => {
+ let restoreDialog;
+ let pgBrowser;
+ let jquerySpy;
+ let alertifySpy;
+ let restoreModelSpy;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: jasmine.createSpyObj('Node[server]', ['getTreeNodeHierarchy']),
+ database: jasmine.createSpyObj('Node[database]', ['getTreeNodeHierarchy']),
+ },
+ };
+ pgBrowser.Nodes.server.hasId = true;
+ pgBrowser.Nodes.database.hasId = true;
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ restoreModelSpy = jasmine.createSpy('restoreModelSpy');
+
+ const hierarchy = {
+ children: [
+ {
+ id: 'root',
+ children: [
+ {
+ id: 'serverTreeNode',
+ data: {
+ _id: 10,
+ _type: 'server',
+ label: 'some-tree-label',
+ },
+ children: [
+ {
+ id: 'some_database',
+ data: {
+ _type: 'database',
+ _id: 11,
+ label: 'some_database',
+ _label: 'some_database_label',
+ },
+ }, {
+ id: 'database_with_equal_in_name',
+ data: {
+ _type: 'database',
+ label: 'some_database',
+ _label: '=some_database_label',
+ },
+ },
+ ],
+ },
+ {
+ id: 'ppasServer',
+ data: {
+ _type: 'server',
+ server_type: 'ppas',
+ children: [
+ {id: 'someNodeUnderneathPPASServer'},
+ ],
+ },
+ },
+ ],
+ },
+ ],
+ };
+
+ pgBrowser.treeMenu = TreeFake.build(hierarchy);
+ });
+
+ describe('#draw', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ alertifySpy['pg_restore'] = jasmine.createSpy('pg_restore');
+ restoreDialog = new RestoreDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ restoreModelSpy
+ );
+
+ pgBrowser.get_preference = jasmine.createSpy('get_preferences');
+ });
+
+ context('there are no ancestors of the type server', () => {
+ it('does not create a dialog', () => {
+ pgBrowser.treeMenu.selectNode([{id: 'root'}]);
+ restoreDialog.draw(null, null, null);
+ expect(alertifySpy['pg_restore']).not.toHaveBeenCalled();
+ });
+
+ it('display an alert with a Restore Error', () => {
+ restoreDialog.draw(null, [{id: 'root'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Restore Error',
+ 'Please select server or child node from the browser tree.'
+ );
+ });
+ });
+
+ context('there is an ancestor of the type server', () => {
+ context('no preference can be found', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue(undefined);
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Restore Error"', () => {
+ restoreDialog.draw(null, [{id: 'serverTreeNode'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Restore Error',
+ 'Failed to load preference pg_bin_dir of module paths'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Restore Error"', () => {
+ restoreDialog.draw(null, [{id: 'ppasServer'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Restore Error',
+ 'Failed to load preference ppas_bin_dir of module paths'
+ );
+ });
+ });
+ });
+
+ context('preference can be found', () => {
+ context('binary folder is not configured', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue({});
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ restoreDialog.draw(null, [{id: 'serverTreeNode'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the PostgreSQL Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ restoreDialog.draw(null, [{id: 'ppasServer'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the EDB Advanced Server Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+ });
+
+ context('binary folder is configured', () => {
+ let spy;
+ beforeEach(() => {
+ spy = jasmine.createSpyObj('globals', ['resizeTo']);
+ alertifySpy['pg_restore'].and
+ .returnValue(spy);
+ pgBrowser.get_preference.and.returnValue({value: '/some/path'});
+ pgBrowser.Nodes.server.label = 'some-server-label';
+ });
+
+ it('displays the dialog', () => {
+ restoreDialog.draw(null, [{id: 'serverTreeNode'}], {server: true});
+ expect(alertifySpy['pg_restore']).toHaveBeenCalledWith(
+ 'Restore (some-server-label: some-tree-label)',
+ [{id: 'serverTreeNode'}],
+ {
+ _id: 10,
+ _type: 'server',
+ label: 'some-tree-label',
+ },
+ pgBrowser.Nodes.server
+ );
+ expect(spy.resizeTo).toHaveBeenCalledWith('65%', '60%');
+ });
+
+ context('database label contain "="', () => {
+ it('should create alert dialog with restore error', () => {
+ restoreDialog.draw(null, [{id: 'database_with_equal_in_name'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith('Restore Error',
+ 'Databases with = symbols in the name cannot be backed up or restored using this utility.');
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/restore/restore_dialog_wrapper_spec.js b/web/regression/javascript/restore/restore_dialog_wrapper_spec.js
new file mode 100644
index 00000000..c2a31d55
--- /dev/null
+++ b/web/regression/javascript/restore/restore_dialog_wrapper_spec.js
@@ -0,0 +1,593 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {TreeFake} from '../tree/tree_fake';
+import {RestoreDialogWrapper} from '../../../pgadmin/tools/restore/static/js/restore_dialog_wrapper';
+import MockAdapter from 'axios-mock-adapter';
+import axios from 'axios/index';
+import {FakeModel} from '../fake_model';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+let context = describe;
+
+describe('RestoreDialogWrapper', () => {
+ let jquerySpy;
+ let pgBrowser;
+ let alertifySpy;
+ let dialogModelKlassSpy;
+ let backform;
+ let generatedRestoreModel;
+ let restoreDialogWrapper;
+ let noDataNode;
+ let serverTreeNode;
+ let viewSchema;
+ let restoreJQueryContainerSpy;
+ let restoreNodeChildNodeSpy;
+ let restoreNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: {
+ hasId: true,
+ getTreeNodeHierarchy: jasmine.createSpy('getTreeNodeHierarchy'),
+ },
+ },
+ keyboardNavigation: jasmine.createSpyObj('keyboardNavigation', ['getDialogTabNavigator']),
+ };
+
+ noDataNode = pgBrowser.treeMenu.addNewNode('level1.1', undefined, [{id: 'level1'}]);
+ serverTreeNode = pgBrowser.treeMenu.addNewNode('level2.1', {
+ _type: 'server',
+ _id: 10,
+ label: 'some-tree-label',
+ }, [{id: 'level2.1'}]);
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ dialogModelKlassSpy = jasmine.createSpy('dialogModelKlass');
+ generatedRestoreModel = {};
+ viewSchema = {};
+ backform = jasmine.createSpyObj('backform', ['generateViewSchema', 'Dialog']);
+ backform.generateViewSchema.and.returnValue(viewSchema);
+ dialogModelKlassSpy.and.returnValue(generatedRestoreModel);
+ restoreJQueryContainerSpy = jasmine.createSpyObj('restoreJQueryContainer', ['get', 'attr']);
+ restoreJQueryContainerSpy.get.and.returnValue(restoreJQueryContainerSpy);
+
+ restoreNode = {
+ __internal: {
+ buttons: [
+ {}, {},
+ {
+ element: {
+ disabled: false,
+ },
+ },
+ ],
+ },
+ elements: {
+ body: {
+ childNodes: [
+ {},
+ ],
+ },
+ content: jasmine.createSpyObj('content', ['appendChild', 'attr']),
+ },
+ };
+
+
+ restoreNodeChildNodeSpy = jasmine.createSpyObj('something', ['addClass']);
+
+ jquerySpy.and.callFake((selector) => {
+ if (selector === '<div class=\'restore_dialog\'></div>') {
+ return restoreJQueryContainerSpy;
+ } else if (selector === restoreNode.elements.body.childNodes[0]) {
+ return restoreNodeChildNodeSpy;
+ }
+ });
+ });
+
+ describe('#prepare', () => {
+
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ restoreDialogWrapper = new RestoreDialogWrapper(
+ '<div class=\'restore_dialog\'></div>',
+ 'restoreDialogTitle',
+ 'restore',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+ restoreDialogWrapper = Object.assign(restoreDialogWrapper, restoreNode);
+ });
+ context('no tree element is selected', () => {
+ it('does not create a backform dialog', () => {
+ restoreDialogWrapper.prepare();
+ expect(backform.Dialog).not.toHaveBeenCalled();
+ });
+
+ it('disables the button "submit button" until a filename is selected', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreDialogWrapper.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('selected tree node has no data', () => {
+ beforeEach(() => {
+ pgBrowser.treeMenu.selectNode(noDataNode.domNode);
+ });
+
+ it('does not create a backform dialog', () => {
+ restoreDialogWrapper.prepare();
+ expect(backform.Dialog).not.toHaveBeenCalled();
+ });
+
+ it('disables the button "submit button" until a filename is selected', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreDialogWrapper.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('tree element is selected', () => {
+ let treeHierarchyInformation;
+ let dialogSpy;
+ beforeEach(() => {
+ treeHierarchyInformation = {
+ server: {
+ _type: 'server',
+ _id: 10,
+ priority: 0,
+ label: 'some-tree-label',
+ },
+ };
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ pgBrowser.Nodes['server'].getTreeNodeHierarchy.and
+ .returnValue(treeHierarchyInformation);
+ dialogSpy = jasmine.createSpyObj('newView', ['render']);
+ dialogSpy.$el = jasmine.createSpyObj('$el', ['find', 'attr']);
+ dialogSpy.model = jasmine.createSpyObj('model', ['on']);
+ dialogSpy.$el.find.and.returnValue([]);
+
+ backform.Dialog.and.returnValue(dialogSpy);
+ });
+
+ it('creates a backform dialog and displays it', () => {
+ restoreDialogWrapper.prepare();
+ expect(backform.Dialog).toHaveBeenCalledWith({
+ el: restoreJQueryContainerSpy,
+ model: generatedRestoreModel,
+ schema: viewSchema,
+ });
+
+ expect(dialogSpy.render).toHaveBeenCalled();
+ });
+
+ it('add alertify classes to restore node childnode', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreNodeChildNodeSpy.addClass)
+ .toHaveBeenCalledWith('alertify_tools_dialog_properties obj_properties');
+ });
+
+ it('disables the button submit button until a filename is selected', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreNode.__internal.buttons[2].element.disabled).toBe(true);
+ });
+
+ it('generates a new restore model', () => {
+ restoreDialogWrapper.prepare();
+ expect(dialogModelKlassSpy).toHaveBeenCalledWith(
+ {node_data: pgBrowser.Nodes['server']},
+ {node_info: treeHierarchyInformation}
+ );
+ });
+
+ it('add the new dialog to the restore node HTML', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreNode.elements.content.appendChild).toHaveBeenCalledWith(restoreJQueryContainerSpy);
+ });
+ });
+ });
+
+ describe('onButtonClicked', () => {
+ let networkMock;
+
+ beforeEach(() => {
+ pgBrowser.showHelp = jasmine.createSpy('showHelp');
+ networkMock = new MockAdapter(axios);
+ alertifySpy = jasmine.createSpyObj('alertify', ['success', 'alert']);
+ restoreDialogWrapper = new RestoreDialogWrapper(
+ '<div class=\'restore_dialog\'></div>',
+ 'restoreDialogTitle',
+ 'restore',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+ restoreDialogWrapper = Object.assign(restoreDialogWrapper, restoreNode);
+
+ });
+
+ afterEach(function () {
+ networkMock.restore();
+ });
+
+ context('dialog help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ element: {
+ name: 'dialog_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ },
+ },
+ },
+ };
+ restoreDialogWrapper.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'dialog_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ );
+ });
+
+ it('does not start the restore', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('object help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ element: {
+ name: 'object_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ },
+ },
+ },
+ };
+ restoreDialogWrapper.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'object_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ );
+ });
+
+ it('does not start the restore', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('restore button was pressed', () => {
+ let networkCalled;
+ let event;
+
+ context('no tree node is selected', () => {
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+ event = {
+ button: {
+ 'data-btn-name': 'restore',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ });
+
+ it('does not start the restore', () => {
+ restoreDialogWrapper.callback(event);
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('tree node selected has no data', () => {
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+ event = {
+ button: {
+ 'data-btn-name': 'restore',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ pgBrowser.treeMenu.selectNode(noDataNode.domNode);
+ });
+
+ it('does not start the restore', () => {
+ restoreDialogWrapper.callback(event);
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('tree node select has data', () => {
+
+ let databaseTreeNode;
+
+ beforeEach(() => {
+ databaseTreeNode = pgBrowser.treeMenu.addNewNode('level3.1', {
+ _type: 'database',
+ _id: 10,
+ _label: 'some-database-label',
+ }, [{id: 'level3.1'}]);
+ pgBrowser.treeMenu.addChild(serverTreeNode, databaseTreeNode);
+ pgBrowser.Nodes.database = {
+ hasId: true,
+ _label: 'some-database-label',
+ };
+ let fakeModel = new FakeModel();
+ fakeModel.set('some-key', 'some-value');
+ restoreDialogWrapper.view = {
+ model: fakeModel,
+ };
+ pgBrowser.treeMenu.selectNode(databaseTreeNode.domNode);
+ pgBrowser.Events = jasmine.createSpyObj('pgBrowserEventsSpy', ['trigger']);
+ event = {
+ button: {
+ 'data-btn-name': 'restore',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ });
+ context('restore job created successfully', () => {
+ let dataSentToServer;
+ beforeEach(() => {
+ networkMock.onPost('/restore/job/10').reply((request) => {
+ dataSentToServer = request.data;
+ return [200, {}];
+ });
+ });
+
+ it('create an success alert box', (done) => {
+ restoreDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.success).toHaveBeenCalledWith(
+ 'Restore job created.',
+ 5
+ );
+ done();
+ }, 0);
+ });
+
+ it('trigger background process', (done) => {
+ restoreDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(pgBrowser.Events.trigger).toHaveBeenCalledWith(
+ 'pgadmin-bgprocess:created',
+ restoreDialogWrapper
+ );
+ done();
+ }, 0);
+ });
+
+ it('send correct data to server', (done) => {
+ restoreDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(JSON.parse(dataSentToServer)).toEqual({
+ 'some-key': 'some-value',
+ 'database': 'some-database-label',
+ });
+ done();
+ }, 0);
+ });
+ });
+
+ context('error creating restore job', () => {
+ beforeEach(() => {
+ networkMock.onPost('/restore/job/10').reply(() => {
+ return [400, {}];
+ });
+ });
+
+ it('creates an alert box', (done) => {
+ restoreDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Restore job failed.',
+ undefined
+ );
+ done();
+ }, 0);
+ });
+ });
+ });
+ });
+ });
+
+ describe('setExtraParameters', () => {
+ let selectedNode;
+ let treeInfo;
+ let model;
+
+ beforeEach(() => {
+ restoreDialogWrapper = new RestoreDialogWrapper(
+ '<div class=\'restore_dialog\'></div>',
+ 'restoreDialogTitle',
+ 'restore',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ model = new FakeModel();
+ restoreDialogWrapper.view = {
+ model: model,
+ };
+ });
+
+ context('when it is a custom model', () => {
+ beforeEach(() => {
+ model.set('custom', true);
+ treeInfo = {
+ 'database': {
+ '_label': 'some-database-label',
+ },
+ };
+ });
+
+ it('only sets the database', () => {
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ 'custom': true,
+ 'database': 'some-database-label',
+ });
+ });
+ });
+
+ context('when it is not a custom model', () => {
+ beforeEach(() => {
+ model.set('custom', false);
+ treeInfo = {
+ 'database': {
+ '_label': 'some-database-label',
+ },
+ 'schema': {
+ '_label': 'some-schema-label',
+ },
+ };
+ });
+
+ context('when selected node is a schema', () => {
+ it('sets schemas on the model', () => {
+ selectedNode = new TreeNode('schema', {_type: 'schema', _label: 'some-schema-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ });
+ });
+ });
+
+ context('when selected node is a table', () => {
+ it('sets schemas and table on the model', () => {
+ selectedNode = new TreeNode('table', {_type: 'table', _label: 'some-table-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ tables: ['some-table-label'],
+ });
+ });
+ });
+
+ context('when selected node is a function', () => {
+ it('sets schemas and function on the model', () => {
+ selectedNode = new TreeNode('function', {_type: 'function', _label: 'some-function-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ functions: ['some-function-label'],
+ });
+ });
+ });
+
+ context('when selected node is an index', () => {
+ it('sets schemas and index on the model', () => {
+ selectedNode = new TreeNode('index', {_type: 'index', _label: 'some-index-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ indexes: ['some-index-label'],
+ });
+ });
+ });
+
+ context('when selected node is a trigger', () => {
+ it('sets schemas and trigger on the model', () => {
+ selectedNode = new TreeNode('trigger', {_type: 'trigger', _label: 'some-trigger-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ triggers: ['some-trigger-label'],
+ });
+ });
+ });
+
+ context('when selected node is a trigger_func', () => {
+ it('sets schemas and trigger_func on the model', () => {
+ selectedNode = new TreeNode('trigger_func', {_type: 'trigger_func', _label: 'some-trigger_func-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ trigger_funcs: ['some-trigger_func-label'],
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/schema/can_drop_child_spec.js b/web/regression/javascript/schema/can_drop_child_spec.js
new file mode 100644
index 00000000..4403b274
--- /dev/null
+++ b/web/regression/javascript/schema/can_drop_child_spec.js
@@ -0,0 +1,82 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {canDropChild} from '../../../pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child';
+import {TreeFake} from '../tree/tree_fake';
+
+let context = describe;
+
+describe('can_drop_child', () => {
+
+ let browser;
+ let itemData;
+ let item;
+
+ beforeEach(() => {
+ browser = {
+ treeMenu: new TreeFake(),
+ };
+ item = [];
+ browser.treeMenu.addNewNode('node1', {_type: 'schema'}, [{id: 'node1'}], []);
+ browser.treeMenu.addNewNode('node1.1', {_type: 'database'}, [{id: 'node1.1'}], ['node1']);
+ browser.treeMenu.addNewNode('node2', {_type: 'catalog'}, [{id: 'node2'}], []);
+ browser.treeMenu.addNewNode('node2.1', {_type: 'table'}, [{id: 'node2.1'}], ['node2']);
+ browser.treeMenu.addNewNode('node3', {_type: 'function'}, [{id: 'node3'}], []);
+ browser.treeMenu.addNewNode('node3.1', {_type: 'procedure'}, [{id: 'node3.1'}], ['node3']);
+ });
+
+ context('when current node is of the type schema', () => {
+ beforeEach(() => {
+ itemData = {
+ _type: 'schema',
+ };
+ item = [{id: 'node1'}];
+ });
+
+ it('returns true', () => {
+ let bool = canDropChild(browser, itemData, item);
+ expect(bool).toBe(true);
+ });
+ });
+
+ context('when a parent of the current node is a schema', () => {
+ it('returns true', () => {
+ itemData = {
+ _type: 'database',
+ };
+ item = [{id: 'node1.1'}];
+ let bool = canDropChild(browser, itemData, item);
+ expect(bool).toBe(true);
+ });
+ });
+
+ context('when a parent of the current node is a catalog', () => {
+ it('returns false', () => {
+ itemData= {
+ _type: 'table',
+ };
+ item = [{id: 'node2.1'}];
+
+ let bool = canDropChild(browser, itemData, item);
+ expect(bool).toBe(false);
+ });
+ });
+
+ context('when a parent of the current node is not catalog nor schema', () => {
+ it('returns true', () => {
+ itemData = {
+ _type: 'procedure',
+ };
+ item = [{id: 'node3.1'}];
+
+ let bool = canDropChild(browser, itemData, item);
+ expect(bool).toBe(true);
+ });
+ });
+});
diff --git a/web/regression/javascript/sqleditor/filter_dialog_specs.js b/web/regression/javascript/sqleditor/filter_dialog_specs.js
index ed77dff5..cea75e6b 100644
--- a/web/regression/javascript/sqleditor/filter_dialog_specs.js
+++ b/web/regression/javascript/sqleditor/filter_dialog_specs.js
@@ -7,10 +7,8 @@
//
//////////////////////////////////////////////////////////////////////////
import filterDialog from 'sources/sqleditor/filter_dialog';
-// import filterDialogModel from 'sources/sqleditor/filter_dialog_model';
describe('filterDialog', () => {
- jasmine.createSpy('sqlEditorController');
describe('filterDialog', () => {
describe('when using filter dialog', () => {
beforeEach(() => {
diff --git a/web/regression/javascript/table/enable_disable_triggers_spec.js b/web/regression/javascript/table/enable_disable_triggers_spec.js
new file mode 100644
index 00000000..7bdd284e
--- /dev/null
+++ b/web/regression/javascript/table/enable_disable_triggers_spec.js
@@ -0,0 +1,271 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import MockAdapter from 'axios-mock-adapter';
+import axios from 'axios/index';
+import {
+ enableTriggers,
+ disableTriggers,
+} from '../../../pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers';
+import {TreeFake} from '../tree/tree_fake';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+describe('#enableTriggers', () => {
+ let networkMock;
+ let tree;
+ let alertify;
+ let generateUrlSpy;
+ beforeEach(() => {
+ networkMock = new MockAdapter(axios);
+ tree = new TreeFake();
+ const server1 = tree.addNewNode('server1', {_id: 1}, ['<li>server1</li>']);
+ const database1 = tree.addNewNode('database1', {_type: 'database'}, ['<li>database1</li>']);
+ tree.addChild(server1, database1);
+
+ const schema1 = tree.addNewNode('schema1', {_type: 'schema'}, ['<li>schema1</li>']);
+ tree.addChild(database1, schema1);
+
+ const table1 = tree.addNewNode('table1', {_type: 'table'}, ['<li>table1</li>']);
+ tree.addChild(schema1, table1);
+
+ const column1 = tree.addNewNode('column1', {_type: 'column'}, ['<li>column1</li>']);
+ tree.addChild(table1, column1);
+
+ const tableNoData = tree.addNewNode('table-no-data', undefined, ['<li>table-no-data</li>']);
+ tree.addChild(schema1, tableNoData);
+
+ alertify = jasmine.createSpyObj('alertify', ['success', 'error']);
+ generateUrlSpy = jasmine.createSpy('generateUrl');
+ generateUrlSpy.and.returnValue('/some/place');
+ });
+
+ describe('no node is selected', () => {
+ it('does not send the request to the backend', (done) => {
+ networkMock.onAny('.*').reply(200, () => {
+ });
+
+ setTimeout(() => {
+ expect(enableTriggers(tree, alertify, generateUrlSpy, {})).toBe(false);
+ done();
+ }, 0);
+ });
+ });
+
+ describe('a node is selected', () => {
+ describe('node as no data', () => {
+ it('does not send the request to the backend', () => {
+ tree.selectNode([{id: 'table-no-data'}]);
+
+ networkMock.onAny('.*').reply(200, () => {
+ });
+
+ setTimeout(() => {
+ expect(enableTriggers(tree, alertify, generateUrlSpy, {})).toBe(false);
+ }, 0);
+ });
+ });
+
+ describe('node as data', () => {
+ describe('backend responds with success', () => {
+ let networkMockCalledWith;
+ beforeEach(() => {
+ networkMockCalledWith = false;
+ networkMock.onPut(/.*/).reply((configuration) => {
+ networkMockCalledWith = configuration;
+ return [200, {
+ success: 1,
+ info: 'some information',
+ }];
+ });
+ });
+
+ it('displays an alert box with success', (done) => {
+ tree.selectNode([{id: 'table1'}]);
+ enableTriggers(tree, alertify, generateUrlSpy, {});
+ setTimeout(() => {
+ expect(alertify.success).toHaveBeenCalledWith('some information');
+ done();
+ }, 0);
+ });
+
+ it('reloads the node', (done) => {
+ enableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+ setTimeout(() => {
+ expect(tree.selected()).toEqual(['<li>table1</li>']);
+ done();
+ }, 20);
+ });
+
+ it('call backend with the correct parameters', (done) => {
+ enableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+ setTimeout(() => {
+ expect(networkMockCalledWith.data).toEqual(JSON.stringify({enable: 'true'}));
+ done();
+ }, 0);
+ });
+ });
+
+ describe('backend responds with error', () => {
+ beforeEach(() => {
+ networkMock.onPut(/.*/).reply(() => {
+ return [500, {
+ success: 0,
+ errormsg: 'some error message',
+ }];
+ });
+ });
+
+ it('displays an error alert', (done) => {
+ tree.selectNode([{id: 'table1'}]);
+ enableTriggers(tree, alertify, generateUrlSpy, {});
+ setTimeout(() => {
+ expect(alertify.error).toHaveBeenCalledWith('some error message');
+ done();
+ }, 0);
+ });
+
+ it('unload the node', (done) => {
+ enableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+
+ setTimeout(() => {
+ expect(tree.findNodeByDomElement([{id: 'table1'}]).children.length).toBe(0);
+ done();
+ }, 20);
+ });
+ });
+ });
+ });
+});
+
+describe('#disableTriggers', () => {
+ let networkMock;
+ let tree;
+ let alertify;
+ let generateUrlSpy;
+ beforeEach(() => {
+ networkMock = new MockAdapter(axios);
+ tree = new TreeFake();
+ const server1 = tree.addNewNode('server1', {_id: 1}, ['<li>server1</li>']);
+ const database1 = new TreeNode('database1', {_type: 'database'}, ['<li>database1</li>']);
+ tree.addChild(server1, database1);
+
+ const schema1 = new TreeNode('schema1', {_type: 'schema'}, ['<li>schema1</li>']);
+ tree.addChild(database1, schema1);
+
+ const table1 = new TreeNode('table1', {_type: 'table'}, ['<li>table1</li>']);
+ tree.addChild(schema1, table1);
+
+ const column1 = new TreeNode('column1', {_type: 'column'}, ['<li>column1</li>']);
+ tree.addChild(table1, column1);
+
+ const tableNoData = new TreeNode('table-no-data', undefined, ['<li>table-no-data</li>']);
+ tree.addChild(schema1, tableNoData);
+
+ alertify = jasmine.createSpyObj('alertify', ['success', 'error']);
+ generateUrlSpy = jasmine.createSpy('generateUrl');
+ generateUrlSpy.and.returnValue('/some/place');
+ });
+
+ describe('no node is selected', () => {
+ it('does not send the request to the backend', (done) => {
+ networkMock.onAny('.*').reply(200, () => {
+ });
+
+ setTimeout(() => {
+ expect(disableTriggers(tree, alertify, generateUrlSpy, {})).toBe(false);
+ done();
+ }, 0);
+ });
+ });
+
+ describe('a node is selected', () => {
+ describe('node as no data', () => {
+ it('does not send the request to the backend', () => {
+ tree.selectNode([{id: 'table-no-data'}]);
+
+ networkMock.onAny('.*').reply(200, () => {
+ });
+
+ setTimeout(() => {
+ expect(disableTriggers(tree, alertify, generateUrlSpy, {})).toBe(false);
+ }, 0);
+ });
+ });
+
+ describe('node as data', () => {
+ describe('backend responds with success', () => {
+ let networkMockCalledWith;
+ beforeEach(() => {
+ networkMockCalledWith = false;
+ networkMock.onPut(/.*/).reply((configuration) => {
+ networkMockCalledWith = configuration;
+ return [200, {
+ success: 1,
+ info: 'some information',
+ }];
+ });
+ });
+
+ it('displays an alert box with success', (done) => {
+ tree.selectNode([{id: 'table1'}]);
+ disableTriggers(tree, alertify, generateUrlSpy, {});
+ setTimeout(() => {
+ expect(alertify.success).toHaveBeenCalledWith('some information');
+ done();
+ }, 0);
+ });
+
+ it('reloads the node', (done) => {
+ disableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+ setTimeout(() => {
+ expect(tree.selected()).toEqual(['<li>table1</li>']);
+ done();
+ }, 20);
+ });
+
+ it('call backend with the correct parameters', (done) => {
+ disableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+ setTimeout(() => {
+ expect(networkMockCalledWith.data).toEqual(JSON.stringify({enable: 'false'}));
+ done();
+ }, 0);
+ });
+ });
+
+ describe('backend responds with error', () => {
+ beforeEach(() => {
+ networkMock.onPut(/.*/).reply(() => {
+ return [500, {
+ success: 0,
+ errormsg: 'some error message',
+ }];
+ });
+ });
+
+ it('displays an error alert', (done) => {
+ tree.selectNode([{id: 'table1'}]);
+ disableTriggers(tree, alertify, generateUrlSpy, {});
+ setTimeout(() => {
+ expect(alertify.error).toHaveBeenCalledWith('some error message');
+ done();
+ }, 0);
+ });
+
+ it('unload the node', (done) => {
+ disableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+
+ setTimeout(() => {
+ expect(tree.findNodeByDomElement([{id: 'table1'}]).children.length).toBe(0);
+ done();
+ }, 20);
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/tree/pgadmin_tree_node_spec.js b/web/regression/javascript/tree/pgadmin_tree_node_spec.js
new file mode 100644
index 00000000..479e515c
--- /dev/null
+++ b/web/regression/javascript/tree/pgadmin_tree_node_spec.js
@@ -0,0 +1,353 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+import {
+ getTreeNodeHierarchyFromElement,
+ getTreeNodeHierarchyFromIdentifier,
+} from '../../../pgadmin/static/js/tree/pgadmin_tree_node';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+import {TreeFake} from './tree_fake';
+
+const context = describe;
+
+describe('tree#node#getTreeNodeHierarchy', () => {
+ let browser;
+ let newTree;
+ beforeEach(() => {
+ newTree = new TreeFake();
+ browser = {
+ Nodes: {
+ 'special one': {hasId: true},
+ 'child special': {hasId: true},
+ 'other type': {hasId: true},
+ 'table': {hasId: true},
+ 'partition': {hasId: true},
+ 'no id': {hasId: false},
+ },
+ };
+ browser.treeMenu = newTree;
+ });
+
+ context('getTreeNodeHierarchy is called with aciTreeNode object', () => {
+ describe('When the current node is root element', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ });
+ });
+
+ it('returns a object with the element type passed data and priority == 0', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)([{id: 'root'}]);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+
+ describe('When the current node is not of a known type', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'do not exist',
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)('root');
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node type has no id', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'no id',
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)('root');
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node is at the second level', () => {
+ beforeEach(() => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ });
+ const firstChild = new TreeNode('first child', {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ }, ['root']);
+ newTree.addChild(root, firstChild);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)([{id: 'first child'}]);
+ expect(result).toEqual({
+ 'child special': {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ 'priority': 0,
+ },
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+
+ context('When tree as "special type"', () => {
+ context('When "special type" have "other type"', () => {
+ context('When "other type" have "special type"', () => {
+ describe('When retrieving lead node', () => {
+ it('does not override previous node type data', () => {
+ const rootNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+
+ const level1 = new TreeNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'other type',
+ });
+ newTree.addChild(rootNode, level1);
+
+ newTree.addChild(level1, new TreeNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ }));
+
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)([{id: 'level 2'}]);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ 'priority': 0,
+ },
+ 'other type': {
+ 'some key': 'some value',
+ '_type': 'other type',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+ });
+ });
+ });
+
+ context('When tree has table', () => {
+ context('when table has partition', () => {
+ it('returns table with partition parameters', () => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+ const level1 = new TreeNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'table',
+ });
+ newTree.addChild(root, level1);
+ newTree.addChild(level1, new TreeNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'partition',
+ 'some other key': 'some other value',
+ }));
+
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)([{id:'level 2'}]);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ 'table': {
+ 'some key': 'expected value',
+ 'some other key': 'some other value',
+ '_type': 'partition',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+ });
+ });
+
+ context('getTreeNodeHierarchy is called with TreeNode object', () => {
+ let treeNode;
+ describe('When the current node is root element', () => {
+ beforeEach(() => {
+ treeNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+ });
+
+ it('returns a object with the element type passed data and priority == 0', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+
+ describe('When the current node is not of a known type', () => {
+ beforeEach(() => {
+ treeNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'do not exist',
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node type has no id', () => {
+ beforeEach(() => {
+ treeNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'no id',
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node is at the second level', () => {
+ beforeEach(() => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+ treeNode = new TreeNode('first child', {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ });
+ newTree.addChild(root, treeNode);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'child special': {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ 'priority': 0,
+ },
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+
+ context('When tree as "special type"', () => {
+ context('When "special type" have "other type"', () => {
+ context('When "other type" have "special type"', () => {
+ describe('When retrieving lead node', () => {
+ it('does not override previous node type data', () => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+ const level1 = new TreeNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'other type',
+ });
+ newTree.addChild(root, level1);
+ treeNode = new TreeNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ });
+ newTree.addChild(level1, treeNode);
+
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ 'priority': 0,
+ },
+ 'other type': {
+ 'some key': 'some value',
+ '_type': 'other type',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+ });
+ });
+ });
+
+ context('When tree has table', () => {
+ context('when table has partition', () => {
+ it('returns table with partition parameters', () => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ });
+ const level1 = newTree.addNewNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'table',
+ });
+ newTree.addChild(root, level1);
+ treeNode = new TreeNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'partition',
+ 'some other key': 'some other value',
+ });
+ newTree.addChild(level1, treeNode);
+
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ 'table': {
+ 'some key': 'expected value',
+ 'some other key': 'some other value',
+ '_type': 'partition',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/tree/tree_fake.js b/web/regression/javascript/tree/tree_fake.js
index b285a45f..e03d71fb 100644
--- a/web/regression/javascript/tree/tree_fake.js
+++ b/web/regression/javascript/tree/tree_fake.js
@@ -10,6 +10,32 @@
import {Tree} from '../../../pgadmin/static/js/tree/tree';
export class TreeFake extends Tree {
+ static build(structure) {
+ let tree = new TreeFake();
+ let rootNode = tree.rootNode;
+
+ if (structure.children !== undefined) {
+ structure.children.forEach((child) => {
+ TreeFake.recursivelyAddNodes(tree, child, rootNode);
+ });
+ }
+
+ return tree;
+ }
+
+ static recursivelyAddNodes(tree, newNode, parent) {
+ let id = newNode.id;
+ let data = newNode.data ? newNode.data : {};
+ let domNode = newNode.domNode ? newNode.domNode : [{id: id}];
+ tree.addNewNode(id, data, domNode, tree.translateTreeNodeIdFromACITree([parent]));
+
+ if (newNode.children !== undefined) {
+ newNode.children.forEach((child) => {
+ TreeFake.recursivelyAddNodes(tree, child, newNode);
+ });
+ }
+ }
+
constructor() {
super();
this.aciTreeToOurTreeTranslator = {};
@@ -45,7 +71,7 @@ export class TreeFake extends Tree {
}
translateTreeNodeIdFromACITree(aciTreeNode) {
- if(aciTreeNode === undefined || aciTreeNode[0] === undefined) {
+ if (aciTreeNode === undefined || aciTreeNode[0] === undefined) {
return null;
}
return this.aciTreeToOurTreeTranslator[aciTreeNode[0].id];
--
2.16.2
[application/octet-stream] 0002-New-tree-implementation.patch (22.9K, 6-0002-New-tree-implementation.patch)
download | inline diff:
diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js
index 14640ae8..d9693486 100644
--- a/web/pgadmin/browser/static/js/browser.js
+++ b/web/pgadmin/browser/static/js/browser.js
@@ -1,4 +1,5 @@
define('pgadmin.browser', [
+ 'sources/tree/tree',
'sources/gettext', 'sources/url_for', 'require', 'jquery', 'underscore', 'underscore.string',
'bootstrap', 'sources/pgadmin', 'pgadmin.alertifyjs', 'bundled_codemirror',
'sources/check_node_visibility', 'sources/modify_animation', 'pgadmin.browser.utils', 'wcdocker',
@@ -10,6 +11,7 @@ define('pgadmin.browser', [
'sources/codemirror/addon/fold/pgadmin-sqlfoldcode',
'pgadmin.browser.keyboard',
], function(
+ tree,
gettext, url_for, require, $, _, S, Bootstrap, pgAdmin, Alertify,
codemirror, checkNodeVisibility, modifyAnimation
) {
@@ -86,6 +88,7 @@ define('pgadmin.browser', [
});
b.tree = $('#tree').aciTree('api');
+ b.treeMenu.register($('#tree'));
};
// Extend the browser class attributes
@@ -100,6 +103,7 @@ define('pgadmin.browser', [
editor:null,
// Left hand browser tree
tree:null,
+ treeMenu: new tree.Tree(),
// list of script to be loaded, when a certain type of node is loaded
// It will be used to register extensions, tools, child node scripts,
// etc.
diff --git a/web/pgadmin/static/js/tree/tree.js b/web/pgadmin/static/js/tree/tree.js
new file mode 100644
index 00000000..974ee8de
--- /dev/null
+++ b/web/pgadmin/static/js/tree/tree.js
@@ -0,0 +1,214 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+export class TreeNode {
+ constructor(id, data, domNode, parent) {
+ this.id = id;
+ this.data = data;
+ this.setParent(parent);
+ this.children = [];
+ this.domNode = domNode;
+ }
+
+ hasParent() {
+ return this.parentNode !== null && this.parentNode !== undefined;
+ }
+
+ parent() {
+ return this.parentNode;
+ }
+
+ setParent(parent) {
+ this.parentNode = parent;
+ this.path = this.id;
+ if (parent !== null && parent !== undefined && parent.path !== undefined) {
+ this.path = parent.path + '.' + this.id;
+ }
+ }
+
+ getData() {
+ if (this.data === undefined) {
+ return undefined;
+ } else if (this.data === null) {
+ return null;
+ }
+ return Object.assign({}, this.data);
+ }
+
+ getHtmlIdentifier() {
+ return this.domNode;
+ }
+
+ reload(tree) {
+ this.unload(tree);
+ tree.aciTreeApi.setInode(this.domNode);
+ tree.aciTreeApi.deselect(this.domNode);
+ setTimeout(() => {
+ tree.selectNode(this.domNode);
+ }, 0);
+ }
+
+ unload(tree) {
+ this.children = [];
+ tree.aciTreeApi.unload(this.domNode);
+ }
+
+ anyParent(condition) {
+ let node = this;
+
+ while (node.hasParent()) {
+ node = node.parent();
+ if (condition(node)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Given a condition returns true if the current node
+ * or any of the parent nodes condition result is true
+ */
+ anyFamilyMember(condition) {
+ if(condition(this)) {
+ return true;
+ }
+
+ return this.anyParent(condition);
+ }
+}
+
+export class Tree {
+ constructor() {
+ this.rootNode = new TreeNode(undefined, {});
+ this.aciTreeApi = undefined;
+ }
+
+ addNewNode(id, data, domNode, parentPath) {
+ const parent = this.findNode(parentPath);
+ return this.createOrUpdateNode(id, data, parent, domNode);
+ }
+
+ findNode(path) {
+ if (path === null || path === undefined || path.length === 0) {
+ return this.rootNode;
+ }
+ return findInTree(this.rootNode, path.join('.'));
+ }
+
+ findNodeByDomElement(domElement) {
+ const path = this.translateTreeNodeIdFromACITree(domElement);
+ if(!path || !path[0]) {
+ return undefined;
+ }
+
+ return this.findNode(path);
+ }
+
+ selected() {
+ return this.aciTreeApi.selected();
+ }
+
+ selectNode(aciTreeIdentifier) {
+ this.aciTreeApi.select(aciTreeIdentifier);
+ }
+
+ createOrUpdateNode(id, data, parent, domNode) {
+ let oldNodePath = [id];
+ if(parent !== null && parent !== undefined) {
+ oldNodePath = [parent.path, id];
+ }
+ const oldNode = this.findNode(oldNodePath);
+ if (oldNode !== null) {
+ oldNode.data = Object.assign({}, data);
+ return oldNode;
+ }
+
+ const node = new TreeNode(id, data, domNode, parent);
+ if (parent === this.rootNode) {
+ node.parentNode = null;
+ }
+ parent.children.push(node);
+ return node;
+ }
+
+ /**
+ * Given the JQuery object that contains the ACI Tree
+ * this method is responsible for registering this tree class
+ * to listen to all the events that happen in the ACI Tree.
+ *
+ * At this point in time the only event that we care about is
+ * the addition of a new node.
+ * The function will create a new tree node to store the information
+ * that exist in the ACI for it.
+ */
+ register($treeJQuery) {
+ $treeJQuery.on('acitree', function (event, api, item, eventName) {
+ if (api.isItem(item)) {
+ if (eventName === 'added') {
+ const id = api.getId(item);
+ const data = api.itemData(item);
+ const parentId = this.translateTreeNodeIdFromACITree(api.parent(item));
+ this.addNewNode(id, data, item, parentId);
+ }
+ }
+ }.bind(this));
+ this.aciTreeApi = $treeJQuery.aciTree('api');
+ }
+
+ /**
+ * As the name hints this functions works as a layer in between ACI and
+ * the adaptor. Given a ACITree JQuery node find the location of it in the
+ * Tree and then returns and array with the path to to the Tree Node in
+ * question
+ *
+ * This is not optimized and will always go through the full tree
+ */
+ translateTreeNodeIdFromACITree(aciTreeNode) {
+ let currentTreeNode = aciTreeNode;
+ let path = [];
+ while (currentTreeNode !== null && currentTreeNode !== undefined && currentTreeNode.length > 0) {
+ path.unshift(this.aciTreeApi.getId(currentTreeNode));
+ if (this.aciTreeApi.hasParent(currentTreeNode)) {
+ currentTreeNode = this.aciTreeApi.parent(currentTreeNode);
+ } else {
+ break;
+ }
+ }
+ return path;
+ }
+}
+
+export let tree = new Tree();
+
+/**
+ * Given an initial node and a path, it will navigate through
+ * the new tree to find the node that matches the path
+ */
+function findInTree(rootNode, path) {
+ if (path === null) {
+ return rootNode;
+ }
+
+ return (function findInNode(currentNode) {
+ for (let i = 0, length = currentNode.children.length; i < length; i++) {
+ const calculatedNode = findInNode(currentNode.children[i]);
+ if (calculatedNode !== null) {
+ return calculatedNode;
+ }
+ }
+
+ if (currentNode.path === path) {
+ return currentNode;
+ } else {
+ return null;
+ }
+ })(rootNode);
+}
diff --git a/web/regression/javascript/tree/tree_fake.js b/web/regression/javascript/tree/tree_fake.js
new file mode 100644
index 00000000..b285a45f
--- /dev/null
+++ b/web/regression/javascript/tree/tree_fake.js
@@ -0,0 +1,69 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {Tree} from '../../../pgadmin/static/js/tree/tree';
+
+export class TreeFake extends Tree {
+ constructor() {
+ super();
+ this.aciTreeToOurTreeTranslator = {};
+ this.aciTreeApi = jasmine.createSpyObj(
+ ['ACITreeApi'], ['setInode', 'unload', 'deselect', 'select']);
+ }
+
+ addNewNode(id, data, domNode, path) {
+ this.aciTreeToOurTreeTranslator[id] = [id];
+ if (path !== null && path !== undefined) {
+ this.aciTreeToOurTreeTranslator[id] = path.concat(id);
+ }
+ return super.addNewNode(id, data, domNode, path);
+ }
+
+ addChild(parent, child) {
+ child.setParent(parent);
+ this.aciTreeToOurTreeTranslator[child.id] = this.aciTreeToOurTreeTranslator[parent.id].concat(child.id);
+ parent.children.push(child);
+ }
+
+ hasParent(aciTreeNode) {
+ return this.translateTreeNodeIdFromACITree(aciTreeNode).length > 1;
+ }
+
+ parent(aciTreeNode) {
+ if (this.hasParent(aciTreeNode)) {
+ let path = this.translateTreeNodeIdFromACITree(aciTreeNode);
+ return [{id: this.findNode(path).parent().id}];
+ }
+
+ return null;
+ }
+
+ translateTreeNodeIdFromACITree(aciTreeNode) {
+ if(aciTreeNode === undefined || aciTreeNode[0] === undefined) {
+ return null;
+ }
+ return this.aciTreeToOurTreeTranslator[aciTreeNode[0].id];
+ }
+
+ itemData(aciTreeNode) {
+ let node = this.findNodeByDomElement(aciTreeNode);
+ if (node === undefined || node === null) {
+ return undefined;
+ }
+ return node.getData();
+ }
+
+ selected() {
+ return this.selectedNode;
+ }
+
+ selectNode(selectedNode) {
+ this.selectedNode = selectedNode;
+ }
+}
diff --git a/web/regression/javascript/tree/tree_spec.js b/web/regression/javascript/tree/tree_spec.js
new file mode 100644
index 00000000..164ceb5b
--- /dev/null
+++ b/web/regression/javascript/tree/tree_spec.js
@@ -0,0 +1,421 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+import {Tree, TreeNode} from '../../../pgadmin/static/js/tree/tree';
+import {TreeFake} from './tree_fake';
+
+const context = describe;
+
+const treeTests = (treeClass, setDefaultCallBack) => {
+ let tree;
+ beforeEach(() => {
+ tree = new treeClass();
+ });
+
+ describe('#addNewNode', () => {
+ describe('when add a new root element', () => {
+ context('using [] as the parent', () => {
+ beforeEach(() => {
+ tree.addNewNode('some new node', {data: 'interesting'}, undefined, []);
+ });
+
+ it('can be retrieved', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.data).toEqual({data: 'interesting'});
+ });
+
+ it('return false for #hasParent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.hasParent()).toBe(false);
+ });
+
+ it('return null for #parent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.parent()).toBeNull();
+ });
+ });
+
+ context('using null as the parent', () => {
+ beforeEach(() => {
+ tree.addNewNode('some new node', {data: 'interesting'}, undefined, null);
+ });
+
+ it('can be retrieved', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.data).toEqual({data: 'interesting'});
+ });
+
+ it('return false for #hasParent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.hasParent()).toBe(false);
+ });
+
+ it('return null for #parent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.parent()).toBeNull();
+ });
+ });
+
+ context('using undefined as the parent', () => {
+ beforeEach(() => {
+ tree.addNewNode('some new node', {data: 'interesting'});
+ });
+
+ it('can be retrieved', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.data).toEqual({data: 'interesting'});
+ });
+
+ it('return false for #hasParent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.hasParent()).toBe(false);
+ });
+
+ it('return null for #parent()', () => {
+ const node = tree.findNode(['some new node']);
+ expect(node.parent()).toBeNull();
+ });
+ });
+ });
+
+ describe('when add a new element as a child', () => {
+ let parentNode;
+ beforeEach(() => {
+ parentNode = tree.addNewNode('parent node', {data: 'parent data'}, undefined, []);
+ tree.addNewNode('some new node', {data: 'interesting'}, undefined, ['parent' +
+ ' node']);
+ });
+
+ it('can be retrieved', () => {
+ const node = tree.findNode(['parent node', 'some new node']);
+ expect(node.data).toEqual({data: 'interesting'});
+ });
+
+ it('return true for #hasParent()', () => {
+ const node = tree.findNode(['parent node', 'some new node']);
+ expect(node.hasParent()).toBe(true);
+ });
+
+ it('return "parent node" object for #parent()', () => {
+ const node = tree.findNode(['parent node', 'some new node']);
+ expect(node.parent()).toEqual(parentNode);
+ });
+ });
+
+ describe('when add an element that already exists under a parent', () => {
+ beforeEach(() => {
+ tree.addNewNode('parent node', {data: 'parent data'}, undefined, []);
+ tree.addNewNode('some new node', {data: 'interesting'}, undefined, ['parent' +
+ ' node']);
+ });
+
+ it('does not add a new child', () => {
+ tree.addNewNode('some new node', {data: 'interesting 1'}, undefined, ['parent' +
+ ' node']);
+ const parentNode = tree.findNode(['parent node']);
+ expect(parentNode.children.length).toBe(1);
+ });
+
+ it('updates the existing node data', () => {
+ tree.addNewNode('some new node', {data: 'interesting 1'}, undefined, ['parent' +
+ ' node']);
+ const node = tree.findNode(['parent node', 'some new node']);
+ expect(node.data).toEqual({data: 'interesting 1'});
+ });
+ });
+ });
+
+ describe('#translateTreeNodeIdFromACITree', () => {
+ let aciTreeApi;
+ beforeEach(() => {
+ aciTreeApi = jasmine.createSpyObj('ACITreeApi', [
+ 'hasParent',
+ 'parent',
+ 'getId',
+ ]);
+
+ aciTreeApi.getId.and.callFake((node) => {
+ return node[0].id;
+ });
+ tree.aciTreeApi = aciTreeApi;
+ });
+
+ describe('When tree as a single level', () => {
+ beforeEach(() => {
+ aciTreeApi.hasParent.and.returnValue(false);
+ });
+
+ it('returns an array with the ID of the first level', () => {
+ let node = [{
+ id: 'some id',
+ }];
+ tree.addNewNode('some id', {}, undefined, []);
+
+ expect(tree.translateTreeNodeIdFromACITree(node)).toEqual(['some id']);
+ });
+ });
+
+ describe('When tree as a 2 levels', () => {
+ describe('When we try to retrieve the node in the second level', () => {
+ it('returns an array with the ID of the first level and second level', () => {
+ aciTreeApi.hasParent.and.returnValues(true, false);
+ aciTreeApi.parent.and.returnValue([{
+ id: 'parent id',
+ }]);
+ let node = [{
+ id: 'some id',
+ }];
+
+ tree.addNewNode('parent id', {}, undefined, []);
+ tree.addNewNode('some id', {}, undefined, ['parent id']);
+
+ expect(tree.translateTreeNodeIdFromACITree(node))
+ .toEqual(['parent id', 'some id']);
+ });
+ });
+ });
+ });
+
+ describe('#selected', () => {
+ context('a node in the tree is selected', () => {
+ it('returns that node object', () => {
+ let selectedNode = new TreeNode('bamm', {}, []);
+ setDefaultCallBack(tree, selectedNode);
+ expect(tree.selected()).toEqual(selectedNode);
+ });
+ });
+ });
+
+ describe('#findNodeByTreeElement', () => {
+ context('retrieve data from node not found', () => {
+ it('return undefined', () => {
+ let aciTreeApi = jasmine.createSpyObj('ACITreeApi', [
+ 'hasParent',
+ 'parent',
+ 'getId',
+ ]);
+
+ aciTreeApi.getId.and.callFake((node) => {
+ return node[0].id;
+ });
+ tree.aciTreeApi = aciTreeApi;
+ expect(tree.findNodeByDomElement(['<li>something</li>'])).toBeUndefined();
+ });
+ });
+ });
+};
+
+describe('tree tests', () => {
+ describe('TreeNode', () => {
+ describe('#hasParent', () => {
+ context('parent is null', () => {
+ it('returns false', () => {
+ let treeNode = new TreeNode('123', {}, [], null);
+ expect(treeNode.hasParent()).toBe(false);
+ });
+ });
+ context('parent is undefined', () => {
+ it('returns false', () => {
+ let treeNode = new TreeNode('123', {}, [], undefined);
+ expect(treeNode.hasParent()).toBe(false);
+ });
+ });
+ context('parent exists', () => {
+ it('returns true', () => {
+ let parentNode = new TreeNode('456', {}, []);
+ let treeNode = new TreeNode('123', {}, [], parentNode);
+ expect(treeNode.hasParent()).toBe(true);
+ });
+ });
+ });
+
+ describe('#reload', () => {
+ let tree;
+ let level2;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, [{id: 'level1'}], []);
+ level2 = tree.addNewNode('level2', {data: 'data'}, [{id: 'level2'}], ['level1']);
+ tree.addNewNode('level3', {data: 'more data'}, [{id: 'level3'}], ['level1', 'level2']);
+
+ tree.aciTreeApi = jasmine.createSpyObj(
+ 'ACITreeApi', ['setInode', 'unload', 'deselect', 'select']);
+ });
+
+ it('reloads the node and its children', () => {
+ level2.reload(tree);
+ expect(tree.findNodeByDomElement([{id: 'level2'}])).toEqual(level2);
+ });
+
+ it('does not reload the children of node', () => {
+ level2.reload(tree);
+ expect(tree.findNodeByDomElement([{id: 'level3'}])).toBeNull();
+ });
+
+ it('select the node', (done) => {
+ level2.reload(tree);
+ setTimeout(() => {
+ expect(tree.selected()).toEqual([{id: 'level2'}]);
+ done();
+ }, 20);
+ });
+
+ describe('ACITree specific', () => {
+ it('sets the current node as a Inode, changing the Icon back to +', () => {
+ level2.reload(tree);
+ expect(tree.aciTreeApi.setInode).toHaveBeenCalledWith([{id: 'level2'}]);
+ });
+
+ it('deselect the node and selects it again to trigger ACI tree' +
+ ' events', (done) => {
+ level2.reload(tree);
+ setTimeout(() => {
+ expect(tree.aciTreeApi.deselect).toHaveBeenCalledWith([{id: 'level2'}]);
+ done();
+ }, 20);
+ });
+ });
+ });
+
+ describe('#unload', () => {
+ let tree;
+ let level2;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, ['<li>level1</li>'], []);
+ level2 = tree.addNewNode('level2', {data: 'data'}, ['<li>level2</li>'], ['level1']);
+ tree.addNewNode('level3', {data: 'more data'}, ['<li>level3</li>'], ['level1', 'level2']);
+ tree.aciTreeApi = jasmine.createSpyObj('ACITreeApi', ['unload']);
+ });
+
+ it('unloads the children of the current node', () => {
+ level2.unload(tree);
+ expect(tree.findNodeByDomElement([{id: 'level2'}])).toEqual(level2);
+ expect(tree.findNodeByDomElement([{id: 'level3'}])).toBeNull();
+ });
+
+ it('calls unload on the ACI Tree', () => {
+ level2.unload(tree);
+ expect(tree.aciTreeApi.unload).toHaveBeenCalledWith(['<li>level2</li>']);
+ });
+ });
+ });
+
+ describe('Tree', () => {
+ function realTreeSelectNode(tree, selectedNode) {
+ let aciTreeApi = jasmine.createSpyObj('ACITreeApi', [
+ 'selected',
+ ]);
+ tree.aciTreeApi = aciTreeApi;
+ aciTreeApi.selected.and.returnValue(selectedNode);
+ }
+
+ treeTests(Tree, realTreeSelectNode);
+ });
+
+ describe('TreeFake', () => {
+ function fakeTreeSelectNode(tree, selectedNode) {
+ tree.selectNode(selectedNode);
+ }
+
+ treeTests(TreeFake, fakeTreeSelectNode);
+
+ describe('#hasParent', () => {
+ context('tree contains multiple levels', () => {
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, undefined, []);
+ tree.addNewNode('level2', {data: 'interesting'}, undefined, ['level1']);
+ });
+
+ context('node is at the first level', () => {
+ it('returns false', () => {
+ expect(tree.hasParent([{id: 'level1'}])).toBe(false);
+ });
+ });
+
+ context('node is at the second level', () => {
+ it('returns true', () => {
+ expect(tree.hasParent([{id: 'level2'}])).toBe(true);
+ });
+ });
+ });
+ });
+
+ describe('#parent', () => {
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, undefined, []);
+ tree.addNewNode('level2', {data: 'interesting'}, undefined, ['level1']);
+ });
+
+ context('node is the root', () => {
+ it('returns null', () => {
+ expect(tree.parent([{id: 'level1'}])).toBeNull();
+ });
+ });
+
+ context('node is not root', () => {
+ it('returns root element', () => {
+ expect(tree.parent([{id: 'level2'}])).toEqual([{id: 'level1'}]);
+ });
+ });
+ });
+
+ describe('#itemData', () => {
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ tree.addNewNode('level1', {data: 'interesting'}, undefined, []);
+ tree.addNewNode('level2', {data: 'expected data'}, undefined, ['level1']);
+ });
+
+ context('retrieve data from the node', () => {
+ it('return the node data', () => {
+ expect(tree.itemData([{id: 'level2'}])).toEqual({
+ data: 'expected' +
+ ' data',
+ });
+ });
+ });
+
+ context('retrieve data from node not found', () => {
+ it('return undefined', () => {
+ expect(tree.itemData([{id: 'bamm'}])).toBeUndefined();
+ });
+ });
+ });
+
+ describe('#addChild', () => {
+ let root, child;
+ beforeEach(() => {
+ let tree = new TreeFake();
+ root = tree.addNewNode('root', {}, [{id: 'root'}]);
+ child = new TreeNode('node.1', {}, [{id: 'node.1'}]);
+ tree.addChild(root, child);
+ });
+
+ it('adds a new child to a node', () => {
+ expect(root.children).toEqual([child]);
+ });
+
+ it('changes the parent of the child node', () => {
+ expect(root.children[0].parentNode).toEqual(root);
+ expect(child.parentNode).toEqual(root);
+ });
+
+ it('changes the path of the child', () => {
+ expect(child.path).toEqual('root.node.1');
+ });
+ });
+ });
+});
+
--
2.16.2
[application/octet-stream] 0001-Change-the-order-of-the-shims-on-the-shim-file.patch (22.4K, 7-0001-Change-the-order-of-the-shims-on-the-shim-file.patch)
download | inline diff:
diff --git a/web/webpack.shim.js b/web/webpack.shim.js
index 1b6442a6..487d73cb 100644
--- a/web/webpack.shim.js
+++ b/web/webpack.shim.js
@@ -129,6 +129,7 @@ var webpackShimConfig = {
'sources/gettext': path.join(__dirname, './pgadmin/static/js/gettext'),
'sources/utils': path.join(__dirname, './pgadmin/static/js/utils'),
'babel-polyfill': path.join(__dirname, './node_modules/babel-polyfill/dist/polyfill'),
+ 'tools': path.join(__dirname, './pgadmin/tools/'),
// Vendor JS
'jquery': path.join(__dirname, './node_modules/jquery/dist/jquery'),
@@ -168,105 +169,105 @@ var webpackShimConfig = {
'pgadmin.backform': path.join(__dirname, './pgadmin/static/js/backform.pgadmin'),
'pgadmin.backgrid': path.join(__dirname, './pgadmin/static/js/backgrid.pgadmin'),
- 'pgadmin.browser.panel': path.join(__dirname, './pgadmin/browser/static/js/panel'),
- 'pgadmin.node.language': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/languages/static/js/language'),
- 'pgadmin.node.mview': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview'),
- 'pgadmin.tools.maintenance': path.join(__dirname, './pgadmin/tools/maintenance/static/js/maintenance'),
- 'pgadmin.node.pga_jobstep': path.join(__dirname, './pgadmin/browser/server_groups/servers/pgagent/steps/static/js/pga_jobstep'),
- 'pgadmin.sqleditor': path.join(__dirname, './pgadmin/tools/sqleditor/static/js/sqleditor'),
- 'pgadmin.node.foreign_server': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/foreign_servers/static/js/foreign_server'),
- 'pgadmin.node.domain': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain'),
- 'pgadmin.tools.user_management': path.join(__dirname, './pgadmin/tools/user_management/static/js/user_management'),
+ 'pgadmin.about': path.join(__dirname, './pgadmin/about/static/js/about'),
'pgadmin.browser': path.join(__dirname, './pgadmin/browser/static/js/browser'),
+ 'pgadmin.browser.bgprocess': path.join(__dirname, './pgadmin/misc/bgprocess/static/js/bgprocess'),
+ 'pgadmin.browser.collection': path.join(__dirname, './pgadmin/browser/static/js/collection'),
+ 'pgadmin.browser.datamodel': path.join(__dirname, './pgadmin/browser/static/js/datamodel'),
+ 'pgadmin.browser.endpoints': '/browser/js/endpoints',
+ 'pgadmin.browser.error': path.join(__dirname, './pgadmin/browser/static/js/error'),
+ 'pgadmin.browser.frame': path.join(__dirname, './pgadmin/browser/static/js/frame'),
+ 'pgadmin.browser.keyboard': path.join(__dirname, './pgadmin/browser/static/js/keyboard'),
+ 'pgadmin.browser.menu': path.join(__dirname, './pgadmin/browser/static/js/menu'),
+ 'pgadmin.browser.messages': '/browser/js/messages',
+ 'pgadmin.browser.node': path.join(__dirname, './pgadmin/browser/static/js/node'),
+ 'pgadmin.browser.node.ui': path.join(__dirname, './pgadmin/browser/static/js/node.ui'),
+ 'pgadmin.browser.object_depends': path.join(__dirname, './pgadmin/misc/depends/static/js/depends'),
+ 'pgadmin.browser.object_sql': path.join(__dirname, './pgadmin/misc/sql/static/js/sql'),
'pgadmin.browser.object_statistics': path.join(__dirname, './pgadmin/misc/statistics/static/js/statistics'),
- 'pgadmin.node.constraints': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints'),
+ 'pgadmin.browser.panel': path.join(__dirname, './pgadmin/browser/static/js/panel'),
+ 'pgadmin.browser.server.privilege': path.join(__dirname, './pgadmin/browser/server_groups/servers/static/js/privilege'),
+ 'pgadmin.browser.server.variable': path.join(__dirname, './pgadmin/browser/server_groups/servers/static/js/variable'),
'pgadmin.browser.table.partition.utils': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/partition.utils'),
+ 'pgadmin.browser.utils': '/browser/js/utils',
+ 'pgadmin.browser.wizard': path.join(__dirname, './pgadmin/browser/static/js/wizard'),
+ 'pgadmin.dashboard': path.join(__dirname, './pgadmin/dashboard/static/js/dashboard'),
+ 'pgadmin.datagrid': path.join(__dirname, './pgadmin/tools/datagrid/static/js/datagrid'),
+ 'pgadmin.file_manager': path.join(__dirname, './pgadmin/misc/file_manager/static/js/file_manager'),
+ 'pgadmin.file_utility': path.join(__dirname, './pgadmin/misc/file_manager/static/js/utility'),
+ 'pgadmin.misc.explain': path.join(__dirname, './pgadmin/misc/static/explain/js/explain'),
+ 'pgadmin.node.cast': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/casts/static/js/cast'),
'pgadmin.node.catalog': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/static/js/catalog'),
- 'pgadmin.node.role': path.join(__dirname, './pgadmin/browser/server_groups/servers/roles/static/js/role'),
+ 'pgadmin.node.catalog_object': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/catalog_objects/static/js/catalog_object'),
+ 'pgadmin.node.catalog_object_column': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/catalog_objects/columns/static/js/catalog_object_column'),
'pgadmin.node.check_constraint': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint'),
- 'pgadmin.node.user_mapping': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/foreign_servers/user_mapping/static/js/user_mapping'),
- 'pgadmin.browser.messages': '/browser/js/messages',
'pgadmin.node.collation': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation'),
- 'pgadmin.browser.endpoints': '/browser/js/endpoints',
- 'pgadmin.node.table': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table'),
- 'pgadmin.node.tablespace': path.join(__dirname, './pgadmin/browser/server_groups/servers/tablespaces/static/js/tablespace'),
- 'pgadmin.node.server': path.join(__dirname, './pgadmin/browser/server_groups/servers/static/js/server'),
- 'pgadmin.node.package': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/packages/static/js/package'),
- 'pgadmin.browser.menu': path.join(__dirname, './pgadmin/browser/static/js/menu'),
- 'pgadmin.browser.node.ui': path.join(__dirname, './pgadmin/browser/static/js/node.ui'),
- 'pgadmin.node.rule': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule'),
- 'pgadmin.node.server_group': path.join(__dirname, './pgadmin/browser/server_groups/static/js/server_group'),
- 'pgadmin.tools.backup': path.join(__dirname, './pgadmin/tools/backup/static/js/backup'),
- 'pgadmin.node.fts_configuration': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration'),
+ 'pgadmin.node.column': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column'),
+ 'pgadmin.node.constraints': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints'),
+ 'pgadmin.node.database': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/static/js/database'),
+ 'pgadmin.node.domain': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain'),
+ 'pgadmin.node.domain_constraints': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/domains/domain_constraints/static/js/domain_constraints'),
'pgadmin.node.event_trigger': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/event_triggers/static/js/event_trigger'),
- 'pgadmin.browser.bgprocess': path.join(__dirname, './pgadmin/misc/bgprocess/static/js/bgprocess'),
- 'pgadmin.node.pga_job': path.join(__dirname, './pgadmin/browser/server_groups/servers/pgagent/static/js/pga_job'),
- 'pgadmin.node.function': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function'),
- 'pgadmin.file_utility': path.join(__dirname, './pgadmin/misc/file_manager/static/js/utility'),
- 'pgadmin.browser.object_depends': path.join(__dirname, './pgadmin/misc/depends/static/js/depends'),
'pgadmin.node.edbfunc': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/packages/edbfuncs/static/js/edbfunc'),
- 'pgadmin.node.sequence': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence'),
'pgadmin.node.edbproc': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/packages/edbfuncs/static/js/edbproc'),
- 'pgadmin.browser.object_sql': path.join(__dirname, './pgadmin/misc/sql/static/js/sql'),
- 'pgadmin.preferences': path.join(__dirname, './pgadmin/preferences/static/js/preferences'),
- 'pgadmin.node.domain_constraints': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/domains/domain_constraints/static/js/domain_constraints'),
- 'slick.pgadmin.formatters': path.join(__dirname, './pgadmin/tools/../static/js/slickgrid/formatters'),
- 'pgadmin.node.catalog_object': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/catalog_objects/static/js/catalog_object'),
- 'pgadmin.dashboard': path.join(__dirname, './pgadmin/dashboard/static/js/dashboard'),
- 'pgadmin.node.foreign_data_wrapper': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/static/js/foreign_data_wrapper'),
+ 'pgadmin.node.edbvar': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/packages/edbvars/static/js/edbvar'),
+ 'pgadmin.node.exclusion_constraint': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint'),
+ 'pgadmin.node.extension': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/extensions/static/js/extension'),
'pgadmin.node.external_table': path.join(__dirname, './pgadmin/static/js/browser/server_groups/servers/databases/external_tables/index'),
- 'pgadmin.node.external_tables': path.join(__dirname, './pgadmin/static/js/browser/server_groups/servers/databases/external_tables/index'),
+ 'pgadmin.node.foreign_data_wrapper': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/static/js/foreign_data_wrapper'),
'pgadmin.node.foreign_key': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key'),
- 'pgadmin.browser.server.variable': path.join(__dirname, './pgadmin/browser/server_groups/servers/static/js/variable'),
- 'pgadmin.tools.grant_wizard': path.join(__dirname, './pgadmin/tools/grant_wizard/static/js/grant_wizard'),
- 'pgadmin.browser.datamodel': path.join(__dirname, './pgadmin/browser/static/js/datamodel'),
- 'pgadmin.tools.restore': path.join(__dirname, './pgadmin/tools/restore/static/js/restore'),
+ 'pgadmin.node.foreign_server': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/foreign_servers/static/js/foreign_server'),
+ 'pgadmin.node.foreign_table': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table'),
+ 'pgadmin.node.fts_configuration': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration'),
+ 'pgadmin.node.fts_dictionary': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary'),
+ 'pgadmin.node.fts_parser': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser'),
+ 'pgadmin.node.fts_template': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template'),
+ 'pgadmin.node.function': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function'),
+ 'pgadmin.node.index': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index'),
+ 'pgadmin.node.language': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/languages/static/js/language'),
+ 'pgadmin.node.mview': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview'),
+ 'pgadmin.node.package': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/packages/static/js/package'),
+ 'pgadmin.node.partition': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition'),
+ 'pgadmin.node.pga_job': path.join(__dirname, './pgadmin/browser/server_groups/servers/pgagent/static/js/pga_job'),
+ 'pgadmin.node.pga_jobstep': path.join(__dirname, './pgadmin/browser/server_groups/servers/pgagent/steps/static/js/pga_jobstep'),
+ 'pgadmin.node.pga_schedule': path.join(__dirname, './pgadmin/browser/server_groups/servers/pgagent/schedules/static/js/pga_schedule'),
+ 'pgadmin.node.primary_key': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key'),
'pgadmin.node.procedure': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/procedure'),
- 'pgadmin.browser.server.privilege': path.join(__dirname, './pgadmin/browser/server_groups/servers/static/js/privilege'),
'pgadmin.node.resource_group': path.join(__dirname, './pgadmin/browser/server_groups/servers/resource_groups/static/js/resource_group'),
- 'pgadmin.node.exclusion_constraint': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint'),
- 'pgadmin.node.primary_key': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key'),
- 'pgadmin.tools.debugger.direct': path.join(__dirname, './pgadmin/tools/debugger/static/js/direct'),
- 'pgadmin.settings': path.join(__dirname, './pgadmin/settings/static/js/settings'),
+ 'pgadmin.node.role': path.join(__dirname, './pgadmin/browser/server_groups/servers/roles/static/js/role'),
+ 'pgadmin.node.rule': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule'),
'pgadmin.node.schema': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema'),
- 'pgadmin.browser.error': path.join(__dirname, './pgadmin/browser/static/js/error'),
- 'pgadmin.tools.import_export': path.join(__dirname, './pgadmin/tools/import_export/static/js/import_export'),
- 'pgadmin.node.view': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view'),
- 'pgadmin.tools.debugger.ui': path.join(__dirname, './pgadmin/tools/debugger/static/js/debugger_ui'),
- 'pgadmin.tools.debugger.utils': path.join(__dirname, './pgadmin/tools/debugger/static/js/debugger_utils'),
- 'pgadmin.node.pga_schedule': path.join(__dirname, './pgadmin/browser/server_groups/servers/pgagent/schedules/static/js/pga_schedule'),
- 'pgadmin.node.catalog_object_column': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/catalog_objects/columns/static/js/catalog_object_column'),
- 'pgadmin.browser.collection': path.join(__dirname, './pgadmin/browser/static/js/collection'),
- 'pgadmin.browser.node': path.join(__dirname, './pgadmin/browser/static/js/node'),
- 'pgadmin.misc.explain': path.join(__dirname, './pgadmin/misc/static/explain/js/explain'),
+ 'pgadmin.node.schema.dir': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/static/js/'),
+ 'pgadmin.node.sequence': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence'),
+ 'pgadmin.node.server': path.join(__dirname, './pgadmin/browser/server_groups/servers/static/js/server'),
+ 'pgadmin.node.server_group': path.join(__dirname, './pgadmin/browser/server_groups/static/js/server_group'),
'pgadmin.node.synonym': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/synonyms/static/js/synonym'),
- 'pgadmin.node.extension': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/extensions/static/js/extension'),
- 'pgadmin.node.unique_constraint': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint'),
- 'pgadmin.node.database': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/static/js/database'),
- 'pgadmin.node.edbvar': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/packages/edbvars/static/js/edbvar'),
+ 'pgadmin.node.table': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table'),
+ 'pgadmin.node.tablespace': path.join(__dirname, './pgadmin/browser/server_groups/servers/tablespaces/static/js/tablespace'),
'pgadmin.node.trigger': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger'),
- 'pgadmin.browser.wizard': path.join(__dirname, './pgadmin/browser/static/js/wizard'),
- 'pgadmin.server.supported_servers': '/browser/server/supported_servers',
- 'pgadmin.node.partition': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition'),
- 'pgadmin.node.fts_template': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template'),
- 'pgadmin.node.cast': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/casts/static/js/cast'),
- 'pgadmin.tools.debugger.controller': path.join(__dirname, './pgadmin/tools/debugger/static/js/debugger'),
- 'pgadmin.node.fts_parser': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser'),
- 'pgadmin.browser.utils': '/browser/js/utils',
- 'pgadmin.about': path.join(__dirname, './pgadmin/about/static/js/about'),
- 'slick.pgadmin.editors': path.join(__dirname, './pgadmin/tools/../static/js/slickgrid/editors'),
- 'pgadmin.node.column': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column'),
- 'pgadmin.node.foreign_table': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table'),
- 'pgadmin.node.fts_dictionary': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary'),
- 'pgadmin.datagrid': path.join(__dirname, './pgadmin/tools/datagrid/static/js/datagrid'),
'pgadmin.node.trigger_function': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function'),
- 'pgadmin.user_management.current_user': '/user_management/current_user',
- 'pgadmin.node.index': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index'),
- 'pgadmin.browser.frame': path.join(__dirname, './pgadmin/browser/static/js/frame'),
'pgadmin.node.type': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type'),
- 'pgadmin.file_manager': path.join(__dirname, './pgadmin/misc/file_manager/static/js/file_manager'),
- 'pgadmin.browser.keyboard': path.join(__dirname, './pgadmin/browser/static/js/keyboard'),
+ 'pgadmin.node.unique_constraint': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint'),
+ 'pgadmin.node.user_mapping': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/foreign_servers/user_mapping/static/js/user_mapping'),
+ 'pgadmin.node.view': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view'),
+ 'pgadmin.preferences': path.join(__dirname, './pgadmin/preferences/static/js/preferences'),
+ 'pgadmin.settings': path.join(__dirname, './pgadmin/settings/static/js/settings'),
+ 'pgadmin.server.supported_servers': '/browser/server/supported_servers',
+ 'pgadmin.sqleditor': path.join(__dirname, './pgadmin/tools/sqleditor/static/js/sqleditor'),
'pgadmin.tables.js': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/'),
+ 'pgadmin.tools.backup': path.join(__dirname, './pgadmin/tools/backup/static/js/backup'),
+ 'pgadmin.tools.debugger.controller': path.join(__dirname, './pgadmin/tools/debugger/static/js/debugger'),
+ 'pgadmin.tools.debugger.direct': path.join(__dirname, './pgadmin/tools/debugger/static/js/direct'),
+ 'pgadmin.tools.debugger.ui': path.join(__dirname, './pgadmin/tools/debugger/static/js/debugger_ui'),
+ 'pgadmin.tools.debugger.utils': path.join(__dirname, './pgadmin/tools/debugger/static/js/debugger_utils'),
+ 'pgadmin.tools.grant_wizard': path.join(__dirname, './pgadmin/tools/grant_wizard/static/js/grant_wizard'),
+ 'pgadmin.tools.import_export': path.join(__dirname, './pgadmin/tools/import_export/static/js/import_export'),
+ 'pgadmin.tools.maintenance': path.join(__dirname, './pgadmin/tools/maintenance/static/js/maintenance'),
+ 'pgadmin.tools.restore': path.join(__dirname, './pgadmin/tools/restore/static/js/restore'),
+ 'pgadmin.tools.user_management': path.join(__dirname, './pgadmin/tools/user_management/static/js/user_management'),
+ 'pgadmin.user_management.current_user': '/user_management/current_user',
+ 'slick.pgadmin.editors': path.join(__dirname, './pgadmin/tools/../static/js/slickgrid/editors'),
+ 'slick.pgadmin.formatters': path.join(__dirname, './pgadmin/tools/../static/js/slickgrid/formatters'),
},
externals: [
'pgadmin.user_management.current_user',
--
2.16.2
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-09 11:02 Khushboo Vashi <[email protected]>
parent: Joao De Almeida Pereira <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Khushboo Vashi @ 2018-05-09 11:02 UTC (permalink / raw)
To: Joao De Almeida Pereira <[email protected]>; +Cc: Dave Page <[email protected]>; Ashesh Vashi <[email protected]>; Anthony Emengo <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
Hi Joao,
Patch 0001 & 0002 (esp the new tree implementation) look good to me.
Patch 0003 - I have some concerns regarding the approach taken to separate
out the PG utilities. The dialogue factory is responsible for creating the
dialogue which uses dialogue wrapper. So, any module should call the
dialogue factory to create a new dialogue instance. Here dialogue itself
call the dialogue factory for creating it.
In the enable_disable_triggers.js file, can we just import alertifyjs
instead of passing in all the functions?
Patch 0004 - Right now with the new proposed architecture only JS files are
re-arranged. To maintain the pluggability I would also like to have some
discussion on the back end side. Right now, the code maintains everything.
Let say I want to add any node under schema I will just drop the related
folder and files in that hierarchy and it will work. But with the proposed
architecture I can not visualize the backend architecture and this should
not be ended with only JS restructuring.
Thanks,
Khushboo
On Tue, May 8, 2018 at 12:31 AM, Joao De Almeida Pereira <
[email protected]> wrote:
> Hi Hackers,
> We noticed that some of the latest changes created some conflicts with
> this patch. So you can find attached the patch rebased on the new master.
>
> Thanks
> Victoria && Joao
>
> On Fri, May 4, 2018 at 6:03 PM Joao De Almeida Pereira <
> [email protected]> wrote:
>
>> And now the patches......
>>
>>
>> On Fri, May 4, 2018 at 6:01 PM Joao De Almeida Pereira <
>> [email protected]> wrote:
>>
>>> Hi Hackers,
>>>
>>> *Be prepared this is going to be a big email.*
>>> Objectives of this patch:
>>>
>>> 1. Start the work to split ACI from pgAdmin code
>>> 2. Add more tests around the front end components
>>> 3. Start the discussion on application architecture
>>>
>>> 1. Start the work to split ACI from pgAdmin code Why
>>>
>>> This journey started on our first attempt to store state of the current
>>> tree, so that when a user accessed again pgAdmin
>>> there would be no need to reopen all the nodes and make the flow better.
>>> Another problem users brought to us was related
>>> to the UI behavior when the number of objects was too big.
>>>
>>> In order to do implement this features we would have to play around with
>>> aciTree or some functions from our code that
>>> called and leveraged the aciTree functionalities. To do this we needed
>>> to have some confidence that our changes would
>>> implement correctly the new feature and it would not break the current
>>> functionality.
>>> The process that we used was first extract the functions that interacted
>>> with the tree, wrap them with
>>> tests and then refactor them in some way. While doing this work we
>>> realized that the tree started spreading roots
>>> throughout the majority of the application.
>>> How
>>>
>>> Options:
>>>
>>> 1. Rewrite the front end
>>> 2. One bang replace ACI Tree without change on the application
>>> 3. Create an abstraction layer between ACI Tree and the application
>>>
>>> 1. Rewrite the front end
>>> Pros Const
>>> Achieve decoupling by recreating the frontend Cost is too high
>>> Do not rely on deprecated or unmaintained libraries Turn around is too
>>> long
>>> One shot change 2. One bang replace ACI Tree
>>> Pros Const
>>> Achieve decoupling by recreating the frontend Cost is too high
>>> Does not achieve the decoupling unless we change the application
>>> Need to create an adaptor
>>> No garantee of success
>>> One shot change 3. Create an abstraction layer between ACI Tree and the
>>> application
>>> Pros Const
>>> Achieve decoupling of the application and the tree 90% of the time on
>>> Code archaeology
>>> Lower cost
>>> Ability to change or keep ACI Tree, decision TBD
>>> Can be done in a iterative way
>>>
>>> We decided that options 3 looked more attractive specially because we
>>> could do it in a iterative way.
>>>
>>> The next image depicts the current state of the interactions between the
>>> application and the ACI and the place we want
>>> to be in.
>>>
>>> [image: Screen Shot 2018-05-04 at 4.06.17 PM.png]
>>>
>>> for retrieving of data from the ACI Tree that then is used in different
>>> ways.
>>>
>>> Because we are doing this in a iterative process we need to keep the
>>> current tree working and create our adaptor next
>>> to it. In order to do that we created a new property on PGBrowser class
>>> called treeMenu and when ACI Tree receives
>>> the node information we also populate the new Tree with all the needed
>>> information.
>>>
>>> This approach allow us not to worry, for now, with data retrieval, URL
>>> system and other issues that should be addressed
>>> in the future when the adaptor is done.
>>>
>>> This first patch tries to deal with the low hanging fruit, functions
>>> that are triggered by events from ACI Tree.
>>> Cases like registering to events and triggering events need to be
>>> handled in the future as well.
>>> 2. Add more tests around the front end components Why
>>>
>>> Go Fast Forever: https://builttoadapt.io/why-tdd-489fdcdda05e
>>> I think this is a very good summary of why do we need tests in our
>>> applications.
>>> 3. Start the discussion on application architecture
>>>
>>> Why should we care about location of files inside a our application?
>>>
>>> Why is this way better the another?
>>>
>>> These are 2 good questions that have very lengthy answers. Trying to
>>> more or less summarize the answers we care about
>>> the location of the files, because we want our application to
>>> communicate intent and there are always pros and cons
>>> on all the decisions that we make.
>>>
>>> At this point the application structure follows our menu, this approach
>>> eventually make is easier to follow the code
>>> but at the same time if the menu changes down the line, will we change
>>> the structure of our folders?
>>>
>>> The proposal that we do with the last diff of this patch is to change to
>>> a structure that slices vertically the
>>> application. This way we can understand intent behind the code and more
>>> easily find what we are looking for.
>>>
>>> In the current structure if you want to see the tables code you need to
>>> go to
>>> pgAdmin/browser/server_groups/servers/databases/schemas/tables/ this is
>>> a huge path to remember and to get to. What
>>> do we win with this? If we open pgAdmin we know which nodes to click in
>>> order to get to tables. But for development
>>> every time that you are looking for a specific functionality you need to
>>> run the application, navigate the menu so that
>>> you know where you can find the code. This doesn’t sound very appealing.
>>>
>>> What if our structure would look like this:
>>>
>>> - web
>>> - tables
>>> - controller
>>> - get_nodes.py
>>> - get_sql.py
>>> - __init__.py
>>> - frontend
>>> - component
>>> - ddl_component.js
>>> - services
>>> - table-service.js
>>> - schemas
>>> - servers
>>> - ....
>>>
>>> This would saves us time because all the information that we need is
>>> what are we working on and everything is right there.
>>> Menu driven structure Intent Driven Structure
>>> *Pros:* *Pros:*
>>> Already in place Explicitly shows features
>>> Self contained features Self contained features
>>> Support for drop in features Support for drop in features
>>> *Cons:* *Cons:*
>>> Follows the menu, and it could change Need to change current code
>>> Hard to find features Some additional plumbing might be needed
>>> Drop in features need to be placed in a specific location according to
>>> the menu location
>>>
>>> What are your thought about this architecture?
>>>
>>> Around minute 7 of this video
>>> <https://www.youtube.com/watch?v=hALFGQNeEnU; Uncle Bob shows an
>>> application written
>>> in rails to talk about architecture. It is a long KeyNote if you are
>>> curious I would advise you to see the full video.
>>> His approach to architecture of the application is pretty interesting.
>>> ------------------------------
>>> Patches 0001 Change the order of the shims on the shim file
>>>
>>> Simple change that orders the shims inside the shim file
>>> 0002 New tree implementation
>>>
>>> This is the first version of our Tree implementation. At this point is a
>>> very simple tree without no abstractions and
>>> with code that eventually is not very performant, but this is only the
>>> first iteration and we are trying to follow the
>>> Last Responsible Moment Principle
>>> <https://medium.com/@aidanjcasey/guiding-principles-for-an-evolutionary-software-architecture-b6dc2cb...;
>>> .
>>>
>>> What can you find in this patch:
>>>
>>> - Creation of PGBrowser.treeMenu
>>> - Initial version of the Tree Adaptor web/pgadmin/static/js/tree/
>>> tree.js
>>> - TreeFake test double
>>> <https://martinfowler.com/bliki/TestDouble.html; that can replace
>>> the Tree for testing purposes
>>> - Tests. As an interesting asside because Fake’s need to behave like
>>> the real object you will noticed that there are
>>> tests for this type of double and they the same as of the real
>>> object.
>>>
>>> 0003 Extract, Test and Refactor Methods
>>>
>>> This is the patch that contains the change that we talked a in the
>>> objectives. It touches in a subset of the places
>>> where itemData function is called. To have a sense of progress the
>>> following image depicts, on the left all places
>>> where this functions can be found in the code and on the right the
>>> places where it still remains after this patch.
>>>
>>> But this patch is not only related to the ACI Tree, it also creates some
>>> abstractions from code that we found repeated.
>>> Some examples of this are the dialogs for Backup and Restore, where the
>>> majority of the logic was common, so we created
>>> in web/pgadmin/static/js/alertify/ 3 different objects:
>>> A Factory DialogFactory that is responsible for creating new Dialog and
>>> these dialogs will use the DialogWrapper.
>>>
>>> This is also a good example of the Last Responsible Moment Principle
>>> <https://medium.com/@aidanjcasey/guiding-principles-for-an-evolutionary-software-architecture-b6dc2cb...;
>>> ,
>>> in Dialog there are still some functions that are used in a Recover and
>>> Backup scenario. When we have more examples
>>> we will be able to, if we want, to extract that into another function.
>>>
>>> While doing some code refactoring we found out that the Right Click Menu
>>> as some checks that can be centralized. Examples
>>> of these are checks for Enable/Disable of an option of the display of
>>> the Create/Edit/Drop option in some menus.
>>> Check web/pgadmin/static/js/menu/ for more information. This simplified
>>> code like:
>>>
>>> canCreate: function(itemData, item, data) {
>>> - //If check is false then , we will allow create menu
>>> - if (data && data.check == false)
>>> - return true;
>>> -
>>> - var t = pgBrowser.tree, i = item, d = itemData;
>>> - // To iterate over tree to check parent node
>>> - while (i) {
>>> - // If it is schema then allow user to create domain
>>> - if (_.indexOf(['schema'], d._type) > -1)
>>> - return true;
>>> -
>>> - if ('coll-domain' == d._type) {
>>> - //Check if we are not child of catalog
>>> - var prev_i = t.hasParent(i) ? t.parent(i) : null,
>>> - prev_d = prev_i ? t.itemData(prev_i) : null;
>>> - if( prev_d._type == 'catalog') {
>>> - return false;
>>> - } else {
>>> - return true;
>>> - }
>>> - }
>>> - i = t.hasParent(i) ? t.parent(i) : null;
>>> - d = i ? t.itemData(i) : null;
>>> - }
>>> - // by default we do not want to allow create menu
>>> - return true;
>>> + return canCreate.canCreate(pgBrowser, 'coll-domain', item, data);
>>> }
>>>
>>> This refactor was made in a couple of places throughout the code.
>>>
>>> Another refactor that had to be done was the creation of the functions
>>> getTreeNodeHierarchyFromElement and
>>> getTreeNodeHierarchyFromIdentifier that replace the old
>>> getTreeNodeHierarchy. This implementation was done
>>> because for the time being we have 2 different types of trees and calls
>>> to them.
>>>
>>> The rest of the changes resulted from our process of refactoring that we
>>> are following:
>>>
>>> 1. Find a location where itemData is used
>>> 2. Extract that into a function outside of the current code
>>> 3. Wrap it with tests
>>> 4. When we have enough confidence on the tests, we refactor the
>>> function to use the new Tree
>>> 5. We refactor the function to become more readable
>>> 6. Start over
>>>
>>> 0004 Architecture
>>>
>>> The proposed structure is our vision for the application, this is not
>>> the state we will be in when this patch lands.
>>> For now the only change we are doing is move the javascript into
>>> static/js/FEATURE_NAME. This is a first step in the
>>> direction that we would like to see the product heading.
>>>
>>> Even if we decide to keep using the same architecture the move of
>>> Javascript to a centralized place will
>>> not break any plugability as webpack can retrieve all the Javascript
>>> from any place in the code.
>>>
>>> Have a nice weekend
>>> Joao
>>>
>>>
>>>
Attachments:
[image/png] Screen Shot 2018-05-04 at 4.06.17 PM.png (104.4K, 3-Screen%20Shot%202018-05-04%20at%204.06.17%20PM.png)
download | view image
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-09 12:38 Akshay Joshi <[email protected]>
parent: Khushboo Vashi <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Akshay Joshi @ 2018-05-09 12:38 UTC (permalink / raw)
To: Joao Pedro De Almeida Pereira <[email protected]>; +Cc: Dave Page <[email protected]>; Ashesh Vashi <[email protected]>; Anthony Emengo <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
Hi Joao
Below are my review comments:
- In *menu/can_create.js,* "parentCatalogOfTableChild" function name is
unable to understand.
- Below code is bit confusing in terms of naming convention canCreate.
canCreate inside "canCreate".
canCreate: function(itemData, item, data) {
return canCreate.canCreate(pgBrowser, 'coll-collation', item, data);
}
Can we have something like "*menuCreator.canCreate*"?
- Why we have "*menu_utils.js*" for Backup, Maintenance, Restore and
Grant Wizard? Can we have one single file with all the supported node for
respective module? Also I have seen that the folder hierarchy for grant
wizard is "/*web/pgadmin/static/js/grant/wizard/menu_utils.js*" why
there are two separate folders grant and wizard (sub folder of grant)?
On Wed, May 9, 2018 at 4:32 PM, Khushboo Vashi <
[email protected]> wrote:
> Hi Joao,
>
> Patch 0001 & 0002 (esp the new tree implementation) look good to me.
>
> Patch 0003 - I have some concerns regarding the approach taken to separate
> out the PG utilities. The dialogue factory is responsible for creating the
> dialogue which uses dialogue wrapper. So, any module should call the
> dialogue factory to create a new dialogue instance. Here dialogue itself
> call the dialogue factory for creating it.
>
> In the enable_disable_triggers.js file, can we just import alertifyjs
> instead of passing in all the functions?
>
> Patch 0004 - Right now with the new proposed architecture only JS files
> are re-arranged. To maintain the pluggability I would also like to have
> some discussion on the back end side. Right now, the code maintains
> everything. Let say I want to add any node under schema I will just drop
> the related folder and files in that hierarchy and it will work. But with
> the proposed architecture I can not visualize the backend architecture and
> this should not be ended with only JS restructuring.
>
> Thanks,
> Khushboo
>
> On Tue, May 8, 2018 at 12:31 AM, Joao De Almeida Pereira <
> [email protected]> wrote:
>
>> Hi Hackers,
>> We noticed that some of the latest changes created some conflicts with
>> this patch. So you can find attached the patch rebased on the new master.
>>
>> Thanks
>> Victoria && Joao
>>
>> On Fri, May 4, 2018 at 6:03 PM Joao De Almeida Pereira <
>> [email protected]> wrote:
>>
>>> And now the patches......
>>>
>>>
>>> On Fri, May 4, 2018 at 6:01 PM Joao De Almeida Pereira <
>>> [email protected]> wrote:
>>>
>>>> Hi Hackers,
>>>>
>>>> *Be prepared this is going to be a big email.*
>>>> Objectives of this patch:
>>>>
>>>> 1. Start the work to split ACI from pgAdmin code
>>>> 2. Add more tests around the front end components
>>>> 3. Start the discussion on application architecture
>>>>
>>>> 1. Start the work to split ACI from pgAdmin code Why
>>>>
>>>> This journey started on our first attempt to store state of the current
>>>> tree, so that when a user accessed again pgAdmin
>>>> there would be no need to reopen all the nodes and make the flow
>>>> better. Another problem users brought to us was related
>>>> to the UI behavior when the number of objects was too big.
>>>>
>>>> In order to do implement this features we would have to play around
>>>> with aciTree or some functions from our code that
>>>> called and leveraged the aciTree functionalities. To do this we needed
>>>> to have some confidence that our changes would
>>>> implement correctly the new feature and it would not break the current
>>>> functionality.
>>>> The process that we used was first extract the functions that
>>>> interacted with the tree, wrap them with
>>>> tests and then refactor them in some way. While doing this work we
>>>> realized that the tree started spreading roots
>>>> throughout the majority of the application.
>>>> How
>>>>
>>>> Options:
>>>>
>>>> 1. Rewrite the front end
>>>> 2. One bang replace ACI Tree without change on the application
>>>> 3. Create an abstraction layer between ACI Tree and the application
>>>>
>>>> 1. Rewrite the front end
>>>> Pros Const
>>>> Achieve decoupling by recreating the frontend Cost is too high
>>>> Do not rely on deprecated or unmaintained libraries Turn around is too
>>>> long
>>>> One shot change 2. One bang replace ACI Tree
>>>> Pros Const
>>>> Achieve decoupling by recreating the frontend Cost is too high
>>>> Does not achieve the decoupling unless we change the application
>>>> Need to create an adaptor
>>>> No garantee of success
>>>> One shot change 3. Create an abstraction layer between ACI Tree and
>>>> the application
>>>> Pros Const
>>>> Achieve decoupling of the application and the tree 90% of the time on
>>>> Code archaeology
>>>> Lower cost
>>>> Ability to change or keep ACI Tree, decision TBD
>>>> Can be done in a iterative way
>>>>
>>>> We decided that options 3 looked more attractive specially because we
>>>> could do it in a iterative way.
>>>>
>>>> The next image depicts the current state of the interactions between
>>>> the application and the ACI and the place we want
>>>> to be in.
>>>>
>>>> [image: Screen Shot 2018-05-04 at 4.06.17 PM.png]
>>>>
>>>> for retrieving of data from the ACI Tree that then is used in different
>>>> ways.
>>>>
>>>> Because we are doing this in a iterative process we need to keep the
>>>> current tree working and create our adaptor next
>>>> to it. In order to do that we created a new property on PGBrowser
>>>> class called treeMenu and when ACI Tree receives
>>>> the node information we also populate the new Tree with all the needed
>>>> information.
>>>>
>>>> This approach allow us not to worry, for now, with data retrieval, URL
>>>> system and other issues that should be addressed
>>>> in the future when the adaptor is done.
>>>>
>>>> This first patch tries to deal with the low hanging fruit, functions
>>>> that are triggered by events from ACI Tree.
>>>> Cases like registering to events and triggering events need to be
>>>> handled in the future as well.
>>>> 2. Add more tests around the front end components Why
>>>>
>>>> Go Fast Forever: https://builttoadapt.io/why-tdd-489fdcdda05e
>>>> I think this is a very good summary of why do we need tests in our
>>>> applications.
>>>> 3. Start the discussion on application architecture
>>>>
>>>> Why should we care about location of files inside a our application?
>>>>
>>>> Why is this way better the another?
>>>>
>>>> These are 2 good questions that have very lengthy answers. Trying to
>>>> more or less summarize the answers we care about
>>>> the location of the files, because we want our application to
>>>> communicate intent and there are always pros and cons
>>>> on all the decisions that we make.
>>>>
>>>> At this point the application structure follows our menu, this approach
>>>> eventually make is easier to follow the code
>>>> but at the same time if the menu changes down the line, will we change
>>>> the structure of our folders?
>>>>
>>>> The proposal that we do with the last diff of this patch is to change
>>>> to a structure that slices vertically the
>>>> application. This way we can understand intent behind the code and more
>>>> easily find what we are looking for.
>>>>
>>>> In the current structure if you want to see the tables code you need to
>>>> go to
>>>> pgAdmin/browser/server_groups/servers/databases/schemas/tables/ this
>>>> is a huge path to remember and to get to. What
>>>> do we win with this? If we open pgAdmin we know which nodes to click in
>>>> order to get to tables. But for development
>>>> every time that you are looking for a specific functionality you need
>>>> to run the application, navigate the menu so that
>>>> you know where you can find the code. This doesn’t sound very appealing.
>>>>
>>>> What if our structure would look like this:
>>>>
>>>> - web
>>>> - tables
>>>> - controller
>>>> - get_nodes.py
>>>> - get_sql.py
>>>> - __init__.py
>>>> - frontend
>>>> - component
>>>> - ddl_component.js
>>>> - services
>>>> - table-service.js
>>>> - schemas
>>>> - servers
>>>> - ....
>>>>
>>>> This would saves us time because all the information that we need is
>>>> what are we working on and everything is right there.
>>>> Menu driven structure Intent Driven Structure
>>>> *Pros:* *Pros:*
>>>> Already in place Explicitly shows features
>>>> Self contained features Self contained features
>>>> Support for drop in features Support for drop in features
>>>> *Cons:* *Cons:*
>>>> Follows the menu, and it could change Need to change current code
>>>> Hard to find features Some additional plumbing might be needed
>>>> Drop in features need to be placed in a specific location according to
>>>> the menu location
>>>>
>>>> What are your thought about this architecture?
>>>>
>>>> Around minute 7 of this video
>>>> <https://www.youtube.com/watch?v=hALFGQNeEnU; Uncle Bob shows an
>>>> application written
>>>> in rails to talk about architecture. It is a long KeyNote if you are
>>>> curious I would advise you to see the full video.
>>>> His approach to architecture of the application is pretty interesting.
>>>> ------------------------------
>>>> Patches 0001 Change the order of the shims on the shim file
>>>>
>>>> Simple change that orders the shims inside the shim file
>>>> 0002 New tree implementation
>>>>
>>>> This is the first version of our Tree implementation. At this point is
>>>> a very simple tree without no abstractions and
>>>> with code that eventually is not very performant, but this is only the
>>>> first iteration and we are trying to follow the
>>>> Last Responsible Moment Principle
>>>> <https://medium.com/@aidanjcasey/guiding-principles-for-an-evolutionary-software-architecture-b6dc2cb...;
>>>> .
>>>>
>>>> What can you find in this patch:
>>>>
>>>> - Creation of PGBrowser.treeMenu
>>>> - Initial version of the Tree Adaptor web/pgadmin/static/js/tree/tre
>>>> e.js
>>>> - TreeFake test double
>>>> <https://martinfowler.com/bliki/TestDouble.html; that can replace
>>>> the Tree for testing purposes
>>>> - Tests. As an interesting asside because Fake’s need to behave
>>>> like the real object you will noticed that there are
>>>> tests for this type of double and they the same as of the real
>>>> object.
>>>>
>>>> 0003 Extract, Test and Refactor Methods
>>>>
>>>> This is the patch that contains the change that we talked a in the
>>>> objectives. It touches in a subset of the places
>>>> where itemData function is called. To have a sense of progress the
>>>> following image depicts, on the left all places
>>>> where this functions can be found in the code and on the right the
>>>> places where it still remains after this patch.
>>>>
>>>> But this patch is not only related to the ACI Tree, it also creates
>>>> some abstractions from code that we found repeated.
>>>> Some examples of this are the dialogs for Backup and Restore, where the
>>>> majority of the logic was common, so we created
>>>> in web/pgadmin/static/js/alertify/ 3 different objects:
>>>> A Factory DialogFactory that is responsible for creating new Dialog
>>>> and these dialogs will use the DialogWrapper.
>>>>
>>>> This is also a good example of the Last Responsible Moment Principle
>>>> <https://medium.com/@aidanjcasey/guiding-principles-for-an-evolutionary-software-architecture-b6dc2cb...;
>>>> ,
>>>> in Dialog there are still some functions that are used in a Recover
>>>> and Backup scenario. When we have more examples
>>>> we will be able to, if we want, to extract that into another function.
>>>>
>>>> While doing some code refactoring we found out that the Right Click
>>>> Menu as some checks that can be centralized. Examples
>>>> of these are checks for Enable/Disable of an option of the display of
>>>> the Create/Edit/Drop option in some menus.
>>>> Check web/pgadmin/static/js/menu/ for more information. This
>>>> simplified code like:
>>>>
>>>> canCreate: function(itemData, item, data) {
>>>> - //If check is false then , we will allow create menu
>>>> - if (data && data.check == false)
>>>> - return true;
>>>> -
>>>> - var t = pgBrowser.tree, i = item, d = itemData;
>>>> - // To iterate over tree to check parent node
>>>> - while (i) {
>>>> - // If it is schema then allow user to create domain
>>>> - if (_.indexOf(['schema'], d._type) > -1)
>>>> - return true;
>>>> -
>>>> - if ('coll-domain' == d._type) {
>>>> - //Check if we are not child of catalog
>>>> - var prev_i = t.hasParent(i) ? t.parent(i) : null,
>>>> - prev_d = prev_i ? t.itemData(prev_i) : null;
>>>> - if( prev_d._type == 'catalog') {
>>>> - return false;
>>>> - } else {
>>>> - return true;
>>>> - }
>>>> - }
>>>> - i = t.hasParent(i) ? t.parent(i) : null;
>>>> - d = i ? t.itemData(i) : null;
>>>> - }
>>>> - // by default we do not want to allow create menu
>>>> - return true;
>>>> + return canCreate.canCreate(pgBrowser, 'coll-domain', item, data);
>>>> }
>>>>
>>>> This refactor was made in a couple of places throughout the code.
>>>>
>>>> Another refactor that had to be done was the creation of the functions
>>>> getTreeNodeHierarchyFromElement and
>>>> getTreeNodeHierarchyFromIdentifier that replace the old
>>>> getTreeNodeHierarchy. This implementation was done
>>>> because for the time being we have 2 different types of trees and calls
>>>> to them.
>>>>
>>>> The rest of the changes resulted from our process of refactoring that
>>>> we are following:
>>>>
>>>> 1. Find a location where itemData is used
>>>> 2. Extract that into a function outside of the current code
>>>> 3. Wrap it with tests
>>>> 4. When we have enough confidence on the tests, we refactor the
>>>> function to use the new Tree
>>>> 5. We refactor the function to become more readable
>>>> 6. Start over
>>>>
>>>> 0004 Architecture
>>>>
>>>> The proposed structure is our vision for the application, this is not
>>>> the state we will be in when this patch lands.
>>>> For now the only change we are doing is move the javascript into
>>>> static/js/FEATURE_NAME. This is a first step in the
>>>> direction that we would like to see the product heading.
>>>>
>>>> Even if we decide to keep using the same architecture the move of
>>>> Javascript to a centralized place will
>>>> not break any plugability as webpack can retrieve all the Javascript
>>>> from any place in the code.
>>>>
>>>> Have a nice weekend
>>>> Joao
>>>>
>>>>
>>>>
>
--
*Akshay Joshi*
*Sr. Software Architect *
*Phone: +91 20-3058-9517Mobile: +91 976-788-8246*
Attachments:
[image/png] Screen Shot 2018-05-04 at 4.06.17 PM.png (104.4K, 3-Screen%20Shot%202018-05-04%20at%204.06.17%20PM.png)
download | view image
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-09 15:00 Joao De Almeida Pereira <[email protected]>
parent: Akshay Joshi <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Joao De Almeida Pereira @ 2018-05-09 15:00 UTC (permalink / raw)
To: Akshay Joshi <[email protected]>; +Cc: Dave Page <[email protected]>; Ashesh Vashi <[email protected]>; Anthony Emengo <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
Hi Hackers
There were very good points raised. Do you think that these are blockers
for 0001, 0002, and 0003 getting committed? In general, we felt like there
were points made about very specific issues, were there any broader
concerns about the patches? We don't need an agreement on architecture yet
for patches 0001, 0002, and 0003.
@Khushboo Vashi <[email protected]>
> Patch 0003 - I have some concerns regarding the approach taken to separate
> out the PG utilities. The dialogue factory is responsible for creating the
> dialogue which uses dialogue wrapper. So, any module should call the
> dialogue factory to create a new dialogue instance. Here dialogue itself
> call the dialogue factory for creating it.
>
We did not fully understand what your concern is on this point.
*If you are concerned that we have a dialogWrapper and a dialog class and
don't understand the difference between the two?*
We don't feel too strongly about this decision. It seemed easier at the
time (to test) because the wrapper represents what will be passed into the
alertify library. We're certainly open to having this implementation change
in the future.
In the enable_disable_triggers.js file, can we just import alertifyjs
> instead of passing in all the functions?
We made this decision to make testing of the functionality easier to test,
by using a principle called dependency injection.
https://martinfowler.com/articles/injection.html. If we import alertifyjs
directly, then we can't stub out its functionality.
>
Patch 0004 - Right now with the new proposed architecture only JS files are
> re-arranged. To maintain the pluggability I would also like to have some
> discussion on the back end side. Right now, the code maintains everything.
> Let say I want to add any node under schema I will just drop the related
> folder and files in that hierarchy and it will work. But with the proposed
> architecture I can not visualize the backend architecture and this should
> not be ended with only JS restructuring.
This patch in fact only moves Javascript files from one folder structure to
a different folder structure. The process we are following is an iterative
process and will be built upon itself. The end vision that we have for the
product is as depicted in the previous email an Intent Driven Structure and
in this structure each folder will contain a Feature that will be self
contained (Backend + Frontend). The major difference between what we have
now and what we envision this application to look like is that these
folders can live at the same level so that we know at a glance what this
application is capable of doing. This structure will also make it easier to
onboard someone that is looking to help building any feature because they
will have access to all of the features in the "same screen" instead of
having to do a deep dive into the application code in order to find the
place to start.
As of right now we haven't though very deep on the backend side of things.
But looking at the current structure moving from a hierarchical file
structure into a horizontal one, doesn't look like a lot of plumbing.
Nevertheless this is something that we should address when we get there and
we believe we are not there yet.
@Akshay Joshi <[email protected]>
> In *menu/can_create.js,* "parentCatalogOfTableChild" function name is
> unable to understand.
The intent behind this function is to retrieve all the tree child node that
matches a given type argument and has a direct parent of the catalog type.
The name was in a hurry and could certainly be improved. Any suggestions
are welcome. How about **matchesTypeAndHasCatalogParent(self, arg)**?
Below code is bit confusing in terms of naming convention
> canCreate.canCreate inside "canCreate".
That sounds great. Let's do that.
Why we have "menu_utils.js" for Backup, Maintenance, Restore and Grant
> Wizard? Can we have one single file with all the supported node for
> respective module?
It was for expressing developer intent. The menu_utils.js that were placed
in discrete packages have functionality that pertain only to its package.
If we thought that the functionality were shared, we wouldn't have put them
in the shared folder `pgadmin4/web/pgadmin/static/js/menu/...`
> Also I have seen that the folder hierarchy for grant wizard is
> "/web/pgadmin/static/js/grant/wizard/menu_utils.js" why there are two
> separate folders grant and wizard (sub folder of grant)?
It makes sense to think of the wizard as separate entity from grants. You
could imagine that when we add additional functionality for grants, that
we'd want it alongside the wizard functionality. You could call it
premature at this point.
@pgadmin-hackers <[email protected]>
We understand that our vision for the application is quite bold and if we,
as a community, decide that this is the way to go it is going to take some
time to accomplish and will involve effort. But we believe this will take
the code into a better place.
Patches 1,2 and 3 are our priority at this point, because there are
features that we want to implement that depend on the separation from ACI
Tree, and we would like to see these in master as soon as possible.
Joao and Anthony
On Wed, May 9, 2018 at 8:38 AM Akshay Joshi <[email protected]>
wrote:
> Hi Joao
>
> Below are my review comments:
>
> - In *menu/can_create.js,* "parentCatalogOfTableChild" function name
> is unable to understand.
> - Below code is bit confusing in terms of naming convention canCreate.
> canCreate inside "canCreate".
>
> canCreate: function(itemData, item, data) {
>
> return canCreate.canCreate(pgBrowser, 'coll-collation', item, data);
>
> }
>
> Can we have something like "*menuCreator.canCreate*"?
>
>
> - Why we have "*menu_utils.js*" for Backup, Maintenance, Restore and
> Grant Wizard? Can we have one single file with all the supported node for
> respective module? Also I have seen that the folder hierarchy for grant
> wizard is "/*web/pgadmin/static/js/grant/wizard/menu_utils.js*" why
> there are two separate folders grant and wizard (sub folder of grant)?
>
>
>
> On Wed, May 9, 2018 at 4:32 PM, Khushboo Vashi <
> [email protected]> wrote:
>
>> Hi Joao,
>>
>> Patch 0001 & 0002 (esp the new tree implementation) look good to me.
>>
>> Patch 0003 - I have some concerns regarding the approach taken to
>> separate out the PG utilities. The dialogue factory is responsible for
>> creating the dialogue which uses dialogue wrapper. So, any module should
>> call the dialogue factory to create a new dialogue instance. Here dialogue
>> itself call the dialogue factory for creating it.
>>
>> In the enable_disable_triggers.js file, can we just import alertifyjs
>> instead of passing in all the functions?
>>
>> Patch 0004 - Right now with the new proposed architecture only JS files
>> are re-arranged. To maintain the pluggability I would also like to have
>> some discussion on the back end side. Right now, the code maintains
>> everything. Let say I want to add any node under schema I will just drop
>> the related folder and files in that hierarchy and it will work. But with
>> the proposed architecture I can not visualize the backend architecture and
>> this should not be ended with only JS restructuring.
>>
>> Thanks,
>> Khushboo
>>
>> On Tue, May 8, 2018 at 12:31 AM, Joao De Almeida Pereira <
>> [email protected]> wrote:
>>
>>> Hi Hackers,
>>> We noticed that some of the latest changes created some conflicts with
>>> this patch. So you can find attached the patch rebased on the new master.
>>>
>>> Thanks
>>> Victoria && Joao
>>>
>>> On Fri, May 4, 2018 at 6:03 PM Joao De Almeida Pereira <
>>> [email protected]> wrote:
>>>
>>>> And now the patches......
>>>>
>>>>
>>>> On Fri, May 4, 2018 at 6:01 PM Joao De Almeida Pereira <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi Hackers,
>>>>>
>>>>> *Be prepared this is going to be a big email.*
>>>>> Objectives of this patch:
>>>>>
>>>>> 1. Start the work to split ACI from pgAdmin code
>>>>> 2. Add more tests around the front end components
>>>>> 3. Start the discussion on application architecture
>>>>>
>>>>> 1. Start the work to split ACI from pgAdmin code Why
>>>>>
>>>>> This journey started on our first attempt to store state of the
>>>>> current tree, so that when a user accessed again pgAdmin
>>>>> there would be no need to reopen all the nodes and make the flow
>>>>> better. Another problem users brought to us was related
>>>>> to the UI behavior when the number of objects was too big.
>>>>>
>>>>> In order to do implement this features we would have to play around
>>>>> with aciTree or some functions from our code that
>>>>> called and leveraged the aciTree functionalities. To do this we needed
>>>>> to have some confidence that our changes would
>>>>> implement correctly the new feature and it would not break the current
>>>>> functionality.
>>>>> The process that we used was first extract the functions that
>>>>> interacted with the tree, wrap them with
>>>>> tests and then refactor them in some way. While doing this work we
>>>>> realized that the tree started spreading roots
>>>>> throughout the majority of the application.
>>>>> How
>>>>>
>>>>> Options:
>>>>>
>>>>> 1. Rewrite the front end
>>>>> 2. One bang replace ACI Tree without change on the application
>>>>> 3. Create an abstraction layer between ACI Tree and the application
>>>>>
>>>>> 1. Rewrite the front end
>>>>> Pros Const
>>>>> Achieve decoupling by recreating the frontend Cost is too high
>>>>> Do not rely on deprecated or unmaintained libraries Turn around is
>>>>> too long
>>>>> One shot change 2. One bang replace ACI Tree
>>>>> Pros Const
>>>>> Achieve decoupling by recreating the frontend Cost is too high
>>>>> Does not achieve the decoupling unless we change the application
>>>>> Need to create an adaptor
>>>>> No garantee of success
>>>>> One shot change 3. Create an abstraction layer between ACI Tree and
>>>>> the application
>>>>> Pros Const
>>>>> Achieve decoupling of the application and the tree 90% of the time on
>>>>> Code archaeology
>>>>> Lower cost
>>>>> Ability to change or keep ACI Tree, decision TBD
>>>>> Can be done in a iterative way
>>>>>
>>>>> We decided that options 3 looked more attractive specially because we
>>>>> could do it in a iterative way.
>>>>>
>>>>> The next image depicts the current state of the interactions between
>>>>> the application and the ACI and the place we want
>>>>> to be in.
>>>>>
>>>>> [image: Screen Shot 2018-05-04 at 4.06.17 PM.png]
>>>>>
>>>>> for retrieving of data from the ACI Tree that then is used in
>>>>> different ways.
>>>>>
>>>>> Because we are doing this in a iterative process we need to keep the
>>>>> current tree working and create our adaptor next
>>>>> to it. In order to do that we created a new property on PGBrowser
>>>>> class called treeMenu and when ACI Tree receives
>>>>> the node information we also populate the new Tree with all the needed
>>>>> information.
>>>>>
>>>>> This approach allow us not to worry, for now, with data retrieval, URL
>>>>> system and other issues that should be addressed
>>>>> in the future when the adaptor is done.
>>>>>
>>>>> This first patch tries to deal with the low hanging fruit, functions
>>>>> that are triggered by events from ACI Tree.
>>>>> Cases like registering to events and triggering events need to be
>>>>> handled in the future as well.
>>>>> 2. Add more tests around the front end components Why
>>>>>
>>>>> Go Fast Forever: https://builttoadapt.io/why-tdd-489fdcdda05e
>>>>> I think this is a very good summary of why do we need tests in our
>>>>> applications.
>>>>> 3. Start the discussion on application architecture
>>>>>
>>>>> Why should we care about location of files inside a our application?
>>>>>
>>>>> Why is this way better the another?
>>>>>
>>>>> These are 2 good questions that have very lengthy answers. Trying to
>>>>> more or less summarize the answers we care about
>>>>> the location of the files, because we want our application to
>>>>> communicate intent and there are always pros and cons
>>>>> on all the decisions that we make.
>>>>>
>>>>> At this point the application structure follows our menu, this
>>>>> approach eventually make is easier to follow the code
>>>>> but at the same time if the menu changes down the line, will we change
>>>>> the structure of our folders?
>>>>>
>>>>> The proposal that we do with the last diff of this patch is to change
>>>>> to a structure that slices vertically the
>>>>> application. This way we can understand intent behind the code and
>>>>> more easily find what we are looking for.
>>>>>
>>>>> In the current structure if you want to see the tables code you need
>>>>> to go to
>>>>> pgAdmin/browser/server_groups/servers/databases/schemas/tables/ this
>>>>> is a huge path to remember and to get to. What
>>>>> do we win with this? If we open pgAdmin we know which nodes to click
>>>>> in order to get to tables. But for development
>>>>> every time that you are looking for a specific functionality you need
>>>>> to run the application, navigate the menu so that
>>>>> you know where you can find the code. This doesn’t sound very
>>>>> appealing.
>>>>>
>>>>> What if our structure would look like this:
>>>>>
>>>>> - web
>>>>> - tables
>>>>> - controller
>>>>> - get_nodes.py
>>>>> - get_sql.py
>>>>> - __init__.py
>>>>> - frontend
>>>>> - component
>>>>> - ddl_component.js
>>>>> - services
>>>>> - table-service.js
>>>>> - schemas
>>>>> - servers
>>>>> - ....
>>>>>
>>>>> This would saves us time because all the information that we need is
>>>>> what are we working on and everything is right there.
>>>>> Menu driven structure Intent Driven Structure
>>>>> *Pros:* *Pros:*
>>>>> Already in place Explicitly shows features
>>>>> Self contained features Self contained features
>>>>> Support for drop in features Support for drop in features
>>>>> *Cons:* *Cons:*
>>>>> Follows the menu, and it could change Need to change current code
>>>>> Hard to find features Some additional plumbing might be needed
>>>>> Drop in features need to be placed in a specific location according to
>>>>> the menu location
>>>>>
>>>>> What are your thought about this architecture?
>>>>>
>>>>> Around minute 7 of this video
>>>>> <https://www.youtube.com/watch?v=hALFGQNeEnU; Uncle Bob shows an
>>>>> application written
>>>>> in rails to talk about architecture. It is a long KeyNote if you are
>>>>> curious I would advise you to see the full video.
>>>>> His approach to architecture of the application is pretty interesting.
>>>>> ------------------------------
>>>>> Patches 0001 Change the order of the shims on the shim file
>>>>>
>>>>> Simple change that orders the shims inside the shim file
>>>>> 0002 New tree implementation
>>>>>
>>>>> This is the first version of our Tree implementation. At this point is
>>>>> a very simple tree without no abstractions and
>>>>> with code that eventually is not very performant, but this is only the
>>>>> first iteration and we are trying to follow the
>>>>> Last Responsible Moment Principle
>>>>> <https://medium.com/@aidanjcasey/guiding-principles-for-an-evolutionary-software-architecture-b6dc2cb...;
>>>>> .
>>>>>
>>>>> What can you find in this patch:
>>>>>
>>>>> - Creation of PGBrowser.treeMenu
>>>>> - Initial version of the Tree Adaptor
>>>>> web/pgadmin/static/js/tree/tree.js
>>>>> - TreeFake test double
>>>>> <https://martinfowler.com/bliki/TestDouble.html; that can replace
>>>>> the Tree for testing purposes
>>>>> - Tests. As an interesting asside because Fake’s need to behave
>>>>> like the real object you will noticed that there are
>>>>> tests for this type of double and they the same as of the real
>>>>> object.
>>>>>
>>>>> 0003 Extract, Test and Refactor Methods
>>>>>
>>>>> This is the patch that contains the change that we talked a in the
>>>>> objectives. It touches in a subset of the places
>>>>> where itemData function is called. To have a sense of progress the
>>>>> following image depicts, on the left all places
>>>>> where this functions can be found in the code and on the right the
>>>>> places where it still remains after this patch.
>>>>>
>>>>> But this patch is not only related to the ACI Tree, it also creates
>>>>> some abstractions from code that we found repeated.
>>>>> Some examples of this are the dialogs for Backup and Restore, where
>>>>> the majority of the logic was common, so we created
>>>>> in web/pgadmin/static/js/alertify/ 3 different objects:
>>>>> A Factory DialogFactory that is responsible for creating new Dialog
>>>>> and these dialogs will use the DialogWrapper.
>>>>>
>>>>> This is also a good example of the Last Responsible Moment Principle
>>>>> <https://medium.com/@aidanjcasey/guiding-principles-for-an-evolutionary-software-architecture-b6dc2cb...;
>>>>> ,
>>>>> in Dialog there are still some functions that are used in a Recover
>>>>> and Backup scenario. When we have more examples
>>>>> we will be able to, if we want, to extract that into another function.
>>>>>
>>>>> While doing some code refactoring we found out that the Right Click
>>>>> Menu as some checks that can be centralized. Examples
>>>>> of these are checks for Enable/Disable of an option of the display of
>>>>> the Create/Edit/Drop option in some menus.
>>>>> Check web/pgadmin/static/js/menu/ for more information. This
>>>>> simplified code like:
>>>>>
>>>>> canCreate: function(itemData, item, data) {
>>>>> - //If check is false then , we will allow create menu
>>>>> - if (data && data.check == false)
>>>>> - return true;
>>>>> -
>>>>> - var t = pgBrowser.tree, i = item, d = itemData;
>>>>> - // To iterate over tree to check parent node
>>>>> - while (i) {
>>>>> - // If it is schema then allow user to create domain
>>>>> - if (_.indexOf(['schema'], d._type) > -1)
>>>>> - return true;
>>>>> -
>>>>> - if ('coll-domain' == d._type) {
>>>>> - //Check if we are not child of catalog
>>>>> - var prev_i = t.hasParent(i) ? t.parent(i) : null,
>>>>> - prev_d = prev_i ? t.itemData(prev_i) : null;
>>>>> - if( prev_d._type == 'catalog') {
>>>>> - return false;
>>>>> - } else {
>>>>> - return true;
>>>>> - }
>>>>> - }
>>>>> - i = t.hasParent(i) ? t.parent(i) : null;
>>>>> - d = i ? t.itemData(i) : null;
>>>>> - }
>>>>> - // by default we do not want to allow create menu
>>>>> - return true;
>>>>> + return canCreate.canCreate(pgBrowser, 'coll-domain', item, data);
>>>>> }
>>>>>
>>>>> This refactor was made in a couple of places throughout the code.
>>>>>
>>>>> Another refactor that had to be done was the creation of the functions
>>>>> getTreeNodeHierarchyFromElement and
>>>>> getTreeNodeHierarchyFromIdentifier that replace the old
>>>>> getTreeNodeHierarchy. This implementation was done
>>>>> because for the time being we have 2 different types of trees and
>>>>> calls to them.
>>>>>
>>>>> The rest of the changes resulted from our process of refactoring that
>>>>> we are following:
>>>>>
>>>>> 1. Find a location where itemData is used
>>>>> 2. Extract that into a function outside of the current code
>>>>> 3. Wrap it with tests
>>>>> 4. When we have enough confidence on the tests, we refactor the
>>>>> function to use the new Tree
>>>>> 5. We refactor the function to become more readable
>>>>> 6. Start over
>>>>>
>>>>> 0004 Architecture
>>>>>
>>>>> The proposed structure is our vision for the application, this is not
>>>>> the state we will be in when this patch lands.
>>>>> For now the only change we are doing is move the javascript into
>>>>> static/js/FEATURE_NAME. This is a first step in the
>>>>> direction that we would like to see the product heading.
>>>>>
>>>>> Even if we decide to keep using the same architecture the move of
>>>>> Javascript to a centralized place will
>>>>> not break any plugability as webpack can retrieve all the Javascript
>>>>> from any place in the code.
>>>>>
>>>>> Have a nice weekend
>>>>> Joao
>>>>>
>>>>>
>>>>>
>>
>
>
> --
> *Akshay Joshi*
>
> *Sr. Software Architect *
>
>
>
> *Phone: +91 20-3058-9517Mobile: +91 976-788-8246*
>
Attachments:
[image/png] Screen Shot 2018-05-04 at 4.06.17 PM.png (104.4K, 3-Screen%20Shot%202018-05-04%20at%204.06.17%20PM.png)
download | view image
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-10 06:52 Ashesh Vashi <[email protected]>
parent: Joao De Almeida Pereira <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Ashesh Vashi @ 2018-05-10 06:52 UTC (permalink / raw)
To: Joao De Almeida Pereira <[email protected]>; +Cc: Akshay Joshi <[email protected]>; Dave Page <[email protected]>; Anthony Emengo <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
Hi Joao,
On Wed, May 9, 2018 at 8:30 PM, Joao De Almeida Pereira <
[email protected]> wrote:
> Hi Hackers
>
> There were very good points raised. Do you think that these are blockers
> for 0001, 0002, and 0003 getting committed? In general, we felt like there
> were points made about very specific issues, were there any broader
> concerns about the patches? We don't need an agreement on architecture yet
> for patches 0001, 0002, and 0003.
>
I have already commited the patch - 0001.
I have two review comments for 0002 patch.
*In pgadmin/static/js/tree/tree.js:*
1. In TreeNode, we're keepging the reference of DOMElement, do we really
need it?
i.e.
*export class TreeNode {*
* constructor(id, data, domNode, parent) {*
* this.id <http://this.id; = id;*
* this.data = data;*
* this.setParent(parent);*
* this.children = [];*
* this.domNode = domNode;*
* }*
* ...*
*}*
*....*
2. We're exporting a tree object on line #189.
i.e.
*export let tree = new Tree();*
Are you expecting the tree class to be a singleton class?
I think not. Hence - it should not be exported.
I am currently reviewing 0003 patch.
Will share my comments soon.
We will talk about the architecture change in separate mail chain.
-- Thanks, Ashesh
> @Khushboo Vashi <[email protected]>
>
>> Patch 0003 - I have some concerns regarding the approach taken to
>> separate out the PG utilities. The dialogue factory is responsible for
>> creating the dialogue which uses dialogue wrapper. So, any module should
>> call the dialogue factory to create a new dialogue instance. Here dialogue
>> itself call the dialogue factory for creating it.
>>
> We did not fully understand what your concern is on this point.
> *If you are concerned that we have a dialogWrapper and a dialog class and
> don't understand the difference between the two?*
> We don't feel too strongly about this decision. It seemed easier at the
> time (to test) because the wrapper represents what will be passed into the
> alertify library. We're certainly open to having this implementation change
> in the future.
>
> In the enable_disable_triggers.js file, can we just import alertifyjs
>> instead of passing in all the functions?
>
> We made this decision to make testing of the functionality easier to
> test, by using a principle called dependency injection.
> https://martinfowler.com/articles/injection.html. If we import alertifyjs
> directly, then we can't stub out its functionality.
>
>>
>
> Patch 0004 - Right now with the new proposed architecture only JS files
>> are re-arranged. To maintain the pluggability I would also like to have
>> some discussion on the back end side. Right now, the code maintains
>> everything. Let say I want to add any node under schema I will just drop
>> the related folder and files in that hierarchy and it will work. But with
>> the proposed architecture I can not visualize the backend architecture and
>> this should not be ended with only JS restructuring.
>
> This patch in fact only moves Javascript files from one folder structure
> to a different folder structure. The process we are following is an
> iterative process and will be built upon itself. The end vision that we
> have for the product is as depicted in the previous email an Intent Driven
> Structure and in this structure each folder will contain a Feature that
> will be self contained (Backend + Frontend). The major difference between
> what we have now and what we envision this application to look like is that
> these folders can live at the same level so that we know at a glance what
> this application is capable of doing. This structure will also make it
> easier to onboard someone that is looking to help building any feature
> because they will have access to all of the features in the "same screen"
> instead of having to do a deep dive into the application code in order to
> find the place to start.
>
> As of right now we haven't though very deep on the backend side of things.
> But looking at the current structure moving from a hierarchical file
> structure into a horizontal one, doesn't look like a lot of plumbing.
> Nevertheless this is something that we should address when we get there and
> we believe we are not there yet.
>
> @Akshay Joshi <[email protected]>
>
>> In *menu/can_create.js,* "parentCatalogOfTableChild" function name is
>> unable to understand.
>
> The intent behind this function is to retrieve all the tree child node
> that matches a given type argument and has a direct parent of the catalog
> type. The name was in a hurry and could certainly be improved. Any
> suggestions are welcome. How about **matchesTypeAndHasCatalogParent(self,
> arg)**?
>
> Below code is bit confusing in terms of naming convention
>> canCreate.canCreate inside "canCreate".
>
> That sounds great. Let's do that.
>
> Why we have "menu_utils.js" for Backup, Maintenance, Restore and Grant
>> Wizard? Can we have one single file with all the supported node for
>> respective module?
>
> It was for expressing developer intent. The menu_utils.js that were placed
> in discrete packages have functionality that pertain only to its package.
> If we thought that the functionality were shared, we wouldn't have put them
> in the shared folder `pgadmin4/web/pgadmin/static/js/menu/...`
>
>
>> Also I have seen that the folder hierarchy for grant wizard is
>> "/web/pgadmin/static/js/grant/wizard/menu_utils.js" why there are two
>> separate folders grant and wizard (sub folder of grant)?
>
> It makes sense to think of the wizard as separate entity from grants. You
> could imagine that when we add additional functionality for grants, that
> we'd want it alongside the wizard functionality. You could call it
> premature at this point.
>
> @pgadmin-hackers <[email protected]>
> We understand that our vision for the application is quite bold and if we,
> as a community, decide that this is the way to go it is going to take some
> time to accomplish and will involve effort. But we believe this will take
> the code into a better place.
> Patches 1,2 and 3 are our priority at this point, because there are
> features that we want to implement that depend on the separation from ACI
> Tree, and we would like to see these in master as soon as possible.
>
> Joao and Anthony
>
> On Wed, May 9, 2018 at 8:38 AM Akshay Joshi <[email protected]>
> wrote:
>
>> Hi Joao
>>
>> Below are my review comments:
>>
>> - In *menu/can_create.js,* "parentCatalogOfTableChild" function name
>> is unable to understand.
>> - Below code is bit confusing in terms of naming convention canCreate.
>> canCreate inside "canCreate".
>>
>> canCreate: function(itemData, item, data) {
>>
>> return canCreate.canCreate(pgBrowser, 'coll-collation', item, data);
>>
>> }
>>
>> Can we have something like "*menuCreator.canCreate*"?
>>
>>
>> - Why we have "*menu_utils.js*" for Backup, Maintenance, Restore and
>> Grant Wizard? Can we have one single file with all the supported node for
>> respective module? Also I have seen that the folder hierarchy for grant
>> wizard is "/*web/pgadmin/static/js/grant/wizard/menu_utils.js*" why
>> there are two separate folders grant and wizard (sub folder of grant)?
>>
>>
>>
>> On Wed, May 9, 2018 at 4:32 PM, Khushboo Vashi <
>> [email protected]> wrote:
>>
>>> Hi Joao,
>>>
>>> Patch 0001 & 0002 (esp the new tree implementation) look good to me.
>>>
>>> Patch 0003 - I have some concerns regarding the approach taken to
>>> separate out the PG utilities. The dialogue factory is responsible for
>>> creating the dialogue which uses dialogue wrapper. So, any module should
>>> call the dialogue factory to create a new dialogue instance. Here dialogue
>>> itself call the dialogue factory for creating it.
>>>
>>> In the enable_disable_triggers.js file, can we just import alertifyjs
>>> instead of passing in all the functions?
>>>
>>> Patch 0004 - Right now with the new proposed architecture only JS files
>>> are re-arranged. To maintain the pluggability I would also like to have
>>> some discussion on the back end side. Right now, the code maintains
>>> everything. Let say I want to add any node under schema I will just drop
>>> the related folder and files in that hierarchy and it will work. But with
>>> the proposed architecture I can not visualize the backend architecture and
>>> this should not be ended with only JS restructuring.
>>>
>>> Thanks,
>>> Khushboo
>>>
>>> On Tue, May 8, 2018 at 12:31 AM, Joao De Almeida Pereira <
>>> [email protected]> wrote:
>>>
>>>> Hi Hackers,
>>>> We noticed that some of the latest changes created some conflicts with
>>>> this patch. So you can find attached the patch rebased on the new master.
>>>>
>>>> Thanks
>>>> Victoria && Joao
>>>>
>>>> On Fri, May 4, 2018 at 6:03 PM Joao De Almeida Pereira <
>>>> [email protected]> wrote:
>>>>
>>>>> And now the patches......
>>>>>
>>>>>
>>>>> On Fri, May 4, 2018 at 6:01 PM Joao De Almeida Pereira <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hi Hackers,
>>>>>>
>>>>>> *Be prepared this is going to be a big email.*
>>>>>> Objectives of this patch:
>>>>>>
>>>>>> 1. Start the work to split ACI from pgAdmin code
>>>>>> 2. Add more tests around the front end components
>>>>>> 3. Start the discussion on application architecture
>>>>>>
>>>>>> 1. Start the work to split ACI from pgAdmin code Why
>>>>>>
>>>>>> This journey started on our first attempt to store state of the
>>>>>> current tree, so that when a user accessed again pgAdmin
>>>>>> there would be no need to reopen all the nodes and make the flow
>>>>>> better. Another problem users brought to us was related
>>>>>> to the UI behavior when the number of objects was too big.
>>>>>>
>>>>>> In order to do implement this features we would have to play around
>>>>>> with aciTree or some functions from our code that
>>>>>> called and leveraged the aciTree functionalities. To do this we
>>>>>> needed to have some confidence that our changes would
>>>>>> implement correctly the new feature and it would not break the
>>>>>> current functionality.
>>>>>> The process that we used was first extract the functions that
>>>>>> interacted with the tree, wrap them with
>>>>>> tests and then refactor them in some way. While doing this work we
>>>>>> realized that the tree started spreading roots
>>>>>> throughout the majority of the application.
>>>>>> How
>>>>>>
>>>>>> Options:
>>>>>>
>>>>>> 1. Rewrite the front end
>>>>>> 2. One bang replace ACI Tree without change on the application
>>>>>> 3. Create an abstraction layer between ACI Tree and the
>>>>>> application
>>>>>>
>>>>>> 1. Rewrite the front end
>>>>>> Pros Const
>>>>>> Achieve decoupling by recreating the frontend Cost is too high
>>>>>> Do not rely on deprecated or unmaintained libraries Turn around is
>>>>>> too long
>>>>>> One shot change 2. One bang replace ACI Tree
>>>>>> Pros Const
>>>>>> Achieve decoupling by recreating the frontend Cost is too high
>>>>>> Does not achieve the decoupling unless we change the application
>>>>>> Need to create an adaptor
>>>>>> No garantee of success
>>>>>> One shot change 3. Create an abstraction layer between ACI Tree and
>>>>>> the application
>>>>>> Pros Const
>>>>>> Achieve decoupling of the application and the tree 90% of the time
>>>>>> on Code archaeology
>>>>>> Lower cost
>>>>>> Ability to change or keep ACI Tree, decision TBD
>>>>>> Can be done in a iterative way
>>>>>>
>>>>>> We decided that options 3 looked more attractive specially because we
>>>>>> could do it in a iterative way.
>>>>>>
>>>>>> The next image depicts the current state of the interactions between
>>>>>> the application and the ACI and the place we want
>>>>>> to be in.
>>>>>>
>>>>>> [image: Screen Shot 2018-05-04 at 4.06.17 PM.png]
>>>>>>
>>>>>> for retrieving of data from the ACI Tree that then is used in
>>>>>> different ways.
>>>>>>
>>>>>> Because we are doing this in a iterative process we need to keep the
>>>>>> current tree working and create our adaptor next
>>>>>> to it. In order to do that we created a new property on PGBrowser
>>>>>> class called treeMenu and when ACI Tree receives
>>>>>> the node information we also populate the new Tree with all the
>>>>>> needed information.
>>>>>>
>>>>>> This approach allow us not to worry, for now, with data retrieval,
>>>>>> URL system and other issues that should be addressed
>>>>>> in the future when the adaptor is done.
>>>>>>
>>>>>> This first patch tries to deal with the low hanging fruit, functions
>>>>>> that are triggered by events from ACI Tree.
>>>>>> Cases like registering to events and triggering events need to be
>>>>>> handled in the future as well.
>>>>>> 2. Add more tests around the front end components Why
>>>>>>
>>>>>> Go Fast Forever: https://builttoadapt.io/why-tdd-489fdcdda05e
>>>>>> I think this is a very good summary of why do we need tests in our
>>>>>> applications.
>>>>>> 3. Start the discussion on application architecture
>>>>>>
>>>>>> Why should we care about location of files inside a our application?
>>>>>>
>>>>>> Why is this way better the another?
>>>>>>
>>>>>> These are 2 good questions that have very lengthy answers. Trying to
>>>>>> more or less summarize the answers we care about
>>>>>> the location of the files, because we want our application to
>>>>>> communicate intent and there are always pros and cons
>>>>>> on all the decisions that we make.
>>>>>>
>>>>>> At this point the application structure follows our menu, this
>>>>>> approach eventually make is easier to follow the code
>>>>>> but at the same time if the menu changes down the line, will we
>>>>>> change the structure of our folders?
>>>>>>
>>>>>> The proposal that we do with the last diff of this patch is to change
>>>>>> to a structure that slices vertically the
>>>>>> application. This way we can understand intent behind the code and
>>>>>> more easily find what we are looking for.
>>>>>>
>>>>>> In the current structure if you want to see the tables code you need
>>>>>> to go to
>>>>>> pgAdmin/browser/server_groups/servers/databases/schemas/tables/ this
>>>>>> is a huge path to remember and to get to. What
>>>>>> do we win with this? If we open pgAdmin we know which nodes to click
>>>>>> in order to get to tables. But for development
>>>>>> every time that you are looking for a specific functionality you need
>>>>>> to run the application, navigate the menu so that
>>>>>> you know where you can find the code. This doesn’t sound very
>>>>>> appealing.
>>>>>>
>>>>>> What if our structure would look like this:
>>>>>>
>>>>>> - web
>>>>>> - tables
>>>>>> - controller
>>>>>> - get_nodes.py
>>>>>> - get_sql.py
>>>>>> - __init__.py
>>>>>> - frontend
>>>>>> - component
>>>>>> - ddl_component.js
>>>>>> - services
>>>>>> - table-service.js
>>>>>> - schemas
>>>>>> - servers
>>>>>> - ....
>>>>>>
>>>>>> This would saves us time because all the information that we need is
>>>>>> what are we working on and everything is right there.
>>>>>> Menu driven structure Intent Driven Structure
>>>>>> *Pros:* *Pros:*
>>>>>> Already in place Explicitly shows features
>>>>>> Self contained features Self contained features
>>>>>> Support for drop in features Support for drop in features
>>>>>> *Cons:* *Cons:*
>>>>>> Follows the menu, and it could change Need to change current code
>>>>>> Hard to find features Some additional plumbing might be needed
>>>>>> Drop in features need to be placed in a specific location according
>>>>>> to the menu location
>>>>>>
>>>>>> What are your thought about this architecture?
>>>>>>
>>>>>> Around minute 7 of this video
>>>>>> <https://www.youtube.com/watch?v=hALFGQNeEnU; Uncle Bob shows an
>>>>>> application written
>>>>>> in rails to talk about architecture. It is a long KeyNote if you are
>>>>>> curious I would advise you to see the full video.
>>>>>> His approach to architecture of the application is pretty interesting.
>>>>>> ------------------------------
>>>>>> Patches 0001 Change the order of the shims on the shim file
>>>>>>
>>>>>> Simple change that orders the shims inside the shim file
>>>>>> 0002 New tree implementation
>>>>>>
>>>>>> This is the first version of our Tree implementation. At this point
>>>>>> is a very simple tree without no abstractions and
>>>>>> with code that eventually is not very performant, but this is only
>>>>>> the first iteration and we are trying to follow the
>>>>>> Last Responsible Moment Principle
>>>>>> <https://medium.com/@aidanjcasey/guiding-principles-for-an-evolutionary-software-architecture-b6dc2cb...;
>>>>>> .
>>>>>>
>>>>>> What can you find in this patch:
>>>>>>
>>>>>> - Creation of PGBrowser.treeMenu
>>>>>> - Initial version of the Tree Adaptor
>>>>>> web/pgadmin/static/js/tree/tree.js
>>>>>> - TreeFake test double
>>>>>> <https://martinfowler.com/bliki/TestDouble.html; that can replace
>>>>>> the Tree for testing purposes
>>>>>> - Tests. As an interesting asside because Fake’s need to behave
>>>>>> like the real object you will noticed that there are
>>>>>> tests for this type of double and they the same as of the real
>>>>>> object.
>>>>>>
>>>>>> 0003 Extract, Test and Refactor Methods
>>>>>>
>>>>>> This is the patch that contains the change that we talked a in the
>>>>>> objectives. It touches in a subset of the places
>>>>>> where itemData function is called. To have a sense of progress the
>>>>>> following image depicts, on the left all places
>>>>>> where this functions can be found in the code and on the right the
>>>>>> places where it still remains after this patch.
>>>>>>
>>>>>> But this patch is not only related to the ACI Tree, it also creates
>>>>>> some abstractions from code that we found repeated.
>>>>>> Some examples of this are the dialogs for Backup and Restore, where
>>>>>> the majority of the logic was common, so we created
>>>>>> in web/pgadmin/static/js/alertify/ 3 different objects:
>>>>>> A Factory DialogFactory that is responsible for creating new Dialog
>>>>>> and these dialogs will use the DialogWrapper.
>>>>>>
>>>>>> This is also a good example of the Last Responsible Moment Principle
>>>>>> <https://medium.com/@aidanjcasey/guiding-principles-for-an-evolutionary-software-architecture-b6dc2cb...;
>>>>>> ,
>>>>>> in Dialog there are still some functions that are used in a Recover
>>>>>> and Backup scenario. When we have more examples
>>>>>> we will be able to, if we want, to extract that into another function.
>>>>>>
>>>>>> While doing some code refactoring we found out that the Right Click
>>>>>> Menu as some checks that can be centralized. Examples
>>>>>> of these are checks for Enable/Disable of an option of the display of
>>>>>> the Create/Edit/Drop option in some menus.
>>>>>> Check web/pgadmin/static/js/menu/ for more information. This
>>>>>> simplified code like:
>>>>>>
>>>>>> canCreate: function(itemData, item, data) {
>>>>>> - //If check is false then , we will allow create menu
>>>>>> - if (data && data.check == false)
>>>>>> - return true;
>>>>>> -
>>>>>> - var t = pgBrowser.tree, i = item, d = itemData;
>>>>>> - // To iterate over tree to check parent node
>>>>>> - while (i) {
>>>>>> - // If it is schema then allow user to create domain
>>>>>> - if (_.indexOf(['schema'], d._type) > -1)
>>>>>> - return true;
>>>>>> -
>>>>>> - if ('coll-domain' == d._type) {
>>>>>> - //Check if we are not child of catalog
>>>>>> - var prev_i = t.hasParent(i) ? t.parent(i) : null,
>>>>>> - prev_d = prev_i ? t.itemData(prev_i) : null;
>>>>>> - if( prev_d._type == 'catalog') {
>>>>>> - return false;
>>>>>> - } else {
>>>>>> - return true;
>>>>>> - }
>>>>>> - }
>>>>>> - i = t.hasParent(i) ? t.parent(i) : null;
>>>>>> - d = i ? t.itemData(i) : null;
>>>>>> - }
>>>>>> - // by default we do not want to allow create menu
>>>>>> - return true;
>>>>>> + return canCreate.canCreate(pgBrowser, 'coll-domain', item, data);
>>>>>> }
>>>>>>
>>>>>> This refactor was made in a couple of places throughout the code.
>>>>>>
>>>>>> Another refactor that had to be done was the creation of the
>>>>>> functions getTreeNodeHierarchyFromElement and
>>>>>> getTreeNodeHierarchyFromIdentifier that replace the old
>>>>>> getTreeNodeHierarchy. This implementation was done
>>>>>> because for the time being we have 2 different types of trees and
>>>>>> calls to them.
>>>>>>
>>>>>> The rest of the changes resulted from our process of refactoring that
>>>>>> we are following:
>>>>>>
>>>>>> 1. Find a location where itemData is used
>>>>>> 2. Extract that into a function outside of the current code
>>>>>> 3. Wrap it with tests
>>>>>> 4. When we have enough confidence on the tests, we refactor the
>>>>>> function to use the new Tree
>>>>>> 5. We refactor the function to become more readable
>>>>>> 6. Start over
>>>>>>
>>>>>> 0004 Architecture
>>>>>>
>>>>>> The proposed structure is our vision for the application, this is not
>>>>>> the state we will be in when this patch lands.
>>>>>> For now the only change we are doing is move the javascript into
>>>>>> static/js/FEATURE_NAME. This is a first step in the
>>>>>> direction that we would like to see the product heading.
>>>>>>
>>>>>> Even if we decide to keep using the same architecture the move of
>>>>>> Javascript to a centralized place will
>>>>>> not break any plugability as webpack can retrieve all the Javascript
>>>>>> from any place in the code.
>>>>>>
>>>>>> Have a nice weekend
>>>>>> Joao
>>>>>>
>>>>>>
>>>>>>
>>>
>>
>>
>> --
>> *Akshay Joshi*
>>
>> *Sr. Software Architect *
>>
>>
>>
>> *Phone: +91 20-3058-9517Mobile: +91 976-788-8246*
>>
>
Attachments:
[image/png] Screen Shot 2018-05-04 at 4.06.17 PM.png (104.4K, 3-Screen%20Shot%202018-05-04%20at%204.06.17%20PM.png)
download | view image
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-10 14:38 Anthony Emengo <[email protected]>
parent: Ashesh Vashi <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Anthony Emengo @ 2018-05-10 14:38 UTC (permalink / raw)
To: Ashesh Vashi <[email protected]>; +Cc: Joao De Almeida Pereira <[email protected]>; Akshay Joshi <[email protected]>; Dave Page <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
>
> 1. In TreeNode, we're keepging the reference of DOMElement, do we really
> need it?
As of right now, our Tree abstraction acts as an adapter to the aciTree
library. The aciTree library needs the domElement for most of its functions
(setInode, unload, etc). Thus this is the easiest way to introduce our
abstraction and keep the functionality as before - at least until we decide
that whether we want to switch out the library or not.
2. Are you expecting the tree class to be a singleton class
Since this tree is referenced throughout the codebase, considering it to be
a singleton seems like the most appropriate pattern for this usecase. It is
very much the same way how we create a single instance of the aciTree
library and use that throughout the codebase. Moreover, it opens up
opportunities to improve performance, for example caching lockups of nodes.
I’m not a fan of singletons myself, but I feel like we’re simply keeping
the architecture the same in the instance.
Sincerely,
Anthony and Victoria
Attachments:
[image/png] Screen Shot 2018-05-04 at 4.06.17 PM.png (104.4K, 3-Screen%20Shot%202018-05-04%20at%204.06.17%20PM.png)
download | view image
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-11 06:31 Ashesh Vashi <[email protected]>
parent: Anthony Emengo <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Ashesh Vashi @ 2018-05-11 06:31 UTC (permalink / raw)
To: Anthony Emengo <[email protected]>; +Cc: Joao De Almeida Pereira <[email protected]>; Akshay Joshi <[email protected]>; Dave Page <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
On Thu, May 10, 2018 at 8:08 PM, Anthony Emengo <[email protected]> wrote:
> 1. In TreeNode, we're keepging the reference of DOMElement, do we really
>> need it?
>
> As of right now, our Tree abstraction acts as an adapter to the aciTree
> library. The aciTree library needs the domElement for most of its functions
> (setInode, unload, etc). Thus this is the easiest way to introduce our
> abstraction and keep the functionality as before - at least until we decide
> that whether we want to switch out the library or not.
>
I understand that. But - I've not seen any reference of domElement the code
yet, hence - pointed that out.
> 2. Are you expecting the tree class to be a singleton class
>
> Since this tree is referenced throughout the codebase, considering it to
> be a singleton seems like the most appropriate pattern for this usecase. It
> is very much the same way how we create a single instance of the aciTree
> library and use that throughout the codebase. Moreover, it opens up
> opportunities to improve performance, for example caching lockups of nodes.
> I’m not a fan of singletons myself, but I feel like we’re simply keeping
> the architecture the same in the instance.
>
Yeah - I don't see any usage of tree object from anywhere.
And, we're already creating new object in browser.js (and, not utitlizing
that instance anywhere.)
-- Thanks, Ashesh
>
>
> Sincerely,
>
> Anthony and Victoria
>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-11 21:27 Joao De Almeida Pereira <[email protected]>
parent: Ashesh Vashi <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Joao De Almeida Pereira @ 2018-05-11 21:27 UTC (permalink / raw)
To: Ashesh Vashi <[email protected]>; +Cc: Anthony Emengo <[email protected]>; Akshay Joshi <[email protected]>; Dave Page <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
Hello Ashesh,
1. In TreeNode, we're keepging the reference of DOMElement, do we really
>> need it?
>
> As of right now, our Tree abstraction acts as an adapter to the aciTree
>> library. The aciTree library needs the domElement for most of its functions
>> (setInode, unload, etc). Thus this is the easiest way to introduce our
>> abstraction and keep the functionality as before - at least until we decide
>> that whether we want to switch out the library or not.
>
> I understand that. But - I've not seen any reference of domElement the
> code yet, hence - pointed that out.
If you look at the function: reload, unload you will see that domNode is
used to communicate with the ACITree
> 2. Are you expecting the tree class to be a singleton class
>
> Since this tree is referenced throughout the codebase, considering it to
>> be a singleton seems like the most appropriate pattern for this usecase. It
>> is very much the same way how we create a single instance of the aciTree
>> library and use that throughout the codebase. Moreover, it opens up
>> opportunities to improve performance, for example caching lockups of nodes.
>> I’m not a fan of singletons myself, but I feel like we’re simply keeping
>> the architecture the same in the instance.
>
> Yeah - I don't see any usage of tree object from anywhere.
> And, we're already creating new object in browser.js (and, not utitlizing
> that instance anywhere.)
You are right, we do not need to export tree as a singleton for now. The
line that exports the variable tree can be remove when applying the patch
number 2.
I think we addressed all the concern raised about this patch. Does this
mean that the patch is going to get committed?
We are looking forward to continue the development on this track of work,
but as you know, we are blocked until it gets committed into master. We
hope that this gets unblocked soon because this is a very big track of work
and the longer it is blocked, the longer it will take to get finalized.
Thanks
Joao
On Fri, May 11, 2018 at 2:32 AM Ashesh Vashi <[email protected]>
wrote:
> On Thu, May 10, 2018 at 8:08 PM, Anthony Emengo <[email protected]>
> wrote:
>
>> 1. In TreeNode, we're keepging the reference of DOMElement, do we really
>>> need it?
>>
>> As of right now, our Tree abstraction acts as an adapter to the aciTree
>> library. The aciTree library needs the domElement for most of its functions
>> (setInode, unload, etc). Thus this is the easiest way to introduce our
>> abstraction and keep the functionality as before - at least until we decide
>> that whether we want to switch out the library or not.
>>
> I understand that. But - I've not seen any reference of domElement the
> code yet, hence - pointed that out.
>
>> 2. Are you expecting the tree class to be a singleton class
>>
>> Since this tree is referenced throughout the codebase, considering it to
>> be a singleton seems like the most appropriate pattern for this usecase. It
>> is very much the same way how we create a single instance of the aciTree
>> library and use that throughout the codebase. Moreover, it opens up
>> opportunities to improve performance, for example caching lockups of nodes.
>> I’m not a fan of singletons myself, but I feel like we’re simply keeping
>> the architecture the same in the instance.
>>
> Yeah - I don't see any usage of tree object from anywhere.
> And, we're already creating new object in browser.js (and, not utitlizing
> that instance anywhere.)
>
> -- Thanks, Ashesh
>
>>
>>
>> Sincerely,
>>
>> Anthony and Victoria
>>
>
>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-11 23:10 Ashesh Vashi <[email protected]>
parent: Joao De Almeida Pereira <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Ashesh Vashi @ 2018-05-11 23:10 UTC (permalink / raw)
To: Joao Pedro De Almeida Pereira <[email protected]>; +Cc: Anthony Emengo <[email protected]>; Akshay Joshi (EDB) <[email protected]>; Dave Page <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
On Sat, May 12, 2018, 02:58 Joao De Almeida Pereira <
[email protected]> wrote:
> Hello Ashesh,
>
> 1. In TreeNode, we're keepging the reference of DOMElement, do we really
>>> need it?
>>
>> As of right now, our Tree abstraction acts as an adapter to the aciTree
>>> library. The aciTree library needs the domElement for most of its functions
>>> (setInode, unload, etc). Thus this is the easiest way to introduce our
>>> abstraction and keep the functionality as before - at least until we decide
>>> that whether we want to switch out the library or not.
>>
>> I understand that. But - I've not seen any reference of domElement the
>> code yet, hence - pointed that out.
>
> If you look at the function: reload, unload you will see that domNode is
> used to communicate with the ACITree
>
>
>> 2. Are you expecting the tree class to be a singleton class
>>
>> Since this tree is referenced throughout the codebase, considering it to
>>> be a singleton seems like the most appropriate pattern for this usecase. It
>>> is very much the same way how we create a single instance of the aciTree
>>> library and use that throughout the codebase. Moreover, it opens up
>>> opportunities to improve performance, for example caching lockups of nodes.
>>> I’m not a fan of singletons myself, but I feel like we’re simply keeping
>>> the architecture the same in the instance.
>>
>> Yeah - I don't see any usage of tree object from anywhere.
>> And, we're already creating new object in browser.js (and, not utitlizing
>> that instance anywhere.)
>
>
> You are right, we do not need to export tree as a singleton for now. The
> line that exports the variable tree can be remove when applying the patch
> number 2.
>
>
> I think we addressed all the concern raised about this patch. Does this
> mean that the patch is going to get committed?
>
Yes - from me for 0002.
-- Thanks, Ashesh
> We are looking forward to continue the development on this track of work,
> but as you know, we are blocked until it gets committed into master. We
> hope that this gets unblocked soon because this is a very big track of work
> and the longer it is blocked, the longer it will take to get finalized.
>
> Thanks
> Joao
>
>
> On Fri, May 11, 2018 at 2:32 AM Ashesh Vashi <
> [email protected]> wrote:
>
>> On Thu, May 10, 2018 at 8:08 PM, Anthony Emengo <[email protected]>
>> wrote:
>>
>>> 1. In TreeNode, we're keepging the reference of DOMElement, do we really
>>>> need it?
>>>
>>> As of right now, our Tree abstraction acts as an adapter to the aciTree
>>> library. The aciTree library needs the domElement for most of its functions
>>> (setInode, unload, etc). Thus this is the easiest way to introduce our
>>> abstraction and keep the functionality as before - at least until we decide
>>> that whether we want to switch out the library or not.
>>>
>> I understand that. But - I've not seen any reference of domElement the
>> code yet, hence - pointed that out.
>>
>>> 2. Are you expecting the tree class to be a singleton class
>>>
>>> Since this tree is referenced throughout the codebase, considering it to
>>> be a singleton seems like the most appropriate pattern for this usecase. It
>>> is very much the same way how we create a single instance of the aciTree
>>> library and use that throughout the codebase. Moreover, it opens up
>>> opportunities to improve performance, for example caching lockups of nodes.
>>> I’m not a fan of singletons myself, but I feel like we’re simply keeping
>>> the architecture the same in the instance.
>>>
>> Yeah - I don't see any usage of tree object from anywhere.
>> And, we're already creating new object in browser.js (and, not utitlizing
>> that instance anywhere.)
>>
>> -- Thanks, Ashesh
>>
>>>
>>>
>>> Sincerely,
>>>
>>> Anthony and Victoria
>>>
>>
>>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-14 09:29 Dave Page <[email protected]>
parent: Ashesh Vashi <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Dave Page @ 2018-05-14 09:29 UTC (permalink / raw)
To: Ashesh Vashi <[email protected]>; +Cc: Joao Pedro De Almeida Pereira <[email protected]>; Anthony Emengo <[email protected]>; Akshay Joshi (EDB) <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
On Sat, May 12, 2018 at 12:10 AM, Ashesh Vashi <
[email protected]> wrote:
> On Sat, May 12, 2018, 02:58 Joao De Almeida Pereira <
> [email protected]> wrote:
>
>> Hello Ashesh,
>>
>> 1. In TreeNode, we're keepging the reference of DOMElement, do we really
>>>> need it?
>>>
>>> As of right now, our Tree abstraction acts as an adapter to the aciTree
>>>> library. The aciTree library needs the domElement for most of its functions
>>>> (setInode, unload, etc). Thus this is the easiest way to introduce our
>>>> abstraction and keep the functionality as before - at least until we decide
>>>> that whether we want to switch out the library or not.
>>>
>>> I understand that. But - I've not seen any reference of domElement the
>>> code yet, hence - pointed that out.
>>
>> If you look at the function: reload, unload you will see that domNode is
>> used to communicate with the ACITree
>>
>>
>>> 2. Are you expecting the tree class to be a singleton class
>>>
>>> Since this tree is referenced throughout the codebase, considering it to
>>>> be a singleton seems like the most appropriate pattern for this usecase. It
>>>> is very much the same way how we create a single instance of the aciTree
>>>> library and use that throughout the codebase. Moreover, it opens up
>>>> opportunities to improve performance, for example caching lockups of nodes.
>>>> I’m not a fan of singletons myself, but I feel like we’re simply keeping
>>>> the architecture the same in the instance.
>>>
>>> Yeah - I don't see any usage of tree object from anywhere.
>>> And, we're already creating new object in browser.js (and, not
>>> utitlizing that instance anywhere.)
>>
>>
>> You are right, we do not need to export tree as a singleton for now. The
>> line that exports the variable tree can be remove when applying the
>> patch number 2.
>>
>>
>> I think we addressed all the concern raised about this patch. Does this
>> mean that the patch is going to get committed?
>>
> Yes - from me for 0002.
>
Can you do that today?
--
Dave Page
Blog: http://pgsnake.blogspot.com
Twitter: @pgsnake
EnterpriseDB UK: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-14 12:38 Ashesh Vashi <[email protected]>
parent: Dave Page <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Ashesh Vashi @ 2018-05-14 12:38 UTC (permalink / raw)
To: Dave Page <[email protected]>; +Cc: Joao Pedro De Almeida Pereira <[email protected]>; Anthony Emengo <[email protected]>; Akshay Joshi (EDB) <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
On Mon, May 14, 2018 at 2:59 PM, Dave Page <[email protected]> wrote:
>
>
> On Sat, May 12, 2018 at 12:10 AM, Ashesh Vashi <
> [email protected]> wrote:
>
>> On Sat, May 12, 2018, 02:58 Joao De Almeida Pereira <
>> [email protected]> wrote:
>>
>>> Hello Ashesh,
>>>
>>> 1. In TreeNode, we're keepging the reference of DOMElement, do we really
>>>>> need it?
>>>>
>>>> As of right now, our Tree abstraction acts as an adapter to the
>>>>> aciTree library. The aciTree library needs the domElement for most of its
>>>>> functions (setInode, unload, etc). Thus this is the easiest way to
>>>>> introduce our abstraction and keep the functionality as before - at least
>>>>> until we decide that whether we want to switch out the library or not.
>>>>
>>>> I understand that. But - I've not seen any reference of domElement the
>>>> code yet, hence - pointed that out.
>>>
>>> If you look at the function: reload, unload you will see that domNode
>>> is used to communicate with the ACITree
>>>
>>>
>>>> 2. Are you expecting the tree class to be a singleton class
>>>>
>>>> Since this tree is referenced throughout the codebase, considering it
>>>>> to be a singleton seems like the most appropriate pattern for this usecase.
>>>>> It is very much the same way how we create a single instance of the aciTree
>>>>> library and use that throughout the codebase. Moreover, it opens up
>>>>> opportunities to improve performance, for example caching lockups of nodes.
>>>>> I’m not a fan of singletons myself, but I feel like we’re simply keeping
>>>>> the architecture the same in the instance.
>>>>
>>>> Yeah - I don't see any usage of tree object from anywhere.
>>>> And, we're already creating new object in browser.js (and, not
>>>> utitlizing that instance anywhere.)
>>>
>>>
>>> You are right, we do not need to export tree as a singleton for now. The
>>> line that exports the variable tree can be remove when applying the
>>> patch number 2.
>>>
>>>
>>> I think we addressed all the concern raised about this patch. Does this
>>> mean that the patch is going to get committed?
>>>
>> Yes - from me for 0002.
>>
>
> Can you do that today?
>
Done.
-- Thanks, Ashesh
>
> --
> Dave Page
> Blog: http://pgsnake.blogspot.com
> Twitter: @pgsnake
>
> EnterpriseDB UK: http://www.enterprisedb.com
> The Enterprise PostgreSQL Company
>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-14 12:40 Dave Page <[email protected]>
parent: Ashesh Vashi <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Dave Page @ 2018-05-14 12:40 UTC (permalink / raw)
To: Ashesh Vashi <[email protected]>; +Cc: Joao Pedro De Almeida Pereira <[email protected]>; Anthony Emengo <[email protected]>; Akshay Joshi (EDB) <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
On Mon, May 14, 2018 at 1:38 PM, Ashesh Vashi <[email protected]
> wrote:
> On Mon, May 14, 2018 at 2:59 PM, Dave Page <[email protected]> wrote:
>
>>
>>
>> On Sat, May 12, 2018 at 12:10 AM, Ashesh Vashi <
>> [email protected]> wrote:
>>
>>> On Sat, May 12, 2018, 02:58 Joao De Almeida Pereira <
>>> [email protected]> wrote:
>>>
>>>> Hello Ashesh,
>>>>
>>>> 1. In TreeNode, we're keepging the reference of DOMElement, do we
>>>>>> really need it?
>>>>>
>>>>> As of right now, our Tree abstraction acts as an adapter to the
>>>>>> aciTree library. The aciTree library needs the domElement for most of its
>>>>>> functions (setInode, unload, etc). Thus this is the easiest way to
>>>>>> introduce our abstraction and keep the functionality as before - at least
>>>>>> until we decide that whether we want to switch out the library or not.
>>>>>
>>>>> I understand that. But - I've not seen any reference of domElement the
>>>>> code yet, hence - pointed that out.
>>>>
>>>> If you look at the function: reload, unload you will see that domNode
>>>> is used to communicate with the ACITree
>>>>
>>>>
>>>>> 2. Are you expecting the tree class to be a singleton class
>>>>>
>>>>> Since this tree is referenced throughout the codebase, considering it
>>>>>> to be a singleton seems like the most appropriate pattern for this usecase.
>>>>>> It is very much the same way how we create a single instance of the aciTree
>>>>>> library and use that throughout the codebase. Moreover, it opens up
>>>>>> opportunities to improve performance, for example caching lockups of nodes.
>>>>>> I’m not a fan of singletons myself, but I feel like we’re simply keeping
>>>>>> the architecture the same in the instance.
>>>>>
>>>>> Yeah - I don't see any usage of tree object from anywhere.
>>>>> And, we're already creating new object in browser.js (and, not
>>>>> utitlizing that instance anywhere.)
>>>>
>>>>
>>>> You are right, we do not need to export tree as a singleton for now.
>>>> The line that exports the variable tree can be remove when applying
>>>> the patch number 2.
>>>>
>>>>
>>>> I think we addressed all the concern raised about this patch. Does this
>>>> mean that the patch is going to get committed?
>>>>
>>> Yes - from me for 0002.
>>>
>>
>> Can you do that today?
>>
> Done.
>
Great, thanks!
On to patch 0003 then :-)
--
Dave Page
Blog: http://pgsnake.blogspot.com
Twitter: @pgsnake
EnterpriseDB UK: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-14 12:41 Ashesh Vashi <[email protected]>
parent: Dave Page <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Ashesh Vashi @ 2018-05-14 12:41 UTC (permalink / raw)
To: Dave Page <[email protected]>; +Cc: Joao Pedro De Almeida Pereira <[email protected]>; Anthony Emengo <[email protected]>; Akshay Joshi (EDB) <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
On Mon, May 14, 2018 at 6:10 PM, Dave Page <[email protected]> wrote:
>
>
> On Mon, May 14, 2018 at 1:38 PM, Ashesh Vashi <
> [email protected]> wrote:
>
>> On Mon, May 14, 2018 at 2:59 PM, Dave Page <[email protected]> wrote:
>>
>>>
>>>
>>> On Sat, May 12, 2018 at 12:10 AM, Ashesh Vashi <
>>> [email protected]> wrote:
>>>
>>>> On Sat, May 12, 2018, 02:58 Joao De Almeida Pereira <
>>>> [email protected]> wrote:
>>>>
>>>>> Hello Ashesh,
>>>>>
>>>>> 1. In TreeNode, we're keepging the reference of DOMElement, do we
>>>>>>> really need it?
>>>>>>
>>>>>> As of right now, our Tree abstraction acts as an adapter to the
>>>>>>> aciTree library. The aciTree library needs the domElement for most of its
>>>>>>> functions (setInode, unload, etc). Thus this is the easiest way to
>>>>>>> introduce our abstraction and keep the functionality as before - at least
>>>>>>> until we decide that whether we want to switch out the library or not.
>>>>>>
>>>>>> I understand that. But - I've not seen any reference of domElement
>>>>>> the code yet, hence - pointed that out.
>>>>>
>>>>> If you look at the function: reload, unload you will see that domNode
>>>>> is used to communicate with the ACITree
>>>>>
>>>>>
>>>>>> 2. Are you expecting the tree class to be a singleton class
>>>>>>
>>>>>> Since this tree is referenced throughout the codebase, considering it
>>>>>>> to be a singleton seems like the most appropriate pattern for this usecase.
>>>>>>> It is very much the same way how we create a single instance of the aciTree
>>>>>>> library and use that throughout the codebase. Moreover, it opens up
>>>>>>> opportunities to improve performance, for example caching lockups of nodes.
>>>>>>> I’m not a fan of singletons myself, but I feel like we’re simply keeping
>>>>>>> the architecture the same in the instance.
>>>>>>
>>>>>> Yeah - I don't see any usage of tree object from anywhere.
>>>>>> And, we're already creating new object in browser.js (and, not
>>>>>> utitlizing that instance anywhere.)
>>>>>
>>>>>
>>>>> You are right, we do not need to export tree as a singleton for now.
>>>>> The line that exports the variable tree can be remove when applying
>>>>> the patch number 2.
>>>>>
>>>>>
>>>>> I think we addressed all the concern raised about this patch. Does
>>>>> this mean that the patch is going to get committed?
>>>>>
>>>> Yes - from me for 0002.
>>>>
>>>
>>> Can you do that today?
>>>
>> Done.
>>
>
> Great, thanks!
>
> On to patch 0003 then :-)
>
Yes - already working on it! :-)
-- Thanks, Ashesh
>
> --
> Dave Page
> Blog: http://pgsnake.blogspot.com
> Twitter: @pgsnake
>
> EnterpriseDB UK: http://www.enterprisedb.com
> The Enterprise PostgreSQL Company
>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-15 10:37 Ashesh Vashi <[email protected]>
parent: Ashesh Vashi <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Ashesh Vashi @ 2018-05-15 10:37 UTC (permalink / raw)
To: Joao Pedro De Almeida Pereira <[email protected]>; +Cc: Anthony Emengo <[email protected]>; Dave Page <[email protected]>; Akshay Joshi (EDB) <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
Hi Joao,
On Mon, May 14, 2018 at 6:11 PM, Ashesh Vashi <[email protected]
> wrote:
>
> On Mon, May 14, 2018 at 6:10 PM, Dave Page <[email protected]> wrote:
>
>>
>>
>> On Mon, May 14, 2018 at 1:38 PM, Ashesh Vashi <
>> [email protected]> wrote:
>>
>>> On Mon, May 14, 2018 at 2:59 PM, Dave Page <[email protected]> wrote:
>>>
>>>>
>>>>
>>>> On Sat, May 12, 2018 at 12:10 AM, Ashesh Vashi <
>>>> [email protected]> wrote:
>>>>
>>>>> On Sat, May 12, 2018, 02:58 Joao De Almeida Pereira <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hello Ashesh,
>>>>>>
>>>>>> 1. In TreeNode, we're keepging the reference of DOMElement, do we
>>>>>>>> really need it?
>>>>>>>
>>>>>>> As of right now, our Tree abstraction acts as an adapter to the
>>>>>>>> aciTree library. The aciTree library needs the domElement for most of its
>>>>>>>> functions (setInode, unload, etc). Thus this is the easiest way to
>>>>>>>> introduce our abstraction and keep the functionality as before - at least
>>>>>>>> until we decide that whether we want to switch out the library or not.
>>>>>>>
>>>>>>> I understand that. But - I've not seen any reference of domElement
>>>>>>> the code yet, hence - pointed that out.
>>>>>>
>>>>>> If you look at the function: reload, unload you will see that domNode
>>>>>> is used to communicate with the ACITree
>>>>>>
>>>>>>
>>>>>>> 2. Are you expecting the tree class to be a singleton class
>>>>>>>
>>>>>>> Since this tree is referenced throughout the codebase, considering
>>>>>>>> it to be a singleton seems like the most appropriate pattern for this
>>>>>>>> usecase. It is very much the same way how we create a single instance of
>>>>>>>> the aciTree library and use that throughout the codebase. Moreover, it
>>>>>>>> opens up opportunities to improve performance, for example caching lockups
>>>>>>>> of nodes. I’m not a fan of singletons myself, but I feel like we’re simply
>>>>>>>> keeping the architecture the same in the instance.
>>>>>>>
>>>>>>> Yeah - I don't see any usage of tree object from anywhere.
>>>>>>> And, we're already creating new object in browser.js (and, not
>>>>>>> utitlizing that instance anywhere.)
>>>>>>
>>>>>>
>>>>>> You are right, we do not need to export tree as a singleton for now.
>>>>>> The line that exports the variable tree can be remove when applying
>>>>>> the patch number 2.
>>>>>>
>>>>>>
>>>>>> I think we addressed all the concern raised about this patch. Does
>>>>>> this mean that the patch is going to get committed?
>>>>>>
>>>>> Yes - from me for 0002.
>>>>>
>>>>
>>>> Can you do that today?
>>>>
>>> Done.
>>>
>>
>> Great, thanks!
>>
>> On to patch 0003 then :-)
>>
> Yes - already working on it! :-)
>
Majority part of the 0003 patch looks good to me.
Except choice of the path of some of the file, and name of the functions.
Please find the updated patch.
I've moved files under the 'pgadmin/static/js/menu' directory under the
'pgadmin/static/js/tree', as they're using tree functionalities directly.
Please review it, and let me know your concern.
-- Thanks, Ashesh
>
> -- Thanks, Ashesh
>
>>
>> --
>> Dave Page
>> Blog: http://pgsnake.blogspot.com
>> Twitter: @pgsnake
>>
>> EnterpriseDB UK: http://www.enterprisedb.com
>> The Enterprise PostgreSQL Company
>>
>
>
Attachments:
[application/octet-stream] 0001-Extract-test-and-refactor-methods.patch (262.9K, 3-0001-Extract-test-and-refactor-methods.patch)
download | inline diff:
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.js
index 9015d8d2..aefdbe41 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.js
@@ -1,8 +1,9 @@
define('pgadmin.node.collation', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
+ 'sources/tree/node_menu',
'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser) {
+], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, treeNodeMenu) {
if (!pgBrowser.Nodes['coll-collation']) {
pgAdmin.Browser.Nodes['coll-collation'] =
@@ -222,34 +223,7 @@ define('pgadmin.node.collation', [
return true;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create collation
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-collation' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
+ canCreate: treeNodeMenu.canCreate(pgBrowser, 'coll-collation'),
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.js
index 403ca471..ec6adb6c 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.js
@@ -2,9 +2,11 @@
define('pgadmin.node.domain', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid',
+ 'sources/tree/node_menu',
'pgadmin.browser.collection',
], function(
- gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid
+ gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid,
+ treeNodeMenu
) {
// Define Domain Collection Node
@@ -296,34 +298,7 @@ define('pgadmin.node.domain', [
return errmsg;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create domain
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-domain' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
+ canCreate: treeNodeMenu.canCreate(pgBrowser, 'coll-domain'),
isDisabled: function(m){
if (!m.isNew()) {
var server = this.node_info.server;
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table.js
index 160db83f..4a8876cf 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table.js
@@ -2,9 +2,11 @@
define('pgadmin.node.foreign_table', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid',
+ 'sources/tree/node_menu',
'pgadmin.browser.collection',
], function(
- gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid
+ gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid,
+ treeNodeMenu
) {
if (!pgBrowser.Nodes['coll-foreign_table']) {
@@ -659,34 +661,7 @@ define('pgadmin.node.foreign_table', [
return errmsg;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create foreign table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-foreign_table' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
+ canCreate: treeNodeMenu.canCreate(pgBrowser, 'coll-foreign_table'),
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.js
index 89806681..06edc705 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.js
@@ -1,9 +1,11 @@
define('pgadmin.node.fts_configuration', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid',
+ 'sources/tree/node_menu',
'pgadmin.browser.collection',
], function(
- gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid
+ gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid,
+ treeNodeMenu
) {
// Model for tokens control
@@ -577,34 +579,7 @@ define('pgadmin.node.fts_configuration', [
return null;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create fts configuration
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-fts_configuration' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
+ canCreate: treeNodeMenu.canCreate(pgBrowser, 'coll-fts_configuration'),
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary.js
index ed83feb1..2879b2be 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary.js
@@ -1,8 +1,10 @@
define('pgadmin.node.fts_dictionary', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
+ 'sources/tree/node_menu',
'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform) {
+], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform,
+ treeNodeMenu) {
// Extend the browser's node model class to create a option/value pair
var OptionLabelModel = pgAdmin.Browser.Node.Model.extend({
@@ -186,34 +188,7 @@ define('pgadmin.node.fts_dictionary', [
return null;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create fts dictionary
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-fts_dictionary' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
+ canCreate: treeNodeMenu.canCreate(pgBrowser, 'coll-fts_dictionary'),
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser.js
index 92c0786e..abacdbc7 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser.js
@@ -1,7 +1,9 @@
define('pgadmin.node.fts_parser', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
- 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser) {
+ 'sources/pgadmin', 'pgadmin.browser',
+ 'sources/tree/node_menu',
+ 'pgadmin.browser.collection',
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, treeNodeMenu) {
// Extend the collection class for fts parser
if (!pgBrowser.Nodes['coll-fts_parser']) {
@@ -199,34 +201,7 @@ define('pgadmin.node.fts_parser', [
return null;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create fts parser
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-fts_parser' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
+ canCreate: treeNodeMenu.canCreate(pgBrowser, 'coll-fts_parser'),
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template.js
index 606a57a6..66d51c98 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template.js
@@ -1,7 +1,9 @@
define('pgadmin.node.fts_template', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
- 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser) {
+ 'sources/pgadmin', 'pgadmin.browser',
+ 'sources/tree/node_menu',
+ 'pgadmin.browser.collection',
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, treeNodeMenu) {
// Extend the collection class for fts template
if (!pgBrowser.Nodes['coll-fts_template']) {
@@ -139,34 +141,7 @@ define('pgadmin.node.fts_template', [
return null;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create fts fts_template
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-fts_template' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
+ canCreate: treeNodeMenu.canCreate(pgBrowser, 'coll-fts_template'),
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function.js
index 6e405165..af0a1f9f 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function.js
@@ -2,8 +2,10 @@
define('pgadmin.node.function', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
+ 'sources/tree/node_menu',
'pgadmin.browser.collection', 'pgadmin.browser.server.privilege',
-], function(gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform) {
+], function(gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform,
+ treeNodeMenu) {
if (!pgBrowser.Nodes['coll-function']) {
pgBrowser.Nodes['coll-function'] =
@@ -438,34 +440,7 @@ define('pgadmin.node.function', [
return true;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create Function
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-function' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
+ canCreate: treeNodeMenu.canCreate(pgBrowser, 'coll-function'),
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js
index aeb8271b..ca552a1c 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js
@@ -2,8 +2,10 @@
define('pgadmin.node.trigger_function', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
+ 'sources/tree/node_menu',
'pgadmin.browser.collection', 'pgadmin.browser.server.privilege',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform) {
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform,
+ treeNodeMenu) {
if (!pgBrowser.Nodes['coll-trigger_function']) {
pgBrowser.Nodes['coll-trigger_function'] =
@@ -357,34 +359,7 @@ define('pgadmin.node.trigger_function', [
return !(this.node_info && 'catalog' in this.node_info);
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create Function
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-trigger_function' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
+ canCreate: treeNodeMenu.canCreate(pgBrowser, 'coll-trigger_function'),
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence.js
index 57c95acd..44d5f6b3 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence.js
@@ -1,8 +1,10 @@
define('pgadmin.node.sequence', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
+ 'sources/tree/node_menu',
'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform) {
+], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform,
+ treeNodeMenu) {
// Extend the browser's collection class for sequence collection
if (!pgBrowser.Nodes['coll-sequence']) {
@@ -60,34 +62,7 @@ define('pgadmin.node.sequence', [
},
canDrop: pgBrowser.Nodes['schema'].canChildDrop,
canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create collation
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-sequence' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we want to allow create menu
- return true;
- },
+ canCreate: treeNodeMenu.canCreate(pgBrowser, 'coll-sequence'),
// Define the model for sequence node.
model: pgBrowser.Node.Model.extend({
defaults: {
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child.js
new file mode 100644
index 00000000..84e25b5c
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child.js
@@ -0,0 +1,18 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export function canDropChild(pgBrowser, itemData, item) {
+ let node = pgBrowser.treeMenu.findNodeByDomElement(item);
+
+ if (node.anyParent((parent) => parent.getData()._type === 'catalog')) {
+ return false;
+ }
+
+ return true;
+}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
index a7fd4c7c..a09610ad 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
@@ -1,8 +1,10 @@
define('pgadmin.node.schema', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid',
+ 'pgadmin.node.schema.dir/can_drop_child',
'pgadmin.browser.collection', 'pgadmin.browser.server.privilege',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, Backgrid) {
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, Backgrid,
+canDropChild) {
// VacuumSettings Collection to display all settings parameters as Grid
Backform.VacuumCollectionControl =
@@ -428,51 +430,12 @@ define('pgadmin.node.schema', [
// This function will checks whether we can allow user to
// drop object or not based on location within schema & catalog
canChildDrop: function(itemData, item) {
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create collation
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if(prev_d && prev_d._type == 'catalog') {
- return false;
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
+ return canDropChild.canDropChild(pgBrowser, itemData, item);
},
});
pgBrowser.tableChildTreeNodeHierarchy = function(i) {
- var idx = 0,
- res = {},
- t = pgBrowser.tree;
-
- do {
- var d = t.itemData(i);
- if (
- d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId
- ) {
- if (d._type === 'partition' || d._type === 'table') {
- if (!('table' in res)) {
- res['table'] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- } else {
- res[d._type] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- } while (i);
-
- return res;
+ return this.getTreeNodeHierarchy(i);
};
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js
index 72b6eb0c..1fdacf52 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js
@@ -88,7 +88,6 @@ define('pgadmin.node.column', [
if (!pgBrowser.Nodes['column']) {
pgBrowser.Nodes['column'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table', 'view', 'mview'],
collection_type: ['coll-table', 'coll-view', 'coll-mview'],
type: 'column',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js
index 857cf4c4..ab28a86b 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js
@@ -8,7 +8,6 @@ define('pgadmin.node.check_constraint', [
// Check Constraint Node
if (!pgBrowser.Nodes['check_constraint']) {
pgAdmin.Browser.Nodes['check_constraint'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'check_constraint',
label: gettext('Check'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js
index 0bbf66a1..adccf2e9 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js
@@ -605,7 +605,6 @@ define('pgadmin.node.exclusion_constraint', [
// Extend the browser's node class for exclusion constraint node
if (!pgBrowser.Nodes['exclusion_constraint']) {
pgAdmin.Browser.Nodes['exclusion_constraint'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'exclusion_constraint',
label: gettext('Exclusion constraint'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js
index 3c4b89f3..9899df92 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js
@@ -603,7 +603,6 @@ define('pgadmin.node.foreign_key', [
// Extend the browser's node class for foreign key node
if (!pgBrowser.Nodes['foreign_key']) {
pgAdmin.Browser.Nodes['foreign_key'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'foreign_key',
label: gettext('Foreign key'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js
index d3a6cff4..0ad0f054 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js
@@ -20,7 +20,6 @@ define('pgadmin.node.primary_key', [
parent_type: ['table','partition'],
canDrop: true,
canDropCascade: true,
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
Init: function() {
/* Avoid multiple registration of menus */
if (this.initialized)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js
index 769185d6..18d3ca33 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js
@@ -20,7 +20,6 @@ define('pgadmin.node.unique_constraint', [
parent_type: ['table','partition'],
canDrop: true,
canDropCascade: true,
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
Init: function() {
/* Avoid multiple registration of menus */
if (this.initialized)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js
index cb242cd2..9c0e24fe 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js
@@ -12,14 +12,12 @@ define('pgadmin.node.constraints', [
node: 'constraints',
label: gettext('Constraints'),
type: 'coll-constraints',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
columns: ['name', 'comment'],
});
}
if (!pgBrowser.Nodes['constraints']) {
pgAdmin.Browser.Nodes['constraints'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'constraints',
label: gettext('Constraints'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js
index ec2b4da1..2b687c1a 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js
@@ -13,7 +13,6 @@ define('pgadmin.node.index', [
node: 'index',
label: gettext('Indexes'),
type: 'coll-index',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
sqlAlterHelp: 'sql-alterindex.html',
sqlCreateHelp: 'sql-createindex.html',
dialogHelp: url_for('help.static', {'filename': 'index_dialog.html'}),
@@ -215,7 +214,6 @@ define('pgadmin.node.index', [
if (!pgBrowser.Nodes['index']) {
pgAdmin.Browser.Nodes['index'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table', 'view', 'mview', 'partition'],
collection_type: ['coll-table', 'coll-view'],
sqlAlterHelp: 'sql-alterindex.html',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js
index d807304e..d086473a 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js
@@ -2,10 +2,12 @@ define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.backgrid',
+ 'sources/tree/node_menu',
'pgadmin.browser.collection', 'pgadmin.browser.table.partition.utils',
],
function(
- gettext, url_for, $, _, S, pgAdmin, pgBrowser, Alertify, Backform, Backgrid
+ gettext, url_for, $, _, S, pgAdmin, pgBrowser, Alertify, Backform, Backgrid,
+ treeNodeMenu
) {
if (!pgBrowser.Nodes['coll-partition']) {
@@ -13,7 +15,6 @@ function(
pgAdmin.Browser.Collection.extend({
node: 'partition',
label: gettext('Partitions'),
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'coll-partition',
columns: [
'name', 'schema', 'partition_value', 'is_partitioned', 'description',
@@ -80,36 +81,6 @@ function(
},
]);
},
- getTreeNodeHierarchy: function(i) {
- var idx = 0,
- res = {},
- t = pgBrowser.tree;
-
- do {
- var d = t.itemData(i);
- if (
- d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId
- ) {
- if (d._type == 'partition' && 'partition' in res) {
- if (!('table' in res)) {
- res['table'] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- } else if (d._type == 'table') {
- if (!('table' in res)) {
- res['table'] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- } else {
- res[d._type] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- } while (i);
-
- return res;
- },
generate_url: function(item, type, d, with_id, info) {
if (_.indexOf([
'stats', 'statistics', 'dependency', 'dependent', 'reset',
@@ -1189,34 +1160,7 @@ function(
return data;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-table' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null;
- var prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
+ canCreate: treeNodeMenu.canCreate(pgBrowser, 'coll-table'),
// Check to whether table has disable trigger(s)
canCreate_with_trigger_enable: function(itemData, item, data) {
if(this.canCreate.apply(this, [itemData, item, data])) {
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js
index 3af61754..354909f3 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js
@@ -16,7 +16,6 @@ define('pgadmin.node.rule', [
node: 'rule',
label: gettext('Rules'),
type: 'coll-rule',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
columns: ['name', 'owner', 'comment'],
});
}
@@ -35,7 +34,6 @@ define('pgadmin.node.rule', [
*/
if (!pgBrowser.Nodes['rule']) {
pgAdmin.Browser.Nodes['rule'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table','view', 'partition'],
type: 'rule',
sqlAlterHelp: 'sql-alterrule.html',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers.js
new file mode 100644
index 00000000..2d792043
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers.js
@@ -0,0 +1,52 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import axios from 'axios';
+
+export function disableTriggers(tree, alertify, generateUrl, args) {
+ return setTriggers(tree, alertify, generateUrl, args, {enable: 'false' });
+}
+export function enableTriggers(tree, alertify, generateUrl, args) {
+ return setTriggers(tree, alertify, generateUrl, args, {enable: 'true' });
+}
+
+function setTriggers(tree, alertify, generateUrl, args, params) {
+ const treeNode = retrieveTreeNode(args, tree);
+
+ if (!treeNode || treeNode.getData() === null || treeNode.getData() === undefined)
+ return false;
+
+ axios.put(
+ generateUrl(treeNode.getHtmlIdentifier(), 'set_trigger', treeNode.getData(), true),
+ params
+ )
+ .then((res) => {
+ if (res.data.success === 1) {
+ alertify.success(res.data.info);
+ treeNode.reload(tree);
+ }
+ })
+ .catch((xhr) => {
+ try {
+ const err = xhr.response.data;
+ if (err.success === 0) {
+ alertify.error(err.errormsg);
+ }
+ } catch (e) {
+ console.warn(e.stack || e);
+ }
+ treeNode.unload(tree);
+ });
+}
+
+function retrieveTreeNode(args, tree) {
+ const input = args || {};
+ const domElementIdentifier = input.item || tree.selected();
+ return tree.findNodeByDomElement(domElementIdentifier);
+}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
index d440bf04..b0bc05b9 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
@@ -1,13 +1,16 @@
define('pgadmin.node.table', [
+ 'pgadmin.tables.js/enable_disable_triggers',
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.backgrid',
'pgadmin.tables.js/show_advanced_tab',
+ 'sources/tree/node_menu',
'pgadmin.browser.collection', 'pgadmin.node.column',
'pgadmin.node.constraints', 'pgadmin.browser.table.partition.utils',
], function(
+ tableFunctions,
gettext, url_for, $, _, S, pgAdmin, pgBrowser, Alertify, Backform, Backgrid,
- ShowAdvancedTab
+ ShowAdvancedTab, treeNodeMenu
) {
if (!pgBrowser.Nodes['coll-table']) {
@@ -26,7 +29,6 @@ define('pgadmin.node.table', [
if (!pgBrowser.Nodes['table']) {
pgBrowser.Nodes['table'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'table',
label: gettext('Table'),
collection_type: 'coll-table',
@@ -118,46 +120,21 @@ define('pgadmin.node.table', [
callbacks: {
/* Enable trigger(s) on table */
enable_triggers_on_table: function(args) {
- var params = {'enable': true };
- this.callbacks.set_triggers.apply(this, [args, params]);
+ tableFunctions.enableTriggers(
+ pgBrowser.treeMenu,
+ Alertify,
+ this.generate_url.bind(this),
+ args
+ );
},
/* Disable trigger(s) on table */
disable_triggers_on_table: function(args) {
- var params = {'enable': false };
- this.callbacks.set_triggers.apply(this, [args, params]);
- },
- set_triggers: function(args, params) {
- // This function will send request to enable or
- // disable triggers on table level
- var input = args || {},
- obj = this,
- t = pgBrowser.tree,
- i = input.item || t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined;
- if (!d)
- return false;
-
- $.ajax({
- url: obj.generate_url(i, 'set_trigger' , d, true),
- type:'PUT',
- data: params,
- dataType: 'json',
- success: function(res) {
- if (res.success == 1) {
- Alertify.success(res.info);
- t.unload(i);
- t.setInode(i);
- t.deselect(i);
- setTimeout(function() {
- t.select(i);
- }, 10);
- }
- },
- error: function(xhr, status, error) {
- Alertify.pgRespErrorNotify(xhr, error);
- t.unload(i);
- },
- });
+ tableFunctions.disableTriggers(
+ pgBrowser.treeMenu,
+ Alertify,
+ this.generate_url.bind(this),
+ args
+ );
},
/* Truncate table */
truncate_table: function(args) {
@@ -1299,34 +1276,7 @@ define('pgadmin.node.table', [
return data;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-table' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
+ canCreate: treeNodeMenu.canCreate(pgBrowser, 'coll-table'),
// Check to whether table has disable trigger(s)
canCreate_with_trigger_enable: function(itemData, item, data) {
if(this.canCreate.apply(this, [itemData, item, data])) {
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js
index a2c27188..4c25e3ea 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js
@@ -29,14 +29,12 @@ define('pgadmin.node.trigger', [
node: 'trigger',
label: gettext('Triggers'),
type: 'coll-trigger',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
columns: ['name', 'description'],
});
}
if (!pgBrowser.Nodes['trigger']) {
pgAdmin.Browser.Nodes['trigger'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table', 'view', 'partition'],
collection_type: ['coll-table', 'coll-view'],
type: 'trigger',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.js
index c1c24861..28a223ad 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.js
@@ -1,8 +1,9 @@
define('pgadmin.node.type', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
- 'pgadmin.backgrid', 'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, Backgrid) {
+ 'pgadmin.backgrid', 'sources/tree/node_menu', 'pgadmin.browser.collection',
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, Backgrid,
+ treeNodeMenu) {
if (!pgBrowser.Nodes['coll-type']) {
pgBrowser.Nodes['coll-type'] =
@@ -911,34 +912,7 @@ define('pgadmin.node.type', [
return result;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-type' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
+ canCreate: treeNodeMenu.canCreate(pgBrowser, 'coll-type'),
});
}
return pgBrowser.Nodes['type'];
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.js
index 073ef5cb..ae3d55c1 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.js
@@ -1,8 +1,10 @@
define('pgadmin.node.mview', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.alertifyjs', 'pgadmin.browser',
- 'pgadmin.backform', 'pgadmin.browser.server.privilege',
-], function(gettext, url_for, $, _, pgAdmin, Alertify, pgBrowser, Backform) {
+ 'pgadmin.backform', 'sources/tree/node_menu',
+ 'pgadmin.browser.server.privilege',
+], function(gettext, url_for, $, _, pgAdmin, Alertify, pgBrowser, Backform,
+ treeNodeMenu) {
/**
Create and add a view collection into nodes
@@ -240,39 +242,7 @@ define('pgadmin.node.mview', [
Show or hide create view menu option on parent node
and hide for system view in catalogs.
*/
- canCreate: function(itemData, item, data) {
-
- // If check is false then, we will allow create menu
- if (data && data.check === false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
-
- // To iterate over tree to check parent node
- while (i) {
-
- // If it is schema then allow user to create view
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-mview' == d._type) {
-
- // Check if we are not child of view
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
-
- // by default we do not want to allow create menu
- return true;
- },
+ canCreate: treeNodeMenu.canCreate(pgBrowser, 'coll-mview'),
refresh_mview: function(args) {
var input = args || {},
obj = this,
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js
index 5755a509..bd9e1fda 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js
@@ -1,9 +1,11 @@
define('pgadmin.node.view', [
'sources/gettext',
'sources/url_for', 'jquery', 'underscore', 'sources/pgadmin',
- 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.browser.server.privilege',
+ 'pgadmin.browser', 'pgadmin.backform',
+ 'sources/tree/node_menu', 'pgadmin.browser.server.privilege',
'pgadmin.node.rule',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform) {
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform,
+ treeNodeMenu) {
/**
Create and add a view collection into nodes
@@ -202,40 +204,7 @@ define('pgadmin.node.view', [
Show or hide create view menu option on parent node
and hide for system view in catalogs.
*/
- canCreate: function(itemData, item, data) {
-
- // If check is false then, we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
-
- // To iterate over tree to check parent node
- while (i) {
-
- // If it is schema then allow user to create view
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-view' == d._type) {
-
- // Check if we are not child of view
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
-
- // by default we do not want to allow create menu
- return true;
-
- },
+ canCreate: treeNodeMenu.canCreate(pgBrowser, 'coll-view'),
});
}
diff --git a/web/pgadmin/browser/static/js/node.js b/web/pgadmin/browser/static/js/node.js
index ad582483..3baf554a 100644
--- a/web/pgadmin/browser/static/js/node.js
+++ b/web/pgadmin/browser/static/js/node.js
@@ -1,9 +1,12 @@
define('pgadmin.browser.node', [
+ 'sources/tree/pgadmin_tree_node',
'sources/gettext', 'jquery', 'underscore', 'underscore.string', 'sources/pgadmin',
'pgadmin.browser.menu', 'backbone', 'pgadmin.alertifyjs', 'pgadmin.browser.datamodel',
'backform', 'sources/browser/generate_url', 'sources/utils', 'pgadmin.browser.utils',
'pgadmin.backform',
-], function(gettext, $, _, S, pgAdmin, Menu, Backbone, Alertify, pgBrowser, Backform, generateUrl, commonUtils) {
+], function(
+ pgadminTreeNode,
+ gettext, $, _, S, pgAdmin, Menu, Backbone, Alertify, pgBrowser, Backform, generateUrl, commonUtils) {
var wcDocker = window.wcDocker,
keyCode = {
@@ -1566,7 +1569,6 @@ define('pgadmin.browser.node', [
* depends, statistics
*/
generate_url: function(item, type, d, with_id, info) {
-
var opURL = {
'create': 'obj',
'drop': 'obj',
@@ -1608,24 +1610,7 @@ define('pgadmin.browser.node', [
Collection: pgBrowser.DataCollection,
// Base class for Node Data Model
Model: pgBrowser.DataModel,
- getTreeNodeHierarchy: function(i) {
- var idx = 0,
- res = {},
- t = pgBrowser.tree,
- d;
- do {
- d = t.itemData(i);
- if (d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId) {
- res[d._type] = _.extend({}, d, {
- 'priority': idx,
- });
- idx -= 1;
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- } while (i);
-
- return res;
- },
+ getTreeNodeHierarchy: pgadminTreeNode.getTreeNodeHierarchyFromIdentifier.bind(pgBrowser),
cache: function(url, node_info, level, data) {
var cached = this.cached = this.cached || {},
hash = url,
diff --git a/web/pgadmin/static/js/alertify/dialog.js b/web/pgadmin/static/js/alertify/dialog.js
new file mode 100644
index 00000000..5a8646f9
--- /dev/null
+++ b/web/pgadmin/static/js/alertify/dialog.js
@@ -0,0 +1,129 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../gettext';
+import {sprintf} from 'sprintf-js';
+import {DialogFactory} from './dialog_factory';
+import Backform from '../backform.pgadmin';
+import {getTreeNodeHierarchyFromIdentifier} from '../tree/pgadmin_tree_node';
+
+/**
+ * This class can be extended to create new dialog boxes.
+ * Examples of this can be found in:
+ * `web/pgadmin/static/js/backup/backup_dialog.js`
+ *
+ * Do not forget to add the new Dialog type to the `DialogFactory`
+ */
+export class Dialog {
+ constructor(errorAlertTitle,
+ dialogContainerSelector,
+ pgBrowser, $, alertify, DialogModel,
+ backform = Backform) {
+ this.errorAlertTitle = errorAlertTitle;
+ this.alertify = alertify;
+ this.pgBrowser = pgBrowser;
+ this.jquery = $;
+ this.dialogModel = DialogModel;
+ this.backform = backform;
+ this.dialogContainerSelector = dialogContainerSelector;
+ }
+
+ retrieveAncestorOfTypeServer(item) {
+ let serverInformation = null;
+ let aciTreeItem = item || this.pgBrowser.treeMenu.selected();
+ let treeNode = this.pgBrowser.treeMenu.findNodeByDomElement(aciTreeItem);
+
+ while (treeNode) {
+ const node_data = treeNode.getData();
+ if (node_data._type === 'server') {
+ serverInformation = node_data;
+ break;
+ }
+
+ if (treeNode.hasParent()) {
+ treeNode = treeNode.parent();
+ } else {
+ this.alertify.alert(
+ gettext(this.errorAlertTitle),
+ gettext('Please select server or child node from the browser tree.')
+ );
+ break;
+ }
+ }
+ return serverInformation;
+ }
+
+ hasBinariesConfiguration(serverInformation) {
+ const module = 'paths';
+ let preference_name = 'pg_bin_dir';
+ let msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
+
+ if ((serverInformation.type && serverInformation.type === 'ppas') ||
+ serverInformation.server_type === 'ppas') {
+ preference_name = 'ppas_bin_dir';
+ msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
+ }
+ const preference = this.pgBrowser.get_preference(module, preference_name);
+
+ if (preference) {
+ if (!preference.value) {
+ this.alertify.alert(gettext('Configuration required'), msg);
+ return false;
+ }
+ } else {
+ this.alertify.alert(
+ gettext(this.errorAlertTitle),
+ sprintf(gettext('Failed to load preference %s of module %s'), preference_name, module)
+ );
+ return false;
+ }
+ return true;
+ }
+
+ dialogName() {
+ return undefined;
+ }
+
+ createOrGetDialog(dialogTitle, typeOfDialog) {
+ const dialogName = this.dialogName(typeOfDialog);
+
+ if (!this.alertify[dialogName]) {
+ const self = this;
+ this.alertify.dialog(dialogName, function factory() {
+ return self.dialogFactory(dialogTitle, typeOfDialog);
+ });
+ }
+ return this.alertify[dialogName];
+ }
+
+ dialogFactory(dialogTitle, typeOfDialog) {
+ const factory = new DialogFactory(
+ this.pgBrowser,
+ this.jquery,
+ this.alertify,
+ this.dialogModel,
+ this.backform,
+ this.dialogContainerSelector);
+ return factory.create(dialogTitle, typeOfDialog);
+ }
+
+ canExecuteOnCurrentDatabase(aciTreeItem) {
+ const treeInfo = getTreeNodeHierarchyFromIdentifier.apply(this.pgBrowser, [aciTreeItem]);
+
+ if (treeInfo.database && treeInfo.database._label.indexOf('=') >= 0) {
+ this.alertify.alert(
+ gettext(this.errorAlertTitle),
+ gettext('Databases with = symbols in the name cannot be backed up or restored using this utility.')
+ );
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/web/pgadmin/static/js/alertify/dialog_factory.js b/web/pgadmin/static/js/alertify/dialog_factory.js
new file mode 100644
index 00000000..500140b8
--- /dev/null
+++ b/web/pgadmin/static/js/alertify/dialog_factory.js
@@ -0,0 +1,52 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import * as BackupDialog from '../../../tools/backup/static/js/backup_dialog_wrapper';
+import {RestoreDialogWrapper} from '../../../tools/restore/static/js/restore_dialog_wrapper';
+
+export class DialogFactory {
+ constructor(pgBrowser, $,
+ alertify, DialogModel,
+ backform, dialogContainerSelector) {
+ this.pgBrowser = pgBrowser;
+ this.jquery = $;
+ this.alertify = alertify;
+ this.dialogModel = DialogModel;
+ this.backform = backform;
+ this.dialogContainerSelector = dialogContainerSelector;
+ }
+
+ create(dialogTitle, typeOfDialog) {
+ if (typeOfDialog === 'restore') {
+ return this.createRestoreDialog(dialogTitle, typeOfDialog);
+ } else {
+ return this.createBackupDialog(dialogTitle, typeOfDialog);
+ }
+ }
+
+ createRestoreDialog(dialogTitle, typeOfDialog) {
+ return new RestoreDialogWrapper(
+ this.dialogContainerSelector, dialogTitle, typeOfDialog,
+ this.jquery,
+ this.pgBrowser,
+ this.alertify,
+ this.dialogModel,
+ this.backform);
+ }
+
+ createBackupDialog(dialogTitle, typeOfDialog) {
+ return new BackupDialog.BackupDialogWrapper(
+ this.dialogContainerSelector, dialogTitle, typeOfDialog,
+ this.jquery,
+ this.pgBrowser,
+ this.alertify,
+ this.dialogModel,
+ this.backform);
+ }
+}
diff --git a/web/pgadmin/static/js/alertify/dialog_wrapper.js b/web/pgadmin/static/js/alertify/dialog_wrapper.js
new file mode 100644
index 00000000..b5ff8204
--- /dev/null
+++ b/web/pgadmin/static/js/alertify/dialog_wrapper.js
@@ -0,0 +1,57 @@
+import * as commonUtils from '../utils';
+
+export class DialogWrapper {
+ constructor(
+ dialogContainerSelector, dialogTitle, jquery, pgBrowser,
+ alertify, dialogModel, backform) {
+ this.hooks = {
+ onclose: function () {
+ if (this.view) {
+ this.view.remove({
+ data: true,
+ internal: true,
+ silent: true,
+ });
+ }
+ },
+ };
+ this.dialogContainerSelector = dialogContainerSelector;
+ this.dialogTitle = dialogTitle;
+ this.jquery = jquery;
+ this.pgBrowser = pgBrowser;
+ this.alertify = alertify;
+ this.dialogModel = dialogModel;
+ this.backform = backform;
+ }
+
+ build() {
+ this.alertify.pgDialogBuild.apply(this);
+ }
+
+ wasHelpButtonPressed(e) {
+ return e.button.element.name === 'dialog_help'
+ || e.button.element.name === 'object_help';
+ }
+
+ getSelectedNodeData(selectedTreeNode) {
+ if (!this.isNodeSelected(selectedTreeNode)) {
+ return undefined;
+ }
+ const treeNodeData = selectedTreeNode.getData();
+ if (treeNodeData) {
+ return treeNodeData;
+ }
+ return undefined;
+ }
+
+ focusOnDialog(dialog) {
+ dialog.$el.attr('tabindex', -1);
+ this.pgBrowser.keyboardNavigation.getDialogTabNavigator(dialog);
+ const container = dialog.$el.find('.tab-content:first > .tab-pane.active:first');
+ commonUtils.findAndSetFocus(container);
+ }
+
+ isNodeSelected(selectedTreeNode) {
+ return selectedTreeNode;
+ }
+}
diff --git a/web/pgadmin/static/js/tree/node_menu.js b/web/pgadmin/static/js/tree/node_menu.js
new file mode 100644
index 00000000..7aedb54e
--- /dev/null
+++ b/web/pgadmin/static/js/tree/node_menu.js
@@ -0,0 +1,46 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+/////////////////////////////////////////////////////////////
+
+export function canCreate(pgBrowser, childOfCatalogType) {
+ return canCreateObject.bind({
+ browser: pgBrowser,
+ childOfCatalogType: childOfCatalogType,
+ });
+}
+
+function canCreateObject(itemData, item, data) {
+ //If check is false then , we will allow create menu
+ if (data && data.check === false) {
+ return true;
+ }
+
+ let node = this.browser.treeMenu.findNodeByDomElement(item);
+
+ if (node.anyFamilyMember(
+ parentCatalogOfTableChild.bind(null, this.childOfCatalogType)
+ )) {
+ return false;
+ }
+
+ return true;
+}
+
+function parentCatalogOfTableChild(arg, node) {
+ if (arg === node.getData()._type) {
+ if (node.hasParent()) {
+
+ let parent = node.parent();
+ if ('catalog' === parent.getData()._type) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
diff --git a/web/pgadmin/static/js/tree/pgadmin_tree_node.js b/web/pgadmin/static/js/tree/pgadmin_tree_node.js
new file mode 100644
index 00000000..00f10d24
--- /dev/null
+++ b/web/pgadmin/static/js/tree/pgadmin_tree_node.js
@@ -0,0 +1,72 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+/**
+ * This method received pgBrowser and new TreeNode object
+ *
+ * This method retrieves all the data that exists in the tree node and in
+ * `pgBrowser.Nodes` for all the parent node of the provided node.
+ *
+ * The 2 condition to get the information from pgBrowser.Nodes are:
+ * 1 - the variable _type of the tree node
+ * 2 - the presence of hasId in the pgBrowser.Nodes for the specific node
+ *
+ * Number 2 is used to ignore coll-* nodes as they do not add any useful
+ * information
+ */
+export function getTreeNodeHierarchyFromElement(pgBrowser, treeNode) {
+ return getTreeNodeHierarchy.call(pgBrowser, treeNode);
+}
+
+/**
+ * This method received an ACI Tree JQuery node
+ *
+ * NOTE: this function need to be called on pgBrowser instance.
+ * getTreeNodeHierarchyFromIdentifier.apply(pgBrowser, [aciTreeNodeIdentifier])
+ *
+ * This method retrieves all the data that exists in the tree node and in
+ * `pgBrowser.Nodes` for all the parent node of the provided node.
+ *
+ * The 2 condition to get the information from pgBrowser.Nodes are:
+ * 1 - the variable _type of the tree node
+ * 2 - the presence of hasId in the pgBrowser.Nodes for the specific node
+ *
+ * Number 2 is used to ignore coll-* nodes as they do not add any useful
+ * information
+ */
+export function getTreeNodeHierarchyFromIdentifier(aciTreeNodeIdentifier) {
+ let identifier = this.treeMenu.translateTreeNodeIdFromACITree(aciTreeNodeIdentifier);
+ let currentNode = this.treeMenu.findNode(identifier);
+ return getTreeNodeHierarchy.call(this, currentNode);
+}
+
+export function getTreeNodeHierarchy(currentNode) {
+ let idx = 0;
+ let result = {};
+
+ do {
+ const currentNodeData = currentNode.getData();
+ if (currentNodeData._type in this.Nodes && this.Nodes[currentNodeData._type].hasId) {
+ const nodeType = mapType(currentNodeData._type);
+ if (result[nodeType] === undefined) {
+ result[nodeType] = _.extend({}, currentNodeData, {
+ 'priority': idx,
+ });
+ idx -= 1;
+ }
+ }
+ currentNode = currentNode.hasParent() ? currentNode.parent() : null;
+ } while (currentNode);
+
+ return result;
+}
+
+function mapType(type) {
+ return type === 'partition' ? 'table' : type;
+}
diff --git a/web/pgadmin/static/js/tree/supported_nodes_menu.js b/web/pgadmin/static/js/tree/supported_nodes_menu.js
new file mode 100644
index 00000000..8cdfcd98
--- /dev/null
+++ b/web/pgadmin/static/js/tree/supported_nodes_menu.js
@@ -0,0 +1,50 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {isValidTreeNodeData} from './tree';
+
+function isNodeTypeSupported(supportedNodes, nodeDataType, treeNode) {
+ return _.indexOf(supportedNodes, nodeDataType) !== -1
+ && ancestorWithTypeCatalogDoesNotExists(treeNode);
+}
+
+function doesNodeHaveMenu(treeNodeData) {
+ return (treeNodeData._type === 'database' && treeNodeData.allowConn)
+ || treeNodeData._type !== 'database';
+}
+
+function ancestorWithTypeCatalogDoesNotExists(treeNode) {
+ let currentNode = treeNode;
+
+ while(currentNode.hasParent() && treeNode.parent().getData() !== null) {
+ if(currentNode.parent().getData()._type === 'catalog') {
+ return false;
+ }
+
+ currentNode = currentNode.parent();
+ }
+
+ return true;
+}
+
+export function enabled(tree, supportedNodes, treeNodeData, domTreeNode) {
+ let treeNode = tree.findNodeByDomElement(domTreeNode);
+ if (!treeNode) {
+ return false;
+ }
+
+ if (isValidTreeNodeData(treeNodeData)) {
+ return isNodeTypeSupported(supportedNodes, treeNodeData._type, treeNode)
+ && doesNodeHaveMenu(treeNodeData);
+ } else {
+ return false;
+ }
+}
+
+
diff --git a/web/pgadmin/static/js/tree/tree.js b/web/pgadmin/static/js/tree/tree.js
index 01edb6c3..a8b122cf 100644
--- a/web/pgadmin/static/js/tree/tree.js
+++ b/web/pgadmin/static/js/tree/tree.js
@@ -210,3 +210,7 @@ function findInTree(rootNode, path) {
}
})(rootNode);
}
+
+export function isValidTreeNodeData(treeNodeData) {
+ return !_.isUndefined(treeNodeData) && !_.isNull(treeNodeData);
+}
diff --git a/web/pgadmin/tools/backup/static/js/backup.js b/web/pgadmin/tools/backup/static/js/backup.js
index 1b0b3628..fa4e5c6d 100644
--- a/web/pgadmin/tools/backup/static/js/backup.js
+++ b/web/pgadmin/tools/backup/static/js/backup.js
@@ -3,9 +3,12 @@ define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'pgadmin.alertifyjs', 'backbone', 'pgadmin.backgrid',
'pgadmin.backform', 'pgadmin.browser', 'sources/utils',
+ 'tools/backup/static/js/menu_utils',
+ 'tools/backup/static/js/backup_dialog',
+ 'sources/tree/supported_nodes_menu',
], function(
gettext, url_for, $, _, S, alertify, Backbone, Backgrid, Backform, pgBrowser,
-commonUtils
+commonUtils, menuUtils, globalBackupDialog, supportedNodesMenu
) {
// if module is already initialized, refer to that.
@@ -394,48 +397,6 @@ commonUtils
this.initialized = true;
- // Define list of nodes on which backup context menu option appears
- var backup_supported_nodes = [
- 'database', 'schema', 'table', 'partition',
- ];
-
- /**
- Enable/disable backup menu in tools based
- on node selected
- if selected node is present in supported_nodes,
- menu will be enabled otherwise disabled.
- Also, hide it for system view in catalogs
- */
- var menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData,
- parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
-
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(backup_supported_nodes, d._type) !== -1 &&
- parent_data._type != 'catalog') {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
- var menu_enabled_server = function(itemData) {
- // If server node selected && connected
- if (!_.isUndefined(itemData) && !_.isNull(itemData))
- return (('server' === itemData._type) && itemData.connected);
- else
- return false;
- };
-
// Define the nodes on which the menus to be appear
var menus = [{
name: 'backup_global',
@@ -445,7 +406,7 @@ commonUtils
priority: 12,
label: gettext('Backup Globals...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_server',
module: this,
@@ -454,7 +415,7 @@ commonUtils
priority: 12,
label: gettext('Backup Server...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_global_ctx',
module: this,
@@ -464,7 +425,7 @@ commonUtils
priority: 12,
label: gettext('Backup Globals...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_server_ctx',
module: this,
@@ -474,7 +435,7 @@ commonUtils
priority: 12,
label: gettext('Backup Server...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_object',
module: this,
@@ -483,20 +444,24 @@ commonUtils
priority: 11,
label: gettext('Backup...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled,
+ enable: supportedNodesMenu.enabled.bind(
+ null, pgBrowser.treeMenu, menuUtils.backupSupportedNodes
+ ),
}];
- for (var idx = 0; idx < backup_supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.backupSupportedNodes.length; idx++) {
menus.push({
- name: 'backup_' + backup_supported_nodes[idx],
- node: backup_supported_nodes[idx],
+ name: 'backup_' + menuUtils.backupSupportedNodes[idx],
+ node: menuUtils.backupSupportedNodes[idx],
module: this,
applies: ['context'],
callback: 'backup_objects',
priority: 11,
label: gettext('Backup...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled,
+ enable: supportedNodesMenu.enabled.bind(
+ null, pgBrowser.treeMenu, menuUtils.backupSupportedNodes
+ ),
});
}
@@ -521,542 +486,25 @@ commonUtils
},
// Callback to draw Backup Dialog for globals/server
- start_backup_global_server: function(action, item, params) {
- var i = item || pgBrowser.tree.selected(),
- server_data = null;
-
- while (i) {
- var node_data = pgBrowser.tree.itemData(i);
- if (node_data._type == 'server') {
- server_data = node_data;
- break;
- }
-
- if (pgBrowser.tree.hasParent(i)) {
- i = $(pgBrowser.tree.parent(i));
- } else {
- alertify.alert(
- gettext('Backup Error'),
- gettext('Please select server or child node from the browser tree.')
- );
- break;
- }
- }
-
- if (!server_data) {
- return;
- }
-
- var module = 'paths',
- preference_name = 'pg_bin_dir',
- msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
-
- if ((server_data.type && server_data.type == 'ppas') ||
- server_data.server_type == 'ppas') {
- preference_name = 'ppas_bin_dir';
- msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
- }
-
- var preference = pgBrowser.get_preference(module, preference_name);
-
- if (preference) {
- if (!preference.value) {
- alertify.alert(gettext('Configuration required'), msg);
- return;
- }
- } else {
- alertify.alert(
- gettext('Backup Error'),
- S(gettext('Failed to load preference %s of module %s')).sprintf(preference_name, module).value()
- );
- return;
- }
-
- var of_type = undefined;
-
- // Set Notes according to type of backup
- if (!_.isUndefined(params['globals']) && params['globals']) {
- of_type = 'globals';
- } else {
- of_type = 'server';
- }
-
- var DialogName = 'BackupDialog_' + of_type,
- DialogTitle = ((of_type == 'globals') ?
- gettext('Backup Globals...') :
- gettext('Backup Server...'));
-
- if (!alertify[DialogName]) {
- alertify.dialog(DialogName, function factory() {
- return {
- main: function(title) {
- this.set('title', title);
- },
- build: function() {
- alertify.pgDialogBuild.apply(this);
- },
- setup: function() {
- return {
- buttons: [{
- text: '',
- className: 'btn btn-default pull-left fa fa-lg fa-info',
- attrs: {
- name: 'object_help',
- type: 'button',
- url: 'backup.html',
- label: gettext('Backup'),
- },
- }, {
- text: '',
- key: 112,
- className: 'btn btn-default pull-left fa fa-lg fa-question',
- attrs: {
- name: 'dialog_help',
- type: 'button',
- label: gettext('Backup'),
- url: url_for('help.static', {
- 'filename': 'backup_dialog.html',
- }),
- },
- }, {
- text: gettext('Backup'),
- key: 13,
- className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
- 'data-btn-name': 'backup',
- }, {
- text: gettext('Cancel'),
- key: 27,
- className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
- 'data-btn-name': 'cancel',
- }],
- // Set options for dialog
- options: {
- title: DialogTitle,
- //disable both padding and overflow control.
- padding: !1,
- overflow: !1,
- model: 0,
- resizable: true,
- maximizable: true,
- pinnable: false,
- closableByDimmer: false,
- modal: false,
- },
- };
- },
- hooks: {
- // Triggered when the dialog is closed
- onclose: function() {
- if (this.view) {
- // clear our backform model/view
- this.view.remove({
- data: true,
- internal: true,
- silent: true,
- });
- }
- },
- },
- prepare: function() {
- var self = this;
- // Disable Backup button until user provides Filename
- this.__internal.buttons[2].element.disabled = true;
-
- var $container = $('<div class=\'backup_dialog\'></div>');
- // Find current/selected node
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (!d)
- return;
- // Create treeInfo
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
- // Instance of backbone model
- var newModel = new BackupModel({
- type: of_type,
- }, {
- node_info: treeInfo,
- }),
- fields = Backform.generateViewSchema(
- treeInfo, newModel, 'create', node, treeInfo.server, true
- );
-
- var view = this.view = new Backform.Dialog({
- el: $container,
- model: newModel,
- schema: fields,
- });
- // Add our class to alertify
- $(this.elements.body.childNodes[0]).addClass(
- 'alertify_tools_dialog_properties obj_properties'
- );
- // Render dialog
- view.render();
-
- this.elements.content.appendChild($container.get(0));
-
- var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
- commonUtils.findAndSetFocus(container);
-
- // Listen to model & if filename is provided then enable Backup button
- this.view.model.on('change', function() {
- if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
- this.errorModel.clear();
- self.__internal.buttons[2].element.disabled = false;
- } else {
- self.__internal.buttons[2].element.disabled = true;
- this.errorModel.set('file', gettext('Please provide a filename'));
- }
- });
- },
- // Callback functions when click on the buttons of the Alertify dialogs
- callback: function(e) {
- // Fetch current server id
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (e.button.element.name == 'dialog_help' || e.button.element.name == 'object_help') {
- e.cancel = true;
- pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
- node, i, e.button.element.getAttribute('label'));
- return;
- }
-
- if (e.button['data-btn-name'] === 'backup') {
-
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- var self = this,
- baseUrl = url_for('backup.create_server_job', {
- 'sid': treeInfo.server._id,
- }),
- args = this.view.model.toJSON();
-
- $.ajax({
- url: baseUrl,
- method: 'POST',
- data: {
- 'data': JSON.stringify(args),
- },
- success: function(res) {
- if (res.success) {
- alertify.success(gettext('Backup job created.'), 5);
- pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
- } else {
- console.warn(res);
- }
- },
- error: function(xhr) {
- try {
- var err = $.parseJSON(xhr.responseText);
- alertify.alert(
- gettext('Backup job failed.'),
- err.errormsg
- );
- } catch (e) {
- console.warn(e.stack || e);
- }
- },
- });
- }
- },
- };
- });
- }
- alertify[DialogName](true).resizeTo('60%', '50%');
+ start_backup_global_server: function(action, treeItem, params) {
+ let dialog = new globalBackupDialog.BackupDialog(
+ pgBrowser,
+ $,
+ alertify,
+ BackupModel
+ );
+ dialog.draw(action, treeItem, params);
},
// Callback to draw Backup Dialog for objects
backup_objects: function(action, treeItem) {
-
- var i = treeItem || pgBrowser.tree.selected(),
- server_data = null;
-
- while (i) {
- var node_data = pgBrowser.tree.itemData(i);
- if (node_data._type == 'server') {
- server_data = node_data;
- break;
- }
-
- if (pgBrowser.tree.hasParent(i)) {
- i = $(pgBrowser.tree.parent(i));
- } else {
- alertify.alert(
- gettext('Backup Error'),
- gettext('Please select server or child node from tree.')
- );
- break;
- }
- }
-
- if (!server_data) {
- return;
- }
-
- var module = 'paths',
- preference_name = 'pg_bin_dir',
- msg = gettext('Please set binary path for PostgreSQL Server from preferences.');
-
- if ((server_data.type && server_data.type == 'ppas') ||
- server_data.server_type == 'ppas') {
- preference_name = 'ppas_bin_dir';
- msg = gettext('Please set binary path for EDB Postgres Advanced Server from preferences.');
- }
-
- var preference = pgBrowser.get_preference(module, preference_name);
-
- if (preference) {
- if (!preference.value) {
- alertify.alert(gettext('Configuration required'), msg);
- return;
- }
- } else {
- alertify.alert(
- gettext('Backup Error'),
- S(gettext('Failed to load preference %s of module %s')).sprintf(preference_name, module).value()
- );
- return;
- }
-
- var title = S(gettext('Backup (%s: %s)')),
- tree = pgBrowser.tree,
- item = treeItem || tree.selected(),
- data = item && item.length == 1 && tree.itemData(item),
- node = data && data._type && pgBrowser.Nodes[data._type];
-
- if (!node)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [item]);
-
- if (treeInfo.database._label.indexOf('=') >= 0) {
- alertify.alert(
- gettext('Backup error'),
- gettext('Backup job creation failed. '+
- 'Databases with = symbols in the name cannot be backed up using this utility.')
- );
- return;
- }
-
- title = title.sprintf(node.label, data.label).value();
-
- if (!alertify.backup_objects) {
- // Create Dialog title on the fly with node details
- alertify.dialog('backup_objects', function factory() {
- return {
- main: function(title) {
- this.set('title', title);
- },
- build: function() {
- alertify.pgDialogBuild.apply(this);
- },
- setup: function() {
- return {
- buttons: [{
- text: '',
- className: 'btn btn-default pull-left fa fa-lg fa-info',
- attrs: {
- name: 'object_help',
- type: 'button',
- url: 'backup.html',
- label: gettext('Backup'),
- },
- }, {
- text: '',
- key: 112,
- className: 'btn btn-default pull-left fa fa-lg fa-question',
- attrs: {
- name: 'dialog_help',
- type: 'button',
- label: gettext('Backup'),
- url: url_for('help.static', {
- 'filename': 'backup_dialog.html',
- }),
- },
- }, {
- text: gettext('Backup'),
- key: 13,
- className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
- 'data-btn-name': 'backup',
- }, {
- text: gettext('Cancel'),
- key: 27,
- className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
- 'data-btn-name': 'cancel',
- }],
- // Set options for dialog
- options: {
- title: title,
- //disable both padding and overflow control.
- padding: !1,
- overflow: !1,
- model: 0,
- resizable: true,
- maximizable: true,
- pinnable: false,
- closableByDimmer: false,
- modal: false,
- },
- };
- },
- hooks: {
- // triggered when the dialog is closed
- onclose: function() {
- if (this.view) {
- this.view.remove({
- data: true,
- internal: true,
- silent: true,
- });
- }
- },
- },
- prepare: function() {
- var self = this;
- // Disable Backup button until user provides Filename
- this.__internal.buttons[2].element.disabled = true;
- var $container = $('<div class=\'backup_dialog\'></div>');
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- var newModel = new BackupObjectModel({}, {
- node_info: treeInfo,
- }),
- fields = Backform.generateViewSchema(
- treeInfo, newModel, 'create', node, treeInfo.server, true
- );
-
- var view = this.view = new Backform.Dialog({
- el: $container,
- model: newModel,
- schema: fields,
- });
-
- $(this.elements.body.childNodes[0]).addClass(
- 'alertify_tools_dialog_properties obj_properties'
- );
-
- view.render();
-
- this.elements.content.appendChild($container.get(0));
-
- if(view) {
- view.$el.attr('tabindex', -1);
- // var dialogTabNavigator = pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
- commonUtils.findAndSetFocus(container);
- }
- // Listen to model & if filename is provided then enable Backup button
- this.view.model.on('change', function() {
- if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
- this.errorModel.clear();
- self.__internal.buttons[2].element.disabled = false;
- } else {
- self.__internal.buttons[2].element.disabled = true;
- this.errorModel.set('file', gettext('Please provide filename'));
- }
- });
-
- },
- // Callback functions when click on the buttons of the Alertify dialogs
- callback: function(e) {
- // Fetch current server id
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (e.button.element.name == 'dialog_help' || e.button.element.name == 'object_help') {
- e.cancel = true;
- pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
- node, i, e.button.element.getAttribute('label'));
- return;
- }
-
- if (e.button['data-btn-name'] === 'backup') {
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- // Set current database into model
- this.view.model.set('database', treeInfo.database._label);
-
- // We will remove once object tree is implemented
- // If selected node is Schema then add it in model
- if (d._type == 'schema') {
- var schemas = [];
- schemas.push(d._label);
- this.view.model.set('schemas', schemas);
- }
- // If selected node is Table then add it in model along with
- // its schema
- if (d._type == 'table') {
- this.view.model.set(
- 'tables', [
- [treeInfo.schema._label, d._label],
- ]
- );
- }
-
- // Remove ratio attribute from model if it has empty string.
- // The valid value can be between 0 to 9.
- if (_.isEmpty(this.view.model.get('ratio'))) {
- this.view.model.unset('ratio');
- }
-
- var self = this,
- baseUrl = url_for('backup.create_object_job', {
- 'sid': treeInfo.server._id,
- }),
- args = this.view.model.toJSON();
-
- $.ajax({
- url: baseUrl,
- method: 'POST',
- data: {
- 'data': JSON.stringify(args),
- },
- success: function(res) {
- if (res.success) {
- alertify.success(gettext('Backup job created.'), 5);
- pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
- }
- },
- error: function(xhr) {
- try {
- var err = $.parseJSON(xhr.responseText);
- alertify.alert(
- gettext('Backup job failed.'),
- err.errormsg
- );
- } catch (e) {
- console.warn(e.stack || e);
- }
- },
- });
- }
- },
- };
- });
- }
- alertify.backup_objects(title).resizeTo('65%', '60%');
+ let dialog = new globalBackupDialog.BackupDialog(
+ pgBrowser,
+ $,
+ alertify,
+ BackupObjectModel
+ );
+ dialog.draw(action, treeItem, null);
},
};
return pgBrowser.Backup;
diff --git a/web/pgadmin/tools/backup/static/js/backup_dialog.js b/web/pgadmin/tools/backup/static/js/backup_dialog.js
new file mode 100644
index 00000000..c74c376c
--- /dev/null
+++ b/web/pgadmin/tools/backup/static/js/backup_dialog.js
@@ -0,0 +1,69 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../../../../static/js/gettext';
+import Backform from '../../../../static/js/backform.pgadmin';
+import {Dialog} from '../../../../static/js/alertify/dialog';
+
+export class BackupDialog extends Dialog {
+ constructor(pgBrowser, $, alertify, BackupModel, backform = Backform) {
+ super('Backup Error',
+ '<div class=\'backup_dialog\'></div>',
+ pgBrowser, $, alertify, BackupModel, backform);
+ }
+
+ draw(action, aciTreeItem, params) {
+ const serverInformation = this.retrieveAncestorOfTypeServer(aciTreeItem);
+
+ if (!serverInformation) {
+ return;
+ }
+
+ if (!this.hasBinariesConfiguration(serverInformation)) {
+ return;
+ }
+
+ const typeOfDialog = BackupDialog.typeOfDialog(params);
+
+ if (!this.canExecuteOnCurrentDatabase(aciTreeItem)) {
+ return;
+ }
+
+ const dialog = this.createOrGetDialog(BackupDialog.dialogTitle(typeOfDialog),
+ typeOfDialog);
+ dialog(true).resizeTo('60%', '50%');
+ }
+
+ static typeOfDialog(params) {
+ if (params === null) {
+ return 'backup_objects';
+ }
+ let typeOfDialog = 'server';
+ if (!_.isUndefined(params['globals']) && params['globals']) {
+ typeOfDialog = 'globals';
+ }
+ return typeOfDialog;
+ }
+
+ static dialogTitle(typeOfDialog) {
+ if (typeOfDialog === 'backup_objects') {
+ return null;
+ }
+ return ((typeOfDialog === 'globals') ?
+ gettext('Backup Globals...') :
+ gettext('Backup Server...'));
+ }
+
+ dialogName(typeOfDialog) {
+ if (typeOfDialog === 'backup_objects') {
+ return typeOfDialog;
+ }
+ return 'BackupDialog_' + typeOfDialog;
+ }
+}
diff --git a/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js b/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js
new file mode 100644
index 00000000..2cebe3d1
--- /dev/null
+++ b/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js
@@ -0,0 +1,258 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {getTreeNodeHierarchyFromElement} from '../../../../static/js/tree/pgadmin_tree_node';
+import axios from 'axios/index';
+import gettext from '../../../../static/js/gettext';
+import url_for from '../../../../static/js/url_for';
+import _ from 'underscore';
+import {DialogWrapper} from '../../../../static/js/alertify/dialog_wrapper';
+
+export class BackupDialogWrapper extends DialogWrapper {
+ constructor(dialogContainerSelector, dialogTitle, typeOfDialog,
+ jquery, pgBrowser, alertify, dialogModel, backform) {
+ super(dialogContainerSelector, dialogTitle, jquery,
+ pgBrowser, alertify, dialogModel, backform);
+ this.typeOfDialog = typeOfDialog;
+ }
+
+ main(title) {
+ this.set('title', title);
+ }
+
+ setup() {
+ return {
+ buttons: [{
+ text: '',
+ className: 'btn btn-default pull-left fa fa-lg fa-info',
+ attrs: {
+ name: 'object_help',
+ type: 'button',
+ url: 'backup.html',
+ label: gettext('Backup'),
+ },
+ }, {
+ text: '',
+ key: 112,
+ className: 'btn btn-default pull-left fa fa-lg fa-question',
+ attrs: {
+ name: 'dialog_help',
+ type: 'button',
+ label: gettext('Backup'),
+ url: url_for('help.static', {
+ 'filename': 'backup_dialog.html',
+ }),
+ },
+ }, {
+ text: gettext('Backup'),
+ key: 13,
+ className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
+ 'data-btn-name': 'backup',
+ }, {
+ text: gettext('Cancel'),
+ key: 27,
+ className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
+ 'data-btn-name': 'cancel',
+ }],
+ // Set options for dialog
+ options: {
+ title: this.dialogTitle,
+ //disable both padding and overflow control.
+ padding: !1,
+ overflow: !1,
+ model: 0,
+ resizable: true,
+ maximizable: true,
+ pinnable: false,
+ closableByDimmer: false,
+ modal: false,
+ },
+ };
+ }
+
+ prepare() {
+ this.disableBackupButton();
+
+ const $container = this.jquery(this.dialogContainerSelector);
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = this.getSelectedNodeData(selectedTreeNode);
+ if (!selectedTreeNodeData) {
+ return;
+ }
+
+ const node = this.pgBrowser.Nodes[selectedTreeNodeData._type];
+ if (this.dialogTitle === null) {
+ const title = `Backup (${node.label}: ${selectedTreeNodeData.label})`;
+ this.main(title);
+ }
+
+ const treeInfo = getTreeNodeHierarchyFromElement(this.pgBrowser, selectedTreeNode);
+ const dialog = this.createDialog(node, treeInfo, this.typeOfDialog, $container);
+ this.addAlertifyClassToBackupNodeChildNodes();
+ dialog.render();
+
+ this.elements.content.appendChild($container.get(0));
+
+ this.focusOnDialog(dialog);
+ this.setListenersForFilenameChanges();
+ }
+
+ callback(event) {
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = this.getSelectedNodeData(selectedTreeNode);
+ const node = selectedTreeNodeData && this.pgBrowser.Nodes[selectedTreeNodeData._type];
+
+ if (this.wasHelpButtonPressed(event)) {
+ event.cancel = true;
+ this.pgBrowser.showHelp(
+ event.button.element.name,
+ event.button.element.getAttribute('url'),
+ node,
+ selectedTreeNode,
+ event.button.element.getAttribute('label')
+ );
+ return;
+ }
+
+ if (this.wasBackupButtonPressed(event)) {
+
+ if (!selectedTreeNodeData)
+ return;
+
+ const serverIdentifier = this.retrieveServerIdentifier(node, selectedTreeNode);
+
+ const dialog = this;
+ let urlShortcut = 'backup.create_server_job';
+ if (this.typeOfDialog === 'backup_objects') {
+ urlShortcut = 'backup.create_object_job';
+ }
+ const baseUrl = url_for(urlShortcut, {
+ 'sid': serverIdentifier,
+ });
+
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.pgBrowser,
+ selectedTreeNode
+ );
+
+ this.setExtraParameters(selectedTreeNode, treeInfo);
+
+ let service = axios.create({});
+ service.post(
+ baseUrl,
+ this.view.model.toJSON()
+ ).then(function () {
+ dialog.alertify.success(gettext('Backup job created.'), 5);
+ dialog.pgBrowser.Events.trigger('pgadmin-bgprocess:created', dialog);
+ }).catch(function (error) {
+ try {
+ const err = error.response.data;
+ dialog.alertify.alert(
+ gettext('Backup job failed.'),
+ err.errormsg
+ );
+ } catch (e) {
+ console.warn(e.stack || e);
+ }
+ });
+ }
+ }
+
+ addAlertifyClassToBackupNodeChildNodes() {
+ this.jquery(this.elements.body.childNodes[0]).addClass(
+ 'alertify_tools_dialog_properties obj_properties'
+ );
+ }
+
+ getSelectedNode() {
+ const tree = this.pgBrowser.treeMenu;
+ const selectedNode = tree.selected();
+ if (selectedNode) {
+ return tree.findNodeByDomElement(selectedNode);
+ } else {
+ return undefined;
+ }
+ }
+
+ disableBackupButton() {
+ this.__internal.buttons[2].element.disabled = true;
+ }
+
+ enableBackupButton() {
+ this.__internal.buttons[2].element.disabled = false;
+ }
+
+ createDialog(node, treeInfo, typeOfDialog, $container) {
+ let attributes = {};
+ if (typeOfDialog !== 'backup_objects') {
+ attributes['type'] = typeOfDialog;
+ }
+ // Instance of backbone model
+ const newModel = new this.dialogModel(attributes, {
+ node_info: treeInfo,
+ });
+ const fields = this.backform.generateViewSchema(
+ treeInfo, newModel, 'create', node, treeInfo.server, true
+ );
+
+ return this.view = new this.backform.Dialog({
+ el: $container,
+ model: newModel,
+ schema: fields,
+ });
+ }
+
+ retrieveServerIdentifier(node, selectedTreeNode) {
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.pgBrowser,
+ selectedTreeNode
+ );
+ return treeInfo.server._id;
+ }
+
+ setListenersForFilenameChanges() {
+ const self = this;
+
+ this.view.model.on('change', function () {
+ if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
+ this.errorModel.clear();
+ self.enableBackupButton();
+ } else {
+ self.disableBackupButton();
+ this.errorModel.set('file', gettext('Please provide a filename'));
+ }
+ });
+ }
+
+ setExtraParameters(selectedTreeNode, treeInfo) {
+ if (this.typeOfDialog === 'backup_objects') {
+
+ this.view.model.set('database', treeInfo.database._label);
+
+ const nodeData = selectedTreeNode.getData();
+ if (nodeData._type === 'schema') {
+ this.view.model.set('schemas', [nodeData._label]);
+ }
+
+ if (nodeData._type === 'table') {
+ this.view.model.set('tables', [
+ [treeInfo.schema._label, nodeData._label],
+ ]);
+ }
+
+ if (_.isEmpty(this.view.model.get('ratio'))) {
+ this.view.model.unset('ratio');
+ }
+ }
+ }
+
+ wasBackupButtonPressed(event) {
+ return event.button['data-btn-name'] === 'backup';
+ }
+}
diff --git a/web/pgadmin/tools/backup/static/js/menu_utils.js b/web/pgadmin/tools/backup/static/js/menu_utils.js
new file mode 100644
index 00000000..47a9f0d3
--- /dev/null
+++ b/web/pgadmin/tools/backup/static/js/menu_utils.js
@@ -0,0 +1,23 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {isValidTreeNodeData} from '../../../../static/js/tree/tree';
+
+export const backupSupportedNodes = [
+ 'database', 'schema', 'table', 'partition',
+];
+
+function isNodeAServerAndConnected(treeNodeData) {
+ return (('server' === treeNodeData._type) && treeNodeData.connected);
+}
+
+export function menuEnabledServer(treeNodeData) {
+ return isValidTreeNodeData(treeNodeData)
+ && isNodeAServerAndConnected(treeNodeData);
+}
diff --git a/web/pgadmin/tools/datagrid/static/js/datagrid.js b/web/pgadmin/tools/datagrid/static/js/datagrid.js
index b0ed60f6..520a9ce5 100644
--- a/web/pgadmin/tools/datagrid/static/js/datagrid.js
+++ b/web/pgadmin/tools/datagrid/static/js/datagrid.js
@@ -1,10 +1,14 @@
define('pgadmin.datagrid', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'pgadmin.alertifyjs', 'sources/pgadmin', 'bundled_codemirror',
- 'sources/sqleditor_utils', 'backbone', 'wcdocker',
+ 'sources/sqleditor_utils', 'backbone',
+ 'tools/datagrid/static/js/show_data',
+ 'tools/datagrid/static/js/get_panel_title',
+ 'tools/datagrid/static/js/show_query_tool',
+ 'wcdocker',
], function(
gettext, url_for, $, _, alertify, pgAdmin, codemirror, sqlEditorUtils,
- Backbone
+ Backbone, showData, panelTitle, showQueryTool
) {
// Some scripts do export their object in the window only.
// Generally the one, which do no have AMD support.
@@ -161,55 +165,7 @@ define('pgadmin.datagrid', [
// This is a callback function to show data when user click on menu item.
show_data_grid: function(data, i) {
- var self = this,
- d = pgAdmin.Browser.tree.itemData(i);
- if (d === undefined) {
- alertify.alert(
- gettext('Data Grid Error'),
- gettext('No object selected.')
- );
- return;
- }
-
- // Get the parent data from the tree node hierarchy.
- var node = pgBrowser.Nodes[d._type],
- parentData = node.getTreeNodeHierarchy(i);
-
- // If server, database or schema is undefined then return from the function.
- if (parentData.server === undefined || parentData.database === undefined) {
- return;
- }
- // If schema, view, catalog object all are undefined then return from the function.
- if (parentData.schema === undefined && parentData.view === undefined &&
- parentData.catalog === undefined) {
- return;
- }
-
- var nsp_name = '';
-
- if (parentData.schema != undefined) {
- nsp_name = parentData.schema.label;
- }
- else if (parentData.view != undefined) {
- nsp_name = parentData.view.label;
- }
- else if (parentData.catalog != undefined) {
- nsp_name = parentData.catalog.label;
- }
- var url_params = {
- 'cmd_type': data.mnuid,
- 'obj_type': d._type,
- 'sgid': parentData.server_group._id,
- 'sid': parentData.server._id,
- 'did': parentData.database._id,
- 'obj_id': d._id,
- };
-
- var baseUrl = url_for('datagrid.initialize_datagrid', url_params);
- var grid_title = parentData.server.label + ' - ' + parentData.database.label + ' - '
- + nsp_name + '.' + d.label;
-
- self.create_transaction(baseUrl, null, 'false', parentData.server.server_type, '', grid_title, '');
+ showData.showDataGrid(this, pgBrowser, alertify, data, i);
},
// This is a callback function to show filtered data when user click on menu item.
@@ -384,63 +340,12 @@ define('pgadmin.datagrid', [
},
get_panel_title: function() {
- // Get the parent data from the tree node hierarchy.
- var tree = pgAdmin.Browser.tree,
- selected_item = tree.selected(),
- item_data = tree.itemData(selected_item);
-
- var node = pgBrowser.Nodes[item_data._type],
- parentData = node.getTreeNodeHierarchy(selected_item);
-
- // If server, database is undefined then return from the function.
- if (parentData.server === undefined) {
- return;
- }
- // If Database is not available then use default db
- var db_label = parentData.database ? parentData.database.label
- : parentData.server.db;
-
- var grid_title = db_label + ' on ' + parentData.server.user.name + '@' +
- parentData.server.label;
- return grid_title;
+ return panelTitle.getPanelTitle(pgBrowser);
},
// This is a callback function to show query tool when user click on menu item.
- show_query_tool: function(url, i, panel_title) {
- var sURL = url || '',
- d = pgAdmin.Browser.tree.itemData(i);
-
- panel_title = panel_title || '';
- if (d === undefined) {
- alertify.alert(
- gettext('Query Tool Error'),
- gettext('No object selected.')
- );
- return;
- }
-
- // Get the parent data from the tree node hierarchy.
- var node = pgBrowser.Nodes[d._type],
- parentData = node.getTreeNodeHierarchy(i);
-
- // If server, database is undefined then return from the function.
- if (parentData.server === undefined) {
- return;
- }
-
- var url_params = {
- 'sgid': parentData.server_group._id,
- 'sid': parentData.server._id,
- },
- url_endpoint = 'datagrid.initialize_query_tool';
- // If database not present then use Maintenance database
- // We will handle this at server side
- if (parentData.database) {
- url_params['did'] = parentData.database._id;
- url_endpoint = 'datagrid.initialize_query_tool_with_did';
- }
- var baseUrl = url_for(url_endpoint, url_params);
-
- this.create_transaction(baseUrl, null, 'true', parentData.server.server_type, sURL, panel_title, '', false);
+ show_query_tool: function(url, aciTreeIdentifier, panelTitle) {
+ showQueryTool.showQueryTool(this, pgBrowser, alertify, url,
+ aciTreeIdentifier, panelTitle);
},
create_transaction: function(baseUrl, target, is_query_tool, server_type, sURL, panel_title, sql_filter, recreate) {
var self = this;
diff --git a/web/pgadmin/tools/datagrid/static/js/get_panel_title.js b/web/pgadmin/tools/datagrid/static/js/get_panel_title.js
new file mode 100644
index 00000000..64b3a3d2
--- /dev/null
+++ b/web/pgadmin/tools/datagrid/static/js/get_panel_title.js
@@ -0,0 +1,33 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {getTreeNodeHierarchyFromIdentifier} from '../../../../static/js/tree/pgadmin_tree_node';
+
+function getDatabaseLabel(parentData) {
+ return parentData.database ? parentData.database.label
+ : parentData.server.db;
+}
+
+function isServerInformationAvailable(parentData) {
+ return parentData.server === undefined;
+}
+
+export function getPanelTitle(pgBrowser) {
+ const selected_item = pgBrowser.treeMenu.selected();
+
+ const parentData = getTreeNodeHierarchyFromIdentifier
+ .call(pgBrowser, selected_item);
+ if (isServerInformationAvailable(parentData)) {
+ return;
+ }
+
+ const db_label = getDatabaseLabel(parentData);
+
+ return `${db_label} on ${parentData.server.user.name}@${parentData.server.label}`;
+}
diff --git a/web/pgadmin/tools/datagrid/static/js/show_data.js b/web/pgadmin/tools/datagrid/static/js/show_data.js
new file mode 100644
index 00000000..373c97cd
--- /dev/null
+++ b/web/pgadmin/tools/datagrid/static/js/show_data.js
@@ -0,0 +1,92 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import gettext from '../../../../static/js/gettext';
+import url_for from '../../../../static/js/url_for';
+import {getTreeNodeHierarchyFromIdentifier} from '../../../../static/js/tree/pgadmin_tree_node';
+
+export function showDataGrid(
+ datagrid,
+ pgBrowser,
+ alertify,
+ connectionData,
+ aciTreeIdentifier
+) {
+ const node = pgBrowser.treeMenu.findNodeByDomElement(aciTreeIdentifier);
+ if (node === undefined || !node.getData()) {
+ alertify.alert(
+ gettext('Data Grid Error'),
+ gettext('No object selected.')
+ );
+ return;
+ }
+
+ const parentData = getTreeNodeHierarchyFromIdentifier.call(
+ pgBrowser,
+ aciTreeIdentifier
+ );
+
+ if (hasServerOrDatabaseConfiguration(parentData)
+ || !hasSchemaOrCatalogOrViewInformation(parentData)) {
+ return;
+ }
+
+ let namespaceName = retrieveNameSpaceName(parentData);
+ const baseUrl = generateUrl(connectionData, node.getData(), parentData);
+ const grid_title = generateDatagridTitle(parentData, namespaceName, node.getData());
+
+ datagrid.create_transaction(
+ baseUrl,
+ null,
+ 'false',
+ parentData.server.server_type,
+ '',
+ grid_title,
+ ''
+ );
+}
+
+
+function retrieveNameSpaceName(parentData) {
+ if (parentData.schema !== undefined) {
+ return parentData.schema.label;
+ }
+ else if (parentData.view !== undefined) {
+ return parentData.view.label;
+ }
+ else if (parentData.catalog !== undefined) {
+ return parentData.catalog.label;
+ }
+ return '';
+}
+
+function generateUrl(connectionData, nodeData, parentData) {
+ const url_params = {
+ 'cmd_type': connectionData.mnuid,
+ 'obj_type': nodeData._type,
+ 'sgid': parentData.server_group._id,
+ 'sid': parentData.server._id,
+ 'did': parentData.database._id,
+ 'obj_id': nodeData._id,
+ };
+
+ return url_for('datagrid.initialize_datagrid', url_params);
+}
+
+function hasServerOrDatabaseConfiguration(parentData) {
+ return parentData.server === undefined || parentData.database === undefined;
+}
+
+function hasSchemaOrCatalogOrViewInformation(parentData) {
+ return parentData.schema !== undefined || parentData.view !== undefined ||
+ parentData.catalog !== undefined;
+}
+
+function generateDatagridTitle(parentData, namespaceName, nodeData) {
+ return `${parentData.server.label} - ${parentData.database.label} - ${namespaceName}.${nodeData.label}`;
+}
diff --git a/web/pgadmin/tools/datagrid/static/js/show_query_tool.js b/web/pgadmin/tools/datagrid/static/js/show_query_tool.js
new file mode 100644
index 00000000..0eb12b8b
--- /dev/null
+++ b/web/pgadmin/tools/datagrid/static/js/show_query_tool.js
@@ -0,0 +1,63 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../../../../static/js/gettext';
+import url_for from '../../../../static/js/url_for';
+import {getTreeNodeHierarchyFromIdentifier} from '../../../../static/js/tree/pgadmin_tree_node';
+
+function hasDatabaseInformation(parentData) {
+ return parentData.database;
+}
+
+function generateUrl(parentData) {
+ let url_endpoint = 'datagrid.initialize_query_tool';
+ let url_params = {
+ 'sgid': parentData.server_group._id,
+ 'sid': parentData.server._id,
+ };
+
+ if (hasDatabaseInformation(parentData)) {
+ url_params['did'] = parentData.database._id;
+ url_endpoint = 'datagrid.initialize_query_tool_with_did';
+ }
+
+ return url_for(url_endpoint, url_params);
+}
+
+function hasServerInformations(parentData) {
+ return parentData.server === undefined;
+}
+
+export function showQueryTool(datagrid, pgBrowser, alertify, url,
+ aciTreeIdentifier, panelTitle) {
+ const sURL = url || '';
+ const queryToolTitle = panelTitle || '';
+
+ const currentNode = pgBrowser.treeMenu.findNodeByDomElement(aciTreeIdentifier);
+ if (currentNode === undefined) {
+ alertify.alert(
+ gettext('Query Tool Error'),
+ gettext('No object selected.')
+ );
+ return;
+ }
+
+ const parentData = getTreeNodeHierarchyFromIdentifier.call(
+ pgBrowser, aciTreeIdentifier);
+
+ if (hasServerInformations(parentData)) {
+ return;
+ }
+
+ const baseUrl = generateUrl(parentData);
+
+ datagrid.create_transaction(
+ baseUrl, null, 'true',
+ parentData.server.server_type, sURL, queryToolTitle, '', false);
+}
diff --git a/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js b/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js
index 750887ec..1b9e6db5 100644
--- a/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js
+++ b/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js
@@ -2,12 +2,15 @@
define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'pgadmin.alertifyjs', 'pgadmin.backgrid', 'pgadmin.backform',
- 'pgadmin.browser', 'pgadmin.browser.node', 'backgrid.select.all',
+ 'pgadmin.browser', 'pgadmin.browser.node',
+ 'tools/grant_wizard/static/js/menu_utils',
+ 'sources/tree/supported_nodes_menu',
+ 'backgrid.select.all',
'backgrid.filter', 'pgadmin.browser.server.privilege',
'pgadmin.browser.wizard',
], function(
gettext, url_for, $, _, Backbone, Alertify, Backgrid, Backform, pgBrowser,
- pgNode
+ pgNode, menuUtils, supportedNodesMenu
) {
// if module is already initialized, refer to that.
@@ -143,41 +146,6 @@ define([
this.initialized = true;
- // Define list of nodes on which grant wizard context menu option appears
- var supported_nodes = [
- 'schema', 'coll-function', 'coll-sequence',
- 'coll-table', 'coll-view', 'coll-procedure',
- 'coll-mview', 'database', 'coll-trigger_function',
- ],
-
- /**
- Enable/disable grantwizard menu in tools based
- on node selected
- if selected node is present in supported_nodes,
- menu will be enabled otherwise disabled.
- Also, hide it for system view in catalogs
- */
- menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
- var parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(supported_nodes, d._type) !== -1 &&
- parent_data._type != 'catalog') {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
// Define the nodes on which the menus to be appear
var menus = [{
name: 'grant_wizard_schema',
@@ -187,21 +155,25 @@ define([
priority: 14,
label: gettext('Grant Wizard...'),
icon: 'fa fa-unlock-alt',
- enable: menu_enabled,
+ enable: supportedNodesMenu.enabled.bind(
+ null, pgBrowser.treeMenu, menuUtils.supportedNodes
+ ),
}];
// Add supported menus into the menus list
- for (var idx = 0; idx < supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.supportedNodes.length; idx++) {
menus.push({
- name: 'grant_wizard_schema_context_' + supported_nodes[idx],
- node: supported_nodes[idx],
+ name: 'grant_wizard_schema_context_' + menuUtils.supportedNodes[idx],
+ node: menuUtils.supportedNodes[idx],
module: this,
applies: ['context'],
callback: 'start_grant_wizard',
priority: 14,
label: gettext('Grant Wizard...'),
icon: 'fa fa-unlock-alt',
- enable: menu_enabled,
+ enable: supportedNodesMenu.enabled.bind(
+ null, pgBrowser.treeMenu, menuUtils.supportedNodes
+ ),
});
}
pgBrowser.add_menus(menus);
diff --git a/web/pgadmin/tools/grant_wizard/static/js/menu_utils.js b/web/pgadmin/tools/grant_wizard/static/js/menu_utils.js
new file mode 100644
index 00000000..b56ce3cd
--- /dev/null
+++ b/web/pgadmin/tools/grant_wizard/static/js/menu_utils.js
@@ -0,0 +1,16 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export const supportedNodes = [
+ 'schema', 'coll-function', 'coll-sequence',
+ 'coll-table', 'coll-view', 'coll-procedure',
+ 'coll-mview', 'database', 'coll-trigger_function',
+];
+
+
diff --git a/web/pgadmin/tools/import_export/static/js/import_export.js b/web/pgadmin/tools/import_export/static/js/import_export.js
index 3058f122..1ede8536 100644
--- a/web/pgadmin/tools/import_export/static/js/import_export.js
+++ b/web/pgadmin/tools/import_export/static/js/import_export.js
@@ -1,10 +1,12 @@
define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'underscore.string', 'pgadmin.alertifyjs',
'sources/pgadmin', 'pgadmin.browser', 'backbone', 'backgrid', 'backform',
- 'sources/utils', 'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser.node.ui',
+ 'sources/utils',
+ 'sources/tree/supported_nodes_menu',
+ 'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser.node.ui',
], function(
gettext, url_for, $, _, S, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid,
-Backform, commonUtils
+Backform, commonUtils, supportedNodesMenu
) {
pgAdmin = pgAdmin || window.pgAdmin || {};
@@ -383,25 +385,6 @@ Backform, commonUtils
this.initialized = true;
- /*
- * Enable/disable import menu in tools based on node selected. Import
- * menu will be enabled only when user select table node.
- */
- var menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
- var parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data))
- return (
- (_.indexOf(['table'], d._type) !== -1 &&
- parent_data._type != 'catalog') ? true : false
- );
- else
- return false;
- };
-
// Initialize the context menu to display the import options when user open the context menu for table
pgBrowser.add_menus([{
name: 'import',
@@ -413,7 +396,9 @@ Backform, commonUtils
priority: 10,
label: gettext('Import/Export...'),
icon: 'fa fa-shopping-cart',
- enable: menu_enabled,
+ enable: supportedNodesMenu.enabled.bind(
+ null, pgBrowser.treeMenu, ['table']
+ ),
}]);
},
diff --git a/web/pgadmin/tools/maintenance/static/js/maintenance.js b/web/pgadmin/tools/maintenance/static/js/maintenance.js
index f2102602..5f703c6d 100644
--- a/web/pgadmin/tools/maintenance/static/js/maintenance.js
+++ b/web/pgadmin/tools/maintenance/static/js/maintenance.js
@@ -2,11 +2,14 @@ define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'pgadmin.alertifyjs', 'sources/pgadmin', 'pgadmin.browser', 'backbone',
'backgrid', 'backform', 'sources/utils',
+ 'tools/maintenance/static/js/menu_utils',
+ 'sources/tree/supported_nodes_menu',
'pgadmin.backform', 'pgadmin.backgrid',
'pgadmin.browser.node.ui',
], function(
gettext, url_for, $, _, S, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid,
- Backform, commonUtils
+ Backform, commonUtils,
+ menuUtils, supportedNodesMenu
) {
pgAdmin = pgAdmin || window.pgAdmin || {};
@@ -168,36 +171,6 @@ define([
this.initialized = true;
- var maintenance_supported_nodes = [
- 'database', 'table', 'primary_key',
- 'unique_constraint', 'index', 'partition',
- ];
-
- /**
- Enable/disable Maintenance menu in tools based on node selected.
- Maintenance menu will be enabled only when user select table and database node.
- */
- var menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
- var parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(maintenance_supported_nodes, d._type) !== -1 &&
- parent_data._type != 'catalog') {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
var menus = [{
name: 'maintenance',
module: this,
@@ -206,21 +179,25 @@ define([
priority: 10,
label: gettext('Maintenance...'),
icon: 'fa fa-wrench',
- enable: menu_enabled,
+ enable: supportedNodesMenu.enabled.bind(
+ null, pgBrowser.treeMenu, menuUtils.maintenanceSupportedNodes
+ ),
}];
// Add supported menus into the menus list
- for (var idx = 0; idx < maintenance_supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.maintenanceSupportedNodes.length; idx++) {
menus.push({
- name: 'maintenance_context_' + maintenance_supported_nodes[idx],
- node: maintenance_supported_nodes[idx],
+ name: 'maintenance_context_' + menuUtils.maintenanceSupportedNodes[idx],
+ node: menuUtils.maintenanceSupportedNodes[idx],
module: this,
applies: ['context'],
callback: 'callback_maintenance',
priority: 10,
label: gettext('Maintenance...'),
icon: 'fa fa-wrench',
- enable: menu_enabled,
+ enable: supportedNodesMenu.enabled.bind(
+ null, pgBrowser.treeMenu, menuUtils.maintenanceSupportedNodes
+ ),
});
}
pgBrowser.add_menus(menus);
diff --git a/web/pgadmin/tools/maintenance/static/js/menu_utils.js b/web/pgadmin/tools/maintenance/static/js/menu_utils.js
new file mode 100644
index 00000000..8cde1baa
--- /dev/null
+++ b/web/pgadmin/tools/maintenance/static/js/menu_utils.js
@@ -0,0 +1,13 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export const maintenanceSupportedNodes = [
+ 'database', 'table', 'primary_key',
+ 'unique_constraint', 'index', 'partition',
+];
diff --git a/web/pgadmin/tools/restore/static/js/menu_utils.js b/web/pgadmin/tools/restore/static/js/menu_utils.js
new file mode 100644
index 00000000..2d35c951
--- /dev/null
+++ b/web/pgadmin/tools/restore/static/js/menu_utils.js
@@ -0,0 +1,18 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export const restoreSupportedNodes = [
+ 'database',
+ 'schema',
+ 'table',
+ 'function',
+ 'trigger',
+ 'index',
+ 'partition',
+];
diff --git a/web/pgadmin/tools/restore/static/js/restore.js b/web/pgadmin/tools/restore/static/js/restore.js
index 585b9729..bf4a34ca 100644
--- a/web/pgadmin/tools/restore/static/js/restore.js
+++ b/web/pgadmin/tools/restore/static/js/restore.js
@@ -1,11 +1,13 @@
-// Restore dialog
define('tools.restore', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'underscore.string', 'pgadmin.alertifyjs', 'pgadmin.browser',
'pgadmin.backgrid', 'pgadmin.backform', 'sources/utils',
+ 'tools/restore/static/js/menu_utils',
+ 'sources/tree/supported_nodes_menu',
+ 'tools/restore/static/js/restore_dialog',
], function(
gettext, url_for, $, _, Backbone, S, alertify, pgBrowser, Backgrid, Backform,
-commonUtils
+commonUtils, menuUtils, supportedNodesMenu, restoreDialog
) {
// if module is already initialized, refer to that.
@@ -307,59 +309,6 @@ commonUtils
this.initialized = true;
- // Define list of nodes on which restore context menu option appears
- var restore_supported_nodes = [
- 'database', 'schema',
- 'table', 'function',
- 'trigger', 'index',
- 'partition',
- ];
-
- /**
- Enable/disable restore menu in tools based
- on node selected
- if selected node is present in supported_nodes,
- menu will be enabled otherwise disabled.
- Also, hide it for system view in catalogs
- */
- var menu_enabled = function(itemData, item, data) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
- var parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(restore_supported_nodes, d._type) !== -1 &&
- is_parent_catalog(itemData, item, data)) {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
- var is_parent_catalog = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
-
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to restore
- if (_.indexOf(['catalog'], d._type) > -1)
- return false;
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- };
-
// Define the nodes on which the menus to be appear
var menus = [{
name: 'restore_object',
@@ -369,20 +318,24 @@ commonUtils
priority: 13,
label: gettext('Restore...'),
icon: 'fa fa-upload',
- enable: menu_enabled,
+ enable: supportedNodesMenu.enabled.bind(
+ null, pgBrowser.treeMenu, menuUtils.restoreSupportedNodes
+ ),
}];
- for (var idx = 0; idx < restore_supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.restoreSupportedNodes.length; idx++) {
menus.push({
- name: 'restore_' + restore_supported_nodes[idx],
- node: restore_supported_nodes[idx],
+ name: 'restore_' + menuUtils.restoreSupportedNodes[idx],
+ node: menuUtils.restoreSupportedNodes[idx],
module: this,
applies: ['context'],
callback: 'restore_objects',
priority: 13,
label: gettext('Restore...'),
icon: 'fa fa-upload',
- enable: menu_enabled,
+ enable: supportedNodesMenu.enabled.bind(
+ null, pgBrowser.treeMenu, menuUtils.restoreSupportedNodes
+ ),
});
}
@@ -391,318 +344,8 @@ commonUtils
},
// Callback to draw Backup Dialog for objects
restore_objects: function(action, treeItem) {
-
- var i = treeItem || pgBrowser.tree.selected(),
- server_data = null;
-
- while (i) {
- var node_data = pgBrowser.tree.itemData(i);
- if (node_data._type == 'server') {
- server_data = node_data;
- break;
- }
-
- if (pgBrowser.tree.hasParent(i)) {
- i = $(pgBrowser.tree.parent(i));
- } else {
- alertify.alert(
- gettext('Restore Error'),
- gettext('Please select server or child node from tree.')
- );
- break;
- }
- }
-
- if (!server_data) {
- return;
- }
-
- var module = 'paths',
- preference_name = 'pg_bin_dir',
- msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
-
- if ((server_data.type && server_data.type == 'ppas') ||
- server_data.server_type == 'ppas') {
- preference_name = 'ppas_bin_dir';
- msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
- }
-
- var preference = pgBrowser.get_preference(module, preference_name);
-
- if (preference) {
- if (!preference.value) {
- alertify.alert(gettext('Configuration required'), msg);
- return;
- }
- } else {
- alertify.alert(
- gettext('Restore Error'),
- S(gettext('Failed to load preference %s of module %s')).sprintf(preference_name, module).value()
- );
- return;
- }
-
- var title = S(gettext('Restore (%s: %s)')),
- tree = pgBrowser.tree,
- item = treeItem || tree.selected(),
- data = item && item.length == 1 && tree.itemData(item),
- node = data && data._type && pgBrowser.Nodes[data._type];
-
- if (!node)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [item]);
-
- if (treeInfo.database._label.indexOf('=') >= 0) {
- alertify.alert(
- gettext('Restore error'),
- gettext('Restore job creation failed. '+
- 'Databases with = symbols in the name cannot be restored using this utility.')
- );
- return;
- }
-
- title = title.sprintf(node.label, data.label).value();
-
- if (!alertify.pg_restore) {
- // Create Dialog title on the fly with node details
- alertify.dialog('pg_restore', function factory() {
- return {
- main: function(title, item, data, node) {
- this.set('title', title);
- this.setting('pg_node', node);
- this.setting('pg_item', item);
- this.setting('pg_item_data', data);
- },
- build: function() {
- alertify.pgDialogBuild.apply(this);
- },
- setup: function() {
- return {
- buttons: [{
- text: '',
- className: 'btn btn-default pull-left fa fa-lg fa-info',
- attrs: {
- name: 'object_help',
- type: 'button',
- url: 'backup.html',
- label: gettext('Restore'),
- },
- }, {
- text: '',
- key: 112,
- className: 'btn btn-default pull-left fa fa-lg fa-question',
- attrs: {
- name: 'dialog_help',
- type: 'button',
- label: gettext('Restore'),
- url: url_for('help.static', {
- 'filename': 'restore_dialog.html',
- }),
- },
- }, {
- text: gettext('Restore'),
- key: 13,
- className: 'btn btn-primary fa fa-upload pg-alertify-button',
- restore: true,
- 'data-btn-name': 'restore',
- }, {
- text: gettext('Cancel'),
- key: 27,
- className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
- restore: false,
- 'data-btn-name': 'cancel',
- }],
- // Set options for dialog
- options: {
- title: title,
- //disable both padding and overflow control.
- padding: !1,
- overflow: !1,
- model: 0,
- resizable: true,
- maximizable: true,
- pinnable: false,
- closableByDimmer: false,
- modal: false,
- },
- };
- },
- hooks: {
- // triggered when the dialog is closed
- onclose: function() {
- if (this.view) {
- this.view.remove({
- data: true,
- internal: true,
- silent: true,
- });
- }
- },
- },
- settings: {
- pg_node: null,
- pg_item: null,
- pg_item_data: null,
- },
- prepare: function() {
-
- var self = this;
- // Disable Backup button until user provides Filename
- this.__internal.buttons[2].element.disabled = true;
- var $container = $('<div class=\'restore_dialog\'></div>');
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- var newModel = new RestoreObjectModel({
- node_data: node,
- }, {
- node_info: treeInfo,
- }),
- fields = Backform.generateViewSchema(
- treeInfo, newModel, 'create', node, treeInfo.server, true
- );
-
- var view = this.view = new Backform.Dialog({
- el: $container,
- model: newModel,
- schema: fields,
- });
-
- $(this.elements.body.childNodes[0]).addClass(
- 'alertify_tools_dialog_properties obj_properties'
- );
-
- view.render();
-
- this.elements.content.appendChild($container.get(0));
-
- view.$el.attr('tabindex', -1);
- // var dialogTabNavigator = pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
- commonUtils.findAndSetFocus(container);
-
- // Listen to model & if filename is provided then enable Backup button
- this.view.model.on('change', function() {
- if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
- this.errorModel.clear();
- self.__internal.buttons[2].element.disabled = false;
- } else {
- self.__internal.buttons[2].element.disabled = true;
- this.errorModel.set('file', gettext('Please provide filename'));
- }
- });
-
- },
- // Callback functions when click on the buttons of the Alertify dialogs
- callback: function(e) {
- // Fetch current server id
- var t = pgBrowser.tree,
- i = this.settings['pg_item'] || t.selected(),
- d = this.settings['pg_item_data'] || (
- i && i.length == 1 ? t.itemData(i) : undefined
- ),
- node = this.settings['pg_node'] || (
- d && pgBrowser.Nodes[d._type]
- );
-
- if (e.button.element.name == 'dialog_help' || e.button.element.name == 'object_help') {
- e.cancel = true;
- pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
- node, i, e.button.element.getAttribute('label'));
- return;
- }
-
- if (e.button['data-btn-name'] === 'restore') {
- if (!d)
- return;
-
- var info = node.getTreeNodeHierarchy.apply(node, [i]),
- m = this.view.model;
- // Set current node info into model
- m.set('database', info.database._label);
- if (!m.get('custom')) {
- switch (d._type) {
- case 'schema':
- m.set('schemas', [d._label]);
- break;
- case 'table':
- m.set('schemas', [info.schema._label]);
- m.set('tables', [d._label]);
- break;
- case 'function':
- m.set('schemas', [info.schema._label]);
- m.set('functions', [d._label]);
- break;
- case 'index':
- m.set('schemas', [info.schema._label]);
- m.set('indexes', [d._label]);
- break;
- case 'trigger':
- m.set('schemas', [info.schema._label]);
- m.set('triggers', [d._label]);
- break;
- case 'trigger_func':
- m.set('schemas', [info.schema._label]);
- m.set('trigger_funcs', [d._label]);
- break;
- }
- } else {
- // TODO::
- // When we will implement the object selection in the
- // import dialog, we will need to select the objects from
- // the tree selection tab.
- }
-
- var self = this,
- baseUrl = url_for('restore.create_job', {
- 'sid': info.server._id,
- }),
- args = this.view.model.toJSON();
-
- $.ajax({
- url: baseUrl,
- method: 'POST',
- data: {
- 'data': JSON.stringify(args),
- },
- success: function(res) {
- if (res.success) {
- alertify.success(
- gettext('Restore job created.'), 5
- );
- pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
- } else {
- console.warn(res);
- }
- },
- error: function(xhr) {
- try {
- var err = $.parseJSON(xhr.responseText);
- alertify.alert(
- gettext('Restore failed.'),
- err.errormsg
- );
- } catch (e) {
- console.warn(e.stack || e);
- }
- },
- });
- }
- },
- };
- });
- }
-
- alertify.pg_restore(title, item, data, node).resizeTo('65%', '60%');
+ let dialog = new restoreDialog.RestoreDialog(pgBrowser, $, alertify, RestoreObjectModel);
+ dialog.draw(action, treeItem);
},
};
return pgBrowser.Restore;
diff --git a/web/pgadmin/tools/restore/static/js/restore_dialog.js b/web/pgadmin/tools/restore/static/js/restore_dialog.js
new file mode 100644
index 00000000..4884d901
--- /dev/null
+++ b/web/pgadmin/tools/restore/static/js/restore_dialog.js
@@ -0,0 +1,57 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../../../../static/js/gettext';
+import {sprintf} from 'sprintf-js';
+import Backform from '../../../../static/js/backform.pgadmin';
+import {Dialog} from '../../../../static/js/alertify/dialog';
+
+export class RestoreDialog extends Dialog {
+ constructor(pgBrowser, $, alertify, RestoreModel, backform = Backform) {
+ super('Restore Error',
+ '<div class=\'restore_dialog\'></div>',
+ pgBrowser, $, alertify, RestoreModel, backform);
+ }
+
+ draw(action, aciTreeItem) {
+
+ const serverInformation = this.retrieveAncestorOfTypeServer(aciTreeItem);
+
+ if (!serverInformation) {
+ return;
+ }
+
+ if (!this.hasBinariesConfiguration(serverInformation)) {
+ return;
+ }
+
+ if (!this.canExecuteOnCurrentDatabase(aciTreeItem)) {
+ return;
+ }
+
+ let aciTreeItem1 = aciTreeItem || this.pgBrowser.treeMenu.selected();
+ let item = this.pgBrowser.treeMenu.findNodeByDomElement(aciTreeItem1);
+ const data = item.getData();
+ const node = this.pgBrowser.Nodes[data._type];
+
+ if (!node)
+ return;
+
+ let title = sprintf(gettext('Restore (%s: %s)'), node.label, data.label);
+
+ this.createOrGetDialog(title, 'restore');
+
+ this.alertify.pg_restore(title, aciTreeItem1, data, node).resizeTo('65%', '60%');
+ }
+
+ dialogName() {
+ return 'pg_restore';
+ }
+}
+
diff --git a/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js b/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js
new file mode 100644
index 00000000..845da7a3
--- /dev/null
+++ b/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js
@@ -0,0 +1,255 @@
+import {getTreeNodeHierarchyFromElement} from '../../../../static/js/tree/pgadmin_tree_node';
+import axios from 'axios/index';
+import _ from 'underscore';
+import gettext from '../../../../static/js/gettext';
+import url_for from '../../../../static/js/url_for';
+import {DialogWrapper} from '../../../../static/js/alertify/dialog_wrapper';
+
+export class RestoreDialogWrapper extends DialogWrapper {
+ constructor(dialogContainerSelector, dialogTitle, typeOfDialog,
+ jquery, pgBrowser, alertify, dialogModel, backform) {
+ super(dialogContainerSelector, dialogTitle, jquery,
+ pgBrowser, alertify, dialogModel, backform);
+ }
+
+ main(title, item, data, node) {
+ this.set('title', title);
+ this.setting('pg_node', node);
+ this.setting('pg_item', item);
+ this.setting('pg_item_data', data);
+ }
+
+ setup() {
+ return {
+ buttons: [{
+ text: '',
+ className: 'btn btn-default pull-left fa fa-lg fa-info',
+ attrs: {
+ name: 'object_help',
+ type: 'button',
+ url: 'backup.html',
+ label: gettext('Restore'),
+ },
+ }, {
+ text: '',
+ key: 112,
+ className: 'btn btn-default pull-left fa fa-lg fa-question',
+ attrs: {
+ name: 'dialog_help',
+ type: 'button',
+ label: gettext('Restore'),
+ url: url_for('help.static', {
+ 'filename': 'restore_dialog.html',
+ }),
+ },
+ }, {
+ text: gettext('Restore'),
+ key: 13,
+ className: 'btn btn-primary fa fa-upload pg-alertify-button',
+ restore: true,
+ 'data-btn-name': 'restore',
+ }, {
+ text: gettext('Cancel'),
+ key: 27,
+ className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
+ restore: false,
+ 'data-btn-name': 'cancel',
+ }],
+ // Set options for dialog
+ options: {
+ title: this.dialogTitle,
+ //disable both padding and overflow control.
+ padding: !1,
+ overflow: !1,
+ model: 0,
+ resizable: true,
+ maximizable: true,
+ pinnable: false,
+ closableByDimmer: false,
+ modal: false,
+ },
+ };
+ }
+
+ prepare() {
+ this.disableRestoreButton();
+
+ const $container = this.jquery(this.dialogContainerSelector);
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = this.getSelectedNodeData(selectedTreeNode);
+ if (!selectedTreeNodeData) {
+ return;
+ }
+
+ const node = this.pgBrowser.Nodes[selectedTreeNodeData._type];
+
+ const treeInfo = getTreeNodeHierarchyFromElement(this.pgBrowser, selectedTreeNode);
+ const dialog = this.createDialog(node, treeInfo, $container);
+ this.addAlertifyClassToRestoreNodeChildNodes();
+ dialog.render();
+
+ this.elements.content.appendChild($container.get(0));
+
+ this.focusOnDialog(dialog);
+ this.setListenersForFilenameChanges();
+ }
+
+ callback(event) {
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = this.getSelectedNodeData(selectedTreeNode);
+ const node = selectedTreeNodeData && this.pgBrowser.Nodes[selectedTreeNodeData._type];
+
+ if (this.wasHelpButtonPressed(event)) {
+ event.cancel = true;
+ this.pgBrowser.showHelp(
+ event.button.element.name,
+ event.button.element.getAttribute('url'),
+ node,
+ selectedTreeNode,
+ event.button.element.getAttribute('label')
+ );
+ return;
+ }
+
+ if (this.wasRestoreButtonPressed(event)) {
+
+ if (!selectedTreeNodeData)
+ return;
+
+ const serverIdentifier = this.retrieveServerIdentifier(node, selectedTreeNode);
+
+ const dialogWrapper = this;
+ let urlShortcut = 'restore.create_job';
+
+ const baseUrl = url_for(urlShortcut, {
+ 'sid': serverIdentifier,
+ });
+
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.pgBrowser,
+ selectedTreeNode
+ );
+
+ this.setExtraParameters(selectedTreeNode, treeInfo);
+
+ let service = axios.create({});
+ service.post(
+ baseUrl,
+ this.view.model.toJSON()
+ ).then(function () {
+ dialogWrapper.alertify.success(gettext('Restore job created.'), 5);
+ dialogWrapper.pgBrowser.Events.trigger('pgadmin-bgprocess:created', dialogWrapper);
+ }).catch(function (error) {
+ try {
+ const err = error.response.data;
+ dialogWrapper.alertify.alert(
+ gettext('Restore job failed.'),
+ err.errormsg
+ );
+ } catch (e) {
+ console.warn(e.stack || e);
+ }
+ });
+ }
+ }
+
+ addAlertifyClassToRestoreNodeChildNodes() {
+ this.jquery(this.elements.body.childNodes[0]).addClass(
+ 'alertify_tools_dialog_properties obj_properties'
+ );
+ }
+
+ getSelectedNode() {
+ const tree = this.pgBrowser.treeMenu;
+ const selectedNode = tree.selected();
+ if (selectedNode) {
+ return tree.findNodeByDomElement(selectedNode);
+ } else {
+ return undefined;
+ }
+ }
+
+ disableRestoreButton() {
+ this.__internal.buttons[2].element.disabled = true;
+ }
+
+ enableRestoreButton() {
+ this.__internal.buttons[2].element.disabled = false;
+ }
+
+ createDialog(node, treeInfo, $container) {
+ const newModel = new this.dialogModel({
+ node_data: node,
+ }, {
+ node_info: treeInfo,
+ });
+ const fields = this.backform.generateViewSchema(
+ treeInfo, newModel, 'create', node, treeInfo.server, true
+ );
+
+ return this.view = new this.backform.Dialog({
+ el: $container,
+ model: newModel,
+ schema: fields,
+ });
+ }
+
+ retrieveServerIdentifier(node, selectedTreeNode) {
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.pgBrowser,
+ selectedTreeNode
+ );
+ return treeInfo.server._id;
+ }
+
+ setListenersForFilenameChanges() {
+ const self = this;
+
+ this.view.model.on('change', function () {
+ if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
+ this.errorModel.clear();
+ self.enableRestoreButton();
+ } else {
+ self.disableRestoreButton();
+ this.errorModel.set('file', gettext('Please provide a filename'));
+ }
+ });
+ }
+
+ setExtraParameters(selectedTreeNode, treeInfo) {
+ this.view.model.set('database', treeInfo.database._label);
+ if (!this.view.model.get('custom')) {
+ const nodeData = selectedTreeNode.getData();
+
+ switch (nodeData._type) {
+ case 'schema':
+ this.view.model.set('schemas', [nodeData._label]);
+ break;
+ case 'table':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('tables', [nodeData._label]);
+ break;
+ case 'function':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('functions', [nodeData._label]);
+ break;
+ case 'index':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('indexes', [nodeData._label]);
+ break;
+ case 'trigger':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('triggers', [nodeData._label]);
+ break;
+ case 'trigger_func':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('trigger_funcs', [nodeData._label]);
+ break;
+ }
+ }
+ }
+
+ wasRestoreButtonPressed(event) {
+ return event.button['data-btn-name'] === 'restore';
+ }
+}
diff --git a/web/regression/javascript/backup/backup_dialog_spec.js b/web/regression/javascript/backup/backup_dialog_spec.js
new file mode 100644
index 00000000..2655059d
--- /dev/null
+++ b/web/regression/javascript/backup/backup_dialog_spec.js
@@ -0,0 +1,205 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {BackupDialog} from '../../../pgadmin/tools/backup/static/js/backup_dialog';
+import {TreeFake} from '../tree/tree_fake';
+
+const context = describe;
+
+describe('BackupDialog', () => {
+ let backupDialog;
+ let pgBrowser;
+ let jquerySpy;
+ let alertifySpy;
+ let backupModelSpy;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: {
+ hasId: true,
+ label: 'server',
+ getTreeNodeHierarchy: jasmine.createSpy('server.getTreeNodeHierarchy'),
+ },
+ database: {
+ hasId: true,
+ label: 'database',
+ getTreeNodeHierarchy: jasmine.createSpy('db.getTreeNodeHierarchy'),
+ },
+ schema: {
+ hasId: true,
+ label: 'schema',
+ getTreeNodeHierarchy: jasmine.createSpy('db.getTreeNodeHierarchy'),
+ },
+ },
+ };
+ pgBrowser.Nodes.server.hasId = true;
+ pgBrowser.Nodes.database.hasId = true;
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ backupModelSpy = jasmine.createSpy('backupModelSpy');
+
+ const hierarchy = {
+ children: [
+ {
+ id: 'root',
+ children: [
+ {
+ id: 'serverTreeNode',
+ data: {
+ _id: 10,
+ _type: 'server',
+ },
+ children: [
+ {
+ id: 'some_database',
+ data: {
+ _type: 'database',
+ _id: 11,
+ label: 'some_database',
+ _label: 'some_database_label',
+ },
+ }, {
+ id: 'database_with_equal_in_name',
+ data: {
+ _type: 'database',
+ label: 'some_database',
+ _label: '=some_database_label',
+ },
+ },
+ ],
+ },
+ {
+ id: 'ppasServer',
+ data: {
+ _type: 'server',
+ server_type: 'ppas',
+ children: [
+ {id: 'someNodeUnderneathPPASServer'},
+ ],
+ },
+ },
+ ],
+ },
+ ],
+ };
+
+ pgBrowser.treeMenu = TreeFake.build(hierarchy);
+ });
+
+ describe('#draw', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ alertifySpy['backup_objects'] = jasmine.createSpy('backup_objects');
+ backupDialog = new BackupDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ backupModelSpy
+ );
+
+ pgBrowser.get_preference = jasmine.createSpy('get_preferences');
+ });
+
+ context('there are no ancestors of the type server', () => {
+ it('does not create a dialog', () => {
+ pgBrowser.treeMenu.selectNode([{id: 'root'}]);
+ backupDialog.draw(null, null, null);
+ expect(alertifySpy['backup_objects']).not.toHaveBeenCalled();
+ });
+
+ it('display an alert with a Backup Error', () => {
+ backupDialog.draw(null, [{id: 'root'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Please select server or child node from the browser tree.'
+ );
+ });
+ });
+
+ context('there is an ancestor of the type server', () => {
+ context('no preference can be found', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue(undefined);
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [{id: 'some_database'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference pg_bin_dir of module paths'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [{id: 'ppasServer'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference ppas_bin_dir of module paths'
+ );
+ });
+ });
+ });
+
+ context('preference can be found', () => {
+ context('binary folder is not configured', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue({});
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [{id: 'serverTreeNode'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the PostgreSQL Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [{id: 'ppasServer'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the EDB Advanced Server Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+ });
+
+ context('binary folder is configured', () => {
+ let backupDialogResizeToSpy;
+ beforeEach(() => {
+ backupDialogResizeToSpy = jasmine.createSpyObj('backupDialogResizeToSpy', ['resizeTo']);
+ alertifySpy['backup_objects'].and
+ .returnValue(backupDialogResizeToSpy);
+ pgBrowser.get_preference.and.returnValue({value: '/some/path'});
+ });
+
+ it('displays the dialog', () => {
+ backupDialog.draw(null, [{id: 'serverTreeNode'}], null);
+ expect(alertifySpy['backup_objects']).toHaveBeenCalledWith(true);
+ expect(backupDialogResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
+ });
+
+ context('database label contain "="', () => {
+ it('should create alert dialog with backup error', () => {
+ backupDialog.draw(null, [{id: 'database_with_equal_in_name'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith('Backup Error',
+ 'Databases with = symbols in the name cannot be backed up or restored using this utility.');
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/backup/backup_dialog_wrapper_spec.js b/web/regression/javascript/backup/backup_dialog_wrapper_spec.js
new file mode 100644
index 00000000..58705318
--- /dev/null
+++ b/web/regression/javascript/backup/backup_dialog_wrapper_spec.js
@@ -0,0 +1,675 @@
+import {TreeFake} from '../tree/tree_fake';
+import {BackupDialogWrapper} from '../../../pgadmin/tools/backup/static/js/backup_dialog_wrapper';
+import axios from 'axios/index';
+import MockAdapter from 'axios-mock-adapter';
+import {FakeModel} from '../fake_model';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+let context = describe;
+
+describe('BackupDialogWrapper', () => {
+ let jquerySpy;
+ let pgBrowser;
+ let alertifySpy;
+ let dialogModelKlassSpy;
+ let backform;
+ let generatedBackupModel;
+ let backupDialogWrapper;
+ let noDataNode;
+ let serverTreeNode;
+ let databaseTreeNode;
+ let viewSchema;
+ let backupJQueryContainerSpy;
+ let backupNodeChildNodeSpy;
+ let backupNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: {
+ hasId: true,
+ getTreeNodeHierarchy: jasmine.createSpy('getTreeNodeHierarchy'),
+ },
+ database: {
+ hasId: true,
+ },
+ },
+ keyboardNavigation: jasmine.createSpyObj('keyboardNavigation', ['getDialogTabNavigator']),
+ };
+ noDataNode = pgBrowser.treeMenu.addNewNode('level1.1', undefined, [{id: 'level1'}]);
+ serverTreeNode = pgBrowser.treeMenu.addNewNode('level2.1', {
+ _type: 'server',
+ _id: 10,
+ label: 'some-tree-label',
+ }, [{id: 'level2.1'}]);
+ databaseTreeNode = new TreeNode('database-tree-node', {
+ _type: 'database',
+ _label: 'some-database-label',
+ }, [{id: 'database-tree-node'}]);
+ pgBrowser.treeMenu.addChild(serverTreeNode, databaseTreeNode);
+
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ backupNode = {
+ __internal: {
+ buttons: [{}, {}, {
+ element: {
+ disabled: false,
+ },
+ }],
+ },
+ elements: {
+ body: {
+ childNodes: [
+ {},
+ ],
+ },
+ content: jasmine.createSpyObj('content', ['appendChild', 'attr']),
+ },
+ };
+
+ backupJQueryContainerSpy = jasmine.createSpyObj('backupJQueryContainer', ['get', 'attr']);
+ backupJQueryContainerSpy.get.and.returnValue(backupJQueryContainerSpy);
+
+ generatedBackupModel = {};
+ dialogModelKlassSpy = jasmine.createSpy('dialogModelKlass');
+ dialogModelKlassSpy.and.returnValue(generatedBackupModel);
+
+ viewSchema = {};
+ backform = jasmine.createSpyObj('backform', ['generateViewSchema', 'Dialog']);
+ backform.generateViewSchema.and.returnValue(viewSchema);
+
+ backupNodeChildNodeSpy = jasmine.createSpyObj('something', ['addClass']);
+ jquerySpy.and.callFake((selector) => {
+ if (selector === '<div class=\'backup_dialog\'></div>') {
+ return backupJQueryContainerSpy;
+ } else if (selector === backupNode.elements.body.childNodes[0]) {
+ return backupNodeChildNodeSpy;
+ }
+ });
+
+ });
+
+ describe('#prepare', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+ backupDialogWrapper = Object.assign(backupDialogWrapper, backupNode);
+ });
+
+ context('no tree element is selected', () => {
+ it('does not create a backform dialog', () => {
+ backupDialogWrapper.prepare();
+ expect(backform.Dialog).not.toHaveBeenCalled();
+ });
+
+ it('disables the button "submit button" until a filename is selected', () => {
+ backupDialogWrapper.prepare();
+ expect(backupDialogWrapper.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('selected tree node has no data', () => {
+ beforeEach(() => {
+ pgBrowser.treeMenu.selectNode(noDataNode.domNode);
+ });
+
+ it('does not create a backform dialog', () => {
+ backupDialogWrapper.prepare();
+ expect(backform.Dialog).not.toHaveBeenCalled();
+ });
+
+ it('disables the button "submit button" until a filename is selected', () => {
+ backupDialogWrapper.prepare();
+ expect(backupDialogWrapper.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('tree element is selected', () => {
+ let treeHierarchyInformation;
+ let dialogSpy;
+
+ beforeEach(() => {
+ treeHierarchyInformation = {
+ server: {
+ _type: 'server',
+ _id: 10,
+ priority: 0,
+ label: 'some-tree-label',
+ },
+ };
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ pgBrowser.Nodes['server'].getTreeNodeHierarchy.and
+ .returnValue(treeHierarchyInformation);
+ dialogSpy = jasmine.createSpyObj('newView', ['render']);
+ dialogSpy.$el = jasmine.createSpyObj('$el', ['find', 'attr']);
+ dialogSpy.model = jasmine.createSpyObj('model', ['on']);
+ dialogSpy.$el.find.and.returnValue([]);
+
+ backform.Dialog.and.returnValue(dialogSpy);
+ });
+
+ it('creates a backform dialog and displays it', () => {
+ backupDialogWrapper.prepare();
+ expect(backform.Dialog).toHaveBeenCalledWith({
+ el: backupJQueryContainerSpy,
+ model: generatedBackupModel,
+ schema: viewSchema,
+ });
+
+ expect(dialogSpy.render).toHaveBeenCalled();
+ });
+
+
+ it('add alertify classes to restore node childnode', () => {
+ backupDialogWrapper.prepare();
+ expect(backupNodeChildNodeSpy.addClass)
+ .toHaveBeenCalledWith('alertify_tools_dialog_properties obj_properties');
+ });
+
+ it('disables the button submit button until a filename is selected', () => {
+ backupDialogWrapper.prepare();
+ expect(backupNode.__internal.buttons[2].element.disabled).toBe(true);
+ });
+
+ it('generates a new backup model', () => {
+ backupDialogWrapper.prepare();
+ expect(dialogModelKlassSpy).toHaveBeenCalledWith(
+ {type: 'backup'},
+ {node_info: treeHierarchyInformation}
+ );
+ });
+
+ it('add the new dialog to the backup node HTML', () => {
+ backupDialogWrapper.prepare();
+ expect(backupNode.elements.content.appendChild).toHaveBeenCalledWith(backupJQueryContainerSpy);
+ });
+ });
+ });
+
+ describe('onButtonClicked', () => {
+ let networkMock;
+ beforeEach(() => {
+ networkMock = new MockAdapter(axios);
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ backupDialogWrapper = Object.assign(backupDialogWrapper, backupNode);
+ });
+
+ afterEach(() => {
+ networkMock.restore();
+ });
+
+ context('dialog help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.*/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ pgBrowser.showHelp = jasmine.createSpy('showHelp');
+
+ const event = {
+ button: {
+ element: {
+ name: 'dialog_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ },
+ },
+ },
+ };
+ backupDialogWrapper.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'dialog_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ );
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('object help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.*/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ pgBrowser.showHelp = jasmine.createSpy('showHelp');
+
+ const event = {
+ button: {
+ element: {
+ name: 'object_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ },
+ },
+ },
+ };
+ backupDialogWrapper.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'object_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ );
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('backup button was pressed', () => {
+ context('no tree node is selected', () => {
+ it('does not start the backup', () => {
+ let networkCalled = false;
+ networkMock.onAny(/.*/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ let event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+
+ backupDialogWrapper.callback(event);
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('tree node has no data', () => {
+ it('does not start the backup', () => {
+ pgBrowser.treeMenu.selectNode(noDataNode.domNode);
+
+ let networkCalled = false;
+ networkMock.onAny(/.*/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ let event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+
+ backupDialogWrapper.callback(event);
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('tree node has data', () => {
+ context('when dialog type is global', () => {
+ let event;
+ beforeEach(() => {
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+
+ backupDialogWrapper.view = {
+ model: new FakeModel(),
+ };
+
+ event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ });
+
+ context('when the backup job is created successfully', () => {
+ let dataSentToServer;
+ beforeEach(() => {
+ pgBrowser.Events = jasmine.createSpyObj('Events', ['trigger']);
+ alertifySpy.success = jasmine.createSpy('success');
+
+ networkMock.onPost('/backup/job/10').reply((request) => {
+ dataSentToServer = request.data;
+ return [200, {}];
+ });
+ });
+
+ it('creates a success alert box', (done) => {
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.success).toHaveBeenCalledWith(
+ 'Backup job created.',
+ 5
+ );
+ done();
+ }, 0);
+ });
+
+ it('trigger an event to background process', (done) => {
+ backupDialogWrapper.callback(event);
+
+ setTimeout(() => {
+ expect(pgBrowser.Events.trigger).toHaveBeenCalledWith(
+ 'pgadmin-bgprocess:created',
+ backupDialogWrapper
+ );
+ done();
+ }, 0);
+ });
+
+ it('send the correct paramenters to the backend', (done) => {
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(JSON.parse(dataSentToServer)).toEqual(
+ {}
+ );
+ done();
+ }, 0);
+ });
+ });
+
+ context('when creating backup job fails', () => {
+ it('creates an alert box', (done) => {
+ alertifySpy.alert = jasmine.createSpy('alert');
+ networkMock.onPost('/backup/job/10').reply(() => {
+ return [400, {
+ errormsg: 'some-error-message',
+ }];
+ });
+
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup job failed.',
+ 'some-error-message'
+ );
+ done();
+ }, 0);
+
+ });
+ });
+ });
+
+ context('when dialog type is object', () => {
+ let event;
+ beforeEach(() => {
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup_objects',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ pgBrowser.treeMenu.selectNode(databaseTreeNode.domNode);
+
+ backupDialogWrapper.view = {
+ model: new FakeModel(),
+ };
+
+ event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ });
+
+ context('when the backup job is created successfully', () => {
+ let dataSentToServer;
+ beforeEach(() => {
+ pgBrowser.Events = jasmine.createSpyObj('Events', ['trigger']);
+ alertifySpy.success = jasmine.createSpy('success');
+
+ networkMock.onPost('/backup/job/10/object').reply((request) => {
+ dataSentToServer = request.data;
+ return [200, {}];
+ });
+ });
+
+ it('creates a success alert box', (done) => {
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.success).toHaveBeenCalledWith(
+ 'Backup job created.',
+ 5
+ );
+ done();
+ }, 0);
+ });
+
+ it('trigger an event to background process', (done) => {
+ backupDialogWrapper.callback(event);
+
+ setTimeout(() => {
+ expect(pgBrowser.Events.trigger).toHaveBeenCalledWith(
+ 'pgadmin-bgprocess:created',
+ backupDialogWrapper
+ );
+ done();
+ }, 0);
+ });
+
+ it('send the correct parameters to the backend', (done) => {
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(JSON.parse(dataSentToServer)).toEqual(
+ {database: 'some-database-label'}
+ );
+ done();
+ }, 0);
+ });
+ });
+
+ context('when creating backup job fails', () => {
+ it('creates an alert box', (done) => {
+ alertifySpy.alert = jasmine.createSpy('alert');
+ networkMock.onPost('/backup/job/10/object').reply(() => {
+ return [400, {
+ errormsg: 'some-error-message',
+ }];
+ });
+
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup job failed.',
+ 'some-error-message'
+ );
+ done();
+ }, 0);
+ });
+ });
+ });
+ });
+ });
+ });
+
+ describe('#setExtraParameters', () => {
+ let selectedTreeNode;
+ let treeInfo;
+ let model;
+
+ context('when dialog type is global', () => {
+ beforeEach(() => {
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ treeInfo = {};
+ model = new FakeModel();
+ backupDialogWrapper.view = {
+ model: model,
+ };
+ });
+
+
+ it('sets nothing on the view model', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.toJSON()).toEqual({});
+ });
+ });
+
+ context('when dialog type is object', () => {
+ beforeEach(() => {
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup_objects',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ treeInfo = {
+ database: {
+ _label: 'some-database-label',
+ },
+ schema: {
+ _label: 'some-treeinfo-label',
+ },
+ };
+
+ model = new FakeModel();
+ selectedTreeNode = new TreeNode('some-selected-node',
+ {_type: 'some-type', _label: 'some-selected-label'},
+ []);
+ backupDialogWrapper.view = {
+ model: model,
+ };
+ });
+
+ it('sets the database label on the model', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ 'database': 'some-database-label',
+ });
+ });
+
+ context('when the selected is a schema type', () => {
+ beforeEach(() => {
+ selectedTreeNode = new TreeNode('some-selected-node',
+ {_type: 'schema', _label: 'some-schema-label'},
+ []);
+ });
+
+ it('sets the schema label on the model', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ 'database': 'some-database-label',
+ 'schemas': ['some-schema-label'],
+ });
+ });
+ });
+
+ context('when the selected is a table type', () => {
+ beforeEach(() => {
+ selectedTreeNode = new TreeNode('some-selected-node',
+ {_type: 'table', _label: 'some-table-label'},
+ []);
+ });
+
+ it('sets the schema label on the model', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ 'database': 'some-database-label',
+ 'tables': [['some-treeinfo-label', 'some-table-label']],
+ });
+ });
+ });
+
+ context('when the model has no ratio value', () => {
+ beforeEach(() => {
+ model.set('ratio', '');
+ });
+
+ it('sets clears the ratio value', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.get('ratio')).toBeUndefined();
+ });
+ });
+
+ context('when the model has a valid ratio value', () => {
+ beforeEach(() => {
+ model.set('ratio', '0.25');
+ });
+
+ it('sets clears the ratio value', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.get('ratio')).toEqual('0.25');
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/backup/global_server_backup_dialog_spec.js b/web/regression/javascript/backup/global_server_backup_dialog_spec.js
new file mode 100644
index 00000000..86df672e
--- /dev/null
+++ b/web/regression/javascript/backup/global_server_backup_dialog_spec.js
@@ -0,0 +1,168 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {BackupDialog} from '../../../pgadmin/tools/backup/static/js/backup_dialog';
+import {TreeFake} from '../tree/tree_fake';
+
+const context = describe;
+
+describe('GlobalServerBackupDialog', () => {
+ let backupDialog;
+ let pgBrowser;
+ let jquerySpy;
+ let alertifySpy;
+ let backupModelSpy;
+
+
+ let rootNode;
+ let serverTreeNode;
+ let ppasServerTreeNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: jasmine.createSpyObj('Node[server]', ['getTreeNodeHierarchy']),
+ },
+ };
+ pgBrowser.Nodes.server.hasId = true;
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ backupModelSpy = jasmine.createSpy('backupModelSpy');
+
+ rootNode = pgBrowser.treeMenu.addNewNode('level1', {}, undefined, []);
+ serverTreeNode = pgBrowser.treeMenu.addNewNode('level1.1', {
+ _type: 'server',
+ _id: 10,
+ }, undefined, ['level1']);
+ ppasServerTreeNode = pgBrowser.treeMenu.addNewNode('level1.2', {
+ _type: 'server',
+ server_type: 'ppas',
+ }, undefined, ['level1']);
+ pgBrowser.treeMenu.addNewNode('level3', {}, undefined, ['level1', 'level1.2']);
+ pgBrowser.treeMenu.addNewNode('level3.1', undefined, undefined, ['level1', 'level1.2', 'level3']);
+ });
+
+ describe('#draw', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ alertifySpy['BackupDialog_globals'] = jasmine.createSpy('BackupDialog_globals');
+ alertifySpy['BackupDialog_server'] = jasmine.createSpy('BackupDialog_server');
+ backupDialog = new BackupDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ backupModelSpy
+ );
+
+ pgBrowser.get_preference = jasmine.createSpy('get_preferences');
+ });
+
+ context('there are no ancestors of the type server', () => {
+ it('does not create a dialog', () => {
+ pgBrowser.treeMenu.selectNode([{id: 'level1'}]);
+ backupDialog.draw(null, null, null);
+ expect(alertifySpy['BackupDialog_globals']).not.toHaveBeenCalled();
+ expect(alertifySpy['BackupDialog_server']).not.toHaveBeenCalled();
+ });
+
+ it('display an alert with a Backup Error', () => {
+ backupDialog.draw(null, [rootNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Please select server or child node from the browser tree.'
+ );
+ });
+ });
+
+ context('there is an ancestor of the type server', () => {
+ context('no preference can be found', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue(undefined);
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference pg_bin_dir of module paths'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference ppas_bin_dir of module paths'
+ );
+ });
+ });
+ });
+
+ context('preference can be found', () => {
+ context('binary folder is not configured', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue({});
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the PostgreSQL Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the EDB Advanced Server Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+ });
+
+ context('binary folder is configured', () => {
+ let globalResizeToSpy;
+ let serverResizeToSpy;
+ beforeEach(() => {
+ globalResizeToSpy = jasmine.createSpyObj('globals', ['resizeTo']);
+ alertifySpy['BackupDialog_globals'].and
+ .returnValue(globalResizeToSpy);
+ serverResizeToSpy = jasmine.createSpyObj('server', ['resizeTo']);
+ alertifySpy['BackupDialog_server'].and
+ .returnValue(serverResizeToSpy);
+ pgBrowser.get_preference.and.returnValue({value: '/some/path'});
+ });
+
+ context('dialog for global backup', () => {
+ it('displays the dialog', () => {
+ backupDialog.draw(null, [serverTreeNode], {globals: true});
+ expect(alertifySpy['BackupDialog_globals']).toHaveBeenCalledWith(true);
+ expect(globalResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
+ });
+ });
+
+ context('dialog for server backup', () => {
+ it('displays the dialog', () => {
+ backupDialog.draw(null, [serverTreeNode], {server: true});
+ expect(alertifySpy['BackupDialog_server']).toHaveBeenCalledWith(true);
+ expect(serverResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/backup/menu_utils_spec.js b/web/regression/javascript/backup/menu_utils_spec.js
new file mode 100644
index 00000000..9435d699
--- /dev/null
+++ b/web/regression/javascript/backup/menu_utils_spec.js
@@ -0,0 +1,55 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+
+import {menuEnabledServer} from '../../../pgadmin/tools/backup/static/js/menu_utils';
+
+const context = describe;
+
+describe('backup.menuUtils', () => {
+ describe('#menuEnabledServer', () => {
+ context('provided node data is undefined', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer(undefined)).toBe(false);
+ });
+ });
+
+ context('provided node data is null', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer(null)).toBe(false);
+ });
+ });
+
+ context('current node type is not of the type server', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer({_type: 'schema'})).toBe(false);
+ });
+ });
+
+ context('current node type is of the type server', () => {
+ context('is connected', () => {
+ it('returns true', () => {
+ expect(menuEnabledServer({
+ _type: 'server',
+ connected: true,
+ })).toBe(true);
+ });
+ });
+ context('is not connected', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer({
+ _type: 'server',
+ connected: false,
+ })).toBe(false);
+ });
+ });
+ });
+ });
+});
+
diff --git a/web/regression/javascript/common_keyboard_shortcuts_spec.js b/web/regression/javascript/common_keyboard_shortcuts_spec.js
index 9ea31efd..e27929bf 100644
--- a/web/regression/javascript/common_keyboard_shortcuts_spec.js
+++ b/web/regression/javascript/common_keyboard_shortcuts_spec.js
@@ -11,10 +11,6 @@ import keyboardShortcuts from 'sources/keyboard_shortcuts';
describe('the keyboard shortcuts', () => {
const F1_KEY = 112;
- // const EDIT_KEY = 71; // Key: G -> Grid values
- // const LEFT_ARROW_KEY = 37;
- // const RIGHT_ARROW_KEY = 39;
- // const MOVE_NEXT = 'right';
let debuggerElementSpy, event, debuggerUserShortcutSpy;
debuggerUserShortcutSpy = jasmine.createSpyObj(
diff --git a/web/regression/javascript/datagrid/get_panel_title_spec.js b/web/regression/javascript/datagrid/get_panel_title_spec.js
new file mode 100644
index 00000000..8a344a84
--- /dev/null
+++ b/web/regression/javascript/datagrid/get_panel_title_spec.js
@@ -0,0 +1,82 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {getPanelTitle} from '../../../pgadmin/tools/datagrid/static/js/get_panel_title';
+import {TreeFake} from '../tree/tree_fake';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+const context = describe;
+
+describe('#getPanelTitle', () => {
+ let pgBrowser;
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ pgBrowser = {
+ treeMenu: tree,
+ Nodes: {
+ server: {
+ hasId: true,
+ _type: 'server',
+ },
+ database: {
+ hasId: true,
+ _type: 'database',
+ },
+ },
+ };
+ });
+
+ context('selected node does not belong to a server', () => {
+ it('returns undefined', () => {
+ const root = tree.addNewNode('level1', {_type: 'server_groups'});
+ tree.addChild(root, new TreeNode('level1.1', {_type: 'other'}));
+ tree.selectNode([{id: 'level1'}]);
+ expect(getPanelTitle(pgBrowser)).toBeUndefined();
+ });
+ });
+
+ context('selected node belong to a server', () => {
+ context('selected node does not belong to a database', () => {
+ it('returns the server label and the username', () => {
+ tree.addNewNode('level1', {
+ _type: 'server',
+ db: 'other db label',
+ user: {name: 'some user name'},
+ label: 'server label',
+ }, []);
+
+ tree.selectNode([{id: 'level1'}]);
+ expect(getPanelTitle(pgBrowser))
+ .toBe('other db label on some user name@server label');
+ });
+ });
+
+ context('selected node belongs to a database', () => {
+ it('returns the database label and the username', () => {
+ const root = tree.addNewNode('level1', {
+ _type: 'server',
+ db: 'other db label',
+ user: {name: 'some user name'},
+ label: 'server label',
+ });
+ const level1 = new TreeNode('level1.1', {
+ _type: 'database',
+ label: 'db label',
+ });
+ tree.addChild(root, level1);
+ tree.addChild(level1,
+ new TreeNode('level1.1.1', {_type: 'table'}));
+ tree.selectNode([{id: 'level1.1.1'}]);
+ expect(getPanelTitle(pgBrowser))
+ .toBe('db label on some user name@server label');
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/datagrid/show_data_spec.js b/web/regression/javascript/datagrid/show_data_spec.js
new file mode 100644
index 00000000..80d25eb3
--- /dev/null
+++ b/web/regression/javascript/datagrid/show_data_spec.js
@@ -0,0 +1,171 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {showDataGrid} from '../../../pgadmin/tools/datagrid/static/js/show_data';
+import {TreeFake} from '../tree/tree_fake';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+const context = describe;
+
+describe('#show_data', () => {
+ let datagrid;
+ let pgBrowser;
+ let alertify;
+ beforeEach(() => {
+ alertify = jasmine.createSpyObj('alertify', ['alert']);
+ datagrid = {
+ create_transaction: jasmine.createSpy('create_transaction'),
+ };
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server_group: {
+ _type: 'server_group',
+ hasId: true,
+ },
+ server: {
+ _type: 'server',
+ hasId: true,
+ },
+ database: {
+ _type: 'database',
+ hasId: true,
+ },
+ schema: {
+ _type: 'schema',
+ hasId: true,
+ },
+ view: {
+ _type: 'view',
+ hasId: true,
+ },
+ catalog: {
+ _type: 'catalog',
+ hasId: true,
+ },
+ },
+ };
+ const parent = pgBrowser.treeMenu.addNewNode('parent', {_type: 'parent'}, []);
+ const serverGroup1 = new TreeNode('server_group1', {
+ _type: 'server_group',
+ _id: 1,
+ });
+ pgBrowser.treeMenu.addChild(parent, serverGroup1);
+
+ const server1 = new TreeNode('server1', {
+ _type: 'server',
+ label: 'server1',
+ server_type: 'pg',
+ _id: 2,
+ }, ['parent', 'server_group1']);
+ pgBrowser.treeMenu.addChild(serverGroup1, server1);
+
+ const database1 = new TreeNode('database1', {
+ _type: 'database',
+ label: 'database1',
+ _id: 3,
+ }, ['parent', 'server_group1', 'server1']);
+ pgBrowser.treeMenu.addChild(server1, database1);
+
+ const schema1 = new TreeNode('schema1', {
+ _type: 'schema',
+ label: 'schema1',
+ _id: 4,
+ });
+ pgBrowser.treeMenu.addChild(database1, schema1);
+
+ const view1 = new TreeNode('view1', {
+ _type: 'view',
+ label: 'view1',
+ _id: 5,
+ }, ['parent', 'server_group1', 'server1', 'database1']);
+ pgBrowser.treeMenu.addChild(database1, view1);
+
+ const catalog1 = new TreeNode('catalog1', {
+ _type: 'catalog',
+ label: 'catalog1',
+ _id: 6,
+ }, ['parent', 'server_group1', 'server1', 'database1']);
+ pgBrowser.treeMenu.addChild(database1, catalog1);
+ });
+
+ context('cannot find the tree node', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: '10'}]);
+ expect(datagrid.create_transaction).not.toHaveBeenCalled();
+ });
+
+ it('display alert', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: '10'}]);
+ expect(alertify.alert).toHaveBeenCalledWith(
+ 'Data Grid Error',
+ 'No object selected.'
+ );
+ });
+ });
+
+ context('current node is not underneath a server', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: 'parent'}]);
+ expect(datagrid.create_transaction).not.toHaveBeenCalled();
+ });
+ });
+
+ context('current node is not underneath a schema or view or catalog', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: 'database1'}]);
+ expect(datagrid.create_transaction).not.toHaveBeenCalled();
+ });
+ });
+
+ context('current node is underneath a schema', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {mnuid: 11}, [{id: 'schema1'}]);
+ expect(datagrid.create_transaction).toHaveBeenCalledWith(
+ '/initialize/datagrid/11/schema/1/2/3/4',
+ null,
+ 'false',
+ 'pg',
+ '',
+ 'server1 - database1 - schema1.schema1',
+ ''
+ );
+ });
+ });
+
+ context('current node is underneath a view', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {mnuid: 11}, [{id: 'view1'}]);
+ expect(datagrid.create_transaction).toHaveBeenCalledWith(
+ '/initialize/datagrid/11/view/1/2/3/5',
+ null,
+ 'false',
+ 'pg',
+ '',
+ 'server1 - database1 - view1.view1',
+ ''
+ );
+ });
+ });
+
+ context('current node is underneath a catalog', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {mnuid: 11}, [{id: 'catalog1'}]);
+ expect(datagrid.create_transaction).toHaveBeenCalledWith(
+ '/initialize/datagrid/11/catalog/1/2/3/6',
+ null,
+ 'false',
+ 'pg',
+ '',
+ 'server1 - database1 - catalog1.catalog1',
+ ''
+ );
+ });
+ });
+});
diff --git a/web/regression/javascript/datagrid/show_query_tool_spec.js b/web/regression/javascript/datagrid/show_query_tool_spec.js
new file mode 100644
index 00000000..66bd37ce
--- /dev/null
+++ b/web/regression/javascript/datagrid/show_query_tool_spec.js
@@ -0,0 +1,125 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {TreeFake} from '../tree/tree_fake';
+import {showQueryTool} from '../../../pgadmin/tools/datagrid/static/js/show_query_tool';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+const context = describe;
+
+describe('#showQueryTool', () => {
+ let queryTool;
+ let pgBrowser;
+ let alertify;
+ beforeEach(() => {
+ alertify = jasmine.createSpyObj('alertify', ['alert']);
+ queryTool = {
+ create_transaction: jasmine.createSpy('create_transaction'),
+ };
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server_group: {
+ _type: 'server_group',
+ hasId: true,
+ },
+ server: {
+ _type: 'server',
+ hasId: true,
+ },
+ database: {
+ _type: 'database',
+ hasId: true,
+ },
+ },
+ };
+ const parent = pgBrowser.treeMenu.addNewNode('parent', {_type: 'parent'});
+ const serverGroup1 = new TreeNode('server_group1', {
+ _type: 'server_group',
+ _id: 1,
+ }, ['parent']);
+ pgBrowser.treeMenu.addChild(parent, serverGroup1);
+
+ const server1 = new TreeNode('server1', {
+ _type: 'server',
+ label: 'server1',
+ server_type: 'pg',
+ _id: 2,
+ });
+ pgBrowser.treeMenu.addChild(serverGroup1, server1);
+
+ const database1 = new TreeNode('database1', {
+ _type: 'database',
+ label: 'database1',
+ _id: 3,
+ });
+ pgBrowser.treeMenu.addChild(server1, database1);
+ });
+
+ context('cannot find the tree node', () => {
+ beforeEach(() => {
+ showQueryTool(queryTool, pgBrowser, alertify, '', [{id: '10'}], 'title');
+ });
+ it('does not create a transaction', () => {
+ expect(queryTool.create_transaction).not.toHaveBeenCalled();
+ });
+
+ it('display alert', () => {
+ expect(alertify.alert).toHaveBeenCalledWith(
+ 'Query Tool Error',
+ 'No object selected.'
+ );
+ });
+ });
+
+ context('current node is not underneath a server', () => {
+ it('does not create a transaction', () => {
+ showQueryTool(queryTool, pgBrowser, alertify, '', [{id: 'parent'}], 'title');
+ expect(queryTool.create_transaction).not.toHaveBeenCalled();
+ });
+
+ it('no alert is displayed', () => {
+ expect(alertify.alert).not.toHaveBeenCalled();
+ });
+ });
+
+ context('current node is underneath a server', () => {
+ context('current node is not underneath a database', () => {
+ it('creates a transaction', () => {
+ showQueryTool(queryTool, pgBrowser, alertify, 'http://someurl', [{id: 'server1'}], 'title');
+ expect(queryTool.create_transaction).toHaveBeenCalledWith(
+ '/initialize/query_tool/1/2',
+ null,
+ 'true',
+ 'pg',
+ 'http://someurl',
+ 'title',
+ '',
+ false
+ );
+ });
+ });
+
+ context('current node is underneath a database', () => {
+ it('creates a transaction', () => {
+ showQueryTool(queryTool, pgBrowser, alertify, 'http://someurl', [{id: 'database1'}], 'title');
+ expect(queryTool.create_transaction).toHaveBeenCalledWith(
+ '/initialize/query_tool/1/2/3',
+ null,
+ 'true',
+ 'pg',
+ 'http://someurl',
+ 'title',
+ '',
+ false
+ );
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/fake_endpoints.js b/web/regression/javascript/fake_endpoints.js
index 54b86a94..c060ba78 100644
--- a/web/regression/javascript/fake_endpoints.js
+++ b/web/regression/javascript/fake_endpoints.js
@@ -12,5 +12,11 @@ define(function () {
'static': '/base/pgadmin/static/<path:filename>',
'sqleditor.poll': '/sqleditor/query_tool/poll/<path:trans_id>',
'sqleditor.query_tool_start': '/sqleditor/query_tool/start/<path:trans_id>',
+ 'backup.create_server_job': '/backup/job/<int:sid>',
+ 'backup.create_object_job': '/backup/job/<int:sid>/object',
+ 'datagrid.initialize_datagrid': '/initialize/datagrid/<int:cmd_type>/<obj_type>/<int:sgid>/<int:sid>/<int:did>/<int:obj_id>',
+ 'datagrid.initialize_query_tool': '/initialize/query_tool/<int:sgid>/<int:sid>',
+ 'datagrid.initialize_query_tool_with_did': '/initialize/query_tool/<int:sgid>/<int:sid>/<int:did>',
+ 'restore.create_job': '/restore/job/<int:sid>',
};
});
diff --git a/web/regression/javascript/fake_model.js b/web/regression/javascript/fake_model.js
new file mode 100644
index 00000000..acfaa532
--- /dev/null
+++ b/web/regression/javascript/fake_model.js
@@ -0,0 +1,21 @@
+export class FakeModel {
+ constructor() {
+ this.values = {};
+ }
+
+ set(key, value) {
+ this.values[key] = value;
+ }
+
+ get(key) {
+ return this.values[key];
+ }
+
+ unset(key) {
+ delete this.values[key];
+ }
+
+ toJSON() {
+ return Object.assign({}, this.values);
+ }
+}
diff --git a/web/regression/javascript/restore/restore_dialog_spec.js b/web/regression/javascript/restore/restore_dialog_spec.js
new file mode 100644
index 00000000..156f56bb
--- /dev/null
+++ b/web/regression/javascript/restore/restore_dialog_spec.js
@@ -0,0 +1,203 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {TreeFake} from '../tree/tree_fake';
+import {RestoreDialog} from '../../../pgadmin/tools/restore/static/js/restore_dialog';
+
+const context = describe;
+
+describe('RestoreDialog', () => {
+ let restoreDialog;
+ let pgBrowser;
+ let jquerySpy;
+ let alertifySpy;
+ let restoreModelSpy;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: jasmine.createSpyObj('Node[server]', ['getTreeNodeHierarchy']),
+ database: jasmine.createSpyObj('Node[database]', ['getTreeNodeHierarchy']),
+ },
+ };
+ pgBrowser.Nodes.server.hasId = true;
+ pgBrowser.Nodes.database.hasId = true;
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ restoreModelSpy = jasmine.createSpy('restoreModelSpy');
+
+ const hierarchy = {
+ children: [
+ {
+ id: 'root',
+ children: [
+ {
+ id: 'serverTreeNode',
+ data: {
+ _id: 10,
+ _type: 'server',
+ label: 'some-tree-label',
+ },
+ children: [
+ {
+ id: 'some_database',
+ data: {
+ _type: 'database',
+ _id: 11,
+ label: 'some_database',
+ _label: 'some_database_label',
+ },
+ }, {
+ id: 'database_with_equal_in_name',
+ data: {
+ _type: 'database',
+ label: 'some_database',
+ _label: '=some_database_label',
+ },
+ },
+ ],
+ },
+ {
+ id: 'ppasServer',
+ data: {
+ _type: 'server',
+ server_type: 'ppas',
+ children: [
+ {id: 'someNodeUnderneathPPASServer'},
+ ],
+ },
+ },
+ ],
+ },
+ ],
+ };
+
+ pgBrowser.treeMenu = TreeFake.build(hierarchy);
+ });
+
+ describe('#draw', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ alertifySpy['pg_restore'] = jasmine.createSpy('pg_restore');
+ restoreDialog = new RestoreDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ restoreModelSpy
+ );
+
+ pgBrowser.get_preference = jasmine.createSpy('get_preferences');
+ });
+
+ context('there are no ancestors of the type server', () => {
+ it('does not create a dialog', () => {
+ pgBrowser.treeMenu.selectNode([{id: 'root'}]);
+ restoreDialog.draw(null, null, null);
+ expect(alertifySpy['pg_restore']).not.toHaveBeenCalled();
+ });
+
+ it('display an alert with a Restore Error', () => {
+ restoreDialog.draw(null, [{id: 'root'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Restore Error',
+ 'Please select server or child node from the browser tree.'
+ );
+ });
+ });
+
+ context('there is an ancestor of the type server', () => {
+ context('no preference can be found', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue(undefined);
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Restore Error"', () => {
+ restoreDialog.draw(null, [{id: 'serverTreeNode'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Restore Error',
+ 'Failed to load preference pg_bin_dir of module paths'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Restore Error"', () => {
+ restoreDialog.draw(null, [{id: 'ppasServer'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Restore Error',
+ 'Failed to load preference ppas_bin_dir of module paths'
+ );
+ });
+ });
+ });
+
+ context('preference can be found', () => {
+ context('binary folder is not configured', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue({});
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ restoreDialog.draw(null, [{id: 'serverTreeNode'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the PostgreSQL Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ restoreDialog.draw(null, [{id: 'ppasServer'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the EDB Advanced Server Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+ });
+
+ context('binary folder is configured', () => {
+ let spy;
+ beforeEach(() => {
+ spy = jasmine.createSpyObj('globals', ['resizeTo']);
+ alertifySpy['pg_restore'].and
+ .returnValue(spy);
+ pgBrowser.get_preference.and.returnValue({value: '/some/path'});
+ pgBrowser.Nodes.server.label = 'some-server-label';
+ });
+
+ it('displays the dialog', () => {
+ restoreDialog.draw(null, [{id: 'serverTreeNode'}], {server: true});
+ expect(alertifySpy['pg_restore']).toHaveBeenCalledWith(
+ 'Restore (some-server-label: some-tree-label)',
+ [{id: 'serverTreeNode'}],
+ {
+ _id: 10,
+ _type: 'server',
+ label: 'some-tree-label',
+ },
+ pgBrowser.Nodes.server
+ );
+ expect(spy.resizeTo).toHaveBeenCalledWith('65%', '60%');
+ });
+
+ context('database label contain "="', () => {
+ it('should create alert dialog with restore error', () => {
+ restoreDialog.draw(null, [{id: 'database_with_equal_in_name'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith('Restore Error',
+ 'Databases with = symbols in the name cannot be backed up or restored using this utility.');
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/restore/restore_dialog_wrapper_spec.js b/web/regression/javascript/restore/restore_dialog_wrapper_spec.js
new file mode 100644
index 00000000..c2a31d55
--- /dev/null
+++ b/web/regression/javascript/restore/restore_dialog_wrapper_spec.js
@@ -0,0 +1,593 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {TreeFake} from '../tree/tree_fake';
+import {RestoreDialogWrapper} from '../../../pgadmin/tools/restore/static/js/restore_dialog_wrapper';
+import MockAdapter from 'axios-mock-adapter';
+import axios from 'axios/index';
+import {FakeModel} from '../fake_model';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+let context = describe;
+
+describe('RestoreDialogWrapper', () => {
+ let jquerySpy;
+ let pgBrowser;
+ let alertifySpy;
+ let dialogModelKlassSpy;
+ let backform;
+ let generatedRestoreModel;
+ let restoreDialogWrapper;
+ let noDataNode;
+ let serverTreeNode;
+ let viewSchema;
+ let restoreJQueryContainerSpy;
+ let restoreNodeChildNodeSpy;
+ let restoreNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: {
+ hasId: true,
+ getTreeNodeHierarchy: jasmine.createSpy('getTreeNodeHierarchy'),
+ },
+ },
+ keyboardNavigation: jasmine.createSpyObj('keyboardNavigation', ['getDialogTabNavigator']),
+ };
+
+ noDataNode = pgBrowser.treeMenu.addNewNode('level1.1', undefined, [{id: 'level1'}]);
+ serverTreeNode = pgBrowser.treeMenu.addNewNode('level2.1', {
+ _type: 'server',
+ _id: 10,
+ label: 'some-tree-label',
+ }, [{id: 'level2.1'}]);
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ dialogModelKlassSpy = jasmine.createSpy('dialogModelKlass');
+ generatedRestoreModel = {};
+ viewSchema = {};
+ backform = jasmine.createSpyObj('backform', ['generateViewSchema', 'Dialog']);
+ backform.generateViewSchema.and.returnValue(viewSchema);
+ dialogModelKlassSpy.and.returnValue(generatedRestoreModel);
+ restoreJQueryContainerSpy = jasmine.createSpyObj('restoreJQueryContainer', ['get', 'attr']);
+ restoreJQueryContainerSpy.get.and.returnValue(restoreJQueryContainerSpy);
+
+ restoreNode = {
+ __internal: {
+ buttons: [
+ {}, {},
+ {
+ element: {
+ disabled: false,
+ },
+ },
+ ],
+ },
+ elements: {
+ body: {
+ childNodes: [
+ {},
+ ],
+ },
+ content: jasmine.createSpyObj('content', ['appendChild', 'attr']),
+ },
+ };
+
+
+ restoreNodeChildNodeSpy = jasmine.createSpyObj('something', ['addClass']);
+
+ jquerySpy.and.callFake((selector) => {
+ if (selector === '<div class=\'restore_dialog\'></div>') {
+ return restoreJQueryContainerSpy;
+ } else if (selector === restoreNode.elements.body.childNodes[0]) {
+ return restoreNodeChildNodeSpy;
+ }
+ });
+ });
+
+ describe('#prepare', () => {
+
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ restoreDialogWrapper = new RestoreDialogWrapper(
+ '<div class=\'restore_dialog\'></div>',
+ 'restoreDialogTitle',
+ 'restore',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+ restoreDialogWrapper = Object.assign(restoreDialogWrapper, restoreNode);
+ });
+ context('no tree element is selected', () => {
+ it('does not create a backform dialog', () => {
+ restoreDialogWrapper.prepare();
+ expect(backform.Dialog).not.toHaveBeenCalled();
+ });
+
+ it('disables the button "submit button" until a filename is selected', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreDialogWrapper.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('selected tree node has no data', () => {
+ beforeEach(() => {
+ pgBrowser.treeMenu.selectNode(noDataNode.domNode);
+ });
+
+ it('does not create a backform dialog', () => {
+ restoreDialogWrapper.prepare();
+ expect(backform.Dialog).not.toHaveBeenCalled();
+ });
+
+ it('disables the button "submit button" until a filename is selected', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreDialogWrapper.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('tree element is selected', () => {
+ let treeHierarchyInformation;
+ let dialogSpy;
+ beforeEach(() => {
+ treeHierarchyInformation = {
+ server: {
+ _type: 'server',
+ _id: 10,
+ priority: 0,
+ label: 'some-tree-label',
+ },
+ };
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ pgBrowser.Nodes['server'].getTreeNodeHierarchy.and
+ .returnValue(treeHierarchyInformation);
+ dialogSpy = jasmine.createSpyObj('newView', ['render']);
+ dialogSpy.$el = jasmine.createSpyObj('$el', ['find', 'attr']);
+ dialogSpy.model = jasmine.createSpyObj('model', ['on']);
+ dialogSpy.$el.find.and.returnValue([]);
+
+ backform.Dialog.and.returnValue(dialogSpy);
+ });
+
+ it('creates a backform dialog and displays it', () => {
+ restoreDialogWrapper.prepare();
+ expect(backform.Dialog).toHaveBeenCalledWith({
+ el: restoreJQueryContainerSpy,
+ model: generatedRestoreModel,
+ schema: viewSchema,
+ });
+
+ expect(dialogSpy.render).toHaveBeenCalled();
+ });
+
+ it('add alertify classes to restore node childnode', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreNodeChildNodeSpy.addClass)
+ .toHaveBeenCalledWith('alertify_tools_dialog_properties obj_properties');
+ });
+
+ it('disables the button submit button until a filename is selected', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreNode.__internal.buttons[2].element.disabled).toBe(true);
+ });
+
+ it('generates a new restore model', () => {
+ restoreDialogWrapper.prepare();
+ expect(dialogModelKlassSpy).toHaveBeenCalledWith(
+ {node_data: pgBrowser.Nodes['server']},
+ {node_info: treeHierarchyInformation}
+ );
+ });
+
+ it('add the new dialog to the restore node HTML', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreNode.elements.content.appendChild).toHaveBeenCalledWith(restoreJQueryContainerSpy);
+ });
+ });
+ });
+
+ describe('onButtonClicked', () => {
+ let networkMock;
+
+ beforeEach(() => {
+ pgBrowser.showHelp = jasmine.createSpy('showHelp');
+ networkMock = new MockAdapter(axios);
+ alertifySpy = jasmine.createSpyObj('alertify', ['success', 'alert']);
+ restoreDialogWrapper = new RestoreDialogWrapper(
+ '<div class=\'restore_dialog\'></div>',
+ 'restoreDialogTitle',
+ 'restore',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+ restoreDialogWrapper = Object.assign(restoreDialogWrapper, restoreNode);
+
+ });
+
+ afterEach(function () {
+ networkMock.restore();
+ });
+
+ context('dialog help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ element: {
+ name: 'dialog_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ },
+ },
+ },
+ };
+ restoreDialogWrapper.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'dialog_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ );
+ });
+
+ it('does not start the restore', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('object help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ element: {
+ name: 'object_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ },
+ },
+ },
+ };
+ restoreDialogWrapper.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'object_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ );
+ });
+
+ it('does not start the restore', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('restore button was pressed', () => {
+ let networkCalled;
+ let event;
+
+ context('no tree node is selected', () => {
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+ event = {
+ button: {
+ 'data-btn-name': 'restore',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ });
+
+ it('does not start the restore', () => {
+ restoreDialogWrapper.callback(event);
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('tree node selected has no data', () => {
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+ event = {
+ button: {
+ 'data-btn-name': 'restore',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ pgBrowser.treeMenu.selectNode(noDataNode.domNode);
+ });
+
+ it('does not start the restore', () => {
+ restoreDialogWrapper.callback(event);
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('tree node select has data', () => {
+
+ let databaseTreeNode;
+
+ beforeEach(() => {
+ databaseTreeNode = pgBrowser.treeMenu.addNewNode('level3.1', {
+ _type: 'database',
+ _id: 10,
+ _label: 'some-database-label',
+ }, [{id: 'level3.1'}]);
+ pgBrowser.treeMenu.addChild(serverTreeNode, databaseTreeNode);
+ pgBrowser.Nodes.database = {
+ hasId: true,
+ _label: 'some-database-label',
+ };
+ let fakeModel = new FakeModel();
+ fakeModel.set('some-key', 'some-value');
+ restoreDialogWrapper.view = {
+ model: fakeModel,
+ };
+ pgBrowser.treeMenu.selectNode(databaseTreeNode.domNode);
+ pgBrowser.Events = jasmine.createSpyObj('pgBrowserEventsSpy', ['trigger']);
+ event = {
+ button: {
+ 'data-btn-name': 'restore',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ });
+ context('restore job created successfully', () => {
+ let dataSentToServer;
+ beforeEach(() => {
+ networkMock.onPost('/restore/job/10').reply((request) => {
+ dataSentToServer = request.data;
+ return [200, {}];
+ });
+ });
+
+ it('create an success alert box', (done) => {
+ restoreDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.success).toHaveBeenCalledWith(
+ 'Restore job created.',
+ 5
+ );
+ done();
+ }, 0);
+ });
+
+ it('trigger background process', (done) => {
+ restoreDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(pgBrowser.Events.trigger).toHaveBeenCalledWith(
+ 'pgadmin-bgprocess:created',
+ restoreDialogWrapper
+ );
+ done();
+ }, 0);
+ });
+
+ it('send correct data to server', (done) => {
+ restoreDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(JSON.parse(dataSentToServer)).toEqual({
+ 'some-key': 'some-value',
+ 'database': 'some-database-label',
+ });
+ done();
+ }, 0);
+ });
+ });
+
+ context('error creating restore job', () => {
+ beforeEach(() => {
+ networkMock.onPost('/restore/job/10').reply(() => {
+ return [400, {}];
+ });
+ });
+
+ it('creates an alert box', (done) => {
+ restoreDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Restore job failed.',
+ undefined
+ );
+ done();
+ }, 0);
+ });
+ });
+ });
+ });
+ });
+
+ describe('setExtraParameters', () => {
+ let selectedNode;
+ let treeInfo;
+ let model;
+
+ beforeEach(() => {
+ restoreDialogWrapper = new RestoreDialogWrapper(
+ '<div class=\'restore_dialog\'></div>',
+ 'restoreDialogTitle',
+ 'restore',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ model = new FakeModel();
+ restoreDialogWrapper.view = {
+ model: model,
+ };
+ });
+
+ context('when it is a custom model', () => {
+ beforeEach(() => {
+ model.set('custom', true);
+ treeInfo = {
+ 'database': {
+ '_label': 'some-database-label',
+ },
+ };
+ });
+
+ it('only sets the database', () => {
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ 'custom': true,
+ 'database': 'some-database-label',
+ });
+ });
+ });
+
+ context('when it is not a custom model', () => {
+ beforeEach(() => {
+ model.set('custom', false);
+ treeInfo = {
+ 'database': {
+ '_label': 'some-database-label',
+ },
+ 'schema': {
+ '_label': 'some-schema-label',
+ },
+ };
+ });
+
+ context('when selected node is a schema', () => {
+ it('sets schemas on the model', () => {
+ selectedNode = new TreeNode('schema', {_type: 'schema', _label: 'some-schema-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ });
+ });
+ });
+
+ context('when selected node is a table', () => {
+ it('sets schemas and table on the model', () => {
+ selectedNode = new TreeNode('table', {_type: 'table', _label: 'some-table-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ tables: ['some-table-label'],
+ });
+ });
+ });
+
+ context('when selected node is a function', () => {
+ it('sets schemas and function on the model', () => {
+ selectedNode = new TreeNode('function', {_type: 'function', _label: 'some-function-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ functions: ['some-function-label'],
+ });
+ });
+ });
+
+ context('when selected node is an index', () => {
+ it('sets schemas and index on the model', () => {
+ selectedNode = new TreeNode('index', {_type: 'index', _label: 'some-index-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ indexes: ['some-index-label'],
+ });
+ });
+ });
+
+ context('when selected node is a trigger', () => {
+ it('sets schemas and trigger on the model', () => {
+ selectedNode = new TreeNode('trigger', {_type: 'trigger', _label: 'some-trigger-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ triggers: ['some-trigger-label'],
+ });
+ });
+ });
+
+ context('when selected node is a trigger_func', () => {
+ it('sets schemas and trigger_func on the model', () => {
+ selectedNode = new TreeNode('trigger_func', {_type: 'trigger_func', _label: 'some-trigger_func-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ trigger_funcs: ['some-trigger_func-label'],
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/schema/can_drop_child_spec.js b/web/regression/javascript/schema/can_drop_child_spec.js
new file mode 100644
index 00000000..4403b274
--- /dev/null
+++ b/web/regression/javascript/schema/can_drop_child_spec.js
@@ -0,0 +1,82 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {canDropChild} from '../../../pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child';
+import {TreeFake} from '../tree/tree_fake';
+
+let context = describe;
+
+describe('can_drop_child', () => {
+
+ let browser;
+ let itemData;
+ let item;
+
+ beforeEach(() => {
+ browser = {
+ treeMenu: new TreeFake(),
+ };
+ item = [];
+ browser.treeMenu.addNewNode('node1', {_type: 'schema'}, [{id: 'node1'}], []);
+ browser.treeMenu.addNewNode('node1.1', {_type: 'database'}, [{id: 'node1.1'}], ['node1']);
+ browser.treeMenu.addNewNode('node2', {_type: 'catalog'}, [{id: 'node2'}], []);
+ browser.treeMenu.addNewNode('node2.1', {_type: 'table'}, [{id: 'node2.1'}], ['node2']);
+ browser.treeMenu.addNewNode('node3', {_type: 'function'}, [{id: 'node3'}], []);
+ browser.treeMenu.addNewNode('node3.1', {_type: 'procedure'}, [{id: 'node3.1'}], ['node3']);
+ });
+
+ context('when current node is of the type schema', () => {
+ beforeEach(() => {
+ itemData = {
+ _type: 'schema',
+ };
+ item = [{id: 'node1'}];
+ });
+
+ it('returns true', () => {
+ let bool = canDropChild(browser, itemData, item);
+ expect(bool).toBe(true);
+ });
+ });
+
+ context('when a parent of the current node is a schema', () => {
+ it('returns true', () => {
+ itemData = {
+ _type: 'database',
+ };
+ item = [{id: 'node1.1'}];
+ let bool = canDropChild(browser, itemData, item);
+ expect(bool).toBe(true);
+ });
+ });
+
+ context('when a parent of the current node is a catalog', () => {
+ it('returns false', () => {
+ itemData= {
+ _type: 'table',
+ };
+ item = [{id: 'node2.1'}];
+
+ let bool = canDropChild(browser, itemData, item);
+ expect(bool).toBe(false);
+ });
+ });
+
+ context('when a parent of the current node is not catalog nor schema', () => {
+ it('returns true', () => {
+ itemData = {
+ _type: 'procedure',
+ };
+ item = [{id: 'node3.1'}];
+
+ let bool = canDropChild(browser, itemData, item);
+ expect(bool).toBe(true);
+ });
+ });
+});
diff --git a/web/regression/javascript/sqleditor/filter_dialog_specs.js b/web/regression/javascript/sqleditor/filter_dialog_specs.js
index ed77dff5..cea75e6b 100644
--- a/web/regression/javascript/sqleditor/filter_dialog_specs.js
+++ b/web/regression/javascript/sqleditor/filter_dialog_specs.js
@@ -7,10 +7,8 @@
//
//////////////////////////////////////////////////////////////////////////
import filterDialog from 'sources/sqleditor/filter_dialog';
-// import filterDialogModel from 'sources/sqleditor/filter_dialog_model';
describe('filterDialog', () => {
- jasmine.createSpy('sqlEditorController');
describe('filterDialog', () => {
describe('when using filter dialog', () => {
beforeEach(() => {
diff --git a/web/regression/javascript/table/enable_disable_triggers_spec.js b/web/regression/javascript/table/enable_disable_triggers_spec.js
new file mode 100644
index 00000000..7bdd284e
--- /dev/null
+++ b/web/regression/javascript/table/enable_disable_triggers_spec.js
@@ -0,0 +1,271 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import MockAdapter from 'axios-mock-adapter';
+import axios from 'axios/index';
+import {
+ enableTriggers,
+ disableTriggers,
+} from '../../../pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers';
+import {TreeFake} from '../tree/tree_fake';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+describe('#enableTriggers', () => {
+ let networkMock;
+ let tree;
+ let alertify;
+ let generateUrlSpy;
+ beforeEach(() => {
+ networkMock = new MockAdapter(axios);
+ tree = new TreeFake();
+ const server1 = tree.addNewNode('server1', {_id: 1}, ['<li>server1</li>']);
+ const database1 = tree.addNewNode('database1', {_type: 'database'}, ['<li>database1</li>']);
+ tree.addChild(server1, database1);
+
+ const schema1 = tree.addNewNode('schema1', {_type: 'schema'}, ['<li>schema1</li>']);
+ tree.addChild(database1, schema1);
+
+ const table1 = tree.addNewNode('table1', {_type: 'table'}, ['<li>table1</li>']);
+ tree.addChild(schema1, table1);
+
+ const column1 = tree.addNewNode('column1', {_type: 'column'}, ['<li>column1</li>']);
+ tree.addChild(table1, column1);
+
+ const tableNoData = tree.addNewNode('table-no-data', undefined, ['<li>table-no-data</li>']);
+ tree.addChild(schema1, tableNoData);
+
+ alertify = jasmine.createSpyObj('alertify', ['success', 'error']);
+ generateUrlSpy = jasmine.createSpy('generateUrl');
+ generateUrlSpy.and.returnValue('/some/place');
+ });
+
+ describe('no node is selected', () => {
+ it('does not send the request to the backend', (done) => {
+ networkMock.onAny('.*').reply(200, () => {
+ });
+
+ setTimeout(() => {
+ expect(enableTriggers(tree, alertify, generateUrlSpy, {})).toBe(false);
+ done();
+ }, 0);
+ });
+ });
+
+ describe('a node is selected', () => {
+ describe('node as no data', () => {
+ it('does not send the request to the backend', () => {
+ tree.selectNode([{id: 'table-no-data'}]);
+
+ networkMock.onAny('.*').reply(200, () => {
+ });
+
+ setTimeout(() => {
+ expect(enableTriggers(tree, alertify, generateUrlSpy, {})).toBe(false);
+ }, 0);
+ });
+ });
+
+ describe('node as data', () => {
+ describe('backend responds with success', () => {
+ let networkMockCalledWith;
+ beforeEach(() => {
+ networkMockCalledWith = false;
+ networkMock.onPut(/.*/).reply((configuration) => {
+ networkMockCalledWith = configuration;
+ return [200, {
+ success: 1,
+ info: 'some information',
+ }];
+ });
+ });
+
+ it('displays an alert box with success', (done) => {
+ tree.selectNode([{id: 'table1'}]);
+ enableTriggers(tree, alertify, generateUrlSpy, {});
+ setTimeout(() => {
+ expect(alertify.success).toHaveBeenCalledWith('some information');
+ done();
+ }, 0);
+ });
+
+ it('reloads the node', (done) => {
+ enableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+ setTimeout(() => {
+ expect(tree.selected()).toEqual(['<li>table1</li>']);
+ done();
+ }, 20);
+ });
+
+ it('call backend with the correct parameters', (done) => {
+ enableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+ setTimeout(() => {
+ expect(networkMockCalledWith.data).toEqual(JSON.stringify({enable: 'true'}));
+ done();
+ }, 0);
+ });
+ });
+
+ describe('backend responds with error', () => {
+ beforeEach(() => {
+ networkMock.onPut(/.*/).reply(() => {
+ return [500, {
+ success: 0,
+ errormsg: 'some error message',
+ }];
+ });
+ });
+
+ it('displays an error alert', (done) => {
+ tree.selectNode([{id: 'table1'}]);
+ enableTriggers(tree, alertify, generateUrlSpy, {});
+ setTimeout(() => {
+ expect(alertify.error).toHaveBeenCalledWith('some error message');
+ done();
+ }, 0);
+ });
+
+ it('unload the node', (done) => {
+ enableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+
+ setTimeout(() => {
+ expect(tree.findNodeByDomElement([{id: 'table1'}]).children.length).toBe(0);
+ done();
+ }, 20);
+ });
+ });
+ });
+ });
+});
+
+describe('#disableTriggers', () => {
+ let networkMock;
+ let tree;
+ let alertify;
+ let generateUrlSpy;
+ beforeEach(() => {
+ networkMock = new MockAdapter(axios);
+ tree = new TreeFake();
+ const server1 = tree.addNewNode('server1', {_id: 1}, ['<li>server1</li>']);
+ const database1 = new TreeNode('database1', {_type: 'database'}, ['<li>database1</li>']);
+ tree.addChild(server1, database1);
+
+ const schema1 = new TreeNode('schema1', {_type: 'schema'}, ['<li>schema1</li>']);
+ tree.addChild(database1, schema1);
+
+ const table1 = new TreeNode('table1', {_type: 'table'}, ['<li>table1</li>']);
+ tree.addChild(schema1, table1);
+
+ const column1 = new TreeNode('column1', {_type: 'column'}, ['<li>column1</li>']);
+ tree.addChild(table1, column1);
+
+ const tableNoData = new TreeNode('table-no-data', undefined, ['<li>table-no-data</li>']);
+ tree.addChild(schema1, tableNoData);
+
+ alertify = jasmine.createSpyObj('alertify', ['success', 'error']);
+ generateUrlSpy = jasmine.createSpy('generateUrl');
+ generateUrlSpy.and.returnValue('/some/place');
+ });
+
+ describe('no node is selected', () => {
+ it('does not send the request to the backend', (done) => {
+ networkMock.onAny('.*').reply(200, () => {
+ });
+
+ setTimeout(() => {
+ expect(disableTriggers(tree, alertify, generateUrlSpy, {})).toBe(false);
+ done();
+ }, 0);
+ });
+ });
+
+ describe('a node is selected', () => {
+ describe('node as no data', () => {
+ it('does not send the request to the backend', () => {
+ tree.selectNode([{id: 'table-no-data'}]);
+
+ networkMock.onAny('.*').reply(200, () => {
+ });
+
+ setTimeout(() => {
+ expect(disableTriggers(tree, alertify, generateUrlSpy, {})).toBe(false);
+ }, 0);
+ });
+ });
+
+ describe('node as data', () => {
+ describe('backend responds with success', () => {
+ let networkMockCalledWith;
+ beforeEach(() => {
+ networkMockCalledWith = false;
+ networkMock.onPut(/.*/).reply((configuration) => {
+ networkMockCalledWith = configuration;
+ return [200, {
+ success: 1,
+ info: 'some information',
+ }];
+ });
+ });
+
+ it('displays an alert box with success', (done) => {
+ tree.selectNode([{id: 'table1'}]);
+ disableTriggers(tree, alertify, generateUrlSpy, {});
+ setTimeout(() => {
+ expect(alertify.success).toHaveBeenCalledWith('some information');
+ done();
+ }, 0);
+ });
+
+ it('reloads the node', (done) => {
+ disableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+ setTimeout(() => {
+ expect(tree.selected()).toEqual(['<li>table1</li>']);
+ done();
+ }, 20);
+ });
+
+ it('call backend with the correct parameters', (done) => {
+ disableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+ setTimeout(() => {
+ expect(networkMockCalledWith.data).toEqual(JSON.stringify({enable: 'false'}));
+ done();
+ }, 0);
+ });
+ });
+
+ describe('backend responds with error', () => {
+ beforeEach(() => {
+ networkMock.onPut(/.*/).reply(() => {
+ return [500, {
+ success: 0,
+ errormsg: 'some error message',
+ }];
+ });
+ });
+
+ it('displays an error alert', (done) => {
+ tree.selectNode([{id: 'table1'}]);
+ disableTriggers(tree, alertify, generateUrlSpy, {});
+ setTimeout(() => {
+ expect(alertify.error).toHaveBeenCalledWith('some error message');
+ done();
+ }, 0);
+ });
+
+ it('unload the node', (done) => {
+ disableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+
+ setTimeout(() => {
+ expect(tree.findNodeByDomElement([{id: 'table1'}]).children.length).toBe(0);
+ done();
+ }, 20);
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/tree/can_create_spec.js b/web/regression/javascript/tree/can_create_spec.js
new file mode 100644
index 00000000..3008d8ff
--- /dev/null
+++ b/web/regression/javascript/tree/can_create_spec.js
@@ -0,0 +1,167 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+/////////////////////////////////////////////////////////////
+
+import {canCreate} from '../../../pgadmin/static/js/tree/node_menu';
+import {TreeFake} from '../tree/tree_fake';
+
+const context = describe;
+
+describe('#canCreate', () => {
+ let ourBrowser;
+ let data;
+ let tree;
+ let createCheck;
+
+ context('data is not null and check is false ', () => {
+ beforeEach(() => {
+ data = {action: 'create', check: false};
+ });
+ it('returns true', () => {
+ createCheck = canCreate({}, {});
+ expect(createCheck({}, {}, data)).toBe(true);
+ });
+ });
+
+ context('data is not null and check is true', () => {
+ beforeEach(() => {
+ data = {action: 'create', check: true};
+ });
+
+ context('is node with type schema', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [
+ {
+ id: 'level2',
+ data: {_type: 'schema'},
+ },
+ ],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ ourBrowser = {treeMenu: tree};
+ });
+
+ it('returns true', () => {
+ createCheck = canCreate(ourBrowser, 'coll-table');
+ expect(createCheck({}, [{id: 'level2'}], data)).toBe(true);
+ });
+ });
+
+ context('has ancestor with type schema', () => {
+ beforeEach(() => {
+
+ let hierarchy = {
+ id: 'root',
+ children: [
+ {
+ id: 'level2',
+ data: {_type: 'schema'},
+ children: [
+ {
+ id: 'level3',
+ data: {_type: 'database'},
+ },
+ ],
+ },
+ ],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ ourBrowser = {treeMenu: tree};
+ });
+
+ it('returns true', () => {
+ createCheck = canCreate(ourBrowser, 'coll-table');
+ expect(createCheck(null, [{id: 'level3'}], data)).toBe(true);
+ });
+ });
+
+ context('when type is not "coll-table"', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [
+ {
+ id: 'level2',
+ data: {_type: 'database'},
+ },
+ ],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ ourBrowser = {treeMenu: tree};
+ });
+
+ it('returns true', () => {
+ createCheck = canCreate(ourBrowser, 'coll-table');
+ expect(createCheck(null, [{id: 'level2'}], data)).toBe(true);
+ });
+ });
+
+ context('when type is "coll-table"', () => {
+ context('when parent type is "catalog"', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [
+ {
+ id: 'level2',
+ data: {_type: 'catalog'},
+ children: [
+ {
+ id: 'level3',
+ data: {_type: 'coll-table'},
+ },
+ ],
+ },
+ ],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ ourBrowser = {treeMenu: tree};
+ });
+
+ it('returns false', () => {
+ createCheck = canCreate(ourBrowser, 'coll-table');
+ expect(createCheck(null, [{id: 'level3'}], data)).toBe(false);
+ });
+ });
+
+ context('when parent type is not "catalog"', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [
+ {
+ id: 'level2',
+ data: {_type: 'database'},
+ children: [
+ {
+ id: 'level3',
+ data: {_type: 'coll-table'},
+ },
+ ],
+ },
+ ],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ ourBrowser = {treeMenu: tree};
+ });
+
+ it('returns false', () => {
+ createCheck = canCreate(ourBrowser, 'coll-table');
+ expect(createCheck(null, [{id: 'level3'}], data)).toBe(true);
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/tree/menu_enabled_spec.js b/web/regression/javascript/tree/menu_enabled_spec.js
new file mode 100644
index 00000000..ac0af8e7
--- /dev/null
+++ b/web/regression/javascript/tree/menu_enabled_spec.js
@@ -0,0 +1,131 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {TreeFake} from '../tree/tree_fake';
+import {enabled as menuEnabled} from '../../../pgadmin/static/js/tree/supported_nodes_menu';
+
+const context = describe;
+describe('#menuEnabled', () => {
+ let ourBrowser;
+ beforeEach(() => {
+ const tree = new TreeFake();
+ ourBrowser = {
+ treeMenu: tree,
+ };
+ tree.addNewNode('level1', {}, undefined, []);
+ tree.addNewNode('level1.1', {_type: 'catalog'}, undefined, ['level1']);
+ tree.addNewNode('level1.1.1', {_type: 'database'}, undefined, ['level1', 'level1.1']);
+ tree.addNewNode('level1.2', {_type: 'bamm'}, undefined, ['level1']);
+ tree.addNewNode('level1.2.1', {
+ _type: 'database',
+ allowConn: true,
+ }, undefined, ['level1', 'level1.2']);
+ tree.addNewNode('level1.2.2', {
+ _type: 'database',
+ allowConn: false,
+ }, undefined, ['level1', 'level1.2']);
+ tree.addNewNode('level1.2.3', {
+ _type: 'table',
+ }, undefined, ['level1', 'level1.2']);
+
+ tree.addNewNode('level2', {}, undefined, []);
+ tree.addNewNode('level2.1', null, undefined, ['level2']);
+ tree.addNewNode('level2.1.1', {}, undefined, ['level2', 'level2.1']);
+ });
+
+ context('When the current node is a root node', () => {
+ it('return false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu, [], {}, [{id: 'level1'}])).toBe(false);
+ });
+ });
+
+ context('when current node does not exist', () => {
+ it('return false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu, [], {}, [{id: 'bamm'}])).toBe(false);
+ });
+ });
+
+ context('provided node data is undefined', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu, [], undefined, [{id: 'level1'}])).toBe(false);
+ });
+ });
+
+ context('provided node data is null', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu, [], null, [{id: 'level1'}])).toBe(false);
+ });
+ });
+
+ context('When the current node is not a root node', () => {
+ context('parent data does not exist', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu, [], {}, [{id: 'level2.1.1'}])).toBe(false);
+ });
+ });
+
+ context('parent as data', () => {
+ context('the current node type is in the supported node types', () => {
+ context('the parent is of the type catalog', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu,
+ ['schema'],
+ {_type: 'schema'},
+ [{id: 'level1.1.1'}]
+ )).toBe(false);
+ });
+ });
+ context('an ancestor with type catalog exists', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu,
+ ['table'],
+ {_type: 'table'},
+ [{id: 'level1.1.1.1'}]
+ )).toBe(false);
+ });
+ });
+ context('the parent is not of the type catalog', () => {
+ context('current node is of the type database', () => {
+ context('current node allows connection', () => {
+ it('returns true', () => {
+ expect(menuEnabled(ourBrowser.treeMenu,
+ ['database'],
+ {
+ _type: 'database',
+ allowConn: true,
+ }, [{id: 'level1.2.1'}])).toBe(true);
+ });
+ });
+ context('current node do not allow connection', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu,
+ ['database'], {
+ _type: 'database',
+ allowConn: false,
+ }, [{id: 'level1.2.2'}])).toBe(false);
+ });
+ });
+ });
+ context('current node is not of the type database', () => {
+ it('returns true', () => {
+ expect(menuEnabled(ourBrowser.treeMenu,
+ ['schema'], {
+ _type: 'schema',
+ }, [{id: 'level1.2.3'}])).toBe(true);
+ });
+ });
+ });
+ });
+ context('the current node type is not in the supported node types', () => {
+ it('returns false', () => {
+ expect(menuEnabled(ourBrowser.treeMenu, [], {_type: 'catalog'}, [{id: 'level1.1'}])).toBe(false);
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/tree/pgadmin_tree_node_spec.js b/web/regression/javascript/tree/pgadmin_tree_node_spec.js
new file mode 100644
index 00000000..479e515c
--- /dev/null
+++ b/web/regression/javascript/tree/pgadmin_tree_node_spec.js
@@ -0,0 +1,353 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+import {
+ getTreeNodeHierarchyFromElement,
+ getTreeNodeHierarchyFromIdentifier,
+} from '../../../pgadmin/static/js/tree/pgadmin_tree_node';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+import {TreeFake} from './tree_fake';
+
+const context = describe;
+
+describe('tree#node#getTreeNodeHierarchy', () => {
+ let browser;
+ let newTree;
+ beforeEach(() => {
+ newTree = new TreeFake();
+ browser = {
+ Nodes: {
+ 'special one': {hasId: true},
+ 'child special': {hasId: true},
+ 'other type': {hasId: true},
+ 'table': {hasId: true},
+ 'partition': {hasId: true},
+ 'no id': {hasId: false},
+ },
+ };
+ browser.treeMenu = newTree;
+ });
+
+ context('getTreeNodeHierarchy is called with aciTreeNode object', () => {
+ describe('When the current node is root element', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ });
+ });
+
+ it('returns a object with the element type passed data and priority == 0', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)([{id: 'root'}]);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+
+ describe('When the current node is not of a known type', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'do not exist',
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)('root');
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node type has no id', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'no id',
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)('root');
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node is at the second level', () => {
+ beforeEach(() => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ });
+ const firstChild = new TreeNode('first child', {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ }, ['root']);
+ newTree.addChild(root, firstChild);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)([{id: 'first child'}]);
+ expect(result).toEqual({
+ 'child special': {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ 'priority': 0,
+ },
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+
+ context('When tree as "special type"', () => {
+ context('When "special type" have "other type"', () => {
+ context('When "other type" have "special type"', () => {
+ describe('When retrieving lead node', () => {
+ it('does not override previous node type data', () => {
+ const rootNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+
+ const level1 = new TreeNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'other type',
+ });
+ newTree.addChild(rootNode, level1);
+
+ newTree.addChild(level1, new TreeNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ }));
+
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)([{id: 'level 2'}]);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ 'priority': 0,
+ },
+ 'other type': {
+ 'some key': 'some value',
+ '_type': 'other type',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+ });
+ });
+ });
+
+ context('When tree has table', () => {
+ context('when table has partition', () => {
+ it('returns table with partition parameters', () => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+ const level1 = new TreeNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'table',
+ });
+ newTree.addChild(root, level1);
+ newTree.addChild(level1, new TreeNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'partition',
+ 'some other key': 'some other value',
+ }));
+
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)([{id:'level 2'}]);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ 'table': {
+ 'some key': 'expected value',
+ 'some other key': 'some other value',
+ '_type': 'partition',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+ });
+ });
+
+ context('getTreeNodeHierarchy is called with TreeNode object', () => {
+ let treeNode;
+ describe('When the current node is root element', () => {
+ beforeEach(() => {
+ treeNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+ });
+
+ it('returns a object with the element type passed data and priority == 0', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+
+ describe('When the current node is not of a known type', () => {
+ beforeEach(() => {
+ treeNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'do not exist',
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node type has no id', () => {
+ beforeEach(() => {
+ treeNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'no id',
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node is at the second level', () => {
+ beforeEach(() => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+ treeNode = new TreeNode('first child', {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ });
+ newTree.addChild(root, treeNode);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'child special': {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ 'priority': 0,
+ },
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+
+ context('When tree as "special type"', () => {
+ context('When "special type" have "other type"', () => {
+ context('When "other type" have "special type"', () => {
+ describe('When retrieving lead node', () => {
+ it('does not override previous node type data', () => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+ const level1 = new TreeNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'other type',
+ });
+ newTree.addChild(root, level1);
+ treeNode = new TreeNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ });
+ newTree.addChild(level1, treeNode);
+
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ 'priority': 0,
+ },
+ 'other type': {
+ 'some key': 'some value',
+ '_type': 'other type',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+ });
+ });
+ });
+
+ context('When tree has table', () => {
+ context('when table has partition', () => {
+ it('returns table with partition parameters', () => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ });
+ const level1 = newTree.addNewNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'table',
+ });
+ newTree.addChild(root, level1);
+ treeNode = new TreeNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'partition',
+ 'some other key': 'some other value',
+ });
+ newTree.addChild(level1, treeNode);
+
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ 'table': {
+ 'some key': 'expected value',
+ 'some other key': 'some other value',
+ '_type': 'partition',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/tree/tree_fake.js b/web/regression/javascript/tree/tree_fake.js
index b285a45f..e03d71fb 100644
--- a/web/regression/javascript/tree/tree_fake.js
+++ b/web/regression/javascript/tree/tree_fake.js
@@ -10,6 +10,32 @@
import {Tree} from '../../../pgadmin/static/js/tree/tree';
export class TreeFake extends Tree {
+ static build(structure) {
+ let tree = new TreeFake();
+ let rootNode = tree.rootNode;
+
+ if (structure.children !== undefined) {
+ structure.children.forEach((child) => {
+ TreeFake.recursivelyAddNodes(tree, child, rootNode);
+ });
+ }
+
+ return tree;
+ }
+
+ static recursivelyAddNodes(tree, newNode, parent) {
+ let id = newNode.id;
+ let data = newNode.data ? newNode.data : {};
+ let domNode = newNode.domNode ? newNode.domNode : [{id: id}];
+ tree.addNewNode(id, data, domNode, tree.translateTreeNodeIdFromACITree([parent]));
+
+ if (newNode.children !== undefined) {
+ newNode.children.forEach((child) => {
+ TreeFake.recursivelyAddNodes(tree, child, newNode);
+ });
+ }
+ }
+
constructor() {
super();
this.aciTreeToOurTreeTranslator = {};
@@ -45,7 +71,7 @@ export class TreeFake extends Tree {
}
translateTreeNodeIdFromACITree(aciTreeNode) {
- if(aciTreeNode === undefined || aciTreeNode[0] === undefined) {
+ if (aciTreeNode === undefined || aciTreeNode[0] === undefined) {
return null;
}
return this.aciTreeToOurTreeTranslator[aciTreeNode[0].id];
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-15 15:03 Joao De Almeida Pereira <[email protected]>
parent: Ashesh Vashi <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Joao De Almeida Pereira @ 2018-05-15 15:03 UTC (permalink / raw)
To: Ashesh Vashi <[email protected]>; +Cc: Anthony Emengo <[email protected]>; Dave Page <[email protected]>; Akshay Joshi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
Hello Ashesh,
These are our comments to the patch:
-
Can you explain the reasoning behind the change you did on the canCreate
function?
Bind creates a new instance of a function, do we really need that?
-
isValidTreeNodeData
1. If it is not Null or Undefined it really means that the data is
Valid? Shouldn’t it be isDataDefined?
2. This looks like a generic function that could be used with objects
of any type not specific to TreeNodeData. So the file location
doesn’t look
correct.
-
The tree folder is just a Tree that we use to store information. The
menu uses a Tree but the 2 things should be separated.
In our point of view the current entanglement of the ACITree into our
code came from missing concepts into a single place (Menu + Storage of
information).
The idea behind having the Tree as a separate block was to ensure that
we do not have the Menu and Tree coupling.
-
supportedNodesMenu.enabled what it does it check if a Node Menu should
be enabled or not. The name of it maybe should be nodeMenu.enabled?
Thanks
Victoria & Joao
On Tue, May 15, 2018 at 6:37 AM Ashesh Vashi <[email protected]>
wrote:
> Hi Joao,
> On Mon, May 14, 2018 at 6:11 PM, Ashesh Vashi <
> [email protected]> wrote:
>
>>
>> On Mon, May 14, 2018 at 6:10 PM, Dave Page <[email protected]> wrote:
>>
>>>
>>>
>>> On Mon, May 14, 2018 at 1:38 PM, Ashesh Vashi <
>>> [email protected]> wrote:
>>>
>>>> On Mon, May 14, 2018 at 2:59 PM, Dave Page <[email protected]> wrote:
>>>>
>>>>>
>>>>>
>>>>> On Sat, May 12, 2018 at 12:10 AM, Ashesh Vashi <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> On Sat, May 12, 2018, 02:58 Joao De Almeida Pereira <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hello Ashesh,
>>>>>>>
>>>>>>> 1. In TreeNode, we're keepging the reference of DOMElement, do we
>>>>>>>>> really need it?
>>>>>>>>
>>>>>>>> As of right now, our Tree abstraction acts as an adapter to the
>>>>>>>>> aciTree library. The aciTree library needs the domElement for most of its
>>>>>>>>> functions (setInode, unload, etc). Thus this is the easiest way to
>>>>>>>>> introduce our abstraction and keep the functionality as before - at least
>>>>>>>>> until we decide that whether we want to switch out the library or not.
>>>>>>>>
>>>>>>>> I understand that. But - I've not seen any reference of domElement
>>>>>>>> the code yet, hence - pointed that out.
>>>>>>>
>>>>>>> If you look at the function: reload, unload you will see that
>>>>>>> domNode is used to communicate with the ACITree
>>>>>>>
>>>>>>>
>>>>>>>> 2. Are you expecting the tree class to be a singleton class
>>>>>>>>
>>>>>>>> Since this tree is referenced throughout the codebase, considering
>>>>>>>>> it to be a singleton seems like the most appropriate pattern for this
>>>>>>>>> usecase. It is very much the same way how we create a single instance of
>>>>>>>>> the aciTree library and use that throughout the codebase. Moreover, it
>>>>>>>>> opens up opportunities to improve performance, for example caching lockups
>>>>>>>>> of nodes. I’m not a fan of singletons myself, but I feel like we’re simply
>>>>>>>>> keeping the architecture the same in the instance.
>>>>>>>>
>>>>>>>> Yeah - I don't see any usage of tree object from anywhere.
>>>>>>>> And, we're already creating new object in browser.js (and, not
>>>>>>>> utitlizing that instance anywhere.)
>>>>>>>
>>>>>>>
>>>>>>> You are right, we do not need to export tree as a singleton for now.
>>>>>>> The line that exports the variable tree can be remove when applying
>>>>>>> the patch number 2.
>>>>>>>
>>>>>>>
>>>>>>> I think we addressed all the concern raised about this patch. Does
>>>>>>> this mean that the patch is going to get committed?
>>>>>>>
>>>>>> Yes - from me for 0002.
>>>>>>
>>>>>
>>>>> Can you do that today?
>>>>>
>>>> Done.
>>>>
>>>
>>> Great, thanks!
>>>
>>> On to patch 0003 then :-)
>>>
>> Yes - already working on it! :-)
>>
> Majority part of the 0003 patch looks good to me.
> Except choice of the path of some of the file, and name of the functions.
>
> Please find the updated patch.
> I've moved files under the 'pgadmin/static/js/menu' directory under the
> 'pgadmin/static/js/tree', as they're using tree functionalities directly.
>
> Please review it, and let me know your concern.
>
> -- Thanks, Ashesh
>
>>
>> -- Thanks, Ashesh
>>
>>>
>>> --
>>> Dave Page
>>> Blog: http://pgsnake.blogspot.com
>>> Twitter: @pgsnake
>>>
>>> EnterpriseDB UK: http://www.enterprisedb.com
>>> The Enterprise PostgreSQL Company
>>>
>>
>>
>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-16 07:20 Ashesh Vashi <[email protected]>
parent: Joao De Almeida Pereira <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Ashesh Vashi @ 2018-05-16 07:20 UTC (permalink / raw)
To: Joao De Almeida Pereira <[email protected]>; +Cc: Anthony Emengo <[email protected]>; Dave Page <[email protected]>; Akshay Joshi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
Hi Joao,
On Tue, May 15, 2018 at 8:33 PM, Joao De Almeida Pereira <
[email protected]> wrote:
> Hello Ashesh,
>
> These are our comments to the patch:
>
>
> -
>
> Can you explain the reasoning behind the change you did on the
> canCreate function?
>
> I do agree as Akshay mentioned ealier, canCreate.canCreate does not look
right to me too, and neither directory structure, nor name of the script
does not make sense to me.
As those functions are always going to apply on the tree nodes only, moved
them under the 'sources/tree' directory, and renamed it as 'node_menu'.
>
> -
>
> Bind creates a new instance of a function, do we really need that?
>
> If you look at the original proposed patch, the implementation looks like
this.
*canCreate: function(itemData, item, data) {*
* return canCreate.canCreate(pgBrowser, <collection_node_type>, item,
data);*
*}*
Here - we're already creating an anonymous function, which is an object
under the hood.
And - for every tree node, they will be a separate anonymous function
anyway.
Why not use better object oriented approach?
>
> -
>
> isValidTreeNodeData
> 1. If it is not Null or Undefined it really means that the data is
> Valid? Shouldn’t it be isDataDefined?
> 2. This looks like a generic function that could be used with
> objects of any type not specific to TreeNodeData. So the file location
> doesn’t look correct.
>
> I renamed it as isValidTreeNodeData, because - we were using it in for
testing the tree data.
I do agree - it is a generic function that could be used with objects of
any type.
But - when I moved the code, the function name was
'isProvidedDataValid(...)', which was present in
'sources/menu/menu_enabled' file, which was definitely not right place. :-)
Please suggest me the right place, and name.
>
> - The tree folder is just a Tree that we use to store information. The
> menu uses a Tree but the 2 things should be separated.
>
> I think - you're missing the point.
Here - we're dealing with two types of menus:
1. Contextual menu (which will always depend on the current selected tree
node)
2. Normal menus
>
> -
>
> In our point of view the current entanglement of the ACITree into our
> code came from missing concepts into a single place (Menu + Storage of
> information).
> The idea behind having the Tree as a separate block was to ensure that
> we do not have the Menu and Tree coupling.
>
> In my opinion - keeping them in them in different directories/files does
not make them decoupled to be honest.
The original patch was separating them in different places, but - still
uses some of the functionalities directly from the tree, which was
happening because we have contextual menu.
To give a better solution, I can think of putting the menus related code
understand 'sources/tree/menu' directory.
That will give clear distiguation between actual tree, and dependent code.
What do you say?
>
> -
>
> supportedNodesMenu.enabled what it does it check if a Node Menu should
> be enabled or not. The name of it maybe should be nodeMenu.enabled?
>
> nodeMenu.enabled(...) looks to general to me.
How about nodeMenu.isSupportedNode(...)?
If you look at the implementation of the function, it checks for the
current tree node is one of the supported nodes, or not.
-- Thanks, Ashesh
>
>
>
> Thanks
> Victoria & Joao
>
> On Tue, May 15, 2018 at 6:37 AM Ashesh Vashi <
> [email protected]> wrote:
>
>> Hi Joao,
>> On Mon, May 14, 2018 at 6:11 PM, Ashesh Vashi <
>> [email protected]> wrote:
>>
>>>
>>> On Mon, May 14, 2018 at 6:10 PM, Dave Page <[email protected]> wrote:
>>>
>>>>
>>>>
>>>> On Mon, May 14, 2018 at 1:38 PM, Ashesh Vashi <
>>>> [email protected]> wrote:
>>>>
>>>>> On Mon, May 14, 2018 at 2:59 PM, Dave Page <[email protected]> wrote:
>>>>>
>>>>>>
>>>>>>
>>>>>> On Sat, May 12, 2018 at 12:10 AM, Ashesh Vashi <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> On Sat, May 12, 2018, 02:58 Joao De Almeida Pereira <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hello Ashesh,
>>>>>>>>
>>>>>>>> 1. In TreeNode, we're keepging the reference of DOMElement, do we
>>>>>>>>>> really need it?
>>>>>>>>>
>>>>>>>>> As of right now, our Tree abstraction acts as an adapter to the
>>>>>>>>>> aciTree library. The aciTree library needs the domElement for most of its
>>>>>>>>>> functions (setInode, unload, etc). Thus this is the easiest way to
>>>>>>>>>> introduce our abstraction and keep the functionality as before - at least
>>>>>>>>>> until we decide that whether we want to switch out the library or not.
>>>>>>>>>
>>>>>>>>> I understand that. But - I've not seen any reference of domElement
>>>>>>>>> the code yet, hence - pointed that out.
>>>>>>>>
>>>>>>>> If you look at the function: reload, unload you will see that
>>>>>>>> domNode is used to communicate with the ACITree
>>>>>>>>
>>>>>>>>
>>>>>>>>> 2. Are you expecting the tree class to be a singleton class
>>>>>>>>>
>>>>>>>>> Since this tree is referenced throughout the codebase, considering
>>>>>>>>>> it to be a singleton seems like the most appropriate pattern for this
>>>>>>>>>> usecase. It is very much the same way how we create a single instance of
>>>>>>>>>> the aciTree library and use that throughout the codebase. Moreover, it
>>>>>>>>>> opens up opportunities to improve performance, for example caching lockups
>>>>>>>>>> of nodes. I’m not a fan of singletons myself, but I feel like we’re simply
>>>>>>>>>> keeping the architecture the same in the instance.
>>>>>>>>>
>>>>>>>>> Yeah - I don't see any usage of tree object from anywhere.
>>>>>>>>> And, we're already creating new object in browser.js (and, not
>>>>>>>>> utitlizing that instance anywhere.)
>>>>>>>>
>>>>>>>>
>>>>>>>> You are right, we do not need to export tree as a singleton for
>>>>>>>> now. The line that exports the variable tree can be remove when
>>>>>>>> applying the patch number 2.
>>>>>>>>
>>>>>>>>
>>>>>>>> I think we addressed all the concern raised about this patch. Does
>>>>>>>> this mean that the patch is going to get committed?
>>>>>>>>
>>>>>>> Yes - from me for 0002.
>>>>>>>
>>>>>>
>>>>>> Can you do that today?
>>>>>>
>>>>> Done.
>>>>>
>>>>
>>>> Great, thanks!
>>>>
>>>> On to patch 0003 then :-)
>>>>
>>> Yes - already working on it! :-)
>>>
>> Majority part of the 0003 patch looks good to me.
>> Except choice of the path of some of the file, and name of the functions.
>>
>> Please find the updated patch.
>> I've moved files under the 'pgadmin/static/js/menu' directory under the
>> 'pgadmin/static/js/tree', as they're using tree functionalities directly.
>>
>> Please review it, and let me know your concern.
>>
>> -- Thanks, Ashesh
>>
>>>
>>> -- Thanks, Ashesh
>>>
>>>>
>>>> --
>>>> Dave Page
>>>> Blog: http://pgsnake.blogspot.com
>>>> Twitter: @pgsnake
>>>>
>>>> EnterpriseDB UK: http://www.enterprisedb.com
>>>> The Enterprise PostgreSQL Company
>>>>
>>>
>>>
>>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-16 15:25 Anthony Emengo <[email protected]>
parent: Ashesh Vashi <[email protected]>
0 siblings, 2 replies; 69+ messages in thread
From: Anthony Emengo @ 2018-05-16 15:25 UTC (permalink / raw)
To: Ashesh Vashi <[email protected]>; +Cc: Joao De Almeida Pereira <[email protected]>; Dave Page <[email protected]>; Akshay Joshi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
export function canCreate(pgBrowser, childOfCatalogType) {
return canCreateObject.bind({
browser: pgBrowser,
childOfCatalogType: childOfCatalogType,
});
}
With respect to the above code: this bind pattern looks good and seems like
the idiomatic way to handle this in JavaScript. On a lighter node, I don’t
even see the need for an additional method to wrap it. The invocation could
have easily been like canCreate: canCreateObject.bind({ browser: pgBrowser,
childOfCatalogType: childOfCatalogType }), I don’t feel too strongly here.
I renamed it as isValidTreeNodeData, because - we were using it in for
testing the tree data. Please suggest me the right place, and name.
We’re not sure; maybe after continued refactoring, we will come across more
generic functions. At that point we can revisit this and create a utils.js
file.
The original patch was separating them in different places, but - still
uses some of the functionalities directly from the tree, which was
happening because we have contextual menu.
To give a better solution, I can think of putting the menus related code
understand ‘sources/tree/menu’ directory.
We’re particularly worried because we’re trying to avoid the coupling that
we see in the code base today. We want to decouple *application state*
from *business
domain* logic as much as we can - because this makes the code much easier
to understand. We achieve lower coupling by have more suitable interfaces
to retrieve *application state* like: anyParent (the menu doesn’t care how
this happens). This is the direction that we’re trying to move towards, we
just don’t want the package structure to undermine that developer intent.
How about nodeMenu.isSupportedNode(…)?
Naming is one of the hardest problems in programming. I don’t feel too
strongly about this one. For now, let’s keep it as is
Thanks
Anthony && Victoria
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-21 13:46 Anthony Emengo <[email protected]>
parent: Anthony Emengo <[email protected]>
1 sibling, 1 reply; 69+ messages in thread
From: Anthony Emengo @ 2018-05-21 13:46 UTC (permalink / raw)
To: Ashesh Vashi <[email protected]>; +Cc: Joao De Almeida Pereira <[email protected]>; Dave Page <[email protected]>; Akshay Joshi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
Hey all,
We haven't heard from you all in a while regarding our last statements. Is
there any thing that I need to clarify? We feel left in the dark here and
just want to know what we can do help.
Cheers!
Anthony && Joao
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-21 13:56 Dave Page <[email protected]>
parent: Anthony Emengo <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Dave Page @ 2018-05-21 13:56 UTC (permalink / raw)
To: Anthony Emengo <[email protected]>; +Cc: Ashesh Vashi <[email protected]>; Joao De Almeida Pereira <[email protected]>; Akshay Joshi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
Ashesh, please prioritise this so we can move on.
Thanks.
On Mon, May 21, 2018 at 2:46 PM, Anthony Emengo <[email protected]> wrote:
> Hey all,
>
> We haven't heard from you all in a while regarding our last statements. Is
> there any thing that I need to clarify? We feel left in the dark here and
> just want to know what we can do help.
>
> Cheers!
> Anthony && Joao
>
--
Dave Page
Blog: http://pgsnake.blogspot.com
Twitter: @pgsnake
EnterpriseDB UK: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-23 14:14 Joao De Almeida Pereira <[email protected]>
parent: Dave Page <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Joao De Almeida Pereira @ 2018-05-23 14:14 UTC (permalink / raw)
To: Dave Page <[email protected]>; +Cc: Anthony Emengo <[email protected]>; Ashesh Vashi <[email protected]>; Akshay Joshi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
Hello Hackers,
Just to remind that if you need some more information from our side you can
ping us.
Can we expect and email tomorrow morning with the commit?
Thanks
Victoria & Joao
On Mon, May 21, 2018 at 9:56 AM Dave Page <[email protected]> wrote:
> Ashesh, please prioritise this so we can move on.
>
> Thanks.
>
> On Mon, May 21, 2018 at 2:46 PM, Anthony Emengo <[email protected]>
> wrote:
>
>> Hey all,
>>
>> We haven't heard from you all in a while regarding our last statements.
>> Is there any thing that I need to clarify? We feel left in the dark here
>> and just want to know what we can do help.
>>
>> Cheers!
>> Anthony && Joao
>>
>
>
>
> --
> Dave Page
> Blog: http://pgsnake.blogspot.com
> Twitter: @pgsnake
>
> EnterpriseDB UK: http://www.enterprisedb.com
> The Enterprise PostgreSQL Company
>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-23 14:16 Dave Page <[email protected]>
parent: Joao De Almeida Pereira <[email protected]>
0 siblings, 0 replies; 69+ messages in thread
From: Dave Page @ 2018-05-23 14:16 UTC (permalink / raw)
To: Joao De Almeida Pereira <[email protected]>; +Cc: Anthony Emengo <[email protected]>; Ashesh Vashi <[email protected]>; Akshay Joshi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
I spoke to Ashesh earlier - he's working on an update to the proposed patch
at the moment and will get back to you ASAP.
On Wed, May 23, 2018 at 3:14 PM, Joao De Almeida Pereira <
[email protected]> wrote:
> Hello Hackers,
>
> Just to remind that if you need some more information from our side you
> can ping us.
> Can we expect and email tomorrow morning with the commit?
>
> Thanks
> Victoria & Joao
>
> On Mon, May 21, 2018 at 9:56 AM Dave Page <[email protected]> wrote:
>
>> Ashesh, please prioritise this so we can move on.
>>
>> Thanks.
>>
>> On Mon, May 21, 2018 at 2:46 PM, Anthony Emengo <[email protected]>
>> wrote:
>>
>>> Hey all,
>>>
>>> We haven't heard from you all in a while regarding our last statements.
>>> Is there any thing that I need to clarify? We feel left in the dark here
>>> and just want to know what we can do help.
>>>
>>> Cheers!
>>> Anthony && Joao
>>>
>>
>>
>>
>> --
>> Dave Page
>> Blog: http://pgsnake.blogspot.com
>> Twitter: @pgsnake
>>
>> EnterpriseDB UK: http://www.enterprisedb.com
>> The Enterprise PostgreSQL Company
>>
>
--
Dave Page
Blog: http://pgsnake.blogspot.com
Twitter: @pgsnake
EnterpriseDB UK: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-24 06:59 Ashesh Vashi <[email protected]>
parent: Anthony Emengo <[email protected]>
1 sibling, 1 reply; 69+ messages in thread
From: Ashesh Vashi @ 2018-05-24 06:59 UTC (permalink / raw)
To: Anthony Emengo <[email protected]>; +Cc: Joao De Almeida Pereira <[email protected]>; Dave Page <[email protected]>; Akshay Joshi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
Sorry for the late reply.
On Wed, May 16, 2018 at 8:55 PM, Anthony Emengo <[email protected]> wrote:
> export function canCreate(pgBrowser, childOfCatalogType) {
> return canCreateObject.bind({
> browser: pgBrowser,
> childOfCatalogType: childOfCatalogType,
> });
> }
>
> With respect to the above code: this bind pattern looks good and seems
> like the idiomatic way to handle this in JavaScript. On a lighter node, I
> don’t even see the need for an additional method to wrap it. The invocation
> could have easily been like canCreate: canCreateObject.bind({ browser:
> pgBrowser, childOfCatalogType: childOfCatalogType }), I don’t feel too
> strongly here.
>
I do agree - we can handle the same problem many ways.
I prefer object oriented pardigm more in general.
Any way - I have modified the code with some other changes.
> I renamed it as isValidTreeNodeData, because - we were using it in for
> testing the tree data. Please suggest me the right place, and name.
>
> We’re not sure; maybe after continued refactoring, we will come across
> more generic functions. At that point we can revisit this and create a
> utils.js file.
>
Sure.
> The original patch was separating them in different places, but - still
> uses some of the functionalities directly from the tree, which was
> happening because we have contextual menu.
> To give a better solution, I can think of putting the menus related code
> understand ‘sources/tree/menu’ directory.
>
> We’re particularly worried because we’re trying to avoid the coupling that
> we see in the code base today. We want to decouple *application state*
> from *business domain* logic as much as we can - because this makes the
> code much easier to understand. We achieve lower coupling by have more
> suitable interfaces to retrieve *application state* like: anyParent (the
> menu doesn’t care how this happens). This is the direction that we’re
> trying to move towards, we just don’t want the package structure to
> undermine that developer intent.
>
I realized after revisiting the code, menu/can_create.js was only
applicable to the children of the schema/catalog nodes, same as
'can_drop_child'.
We should have put both scripts in the same directory.
Please find the updated patch for the same.
Please review it, and let me know your concerns.
-- Thanks, Ashesh
> How about nodeMenu.isSupportedNode(…)?
>
> Naming is one of the hardest problems in programming. I don’t feel too
> strongly about this one. For now, let’s keep it as is
>
> Thanks
> Anthony && Victoria
>
>
>
>
>
Attachments:
[application/octet-stream] 0001-Extract-test-and-refactor-methods.patch (279.2K, 3-0001-Extract-test-and-refactor-methods.patch)
download | inline diff:
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.js
index 9015d8d2..7fd28a7c 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.js
@@ -1,8 +1,8 @@
define('pgadmin.node.collation', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
- 'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser) {
+ 'pgadmin.node.schema.dir/child', 'pgadmin.browser.collection',
+], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, schemaChild) {
if (!pgBrowser.Nodes['coll-collation']) {
pgAdmin.Browser.Nodes['coll-collation'] =
@@ -15,7 +15,7 @@ define('pgadmin.node.collation', [
}
if (!pgBrowser.Nodes['collation']) {
- pgAdmin.Browser.Nodes['collation'] = pgBrowser.Node.extend({
+ pgAdmin.Browser.Nodes['collation'] = schemaChild.SchemaChildNode.extend({
type: 'collation',
sqlAlterHelp: 'sql-altercollation.html',
sqlCreateHelp: 'sql-createcollation.html',
@@ -222,34 +222,6 @@ define('pgadmin.node.collation', [
return true;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create collation
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-collation' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.js
index 403ca471..a91daa5f 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.js
@@ -2,9 +2,10 @@
define('pgadmin.node.domain', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid',
- 'pgadmin.browser.collection',
+ 'pgadmin.node.schema.dir/child', 'pgadmin.browser.collection',
], function(
- gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid
+ gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid,
+ schemaChild
) {
// Define Domain Collection Node
@@ -79,7 +80,7 @@ define('pgadmin.node.domain', [
// Domain Node
if (!pgBrowser.Nodes['domain']) {
- pgBrowser.Nodes['domain'] = pgBrowser.Node.extend({
+ pgBrowser.Nodes['domain'] = schemaChild.SchemaChildNode.extend({
type: 'domain',
sqlAlterHelp: 'sql-alterdomain.html',
sqlCreateHelp: 'sql-createdomain.html',
@@ -88,7 +89,6 @@ define('pgadmin.node.domain', [
collection_type: 'coll-domain',
hasSQL: true,
hasDepends: true,
- parent_type: ['schema', 'catalog'],
Init: function() {
// Avoid mulitple registration of menus
if (this.initialized)
@@ -118,8 +118,6 @@ define('pgadmin.node.domain', [
]);
},
- canDrop: pgBrowser.Nodes['schema'].canChildDrop,
- canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
// Domain Node Model
model: pgBrowser.Node.Model.extend({
initialize: function(attrs, args) {
@@ -296,34 +294,6 @@ define('pgadmin.node.domain', [
return errmsg;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create domain
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-domain' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
isDisabled: function(m){
if (!m.isNew()) {
var server = this.node_info.server;
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table.js
index 160db83f..24f5e1a7 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table.js
@@ -2,9 +2,10 @@
define('pgadmin.node.foreign_table', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid',
- 'pgadmin.browser.collection',
+ 'pgadmin.node.schema.dir/child', 'pgadmin.browser.collection',
], function(
- gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid
+ gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid,
+ schemaChild
) {
if (!pgBrowser.Nodes['coll-foreign_table']) {
@@ -469,7 +470,7 @@ define('pgadmin.node.foreign_table', [
if (!pgBrowser.Nodes['foreign_table']) {
- pgBrowser.Nodes['foreign_table'] = pgBrowser.Node.extend({
+ pgBrowser.Nodes['foreign_table'] = schemaChild.SchemaChildNode.extend({
type: 'foreign_table',
sqlAlterHelp: 'sql-alterforeigntable.html',
sqlCreateHelp: 'sql-createforeigntable.html',
@@ -509,8 +510,6 @@ define('pgadmin.node.foreign_table', [
]);
},
- canDrop: pgBrowser.Nodes['schema'].canChildDrop,
- canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
model: pgBrowser.Node.Model.extend({
initialize: function(attrs, args) {
var isNew = (_.size(attrs) === 0);
@@ -659,34 +658,6 @@ define('pgadmin.node.foreign_table', [
return errmsg;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create foreign table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-foreign_table' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.js
index 89806681..cba62789 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.js
@@ -1,9 +1,10 @@
define('pgadmin.node.fts_configuration', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid',
- 'pgadmin.browser.collection',
+ 'pgadmin.node.schema.dir/child', 'pgadmin.browser.collection',
], function(
- gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid
+ gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid,
+ schemaChild
) {
// Model for tokens control
@@ -410,14 +411,11 @@ define('pgadmin.node.fts_configuration', [
// Extend the node class for FTS Configuration
if (!pgBrowser.Nodes['fts_configuration']) {
- pgAdmin.Browser.Nodes['fts_configuration'] = pgAdmin.Browser.Node.extend({
- parent_type: ['schema', 'catalog'],
+ pgAdmin.Browser.Nodes['fts_configuration'] = schemaChild.SchemaChildNode.extend({
type: 'fts_configuration',
sqlAlterHelp: 'sql-altertsconfig.html',
sqlCreateHelp: 'sql-createtsconfig.html',
dialogHelp: url_for('help.static', {'filename': 'fts_configuration_dialog.html'}),
- canDrop: true,
- canDropCascade: true,
label: gettext('FTS Configuration'),
hasSQL: true,
hasDepends: true,
@@ -577,34 +575,6 @@ define('pgadmin.node.fts_configuration', [
return null;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create fts configuration
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-fts_configuration' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary.js
index ed83feb1..cf733922 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary.js
@@ -1,8 +1,10 @@
define('pgadmin.node.fts_dictionary', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
- 'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform) {
+ 'pgadmin.node.schema.dir/child', 'pgadmin.browser.collection',
+], function(
+ gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform, schemaChild
+) {
// Extend the browser's node model class to create a option/value pair
var OptionLabelModel = pgAdmin.Browser.Node.Model.extend({
@@ -55,14 +57,11 @@ define('pgadmin.node.fts_dictionary', [
// Extend the node class for FTS Dictionary
if (!pgBrowser.Nodes['fts_dictionary']) {
- pgAdmin.Browser.Nodes['fts_dictionary'] = pgAdmin.Browser.Node.extend({
- parent_type: ['schema', 'catalog'],
+ pgAdmin.Browser.Nodes['fts_dictionary'] = schemaChild.SchemaChildNode.extend({
type: 'fts_dictionary',
sqlAlterHelp: 'sql-altertsdictionary.html',
sqlCreateHelp: 'sql-createtsdictionary.html',
dialogHelp: url_for('help.static', {'filename': 'fts_dictionary_dialog.html'}),
- canDrop: true,
- canDropCascade: true,
label: gettext('FTS Dictionary'),
hasSQL: true,
hasDepends: true,
@@ -186,34 +185,6 @@ define('pgadmin.node.fts_dictionary', [
return null;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create fts dictionary
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-fts_dictionary' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser.js
index 92c0786e..bebc9dc5 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser.js
@@ -1,7 +1,8 @@
define('pgadmin.node.fts_parser', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
- 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser) {
+ 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.node.schema.dir/child',
+ 'pgadmin.browser.collection',
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, schemaChild) {
// Extend the collection class for fts parser
if (!pgBrowser.Nodes['coll-fts_parser']) {
@@ -16,14 +17,11 @@ define('pgadmin.node.fts_parser', [
// Extend the node class for fts parser
if (!pgBrowser.Nodes['fts_parser']) {
- pgAdmin.Browser.Nodes['fts_parser'] = pgAdmin.Browser.Node.extend({
- parent_type: ['schema', 'catalog'],
+ pgAdmin.Browser.Nodes['fts_parser'] = schemaChild.SchemaChildNode.extend({
type: 'fts_parser',
sqlAlterHelp: 'sql-altertsparser.html',
sqlCreateHelp: 'sql-createtsparser.html',
dialogHelp: url_for('help.static', {'filename': 'fts_parser_dialog.html'}),
- canDrop: true,
- canDropCascade: true,
label: gettext('FTS Parser'),
hasSQL: true,
hasDepends: true,
@@ -199,34 +197,6 @@ define('pgadmin.node.fts_parser', [
return null;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create fts parser
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-fts_parser' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template.js
index 606a57a6..cd0207ab 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template.js
@@ -1,7 +1,8 @@
define('pgadmin.node.fts_template', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
- 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser) {
+ 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.node.schema.dir/child',
+ 'pgadmin.browser.collection',
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, schemaChild) {
// Extend the collection class for fts template
if (!pgBrowser.Nodes['coll-fts_template']) {
@@ -16,14 +17,11 @@ define('pgadmin.node.fts_template', [
// Extend the node class for fts template
if (!pgBrowser.Nodes['fts_template']) {
- pgAdmin.Browser.Nodes['fts_template'] = pgAdmin.Browser.Node.extend({
- parent_type: ['schema', 'catalog'],
+ pgAdmin.Browser.Nodes['fts_template'] = schemaChild.SchemaChildNode.extend({
type: 'fts_template',
sqlAlterHelp: 'sql-altertstemplate.html',
sqlCreateHelp: 'sql-createtstemplate.html',
dialogHelp: url_for('help.static', {'filename': 'fts_template_dialog.html'}),
- canDrop: true,
- canDropCascade: true,
label: gettext('FTS Template'),
hasSQL: true,
hasDepends: true,
@@ -139,34 +137,6 @@ define('pgadmin.node.fts_template', [
return null;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create fts fts_template
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-fts_template' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function.js
index 6e405165..c4cd91aa 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function.js
@@ -2,8 +2,11 @@
define('pgadmin.node.function', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
- 'pgadmin.browser.collection', 'pgadmin.browser.server.privilege',
-], function(gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform) {
+ 'pgadmin.node.schema.dir/child', 'pgadmin.browser.collection',
+ 'pgadmin.browser.server.privilege',
+], function(
+ gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, schemaChild
+) {
if (!pgBrowser.Nodes['coll-function']) {
pgBrowser.Nodes['coll-function'] =
@@ -83,7 +86,8 @@ define('pgadmin.node.function', [
});
if (!pgBrowser.Nodes['function']) {
- pgBrowser.Nodes['function'] = pgBrowser.Node.extend({
+
+ pgBrowser.Nodes['function'] = schemaChild.SchemaChildNode.extend({
type: 'function',
sqlAlterHelp: 'sql-alterfunction.html',
sqlCreateHelp: 'sql-createfunction.html',
@@ -96,7 +100,6 @@ define('pgadmin.node.function', [
return treeInformation.server.server_type !== 'gpdb';
},
hasScriptTypes: ['create', 'select'],
- parent_type: ['schema', 'catalog'],
Init: function() {
/* Avoid mulitple registration of menus */
if (this.initialized)
@@ -126,8 +129,6 @@ define('pgadmin.node.function', [
]);
},
- canDrop: pgBrowser.Nodes['schema'].canChildDrop,
- canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
model: pgBrowser.Node.Model.extend({
initialize: function(attrs, args) {
var isNew = (_.size(attrs) === 0);
@@ -438,34 +439,6 @@ define('pgadmin.node.function', [
return true;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create Function
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-function' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js
index aeb8271b..fcdf28fb 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js
@@ -2,8 +2,11 @@
define('pgadmin.node.trigger_function', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
- 'pgadmin.browser.collection', 'pgadmin.browser.server.privilege',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform) {
+ 'pgadmin.node.schema.dir/child', 'pgadmin.browser.collection',
+ 'pgadmin.browser.server.privilege',
+], function(
+ gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, schemaChild
+) {
if (!pgBrowser.Nodes['coll-trigger_function']) {
pgBrowser.Nodes['coll-trigger_function'] =
@@ -17,7 +20,7 @@ define('pgadmin.node.trigger_function', [
}
if (!pgBrowser.Nodes['trigger_function']) {
- pgBrowser.Nodes['trigger_function'] = pgBrowser.Node.extend({
+ pgBrowser.Nodes['trigger_function'] = schemaChild.SchemaChildNode.extend({
type: 'trigger_function',
sqlAlterHelp: 'plpgsql-trigger.html',
sqlCreateHelp: 'plpgsql-trigger.html',
@@ -27,7 +30,6 @@ define('pgadmin.node.trigger_function', [
hasSQL: true,
hasDepends: true,
hasStatistics: true,
- parent_type: ['schema', 'catalog'],
Init: function() {
/* Avoid mulitple registration of menus */
if (this.initialized)
@@ -57,8 +59,6 @@ define('pgadmin.node.trigger_function', [
]);
},
- canDrop: pgBrowser.Nodes['schema'].canChildDrop,
- canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
model: pgBrowser.Node.Model.extend({
initialize: function(attrs, args) {
var isNew = (_.size(attrs) === 0);
@@ -357,34 +357,6 @@ define('pgadmin.node.trigger_function', [
return !(this.node_info && 'catalog' in this.node_info);
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create Function
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-trigger_function' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence.js
index 57c95acd..300f0b10 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence.js
@@ -1,8 +1,10 @@
define('pgadmin.node.sequence', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
- 'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform) {
+ 'pgadmin.node.schema.dir/child', 'pgadmin.browser.collection',
+], function(
+ gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform, schemaChild
+) {
// Extend the browser's collection class for sequence collection
if (!pgBrowser.Nodes['coll-sequence']) {
@@ -18,7 +20,7 @@ define('pgadmin.node.sequence', [
// Extend the browser's node class for sequence node
if (!pgBrowser.Nodes['sequence']) {
- pgBrowser.Nodes['sequence'] = pgBrowser.Node.extend({
+ pgBrowser.Nodes['sequence'] = schemaChild.SchemaChildNode.extend({
type: 'sequence',
sqlAlterHelp: 'sql-altersequence.html',
sqlCreateHelp: 'sql-createsequence.html',
@@ -28,7 +30,6 @@ define('pgadmin.node.sequence', [
hasSQL: true,
hasDepends: true,
hasStatistics: true,
- parent_type: ['schema', 'catalog'],
Init: function() {
/* Avoid mulitple registration of menus */
if (this.initialized)
@@ -58,36 +59,6 @@ define('pgadmin.node.sequence', [
]);
},
- canDrop: pgBrowser.Nodes['schema'].canChildDrop,
- canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create collation
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-sequence' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we want to allow create menu
- return true;
- },
// Define the model for sequence node.
model: pgBrowser.Node.Model.extend({
defaults: {
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js
new file mode 100644
index 00000000..f8e5951c
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js
@@ -0,0 +1,22 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+/////////////////////////////////////////////////////////////
+
+import * as Node from 'pgbrowser/node';
+import {
+ isTreeItemOfChildOfSchema, childCreateMenuEnabled,
+} from './schema_child_tree_node';
+
+let SchemaChildNode = Node.extend({
+ parent_type: ['schema', 'catalog'],
+ canDrop: isTreeItemOfChildOfSchema,
+ canDropCascade: isTreeItemOfChildOfSchema,
+ canCreate: childCreateMenuEnabled,
+}, false);
+
+export {SchemaChildNode};
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
index a7fd4c7c..3b9b0f35 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
@@ -425,54 +425,10 @@ define('pgadmin.node.schema', [
return null;
},
}),
- // This function will checks whether we can allow user to
- // drop object or not based on location within schema & catalog
- canChildDrop: function(itemData, item) {
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create collation
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if(prev_d && prev_d._type == 'catalog') {
- return false;
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
});
pgBrowser.tableChildTreeNodeHierarchy = function(i) {
- var idx = 0,
- res = {},
- t = pgBrowser.tree;
-
- do {
- var d = t.itemData(i);
- if (
- d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId
- ) {
- if (d._type === 'partition' || d._type === 'table') {
- if (!('table' in res)) {
- res['table'] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- } else {
- res[d._type] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- } while (i);
-
- return res;
+ return this.getTreeNodeHierarchy(i);
};
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js
new file mode 100644
index 00000000..1f67d1af
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js
@@ -0,0 +1,41 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+/////////////////////////////////////////////////////////////
+
+import * as pgBrowser from 'pgbrowser/browser';
+
+export function childCreateMenuEnabled(itemData, item, data) {
+ // If check is false then , we will allow create menu
+ if (data && data.check === false) {
+ return true;
+ }
+
+ let node = pgBrowser.treeMenu.findNodeByDomElement(item);
+
+ if (node)
+ return node.anyFamilyMember(
+ (node) => (node.getData()._type === 'schema')
+ );
+
+ return false;
+}
+
+export function isTreeItemOfChildOfSchema(itemData, item) {
+ let node = pgBrowser.treeMenu.findNodeByDomElement(item);
+
+ if (node)
+ return isTreeNodeOfSchemaChild(node);
+
+ return false;
+}
+
+export function isTreeNodeOfSchemaChild(node) {
+ return node.anyParent(
+ (parentNode) => (parentNode.getData()._type === 'schema')
+ );
+}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js
index 72b6eb0c..4dd804a6 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js
@@ -88,7 +88,6 @@ define('pgadmin.node.column', [
if (!pgBrowser.Nodes['column']) {
pgBrowser.Nodes['column'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table', 'view', 'mview'],
collection_type: ['coll-table', 'coll-view', 'coll-mview'],
type: 'column',
@@ -97,27 +96,24 @@ define('pgadmin.node.column', [
sqlAlterHelp: 'sql-altertable.html',
sqlCreateHelp: 'sql-altertable.html',
dialogHelp: url_for('help.static', {'filename': 'column_dialog.html'}),
- canDrop: function(itemData, item, data){
- if (pgBrowser.Nodes['schema'].canChildDrop.apply(this, [itemData, item, data])) {
- var t = pgBrowser.tree, i = item, d = itemData, parents = [];
- // To iterate over tree to check parent node
- while (i) {
- parents.push(d._type);
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
+ canDrop: function(itemData, item){
+ let node = pgBrowser.treeMenu.findNodeByDomElement(item);
- // Check if menu is allowed ?
- if(_.indexOf(parents, 'catalog') > -1 ||
- _.indexOf(parents, 'view') > -1 ||
- _.indexOf(parents, 'mview') > -1) {
- return false;
- } else if(_.indexOf(parents, 'table') > -1) {
- return true;
- }
- } else {
+ if (!node)
return false;
- }
+
+ // Only a column of a table can be droped, and only when it is not of
+ // catalog.
+ return node.anyParent(
+ (parentNode) => (
+ parentNode.getData()._type === 'table' &&
+ !parentNode.anyParent(
+ (grandParentNode) => (
+ grandParentNode.getData()._type === 'catalog'
+ )
+ )
+ )
+ );
},
hasDepends: true,
hasStatistics: true,
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js
index 857cf4c4..ab28a86b 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js
@@ -8,7 +8,6 @@ define('pgadmin.node.check_constraint', [
// Check Constraint Node
if (!pgBrowser.Nodes['check_constraint']) {
pgAdmin.Browser.Nodes['check_constraint'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'check_constraint',
label: gettext('Check'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js
index 0bbf66a1..adccf2e9 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js
@@ -605,7 +605,6 @@ define('pgadmin.node.exclusion_constraint', [
// Extend the browser's node class for exclusion constraint node
if (!pgBrowser.Nodes['exclusion_constraint']) {
pgAdmin.Browser.Nodes['exclusion_constraint'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'exclusion_constraint',
label: gettext('Exclusion constraint'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js
index 3c4b89f3..9899df92 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js
@@ -603,7 +603,6 @@ define('pgadmin.node.foreign_key', [
// Extend the browser's node class for foreign key node
if (!pgBrowser.Nodes['foreign_key']) {
pgAdmin.Browser.Nodes['foreign_key'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'foreign_key',
label: gettext('Foreign key'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js
index d3a6cff4..0ad0f054 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js
@@ -20,7 +20,6 @@ define('pgadmin.node.primary_key', [
parent_type: ['table','partition'],
canDrop: true,
canDropCascade: true,
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
Init: function() {
/* Avoid multiple registration of menus */
if (this.initialized)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js
index 769185d6..18d3ca33 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js
@@ -20,7 +20,6 @@ define('pgadmin.node.unique_constraint', [
parent_type: ['table','partition'],
canDrop: true,
canDropCascade: true,
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
Init: function() {
/* Avoid multiple registration of menus */
if (this.initialized)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js
index cb242cd2..9c0e24fe 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js
@@ -12,14 +12,12 @@ define('pgadmin.node.constraints', [
node: 'constraints',
label: gettext('Constraints'),
type: 'coll-constraints',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
columns: ['name', 'comment'],
});
}
if (!pgBrowser.Nodes['constraints']) {
pgAdmin.Browser.Nodes['constraints'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'constraints',
label: gettext('Constraints'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js
index ec2b4da1..e58bb463 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js
@@ -1,10 +1,12 @@
define('pgadmin.node.index', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'backbone', 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.alertifyjs',
- 'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser.collection',
+ 'pgadmin.backform', 'pgadmin.backgrid',
+ 'pgadmin.node.schema.dir/schema_child_tree_node',
+ 'pgadmin.browser.collection',
], function(
gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Alertify, Backform,
- Backgrid
+ Backgrid, SchemaChildTreeNode
) {
if (!pgBrowser.Nodes['coll-index']) {
@@ -13,7 +15,6 @@ define('pgadmin.node.index', [
node: 'index',
label: gettext('Indexes'),
type: 'coll-index',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
sqlAlterHelp: 'sql-alterindex.html',
sqlCreateHelp: 'sql-createindex.html',
dialogHelp: url_for('help.static', {'filename': 'index_dialog.html'}),
@@ -215,7 +216,6 @@ define('pgadmin.node.index', [
if (!pgBrowser.Nodes['index']) {
pgAdmin.Browser.Nodes['index'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table', 'view', 'mview', 'partition'],
collection_type: ['coll-table', 'coll-view'],
sqlAlterHelp: 'sql-alterindex.html',
@@ -266,8 +266,8 @@ define('pgadmin.node.index', [
},
]);
},
- canDrop: pgBrowser.Nodes['schema'].canChildDrop,
- canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
+ canDrop: SchemaChildTreeNode.isTreeItemOfChildOfSchema,
+ canDropCascade: SchemaChildTreeNode.isTreeItemOfChildOfSchema,
model: pgAdmin.Browser.Node.Model.extend({
idAttribute: 'oid',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js
index d807304e..74649721 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js
@@ -2,10 +2,12 @@ define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.backgrid',
+ 'pgadmin.node.schema.dir/schema_child_tree_node',
'pgadmin.browser.collection', 'pgadmin.browser.table.partition.utils',
],
function(
- gettext, url_for, $, _, S, pgAdmin, pgBrowser, Alertify, Backform, Backgrid
+ gettext, url_for, $, _, S, pgAdmin, pgBrowser, Alertify, Backform, Backgrid,
+ SchemaChildTreeNode
) {
if (!pgBrowser.Nodes['coll-partition']) {
@@ -13,7 +15,6 @@ function(
pgAdmin.Browser.Collection.extend({
node: 'partition',
label: gettext('Partitions'),
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'coll-partition',
columns: [
'name', 'schema', 'partition_value', 'is_partitioned', 'description',
@@ -80,36 +81,6 @@ function(
},
]);
},
- getTreeNodeHierarchy: function(i) {
- var idx = 0,
- res = {},
- t = pgBrowser.tree;
-
- do {
- var d = t.itemData(i);
- if (
- d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId
- ) {
- if (d._type == 'partition' && 'partition' in res) {
- if (!('table' in res)) {
- res['table'] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- } else if (d._type == 'table') {
- if (!('table' in res)) {
- res['table'] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- } else {
- res[d._type] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- } while (i);
-
- return res;
- },
generate_url: function(item, type, d, with_id, info) {
if (_.indexOf([
'stats', 'statistics', 'dependency', 'dependent', 'reset',
@@ -133,8 +104,8 @@ function(
encodeURIComponent(info['partition']._id)
).value();
},
- canDrop: pgBrowser.Nodes['schema'].canChildDrop,
- canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
+ canDrop: SchemaChildTreeNode.isTreeItemOfChildOfSchema,
+ canDropCascade: SchemaChildTreeNode.isTreeItemOfChildOfSchema,
callbacks: {
/* Enable trigger(s) on table */
enable_triggers_on_table: function(args) {
@@ -1189,34 +1160,7 @@ function(
return data;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-table' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null;
- var prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
+ canCreate: SchemaChildTreeNode.isTreeItemOfChildOfSchema,
// Check to whether table has disable trigger(s)
canCreate_with_trigger_enable: function(itemData, item, data) {
if(this.canCreate.apply(this, [itemData, item, data])) {
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js
index 3af61754..354909f3 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js
@@ -16,7 +16,6 @@ define('pgadmin.node.rule', [
node: 'rule',
label: gettext('Rules'),
type: 'coll-rule',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
columns: ['name', 'owner', 'comment'],
});
}
@@ -35,7 +34,6 @@ define('pgadmin.node.rule', [
*/
if (!pgBrowser.Nodes['rule']) {
pgAdmin.Browser.Nodes['rule'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table','view', 'partition'],
type: 'rule',
sqlAlterHelp: 'sql-alterrule.html',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers.js
new file mode 100644
index 00000000..2d792043
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers.js
@@ -0,0 +1,52 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import axios from 'axios';
+
+export function disableTriggers(tree, alertify, generateUrl, args) {
+ return setTriggers(tree, alertify, generateUrl, args, {enable: 'false' });
+}
+export function enableTriggers(tree, alertify, generateUrl, args) {
+ return setTriggers(tree, alertify, generateUrl, args, {enable: 'true' });
+}
+
+function setTriggers(tree, alertify, generateUrl, args, params) {
+ const treeNode = retrieveTreeNode(args, tree);
+
+ if (!treeNode || treeNode.getData() === null || treeNode.getData() === undefined)
+ return false;
+
+ axios.put(
+ generateUrl(treeNode.getHtmlIdentifier(), 'set_trigger', treeNode.getData(), true),
+ params
+ )
+ .then((res) => {
+ if (res.data.success === 1) {
+ alertify.success(res.data.info);
+ treeNode.reload(tree);
+ }
+ })
+ .catch((xhr) => {
+ try {
+ const err = xhr.response.data;
+ if (err.success === 0) {
+ alertify.error(err.errormsg);
+ }
+ } catch (e) {
+ console.warn(e.stack || e);
+ }
+ treeNode.unload(tree);
+ });
+}
+
+function retrieveTreeNode(args, tree) {
+ const input = args || {};
+ const domElementIdentifier = input.item || tree.selected();
+ return tree.findNodeByDomElement(domElementIdentifier);
+}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
index d440bf04..1f9bcf97 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
@@ -1,13 +1,16 @@
define('pgadmin.node.table', [
+ 'pgadmin.tables.js/enable_disable_triggers',
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.backgrid',
'pgadmin.tables.js/show_advanced_tab',
- 'pgadmin.browser.collection', 'pgadmin.node.column',
- 'pgadmin.node.constraints', 'pgadmin.browser.table.partition.utils',
+ 'pgadmin.node.schema.dir/child', 'pgadmin.browser.collection',
+ 'pgadmin.node.column', 'pgadmin.node.constraints',
+ 'pgadmin.browser.table.partition.utils',
], function(
+ tableFunctions,
gettext, url_for, $, _, S, pgAdmin, pgBrowser, Alertify, Backform, Backgrid,
- ShowAdvancedTab
+ ShowAdvancedTab, SchemaChild
) {
if (!pgBrowser.Nodes['coll-table']) {
@@ -25,8 +28,7 @@ define('pgadmin.node.table', [
}
if (!pgBrowser.Nodes['table']) {
- pgBrowser.Nodes['table'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
+ pgBrowser.Nodes['table'] = SchemaChild.SchemaChildNode.extend({
type: 'table',
label: gettext('Table'),
collection_type: 'coll-table',
@@ -39,7 +41,6 @@ define('pgadmin.node.table', [
sqlAlterHelp: 'sql-altertable.html',
sqlCreateHelp: 'sql-createtable.html',
dialogHelp: url_for('help.static', {'filename': 'table_dialog.html'}),
- parent_type: ['schema', 'catalog'],
hasScriptTypes: ['create', 'select', 'insert', 'update', 'delete'],
height: '95%',
width: '85%',
@@ -113,51 +114,24 @@ define('pgadmin.node.table', [
this.handle_cache, this
);
},
- canDrop: pgBrowser.Nodes['schema'].canChildDrop,
- canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
callbacks: {
/* Enable trigger(s) on table */
enable_triggers_on_table: function(args) {
- var params = {'enable': true };
- this.callbacks.set_triggers.apply(this, [args, params]);
+ tableFunctions.enableTriggers(
+ pgBrowser.treeMenu,
+ Alertify,
+ this.generate_url.bind(this),
+ args
+ );
},
/* Disable trigger(s) on table */
disable_triggers_on_table: function(args) {
- var params = {'enable': false };
- this.callbacks.set_triggers.apply(this, [args, params]);
- },
- set_triggers: function(args, params) {
- // This function will send request to enable or
- // disable triggers on table level
- var input = args || {},
- obj = this,
- t = pgBrowser.tree,
- i = input.item || t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined;
- if (!d)
- return false;
-
- $.ajax({
- url: obj.generate_url(i, 'set_trigger' , d, true),
- type:'PUT',
- data: params,
- dataType: 'json',
- success: function(res) {
- if (res.success == 1) {
- Alertify.success(res.info);
- t.unload(i);
- t.setInode(i);
- t.deselect(i);
- setTimeout(function() {
- t.select(i);
- }, 10);
- }
- },
- error: function(xhr, status, error) {
- Alertify.pgRespErrorNotify(xhr, error);
- t.unload(i);
- },
- });
+ tableFunctions.disableTriggers(
+ pgBrowser.treeMenu,
+ Alertify,
+ this.generate_url.bind(this),
+ args
+ );
},
/* Truncate table */
truncate_table: function(args) {
@@ -1299,55 +1273,15 @@ define('pgadmin.node.table', [
return data;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-table' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
// Check to whether table has disable trigger(s)
canCreate_with_trigger_enable: function(itemData, item, data) {
- if(this.canCreate.apply(this, [itemData, item, data])) {
- // We are here means we can create menu, now let's check condition
- if(itemData.tigger_count > 0) {
- return true;
- } else {
- return false;
- }
- }
+ return itemData.tigger_count > 0 &&
+ this.canCreate.apply(this, [itemData, item, data]);
},
// Check to whether table has enable trigger(s)
canCreate_with_trigger_disable: function(itemData, item, data) {
- if(this.canCreate.apply(this, [itemData, item, data])) {
- // We are here means we can create menu, now let's check condition
- if(itemData.tigger_count > 0 && itemData.has_enable_triggers > 0) {
- return true;
- } else {
- return false;
- }
- }
+ return itemData.tigger_count > 0 && itemData.has_enable_triggers > 0 &&
+ this.canCreate.apply(this, [itemData, item, data]);
},
onTableUpdated: function(_node, _oldNodeData, _newNodeData) {
var key, childIDs;
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js
index a2c27188..a6e79ce2 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js
@@ -1,8 +1,13 @@
define('pgadmin.node.trigger', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
- 'underscore.string', 'sources/pgadmin', 'pgadmin.browser', 'backform', 'pgadmin.alertifyjs',
+ 'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
+ 'pgadmin.backform', 'pgadmin.alertifyjs',
+ 'pgadmin.node.schema.dir/schema_child_tree_node',
'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform, alertify) {
+], function(
+ gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform, alertify,
+ SchemaChildTreeNode
+) {
Backform.CustomSwitchControl = Backform.SwitchControl.extend({
template: _.template([
@@ -29,14 +34,12 @@ define('pgadmin.node.trigger', [
node: 'trigger',
label: gettext('Triggers'),
type: 'coll-trigger',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
columns: ['name', 'description'],
});
}
if (!pgBrowser.Nodes['trigger']) {
pgAdmin.Browser.Nodes['trigger'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table', 'view', 'partition'],
collection_type: ['coll-table', 'coll-view'],
type: 'trigger',
@@ -175,8 +178,8 @@ define('pgadmin.node.trigger', [
});
},
},
- canDrop: pgBrowser.Nodes['schema'].canChildDrop,
- canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
+ canDrop: SchemaChildTreeNode.isTreeItemOfChildOfSchema,
+ canDropCascade: SchemaChildTreeNode.isTreeItemOfChildOfSchema,
model: pgAdmin.Browser.Node.Model.extend({
defaults: {
name: undefined,
@@ -618,50 +621,16 @@ define('pgadmin.node.trigger', [
return flag;
},
}),
- // Below function will enable right click menu for creating column
- canCreate: function(itemData, item, data) {
- // If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData, parents = [];
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to c reate table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
- parents.push(d._type);
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // If node is under catalog then do not allow 'create' menu
- if (_.indexOf(parents, 'catalog') > -1) {
- return false;
- } else {
- return true;
- }
- },
+ canCreate: SchemaChildTreeNode.isTreeItemOfChildOfSchema,
// Check to whether trigger is disable ?
canCreate_with_trigger_enable: function(itemData, item, data) {
- if(this.canCreate.apply(this, [itemData, item, data])) {
- // We are here means we can create menu, now let's check condition
- if(itemData.icon === 'icon-trigger-bad') {
- return true;
- } else {
- return false;
- }
- }
+ return itemData.icon === 'icon-trigger-bad' &&
+ this.canCreate.apply(this, [itemData, item, data]);
},
// Check to whether trigger is enable ?
canCreate_with_trigger_disable: function(itemData, item, data) {
- if(this.canCreate.apply(this, [itemData, item, data])) {
- // We are here means we can create menu, now let's check condition
- if(itemData.icon === 'icon-trigger') {
- return true;
- } else {
- return false;
- }
- }
+ return itemData.icon === 'icon-trigger' &&
+ this.canCreate.apply(this, [itemData, item, data]);
},
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.js
index c1c24861..5860a752 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.js
@@ -1,8 +1,12 @@
define('pgadmin.node.type', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
- 'pgadmin.backgrid', 'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, Backgrid) {
+ 'pgadmin.backgrid', 'pgadmin.node.schema.dir/child',
+ 'pgadmin.browser.collection',
+], function(
+ gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, Backgrid,
+ schemaChild
+) {
if (!pgBrowser.Nodes['coll-type']) {
pgBrowser.Nodes['coll-type'] =
@@ -245,7 +249,7 @@ define('pgadmin.node.type', [
});
if (!pgBrowser.Nodes['type']) {
- pgBrowser.Nodes['type'] = pgBrowser.Node.extend({
+ pgBrowser.Nodes['type'] = schemaChild.SchemaChildNode.extend({
type: 'type',
sqlAlterHelp: 'sql-altertype.html',
sqlCreateHelp: 'sql-createtype.html',
@@ -254,7 +258,6 @@ define('pgadmin.node.type', [
collection_type: 'coll-type',
hasSQL: true,
hasDepends: true,
- parent_type: ['schema', 'catalog'],
Init: function() {
/* Avoid multiple registration of menus */
if (this.initialized)
@@ -284,8 +287,6 @@ define('pgadmin.node.type', [
]);
},
- canDrop: pgBrowser.Nodes['schema'].canChildDrop,
- canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
ext_funcs: undefined,
model: pgBrowser.Node.Model.extend({
defaults: {
@@ -911,34 +912,6 @@ define('pgadmin.node.type', [
return result;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-type' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
});
}
return pgBrowser.Nodes['type'];
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.js
index 073ef5cb..dcfdd54b 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.js
@@ -1,8 +1,12 @@
define('pgadmin.node.mview', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.alertifyjs', 'pgadmin.browser',
- 'pgadmin.backform', 'pgadmin.browser.server.privilege',
-], function(gettext, url_for, $, _, pgAdmin, Alertify, pgBrowser, Backform) {
+ 'pgadmin.backform', 'pgadmin.node.schema.dir/child',
+ 'pgadmin.browser.server.privilege',
+], function(
+ gettext, url_for, $, _, pgAdmin, Alertify, pgBrowser, Backform,
+ schemaChild
+) {
/**
Create and add a view collection into nodes
@@ -33,19 +37,16 @@ define('pgadmin.node.mview', [
view option in the context menu
*/
if (!pgBrowser.Nodes['mview']) {
- pgBrowser.Nodes['mview'] = pgBrowser.Node.extend({
- parent_type: ['schema', 'catalog'],
+ pgBrowser.Nodes['mview'] = schemaChild.SchemaChildNode.extend({
type: 'mview',
sqlAlterHelp: 'sql-altermaterializedview.html',
sqlCreateHelp: 'sql-creatematerializedview.html',
dialogHelp: url_for('help.static', {'filename': 'materialized_view_dialog.html'}),
label: gettext('Materialized View'),
- hasSQL: true,
+ hasSQL: true,
hasDepends: true,
hasScriptTypes: ['create', 'select'],
collection_type: 'coll-mview',
- canDrop: pgBrowser.Nodes['schema'].canChildDrop,
- canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
Init: function() {
// Avoid mulitple registration of menus
@@ -236,43 +237,6 @@ define('pgadmin.node.mview', [
}),
- /**
- Show or hide create view menu option on parent node
- and hide for system view in catalogs.
- */
- canCreate: function(itemData, item, data) {
-
- // If check is false then, we will allow create menu
- if (data && data.check === false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
-
- // To iterate over tree to check parent node
- while (i) {
-
- // If it is schema then allow user to create view
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-mview' == d._type) {
-
- // Check if we are not child of view
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
-
- // by default we do not want to allow create menu
- return true;
- },
refresh_mview: function(args) {
var input = args || {},
obj = this,
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js
index 5755a509..cd61ef21 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js
@@ -1,9 +1,12 @@
define('pgadmin.node.view', [
- 'sources/gettext',
- 'sources/url_for', 'jquery', 'underscore', 'sources/pgadmin',
- 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.browser.server.privilege',
+ 'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
+ 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
+ 'pgadmin.node.schema.dir/child', 'pgadmin.browser.server.privilege',
'pgadmin.node.rule',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform) {
+], function(
+ gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, schemaChild
+) {
+
/**
Create and add a view collection into nodes
@@ -28,14 +31,9 @@ define('pgadmin.node.view', [
under which this node to display
@param {variable} type - Type of Node
@param {variable} hasSQL - To show SQL tab
- @param {variable} canDrop - Adds drop view option
- in the context menu
- @param {variable} canDropCascade - Adds drop Cascade
- view option in the context menu
*/
if (!pgBrowser.Nodes['view']) {
- pgBrowser.Nodes['view'] = pgBrowser.Node.extend({
- parent_type: ['schema', 'catalog'],
+ pgBrowser.Nodes['view'] = schemaChild.SchemaChildNode.extend({
type: 'view',
sqlAlterHelp: 'sql-alterview.html',
sqlCreateHelp: 'sql-createview.html',
@@ -45,8 +43,6 @@ define('pgadmin.node.view', [
hasDepends: true,
hasScriptTypes: ['create', 'select', 'insert'],
collection_type: 'coll-view',
- canDrop: pgBrowser.Nodes['schema'].canChildDrop,
- canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
Init: function() {
// Avoid mulitple registration of menus
@@ -197,45 +193,6 @@ define('pgadmin.node.view', [
return false;
},
}),
-
- /**
- Show or hide create view menu option on parent node
- and hide for system view in catalogs.
- */
- canCreate: function(itemData, item, data) {
-
- // If check is false then, we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
-
- // To iterate over tree to check parent node
- while (i) {
-
- // If it is schema then allow user to create view
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-view' == d._type) {
-
- // Check if we are not child of view
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
-
- // by default we do not want to allow create menu
- return true;
-
- },
});
}
diff --git a/web/pgadmin/browser/static/js/collection.js b/web/pgadmin/browser/static/js/collection.js
index 05f0edd5..67e44214 100644
--- a/web/pgadmin/browser/static/js/collection.js
+++ b/web/pgadmin/browser/static/js/collection.js
@@ -115,17 +115,17 @@ define([
// Fetch Data
collection.fetch({
reset: true,
- error: function(xhr, error, message) {
+ error: function(model, error, xhr) {
pgBrowser.Events.trigger(
'pgadmin:collection:retrieval:error', 'properties', xhr, error,
- message, item, that
+ error.message, item, that
);
if (!Alertify.pgHandleItemError(
- xhr, error, message, {item: item, info: info}
+ xhr, error, error.message, {item: item, info: info}
)) {
Alertify.pgNotifier(error, xhr, S(
gettext('Error retrieving properties - %s.')
- ).sprintf(message || that.label).value(), function() {
+ ).sprintf(error.message || that.label).value(), function() {
console.warn(arguments);
});
}
diff --git a/web/pgadmin/browser/static/js/node.js b/web/pgadmin/browser/static/js/node.js
index ad582483..94baeabf 100644
--- a/web/pgadmin/browser/static/js/node.js
+++ b/web/pgadmin/browser/static/js/node.js
@@ -1,9 +1,13 @@
define('pgadmin.browser.node', [
+ 'sources/tree/pgadmin_tree_node',
'sources/gettext', 'jquery', 'underscore', 'underscore.string', 'sources/pgadmin',
'pgadmin.browser.menu', 'backbone', 'pgadmin.alertifyjs', 'pgadmin.browser.datamodel',
'backform', 'sources/browser/generate_url', 'sources/utils', 'pgadmin.browser.utils',
'pgadmin.backform',
-], function(gettext, $, _, S, pgAdmin, Menu, Backbone, Alertify, pgBrowser, Backform, generateUrl, commonUtils) {
+], function(
+ pgadminTreeNode,
+ gettext, $, _, S, pgAdmin, Menu, Backbone, Alertify, pgBrowser, Backform, generateUrl, commonUtils
+) {
var wcDocker = window.wcDocker,
keyCode = {
@@ -28,7 +32,7 @@ define('pgadmin.browser.node', [
//
// It is unlikely - we will instantiate an object for this class.
// (Inspired by Backbone.extend function)
- pgBrowser.Node.extend = function(props) {
+ pgBrowser.Node.extend = function(props, initialize) {
var parent = this;
var child;
@@ -44,6 +48,10 @@ define('pgadmin.browser.node', [
// Make sure - a child have all the callbacks of the parent.
child.callbacks = _.extend({}, parent.callbacks, props.callbacks);
+ // Let's not bind the callbacks, or initialize the child.
+ if (initialize === false)
+ return child;
+
var bindToChild = function(cb) {
if (typeof(child.callbacks[cb]) == 'function') {
child.callbacks[cb] = child.callbacks[cb].bind(child);
@@ -1566,7 +1574,6 @@ define('pgadmin.browser.node', [
* depends, statistics
*/
generate_url: function(item, type, d, with_id, info) {
-
var opURL = {
'create': 'obj',
'drop': 'obj',
@@ -1608,24 +1615,7 @@ define('pgadmin.browser.node', [
Collection: pgBrowser.DataCollection,
// Base class for Node Data Model
Model: pgBrowser.DataModel,
- getTreeNodeHierarchy: function(i) {
- var idx = 0,
- res = {},
- t = pgBrowser.tree,
- d;
- do {
- d = t.itemData(i);
- if (d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId) {
- res[d._type] = _.extend({}, d, {
- 'priority': idx,
- });
- idx -= 1;
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- } while (i);
-
- return res;
- },
+ getTreeNodeHierarchy: pgadminTreeNode.getTreeNodeHierarchyFromIdentifier.bind(pgBrowser),
cache: function(url, node_info, level, data) {
var cached = this.cached = this.cached || {},
hash = url,
diff --git a/web/pgadmin/static/js/alertify/dialog.js b/web/pgadmin/static/js/alertify/dialog.js
new file mode 100644
index 00000000..5a8646f9
--- /dev/null
+++ b/web/pgadmin/static/js/alertify/dialog.js
@@ -0,0 +1,129 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../gettext';
+import {sprintf} from 'sprintf-js';
+import {DialogFactory} from './dialog_factory';
+import Backform from '../backform.pgadmin';
+import {getTreeNodeHierarchyFromIdentifier} from '../tree/pgadmin_tree_node';
+
+/**
+ * This class can be extended to create new dialog boxes.
+ * Examples of this can be found in:
+ * `web/pgadmin/static/js/backup/backup_dialog.js`
+ *
+ * Do not forget to add the new Dialog type to the `DialogFactory`
+ */
+export class Dialog {
+ constructor(errorAlertTitle,
+ dialogContainerSelector,
+ pgBrowser, $, alertify, DialogModel,
+ backform = Backform) {
+ this.errorAlertTitle = errorAlertTitle;
+ this.alertify = alertify;
+ this.pgBrowser = pgBrowser;
+ this.jquery = $;
+ this.dialogModel = DialogModel;
+ this.backform = backform;
+ this.dialogContainerSelector = dialogContainerSelector;
+ }
+
+ retrieveAncestorOfTypeServer(item) {
+ let serverInformation = null;
+ let aciTreeItem = item || this.pgBrowser.treeMenu.selected();
+ let treeNode = this.pgBrowser.treeMenu.findNodeByDomElement(aciTreeItem);
+
+ while (treeNode) {
+ const node_data = treeNode.getData();
+ if (node_data._type === 'server') {
+ serverInformation = node_data;
+ break;
+ }
+
+ if (treeNode.hasParent()) {
+ treeNode = treeNode.parent();
+ } else {
+ this.alertify.alert(
+ gettext(this.errorAlertTitle),
+ gettext('Please select server or child node from the browser tree.')
+ );
+ break;
+ }
+ }
+ return serverInformation;
+ }
+
+ hasBinariesConfiguration(serverInformation) {
+ const module = 'paths';
+ let preference_name = 'pg_bin_dir';
+ let msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
+
+ if ((serverInformation.type && serverInformation.type === 'ppas') ||
+ serverInformation.server_type === 'ppas') {
+ preference_name = 'ppas_bin_dir';
+ msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
+ }
+ const preference = this.pgBrowser.get_preference(module, preference_name);
+
+ if (preference) {
+ if (!preference.value) {
+ this.alertify.alert(gettext('Configuration required'), msg);
+ return false;
+ }
+ } else {
+ this.alertify.alert(
+ gettext(this.errorAlertTitle),
+ sprintf(gettext('Failed to load preference %s of module %s'), preference_name, module)
+ );
+ return false;
+ }
+ return true;
+ }
+
+ dialogName() {
+ return undefined;
+ }
+
+ createOrGetDialog(dialogTitle, typeOfDialog) {
+ const dialogName = this.dialogName(typeOfDialog);
+
+ if (!this.alertify[dialogName]) {
+ const self = this;
+ this.alertify.dialog(dialogName, function factory() {
+ return self.dialogFactory(dialogTitle, typeOfDialog);
+ });
+ }
+ return this.alertify[dialogName];
+ }
+
+ dialogFactory(dialogTitle, typeOfDialog) {
+ const factory = new DialogFactory(
+ this.pgBrowser,
+ this.jquery,
+ this.alertify,
+ this.dialogModel,
+ this.backform,
+ this.dialogContainerSelector);
+ return factory.create(dialogTitle, typeOfDialog);
+ }
+
+ canExecuteOnCurrentDatabase(aciTreeItem) {
+ const treeInfo = getTreeNodeHierarchyFromIdentifier.apply(this.pgBrowser, [aciTreeItem]);
+
+ if (treeInfo.database && treeInfo.database._label.indexOf('=') >= 0) {
+ this.alertify.alert(
+ gettext(this.errorAlertTitle),
+ gettext('Databases with = symbols in the name cannot be backed up or restored using this utility.')
+ );
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/web/pgadmin/static/js/alertify/dialog_factory.js b/web/pgadmin/static/js/alertify/dialog_factory.js
new file mode 100644
index 00000000..500140b8
--- /dev/null
+++ b/web/pgadmin/static/js/alertify/dialog_factory.js
@@ -0,0 +1,52 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import * as BackupDialog from '../../../tools/backup/static/js/backup_dialog_wrapper';
+import {RestoreDialogWrapper} from '../../../tools/restore/static/js/restore_dialog_wrapper';
+
+export class DialogFactory {
+ constructor(pgBrowser, $,
+ alertify, DialogModel,
+ backform, dialogContainerSelector) {
+ this.pgBrowser = pgBrowser;
+ this.jquery = $;
+ this.alertify = alertify;
+ this.dialogModel = DialogModel;
+ this.backform = backform;
+ this.dialogContainerSelector = dialogContainerSelector;
+ }
+
+ create(dialogTitle, typeOfDialog) {
+ if (typeOfDialog === 'restore') {
+ return this.createRestoreDialog(dialogTitle, typeOfDialog);
+ } else {
+ return this.createBackupDialog(dialogTitle, typeOfDialog);
+ }
+ }
+
+ createRestoreDialog(dialogTitle, typeOfDialog) {
+ return new RestoreDialogWrapper(
+ this.dialogContainerSelector, dialogTitle, typeOfDialog,
+ this.jquery,
+ this.pgBrowser,
+ this.alertify,
+ this.dialogModel,
+ this.backform);
+ }
+
+ createBackupDialog(dialogTitle, typeOfDialog) {
+ return new BackupDialog.BackupDialogWrapper(
+ this.dialogContainerSelector, dialogTitle, typeOfDialog,
+ this.jquery,
+ this.pgBrowser,
+ this.alertify,
+ this.dialogModel,
+ this.backform);
+ }
+}
diff --git a/web/pgadmin/static/js/alertify/dialog_wrapper.js b/web/pgadmin/static/js/alertify/dialog_wrapper.js
new file mode 100644
index 00000000..b5ff8204
--- /dev/null
+++ b/web/pgadmin/static/js/alertify/dialog_wrapper.js
@@ -0,0 +1,57 @@
+import * as commonUtils from '../utils';
+
+export class DialogWrapper {
+ constructor(
+ dialogContainerSelector, dialogTitle, jquery, pgBrowser,
+ alertify, dialogModel, backform) {
+ this.hooks = {
+ onclose: function () {
+ if (this.view) {
+ this.view.remove({
+ data: true,
+ internal: true,
+ silent: true,
+ });
+ }
+ },
+ };
+ this.dialogContainerSelector = dialogContainerSelector;
+ this.dialogTitle = dialogTitle;
+ this.jquery = jquery;
+ this.pgBrowser = pgBrowser;
+ this.alertify = alertify;
+ this.dialogModel = dialogModel;
+ this.backform = backform;
+ }
+
+ build() {
+ this.alertify.pgDialogBuild.apply(this);
+ }
+
+ wasHelpButtonPressed(e) {
+ return e.button.element.name === 'dialog_help'
+ || e.button.element.name === 'object_help';
+ }
+
+ getSelectedNodeData(selectedTreeNode) {
+ if (!this.isNodeSelected(selectedTreeNode)) {
+ return undefined;
+ }
+ const treeNodeData = selectedTreeNode.getData();
+ if (treeNodeData) {
+ return treeNodeData;
+ }
+ return undefined;
+ }
+
+ focusOnDialog(dialog) {
+ dialog.$el.attr('tabindex', -1);
+ this.pgBrowser.keyboardNavigation.getDialogTabNavigator(dialog);
+ const container = dialog.$el.find('.tab-content:first > .tab-pane.active:first');
+ commonUtils.findAndSetFocus(container);
+ }
+
+ isNodeSelected(selectedTreeNode) {
+ return selectedTreeNode;
+ }
+}
diff --git a/web/pgadmin/static/js/nodes/supported_database_node.js b/web/pgadmin/static/js/nodes/supported_database_node.js
new file mode 100644
index 00000000..fde1cf98
--- /dev/null
+++ b/web/pgadmin/static/js/nodes/supported_database_node.js
@@ -0,0 +1,37 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {isValidTreeNodeData} from 'sources/tree/tree';
+
+function checkAllowConnIfDatabaseNode(treeNodeData) {
+ return (treeNodeData._type === 'database' && treeNodeData.allowConn)
+ || treeNodeData._type !== 'database';
+}
+
+function ancestorWithTypeCatalog(treeNode) {
+ return treeNode.anyFamilyMember((node) => {
+ return node.getData()._type === 'catalog';
+ });
+}
+
+export function enabled(tree, supportedNodes, treeNodeData, domTreeNode) {
+ if (!isValidTreeNodeData(treeNodeData))
+ return false;
+
+ let treeNode = tree.findNodeByDomElement(domTreeNode);
+ if (!treeNode) {
+ return false;
+ }
+
+ return checkAllowConnIfDatabaseNode(treeNodeData) &&
+ _.indexOf(supportedNodes, treeNodeData._type) !== -1 &&
+ !ancestorWithTypeCatalog(treeNode);
+}
+
+
diff --git a/web/pgadmin/static/js/tree/pgadmin_tree_node.js b/web/pgadmin/static/js/tree/pgadmin_tree_node.js
new file mode 100644
index 00000000..00f10d24
--- /dev/null
+++ b/web/pgadmin/static/js/tree/pgadmin_tree_node.js
@@ -0,0 +1,72 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+/**
+ * This method received pgBrowser and new TreeNode object
+ *
+ * This method retrieves all the data that exists in the tree node and in
+ * `pgBrowser.Nodes` for all the parent node of the provided node.
+ *
+ * The 2 condition to get the information from pgBrowser.Nodes are:
+ * 1 - the variable _type of the tree node
+ * 2 - the presence of hasId in the pgBrowser.Nodes for the specific node
+ *
+ * Number 2 is used to ignore coll-* nodes as they do not add any useful
+ * information
+ */
+export function getTreeNodeHierarchyFromElement(pgBrowser, treeNode) {
+ return getTreeNodeHierarchy.call(pgBrowser, treeNode);
+}
+
+/**
+ * This method received an ACI Tree JQuery node
+ *
+ * NOTE: this function need to be called on pgBrowser instance.
+ * getTreeNodeHierarchyFromIdentifier.apply(pgBrowser, [aciTreeNodeIdentifier])
+ *
+ * This method retrieves all the data that exists in the tree node and in
+ * `pgBrowser.Nodes` for all the parent node of the provided node.
+ *
+ * The 2 condition to get the information from pgBrowser.Nodes are:
+ * 1 - the variable _type of the tree node
+ * 2 - the presence of hasId in the pgBrowser.Nodes for the specific node
+ *
+ * Number 2 is used to ignore coll-* nodes as they do not add any useful
+ * information
+ */
+export function getTreeNodeHierarchyFromIdentifier(aciTreeNodeIdentifier) {
+ let identifier = this.treeMenu.translateTreeNodeIdFromACITree(aciTreeNodeIdentifier);
+ let currentNode = this.treeMenu.findNode(identifier);
+ return getTreeNodeHierarchy.call(this, currentNode);
+}
+
+export function getTreeNodeHierarchy(currentNode) {
+ let idx = 0;
+ let result = {};
+
+ do {
+ const currentNodeData = currentNode.getData();
+ if (currentNodeData._type in this.Nodes && this.Nodes[currentNodeData._type].hasId) {
+ const nodeType = mapType(currentNodeData._type);
+ if (result[nodeType] === undefined) {
+ result[nodeType] = _.extend({}, currentNodeData, {
+ 'priority': idx,
+ });
+ idx -= 1;
+ }
+ }
+ currentNode = currentNode.hasParent() ? currentNode.parent() : null;
+ } while (currentNode);
+
+ return result;
+}
+
+function mapType(type) {
+ return type === 'partition' ? 'table' : type;
+}
diff --git a/web/pgadmin/static/js/tree/tree.js b/web/pgadmin/static/js/tree/tree.js
index 01edb6c3..65330469 100644
--- a/web/pgadmin/static/js/tree/tree.js
+++ b/web/pgadmin/static/js/tree/tree.js
@@ -59,17 +59,20 @@ export class TreeNode {
tree.aciTreeApi.unload(this.domNode);
}
- anyParent(condition) {
+ /*
+ * Find the ancestor with matches this condition
+ */
+ ancestorNode(condition) {
let node = this;
while (node.hasParent()) {
node = node.parent();
if (condition(node)) {
- return true;
+ return node;
}
}
- return false;
+ return null;
}
/**
@@ -81,7 +84,10 @@ export class TreeNode {
return true;
}
- return this.anyParent(condition);
+ return !!this.ancestorNode(condition);
+ }
+ anyParent(condition) {
+ return !!this.ancestorNode(condition);
}
}
@@ -210,3 +216,7 @@ function findInTree(rootNode, path) {
}
})(rootNode);
}
+
+export function isValidTreeNodeData(treeNodeData) {
+ return !_.isUndefined(treeNodeData) && !_.isNull(treeNodeData);
+}
diff --git a/web/pgadmin/tools/backup/static/js/backup.js b/web/pgadmin/tools/backup/static/js/backup.js
index 1b0b3628..d6fd48c5 100644
--- a/web/pgadmin/tools/backup/static/js/backup.js
+++ b/web/pgadmin/tools/backup/static/js/backup.js
@@ -3,9 +3,12 @@ define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'pgadmin.alertifyjs', 'backbone', 'pgadmin.backgrid',
'pgadmin.backform', 'pgadmin.browser', 'sources/utils',
+ 'tools/backup/static/js/menu_utils',
+ 'tools/backup/static/js/backup_dialog',
+ 'sources/nodes/supported_database_node',
], function(
gettext, url_for, $, _, S, alertify, Backbone, Backgrid, Backform, pgBrowser,
-commonUtils
+ commonUtils, menuUtils, globalBackupDialog, supportedNodes
) {
// if module is already initialized, refer to that.
@@ -394,48 +397,6 @@ commonUtils
this.initialized = true;
- // Define list of nodes on which backup context menu option appears
- var backup_supported_nodes = [
- 'database', 'schema', 'table', 'partition',
- ];
-
- /**
- Enable/disable backup menu in tools based
- on node selected
- if selected node is present in supported_nodes,
- menu will be enabled otherwise disabled.
- Also, hide it for system view in catalogs
- */
- var menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData,
- parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
-
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(backup_supported_nodes, d._type) !== -1 &&
- parent_data._type != 'catalog') {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
- var menu_enabled_server = function(itemData) {
- // If server node selected && connected
- if (!_.isUndefined(itemData) && !_.isNull(itemData))
- return (('server' === itemData._type) && itemData.connected);
- else
- return false;
- };
-
// Define the nodes on which the menus to be appear
var menus = [{
name: 'backup_global',
@@ -445,7 +406,7 @@ commonUtils
priority: 12,
label: gettext('Backup Globals...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_server',
module: this,
@@ -454,7 +415,7 @@ commonUtils
priority: 12,
label: gettext('Backup Server...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_global_ctx',
module: this,
@@ -464,7 +425,7 @@ commonUtils
priority: 12,
label: gettext('Backup Globals...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_server_ctx',
module: this,
@@ -474,7 +435,7 @@ commonUtils
priority: 12,
label: gettext('Backup Server...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_object',
module: this,
@@ -483,20 +444,24 @@ commonUtils
priority: 11,
label: gettext('Backup...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled,
+ enable: supportedNodes.enabled.bind(
+ null, pgBrowser.treeMenu, menuUtils.backupSupportedNodes
+ ),
}];
- for (var idx = 0; idx < backup_supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.backupSupportedNodes.length; idx++) {
menus.push({
- name: 'backup_' + backup_supported_nodes[idx],
- node: backup_supported_nodes[idx],
+ name: 'backup_' + menuUtils.backupSupportedNodes[idx],
+ node: menuUtils.backupSupportedNodes[idx],
module: this,
applies: ['context'],
callback: 'backup_objects',
priority: 11,
label: gettext('Backup...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled,
+ enable: supportedNodes.enabled.bind(
+ null, pgBrowser.treeMenu, menuUtils.backupSupportedNodes
+ ),
});
}
@@ -521,542 +486,25 @@ commonUtils
},
// Callback to draw Backup Dialog for globals/server
- start_backup_global_server: function(action, item, params) {
- var i = item || pgBrowser.tree.selected(),
- server_data = null;
-
- while (i) {
- var node_data = pgBrowser.tree.itemData(i);
- if (node_data._type == 'server') {
- server_data = node_data;
- break;
- }
-
- if (pgBrowser.tree.hasParent(i)) {
- i = $(pgBrowser.tree.parent(i));
- } else {
- alertify.alert(
- gettext('Backup Error'),
- gettext('Please select server or child node from the browser tree.')
- );
- break;
- }
- }
-
- if (!server_data) {
- return;
- }
-
- var module = 'paths',
- preference_name = 'pg_bin_dir',
- msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
-
- if ((server_data.type && server_data.type == 'ppas') ||
- server_data.server_type == 'ppas') {
- preference_name = 'ppas_bin_dir';
- msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
- }
-
- var preference = pgBrowser.get_preference(module, preference_name);
-
- if (preference) {
- if (!preference.value) {
- alertify.alert(gettext('Configuration required'), msg);
- return;
- }
- } else {
- alertify.alert(
- gettext('Backup Error'),
- S(gettext('Failed to load preference %s of module %s')).sprintf(preference_name, module).value()
- );
- return;
- }
-
- var of_type = undefined;
-
- // Set Notes according to type of backup
- if (!_.isUndefined(params['globals']) && params['globals']) {
- of_type = 'globals';
- } else {
- of_type = 'server';
- }
-
- var DialogName = 'BackupDialog_' + of_type,
- DialogTitle = ((of_type == 'globals') ?
- gettext('Backup Globals...') :
- gettext('Backup Server...'));
-
- if (!alertify[DialogName]) {
- alertify.dialog(DialogName, function factory() {
- return {
- main: function(title) {
- this.set('title', title);
- },
- build: function() {
- alertify.pgDialogBuild.apply(this);
- },
- setup: function() {
- return {
- buttons: [{
- text: '',
- className: 'btn btn-default pull-left fa fa-lg fa-info',
- attrs: {
- name: 'object_help',
- type: 'button',
- url: 'backup.html',
- label: gettext('Backup'),
- },
- }, {
- text: '',
- key: 112,
- className: 'btn btn-default pull-left fa fa-lg fa-question',
- attrs: {
- name: 'dialog_help',
- type: 'button',
- label: gettext('Backup'),
- url: url_for('help.static', {
- 'filename': 'backup_dialog.html',
- }),
- },
- }, {
- text: gettext('Backup'),
- key: 13,
- className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
- 'data-btn-name': 'backup',
- }, {
- text: gettext('Cancel'),
- key: 27,
- className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
- 'data-btn-name': 'cancel',
- }],
- // Set options for dialog
- options: {
- title: DialogTitle,
- //disable both padding and overflow control.
- padding: !1,
- overflow: !1,
- model: 0,
- resizable: true,
- maximizable: true,
- pinnable: false,
- closableByDimmer: false,
- modal: false,
- },
- };
- },
- hooks: {
- // Triggered when the dialog is closed
- onclose: function() {
- if (this.view) {
- // clear our backform model/view
- this.view.remove({
- data: true,
- internal: true,
- silent: true,
- });
- }
- },
- },
- prepare: function() {
- var self = this;
- // Disable Backup button until user provides Filename
- this.__internal.buttons[2].element.disabled = true;
-
- var $container = $('<div class=\'backup_dialog\'></div>');
- // Find current/selected node
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (!d)
- return;
- // Create treeInfo
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
- // Instance of backbone model
- var newModel = new BackupModel({
- type: of_type,
- }, {
- node_info: treeInfo,
- }),
- fields = Backform.generateViewSchema(
- treeInfo, newModel, 'create', node, treeInfo.server, true
- );
-
- var view = this.view = new Backform.Dialog({
- el: $container,
- model: newModel,
- schema: fields,
- });
- // Add our class to alertify
- $(this.elements.body.childNodes[0]).addClass(
- 'alertify_tools_dialog_properties obj_properties'
- );
- // Render dialog
- view.render();
-
- this.elements.content.appendChild($container.get(0));
-
- var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
- commonUtils.findAndSetFocus(container);
-
- // Listen to model & if filename is provided then enable Backup button
- this.view.model.on('change', function() {
- if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
- this.errorModel.clear();
- self.__internal.buttons[2].element.disabled = false;
- } else {
- self.__internal.buttons[2].element.disabled = true;
- this.errorModel.set('file', gettext('Please provide a filename'));
- }
- });
- },
- // Callback functions when click on the buttons of the Alertify dialogs
- callback: function(e) {
- // Fetch current server id
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (e.button.element.name == 'dialog_help' || e.button.element.name == 'object_help') {
- e.cancel = true;
- pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
- node, i, e.button.element.getAttribute('label'));
- return;
- }
-
- if (e.button['data-btn-name'] === 'backup') {
-
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- var self = this,
- baseUrl = url_for('backup.create_server_job', {
- 'sid': treeInfo.server._id,
- }),
- args = this.view.model.toJSON();
-
- $.ajax({
- url: baseUrl,
- method: 'POST',
- data: {
- 'data': JSON.stringify(args),
- },
- success: function(res) {
- if (res.success) {
- alertify.success(gettext('Backup job created.'), 5);
- pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
- } else {
- console.warn(res);
- }
- },
- error: function(xhr) {
- try {
- var err = $.parseJSON(xhr.responseText);
- alertify.alert(
- gettext('Backup job failed.'),
- err.errormsg
- );
- } catch (e) {
- console.warn(e.stack || e);
- }
- },
- });
- }
- },
- };
- });
- }
- alertify[DialogName](true).resizeTo('60%', '50%');
+ start_backup_global_server: function(action, treeItem, params) {
+ let dialog = new globalBackupDialog.BackupDialog(
+ pgBrowser,
+ $,
+ alertify,
+ BackupModel
+ );
+ dialog.draw(action, treeItem, params);
},
// Callback to draw Backup Dialog for objects
backup_objects: function(action, treeItem) {
-
- var i = treeItem || pgBrowser.tree.selected(),
- server_data = null;
-
- while (i) {
- var node_data = pgBrowser.tree.itemData(i);
- if (node_data._type == 'server') {
- server_data = node_data;
- break;
- }
-
- if (pgBrowser.tree.hasParent(i)) {
- i = $(pgBrowser.tree.parent(i));
- } else {
- alertify.alert(
- gettext('Backup Error'),
- gettext('Please select server or child node from tree.')
- );
- break;
- }
- }
-
- if (!server_data) {
- return;
- }
-
- var module = 'paths',
- preference_name = 'pg_bin_dir',
- msg = gettext('Please set binary path for PostgreSQL Server from preferences.');
-
- if ((server_data.type && server_data.type == 'ppas') ||
- server_data.server_type == 'ppas') {
- preference_name = 'ppas_bin_dir';
- msg = gettext('Please set binary path for EDB Postgres Advanced Server from preferences.');
- }
-
- var preference = pgBrowser.get_preference(module, preference_name);
-
- if (preference) {
- if (!preference.value) {
- alertify.alert(gettext('Configuration required'), msg);
- return;
- }
- } else {
- alertify.alert(
- gettext('Backup Error'),
- S(gettext('Failed to load preference %s of module %s')).sprintf(preference_name, module).value()
- );
- return;
- }
-
- var title = S(gettext('Backup (%s: %s)')),
- tree = pgBrowser.tree,
- item = treeItem || tree.selected(),
- data = item && item.length == 1 && tree.itemData(item),
- node = data && data._type && pgBrowser.Nodes[data._type];
-
- if (!node)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [item]);
-
- if (treeInfo.database._label.indexOf('=') >= 0) {
- alertify.alert(
- gettext('Backup error'),
- gettext('Backup job creation failed. '+
- 'Databases with = symbols in the name cannot be backed up using this utility.')
- );
- return;
- }
-
- title = title.sprintf(node.label, data.label).value();
-
- if (!alertify.backup_objects) {
- // Create Dialog title on the fly with node details
- alertify.dialog('backup_objects', function factory() {
- return {
- main: function(title) {
- this.set('title', title);
- },
- build: function() {
- alertify.pgDialogBuild.apply(this);
- },
- setup: function() {
- return {
- buttons: [{
- text: '',
- className: 'btn btn-default pull-left fa fa-lg fa-info',
- attrs: {
- name: 'object_help',
- type: 'button',
- url: 'backup.html',
- label: gettext('Backup'),
- },
- }, {
- text: '',
- key: 112,
- className: 'btn btn-default pull-left fa fa-lg fa-question',
- attrs: {
- name: 'dialog_help',
- type: 'button',
- label: gettext('Backup'),
- url: url_for('help.static', {
- 'filename': 'backup_dialog.html',
- }),
- },
- }, {
- text: gettext('Backup'),
- key: 13,
- className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
- 'data-btn-name': 'backup',
- }, {
- text: gettext('Cancel'),
- key: 27,
- className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
- 'data-btn-name': 'cancel',
- }],
- // Set options for dialog
- options: {
- title: title,
- //disable both padding and overflow control.
- padding: !1,
- overflow: !1,
- model: 0,
- resizable: true,
- maximizable: true,
- pinnable: false,
- closableByDimmer: false,
- modal: false,
- },
- };
- },
- hooks: {
- // triggered when the dialog is closed
- onclose: function() {
- if (this.view) {
- this.view.remove({
- data: true,
- internal: true,
- silent: true,
- });
- }
- },
- },
- prepare: function() {
- var self = this;
- // Disable Backup button until user provides Filename
- this.__internal.buttons[2].element.disabled = true;
- var $container = $('<div class=\'backup_dialog\'></div>');
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- var newModel = new BackupObjectModel({}, {
- node_info: treeInfo,
- }),
- fields = Backform.generateViewSchema(
- treeInfo, newModel, 'create', node, treeInfo.server, true
- );
-
- var view = this.view = new Backform.Dialog({
- el: $container,
- model: newModel,
- schema: fields,
- });
-
- $(this.elements.body.childNodes[0]).addClass(
- 'alertify_tools_dialog_properties obj_properties'
- );
-
- view.render();
-
- this.elements.content.appendChild($container.get(0));
-
- if(view) {
- view.$el.attr('tabindex', -1);
- // var dialogTabNavigator = pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
- commonUtils.findAndSetFocus(container);
- }
- // Listen to model & if filename is provided then enable Backup button
- this.view.model.on('change', function() {
- if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
- this.errorModel.clear();
- self.__internal.buttons[2].element.disabled = false;
- } else {
- self.__internal.buttons[2].element.disabled = true;
- this.errorModel.set('file', gettext('Please provide filename'));
- }
- });
-
- },
- // Callback functions when click on the buttons of the Alertify dialogs
- callback: function(e) {
- // Fetch current server id
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (e.button.element.name == 'dialog_help' || e.button.element.name == 'object_help') {
- e.cancel = true;
- pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
- node, i, e.button.element.getAttribute('label'));
- return;
- }
-
- if (e.button['data-btn-name'] === 'backup') {
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- // Set current database into model
- this.view.model.set('database', treeInfo.database._label);
-
- // We will remove once object tree is implemented
- // If selected node is Schema then add it in model
- if (d._type == 'schema') {
- var schemas = [];
- schemas.push(d._label);
- this.view.model.set('schemas', schemas);
- }
- // If selected node is Table then add it in model along with
- // its schema
- if (d._type == 'table') {
- this.view.model.set(
- 'tables', [
- [treeInfo.schema._label, d._label],
- ]
- );
- }
-
- // Remove ratio attribute from model if it has empty string.
- // The valid value can be between 0 to 9.
- if (_.isEmpty(this.view.model.get('ratio'))) {
- this.view.model.unset('ratio');
- }
-
- var self = this,
- baseUrl = url_for('backup.create_object_job', {
- 'sid': treeInfo.server._id,
- }),
- args = this.view.model.toJSON();
-
- $.ajax({
- url: baseUrl,
- method: 'POST',
- data: {
- 'data': JSON.stringify(args),
- },
- success: function(res) {
- if (res.success) {
- alertify.success(gettext('Backup job created.'), 5);
- pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
- }
- },
- error: function(xhr) {
- try {
- var err = $.parseJSON(xhr.responseText);
- alertify.alert(
- gettext('Backup job failed.'),
- err.errormsg
- );
- } catch (e) {
- console.warn(e.stack || e);
- }
- },
- });
- }
- },
- };
- });
- }
- alertify.backup_objects(title).resizeTo('65%', '60%');
+ let dialog = new globalBackupDialog.BackupDialog(
+ pgBrowser,
+ $,
+ alertify,
+ BackupObjectModel
+ );
+ dialog.draw(action, treeItem, null);
},
};
return pgBrowser.Backup;
diff --git a/web/pgadmin/tools/backup/static/js/backup_dialog.js b/web/pgadmin/tools/backup/static/js/backup_dialog.js
new file mode 100644
index 00000000..c74c376c
--- /dev/null
+++ b/web/pgadmin/tools/backup/static/js/backup_dialog.js
@@ -0,0 +1,69 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../../../../static/js/gettext';
+import Backform from '../../../../static/js/backform.pgadmin';
+import {Dialog} from '../../../../static/js/alertify/dialog';
+
+export class BackupDialog extends Dialog {
+ constructor(pgBrowser, $, alertify, BackupModel, backform = Backform) {
+ super('Backup Error',
+ '<div class=\'backup_dialog\'></div>',
+ pgBrowser, $, alertify, BackupModel, backform);
+ }
+
+ draw(action, aciTreeItem, params) {
+ const serverInformation = this.retrieveAncestorOfTypeServer(aciTreeItem);
+
+ if (!serverInformation) {
+ return;
+ }
+
+ if (!this.hasBinariesConfiguration(serverInformation)) {
+ return;
+ }
+
+ const typeOfDialog = BackupDialog.typeOfDialog(params);
+
+ if (!this.canExecuteOnCurrentDatabase(aciTreeItem)) {
+ return;
+ }
+
+ const dialog = this.createOrGetDialog(BackupDialog.dialogTitle(typeOfDialog),
+ typeOfDialog);
+ dialog(true).resizeTo('60%', '50%');
+ }
+
+ static typeOfDialog(params) {
+ if (params === null) {
+ return 'backup_objects';
+ }
+ let typeOfDialog = 'server';
+ if (!_.isUndefined(params['globals']) && params['globals']) {
+ typeOfDialog = 'globals';
+ }
+ return typeOfDialog;
+ }
+
+ static dialogTitle(typeOfDialog) {
+ if (typeOfDialog === 'backup_objects') {
+ return null;
+ }
+ return ((typeOfDialog === 'globals') ?
+ gettext('Backup Globals...') :
+ gettext('Backup Server...'));
+ }
+
+ dialogName(typeOfDialog) {
+ if (typeOfDialog === 'backup_objects') {
+ return typeOfDialog;
+ }
+ return 'BackupDialog_' + typeOfDialog;
+ }
+}
diff --git a/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js b/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js
new file mode 100644
index 00000000..2cebe3d1
--- /dev/null
+++ b/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js
@@ -0,0 +1,258 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {getTreeNodeHierarchyFromElement} from '../../../../static/js/tree/pgadmin_tree_node';
+import axios from 'axios/index';
+import gettext from '../../../../static/js/gettext';
+import url_for from '../../../../static/js/url_for';
+import _ from 'underscore';
+import {DialogWrapper} from '../../../../static/js/alertify/dialog_wrapper';
+
+export class BackupDialogWrapper extends DialogWrapper {
+ constructor(dialogContainerSelector, dialogTitle, typeOfDialog,
+ jquery, pgBrowser, alertify, dialogModel, backform) {
+ super(dialogContainerSelector, dialogTitle, jquery,
+ pgBrowser, alertify, dialogModel, backform);
+ this.typeOfDialog = typeOfDialog;
+ }
+
+ main(title) {
+ this.set('title', title);
+ }
+
+ setup() {
+ return {
+ buttons: [{
+ text: '',
+ className: 'btn btn-default pull-left fa fa-lg fa-info',
+ attrs: {
+ name: 'object_help',
+ type: 'button',
+ url: 'backup.html',
+ label: gettext('Backup'),
+ },
+ }, {
+ text: '',
+ key: 112,
+ className: 'btn btn-default pull-left fa fa-lg fa-question',
+ attrs: {
+ name: 'dialog_help',
+ type: 'button',
+ label: gettext('Backup'),
+ url: url_for('help.static', {
+ 'filename': 'backup_dialog.html',
+ }),
+ },
+ }, {
+ text: gettext('Backup'),
+ key: 13,
+ className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
+ 'data-btn-name': 'backup',
+ }, {
+ text: gettext('Cancel'),
+ key: 27,
+ className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
+ 'data-btn-name': 'cancel',
+ }],
+ // Set options for dialog
+ options: {
+ title: this.dialogTitle,
+ //disable both padding and overflow control.
+ padding: !1,
+ overflow: !1,
+ model: 0,
+ resizable: true,
+ maximizable: true,
+ pinnable: false,
+ closableByDimmer: false,
+ modal: false,
+ },
+ };
+ }
+
+ prepare() {
+ this.disableBackupButton();
+
+ const $container = this.jquery(this.dialogContainerSelector);
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = this.getSelectedNodeData(selectedTreeNode);
+ if (!selectedTreeNodeData) {
+ return;
+ }
+
+ const node = this.pgBrowser.Nodes[selectedTreeNodeData._type];
+ if (this.dialogTitle === null) {
+ const title = `Backup (${node.label}: ${selectedTreeNodeData.label})`;
+ this.main(title);
+ }
+
+ const treeInfo = getTreeNodeHierarchyFromElement(this.pgBrowser, selectedTreeNode);
+ const dialog = this.createDialog(node, treeInfo, this.typeOfDialog, $container);
+ this.addAlertifyClassToBackupNodeChildNodes();
+ dialog.render();
+
+ this.elements.content.appendChild($container.get(0));
+
+ this.focusOnDialog(dialog);
+ this.setListenersForFilenameChanges();
+ }
+
+ callback(event) {
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = this.getSelectedNodeData(selectedTreeNode);
+ const node = selectedTreeNodeData && this.pgBrowser.Nodes[selectedTreeNodeData._type];
+
+ if (this.wasHelpButtonPressed(event)) {
+ event.cancel = true;
+ this.pgBrowser.showHelp(
+ event.button.element.name,
+ event.button.element.getAttribute('url'),
+ node,
+ selectedTreeNode,
+ event.button.element.getAttribute('label')
+ );
+ return;
+ }
+
+ if (this.wasBackupButtonPressed(event)) {
+
+ if (!selectedTreeNodeData)
+ return;
+
+ const serverIdentifier = this.retrieveServerIdentifier(node, selectedTreeNode);
+
+ const dialog = this;
+ let urlShortcut = 'backup.create_server_job';
+ if (this.typeOfDialog === 'backup_objects') {
+ urlShortcut = 'backup.create_object_job';
+ }
+ const baseUrl = url_for(urlShortcut, {
+ 'sid': serverIdentifier,
+ });
+
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.pgBrowser,
+ selectedTreeNode
+ );
+
+ this.setExtraParameters(selectedTreeNode, treeInfo);
+
+ let service = axios.create({});
+ service.post(
+ baseUrl,
+ this.view.model.toJSON()
+ ).then(function () {
+ dialog.alertify.success(gettext('Backup job created.'), 5);
+ dialog.pgBrowser.Events.trigger('pgadmin-bgprocess:created', dialog);
+ }).catch(function (error) {
+ try {
+ const err = error.response.data;
+ dialog.alertify.alert(
+ gettext('Backup job failed.'),
+ err.errormsg
+ );
+ } catch (e) {
+ console.warn(e.stack || e);
+ }
+ });
+ }
+ }
+
+ addAlertifyClassToBackupNodeChildNodes() {
+ this.jquery(this.elements.body.childNodes[0]).addClass(
+ 'alertify_tools_dialog_properties obj_properties'
+ );
+ }
+
+ getSelectedNode() {
+ const tree = this.pgBrowser.treeMenu;
+ const selectedNode = tree.selected();
+ if (selectedNode) {
+ return tree.findNodeByDomElement(selectedNode);
+ } else {
+ return undefined;
+ }
+ }
+
+ disableBackupButton() {
+ this.__internal.buttons[2].element.disabled = true;
+ }
+
+ enableBackupButton() {
+ this.__internal.buttons[2].element.disabled = false;
+ }
+
+ createDialog(node, treeInfo, typeOfDialog, $container) {
+ let attributes = {};
+ if (typeOfDialog !== 'backup_objects') {
+ attributes['type'] = typeOfDialog;
+ }
+ // Instance of backbone model
+ const newModel = new this.dialogModel(attributes, {
+ node_info: treeInfo,
+ });
+ const fields = this.backform.generateViewSchema(
+ treeInfo, newModel, 'create', node, treeInfo.server, true
+ );
+
+ return this.view = new this.backform.Dialog({
+ el: $container,
+ model: newModel,
+ schema: fields,
+ });
+ }
+
+ retrieveServerIdentifier(node, selectedTreeNode) {
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.pgBrowser,
+ selectedTreeNode
+ );
+ return treeInfo.server._id;
+ }
+
+ setListenersForFilenameChanges() {
+ const self = this;
+
+ this.view.model.on('change', function () {
+ if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
+ this.errorModel.clear();
+ self.enableBackupButton();
+ } else {
+ self.disableBackupButton();
+ this.errorModel.set('file', gettext('Please provide a filename'));
+ }
+ });
+ }
+
+ setExtraParameters(selectedTreeNode, treeInfo) {
+ if (this.typeOfDialog === 'backup_objects') {
+
+ this.view.model.set('database', treeInfo.database._label);
+
+ const nodeData = selectedTreeNode.getData();
+ if (nodeData._type === 'schema') {
+ this.view.model.set('schemas', [nodeData._label]);
+ }
+
+ if (nodeData._type === 'table') {
+ this.view.model.set('tables', [
+ [treeInfo.schema._label, nodeData._label],
+ ]);
+ }
+
+ if (_.isEmpty(this.view.model.get('ratio'))) {
+ this.view.model.unset('ratio');
+ }
+ }
+ }
+
+ wasBackupButtonPressed(event) {
+ return event.button['data-btn-name'] === 'backup';
+ }
+}
diff --git a/web/pgadmin/tools/backup/static/js/menu_utils.js b/web/pgadmin/tools/backup/static/js/menu_utils.js
new file mode 100644
index 00000000..47a9f0d3
--- /dev/null
+++ b/web/pgadmin/tools/backup/static/js/menu_utils.js
@@ -0,0 +1,23 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {isValidTreeNodeData} from '../../../../static/js/tree/tree';
+
+export const backupSupportedNodes = [
+ 'database', 'schema', 'table', 'partition',
+];
+
+function isNodeAServerAndConnected(treeNodeData) {
+ return (('server' === treeNodeData._type) && treeNodeData.connected);
+}
+
+export function menuEnabledServer(treeNodeData) {
+ return isValidTreeNodeData(treeNodeData)
+ && isNodeAServerAndConnected(treeNodeData);
+}
diff --git a/web/pgadmin/tools/datagrid/static/js/datagrid.js b/web/pgadmin/tools/datagrid/static/js/datagrid.js
index b0ed60f6..520a9ce5 100644
--- a/web/pgadmin/tools/datagrid/static/js/datagrid.js
+++ b/web/pgadmin/tools/datagrid/static/js/datagrid.js
@@ -1,10 +1,14 @@
define('pgadmin.datagrid', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'pgadmin.alertifyjs', 'sources/pgadmin', 'bundled_codemirror',
- 'sources/sqleditor_utils', 'backbone', 'wcdocker',
+ 'sources/sqleditor_utils', 'backbone',
+ 'tools/datagrid/static/js/show_data',
+ 'tools/datagrid/static/js/get_panel_title',
+ 'tools/datagrid/static/js/show_query_tool',
+ 'wcdocker',
], function(
gettext, url_for, $, _, alertify, pgAdmin, codemirror, sqlEditorUtils,
- Backbone
+ Backbone, showData, panelTitle, showQueryTool
) {
// Some scripts do export their object in the window only.
// Generally the one, which do no have AMD support.
@@ -161,55 +165,7 @@ define('pgadmin.datagrid', [
// This is a callback function to show data when user click on menu item.
show_data_grid: function(data, i) {
- var self = this,
- d = pgAdmin.Browser.tree.itemData(i);
- if (d === undefined) {
- alertify.alert(
- gettext('Data Grid Error'),
- gettext('No object selected.')
- );
- return;
- }
-
- // Get the parent data from the tree node hierarchy.
- var node = pgBrowser.Nodes[d._type],
- parentData = node.getTreeNodeHierarchy(i);
-
- // If server, database or schema is undefined then return from the function.
- if (parentData.server === undefined || parentData.database === undefined) {
- return;
- }
- // If schema, view, catalog object all are undefined then return from the function.
- if (parentData.schema === undefined && parentData.view === undefined &&
- parentData.catalog === undefined) {
- return;
- }
-
- var nsp_name = '';
-
- if (parentData.schema != undefined) {
- nsp_name = parentData.schema.label;
- }
- else if (parentData.view != undefined) {
- nsp_name = parentData.view.label;
- }
- else if (parentData.catalog != undefined) {
- nsp_name = parentData.catalog.label;
- }
- var url_params = {
- 'cmd_type': data.mnuid,
- 'obj_type': d._type,
- 'sgid': parentData.server_group._id,
- 'sid': parentData.server._id,
- 'did': parentData.database._id,
- 'obj_id': d._id,
- };
-
- var baseUrl = url_for('datagrid.initialize_datagrid', url_params);
- var grid_title = parentData.server.label + ' - ' + parentData.database.label + ' - '
- + nsp_name + '.' + d.label;
-
- self.create_transaction(baseUrl, null, 'false', parentData.server.server_type, '', grid_title, '');
+ showData.showDataGrid(this, pgBrowser, alertify, data, i);
},
// This is a callback function to show filtered data when user click on menu item.
@@ -384,63 +340,12 @@ define('pgadmin.datagrid', [
},
get_panel_title: function() {
- // Get the parent data from the tree node hierarchy.
- var tree = pgAdmin.Browser.tree,
- selected_item = tree.selected(),
- item_data = tree.itemData(selected_item);
-
- var node = pgBrowser.Nodes[item_data._type],
- parentData = node.getTreeNodeHierarchy(selected_item);
-
- // If server, database is undefined then return from the function.
- if (parentData.server === undefined) {
- return;
- }
- // If Database is not available then use default db
- var db_label = parentData.database ? parentData.database.label
- : parentData.server.db;
-
- var grid_title = db_label + ' on ' + parentData.server.user.name + '@' +
- parentData.server.label;
- return grid_title;
+ return panelTitle.getPanelTitle(pgBrowser);
},
// This is a callback function to show query tool when user click on menu item.
- show_query_tool: function(url, i, panel_title) {
- var sURL = url || '',
- d = pgAdmin.Browser.tree.itemData(i);
-
- panel_title = panel_title || '';
- if (d === undefined) {
- alertify.alert(
- gettext('Query Tool Error'),
- gettext('No object selected.')
- );
- return;
- }
-
- // Get the parent data from the tree node hierarchy.
- var node = pgBrowser.Nodes[d._type],
- parentData = node.getTreeNodeHierarchy(i);
-
- // If server, database is undefined then return from the function.
- if (parentData.server === undefined) {
- return;
- }
-
- var url_params = {
- 'sgid': parentData.server_group._id,
- 'sid': parentData.server._id,
- },
- url_endpoint = 'datagrid.initialize_query_tool';
- // If database not present then use Maintenance database
- // We will handle this at server side
- if (parentData.database) {
- url_params['did'] = parentData.database._id;
- url_endpoint = 'datagrid.initialize_query_tool_with_did';
- }
- var baseUrl = url_for(url_endpoint, url_params);
-
- this.create_transaction(baseUrl, null, 'true', parentData.server.server_type, sURL, panel_title, '', false);
+ show_query_tool: function(url, aciTreeIdentifier, panelTitle) {
+ showQueryTool.showQueryTool(this, pgBrowser, alertify, url,
+ aciTreeIdentifier, panelTitle);
},
create_transaction: function(baseUrl, target, is_query_tool, server_type, sURL, panel_title, sql_filter, recreate) {
var self = this;
diff --git a/web/pgadmin/tools/datagrid/static/js/get_panel_title.js b/web/pgadmin/tools/datagrid/static/js/get_panel_title.js
new file mode 100644
index 00000000..64b3a3d2
--- /dev/null
+++ b/web/pgadmin/tools/datagrid/static/js/get_panel_title.js
@@ -0,0 +1,33 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {getTreeNodeHierarchyFromIdentifier} from '../../../../static/js/tree/pgadmin_tree_node';
+
+function getDatabaseLabel(parentData) {
+ return parentData.database ? parentData.database.label
+ : parentData.server.db;
+}
+
+function isServerInformationAvailable(parentData) {
+ return parentData.server === undefined;
+}
+
+export function getPanelTitle(pgBrowser) {
+ const selected_item = pgBrowser.treeMenu.selected();
+
+ const parentData = getTreeNodeHierarchyFromIdentifier
+ .call(pgBrowser, selected_item);
+ if (isServerInformationAvailable(parentData)) {
+ return;
+ }
+
+ const db_label = getDatabaseLabel(parentData);
+
+ return `${db_label} on ${parentData.server.user.name}@${parentData.server.label}`;
+}
diff --git a/web/pgadmin/tools/datagrid/static/js/show_data.js b/web/pgadmin/tools/datagrid/static/js/show_data.js
new file mode 100644
index 00000000..373c97cd
--- /dev/null
+++ b/web/pgadmin/tools/datagrid/static/js/show_data.js
@@ -0,0 +1,92 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import gettext from '../../../../static/js/gettext';
+import url_for from '../../../../static/js/url_for';
+import {getTreeNodeHierarchyFromIdentifier} from '../../../../static/js/tree/pgadmin_tree_node';
+
+export function showDataGrid(
+ datagrid,
+ pgBrowser,
+ alertify,
+ connectionData,
+ aciTreeIdentifier
+) {
+ const node = pgBrowser.treeMenu.findNodeByDomElement(aciTreeIdentifier);
+ if (node === undefined || !node.getData()) {
+ alertify.alert(
+ gettext('Data Grid Error'),
+ gettext('No object selected.')
+ );
+ return;
+ }
+
+ const parentData = getTreeNodeHierarchyFromIdentifier.call(
+ pgBrowser,
+ aciTreeIdentifier
+ );
+
+ if (hasServerOrDatabaseConfiguration(parentData)
+ || !hasSchemaOrCatalogOrViewInformation(parentData)) {
+ return;
+ }
+
+ let namespaceName = retrieveNameSpaceName(parentData);
+ const baseUrl = generateUrl(connectionData, node.getData(), parentData);
+ const grid_title = generateDatagridTitle(parentData, namespaceName, node.getData());
+
+ datagrid.create_transaction(
+ baseUrl,
+ null,
+ 'false',
+ parentData.server.server_type,
+ '',
+ grid_title,
+ ''
+ );
+}
+
+
+function retrieveNameSpaceName(parentData) {
+ if (parentData.schema !== undefined) {
+ return parentData.schema.label;
+ }
+ else if (parentData.view !== undefined) {
+ return parentData.view.label;
+ }
+ else if (parentData.catalog !== undefined) {
+ return parentData.catalog.label;
+ }
+ return '';
+}
+
+function generateUrl(connectionData, nodeData, parentData) {
+ const url_params = {
+ 'cmd_type': connectionData.mnuid,
+ 'obj_type': nodeData._type,
+ 'sgid': parentData.server_group._id,
+ 'sid': parentData.server._id,
+ 'did': parentData.database._id,
+ 'obj_id': nodeData._id,
+ };
+
+ return url_for('datagrid.initialize_datagrid', url_params);
+}
+
+function hasServerOrDatabaseConfiguration(parentData) {
+ return parentData.server === undefined || parentData.database === undefined;
+}
+
+function hasSchemaOrCatalogOrViewInformation(parentData) {
+ return parentData.schema !== undefined || parentData.view !== undefined ||
+ parentData.catalog !== undefined;
+}
+
+function generateDatagridTitle(parentData, namespaceName, nodeData) {
+ return `${parentData.server.label} - ${parentData.database.label} - ${namespaceName}.${nodeData.label}`;
+}
diff --git a/web/pgadmin/tools/datagrid/static/js/show_query_tool.js b/web/pgadmin/tools/datagrid/static/js/show_query_tool.js
new file mode 100644
index 00000000..0eb12b8b
--- /dev/null
+++ b/web/pgadmin/tools/datagrid/static/js/show_query_tool.js
@@ -0,0 +1,63 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../../../../static/js/gettext';
+import url_for from '../../../../static/js/url_for';
+import {getTreeNodeHierarchyFromIdentifier} from '../../../../static/js/tree/pgadmin_tree_node';
+
+function hasDatabaseInformation(parentData) {
+ return parentData.database;
+}
+
+function generateUrl(parentData) {
+ let url_endpoint = 'datagrid.initialize_query_tool';
+ let url_params = {
+ 'sgid': parentData.server_group._id,
+ 'sid': parentData.server._id,
+ };
+
+ if (hasDatabaseInformation(parentData)) {
+ url_params['did'] = parentData.database._id;
+ url_endpoint = 'datagrid.initialize_query_tool_with_did';
+ }
+
+ return url_for(url_endpoint, url_params);
+}
+
+function hasServerInformations(parentData) {
+ return parentData.server === undefined;
+}
+
+export function showQueryTool(datagrid, pgBrowser, alertify, url,
+ aciTreeIdentifier, panelTitle) {
+ const sURL = url || '';
+ const queryToolTitle = panelTitle || '';
+
+ const currentNode = pgBrowser.treeMenu.findNodeByDomElement(aciTreeIdentifier);
+ if (currentNode === undefined) {
+ alertify.alert(
+ gettext('Query Tool Error'),
+ gettext('No object selected.')
+ );
+ return;
+ }
+
+ const parentData = getTreeNodeHierarchyFromIdentifier.call(
+ pgBrowser, aciTreeIdentifier);
+
+ if (hasServerInformations(parentData)) {
+ return;
+ }
+
+ const baseUrl = generateUrl(parentData);
+
+ datagrid.create_transaction(
+ baseUrl, null, 'true',
+ parentData.server.server_type, sURL, queryToolTitle, '', false);
+}
diff --git a/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js b/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js
index 750887ec..753813f0 100644
--- a/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js
+++ b/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js
@@ -2,12 +2,15 @@
define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'pgadmin.alertifyjs', 'pgadmin.backgrid', 'pgadmin.backform',
- 'pgadmin.browser', 'pgadmin.browser.node', 'backgrid.select.all',
+ 'pgadmin.browser', 'pgadmin.browser.node',
+ 'tools/grant_wizard/static/js/menu_utils',
+ 'sources/nodes/supported_database_node',
+ 'backgrid.select.all',
'backgrid.filter', 'pgadmin.browser.server.privilege',
'pgadmin.browser.wizard',
], function(
gettext, url_for, $, _, Backbone, Alertify, Backgrid, Backform, pgBrowser,
- pgNode
+ pgNode, menuUtils, supportedNodes
) {
// if module is already initialized, refer to that.
@@ -143,41 +146,6 @@ define([
this.initialized = true;
- // Define list of nodes on which grant wizard context menu option appears
- var supported_nodes = [
- 'schema', 'coll-function', 'coll-sequence',
- 'coll-table', 'coll-view', 'coll-procedure',
- 'coll-mview', 'database', 'coll-trigger_function',
- ],
-
- /**
- Enable/disable grantwizard menu in tools based
- on node selected
- if selected node is present in supported_nodes,
- menu will be enabled otherwise disabled.
- Also, hide it for system view in catalogs
- */
- menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
- var parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(supported_nodes, d._type) !== -1 &&
- parent_data._type != 'catalog') {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
// Define the nodes on which the menus to be appear
var menus = [{
name: 'grant_wizard_schema',
@@ -187,21 +155,25 @@ define([
priority: 14,
label: gettext('Grant Wizard...'),
icon: 'fa fa-unlock-alt',
- enable: menu_enabled,
+ enable: supportedNodes.enabled.bind(
+ null, pgBrowser.treeMenu, menuUtils.supportedNodes
+ ),
}];
// Add supported menus into the menus list
- for (var idx = 0; idx < supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.supportedNodes.length; idx++) {
menus.push({
- name: 'grant_wizard_schema_context_' + supported_nodes[idx],
- node: supported_nodes[idx],
+ name: 'grant_wizard_schema_context_' + menuUtils.supportedNodes[idx],
+ node: menuUtils.supportedNodes[idx],
module: this,
applies: ['context'],
callback: 'start_grant_wizard',
priority: 14,
label: gettext('Grant Wizard...'),
icon: 'fa fa-unlock-alt',
- enable: menu_enabled,
+ enable: supportedNodes.enabled.bind(
+ null, pgBrowser.treeMenu, menuUtils.supportedNodes
+ ),
});
}
pgBrowser.add_menus(menus);
diff --git a/web/pgadmin/tools/grant_wizard/static/js/menu_utils.js b/web/pgadmin/tools/grant_wizard/static/js/menu_utils.js
new file mode 100644
index 00000000..b56ce3cd
--- /dev/null
+++ b/web/pgadmin/tools/grant_wizard/static/js/menu_utils.js
@@ -0,0 +1,16 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export const supportedNodes = [
+ 'schema', 'coll-function', 'coll-sequence',
+ 'coll-table', 'coll-view', 'coll-procedure',
+ 'coll-mview', 'database', 'coll-trigger_function',
+];
+
+
diff --git a/web/pgadmin/tools/import_export/static/js/import_export.js b/web/pgadmin/tools/import_export/static/js/import_export.js
index 3058f122..f9e9ac7b 100644
--- a/web/pgadmin/tools/import_export/static/js/import_export.js
+++ b/web/pgadmin/tools/import_export/static/js/import_export.js
@@ -1,10 +1,12 @@
define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'underscore.string', 'pgadmin.alertifyjs',
'sources/pgadmin', 'pgadmin.browser', 'backbone', 'backgrid', 'backform',
- 'sources/utils', 'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser.node.ui',
+ 'sources/utils',
+ 'sources/nodes/supported_database_node',
+ 'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser.node.ui',
], function(
gettext, url_for, $, _, S, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid,
-Backform, commonUtils
+Backform, commonUtils, supportedNodes
) {
pgAdmin = pgAdmin || window.pgAdmin || {};
@@ -383,25 +385,6 @@ Backform, commonUtils
this.initialized = true;
- /*
- * Enable/disable import menu in tools based on node selected. Import
- * menu will be enabled only when user select table node.
- */
- var menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
- var parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data))
- return (
- (_.indexOf(['table'], d._type) !== -1 &&
- parent_data._type != 'catalog') ? true : false
- );
- else
- return false;
- };
-
// Initialize the context menu to display the import options when user open the context menu for table
pgBrowser.add_menus([{
name: 'import',
@@ -413,7 +396,9 @@ Backform, commonUtils
priority: 10,
label: gettext('Import/Export...'),
icon: 'fa fa-shopping-cart',
- enable: menu_enabled,
+ enable: supportedNodes.enabled.bind(
+ null, pgBrowser.treeMenu, ['table']
+ ),
}]);
},
diff --git a/web/pgadmin/tools/maintenance/static/js/maintenance.js b/web/pgadmin/tools/maintenance/static/js/maintenance.js
index f2102602..df05c3d5 100644
--- a/web/pgadmin/tools/maintenance/static/js/maintenance.js
+++ b/web/pgadmin/tools/maintenance/static/js/maintenance.js
@@ -2,11 +2,14 @@ define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'pgadmin.alertifyjs', 'sources/pgadmin', 'pgadmin.browser', 'backbone',
'backgrid', 'backform', 'sources/utils',
+ 'tools/maintenance/static/js/menu_utils',
+ 'sources/nodes/supported_database_node',
'pgadmin.backform', 'pgadmin.backgrid',
'pgadmin.browser.node.ui',
], function(
gettext, url_for, $, _, S, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid,
- Backform, commonUtils
+ Backform, commonUtils,
+ menuUtils, supportedNodes
) {
pgAdmin = pgAdmin || window.pgAdmin || {};
@@ -168,36 +171,6 @@ define([
this.initialized = true;
- var maintenance_supported_nodes = [
- 'database', 'table', 'primary_key',
- 'unique_constraint', 'index', 'partition',
- ];
-
- /**
- Enable/disable Maintenance menu in tools based on node selected.
- Maintenance menu will be enabled only when user select table and database node.
- */
- var menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
- var parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(maintenance_supported_nodes, d._type) !== -1 &&
- parent_data._type != 'catalog') {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
var menus = [{
name: 'maintenance',
module: this,
@@ -206,21 +179,25 @@ define([
priority: 10,
label: gettext('Maintenance...'),
icon: 'fa fa-wrench',
- enable: menu_enabled,
+ enable: supportedNodes.enabled.bind(
+ null, pgBrowser.treeMenu, menuUtils.maintenanceSupportedNodes
+ ),
}];
// Add supported menus into the menus list
- for (var idx = 0; idx < maintenance_supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.maintenanceSupportedNodes.length; idx++) {
menus.push({
- name: 'maintenance_context_' + maintenance_supported_nodes[idx],
- node: maintenance_supported_nodes[idx],
+ name: 'maintenance_context_' + menuUtils.maintenanceSupportedNodes[idx],
+ node: menuUtils.maintenanceSupportedNodes[idx],
module: this,
applies: ['context'],
callback: 'callback_maintenance',
priority: 10,
label: gettext('Maintenance...'),
icon: 'fa fa-wrench',
- enable: menu_enabled,
+ enable: supportedNodes.enabled.bind(
+ null, pgBrowser.treeMenu, menuUtils.maintenanceSupportedNodes
+ ),
});
}
pgBrowser.add_menus(menus);
diff --git a/web/pgadmin/tools/maintenance/static/js/menu_utils.js b/web/pgadmin/tools/maintenance/static/js/menu_utils.js
new file mode 100644
index 00000000..8cde1baa
--- /dev/null
+++ b/web/pgadmin/tools/maintenance/static/js/menu_utils.js
@@ -0,0 +1,13 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export const maintenanceSupportedNodes = [
+ 'database', 'table', 'primary_key',
+ 'unique_constraint', 'index', 'partition',
+];
diff --git a/web/pgadmin/tools/restore/static/js/menu_utils.js b/web/pgadmin/tools/restore/static/js/menu_utils.js
new file mode 100644
index 00000000..2d35c951
--- /dev/null
+++ b/web/pgadmin/tools/restore/static/js/menu_utils.js
@@ -0,0 +1,18 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export const restoreSupportedNodes = [
+ 'database',
+ 'schema',
+ 'table',
+ 'function',
+ 'trigger',
+ 'index',
+ 'partition',
+];
diff --git a/web/pgadmin/tools/restore/static/js/restore.js b/web/pgadmin/tools/restore/static/js/restore.js
index 585b9729..3bde6de7 100644
--- a/web/pgadmin/tools/restore/static/js/restore.js
+++ b/web/pgadmin/tools/restore/static/js/restore.js
@@ -1,11 +1,13 @@
-// Restore dialog
define('tools.restore', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'underscore.string', 'pgadmin.alertifyjs', 'pgadmin.browser',
'pgadmin.backgrid', 'pgadmin.backform', 'sources/utils',
+ 'tools/restore/static/js/menu_utils',
+ 'sources/nodes/supported_database_node',
+ 'tools/restore/static/js/restore_dialog',
], function(
gettext, url_for, $, _, Backbone, S, alertify, pgBrowser, Backgrid, Backform,
-commonUtils
+commonUtils, menuUtils, supportedNodes, restoreDialog
) {
// if module is already initialized, refer to that.
@@ -307,59 +309,6 @@ commonUtils
this.initialized = true;
- // Define list of nodes on which restore context menu option appears
- var restore_supported_nodes = [
- 'database', 'schema',
- 'table', 'function',
- 'trigger', 'index',
- 'partition',
- ];
-
- /**
- Enable/disable restore menu in tools based
- on node selected
- if selected node is present in supported_nodes,
- menu will be enabled otherwise disabled.
- Also, hide it for system view in catalogs
- */
- var menu_enabled = function(itemData, item, data) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
- var parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(restore_supported_nodes, d._type) !== -1 &&
- is_parent_catalog(itemData, item, data)) {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
- var is_parent_catalog = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
-
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to restore
- if (_.indexOf(['catalog'], d._type) > -1)
- return false;
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- };
-
// Define the nodes on which the menus to be appear
var menus = [{
name: 'restore_object',
@@ -369,20 +318,24 @@ commonUtils
priority: 13,
label: gettext('Restore...'),
icon: 'fa fa-upload',
- enable: menu_enabled,
+ enable: supportedNodes.enabled.bind(
+ null, pgBrowser.treeMenu, menuUtils.restoreSupportedNodes
+ ),
}];
- for (var idx = 0; idx < restore_supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.restoreSupportedNodes.length; idx++) {
menus.push({
- name: 'restore_' + restore_supported_nodes[idx],
- node: restore_supported_nodes[idx],
+ name: 'restore_' + menuUtils.restoreSupportedNodes[idx],
+ node: menuUtils.restoreSupportedNodes[idx],
module: this,
applies: ['context'],
callback: 'restore_objects',
priority: 13,
label: gettext('Restore...'),
icon: 'fa fa-upload',
- enable: menu_enabled,
+ enable: supportedNodes.enabled.bind(
+ null, pgBrowser.treeMenu, menuUtils.restoreSupportedNodes
+ ),
});
}
@@ -391,318 +344,8 @@ commonUtils
},
// Callback to draw Backup Dialog for objects
restore_objects: function(action, treeItem) {
-
- var i = treeItem || pgBrowser.tree.selected(),
- server_data = null;
-
- while (i) {
- var node_data = pgBrowser.tree.itemData(i);
- if (node_data._type == 'server') {
- server_data = node_data;
- break;
- }
-
- if (pgBrowser.tree.hasParent(i)) {
- i = $(pgBrowser.tree.parent(i));
- } else {
- alertify.alert(
- gettext('Restore Error'),
- gettext('Please select server or child node from tree.')
- );
- break;
- }
- }
-
- if (!server_data) {
- return;
- }
-
- var module = 'paths',
- preference_name = 'pg_bin_dir',
- msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
-
- if ((server_data.type && server_data.type == 'ppas') ||
- server_data.server_type == 'ppas') {
- preference_name = 'ppas_bin_dir';
- msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
- }
-
- var preference = pgBrowser.get_preference(module, preference_name);
-
- if (preference) {
- if (!preference.value) {
- alertify.alert(gettext('Configuration required'), msg);
- return;
- }
- } else {
- alertify.alert(
- gettext('Restore Error'),
- S(gettext('Failed to load preference %s of module %s')).sprintf(preference_name, module).value()
- );
- return;
- }
-
- var title = S(gettext('Restore (%s: %s)')),
- tree = pgBrowser.tree,
- item = treeItem || tree.selected(),
- data = item && item.length == 1 && tree.itemData(item),
- node = data && data._type && pgBrowser.Nodes[data._type];
-
- if (!node)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [item]);
-
- if (treeInfo.database._label.indexOf('=') >= 0) {
- alertify.alert(
- gettext('Restore error'),
- gettext('Restore job creation failed. '+
- 'Databases with = symbols in the name cannot be restored using this utility.')
- );
- return;
- }
-
- title = title.sprintf(node.label, data.label).value();
-
- if (!alertify.pg_restore) {
- // Create Dialog title on the fly with node details
- alertify.dialog('pg_restore', function factory() {
- return {
- main: function(title, item, data, node) {
- this.set('title', title);
- this.setting('pg_node', node);
- this.setting('pg_item', item);
- this.setting('pg_item_data', data);
- },
- build: function() {
- alertify.pgDialogBuild.apply(this);
- },
- setup: function() {
- return {
- buttons: [{
- text: '',
- className: 'btn btn-default pull-left fa fa-lg fa-info',
- attrs: {
- name: 'object_help',
- type: 'button',
- url: 'backup.html',
- label: gettext('Restore'),
- },
- }, {
- text: '',
- key: 112,
- className: 'btn btn-default pull-left fa fa-lg fa-question',
- attrs: {
- name: 'dialog_help',
- type: 'button',
- label: gettext('Restore'),
- url: url_for('help.static', {
- 'filename': 'restore_dialog.html',
- }),
- },
- }, {
- text: gettext('Restore'),
- key: 13,
- className: 'btn btn-primary fa fa-upload pg-alertify-button',
- restore: true,
- 'data-btn-name': 'restore',
- }, {
- text: gettext('Cancel'),
- key: 27,
- className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
- restore: false,
- 'data-btn-name': 'cancel',
- }],
- // Set options for dialog
- options: {
- title: title,
- //disable both padding and overflow control.
- padding: !1,
- overflow: !1,
- model: 0,
- resizable: true,
- maximizable: true,
- pinnable: false,
- closableByDimmer: false,
- modal: false,
- },
- };
- },
- hooks: {
- // triggered when the dialog is closed
- onclose: function() {
- if (this.view) {
- this.view.remove({
- data: true,
- internal: true,
- silent: true,
- });
- }
- },
- },
- settings: {
- pg_node: null,
- pg_item: null,
- pg_item_data: null,
- },
- prepare: function() {
-
- var self = this;
- // Disable Backup button until user provides Filename
- this.__internal.buttons[2].element.disabled = true;
- var $container = $('<div class=\'restore_dialog\'></div>');
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- var newModel = new RestoreObjectModel({
- node_data: node,
- }, {
- node_info: treeInfo,
- }),
- fields = Backform.generateViewSchema(
- treeInfo, newModel, 'create', node, treeInfo.server, true
- );
-
- var view = this.view = new Backform.Dialog({
- el: $container,
- model: newModel,
- schema: fields,
- });
-
- $(this.elements.body.childNodes[0]).addClass(
- 'alertify_tools_dialog_properties obj_properties'
- );
-
- view.render();
-
- this.elements.content.appendChild($container.get(0));
-
- view.$el.attr('tabindex', -1);
- // var dialogTabNavigator = pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
- commonUtils.findAndSetFocus(container);
-
- // Listen to model & if filename is provided then enable Backup button
- this.view.model.on('change', function() {
- if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
- this.errorModel.clear();
- self.__internal.buttons[2].element.disabled = false;
- } else {
- self.__internal.buttons[2].element.disabled = true;
- this.errorModel.set('file', gettext('Please provide filename'));
- }
- });
-
- },
- // Callback functions when click on the buttons of the Alertify dialogs
- callback: function(e) {
- // Fetch current server id
- var t = pgBrowser.tree,
- i = this.settings['pg_item'] || t.selected(),
- d = this.settings['pg_item_data'] || (
- i && i.length == 1 ? t.itemData(i) : undefined
- ),
- node = this.settings['pg_node'] || (
- d && pgBrowser.Nodes[d._type]
- );
-
- if (e.button.element.name == 'dialog_help' || e.button.element.name == 'object_help') {
- e.cancel = true;
- pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
- node, i, e.button.element.getAttribute('label'));
- return;
- }
-
- if (e.button['data-btn-name'] === 'restore') {
- if (!d)
- return;
-
- var info = node.getTreeNodeHierarchy.apply(node, [i]),
- m = this.view.model;
- // Set current node info into model
- m.set('database', info.database._label);
- if (!m.get('custom')) {
- switch (d._type) {
- case 'schema':
- m.set('schemas', [d._label]);
- break;
- case 'table':
- m.set('schemas', [info.schema._label]);
- m.set('tables', [d._label]);
- break;
- case 'function':
- m.set('schemas', [info.schema._label]);
- m.set('functions', [d._label]);
- break;
- case 'index':
- m.set('schemas', [info.schema._label]);
- m.set('indexes', [d._label]);
- break;
- case 'trigger':
- m.set('schemas', [info.schema._label]);
- m.set('triggers', [d._label]);
- break;
- case 'trigger_func':
- m.set('schemas', [info.schema._label]);
- m.set('trigger_funcs', [d._label]);
- break;
- }
- } else {
- // TODO::
- // When we will implement the object selection in the
- // import dialog, we will need to select the objects from
- // the tree selection tab.
- }
-
- var self = this,
- baseUrl = url_for('restore.create_job', {
- 'sid': info.server._id,
- }),
- args = this.view.model.toJSON();
-
- $.ajax({
- url: baseUrl,
- method: 'POST',
- data: {
- 'data': JSON.stringify(args),
- },
- success: function(res) {
- if (res.success) {
- alertify.success(
- gettext('Restore job created.'), 5
- );
- pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
- } else {
- console.warn(res);
- }
- },
- error: function(xhr) {
- try {
- var err = $.parseJSON(xhr.responseText);
- alertify.alert(
- gettext('Restore failed.'),
- err.errormsg
- );
- } catch (e) {
- console.warn(e.stack || e);
- }
- },
- });
- }
- },
- };
- });
- }
-
- alertify.pg_restore(title, item, data, node).resizeTo('65%', '60%');
+ let dialog = new restoreDialog.RestoreDialog(pgBrowser, $, alertify, RestoreObjectModel);
+ dialog.draw(action, treeItem);
},
};
return pgBrowser.Restore;
diff --git a/web/pgadmin/tools/restore/static/js/restore_dialog.js b/web/pgadmin/tools/restore/static/js/restore_dialog.js
new file mode 100644
index 00000000..4884d901
--- /dev/null
+++ b/web/pgadmin/tools/restore/static/js/restore_dialog.js
@@ -0,0 +1,57 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../../../../static/js/gettext';
+import {sprintf} from 'sprintf-js';
+import Backform from '../../../../static/js/backform.pgadmin';
+import {Dialog} from '../../../../static/js/alertify/dialog';
+
+export class RestoreDialog extends Dialog {
+ constructor(pgBrowser, $, alertify, RestoreModel, backform = Backform) {
+ super('Restore Error',
+ '<div class=\'restore_dialog\'></div>',
+ pgBrowser, $, alertify, RestoreModel, backform);
+ }
+
+ draw(action, aciTreeItem) {
+
+ const serverInformation = this.retrieveAncestorOfTypeServer(aciTreeItem);
+
+ if (!serverInformation) {
+ return;
+ }
+
+ if (!this.hasBinariesConfiguration(serverInformation)) {
+ return;
+ }
+
+ if (!this.canExecuteOnCurrentDatabase(aciTreeItem)) {
+ return;
+ }
+
+ let aciTreeItem1 = aciTreeItem || this.pgBrowser.treeMenu.selected();
+ let item = this.pgBrowser.treeMenu.findNodeByDomElement(aciTreeItem1);
+ const data = item.getData();
+ const node = this.pgBrowser.Nodes[data._type];
+
+ if (!node)
+ return;
+
+ let title = sprintf(gettext('Restore (%s: %s)'), node.label, data.label);
+
+ this.createOrGetDialog(title, 'restore');
+
+ this.alertify.pg_restore(title, aciTreeItem1, data, node).resizeTo('65%', '60%');
+ }
+
+ dialogName() {
+ return 'pg_restore';
+ }
+}
+
diff --git a/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js b/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js
new file mode 100644
index 00000000..845da7a3
--- /dev/null
+++ b/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js
@@ -0,0 +1,255 @@
+import {getTreeNodeHierarchyFromElement} from '../../../../static/js/tree/pgadmin_tree_node';
+import axios from 'axios/index';
+import _ from 'underscore';
+import gettext from '../../../../static/js/gettext';
+import url_for from '../../../../static/js/url_for';
+import {DialogWrapper} from '../../../../static/js/alertify/dialog_wrapper';
+
+export class RestoreDialogWrapper extends DialogWrapper {
+ constructor(dialogContainerSelector, dialogTitle, typeOfDialog,
+ jquery, pgBrowser, alertify, dialogModel, backform) {
+ super(dialogContainerSelector, dialogTitle, jquery,
+ pgBrowser, alertify, dialogModel, backform);
+ }
+
+ main(title, item, data, node) {
+ this.set('title', title);
+ this.setting('pg_node', node);
+ this.setting('pg_item', item);
+ this.setting('pg_item_data', data);
+ }
+
+ setup() {
+ return {
+ buttons: [{
+ text: '',
+ className: 'btn btn-default pull-left fa fa-lg fa-info',
+ attrs: {
+ name: 'object_help',
+ type: 'button',
+ url: 'backup.html',
+ label: gettext('Restore'),
+ },
+ }, {
+ text: '',
+ key: 112,
+ className: 'btn btn-default pull-left fa fa-lg fa-question',
+ attrs: {
+ name: 'dialog_help',
+ type: 'button',
+ label: gettext('Restore'),
+ url: url_for('help.static', {
+ 'filename': 'restore_dialog.html',
+ }),
+ },
+ }, {
+ text: gettext('Restore'),
+ key: 13,
+ className: 'btn btn-primary fa fa-upload pg-alertify-button',
+ restore: true,
+ 'data-btn-name': 'restore',
+ }, {
+ text: gettext('Cancel'),
+ key: 27,
+ className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
+ restore: false,
+ 'data-btn-name': 'cancel',
+ }],
+ // Set options for dialog
+ options: {
+ title: this.dialogTitle,
+ //disable both padding and overflow control.
+ padding: !1,
+ overflow: !1,
+ model: 0,
+ resizable: true,
+ maximizable: true,
+ pinnable: false,
+ closableByDimmer: false,
+ modal: false,
+ },
+ };
+ }
+
+ prepare() {
+ this.disableRestoreButton();
+
+ const $container = this.jquery(this.dialogContainerSelector);
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = this.getSelectedNodeData(selectedTreeNode);
+ if (!selectedTreeNodeData) {
+ return;
+ }
+
+ const node = this.pgBrowser.Nodes[selectedTreeNodeData._type];
+
+ const treeInfo = getTreeNodeHierarchyFromElement(this.pgBrowser, selectedTreeNode);
+ const dialog = this.createDialog(node, treeInfo, $container);
+ this.addAlertifyClassToRestoreNodeChildNodes();
+ dialog.render();
+
+ this.elements.content.appendChild($container.get(0));
+
+ this.focusOnDialog(dialog);
+ this.setListenersForFilenameChanges();
+ }
+
+ callback(event) {
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = this.getSelectedNodeData(selectedTreeNode);
+ const node = selectedTreeNodeData && this.pgBrowser.Nodes[selectedTreeNodeData._type];
+
+ if (this.wasHelpButtonPressed(event)) {
+ event.cancel = true;
+ this.pgBrowser.showHelp(
+ event.button.element.name,
+ event.button.element.getAttribute('url'),
+ node,
+ selectedTreeNode,
+ event.button.element.getAttribute('label')
+ );
+ return;
+ }
+
+ if (this.wasRestoreButtonPressed(event)) {
+
+ if (!selectedTreeNodeData)
+ return;
+
+ const serverIdentifier = this.retrieveServerIdentifier(node, selectedTreeNode);
+
+ const dialogWrapper = this;
+ let urlShortcut = 'restore.create_job';
+
+ const baseUrl = url_for(urlShortcut, {
+ 'sid': serverIdentifier,
+ });
+
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.pgBrowser,
+ selectedTreeNode
+ );
+
+ this.setExtraParameters(selectedTreeNode, treeInfo);
+
+ let service = axios.create({});
+ service.post(
+ baseUrl,
+ this.view.model.toJSON()
+ ).then(function () {
+ dialogWrapper.alertify.success(gettext('Restore job created.'), 5);
+ dialogWrapper.pgBrowser.Events.trigger('pgadmin-bgprocess:created', dialogWrapper);
+ }).catch(function (error) {
+ try {
+ const err = error.response.data;
+ dialogWrapper.alertify.alert(
+ gettext('Restore job failed.'),
+ err.errormsg
+ );
+ } catch (e) {
+ console.warn(e.stack || e);
+ }
+ });
+ }
+ }
+
+ addAlertifyClassToRestoreNodeChildNodes() {
+ this.jquery(this.elements.body.childNodes[0]).addClass(
+ 'alertify_tools_dialog_properties obj_properties'
+ );
+ }
+
+ getSelectedNode() {
+ const tree = this.pgBrowser.treeMenu;
+ const selectedNode = tree.selected();
+ if (selectedNode) {
+ return tree.findNodeByDomElement(selectedNode);
+ } else {
+ return undefined;
+ }
+ }
+
+ disableRestoreButton() {
+ this.__internal.buttons[2].element.disabled = true;
+ }
+
+ enableRestoreButton() {
+ this.__internal.buttons[2].element.disabled = false;
+ }
+
+ createDialog(node, treeInfo, $container) {
+ const newModel = new this.dialogModel({
+ node_data: node,
+ }, {
+ node_info: treeInfo,
+ });
+ const fields = this.backform.generateViewSchema(
+ treeInfo, newModel, 'create', node, treeInfo.server, true
+ );
+
+ return this.view = new this.backform.Dialog({
+ el: $container,
+ model: newModel,
+ schema: fields,
+ });
+ }
+
+ retrieveServerIdentifier(node, selectedTreeNode) {
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.pgBrowser,
+ selectedTreeNode
+ );
+ return treeInfo.server._id;
+ }
+
+ setListenersForFilenameChanges() {
+ const self = this;
+
+ this.view.model.on('change', function () {
+ if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
+ this.errorModel.clear();
+ self.enableRestoreButton();
+ } else {
+ self.disableRestoreButton();
+ this.errorModel.set('file', gettext('Please provide a filename'));
+ }
+ });
+ }
+
+ setExtraParameters(selectedTreeNode, treeInfo) {
+ this.view.model.set('database', treeInfo.database._label);
+ if (!this.view.model.get('custom')) {
+ const nodeData = selectedTreeNode.getData();
+
+ switch (nodeData._type) {
+ case 'schema':
+ this.view.model.set('schemas', [nodeData._label]);
+ break;
+ case 'table':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('tables', [nodeData._label]);
+ break;
+ case 'function':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('functions', [nodeData._label]);
+ break;
+ case 'index':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('indexes', [nodeData._label]);
+ break;
+ case 'trigger':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('triggers', [nodeData._label]);
+ break;
+ case 'trigger_func':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('trigger_funcs', [nodeData._label]);
+ break;
+ }
+ }
+ }
+
+ wasRestoreButtonPressed(event) {
+ return event.button['data-btn-name'] === 'restore';
+ }
+}
diff --git a/web/regression/javascript/backup/backup_dialog_spec.js b/web/regression/javascript/backup/backup_dialog_spec.js
new file mode 100644
index 00000000..2655059d
--- /dev/null
+++ b/web/regression/javascript/backup/backup_dialog_spec.js
@@ -0,0 +1,205 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {BackupDialog} from '../../../pgadmin/tools/backup/static/js/backup_dialog';
+import {TreeFake} from '../tree/tree_fake';
+
+const context = describe;
+
+describe('BackupDialog', () => {
+ let backupDialog;
+ let pgBrowser;
+ let jquerySpy;
+ let alertifySpy;
+ let backupModelSpy;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: {
+ hasId: true,
+ label: 'server',
+ getTreeNodeHierarchy: jasmine.createSpy('server.getTreeNodeHierarchy'),
+ },
+ database: {
+ hasId: true,
+ label: 'database',
+ getTreeNodeHierarchy: jasmine.createSpy('db.getTreeNodeHierarchy'),
+ },
+ schema: {
+ hasId: true,
+ label: 'schema',
+ getTreeNodeHierarchy: jasmine.createSpy('db.getTreeNodeHierarchy'),
+ },
+ },
+ };
+ pgBrowser.Nodes.server.hasId = true;
+ pgBrowser.Nodes.database.hasId = true;
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ backupModelSpy = jasmine.createSpy('backupModelSpy');
+
+ const hierarchy = {
+ children: [
+ {
+ id: 'root',
+ children: [
+ {
+ id: 'serverTreeNode',
+ data: {
+ _id: 10,
+ _type: 'server',
+ },
+ children: [
+ {
+ id: 'some_database',
+ data: {
+ _type: 'database',
+ _id: 11,
+ label: 'some_database',
+ _label: 'some_database_label',
+ },
+ }, {
+ id: 'database_with_equal_in_name',
+ data: {
+ _type: 'database',
+ label: 'some_database',
+ _label: '=some_database_label',
+ },
+ },
+ ],
+ },
+ {
+ id: 'ppasServer',
+ data: {
+ _type: 'server',
+ server_type: 'ppas',
+ children: [
+ {id: 'someNodeUnderneathPPASServer'},
+ ],
+ },
+ },
+ ],
+ },
+ ],
+ };
+
+ pgBrowser.treeMenu = TreeFake.build(hierarchy);
+ });
+
+ describe('#draw', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ alertifySpy['backup_objects'] = jasmine.createSpy('backup_objects');
+ backupDialog = new BackupDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ backupModelSpy
+ );
+
+ pgBrowser.get_preference = jasmine.createSpy('get_preferences');
+ });
+
+ context('there are no ancestors of the type server', () => {
+ it('does not create a dialog', () => {
+ pgBrowser.treeMenu.selectNode([{id: 'root'}]);
+ backupDialog.draw(null, null, null);
+ expect(alertifySpy['backup_objects']).not.toHaveBeenCalled();
+ });
+
+ it('display an alert with a Backup Error', () => {
+ backupDialog.draw(null, [{id: 'root'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Please select server or child node from the browser tree.'
+ );
+ });
+ });
+
+ context('there is an ancestor of the type server', () => {
+ context('no preference can be found', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue(undefined);
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [{id: 'some_database'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference pg_bin_dir of module paths'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [{id: 'ppasServer'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference ppas_bin_dir of module paths'
+ );
+ });
+ });
+ });
+
+ context('preference can be found', () => {
+ context('binary folder is not configured', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue({});
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [{id: 'serverTreeNode'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the PostgreSQL Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [{id: 'ppasServer'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the EDB Advanced Server Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+ });
+
+ context('binary folder is configured', () => {
+ let backupDialogResizeToSpy;
+ beforeEach(() => {
+ backupDialogResizeToSpy = jasmine.createSpyObj('backupDialogResizeToSpy', ['resizeTo']);
+ alertifySpy['backup_objects'].and
+ .returnValue(backupDialogResizeToSpy);
+ pgBrowser.get_preference.and.returnValue({value: '/some/path'});
+ });
+
+ it('displays the dialog', () => {
+ backupDialog.draw(null, [{id: 'serverTreeNode'}], null);
+ expect(alertifySpy['backup_objects']).toHaveBeenCalledWith(true);
+ expect(backupDialogResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
+ });
+
+ context('database label contain "="', () => {
+ it('should create alert dialog with backup error', () => {
+ backupDialog.draw(null, [{id: 'database_with_equal_in_name'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith('Backup Error',
+ 'Databases with = symbols in the name cannot be backed up or restored using this utility.');
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/backup/backup_dialog_wrapper_spec.js b/web/regression/javascript/backup/backup_dialog_wrapper_spec.js
new file mode 100644
index 00000000..58705318
--- /dev/null
+++ b/web/regression/javascript/backup/backup_dialog_wrapper_spec.js
@@ -0,0 +1,675 @@
+import {TreeFake} from '../tree/tree_fake';
+import {BackupDialogWrapper} from '../../../pgadmin/tools/backup/static/js/backup_dialog_wrapper';
+import axios from 'axios/index';
+import MockAdapter from 'axios-mock-adapter';
+import {FakeModel} from '../fake_model';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+let context = describe;
+
+describe('BackupDialogWrapper', () => {
+ let jquerySpy;
+ let pgBrowser;
+ let alertifySpy;
+ let dialogModelKlassSpy;
+ let backform;
+ let generatedBackupModel;
+ let backupDialogWrapper;
+ let noDataNode;
+ let serverTreeNode;
+ let databaseTreeNode;
+ let viewSchema;
+ let backupJQueryContainerSpy;
+ let backupNodeChildNodeSpy;
+ let backupNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: {
+ hasId: true,
+ getTreeNodeHierarchy: jasmine.createSpy('getTreeNodeHierarchy'),
+ },
+ database: {
+ hasId: true,
+ },
+ },
+ keyboardNavigation: jasmine.createSpyObj('keyboardNavigation', ['getDialogTabNavigator']),
+ };
+ noDataNode = pgBrowser.treeMenu.addNewNode('level1.1', undefined, [{id: 'level1'}]);
+ serverTreeNode = pgBrowser.treeMenu.addNewNode('level2.1', {
+ _type: 'server',
+ _id: 10,
+ label: 'some-tree-label',
+ }, [{id: 'level2.1'}]);
+ databaseTreeNode = new TreeNode('database-tree-node', {
+ _type: 'database',
+ _label: 'some-database-label',
+ }, [{id: 'database-tree-node'}]);
+ pgBrowser.treeMenu.addChild(serverTreeNode, databaseTreeNode);
+
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ backupNode = {
+ __internal: {
+ buttons: [{}, {}, {
+ element: {
+ disabled: false,
+ },
+ }],
+ },
+ elements: {
+ body: {
+ childNodes: [
+ {},
+ ],
+ },
+ content: jasmine.createSpyObj('content', ['appendChild', 'attr']),
+ },
+ };
+
+ backupJQueryContainerSpy = jasmine.createSpyObj('backupJQueryContainer', ['get', 'attr']);
+ backupJQueryContainerSpy.get.and.returnValue(backupJQueryContainerSpy);
+
+ generatedBackupModel = {};
+ dialogModelKlassSpy = jasmine.createSpy('dialogModelKlass');
+ dialogModelKlassSpy.and.returnValue(generatedBackupModel);
+
+ viewSchema = {};
+ backform = jasmine.createSpyObj('backform', ['generateViewSchema', 'Dialog']);
+ backform.generateViewSchema.and.returnValue(viewSchema);
+
+ backupNodeChildNodeSpy = jasmine.createSpyObj('something', ['addClass']);
+ jquerySpy.and.callFake((selector) => {
+ if (selector === '<div class=\'backup_dialog\'></div>') {
+ return backupJQueryContainerSpy;
+ } else if (selector === backupNode.elements.body.childNodes[0]) {
+ return backupNodeChildNodeSpy;
+ }
+ });
+
+ });
+
+ describe('#prepare', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+ backupDialogWrapper = Object.assign(backupDialogWrapper, backupNode);
+ });
+
+ context('no tree element is selected', () => {
+ it('does not create a backform dialog', () => {
+ backupDialogWrapper.prepare();
+ expect(backform.Dialog).not.toHaveBeenCalled();
+ });
+
+ it('disables the button "submit button" until a filename is selected', () => {
+ backupDialogWrapper.prepare();
+ expect(backupDialogWrapper.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('selected tree node has no data', () => {
+ beforeEach(() => {
+ pgBrowser.treeMenu.selectNode(noDataNode.domNode);
+ });
+
+ it('does not create a backform dialog', () => {
+ backupDialogWrapper.prepare();
+ expect(backform.Dialog).not.toHaveBeenCalled();
+ });
+
+ it('disables the button "submit button" until a filename is selected', () => {
+ backupDialogWrapper.prepare();
+ expect(backupDialogWrapper.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('tree element is selected', () => {
+ let treeHierarchyInformation;
+ let dialogSpy;
+
+ beforeEach(() => {
+ treeHierarchyInformation = {
+ server: {
+ _type: 'server',
+ _id: 10,
+ priority: 0,
+ label: 'some-tree-label',
+ },
+ };
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ pgBrowser.Nodes['server'].getTreeNodeHierarchy.and
+ .returnValue(treeHierarchyInformation);
+ dialogSpy = jasmine.createSpyObj('newView', ['render']);
+ dialogSpy.$el = jasmine.createSpyObj('$el', ['find', 'attr']);
+ dialogSpy.model = jasmine.createSpyObj('model', ['on']);
+ dialogSpy.$el.find.and.returnValue([]);
+
+ backform.Dialog.and.returnValue(dialogSpy);
+ });
+
+ it('creates a backform dialog and displays it', () => {
+ backupDialogWrapper.prepare();
+ expect(backform.Dialog).toHaveBeenCalledWith({
+ el: backupJQueryContainerSpy,
+ model: generatedBackupModel,
+ schema: viewSchema,
+ });
+
+ expect(dialogSpy.render).toHaveBeenCalled();
+ });
+
+
+ it('add alertify classes to restore node childnode', () => {
+ backupDialogWrapper.prepare();
+ expect(backupNodeChildNodeSpy.addClass)
+ .toHaveBeenCalledWith('alertify_tools_dialog_properties obj_properties');
+ });
+
+ it('disables the button submit button until a filename is selected', () => {
+ backupDialogWrapper.prepare();
+ expect(backupNode.__internal.buttons[2].element.disabled).toBe(true);
+ });
+
+ it('generates a new backup model', () => {
+ backupDialogWrapper.prepare();
+ expect(dialogModelKlassSpy).toHaveBeenCalledWith(
+ {type: 'backup'},
+ {node_info: treeHierarchyInformation}
+ );
+ });
+
+ it('add the new dialog to the backup node HTML', () => {
+ backupDialogWrapper.prepare();
+ expect(backupNode.elements.content.appendChild).toHaveBeenCalledWith(backupJQueryContainerSpy);
+ });
+ });
+ });
+
+ describe('onButtonClicked', () => {
+ let networkMock;
+ beforeEach(() => {
+ networkMock = new MockAdapter(axios);
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ backupDialogWrapper = Object.assign(backupDialogWrapper, backupNode);
+ });
+
+ afterEach(() => {
+ networkMock.restore();
+ });
+
+ context('dialog help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.*/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ pgBrowser.showHelp = jasmine.createSpy('showHelp');
+
+ const event = {
+ button: {
+ element: {
+ name: 'dialog_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ },
+ },
+ },
+ };
+ backupDialogWrapper.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'dialog_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ );
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('object help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.*/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ pgBrowser.showHelp = jasmine.createSpy('showHelp');
+
+ const event = {
+ button: {
+ element: {
+ name: 'object_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ },
+ },
+ },
+ };
+ backupDialogWrapper.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'object_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ );
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('backup button was pressed', () => {
+ context('no tree node is selected', () => {
+ it('does not start the backup', () => {
+ let networkCalled = false;
+ networkMock.onAny(/.*/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ let event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+
+ backupDialogWrapper.callback(event);
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('tree node has no data', () => {
+ it('does not start the backup', () => {
+ pgBrowser.treeMenu.selectNode(noDataNode.domNode);
+
+ let networkCalled = false;
+ networkMock.onAny(/.*/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ let event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+
+ backupDialogWrapper.callback(event);
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('tree node has data', () => {
+ context('when dialog type is global', () => {
+ let event;
+ beforeEach(() => {
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+
+ backupDialogWrapper.view = {
+ model: new FakeModel(),
+ };
+
+ event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ });
+
+ context('when the backup job is created successfully', () => {
+ let dataSentToServer;
+ beforeEach(() => {
+ pgBrowser.Events = jasmine.createSpyObj('Events', ['trigger']);
+ alertifySpy.success = jasmine.createSpy('success');
+
+ networkMock.onPost('/backup/job/10').reply((request) => {
+ dataSentToServer = request.data;
+ return [200, {}];
+ });
+ });
+
+ it('creates a success alert box', (done) => {
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.success).toHaveBeenCalledWith(
+ 'Backup job created.',
+ 5
+ );
+ done();
+ }, 0);
+ });
+
+ it('trigger an event to background process', (done) => {
+ backupDialogWrapper.callback(event);
+
+ setTimeout(() => {
+ expect(pgBrowser.Events.trigger).toHaveBeenCalledWith(
+ 'pgadmin-bgprocess:created',
+ backupDialogWrapper
+ );
+ done();
+ }, 0);
+ });
+
+ it('send the correct paramenters to the backend', (done) => {
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(JSON.parse(dataSentToServer)).toEqual(
+ {}
+ );
+ done();
+ }, 0);
+ });
+ });
+
+ context('when creating backup job fails', () => {
+ it('creates an alert box', (done) => {
+ alertifySpy.alert = jasmine.createSpy('alert');
+ networkMock.onPost('/backup/job/10').reply(() => {
+ return [400, {
+ errormsg: 'some-error-message',
+ }];
+ });
+
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup job failed.',
+ 'some-error-message'
+ );
+ done();
+ }, 0);
+
+ });
+ });
+ });
+
+ context('when dialog type is object', () => {
+ let event;
+ beforeEach(() => {
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup_objects',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ pgBrowser.treeMenu.selectNode(databaseTreeNode.domNode);
+
+ backupDialogWrapper.view = {
+ model: new FakeModel(),
+ };
+
+ event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ });
+
+ context('when the backup job is created successfully', () => {
+ let dataSentToServer;
+ beforeEach(() => {
+ pgBrowser.Events = jasmine.createSpyObj('Events', ['trigger']);
+ alertifySpy.success = jasmine.createSpy('success');
+
+ networkMock.onPost('/backup/job/10/object').reply((request) => {
+ dataSentToServer = request.data;
+ return [200, {}];
+ });
+ });
+
+ it('creates a success alert box', (done) => {
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.success).toHaveBeenCalledWith(
+ 'Backup job created.',
+ 5
+ );
+ done();
+ }, 0);
+ });
+
+ it('trigger an event to background process', (done) => {
+ backupDialogWrapper.callback(event);
+
+ setTimeout(() => {
+ expect(pgBrowser.Events.trigger).toHaveBeenCalledWith(
+ 'pgadmin-bgprocess:created',
+ backupDialogWrapper
+ );
+ done();
+ }, 0);
+ });
+
+ it('send the correct parameters to the backend', (done) => {
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(JSON.parse(dataSentToServer)).toEqual(
+ {database: 'some-database-label'}
+ );
+ done();
+ }, 0);
+ });
+ });
+
+ context('when creating backup job fails', () => {
+ it('creates an alert box', (done) => {
+ alertifySpy.alert = jasmine.createSpy('alert');
+ networkMock.onPost('/backup/job/10/object').reply(() => {
+ return [400, {
+ errormsg: 'some-error-message',
+ }];
+ });
+
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup job failed.',
+ 'some-error-message'
+ );
+ done();
+ }, 0);
+ });
+ });
+ });
+ });
+ });
+ });
+
+ describe('#setExtraParameters', () => {
+ let selectedTreeNode;
+ let treeInfo;
+ let model;
+
+ context('when dialog type is global', () => {
+ beforeEach(() => {
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ treeInfo = {};
+ model = new FakeModel();
+ backupDialogWrapper.view = {
+ model: model,
+ };
+ });
+
+
+ it('sets nothing on the view model', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.toJSON()).toEqual({});
+ });
+ });
+
+ context('when dialog type is object', () => {
+ beforeEach(() => {
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup_objects',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ treeInfo = {
+ database: {
+ _label: 'some-database-label',
+ },
+ schema: {
+ _label: 'some-treeinfo-label',
+ },
+ };
+
+ model = new FakeModel();
+ selectedTreeNode = new TreeNode('some-selected-node',
+ {_type: 'some-type', _label: 'some-selected-label'},
+ []);
+ backupDialogWrapper.view = {
+ model: model,
+ };
+ });
+
+ it('sets the database label on the model', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ 'database': 'some-database-label',
+ });
+ });
+
+ context('when the selected is a schema type', () => {
+ beforeEach(() => {
+ selectedTreeNode = new TreeNode('some-selected-node',
+ {_type: 'schema', _label: 'some-schema-label'},
+ []);
+ });
+
+ it('sets the schema label on the model', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ 'database': 'some-database-label',
+ 'schemas': ['some-schema-label'],
+ });
+ });
+ });
+
+ context('when the selected is a table type', () => {
+ beforeEach(() => {
+ selectedTreeNode = new TreeNode('some-selected-node',
+ {_type: 'table', _label: 'some-table-label'},
+ []);
+ });
+
+ it('sets the schema label on the model', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ 'database': 'some-database-label',
+ 'tables': [['some-treeinfo-label', 'some-table-label']],
+ });
+ });
+ });
+
+ context('when the model has no ratio value', () => {
+ beforeEach(() => {
+ model.set('ratio', '');
+ });
+
+ it('sets clears the ratio value', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.get('ratio')).toBeUndefined();
+ });
+ });
+
+ context('when the model has a valid ratio value', () => {
+ beforeEach(() => {
+ model.set('ratio', '0.25');
+ });
+
+ it('sets clears the ratio value', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.get('ratio')).toEqual('0.25');
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/backup/global_server_backup_dialog_spec.js b/web/regression/javascript/backup/global_server_backup_dialog_spec.js
new file mode 100644
index 00000000..86df672e
--- /dev/null
+++ b/web/regression/javascript/backup/global_server_backup_dialog_spec.js
@@ -0,0 +1,168 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {BackupDialog} from '../../../pgadmin/tools/backup/static/js/backup_dialog';
+import {TreeFake} from '../tree/tree_fake';
+
+const context = describe;
+
+describe('GlobalServerBackupDialog', () => {
+ let backupDialog;
+ let pgBrowser;
+ let jquerySpy;
+ let alertifySpy;
+ let backupModelSpy;
+
+
+ let rootNode;
+ let serverTreeNode;
+ let ppasServerTreeNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: jasmine.createSpyObj('Node[server]', ['getTreeNodeHierarchy']),
+ },
+ };
+ pgBrowser.Nodes.server.hasId = true;
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ backupModelSpy = jasmine.createSpy('backupModelSpy');
+
+ rootNode = pgBrowser.treeMenu.addNewNode('level1', {}, undefined, []);
+ serverTreeNode = pgBrowser.treeMenu.addNewNode('level1.1', {
+ _type: 'server',
+ _id: 10,
+ }, undefined, ['level1']);
+ ppasServerTreeNode = pgBrowser.treeMenu.addNewNode('level1.2', {
+ _type: 'server',
+ server_type: 'ppas',
+ }, undefined, ['level1']);
+ pgBrowser.treeMenu.addNewNode('level3', {}, undefined, ['level1', 'level1.2']);
+ pgBrowser.treeMenu.addNewNode('level3.1', undefined, undefined, ['level1', 'level1.2', 'level3']);
+ });
+
+ describe('#draw', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ alertifySpy['BackupDialog_globals'] = jasmine.createSpy('BackupDialog_globals');
+ alertifySpy['BackupDialog_server'] = jasmine.createSpy('BackupDialog_server');
+ backupDialog = new BackupDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ backupModelSpy
+ );
+
+ pgBrowser.get_preference = jasmine.createSpy('get_preferences');
+ });
+
+ context('there are no ancestors of the type server', () => {
+ it('does not create a dialog', () => {
+ pgBrowser.treeMenu.selectNode([{id: 'level1'}]);
+ backupDialog.draw(null, null, null);
+ expect(alertifySpy['BackupDialog_globals']).not.toHaveBeenCalled();
+ expect(alertifySpy['BackupDialog_server']).not.toHaveBeenCalled();
+ });
+
+ it('display an alert with a Backup Error', () => {
+ backupDialog.draw(null, [rootNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Please select server or child node from the browser tree.'
+ );
+ });
+ });
+
+ context('there is an ancestor of the type server', () => {
+ context('no preference can be found', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue(undefined);
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference pg_bin_dir of module paths'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference ppas_bin_dir of module paths'
+ );
+ });
+ });
+ });
+
+ context('preference can be found', () => {
+ context('binary folder is not configured', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue({});
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the PostgreSQL Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the EDB Advanced Server Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+ });
+
+ context('binary folder is configured', () => {
+ let globalResizeToSpy;
+ let serverResizeToSpy;
+ beforeEach(() => {
+ globalResizeToSpy = jasmine.createSpyObj('globals', ['resizeTo']);
+ alertifySpy['BackupDialog_globals'].and
+ .returnValue(globalResizeToSpy);
+ serverResizeToSpy = jasmine.createSpyObj('server', ['resizeTo']);
+ alertifySpy['BackupDialog_server'].and
+ .returnValue(serverResizeToSpy);
+ pgBrowser.get_preference.and.returnValue({value: '/some/path'});
+ });
+
+ context('dialog for global backup', () => {
+ it('displays the dialog', () => {
+ backupDialog.draw(null, [serverTreeNode], {globals: true});
+ expect(alertifySpy['BackupDialog_globals']).toHaveBeenCalledWith(true);
+ expect(globalResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
+ });
+ });
+
+ context('dialog for server backup', () => {
+ it('displays the dialog', () => {
+ backupDialog.draw(null, [serverTreeNode], {server: true});
+ expect(alertifySpy['BackupDialog_server']).toHaveBeenCalledWith(true);
+ expect(serverResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/backup/menu_utils_spec.js b/web/regression/javascript/backup/menu_utils_spec.js
new file mode 100644
index 00000000..9435d699
--- /dev/null
+++ b/web/regression/javascript/backup/menu_utils_spec.js
@@ -0,0 +1,55 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+
+import {menuEnabledServer} from '../../../pgadmin/tools/backup/static/js/menu_utils';
+
+const context = describe;
+
+describe('backup.menuUtils', () => {
+ describe('#menuEnabledServer', () => {
+ context('provided node data is undefined', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer(undefined)).toBe(false);
+ });
+ });
+
+ context('provided node data is null', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer(null)).toBe(false);
+ });
+ });
+
+ context('current node type is not of the type server', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer({_type: 'schema'})).toBe(false);
+ });
+ });
+
+ context('current node type is of the type server', () => {
+ context('is connected', () => {
+ it('returns true', () => {
+ expect(menuEnabledServer({
+ _type: 'server',
+ connected: true,
+ })).toBe(true);
+ });
+ });
+ context('is not connected', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer({
+ _type: 'server',
+ connected: false,
+ })).toBe(false);
+ });
+ });
+ });
+ });
+});
+
diff --git a/web/regression/javascript/common_keyboard_shortcuts_spec.js b/web/regression/javascript/common_keyboard_shortcuts_spec.js
index 9ea31efd..e27929bf 100644
--- a/web/regression/javascript/common_keyboard_shortcuts_spec.js
+++ b/web/regression/javascript/common_keyboard_shortcuts_spec.js
@@ -11,10 +11,6 @@ import keyboardShortcuts from 'sources/keyboard_shortcuts';
describe('the keyboard shortcuts', () => {
const F1_KEY = 112;
- // const EDIT_KEY = 71; // Key: G -> Grid values
- // const LEFT_ARROW_KEY = 37;
- // const RIGHT_ARROW_KEY = 39;
- // const MOVE_NEXT = 'right';
let debuggerElementSpy, event, debuggerUserShortcutSpy;
debuggerUserShortcutSpy = jasmine.createSpyObj(
diff --git a/web/regression/javascript/datagrid/get_panel_title_spec.js b/web/regression/javascript/datagrid/get_panel_title_spec.js
new file mode 100644
index 00000000..8a344a84
--- /dev/null
+++ b/web/regression/javascript/datagrid/get_panel_title_spec.js
@@ -0,0 +1,82 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {getPanelTitle} from '../../../pgadmin/tools/datagrid/static/js/get_panel_title';
+import {TreeFake} from '../tree/tree_fake';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+const context = describe;
+
+describe('#getPanelTitle', () => {
+ let pgBrowser;
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ pgBrowser = {
+ treeMenu: tree,
+ Nodes: {
+ server: {
+ hasId: true,
+ _type: 'server',
+ },
+ database: {
+ hasId: true,
+ _type: 'database',
+ },
+ },
+ };
+ });
+
+ context('selected node does not belong to a server', () => {
+ it('returns undefined', () => {
+ const root = tree.addNewNode('level1', {_type: 'server_groups'});
+ tree.addChild(root, new TreeNode('level1.1', {_type: 'other'}));
+ tree.selectNode([{id: 'level1'}]);
+ expect(getPanelTitle(pgBrowser)).toBeUndefined();
+ });
+ });
+
+ context('selected node belong to a server', () => {
+ context('selected node does not belong to a database', () => {
+ it('returns the server label and the username', () => {
+ tree.addNewNode('level1', {
+ _type: 'server',
+ db: 'other db label',
+ user: {name: 'some user name'},
+ label: 'server label',
+ }, []);
+
+ tree.selectNode([{id: 'level1'}]);
+ expect(getPanelTitle(pgBrowser))
+ .toBe('other db label on some user name@server label');
+ });
+ });
+
+ context('selected node belongs to a database', () => {
+ it('returns the database label and the username', () => {
+ const root = tree.addNewNode('level1', {
+ _type: 'server',
+ db: 'other db label',
+ user: {name: 'some user name'},
+ label: 'server label',
+ });
+ const level1 = new TreeNode('level1.1', {
+ _type: 'database',
+ label: 'db label',
+ });
+ tree.addChild(root, level1);
+ tree.addChild(level1,
+ new TreeNode('level1.1.1', {_type: 'table'}));
+ tree.selectNode([{id: 'level1.1.1'}]);
+ expect(getPanelTitle(pgBrowser))
+ .toBe('db label on some user name@server label');
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/datagrid/show_data_spec.js b/web/regression/javascript/datagrid/show_data_spec.js
new file mode 100644
index 00000000..80d25eb3
--- /dev/null
+++ b/web/regression/javascript/datagrid/show_data_spec.js
@@ -0,0 +1,171 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {showDataGrid} from '../../../pgadmin/tools/datagrid/static/js/show_data';
+import {TreeFake} from '../tree/tree_fake';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+const context = describe;
+
+describe('#show_data', () => {
+ let datagrid;
+ let pgBrowser;
+ let alertify;
+ beforeEach(() => {
+ alertify = jasmine.createSpyObj('alertify', ['alert']);
+ datagrid = {
+ create_transaction: jasmine.createSpy('create_transaction'),
+ };
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server_group: {
+ _type: 'server_group',
+ hasId: true,
+ },
+ server: {
+ _type: 'server',
+ hasId: true,
+ },
+ database: {
+ _type: 'database',
+ hasId: true,
+ },
+ schema: {
+ _type: 'schema',
+ hasId: true,
+ },
+ view: {
+ _type: 'view',
+ hasId: true,
+ },
+ catalog: {
+ _type: 'catalog',
+ hasId: true,
+ },
+ },
+ };
+ const parent = pgBrowser.treeMenu.addNewNode('parent', {_type: 'parent'}, []);
+ const serverGroup1 = new TreeNode('server_group1', {
+ _type: 'server_group',
+ _id: 1,
+ });
+ pgBrowser.treeMenu.addChild(parent, serverGroup1);
+
+ const server1 = new TreeNode('server1', {
+ _type: 'server',
+ label: 'server1',
+ server_type: 'pg',
+ _id: 2,
+ }, ['parent', 'server_group1']);
+ pgBrowser.treeMenu.addChild(serverGroup1, server1);
+
+ const database1 = new TreeNode('database1', {
+ _type: 'database',
+ label: 'database1',
+ _id: 3,
+ }, ['parent', 'server_group1', 'server1']);
+ pgBrowser.treeMenu.addChild(server1, database1);
+
+ const schema1 = new TreeNode('schema1', {
+ _type: 'schema',
+ label: 'schema1',
+ _id: 4,
+ });
+ pgBrowser.treeMenu.addChild(database1, schema1);
+
+ const view1 = new TreeNode('view1', {
+ _type: 'view',
+ label: 'view1',
+ _id: 5,
+ }, ['parent', 'server_group1', 'server1', 'database1']);
+ pgBrowser.treeMenu.addChild(database1, view1);
+
+ const catalog1 = new TreeNode('catalog1', {
+ _type: 'catalog',
+ label: 'catalog1',
+ _id: 6,
+ }, ['parent', 'server_group1', 'server1', 'database1']);
+ pgBrowser.treeMenu.addChild(database1, catalog1);
+ });
+
+ context('cannot find the tree node', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: '10'}]);
+ expect(datagrid.create_transaction).not.toHaveBeenCalled();
+ });
+
+ it('display alert', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: '10'}]);
+ expect(alertify.alert).toHaveBeenCalledWith(
+ 'Data Grid Error',
+ 'No object selected.'
+ );
+ });
+ });
+
+ context('current node is not underneath a server', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: 'parent'}]);
+ expect(datagrid.create_transaction).not.toHaveBeenCalled();
+ });
+ });
+
+ context('current node is not underneath a schema or view or catalog', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: 'database1'}]);
+ expect(datagrid.create_transaction).not.toHaveBeenCalled();
+ });
+ });
+
+ context('current node is underneath a schema', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {mnuid: 11}, [{id: 'schema1'}]);
+ expect(datagrid.create_transaction).toHaveBeenCalledWith(
+ '/initialize/datagrid/11/schema/1/2/3/4',
+ null,
+ 'false',
+ 'pg',
+ '',
+ 'server1 - database1 - schema1.schema1',
+ ''
+ );
+ });
+ });
+
+ context('current node is underneath a view', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {mnuid: 11}, [{id: 'view1'}]);
+ expect(datagrid.create_transaction).toHaveBeenCalledWith(
+ '/initialize/datagrid/11/view/1/2/3/5',
+ null,
+ 'false',
+ 'pg',
+ '',
+ 'server1 - database1 - view1.view1',
+ ''
+ );
+ });
+ });
+
+ context('current node is underneath a catalog', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {mnuid: 11}, [{id: 'catalog1'}]);
+ expect(datagrid.create_transaction).toHaveBeenCalledWith(
+ '/initialize/datagrid/11/catalog/1/2/3/6',
+ null,
+ 'false',
+ 'pg',
+ '',
+ 'server1 - database1 - catalog1.catalog1',
+ ''
+ );
+ });
+ });
+});
diff --git a/web/regression/javascript/datagrid/show_query_tool_spec.js b/web/regression/javascript/datagrid/show_query_tool_spec.js
new file mode 100644
index 00000000..66bd37ce
--- /dev/null
+++ b/web/regression/javascript/datagrid/show_query_tool_spec.js
@@ -0,0 +1,125 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {TreeFake} from '../tree/tree_fake';
+import {showQueryTool} from '../../../pgadmin/tools/datagrid/static/js/show_query_tool';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+const context = describe;
+
+describe('#showQueryTool', () => {
+ let queryTool;
+ let pgBrowser;
+ let alertify;
+ beforeEach(() => {
+ alertify = jasmine.createSpyObj('alertify', ['alert']);
+ queryTool = {
+ create_transaction: jasmine.createSpy('create_transaction'),
+ };
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server_group: {
+ _type: 'server_group',
+ hasId: true,
+ },
+ server: {
+ _type: 'server',
+ hasId: true,
+ },
+ database: {
+ _type: 'database',
+ hasId: true,
+ },
+ },
+ };
+ const parent = pgBrowser.treeMenu.addNewNode('parent', {_type: 'parent'});
+ const serverGroup1 = new TreeNode('server_group1', {
+ _type: 'server_group',
+ _id: 1,
+ }, ['parent']);
+ pgBrowser.treeMenu.addChild(parent, serverGroup1);
+
+ const server1 = new TreeNode('server1', {
+ _type: 'server',
+ label: 'server1',
+ server_type: 'pg',
+ _id: 2,
+ });
+ pgBrowser.treeMenu.addChild(serverGroup1, server1);
+
+ const database1 = new TreeNode('database1', {
+ _type: 'database',
+ label: 'database1',
+ _id: 3,
+ });
+ pgBrowser.treeMenu.addChild(server1, database1);
+ });
+
+ context('cannot find the tree node', () => {
+ beforeEach(() => {
+ showQueryTool(queryTool, pgBrowser, alertify, '', [{id: '10'}], 'title');
+ });
+ it('does not create a transaction', () => {
+ expect(queryTool.create_transaction).not.toHaveBeenCalled();
+ });
+
+ it('display alert', () => {
+ expect(alertify.alert).toHaveBeenCalledWith(
+ 'Query Tool Error',
+ 'No object selected.'
+ );
+ });
+ });
+
+ context('current node is not underneath a server', () => {
+ it('does not create a transaction', () => {
+ showQueryTool(queryTool, pgBrowser, alertify, '', [{id: 'parent'}], 'title');
+ expect(queryTool.create_transaction).not.toHaveBeenCalled();
+ });
+
+ it('no alert is displayed', () => {
+ expect(alertify.alert).not.toHaveBeenCalled();
+ });
+ });
+
+ context('current node is underneath a server', () => {
+ context('current node is not underneath a database', () => {
+ it('creates a transaction', () => {
+ showQueryTool(queryTool, pgBrowser, alertify, 'http://someurl', [{id: 'server1'}], 'title');
+ expect(queryTool.create_transaction).toHaveBeenCalledWith(
+ '/initialize/query_tool/1/2',
+ null,
+ 'true',
+ 'pg',
+ 'http://someurl',
+ 'title',
+ '',
+ false
+ );
+ });
+ });
+
+ context('current node is underneath a database', () => {
+ it('creates a transaction', () => {
+ showQueryTool(queryTool, pgBrowser, alertify, 'http://someurl', [{id: 'database1'}], 'title');
+ expect(queryTool.create_transaction).toHaveBeenCalledWith(
+ '/initialize/query_tool/1/2/3',
+ null,
+ 'true',
+ 'pg',
+ 'http://someurl',
+ 'title',
+ '',
+ false
+ );
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/fake_browser/browser.js b/web/regression/javascript/fake_browser/browser.js
new file mode 100644
index 00000000..195e5c51
--- /dev/null
+++ b/web/regression/javascript/fake_browser/browser.js
@@ -0,0 +1,12 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+let treeMenu = null;
+
+export {treeMenu};
diff --git a/web/regression/javascript/fake_endpoints.js b/web/regression/javascript/fake_endpoints.js
index 54b86a94..c060ba78 100644
--- a/web/regression/javascript/fake_endpoints.js
+++ b/web/regression/javascript/fake_endpoints.js
@@ -12,5 +12,11 @@ define(function () {
'static': '/base/pgadmin/static/<path:filename>',
'sqleditor.poll': '/sqleditor/query_tool/poll/<path:trans_id>',
'sqleditor.query_tool_start': '/sqleditor/query_tool/start/<path:trans_id>',
+ 'backup.create_server_job': '/backup/job/<int:sid>',
+ 'backup.create_object_job': '/backup/job/<int:sid>/object',
+ 'datagrid.initialize_datagrid': '/initialize/datagrid/<int:cmd_type>/<obj_type>/<int:sgid>/<int:sid>/<int:did>/<int:obj_id>',
+ 'datagrid.initialize_query_tool': '/initialize/query_tool/<int:sgid>/<int:sid>',
+ 'datagrid.initialize_query_tool_with_did': '/initialize/query_tool/<int:sgid>/<int:sid>/<int:did>',
+ 'restore.create_job': '/restore/job/<int:sid>',
};
});
diff --git a/web/regression/javascript/fake_model.js b/web/regression/javascript/fake_model.js
new file mode 100644
index 00000000..acfaa532
--- /dev/null
+++ b/web/regression/javascript/fake_model.js
@@ -0,0 +1,21 @@
+export class FakeModel {
+ constructor() {
+ this.values = {};
+ }
+
+ set(key, value) {
+ this.values[key] = value;
+ }
+
+ get(key) {
+ return this.values[key];
+ }
+
+ unset(key) {
+ delete this.values[key];
+ }
+
+ toJSON() {
+ return Object.assign({}, this.values);
+ }
+}
diff --git a/web/regression/javascript/nodes/schema/child_menu_spec.js b/web/regression/javascript/nodes/schema/child_menu_spec.js
new file mode 100644
index 00000000..3d3dc55e
--- /dev/null
+++ b/web/regression/javascript/nodes/schema/child_menu_spec.js
@@ -0,0 +1,253 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+/////////////////////////////////////////////////////////////
+
+import {
+ isTreeItemOfChildOfSchema, childCreateMenuEnabled,
+} from 'pgadmin.schema.dir/schema_child_tree_node';
+
+import * as pgBrowser from 'pgbrowser/browser';
+import {TreeFake} from '../../tree/tree_fake';
+
+describe('#childCreateMenuEnabled', () => {
+ let data;
+ let tree;
+
+ describe(' - when data is not null', () => {
+ beforeEach(() => {
+ data = {};
+ });
+ describe(' and check is false', () => {
+ beforeEach(() => {
+ data = {check: false};
+ });
+ it(', then it returns true', () => {
+ expect(childCreateMenuEnabled({}, {}, data)).toBe(true);
+ });
+ });
+
+ describe(' and check', () => {
+ describe(' is true', () => {
+ beforeEach(() => {
+ data = {check: true};
+ });
+
+ describe(', on schema node', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [{
+ id: 'level2',
+ data: {_type: 'schema'},
+ }],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ pgBrowser.treeMenu = tree;
+ });
+ it(' it is true', () => {
+ expect(childCreateMenuEnabled(
+ {}, [{id: 'level2'}], data
+ )).toBe(true);
+
+ });
+ });
+
+ describe(', on child collection node under schema node ', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [{
+ id: 'level2',
+ data: {_type: 'schema'},
+ children: [{
+ id: 'coll-table',
+ data: {_type: 'coll-table'},
+ }],
+ }],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ pgBrowser.treeMenu = tree;
+ });
+
+ it(' it is true', () => {
+ expect(childCreateMenuEnabled(
+ {}, [{id: 'coll-table'}], data
+ )).toBe(true);
+ });
+ });
+
+ describe(', on one of the child node under schema node ', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [{
+ id: 'level2',
+ data: {_type: 'schema'},
+ children: [{
+ id: 'coll-table',
+ data: {_type: 'coll-table'},
+ children: [{
+ id: 'table/1',
+ data: {_type: 'table'},
+ }],
+ }],
+ }],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ pgBrowser.treeMenu = tree;
+ });
+
+ it(' it is true', () => {
+ expect(childCreateMenuEnabled(
+ {}, [{id: 'table/1'}], data
+ )).toBe(true);
+ });
+ });
+
+ describe(', on catalog node', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [{
+ id: 'level2',
+ data: {_type: 'catalog'},
+ }],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ pgBrowser.treeMenu = tree;
+ });
+ it(' it is false', () => {
+ expect(
+ childCreateMenuEnabled({}, [{id: 'level2'}], data)
+ ).toBe(false);
+ });
+ });
+
+ describe(', on child collection node under catalog node ', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [{
+ id: 'level2',
+ data: {_type: 'catalog'},
+ children: [{
+ id: 'coll-table',
+ data: {_type: 'coll-table'},
+ }],
+ }],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ pgBrowser.treeMenu = tree;
+ });
+
+ it(' it is false', () => {
+ expect(childCreateMenuEnabled(
+ {}, [{id: 'coll-table'}], data
+ )).toBe(false);
+ });
+ });
+
+ describe(', on one of the child node under catalog node ', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [{
+ id: 'level2',
+ data: {_type: 'catalog'},
+ children: [{
+ id: 'coll-table',
+ data: {_type: 'coll-table'},
+ children: [{
+ id: 'table/1',
+ data: {_type: 'table'},
+ }],
+ }],
+ }],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ pgBrowser.treeMenu = tree;
+ });
+
+ it(' it is false', () => {
+ expect(childCreateMenuEnabled(
+ {}, [{id: 'table/1'}], data
+ )).toBe(false);
+ });
+ });
+ });
+ });
+ });
+});
+
+describe('#childDropMenuEnabled', () => {
+ let tree;
+
+ describe(' - the child node under schema node ', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [{
+ id: 'level2',
+ data: {_type: 'schema'},
+ children: [{
+ id: 'coll-table',
+ data: {_type: 'coll-table'},
+ children: [{
+ id: 'table/1',
+ data: {_type: 'table'},
+ }],
+ }],
+ }],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ pgBrowser.treeMenu = tree;
+ });
+
+ it(' it is true', () => {
+ expect(isTreeItemOfChildOfSchema(
+ {}, [{id: 'table/1'}]
+ )).toBe(true);
+ });
+ });
+
+ describe('- the child node under the catalog node ', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [{
+ id: 'level2',
+ data: {_type: 'catalog'},
+ children: [{
+ id: 'coll-table',
+ data: {_type: 'coll-table'},
+ children: [{
+ id: 'table/1',
+ data: {_type: 'table'},
+ }],
+ }],
+ }],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ pgBrowser.treeMenu = tree;
+ });
+
+ it(' it is false', () => {
+ expect(isTreeItemOfChildOfSchema(
+ {}, [{id: 'table/1'}]
+ )).toBe(false);
+ });
+ });
+});
diff --git a/web/regression/javascript/restore/restore_dialog_spec.js b/web/regression/javascript/restore/restore_dialog_spec.js
new file mode 100644
index 00000000..156f56bb
--- /dev/null
+++ b/web/regression/javascript/restore/restore_dialog_spec.js
@@ -0,0 +1,203 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {TreeFake} from '../tree/tree_fake';
+import {RestoreDialog} from '../../../pgadmin/tools/restore/static/js/restore_dialog';
+
+const context = describe;
+
+describe('RestoreDialog', () => {
+ let restoreDialog;
+ let pgBrowser;
+ let jquerySpy;
+ let alertifySpy;
+ let restoreModelSpy;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: jasmine.createSpyObj('Node[server]', ['getTreeNodeHierarchy']),
+ database: jasmine.createSpyObj('Node[database]', ['getTreeNodeHierarchy']),
+ },
+ };
+ pgBrowser.Nodes.server.hasId = true;
+ pgBrowser.Nodes.database.hasId = true;
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ restoreModelSpy = jasmine.createSpy('restoreModelSpy');
+
+ const hierarchy = {
+ children: [
+ {
+ id: 'root',
+ children: [
+ {
+ id: 'serverTreeNode',
+ data: {
+ _id: 10,
+ _type: 'server',
+ label: 'some-tree-label',
+ },
+ children: [
+ {
+ id: 'some_database',
+ data: {
+ _type: 'database',
+ _id: 11,
+ label: 'some_database',
+ _label: 'some_database_label',
+ },
+ }, {
+ id: 'database_with_equal_in_name',
+ data: {
+ _type: 'database',
+ label: 'some_database',
+ _label: '=some_database_label',
+ },
+ },
+ ],
+ },
+ {
+ id: 'ppasServer',
+ data: {
+ _type: 'server',
+ server_type: 'ppas',
+ children: [
+ {id: 'someNodeUnderneathPPASServer'},
+ ],
+ },
+ },
+ ],
+ },
+ ],
+ };
+
+ pgBrowser.treeMenu = TreeFake.build(hierarchy);
+ });
+
+ describe('#draw', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ alertifySpy['pg_restore'] = jasmine.createSpy('pg_restore');
+ restoreDialog = new RestoreDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ restoreModelSpy
+ );
+
+ pgBrowser.get_preference = jasmine.createSpy('get_preferences');
+ });
+
+ context('there are no ancestors of the type server', () => {
+ it('does not create a dialog', () => {
+ pgBrowser.treeMenu.selectNode([{id: 'root'}]);
+ restoreDialog.draw(null, null, null);
+ expect(alertifySpy['pg_restore']).not.toHaveBeenCalled();
+ });
+
+ it('display an alert with a Restore Error', () => {
+ restoreDialog.draw(null, [{id: 'root'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Restore Error',
+ 'Please select server or child node from the browser tree.'
+ );
+ });
+ });
+
+ context('there is an ancestor of the type server', () => {
+ context('no preference can be found', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue(undefined);
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Restore Error"', () => {
+ restoreDialog.draw(null, [{id: 'serverTreeNode'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Restore Error',
+ 'Failed to load preference pg_bin_dir of module paths'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Restore Error"', () => {
+ restoreDialog.draw(null, [{id: 'ppasServer'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Restore Error',
+ 'Failed to load preference ppas_bin_dir of module paths'
+ );
+ });
+ });
+ });
+
+ context('preference can be found', () => {
+ context('binary folder is not configured', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue({});
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ restoreDialog.draw(null, [{id: 'serverTreeNode'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the PostgreSQL Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ restoreDialog.draw(null, [{id: 'ppasServer'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the EDB Advanced Server Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+ });
+
+ context('binary folder is configured', () => {
+ let spy;
+ beforeEach(() => {
+ spy = jasmine.createSpyObj('globals', ['resizeTo']);
+ alertifySpy['pg_restore'].and
+ .returnValue(spy);
+ pgBrowser.get_preference.and.returnValue({value: '/some/path'});
+ pgBrowser.Nodes.server.label = 'some-server-label';
+ });
+
+ it('displays the dialog', () => {
+ restoreDialog.draw(null, [{id: 'serverTreeNode'}], {server: true});
+ expect(alertifySpy['pg_restore']).toHaveBeenCalledWith(
+ 'Restore (some-server-label: some-tree-label)',
+ [{id: 'serverTreeNode'}],
+ {
+ _id: 10,
+ _type: 'server',
+ label: 'some-tree-label',
+ },
+ pgBrowser.Nodes.server
+ );
+ expect(spy.resizeTo).toHaveBeenCalledWith('65%', '60%');
+ });
+
+ context('database label contain "="', () => {
+ it('should create alert dialog with restore error', () => {
+ restoreDialog.draw(null, [{id: 'database_with_equal_in_name'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith('Restore Error',
+ 'Databases with = symbols in the name cannot be backed up or restored using this utility.');
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/restore/restore_dialog_wrapper_spec.js b/web/regression/javascript/restore/restore_dialog_wrapper_spec.js
new file mode 100644
index 00000000..c2a31d55
--- /dev/null
+++ b/web/regression/javascript/restore/restore_dialog_wrapper_spec.js
@@ -0,0 +1,593 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {TreeFake} from '../tree/tree_fake';
+import {RestoreDialogWrapper} from '../../../pgadmin/tools/restore/static/js/restore_dialog_wrapper';
+import MockAdapter from 'axios-mock-adapter';
+import axios from 'axios/index';
+import {FakeModel} from '../fake_model';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+let context = describe;
+
+describe('RestoreDialogWrapper', () => {
+ let jquerySpy;
+ let pgBrowser;
+ let alertifySpy;
+ let dialogModelKlassSpy;
+ let backform;
+ let generatedRestoreModel;
+ let restoreDialogWrapper;
+ let noDataNode;
+ let serverTreeNode;
+ let viewSchema;
+ let restoreJQueryContainerSpy;
+ let restoreNodeChildNodeSpy;
+ let restoreNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: {
+ hasId: true,
+ getTreeNodeHierarchy: jasmine.createSpy('getTreeNodeHierarchy'),
+ },
+ },
+ keyboardNavigation: jasmine.createSpyObj('keyboardNavigation', ['getDialogTabNavigator']),
+ };
+
+ noDataNode = pgBrowser.treeMenu.addNewNode('level1.1', undefined, [{id: 'level1'}]);
+ serverTreeNode = pgBrowser.treeMenu.addNewNode('level2.1', {
+ _type: 'server',
+ _id: 10,
+ label: 'some-tree-label',
+ }, [{id: 'level2.1'}]);
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ dialogModelKlassSpy = jasmine.createSpy('dialogModelKlass');
+ generatedRestoreModel = {};
+ viewSchema = {};
+ backform = jasmine.createSpyObj('backform', ['generateViewSchema', 'Dialog']);
+ backform.generateViewSchema.and.returnValue(viewSchema);
+ dialogModelKlassSpy.and.returnValue(generatedRestoreModel);
+ restoreJQueryContainerSpy = jasmine.createSpyObj('restoreJQueryContainer', ['get', 'attr']);
+ restoreJQueryContainerSpy.get.and.returnValue(restoreJQueryContainerSpy);
+
+ restoreNode = {
+ __internal: {
+ buttons: [
+ {}, {},
+ {
+ element: {
+ disabled: false,
+ },
+ },
+ ],
+ },
+ elements: {
+ body: {
+ childNodes: [
+ {},
+ ],
+ },
+ content: jasmine.createSpyObj('content', ['appendChild', 'attr']),
+ },
+ };
+
+
+ restoreNodeChildNodeSpy = jasmine.createSpyObj('something', ['addClass']);
+
+ jquerySpy.and.callFake((selector) => {
+ if (selector === '<div class=\'restore_dialog\'></div>') {
+ return restoreJQueryContainerSpy;
+ } else if (selector === restoreNode.elements.body.childNodes[0]) {
+ return restoreNodeChildNodeSpy;
+ }
+ });
+ });
+
+ describe('#prepare', () => {
+
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ restoreDialogWrapper = new RestoreDialogWrapper(
+ '<div class=\'restore_dialog\'></div>',
+ 'restoreDialogTitle',
+ 'restore',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+ restoreDialogWrapper = Object.assign(restoreDialogWrapper, restoreNode);
+ });
+ context('no tree element is selected', () => {
+ it('does not create a backform dialog', () => {
+ restoreDialogWrapper.prepare();
+ expect(backform.Dialog).not.toHaveBeenCalled();
+ });
+
+ it('disables the button "submit button" until a filename is selected', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreDialogWrapper.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('selected tree node has no data', () => {
+ beforeEach(() => {
+ pgBrowser.treeMenu.selectNode(noDataNode.domNode);
+ });
+
+ it('does not create a backform dialog', () => {
+ restoreDialogWrapper.prepare();
+ expect(backform.Dialog).not.toHaveBeenCalled();
+ });
+
+ it('disables the button "submit button" until a filename is selected', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreDialogWrapper.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('tree element is selected', () => {
+ let treeHierarchyInformation;
+ let dialogSpy;
+ beforeEach(() => {
+ treeHierarchyInformation = {
+ server: {
+ _type: 'server',
+ _id: 10,
+ priority: 0,
+ label: 'some-tree-label',
+ },
+ };
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ pgBrowser.Nodes['server'].getTreeNodeHierarchy.and
+ .returnValue(treeHierarchyInformation);
+ dialogSpy = jasmine.createSpyObj('newView', ['render']);
+ dialogSpy.$el = jasmine.createSpyObj('$el', ['find', 'attr']);
+ dialogSpy.model = jasmine.createSpyObj('model', ['on']);
+ dialogSpy.$el.find.and.returnValue([]);
+
+ backform.Dialog.and.returnValue(dialogSpy);
+ });
+
+ it('creates a backform dialog and displays it', () => {
+ restoreDialogWrapper.prepare();
+ expect(backform.Dialog).toHaveBeenCalledWith({
+ el: restoreJQueryContainerSpy,
+ model: generatedRestoreModel,
+ schema: viewSchema,
+ });
+
+ expect(dialogSpy.render).toHaveBeenCalled();
+ });
+
+ it('add alertify classes to restore node childnode', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreNodeChildNodeSpy.addClass)
+ .toHaveBeenCalledWith('alertify_tools_dialog_properties obj_properties');
+ });
+
+ it('disables the button submit button until a filename is selected', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreNode.__internal.buttons[2].element.disabled).toBe(true);
+ });
+
+ it('generates a new restore model', () => {
+ restoreDialogWrapper.prepare();
+ expect(dialogModelKlassSpy).toHaveBeenCalledWith(
+ {node_data: pgBrowser.Nodes['server']},
+ {node_info: treeHierarchyInformation}
+ );
+ });
+
+ it('add the new dialog to the restore node HTML', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreNode.elements.content.appendChild).toHaveBeenCalledWith(restoreJQueryContainerSpy);
+ });
+ });
+ });
+
+ describe('onButtonClicked', () => {
+ let networkMock;
+
+ beforeEach(() => {
+ pgBrowser.showHelp = jasmine.createSpy('showHelp');
+ networkMock = new MockAdapter(axios);
+ alertifySpy = jasmine.createSpyObj('alertify', ['success', 'alert']);
+ restoreDialogWrapper = new RestoreDialogWrapper(
+ '<div class=\'restore_dialog\'></div>',
+ 'restoreDialogTitle',
+ 'restore',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+ restoreDialogWrapper = Object.assign(restoreDialogWrapper, restoreNode);
+
+ });
+
+ afterEach(function () {
+ networkMock.restore();
+ });
+
+ context('dialog help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ element: {
+ name: 'dialog_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ },
+ },
+ },
+ };
+ restoreDialogWrapper.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'dialog_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ );
+ });
+
+ it('does not start the restore', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('object help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ element: {
+ name: 'object_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ },
+ },
+ },
+ };
+ restoreDialogWrapper.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'object_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ );
+ });
+
+ it('does not start the restore', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('restore button was pressed', () => {
+ let networkCalled;
+ let event;
+
+ context('no tree node is selected', () => {
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+ event = {
+ button: {
+ 'data-btn-name': 'restore',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ });
+
+ it('does not start the restore', () => {
+ restoreDialogWrapper.callback(event);
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('tree node selected has no data', () => {
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+ event = {
+ button: {
+ 'data-btn-name': 'restore',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ pgBrowser.treeMenu.selectNode(noDataNode.domNode);
+ });
+
+ it('does not start the restore', () => {
+ restoreDialogWrapper.callback(event);
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('tree node select has data', () => {
+
+ let databaseTreeNode;
+
+ beforeEach(() => {
+ databaseTreeNode = pgBrowser.treeMenu.addNewNode('level3.1', {
+ _type: 'database',
+ _id: 10,
+ _label: 'some-database-label',
+ }, [{id: 'level3.1'}]);
+ pgBrowser.treeMenu.addChild(serverTreeNode, databaseTreeNode);
+ pgBrowser.Nodes.database = {
+ hasId: true,
+ _label: 'some-database-label',
+ };
+ let fakeModel = new FakeModel();
+ fakeModel.set('some-key', 'some-value');
+ restoreDialogWrapper.view = {
+ model: fakeModel,
+ };
+ pgBrowser.treeMenu.selectNode(databaseTreeNode.domNode);
+ pgBrowser.Events = jasmine.createSpyObj('pgBrowserEventsSpy', ['trigger']);
+ event = {
+ button: {
+ 'data-btn-name': 'restore',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ });
+ context('restore job created successfully', () => {
+ let dataSentToServer;
+ beforeEach(() => {
+ networkMock.onPost('/restore/job/10').reply((request) => {
+ dataSentToServer = request.data;
+ return [200, {}];
+ });
+ });
+
+ it('create an success alert box', (done) => {
+ restoreDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.success).toHaveBeenCalledWith(
+ 'Restore job created.',
+ 5
+ );
+ done();
+ }, 0);
+ });
+
+ it('trigger background process', (done) => {
+ restoreDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(pgBrowser.Events.trigger).toHaveBeenCalledWith(
+ 'pgadmin-bgprocess:created',
+ restoreDialogWrapper
+ );
+ done();
+ }, 0);
+ });
+
+ it('send correct data to server', (done) => {
+ restoreDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(JSON.parse(dataSentToServer)).toEqual({
+ 'some-key': 'some-value',
+ 'database': 'some-database-label',
+ });
+ done();
+ }, 0);
+ });
+ });
+
+ context('error creating restore job', () => {
+ beforeEach(() => {
+ networkMock.onPost('/restore/job/10').reply(() => {
+ return [400, {}];
+ });
+ });
+
+ it('creates an alert box', (done) => {
+ restoreDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Restore job failed.',
+ undefined
+ );
+ done();
+ }, 0);
+ });
+ });
+ });
+ });
+ });
+
+ describe('setExtraParameters', () => {
+ let selectedNode;
+ let treeInfo;
+ let model;
+
+ beforeEach(() => {
+ restoreDialogWrapper = new RestoreDialogWrapper(
+ '<div class=\'restore_dialog\'></div>',
+ 'restoreDialogTitle',
+ 'restore',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ model = new FakeModel();
+ restoreDialogWrapper.view = {
+ model: model,
+ };
+ });
+
+ context('when it is a custom model', () => {
+ beforeEach(() => {
+ model.set('custom', true);
+ treeInfo = {
+ 'database': {
+ '_label': 'some-database-label',
+ },
+ };
+ });
+
+ it('only sets the database', () => {
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ 'custom': true,
+ 'database': 'some-database-label',
+ });
+ });
+ });
+
+ context('when it is not a custom model', () => {
+ beforeEach(() => {
+ model.set('custom', false);
+ treeInfo = {
+ 'database': {
+ '_label': 'some-database-label',
+ },
+ 'schema': {
+ '_label': 'some-schema-label',
+ },
+ };
+ });
+
+ context('when selected node is a schema', () => {
+ it('sets schemas on the model', () => {
+ selectedNode = new TreeNode('schema', {_type: 'schema', _label: 'some-schema-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ });
+ });
+ });
+
+ context('when selected node is a table', () => {
+ it('sets schemas and table on the model', () => {
+ selectedNode = new TreeNode('table', {_type: 'table', _label: 'some-table-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ tables: ['some-table-label'],
+ });
+ });
+ });
+
+ context('when selected node is a function', () => {
+ it('sets schemas and function on the model', () => {
+ selectedNode = new TreeNode('function', {_type: 'function', _label: 'some-function-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ functions: ['some-function-label'],
+ });
+ });
+ });
+
+ context('when selected node is an index', () => {
+ it('sets schemas and index on the model', () => {
+ selectedNode = new TreeNode('index', {_type: 'index', _label: 'some-index-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ indexes: ['some-index-label'],
+ });
+ });
+ });
+
+ context('when selected node is a trigger', () => {
+ it('sets schemas and trigger on the model', () => {
+ selectedNode = new TreeNode('trigger', {_type: 'trigger', _label: 'some-trigger-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ triggers: ['some-trigger-label'],
+ });
+ });
+ });
+
+ context('when selected node is a trigger_func', () => {
+ it('sets schemas and trigger_func on the model', () => {
+ selectedNode = new TreeNode('trigger_func', {_type: 'trigger_func', _label: 'some-trigger_func-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ trigger_funcs: ['some-trigger_func-label'],
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/sqleditor/filter_dialog_specs.js b/web/regression/javascript/sqleditor/filter_dialog_specs.js
index ed77dff5..cea75e6b 100644
--- a/web/regression/javascript/sqleditor/filter_dialog_specs.js
+++ b/web/regression/javascript/sqleditor/filter_dialog_specs.js
@@ -7,10 +7,8 @@
//
//////////////////////////////////////////////////////////////////////////
import filterDialog from 'sources/sqleditor/filter_dialog';
-// import filterDialogModel from 'sources/sqleditor/filter_dialog_model';
describe('filterDialog', () => {
- jasmine.createSpy('sqlEditorController');
describe('filterDialog', () => {
describe('when using filter dialog', () => {
beforeEach(() => {
diff --git a/web/regression/javascript/table/enable_disable_triggers_spec.js b/web/regression/javascript/table/enable_disable_triggers_spec.js
new file mode 100644
index 00000000..7bdd284e
--- /dev/null
+++ b/web/regression/javascript/table/enable_disable_triggers_spec.js
@@ -0,0 +1,271 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import MockAdapter from 'axios-mock-adapter';
+import axios from 'axios/index';
+import {
+ enableTriggers,
+ disableTriggers,
+} from '../../../pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers';
+import {TreeFake} from '../tree/tree_fake';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+describe('#enableTriggers', () => {
+ let networkMock;
+ let tree;
+ let alertify;
+ let generateUrlSpy;
+ beforeEach(() => {
+ networkMock = new MockAdapter(axios);
+ tree = new TreeFake();
+ const server1 = tree.addNewNode('server1', {_id: 1}, ['<li>server1</li>']);
+ const database1 = tree.addNewNode('database1', {_type: 'database'}, ['<li>database1</li>']);
+ tree.addChild(server1, database1);
+
+ const schema1 = tree.addNewNode('schema1', {_type: 'schema'}, ['<li>schema1</li>']);
+ tree.addChild(database1, schema1);
+
+ const table1 = tree.addNewNode('table1', {_type: 'table'}, ['<li>table1</li>']);
+ tree.addChild(schema1, table1);
+
+ const column1 = tree.addNewNode('column1', {_type: 'column'}, ['<li>column1</li>']);
+ tree.addChild(table1, column1);
+
+ const tableNoData = tree.addNewNode('table-no-data', undefined, ['<li>table-no-data</li>']);
+ tree.addChild(schema1, tableNoData);
+
+ alertify = jasmine.createSpyObj('alertify', ['success', 'error']);
+ generateUrlSpy = jasmine.createSpy('generateUrl');
+ generateUrlSpy.and.returnValue('/some/place');
+ });
+
+ describe('no node is selected', () => {
+ it('does not send the request to the backend', (done) => {
+ networkMock.onAny('.*').reply(200, () => {
+ });
+
+ setTimeout(() => {
+ expect(enableTriggers(tree, alertify, generateUrlSpy, {})).toBe(false);
+ done();
+ }, 0);
+ });
+ });
+
+ describe('a node is selected', () => {
+ describe('node as no data', () => {
+ it('does not send the request to the backend', () => {
+ tree.selectNode([{id: 'table-no-data'}]);
+
+ networkMock.onAny('.*').reply(200, () => {
+ });
+
+ setTimeout(() => {
+ expect(enableTriggers(tree, alertify, generateUrlSpy, {})).toBe(false);
+ }, 0);
+ });
+ });
+
+ describe('node as data', () => {
+ describe('backend responds with success', () => {
+ let networkMockCalledWith;
+ beforeEach(() => {
+ networkMockCalledWith = false;
+ networkMock.onPut(/.*/).reply((configuration) => {
+ networkMockCalledWith = configuration;
+ return [200, {
+ success: 1,
+ info: 'some information',
+ }];
+ });
+ });
+
+ it('displays an alert box with success', (done) => {
+ tree.selectNode([{id: 'table1'}]);
+ enableTriggers(tree, alertify, generateUrlSpy, {});
+ setTimeout(() => {
+ expect(alertify.success).toHaveBeenCalledWith('some information');
+ done();
+ }, 0);
+ });
+
+ it('reloads the node', (done) => {
+ enableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+ setTimeout(() => {
+ expect(tree.selected()).toEqual(['<li>table1</li>']);
+ done();
+ }, 20);
+ });
+
+ it('call backend with the correct parameters', (done) => {
+ enableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+ setTimeout(() => {
+ expect(networkMockCalledWith.data).toEqual(JSON.stringify({enable: 'true'}));
+ done();
+ }, 0);
+ });
+ });
+
+ describe('backend responds with error', () => {
+ beforeEach(() => {
+ networkMock.onPut(/.*/).reply(() => {
+ return [500, {
+ success: 0,
+ errormsg: 'some error message',
+ }];
+ });
+ });
+
+ it('displays an error alert', (done) => {
+ tree.selectNode([{id: 'table1'}]);
+ enableTriggers(tree, alertify, generateUrlSpy, {});
+ setTimeout(() => {
+ expect(alertify.error).toHaveBeenCalledWith('some error message');
+ done();
+ }, 0);
+ });
+
+ it('unload the node', (done) => {
+ enableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+
+ setTimeout(() => {
+ expect(tree.findNodeByDomElement([{id: 'table1'}]).children.length).toBe(0);
+ done();
+ }, 20);
+ });
+ });
+ });
+ });
+});
+
+describe('#disableTriggers', () => {
+ let networkMock;
+ let tree;
+ let alertify;
+ let generateUrlSpy;
+ beforeEach(() => {
+ networkMock = new MockAdapter(axios);
+ tree = new TreeFake();
+ const server1 = tree.addNewNode('server1', {_id: 1}, ['<li>server1</li>']);
+ const database1 = new TreeNode('database1', {_type: 'database'}, ['<li>database1</li>']);
+ tree.addChild(server1, database1);
+
+ const schema1 = new TreeNode('schema1', {_type: 'schema'}, ['<li>schema1</li>']);
+ tree.addChild(database1, schema1);
+
+ const table1 = new TreeNode('table1', {_type: 'table'}, ['<li>table1</li>']);
+ tree.addChild(schema1, table1);
+
+ const column1 = new TreeNode('column1', {_type: 'column'}, ['<li>column1</li>']);
+ tree.addChild(table1, column1);
+
+ const tableNoData = new TreeNode('table-no-data', undefined, ['<li>table-no-data</li>']);
+ tree.addChild(schema1, tableNoData);
+
+ alertify = jasmine.createSpyObj('alertify', ['success', 'error']);
+ generateUrlSpy = jasmine.createSpy('generateUrl');
+ generateUrlSpy.and.returnValue('/some/place');
+ });
+
+ describe('no node is selected', () => {
+ it('does not send the request to the backend', (done) => {
+ networkMock.onAny('.*').reply(200, () => {
+ });
+
+ setTimeout(() => {
+ expect(disableTriggers(tree, alertify, generateUrlSpy, {})).toBe(false);
+ done();
+ }, 0);
+ });
+ });
+
+ describe('a node is selected', () => {
+ describe('node as no data', () => {
+ it('does not send the request to the backend', () => {
+ tree.selectNode([{id: 'table-no-data'}]);
+
+ networkMock.onAny('.*').reply(200, () => {
+ });
+
+ setTimeout(() => {
+ expect(disableTriggers(tree, alertify, generateUrlSpy, {})).toBe(false);
+ }, 0);
+ });
+ });
+
+ describe('node as data', () => {
+ describe('backend responds with success', () => {
+ let networkMockCalledWith;
+ beforeEach(() => {
+ networkMockCalledWith = false;
+ networkMock.onPut(/.*/).reply((configuration) => {
+ networkMockCalledWith = configuration;
+ return [200, {
+ success: 1,
+ info: 'some information',
+ }];
+ });
+ });
+
+ it('displays an alert box with success', (done) => {
+ tree.selectNode([{id: 'table1'}]);
+ disableTriggers(tree, alertify, generateUrlSpy, {});
+ setTimeout(() => {
+ expect(alertify.success).toHaveBeenCalledWith('some information');
+ done();
+ }, 0);
+ });
+
+ it('reloads the node', (done) => {
+ disableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+ setTimeout(() => {
+ expect(tree.selected()).toEqual(['<li>table1</li>']);
+ done();
+ }, 20);
+ });
+
+ it('call backend with the correct parameters', (done) => {
+ disableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+ setTimeout(() => {
+ expect(networkMockCalledWith.data).toEqual(JSON.stringify({enable: 'false'}));
+ done();
+ }, 0);
+ });
+ });
+
+ describe('backend responds with error', () => {
+ beforeEach(() => {
+ networkMock.onPut(/.*/).reply(() => {
+ return [500, {
+ success: 0,
+ errormsg: 'some error message',
+ }];
+ });
+ });
+
+ it('displays an error alert', (done) => {
+ tree.selectNode([{id: 'table1'}]);
+ disableTriggers(tree, alertify, generateUrlSpy, {});
+ setTimeout(() => {
+ expect(alertify.error).toHaveBeenCalledWith('some error message');
+ done();
+ }, 0);
+ });
+
+ it('unload the node', (done) => {
+ disableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+
+ setTimeout(() => {
+ expect(tree.findNodeByDomElement([{id: 'table1'}]).children.length).toBe(0);
+ done();
+ }, 20);
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/tree/pgadmin_tree_node_spec.js b/web/regression/javascript/tree/pgadmin_tree_node_spec.js
new file mode 100644
index 00000000..479e515c
--- /dev/null
+++ b/web/regression/javascript/tree/pgadmin_tree_node_spec.js
@@ -0,0 +1,353 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+import {
+ getTreeNodeHierarchyFromElement,
+ getTreeNodeHierarchyFromIdentifier,
+} from '../../../pgadmin/static/js/tree/pgadmin_tree_node';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+import {TreeFake} from './tree_fake';
+
+const context = describe;
+
+describe('tree#node#getTreeNodeHierarchy', () => {
+ let browser;
+ let newTree;
+ beforeEach(() => {
+ newTree = new TreeFake();
+ browser = {
+ Nodes: {
+ 'special one': {hasId: true},
+ 'child special': {hasId: true},
+ 'other type': {hasId: true},
+ 'table': {hasId: true},
+ 'partition': {hasId: true},
+ 'no id': {hasId: false},
+ },
+ };
+ browser.treeMenu = newTree;
+ });
+
+ context('getTreeNodeHierarchy is called with aciTreeNode object', () => {
+ describe('When the current node is root element', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ });
+ });
+
+ it('returns a object with the element type passed data and priority == 0', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)([{id: 'root'}]);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+
+ describe('When the current node is not of a known type', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'do not exist',
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)('root');
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node type has no id', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'no id',
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)('root');
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node is at the second level', () => {
+ beforeEach(() => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ });
+ const firstChild = new TreeNode('first child', {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ }, ['root']);
+ newTree.addChild(root, firstChild);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)([{id: 'first child'}]);
+ expect(result).toEqual({
+ 'child special': {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ 'priority': 0,
+ },
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+
+ context('When tree as "special type"', () => {
+ context('When "special type" have "other type"', () => {
+ context('When "other type" have "special type"', () => {
+ describe('When retrieving lead node', () => {
+ it('does not override previous node type data', () => {
+ const rootNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+
+ const level1 = new TreeNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'other type',
+ });
+ newTree.addChild(rootNode, level1);
+
+ newTree.addChild(level1, new TreeNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ }));
+
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)([{id: 'level 2'}]);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ 'priority': 0,
+ },
+ 'other type': {
+ 'some key': 'some value',
+ '_type': 'other type',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+ });
+ });
+ });
+
+ context('When tree has table', () => {
+ context('when table has partition', () => {
+ it('returns table with partition parameters', () => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+ const level1 = new TreeNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'table',
+ });
+ newTree.addChild(root, level1);
+ newTree.addChild(level1, new TreeNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'partition',
+ 'some other key': 'some other value',
+ }));
+
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)([{id:'level 2'}]);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ 'table': {
+ 'some key': 'expected value',
+ 'some other key': 'some other value',
+ '_type': 'partition',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+ });
+ });
+
+ context('getTreeNodeHierarchy is called with TreeNode object', () => {
+ let treeNode;
+ describe('When the current node is root element', () => {
+ beforeEach(() => {
+ treeNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+ });
+
+ it('returns a object with the element type passed data and priority == 0', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+
+ describe('When the current node is not of a known type', () => {
+ beforeEach(() => {
+ treeNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'do not exist',
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node type has no id', () => {
+ beforeEach(() => {
+ treeNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'no id',
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node is at the second level', () => {
+ beforeEach(() => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+ treeNode = new TreeNode('first child', {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ });
+ newTree.addChild(root, treeNode);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'child special': {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ 'priority': 0,
+ },
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+
+ context('When tree as "special type"', () => {
+ context('When "special type" have "other type"', () => {
+ context('When "other type" have "special type"', () => {
+ describe('When retrieving lead node', () => {
+ it('does not override previous node type data', () => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+ const level1 = new TreeNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'other type',
+ });
+ newTree.addChild(root, level1);
+ treeNode = new TreeNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ });
+ newTree.addChild(level1, treeNode);
+
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ 'priority': 0,
+ },
+ 'other type': {
+ 'some key': 'some value',
+ '_type': 'other type',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+ });
+ });
+ });
+
+ context('When tree has table', () => {
+ context('when table has partition', () => {
+ it('returns table with partition parameters', () => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ });
+ const level1 = newTree.addNewNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'table',
+ });
+ newTree.addChild(root, level1);
+ treeNode = new TreeNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'partition',
+ 'some other key': 'some other value',
+ });
+ newTree.addChild(level1, treeNode);
+
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ 'table': {
+ 'some key': 'expected value',
+ 'some other key': 'some other value',
+ '_type': 'partition',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/tree/tree_fake.js b/web/regression/javascript/tree/tree_fake.js
index b285a45f..e03d71fb 100644
--- a/web/regression/javascript/tree/tree_fake.js
+++ b/web/regression/javascript/tree/tree_fake.js
@@ -10,6 +10,32 @@
import {Tree} from '../../../pgadmin/static/js/tree/tree';
export class TreeFake extends Tree {
+ static build(structure) {
+ let tree = new TreeFake();
+ let rootNode = tree.rootNode;
+
+ if (structure.children !== undefined) {
+ structure.children.forEach((child) => {
+ TreeFake.recursivelyAddNodes(tree, child, rootNode);
+ });
+ }
+
+ return tree;
+ }
+
+ static recursivelyAddNodes(tree, newNode, parent) {
+ let id = newNode.id;
+ let data = newNode.data ? newNode.data : {};
+ let domNode = newNode.domNode ? newNode.domNode : [{id: id}];
+ tree.addNewNode(id, data, domNode, tree.translateTreeNodeIdFromACITree([parent]));
+
+ if (newNode.children !== undefined) {
+ newNode.children.forEach((child) => {
+ TreeFake.recursivelyAddNodes(tree, child, newNode);
+ });
+ }
+ }
+
constructor() {
super();
this.aciTreeToOurTreeTranslator = {};
@@ -45,7 +71,7 @@ export class TreeFake extends Tree {
}
translateTreeNodeIdFromACITree(aciTreeNode) {
- if(aciTreeNode === undefined || aciTreeNode[0] === undefined) {
+ if (aciTreeNode === undefined || aciTreeNode[0] === undefined) {
return null;
}
return this.aciTreeToOurTreeTranslator[aciTreeNode[0].id];
diff --git a/web/webpack.shim.js b/web/webpack.shim.js
index 64f24336..c12e7f3b 100644
--- a/web/webpack.shim.js
+++ b/web/webpack.shim.js
@@ -130,6 +130,7 @@ var webpackShimConfig = {
'sources/utils': path.join(__dirname, './pgadmin/static/js/utils'),
'babel-polyfill': path.join(__dirname, './node_modules/babel-polyfill/dist/polyfill'),
'tools': path.join(__dirname, './pgadmin/tools/'),
+ 'pgbrowser': path.join(__dirname, './pgadmin/browser/static/js/'),
// Vendor JS
'jquery': path.join(__dirname, './node_modules/jquery/dist/jquery'),
diff --git a/web/webpack.test.config.js b/web/webpack.test.config.js
index 1b374d62..e78e53e0 100644
--- a/web/webpack.test.config.js
+++ b/web/webpack.test.config.js
@@ -80,6 +80,8 @@ module.exports = {
'pgadmin.alertifyjs': sourcesDir + '/js/alertify.pgadmin.defaults',
'pgadmin.backgrid': sourcesDir + '/js/backgrid.pgadmin',
'pgadmin.backform': sourcesDir + '/js/backform.pgadmin',
+ 'pgbrowser': path.resolve(__dirname, 'regression/javascript/fake_browser'),
+ 'pgadmin.schema.dir': path.resolve(__dirname, 'pgadmin/browser/server_groups/servers/databases/schemas/static/js'),
},
},
};
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-24 14:43 Joao De Almeida Pereira <[email protected]>
parent: Ashesh Vashi <[email protected]>
0 siblings, 2 replies; 69+ messages in thread
From: Joao De Almeida Pereira @ 2018-05-24 14:43 UTC (permalink / raw)
To: Ashesh Vashi <[email protected]>; +Cc: Anthony Emengo <[email protected]>; Dave Page <[email protected]>; Akshay Joshi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
Hey, Thanks so much for the reply.
We've noticed that you've made several modifications on top of our original
patch. Unfortunately, we've found it very hard to follow. Could we please
get a brief synopsis of the changes you have made - just so we can better
understand the rationale behind them? Just like we've done for you
previously.
Let's keep in mind that the original intent was simply to introduce this
abstraction into the code base, which is a big enough task. I'd hate for
the scope of the changes we're making to expand beyond that.
Thanks
Joao && Anthony
On Thu, May 24, 2018 at 2:59 AM Ashesh Vashi <[email protected]>
wrote:
> Sorry for the late reply.
> On Wed, May 16, 2018 at 8:55 PM, Anthony Emengo <[email protected]>
> wrote:
>
>> export function canCreate(pgBrowser, childOfCatalogType) {
>> return canCreateObject.bind({
>> browser: pgBrowser,
>> childOfCatalogType: childOfCatalogType,
>> });
>> }
>>
>> With respect to the above code: this bind pattern looks good and seems
>> like the idiomatic way to handle this in JavaScript. On a lighter node, I
>> don’t even see the need for an additional method to wrap it. The invocation
>> could have easily been like canCreate: canCreateObject.bind({ browser:
>> pgBrowser, childOfCatalogType: childOfCatalogType }), I don’t feel too
>> strongly here.
>>
> I do agree - we can handle the same problem many ways.
> I prefer object oriented pardigm more in general.
> Any way - I have modified the code with some other changes.
>
>> I renamed it as isValidTreeNodeData, because - we were using it in for
>> testing the tree data. Please suggest me the right place, and name.
>>
>> We’re not sure; maybe after continued refactoring, we will come across
>> more generic functions. At that point we can revisit this and create a
>> utils.js file.
>>
> Sure.
>
>> The original patch was separating them in different places, but - still
>> uses some of the functionalities directly from the tree, which was
>> happening because we have contextual menu.
>> To give a better solution, I can think of putting the menus related code
>> understand ‘sources/tree/menu’ directory.
>>
>> We’re particularly worried because we’re trying to avoid the coupling
>> that we see in the code base today. We want to decouple *application
>> state* from *business domain* logic as much as we can - because this
>> makes the code much easier to understand. We achieve lower coupling by have
>> more suitable interfaces to retrieve *application state* like: anyParent
>> (the menu doesn’t care how this happens). This is the direction that we’re
>> trying to move towards, we just don’t want the package structure to
>> undermine that developer intent.
>>
> I realized after revisiting the code, menu/can_create.js was only
> applicable to the children of the schema/catalog nodes, same as
> 'can_drop_child'.
> We should have put both scripts in the same directory.
>
> Please find the updated patch for the same.
>
> Please review it, and let me know your concerns.
>
> -- Thanks, Ashesh
>
>> How about nodeMenu.isSupportedNode(…)?
>>
>> Naming is one of the hardest problems in programming. I don’t feel too
>> strongly about this one. For now, let’s keep it as is
>>
>> Thanks
>> Anthony && Victoria
>>
>>
>>
>>
>>
>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-31 14:39 Robert Eckhardt <[email protected]>
parent: Joao De Almeida Pereira <[email protected]>
1 sibling, 1 reply; 69+ messages in thread
From: Robert Eckhardt @ 2018-05-31 14:39 UTC (permalink / raw)
To: Joao De Almeida Pereira <[email protected]>; +Cc: Ashesh Vashi <[email protected]>; Anthony Emengo <[email protected]>; Dave Page <[email protected]>; Akshay Joshi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
All,
These patches were first proposed on April 2 and the meaningful changes
have yet to be committed. ~8 weeks is long enough that my assumption now is
that these aren't going to be committed.
The goal of these patches is to begin to separate out the ACI tree in order
to allow us to do feature work on that chunk of the code. Are there any
alternate suggestions for this work?
Are there any ideas as to how we can meaningfully move forward with the
goal of allowing the tree view to support a very large number of tables?
-- Rob
On Thu, May 24, 2018 at 10:43 AM, Joao De Almeida Pereira <
[email protected]> wrote:
> Hey, Thanks so much for the reply.
>
> We've noticed that you've made several modifications on top of our
> original patch. Unfortunately, we've found it very hard to follow. Could we
> please get a brief synopsis of the changes you have made - just so we can
> better understand the rationale behind them? Just like we've done for you
> previously.
>
> Let's keep in mind that the original intent was simply to introduce this
> abstraction into the code base, which is a big enough task. I'd hate for
> the scope of the changes we're making to expand beyond that.
>
> Thanks
> Joao && Anthony
>
>
> On Thu, May 24, 2018 at 2:59 AM Ashesh Vashi <
> [email protected]> wrote:
>
>> Sorry for the late reply.
>> On Wed, May 16, 2018 at 8:55 PM, Anthony Emengo <[email protected]>
>> wrote:
>>
>>> export function canCreate(pgBrowser, childOfCatalogType) {
>>> return canCreateObject.bind({
>>> browser: pgBrowser,
>>> childOfCatalogType: childOfCatalogType,
>>> });
>>> }
>>>
>>> With respect to the above code: this bind pattern looks good and seems
>>> like the idiomatic way to handle this in JavaScript. On a lighter node, I
>>> don’t even see the need for an additional method to wrap it. The invocation
>>> could have easily been like canCreate: canCreateObject.bind({ browser:
>>> pgBrowser, childOfCatalogType: childOfCatalogType }), I don’t feel too
>>> strongly here.
>>>
>> I do agree - we can handle the same problem many ways.
>> I prefer object oriented pardigm more in general.
>> Any way - I have modified the code with some other changes.
>>
>>> I renamed it as isValidTreeNodeData, because - we were using it in for
>>> testing the tree data. Please suggest me the right place, and name.
>>>
>>> We’re not sure; maybe after continued refactoring, we will come across
>>> more generic functions. At that point we can revisit this and create a
>>> utils.js file.
>>>
>> Sure.
>>
>>> The original patch was separating them in different places, but - still
>>> uses some of the functionalities directly from the tree, which was
>>> happening because we have contextual menu.
>>> To give a better solution, I can think of putting the menus related code
>>> understand ‘sources/tree/menu’ directory.
>>>
>>> We’re particularly worried because we’re trying to avoid the coupling
>>> that we see in the code base today. We want to decouple *application
>>> state* from *business domain* logic as much as we can - because this
>>> makes the code much easier to understand. We achieve lower coupling by have
>>> more suitable interfaces to retrieve *application state* like: anyParent
>>> (the menu doesn’t care how this happens). This is the direction that we’re
>>> trying to move towards, we just don’t want the package structure to
>>> undermine that developer intent.
>>>
>> I realized after revisiting the code, menu/can_create.js was only
>> applicable to the children of the schema/catalog nodes, same as
>> 'can_drop_child'.
>> We should have put both scripts in the same directory.
>>
>> Please find the updated patch for the same.
>>
>> Please review it, and let me know your concerns.
>>
>> -- Thanks, Ashesh
>>
>>> How about nodeMenu.isSupportedNode(…)?
>>>
>>> Naming is one of the hardest problems in programming. I don’t feel too
>>> strongly about this one. For now, let’s keep it as is
>>>
>>> Thanks
>>> Anthony && Victoria
>>>
>>>
>>>
>>>
>>>
>>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-05-31 15:13 Dave Page <[email protected]>
parent: Robert Eckhardt <[email protected]>
0 siblings, 0 replies; 69+ messages in thread
From: Dave Page @ 2018-05-31 15:13 UTC (permalink / raw)
To: Robert Eckhardt <[email protected]>; +Cc: Joao De Almeida Pereira <[email protected]>; Ashesh Vashi <[email protected]>; Anthony Emengo <[email protected]>; Akshay Joshi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
Rob,
My sincere apologies for the delay - I have told the team to prioritise
getting patch 0003 agreed and committed, and to get an understanding of
0004 and to ask any questions etc. they may have.
This *will* get done ASAP.
On Thu, May 31, 2018 at 10:39 AM, Robert Eckhardt <[email protected]>
wrote:
> All,
>
> These patches were first proposed on April 2 and the meaningful changes
> have yet to be committed. ~8 weeks is long enough that my assumption now is
> that these aren't going to be committed.
>
> The goal of these patches is to begin to separate out the ACI tree in
> order to allow us to do feature work on that chunk of the code. Are there
> any alternate suggestions for this work?
>
> Are there any ideas as to how we can meaningfully move forward with the
> goal of allowing the tree view to support a very large number of tables?
>
> -- Rob
>
> On Thu, May 24, 2018 at 10:43 AM, Joao De Almeida Pereira <
> [email protected]> wrote:
>
>> Hey, Thanks so much for the reply.
>>
>> We've noticed that you've made several modifications on top of our
>> original patch. Unfortunately, we've found it very hard to follow. Could we
>> please get a brief synopsis of the changes you have made - just so we can
>> better understand the rationale behind them? Just like we've done for you
>> previously.
>>
>> Let's keep in mind that the original intent was simply to introduce this
>> abstraction into the code base, which is a big enough task. I'd hate for
>> the scope of the changes we're making to expand beyond that.
>>
>> Thanks
>> Joao && Anthony
>>
>>
>> On Thu, May 24, 2018 at 2:59 AM Ashesh Vashi <
>> [email protected]> wrote:
>>
>>> Sorry for the late reply.
>>> On Wed, May 16, 2018 at 8:55 PM, Anthony Emengo <[email protected]>
>>> wrote:
>>>
>>>> export function canCreate(pgBrowser, childOfCatalogType) {
>>>> return canCreateObject.bind({
>>>> browser: pgBrowser,
>>>> childOfCatalogType: childOfCatalogType,
>>>> });
>>>> }
>>>>
>>>> With respect to the above code: this bind pattern looks good and seems
>>>> like the idiomatic way to handle this in JavaScript. On a lighter node, I
>>>> don’t even see the need for an additional method to wrap it. The invocation
>>>> could have easily been like canCreate: canCreateObject.bind({ browser:
>>>> pgBrowser, childOfCatalogType: childOfCatalogType }), I don’t feel too
>>>> strongly here.
>>>>
>>> I do agree - we can handle the same problem many ways.
>>> I prefer object oriented pardigm more in general.
>>> Any way - I have modified the code with some other changes.
>>>
>>>> I renamed it as isValidTreeNodeData, because - we were using it in for
>>>> testing the tree data. Please suggest me the right place, and name.
>>>>
>>>> We’re not sure; maybe after continued refactoring, we will come across
>>>> more generic functions. At that point we can revisit this and create a
>>>> utils.js file.
>>>>
>>> Sure.
>>>
>>>> The original patch was separating them in different places, but - still
>>>> uses some of the functionalities directly from the tree, which was
>>>> happening because we have contextual menu.
>>>> To give a better solution, I can think of putting the menus related
>>>> code understand ‘sources/tree/menu’ directory.
>>>>
>>>> We’re particularly worried because we’re trying to avoid the coupling
>>>> that we see in the code base today. We want to decouple *application
>>>> state* from *business domain* logic as much as we can - because this
>>>> makes the code much easier to understand. We achieve lower coupling by have
>>>> more suitable interfaces to retrieve *application state* like:
>>>> anyParent (the menu doesn’t care how this happens). This is the
>>>> direction that we’re trying to move towards, we just don’t want the package
>>>> structure to undermine that developer intent.
>>>>
>>> I realized after revisiting the code, menu/can_create.js was only
>>> applicable to the children of the schema/catalog nodes, same as
>>> 'can_drop_child'.
>>> We should have put both scripts in the same directory.
>>>
>>> Please find the updated patch for the same.
>>>
>>> Please review it, and let me know your concerns.
>>>
>>> -- Thanks, Ashesh
>>>
>>>> How about nodeMenu.isSupportedNode(…)?
>>>>
>>>> Naming is one of the hardest problems in programming. I don’t feel too
>>>> strongly about this one. For now, let’s keep it as is
>>>>
>>>> Thanks
>>>> Anthony && Victoria
>>>>
>>>>
>>>>
>>>>
>>>>
>>>
>
--
Dave Page
Blog: http://pgsnake.blogspot.com
Twitter: @pgsnake
EnterpriseDB UK: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-06-01 11:52 Ashesh Vashi <[email protected]>
parent: Joao De Almeida Pereira <[email protected]>
1 sibling, 1 reply; 69+ messages in thread
From: Ashesh Vashi @ 2018-06-01 11:52 UTC (permalink / raw)
To: Joao De Almeida Pereira <[email protected]>; +Cc: Anthony Emengo <[email protected]>; Dave Page <[email protected]>; Akshay Joshi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
On Thu, May 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
[email protected]> wrote:
> Hey, Thanks so much for the reply.
>
> We've noticed that you've made several modifications on top of our
> original patch. Unfortunately, we've found it very hard to follow. Could we
> please get a brief synopsis of the changes you have made - just so we can
> better understand the rationale behind them? Just like we've done for you
> previously.
>
Please find the changes from your original patch:
M webpack.shim.js
M webpack.test.config.js
- In order to specify the fake_browser in regression tests, we need to
use 'pgbrowser/browser' in the 'schema_child_tree_node.js' script.D
pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child.js
- We don't need this with the new implementation.C
pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js
- All the children of schema node have common properties as
'parent_type', 'canDrop', 'canDropCascase', 'canCreate'.
Hence - instead of defining them in each node, we have created a
base node, which will have all these properties.
And, modified all schema children node to inherit from it.C
pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js
- In this script, we're defining three functions
'childCreateMenuEnabled', 'isTreeItemOfChildOfSchema', &
'isTreeNodeOfSchemaChild', which are used by the 'SchemaChildNode'
objects.M pgadmin/browser/static/js/collection.js
- Fixed an issue related to wrongly defined 'error' function for the
Collection object.D pgadmin/static/js/menu/can_create.js
- It defined the function, which was defining a check for creation of
a schema child node, or not by looking at the parent node (i.e. a
schema/catalog node).
The file was not defintely placed under the wrong directory, because
- the similar logic was under 'can_drop_child.js', and it was defined
under 'pgadmin/browser/server_groups/servers/databases/schemas/static/js'
directory.D pgadmin/static/js/menu/menu_enabled.jsC
pgadmin/static/js/nodes/supported_database_node.js
- Used by the external tools for checking whether the 'selected' tree-node is:
+ 'database' node, and it is allowed to connect it.
+ Or, it is one of the schema child (and, not 'catalog' child).
- Finding the correct location was difficult for this, as there is no
defined pattern, also it can be used by other functions too. Hence -
moved it out of 'pgadmin/static/js/menu' directory.M
pgadmin/static/js/tree/tree.js
- Introduced a function, which returns the ancestor node object, fow
which the condition is true.D
regression/javascript/menu/can_create_spec.js
D regression/javascript/menu/menu_enabled_spec.js
D regression/javascript/schema/can_drop_child_spec.jsC
regression/javascript/fake_browser/browser.js
C regression/javascript/nodes/schema/child_menu_spec.js
- Modified the regression to test the new functionalies.M
pgadmin/browser/server_groups/servers/databases/schemas/**/*.js
- Extending the schema child nodes from the 'SchemaChildNode' class
defined in 'pgadmin/.../schemas/static/js/child.js' script.
Let me know if you need more information.
> Let's keep in mind that the original intent was simply to introduce this
> abstraction into the code base, which is a big enough task. I'd hate for
> the scope of the changes we're making to expand beyond that.
>
I have the mutual feeling.
-- Thanks, Ashesh
>
> Thanks
> Joao && Anthony
>
>
> On Thu, May 24, 2018 at 2:59 AM Ashesh Vashi <
> [email protected]> wrote:
>
>> Sorry for the late reply.
>> On Wed, May 16, 2018 at 8:55 PM, Anthony Emengo <[email protected]>
>> wrote:
>>
>>> export function canCreate(pgBrowser, childOfCatalogType) {
>>> return canCreateObject.bind({
>>> browser: pgBrowser,
>>> childOfCatalogType: childOfCatalogType,
>>> });
>>> }
>>>
>>> With respect to the above code: this bind pattern looks good and seems
>>> like the idiomatic way to handle this in JavaScript. On a lighter node, I
>>> don’t even see the need for an additional method to wrap it. The invocation
>>> could have easily been like canCreate: canCreateObject.bind({ browser:
>>> pgBrowser, childOfCatalogType: childOfCatalogType }), I don’t feel too
>>> strongly here.
>>>
>> I do agree - we can handle the same problem many ways.
>> I prefer object oriented pardigm more in general.
>> Any way - I have modified the code with some other changes.
>>
>>> I renamed it as isValidTreeNodeData, because - we were using it in for
>>> testing the tree data. Please suggest me the right place, and name.
>>>
>>> We’re not sure; maybe after continued refactoring, we will come across
>>> more generic functions. At that point we can revisit this and create a
>>> utils.js file.
>>>
>> Sure.
>>
>>> The original patch was separating them in different places, but - still
>>> uses some of the functionalities directly from the tree, which was
>>> happening because we have contextual menu.
>>> To give a better solution, I can think of putting the menus related code
>>> understand ‘sources/tree/menu’ directory.
>>>
>>> We’re particularly worried because we’re trying to avoid the coupling
>>> that we see in the code base today. We want to decouple *application
>>> state* from *business domain* logic as much as we can - because this
>>> makes the code much easier to understand. We achieve lower coupling by have
>>> more suitable interfaces to retrieve *application state* like: anyParent
>>> (the menu doesn’t care how this happens). This is the direction that we’re
>>> trying to move towards, we just don’t want the package structure to
>>> undermine that developer intent.
>>>
>> I realized after revisiting the code, menu/can_create.js was only
>> applicable to the children of the schema/catalog nodes, same as
>> 'can_drop_child'.
>> We should have put both scripts in the same directory.
>>
>> Please find the updated patch for the same.
>>
>> Please review it, and let me know your concerns.
>>
>> -- Thanks, Ashesh
>>
>>> How about nodeMenu.isSupportedNode(…)?
>>>
>>> Naming is one of the hardest problems in programming. I don’t feel too
>>> strongly about this one. For now, let’s keep it as is
>>>
>>> Thanks
>>> Anthony && Victoria
>>>
>>>
>>>
>>>
>>>
>>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-06-01 15:17 Anthony Emengo <[email protected]>
parent: Ashesh Vashi <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Anthony Emengo @ 2018-06-01 15:17 UTC (permalink / raw)
To: Ashesh Vashi <[email protected]>; +Cc: Joao De Almeida Pereira <[email protected]>; Dave Page <[email protected]>; Akshay Joshi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
Hey Ashesh,
Thanks for the explanation. It was great and it really helped!
C pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js
C pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js
It makes sense to remove duplication by extracting these attributes out and
setting the canDrop and canCreate functions here. But is it possible to
combine these two files into one since they are related so we don’t need to
import schema_child_tree_node?
M pgadmin/static/js/tree/tree.js
The creation of the ancestorNode function feels like a pre-optimization.
That function is not used any where outside of the tree.js file, so it’s
more confusing to have it defined. On a lighter note, could we avoid the !!
syntax when possible? For example, instead of return !!obj, we could do
something like return obj === undefined or return _.isUndefined(obj) as
this is more intuitive.
https://softwareengineering.stackexchange.com/a/80092
In addition, please update this patch as it is out of sync with the latest
commit on the master branch. Otherwise, everything looks good!
Thanks
Anthony && Victoria
On Fri, Jun 1, 2018 at 7:52 AM Ashesh Vashi <[email protected]>
wrote:
> On Thu, May 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
> [email protected]> wrote:
>
>> Hey, Thanks so much for the reply.
>>
>> We've noticed that you've made several modifications on top of our
>> original patch. Unfortunately, we've found it very hard to follow. Could we
>> please get a brief synopsis of the changes you have made - just so we can
>> better understand the rationale behind them? Just like we've done for you
>> previously.
>>
> Please find the changes from your original patch:
>
> M webpack.shim.js
> M webpack.test.config.js
> - In order to specify the fake_browser in regression tests, we need to use 'pgbrowser/browser' in the 'schema_child_tree_node.js' script.D pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child.js
> - We don't need this with the new implementation.C pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js
> - All the children of schema node have common properties as 'parent_type', 'canDrop', 'canDropCascase', 'canCreate'.
> Hence - instead of defining them in each node, we have created a base node, which will have all these properties.
> And, modified all schema children node to inherit from it.C pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js
> - In this script, we're defining three functions 'childCreateMenuEnabled', 'isTreeItemOfChildOfSchema', & 'isTreeNodeOfSchemaChild', which are used by the 'SchemaChildNode' objects.M pgadmin/browser/static/js/collection.js
> - Fixed an issue related to wrongly defined 'error' function for the Collection object.D pgadmin/static/js/menu/can_create.js
> - It defined the function, which was defining a check for creation of a schema child node, or not by looking at the parent node (i.e. a schema/catalog node).
> The file was not defintely placed under the wrong directory, because - the similar logic was under 'can_drop_child.js', and it was defined under 'pgadmin/browser/server_groups/servers/databases/schemas/static/js' directory.D pgadmin/static/js/menu/menu_enabled.jsC pgadmin/static/js/nodes/supported_database_node.js
> - Used by the external tools for checking whether the 'selected' tree-node is:
> + 'database' node, and it is allowed to connect it.
> + Or, it is one of the schema child (and, not 'catalog' child).
> - Finding the correct location was difficult for this, as there is no defined pattern, also it can be used by other functions too. Hence - moved it out of 'pgadmin/static/js/menu' directory.M pgadmin/static/js/tree/tree.js
> - Introduced a function, which returns the ancestor node object, fow which the condition is true.D regression/javascript/menu/can_create_spec.js
> D regression/javascript/menu/menu_enabled_spec.js
> D regression/javascript/schema/can_drop_child_spec.jsC regression/javascript/fake_browser/browser.js
> C regression/javascript/nodes/schema/child_menu_spec.js
> - Modified the regression to test the new functionalies.M pgadmin/browser/server_groups/servers/databases/schemas/**/*.js
> - Extending the schema child nodes from the 'SchemaChildNode' class defined in 'pgadmin/.../schemas/static/js/child.js' script.
>
> Let me know if you need more information.
>
>
>> Let's keep in mind that the original intent was simply to introduce this
>> abstraction into the code base, which is a big enough task. I'd hate for
>> the scope of the changes we're making to expand beyond that.
>>
>
> I have the mutual feeling.
>
> -- Thanks, Ashesh
>
>>
>> Thanks
>> Joao && Anthony
>>
>>
>> On Thu, May 24, 2018 at 2:59 AM Ashesh Vashi <
>> [email protected]> wrote:
>>
>>> Sorry for the late reply.
>>> On Wed, May 16, 2018 at 8:55 PM, Anthony Emengo <[email protected]>
>>> wrote:
>>>
>>>> export function canCreate(pgBrowser, childOfCatalogType) {
>>>> return canCreateObject.bind({
>>>> browser: pgBrowser,
>>>> childOfCatalogType: childOfCatalogType,
>>>> });
>>>> }
>>>>
>>>> With respect to the above code: this bind pattern looks good and seems
>>>> like the idiomatic way to handle this in JavaScript. On a lighter node, I
>>>> don’t even see the need for an additional method to wrap it. The invocation
>>>> could have easily been like canCreate: canCreateObject.bind({ browser:
>>>> pgBrowser, childOfCatalogType: childOfCatalogType }), I don’t feel too
>>>> strongly here.
>>>>
>>> I do agree - we can handle the same problem many ways.
>>> I prefer object oriented pardigm more in general.
>>> Any way - I have modified the code with some other changes.
>>>
>>>> I renamed it as isValidTreeNodeData, because - we were using it in for
>>>> testing the tree data. Please suggest me the right place, and name.
>>>>
>>>> We’re not sure; maybe after continued refactoring, we will come across
>>>> more generic functions. At that point we can revisit this and create a
>>>> utils.js file.
>>>>
>>> Sure.
>>>
>>>> The original patch was separating them in different places, but - still
>>>> uses some of the functionalities directly from the tree, which was
>>>> happening because we have contextual menu.
>>>> To give a better solution, I can think of putting the menus related
>>>> code understand ‘sources/tree/menu’ directory.
>>>>
>>>> We’re particularly worried because we’re trying to avoid the coupling
>>>> that we see in the code base today. We want to decouple *application
>>>> state* from *business domain* logic as much as we can - because this
>>>> makes the code much easier to understand. We achieve lower coupling by have
>>>> more suitable interfaces to retrieve *application state* like:
>>>> anyParent (the menu doesn’t care how this happens). This is the
>>>> direction that we’re trying to move towards, we just don’t want the package
>>>> structure to undermine that developer intent.
>>>>
>>> I realized after revisiting the code, menu/can_create.js was only
>>> applicable to the children of the schema/catalog nodes, same as
>>> 'can_drop_child'.
>>> We should have put both scripts in the same directory.
>>>
>>> Please find the updated patch for the same.
>>>
>>> Please review it, and let me know your concerns.
>>>
>>> -- Thanks, Ashesh
>>>
>>>> How about nodeMenu.isSupportedNode(…)?
>>>>
>>>> Naming is one of the hardest problems in programming. I don’t feel too
>>>> strongly about this one. For now, let’s keep it as is
>>>>
>>>> Thanks
>>>> Anthony && Victoria
>>>>
>>>>
>>>>
>>>>
>>>>
>>>
>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-06-01 16:39 Victoria Henry <[email protected]>
parent: Anthony Emengo <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Victoria Henry @ 2018-06-01 16:39 UTC (permalink / raw)
To: Anthony Emengo <[email protected]>; +Cc: Ashesh Vashi <[email protected]>; Joao De Almeida Pereira <[email protected]>; Dave Page <[email protected]>; Akshay Joshi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
Hi Ashesh,
We just attempted to apply your patch over master but it did not work. We
don't want to introduce any bugs or break any functionality. Please update
the patch to make sure it is synced up with the master branch.
Sincerely,
Victoria
On Fri, Jun 1, 2018 at 11:18 AM Anthony Emengo <[email protected]> wrote:
> Hey Ashesh,
>
> Thanks for the explanation. It was great and it really helped!
>
> C pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js
> C pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js
>
> It makes sense to remove duplication by extracting these attributes out
> and setting the canDrop and canCreate functions here. But is it possible
> to combine these two files into one since they are related so we don’t need
> to import schema_child_tree_node?
>
> M pgadmin/static/js/tree/tree.js
>
> The creation of the ancestorNode function feels like a pre-optimization.
> That function is not used any where outside of the tree.js file, so it’s
> more confusing to have it defined. On a lighter note, could we avoid the !!
> syntax when possible? For example, instead of return !!obj, we could do
> something like return obj === undefined or return _.isUndefined(obj) as
> this is more intuitive.
>
> https://softwareengineering.stackexchange.com/a/80092
>
> In addition, please update this patch as it is out of sync with the latest
> commit on the master branch. Otherwise, everything looks good!
>
>
> Thanks
> Anthony && Victoria
>
> On Fri, Jun 1, 2018 at 7:52 AM Ashesh Vashi <[email protected]>
> wrote:
>
>> On Thu, May 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
>> [email protected]> wrote:
>>
>>> Hey, Thanks so much for the reply.
>>>
>>> We've noticed that you've made several modifications on top of our
>>> original patch. Unfortunately, we've found it very hard to follow. Could we
>>> please get a brief synopsis of the changes you have made - just so we can
>>> better understand the rationale behind them? Just like we've done for you
>>> previously.
>>>
>> Please find the changes from your original patch:
>>
>> M webpack.shim.js
>> M webpack.test.config.js
>> - In order to specify the fake_browser in regression tests, we need to use 'pgbrowser/browser' in the 'schema_child_tree_node.js' script.D pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child.js
>> - We don't need this with the new implementation.C pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js
>> - All the children of schema node have common properties as 'parent_type', 'canDrop', 'canDropCascase', 'canCreate'.
>> Hence - instead of defining them in each node, we have created a base node, which will have all these properties.
>> And, modified all schema children node to inherit from it.C pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js
>> - In this script, we're defining three functions 'childCreateMenuEnabled', 'isTreeItemOfChildOfSchema', & 'isTreeNodeOfSchemaChild', which are used by the 'SchemaChildNode' objects.M pgadmin/browser/static/js/collection.js
>> - Fixed an issue related to wrongly defined 'error' function for the Collection object.D pgadmin/static/js/menu/can_create.js
>> - It defined the function, which was defining a check for creation of a schema child node, or not by looking at the parent node (i.e. a schema/catalog node).
>> The file was not defintely placed under the wrong directory, because - the similar logic was under 'can_drop_child.js', and it was defined under 'pgadmin/browser/server_groups/servers/databases/schemas/static/js' directory.D pgadmin/static/js/menu/menu_enabled.jsC pgadmin/static/js/nodes/supported_database_node.js
>> - Used by the external tools for checking whether the 'selected' tree-node is:
>> + 'database' node, and it is allowed to connect it.
>> + Or, it is one of the schema child (and, not 'catalog' child).
>> - Finding the correct location was difficult for this, as there is no defined pattern, also it can be used by other functions too. Hence - moved it out of 'pgadmin/static/js/menu' directory.M pgadmin/static/js/tree/tree.js
>> - Introduced a function, which returns the ancestor node object, fow which the condition is true.D regression/javascript/menu/can_create_spec.js
>> D regression/javascript/menu/menu_enabled_spec.js
>> D regression/javascript/schema/can_drop_child_spec.jsC regression/javascript/fake_browser/browser.js
>> C regression/javascript/nodes/schema/child_menu_spec.js
>> - Modified the regression to test the new functionalies.M pgadmin/browser/server_groups/servers/databases/schemas/**/*.js
>> - Extending the schema child nodes from the 'SchemaChildNode' class defined in 'pgadmin/.../schemas/static/js/child.js' script.
>>
>> Let me know if you need more information.
>>
>>
>>> Let's keep in mind that the original intent was simply to introduce this
>>> abstraction into the code base, which is a big enough task. I'd hate for
>>> the scope of the changes we're making to expand beyond that.
>>>
>>
>> I have the mutual feeling.
>>
>> -- Thanks, Ashesh
>>
>>>
>>> Thanks
>>> Joao && Anthony
>>>
>>>
>>> On Thu, May 24, 2018 at 2:59 AM Ashesh Vashi <
>>> [email protected]> wrote:
>>>
>>>> Sorry for the late reply.
>>>> On Wed, May 16, 2018 at 8:55 PM, Anthony Emengo <[email protected]>
>>>> wrote:
>>>>
>>>>> export function canCreate(pgBrowser, childOfCatalogType) {
>>>>> return canCreateObject.bind({
>>>>> browser: pgBrowser,
>>>>> childOfCatalogType: childOfCatalogType,
>>>>> });
>>>>> }
>>>>>
>>>>> With respect to the above code: this bind pattern looks good and seems
>>>>> like the idiomatic way to handle this in JavaScript. On a lighter node, I
>>>>> don’t even see the need for an additional method to wrap it. The invocation
>>>>> could have easily been like canCreate: canCreateObject.bind({
>>>>> browser: pgBrowser, childOfCatalogType: childOfCatalogType }), I
>>>>> don’t feel too strongly here.
>>>>>
>>>> I do agree - we can handle the same problem many ways.
>>>> I prefer object oriented pardigm more in general.
>>>> Any way - I have modified the code with some other changes.
>>>>
>>>>> I renamed it as isValidTreeNodeData, because - we were using it in for
>>>>> testing the tree data. Please suggest me the right place, and name.
>>>>>
>>>>> We’re not sure; maybe after continued refactoring, we will come across
>>>>> more generic functions. At that point we can revisit this and create a
>>>>> utils.js file.
>>>>>
>>>> Sure.
>>>>
>>>>> The original patch was separating them in different places, but -
>>>>> still uses some of the functionalities directly from the tree, which was
>>>>> happening because we have contextual menu.
>>>>> To give a better solution, I can think of putting the menus related
>>>>> code understand ‘sources/tree/menu’ directory.
>>>>>
>>>>> We’re particularly worried because we’re trying to avoid the coupling
>>>>> that we see in the code base today. We want to decouple *application
>>>>> state* from *business domain* logic as much as we can - because this
>>>>> makes the code much easier to understand. We achieve lower coupling by have
>>>>> more suitable interfaces to retrieve *application state* like:
>>>>> anyParent (the menu doesn’t care how this happens). This is the
>>>>> direction that we’re trying to move towards, we just don’t want the package
>>>>> structure to undermine that developer intent.
>>>>>
>>>> I realized after revisiting the code, menu/can_create.js was only
>>>> applicable to the children of the schema/catalog nodes, same as
>>>> 'can_drop_child'.
>>>> We should have put both scripts in the same directory.
>>>>
>>>> Please find the updated patch for the same.
>>>>
>>>> Please review it, and let me know your concerns.
>>>>
>>>> -- Thanks, Ashesh
>>>>
>>>>> How about nodeMenu.isSupportedNode(…)?
>>>>>
>>>>> Naming is one of the hardest problems in programming. I don’t feel too
>>>>> strongly about this one. For now, let’s keep it as is
>>>>>
>>>>> Thanks
>>>>> Anthony && Victoria
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>>>>
>>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-06-01 18:36 Ashesh Vashi <[email protected]>
parent: Victoria Henry <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Ashesh Vashi @ 2018-06-01 18:36 UTC (permalink / raw)
To: Victoria Henry <[email protected]>; +Cc: Anthony Emengo <[email protected]>; Joao De Almeida Pereira <[email protected]>; Dave Page <[email protected]>; Akshay Joshi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
On Fri, Jun 1, 2018 at 10:09 PM, Victoria Henry <[email protected]> wrote:
> Hi Ashesh,
>
> We just attempted to apply your patch over master but it did not work. We
> don't want to introduce any bugs or break any functionality. Please update
> the patch to make sure it is synced up with the master branch.
>
Please find the updated patch.
>
> Sincerely,
>
> Victoria
>
> On Fri, Jun 1, 2018 at 11:18 AM Anthony Emengo <[email protected]> wrote:
>
>> Hey Ashesh,
>>
>> Thanks for the explanation. It was great and it really helped!
>>
>> C pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js
>> C pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js
>>
>> It makes sense to remove duplication by extracting these attributes out
>> and setting the canDrop and canCreate functions here. But is it possible
>> to combine these two files into one since they are related so we don’t need
>> to import schema_child_tree_node?
>>
> That was the original plan, but 'pgadmin/browser/static/js//node.js'
script has too many dependecies, which are not easily portable.
And - that may lead to change the scope of the patch.
Hence - I decided to use the separate file to make sure we have enough test
coverage (which is more imprortant than changing the scope).
> M pgadmin/static/js/tree/tree.js
>>
>> The creation of the ancestorNode function feels like a pre-optimization.
>> That function is not used any where outside of the tree.js file, so it’s
>> more confusing to have it defined.
>>
> It is being used in the latest changes. :-)
> On a lighter note, could we avoid the !! syntax when possible? For
>> example, instead of return !!obj, we could do something like return obj
>> === undefined or return _.isUndefined(obj) as this is more intuitive.
>>
>> https://softwareengineering.stackexchange.com/a/80092
>>
> I am kind of disagree here. But - I have changed it anyway.
> In addition, please update this patch as it is out of sync with the latest
>> commit on the master branch. Otherwise, everything looks good!
>>
> Here - you go!
-- Thanks, Ashesh
>
>>
>> Thanks
>> Anthony && Victoria
>>
>> On Fri, Jun 1, 2018 at 7:52 AM Ashesh Vashi <
>> [email protected]> wrote:
>>
>>> On Thu, May 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
>>> [email protected]> wrote:
>>>
>>>> Hey, Thanks so much for the reply.
>>>>
>>>> We've noticed that you've made several modifications on top of our
>>>> original patch. Unfortunately, we've found it very hard to follow. Could we
>>>> please get a brief synopsis of the changes you have made - just so we can
>>>> better understand the rationale behind them? Just like we've done for you
>>>> previously.
>>>>
>>> Please find the changes from your original patch:
>>>
>>> M webpack.shim.js
>>> M webpack.test.config.js
>>> - In order to specify the fake_browser in regression tests, we need to use 'pgbrowser/browser' in the 'schema_child_tree_node.js' script.D pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child.js
>>> - We don't need this with the new implementation.C pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js
>>> - All the children of schema node have common properties as 'parent_type', 'canDrop', 'canDropCascase', 'canCreate'.
>>> Hence - instead of defining them in each node, we have created a base node, which will have all these properties.
>>> And, modified all schema children node to inherit from it.C pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js
>>> - In this script, we're defining three functions 'childCreateMenuEnabled', 'isTreeItemOfChildOfSchema', & 'isTreeNodeOfSchemaChild', which are used by the 'SchemaChildNode' objects.M pgadmin/browser/static/js/collection.js
>>> - Fixed an issue related to wrongly defined 'error' function for the Collection object.D pgadmin/static/js/menu/can_create.js
>>> - It defined the function, which was defining a check for creation of a schema child node, or not by looking at the parent node (i.e. a schema/catalog node).
>>> The file was not defintely placed under the wrong directory, because - the similar logic was under 'can_drop_child.js', and it was defined under 'pgadmin/browser/server_groups/servers/databases/schemas/static/js' directory.D pgadmin/static/js/menu/menu_enabled.jsC pgadmin/static/js/nodes/supported_database_node.js
>>> - Used by the external tools for checking whether the 'selected' tree-node is:
>>> + 'database' node, and it is allowed to connect it.
>>> + Or, it is one of the schema child (and, not 'catalog' child).
>>> - Finding the correct location was difficult for this, as there is no defined pattern, also it can be used by other functions too. Hence - moved it out of 'pgadmin/static/js/menu' directory.M pgadmin/static/js/tree/tree.js
>>> - Introduced a function, which returns the ancestor node object, fow which the condition is true.D regression/javascript/menu/can_create_spec.js
>>> D regression/javascript/menu/menu_enabled_spec.js
>>> D regression/javascript/schema/can_drop_child_spec.jsC regression/javascript/fake_browser/browser.js
>>> C regression/javascript/nodes/schema/child_menu_spec.js
>>> - Modified the regression to test the new functionalies.M pgadmin/browser/server_groups/servers/databases/schemas/**/*.js
>>> - Extending the schema child nodes from the 'SchemaChildNode' class defined in 'pgadmin/.../schemas/static/js/child.js' script.
>>>
>>> Let me know if you need more information.
>>>
>>>
>>>> Let's keep in mind that the original intent was simply to introduce
>>>> this abstraction into the code base, which is a big enough task. I'd hate
>>>> for the scope of the changes we're making to expand beyond that.
>>>>
>>>
>>> I have the mutual feeling.
>>>
>>> -- Thanks, Ashesh
>>>
>>>>
>>>> Thanks
>>>> Joao && Anthony
>>>>
>>>>
>>>> On Thu, May 24, 2018 at 2:59 AM Ashesh Vashi <
>>>> [email protected]> wrote:
>>>>
>>>>> Sorry for the late reply.
>>>>> On Wed, May 16, 2018 at 8:55 PM, Anthony Emengo <[email protected]>
>>>>> wrote:
>>>>>
>>>>>> export function canCreate(pgBrowser, childOfCatalogType) {
>>>>>> return canCreateObject.bind({
>>>>>> browser: pgBrowser,
>>>>>> childOfCatalogType: childOfCatalogType,
>>>>>> });
>>>>>> }
>>>>>>
>>>>>> With respect to the above code: this bind pattern looks good and
>>>>>> seems like the idiomatic way to handle this in JavaScript. On a lighter
>>>>>> node, I don’t even see the need for an additional method to wrap it. The
>>>>>> invocation could have easily been like canCreate:
>>>>>> canCreateObject.bind({ browser: pgBrowser, childOfCatalogType:
>>>>>> childOfCatalogType }), I don’t feel too strongly here.
>>>>>>
>>>>> I do agree - we can handle the same problem many ways.
>>>>> I prefer object oriented pardigm more in general.
>>>>> Any way - I have modified the code with some other changes.
>>>>>
>>>>>> I renamed it as isValidTreeNodeData, because - we were using it in
>>>>>> for testing the tree data. Please suggest me the right place, and name.
>>>>>>
>>>>>> We’re not sure; maybe after continued refactoring, we will come
>>>>>> across more generic functions. At that point we can revisit this and create
>>>>>> a utils.js file.
>>>>>>
>>>>> Sure.
>>>>>
>>>>>> The original patch was separating them in different places, but -
>>>>>> still uses some of the functionalities directly from the tree, which was
>>>>>> happening because we have contextual menu.
>>>>>> To give a better solution, I can think of putting the menus related
>>>>>> code understand ‘sources/tree/menu’ directory.
>>>>>>
>>>>>> We’re particularly worried because we’re trying to avoid the coupling
>>>>>> that we see in the code base today. We want to decouple *application
>>>>>> state* from *business domain* logic as much as we can - because this
>>>>>> makes the code much easier to understand. We achieve lower coupling by have
>>>>>> more suitable interfaces to retrieve *application state* like:
>>>>>> anyParent (the menu doesn’t care how this happens). This is the
>>>>>> direction that we’re trying to move towards, we just don’t want the package
>>>>>> structure to undermine that developer intent.
>>>>>>
>>>>> I realized after revisiting the code, menu/can_create.js was only
>>>>> applicable to the children of the schema/catalog nodes, same as
>>>>> 'can_drop_child'.
>>>>> We should have put both scripts in the same directory.
>>>>>
>>>>> Please find the updated patch for the same.
>>>>>
>>>>> Please review it, and let me know your concerns.
>>>>>
>>>>> -- Thanks, Ashesh
>>>>>
>>>>>> How about nodeMenu.isSupportedNode(…)?
>>>>>>
>>>>>> Naming is one of the hardest problems in programming. I don’t feel
>>>>>> too strongly about this one. For now, let’s keep it as is
>>>>>>
>>>>>> Thanks
>>>>>> Anthony && Victoria
>>>>>>
>>>>>>
>>>>>>
>>>>>>
>>>>>>
>>>>>
>>>
Attachments:
[application/octet-stream] 0001-Extract-test-and-refactor-methods_v2.patch (292.1K, 3-0001-Extract-test-and-refactor-methods_v2.patch)
download | inline diff:
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.js
index 9015d8d2..7fd28a7c 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.js
@@ -1,8 +1,8 @@
define('pgadmin.node.collation', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
- 'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser) {
+ 'pgadmin.node.schema.dir/child', 'pgadmin.browser.collection',
+], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, schemaChild) {
if (!pgBrowser.Nodes['coll-collation']) {
pgAdmin.Browser.Nodes['coll-collation'] =
@@ -15,7 +15,7 @@ define('pgadmin.node.collation', [
}
if (!pgBrowser.Nodes['collation']) {
- pgAdmin.Browser.Nodes['collation'] = pgBrowser.Node.extend({
+ pgAdmin.Browser.Nodes['collation'] = schemaChild.SchemaChildNode.extend({
type: 'collation',
sqlAlterHelp: 'sql-altercollation.html',
sqlCreateHelp: 'sql-createcollation.html',
@@ -222,34 +222,6 @@ define('pgadmin.node.collation', [
return true;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create collation
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-collation' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.js
index 403ca471..a91daa5f 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.js
@@ -2,9 +2,10 @@
define('pgadmin.node.domain', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid',
- 'pgadmin.browser.collection',
+ 'pgadmin.node.schema.dir/child', 'pgadmin.browser.collection',
], function(
- gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid
+ gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid,
+ schemaChild
) {
// Define Domain Collection Node
@@ -79,7 +80,7 @@ define('pgadmin.node.domain', [
// Domain Node
if (!pgBrowser.Nodes['domain']) {
- pgBrowser.Nodes['domain'] = pgBrowser.Node.extend({
+ pgBrowser.Nodes['domain'] = schemaChild.SchemaChildNode.extend({
type: 'domain',
sqlAlterHelp: 'sql-alterdomain.html',
sqlCreateHelp: 'sql-createdomain.html',
@@ -88,7 +89,6 @@ define('pgadmin.node.domain', [
collection_type: 'coll-domain',
hasSQL: true,
hasDepends: true,
- parent_type: ['schema', 'catalog'],
Init: function() {
// Avoid mulitple registration of menus
if (this.initialized)
@@ -118,8 +118,6 @@ define('pgadmin.node.domain', [
]);
},
- canDrop: pgBrowser.Nodes['schema'].canChildDrop,
- canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
// Domain Node Model
model: pgBrowser.Node.Model.extend({
initialize: function(attrs, args) {
@@ -296,34 +294,6 @@ define('pgadmin.node.domain', [
return errmsg;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create domain
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-domain' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
isDisabled: function(m){
if (!m.isNew()) {
var server = this.node_info.server;
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table.js
index 160db83f..24f5e1a7 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/foreign_tables/static/js/foreign_table.js
@@ -2,9 +2,10 @@
define('pgadmin.node.foreign_table', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid',
- 'pgadmin.browser.collection',
+ 'pgadmin.node.schema.dir/child', 'pgadmin.browser.collection',
], function(
- gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid
+ gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid,
+ schemaChild
) {
if (!pgBrowser.Nodes['coll-foreign_table']) {
@@ -469,7 +470,7 @@ define('pgadmin.node.foreign_table', [
if (!pgBrowser.Nodes['foreign_table']) {
- pgBrowser.Nodes['foreign_table'] = pgBrowser.Node.extend({
+ pgBrowser.Nodes['foreign_table'] = schemaChild.SchemaChildNode.extend({
type: 'foreign_table',
sqlAlterHelp: 'sql-alterforeigntable.html',
sqlCreateHelp: 'sql-createforeigntable.html',
@@ -509,8 +510,6 @@ define('pgadmin.node.foreign_table', [
]);
},
- canDrop: pgBrowser.Nodes['schema'].canChildDrop,
- canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
model: pgBrowser.Node.Model.extend({
initialize: function(attrs, args) {
var isNew = (_.size(attrs) === 0);
@@ -659,34 +658,6 @@ define('pgadmin.node.foreign_table', [
return errmsg;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create foreign table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-foreign_table' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.js
index 89806681..cba62789 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.js
@@ -1,9 +1,10 @@
define('pgadmin.node.fts_configuration', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid',
- 'pgadmin.browser.collection',
+ 'pgadmin.node.schema.dir/child', 'pgadmin.browser.collection',
], function(
- gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid
+ gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, Backgrid,
+ schemaChild
) {
// Model for tokens control
@@ -410,14 +411,11 @@ define('pgadmin.node.fts_configuration', [
// Extend the node class for FTS Configuration
if (!pgBrowser.Nodes['fts_configuration']) {
- pgAdmin.Browser.Nodes['fts_configuration'] = pgAdmin.Browser.Node.extend({
- parent_type: ['schema', 'catalog'],
+ pgAdmin.Browser.Nodes['fts_configuration'] = schemaChild.SchemaChildNode.extend({
type: 'fts_configuration',
sqlAlterHelp: 'sql-altertsconfig.html',
sqlCreateHelp: 'sql-createtsconfig.html',
dialogHelp: url_for('help.static', {'filename': 'fts_configuration_dialog.html'}),
- canDrop: true,
- canDropCascade: true,
label: gettext('FTS Configuration'),
hasSQL: true,
hasDepends: true,
@@ -577,34 +575,6 @@ define('pgadmin.node.fts_configuration', [
return null;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create fts configuration
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-fts_configuration' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary.js
index ed83feb1..cf733922 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_dictionaries/static/js/fts_dictionary.js
@@ -1,8 +1,10 @@
define('pgadmin.node.fts_dictionary', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
- 'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform) {
+ 'pgadmin.node.schema.dir/child', 'pgadmin.browser.collection',
+], function(
+ gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform, schemaChild
+) {
// Extend the browser's node model class to create a option/value pair
var OptionLabelModel = pgAdmin.Browser.Node.Model.extend({
@@ -55,14 +57,11 @@ define('pgadmin.node.fts_dictionary', [
// Extend the node class for FTS Dictionary
if (!pgBrowser.Nodes['fts_dictionary']) {
- pgAdmin.Browser.Nodes['fts_dictionary'] = pgAdmin.Browser.Node.extend({
- parent_type: ['schema', 'catalog'],
+ pgAdmin.Browser.Nodes['fts_dictionary'] = schemaChild.SchemaChildNode.extend({
type: 'fts_dictionary',
sqlAlterHelp: 'sql-altertsdictionary.html',
sqlCreateHelp: 'sql-createtsdictionary.html',
dialogHelp: url_for('help.static', {'filename': 'fts_dictionary_dialog.html'}),
- canDrop: true,
- canDropCascade: true,
label: gettext('FTS Dictionary'),
hasSQL: true,
hasDepends: true,
@@ -186,34 +185,6 @@ define('pgadmin.node.fts_dictionary', [
return null;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create fts dictionary
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-fts_dictionary' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser.js
index 92c0786e..bebc9dc5 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_parser/static/js/fts_parser.js
@@ -1,7 +1,8 @@
define('pgadmin.node.fts_parser', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
- 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser) {
+ 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.node.schema.dir/child',
+ 'pgadmin.browser.collection',
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, schemaChild) {
// Extend the collection class for fts parser
if (!pgBrowser.Nodes['coll-fts_parser']) {
@@ -16,14 +17,11 @@ define('pgadmin.node.fts_parser', [
// Extend the node class for fts parser
if (!pgBrowser.Nodes['fts_parser']) {
- pgAdmin.Browser.Nodes['fts_parser'] = pgAdmin.Browser.Node.extend({
- parent_type: ['schema', 'catalog'],
+ pgAdmin.Browser.Nodes['fts_parser'] = schemaChild.SchemaChildNode.extend({
type: 'fts_parser',
sqlAlterHelp: 'sql-altertsparser.html',
sqlCreateHelp: 'sql-createtsparser.html',
dialogHelp: url_for('help.static', {'filename': 'fts_parser_dialog.html'}),
- canDrop: true,
- canDropCascade: true,
label: gettext('FTS Parser'),
hasSQL: true,
hasDepends: true,
@@ -199,34 +197,6 @@ define('pgadmin.node.fts_parser', [
return null;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create fts parser
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-fts_parser' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template.js
index 606a57a6..cd0207ab 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_templates/static/js/fts_template.js
@@ -1,7 +1,8 @@
define('pgadmin.node.fts_template', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
- 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser) {
+ 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.node.schema.dir/child',
+ 'pgadmin.browser.collection',
+], function(gettext, url_for, $, _, pgAdmin, pgBrowser, schemaChild) {
// Extend the collection class for fts template
if (!pgBrowser.Nodes['coll-fts_template']) {
@@ -16,14 +17,11 @@ define('pgadmin.node.fts_template', [
// Extend the node class for fts template
if (!pgBrowser.Nodes['fts_template']) {
- pgAdmin.Browser.Nodes['fts_template'] = pgAdmin.Browser.Node.extend({
- parent_type: ['schema', 'catalog'],
+ pgAdmin.Browser.Nodes['fts_template'] = schemaChild.SchemaChildNode.extend({
type: 'fts_template',
sqlAlterHelp: 'sql-altertstemplate.html',
sqlCreateHelp: 'sql-createtstemplate.html',
dialogHelp: url_for('help.static', {'filename': 'fts_template_dialog.html'}),
- canDrop: true,
- canDropCascade: true,
label: gettext('FTS Template'),
hasSQL: true,
hasDepends: true,
@@ -139,34 +137,6 @@ define('pgadmin.node.fts_template', [
return null;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create fts fts_template
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-fts_template' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function.js
index 6e405165..c4cd91aa 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/function.js
@@ -2,8 +2,11 @@
define('pgadmin.node.function', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
- 'pgadmin.browser.collection', 'pgadmin.browser.server.privilege',
-], function(gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform) {
+ 'pgadmin.node.schema.dir/child', 'pgadmin.browser.collection',
+ 'pgadmin.browser.server.privilege',
+], function(
+ gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Backform, schemaChild
+) {
if (!pgBrowser.Nodes['coll-function']) {
pgBrowser.Nodes['coll-function'] =
@@ -83,7 +86,8 @@ define('pgadmin.node.function', [
});
if (!pgBrowser.Nodes['function']) {
- pgBrowser.Nodes['function'] = pgBrowser.Node.extend({
+
+ pgBrowser.Nodes['function'] = schemaChild.SchemaChildNode.extend({
type: 'function',
sqlAlterHelp: 'sql-alterfunction.html',
sqlCreateHelp: 'sql-createfunction.html',
@@ -96,7 +100,6 @@ define('pgadmin.node.function', [
return treeInformation.server.server_type !== 'gpdb';
},
hasScriptTypes: ['create', 'select'],
- parent_type: ['schema', 'catalog'],
Init: function() {
/* Avoid mulitple registration of menus */
if (this.initialized)
@@ -126,8 +129,6 @@ define('pgadmin.node.function', [
]);
},
- canDrop: pgBrowser.Nodes['schema'].canChildDrop,
- canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
model: pgBrowser.Node.Model.extend({
initialize: function(attrs, args) {
var isNew = (_.size(attrs) === 0);
@@ -438,34 +439,6 @@ define('pgadmin.node.function', [
return true;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create Function
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-function' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js
index aeb8271b..fcdf28fb 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js
@@ -2,8 +2,11 @@
define('pgadmin.node.trigger_function', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
- 'pgadmin.browser.collection', 'pgadmin.browser.server.privilege',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform) {
+ 'pgadmin.node.schema.dir/child', 'pgadmin.browser.collection',
+ 'pgadmin.browser.server.privilege',
+], function(
+ gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, schemaChild
+) {
if (!pgBrowser.Nodes['coll-trigger_function']) {
pgBrowser.Nodes['coll-trigger_function'] =
@@ -17,7 +20,7 @@ define('pgadmin.node.trigger_function', [
}
if (!pgBrowser.Nodes['trigger_function']) {
- pgBrowser.Nodes['trigger_function'] = pgBrowser.Node.extend({
+ pgBrowser.Nodes['trigger_function'] = schemaChild.SchemaChildNode.extend({
type: 'trigger_function',
sqlAlterHelp: 'plpgsql-trigger.html',
sqlCreateHelp: 'plpgsql-trigger.html',
@@ -27,7 +30,6 @@ define('pgadmin.node.trigger_function', [
hasSQL: true,
hasDepends: true,
hasStatistics: true,
- parent_type: ['schema', 'catalog'],
Init: function() {
/* Avoid mulitple registration of menus */
if (this.initialized)
@@ -57,8 +59,6 @@ define('pgadmin.node.trigger_function', [
]);
},
- canDrop: pgBrowser.Nodes['schema'].canChildDrop,
- canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
model: pgBrowser.Node.Model.extend({
initialize: function(attrs, args) {
var isNew = (_.size(attrs) === 0);
@@ -357,34 +357,6 @@ define('pgadmin.node.trigger_function', [
return !(this.node_info && 'catalog' in this.node_info);
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create Function
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-trigger_function' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence.js
index 57c95acd..300f0b10 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence.js
@@ -1,8 +1,10 @@
define('pgadmin.node.sequence', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
- 'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform) {
+ 'pgadmin.node.schema.dir/child', 'pgadmin.browser.collection',
+], function(
+ gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform, schemaChild
+) {
// Extend the browser's collection class for sequence collection
if (!pgBrowser.Nodes['coll-sequence']) {
@@ -18,7 +20,7 @@ define('pgadmin.node.sequence', [
// Extend the browser's node class for sequence node
if (!pgBrowser.Nodes['sequence']) {
- pgBrowser.Nodes['sequence'] = pgBrowser.Node.extend({
+ pgBrowser.Nodes['sequence'] = schemaChild.SchemaChildNode.extend({
type: 'sequence',
sqlAlterHelp: 'sql-altersequence.html',
sqlCreateHelp: 'sql-createsequence.html',
@@ -28,7 +30,6 @@ define('pgadmin.node.sequence', [
hasSQL: true,
hasDepends: true,
hasStatistics: true,
- parent_type: ['schema', 'catalog'],
Init: function() {
/* Avoid mulitple registration of menus */
if (this.initialized)
@@ -58,36 +59,6 @@ define('pgadmin.node.sequence', [
]);
},
- canDrop: pgBrowser.Nodes['schema'].canChildDrop,
- canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create collation
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-sequence' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we want to allow create menu
- return true;
- },
// Define the model for sequence node.
model: pgBrowser.Node.Model.extend({
defaults: {
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js
new file mode 100644
index 00000000..f8e5951c
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js
@@ -0,0 +1,22 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+/////////////////////////////////////////////////////////////
+
+import * as Node from 'pgbrowser/node';
+import {
+ isTreeItemOfChildOfSchema, childCreateMenuEnabled,
+} from './schema_child_tree_node';
+
+let SchemaChildNode = Node.extend({
+ parent_type: ['schema', 'catalog'],
+ canDrop: isTreeItemOfChildOfSchema,
+ canDropCascade: isTreeItemOfChildOfSchema,
+ canCreate: childCreateMenuEnabled,
+}, false);
+
+export {SchemaChildNode};
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
index a7fd4c7c..3b9b0f35 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema.js
@@ -425,54 +425,10 @@ define('pgadmin.node.schema', [
return null;
},
}),
- // This function will checks whether we can allow user to
- // drop object or not based on location within schema & catalog
- canChildDrop: function(itemData, item) {
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create collation
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if(prev_d && prev_d._type == 'catalog') {
- return false;
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
});
pgBrowser.tableChildTreeNodeHierarchy = function(i) {
- var idx = 0,
- res = {},
- t = pgBrowser.tree;
-
- do {
- var d = t.itemData(i);
- if (
- d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId
- ) {
- if (d._type === 'partition' || d._type === 'table') {
- if (!('table' in res)) {
- res['table'] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- } else {
- res[d._type] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- } while (i);
-
- return res;
+ return this.getTreeNodeHierarchy(i);
};
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js
new file mode 100644
index 00000000..1f67d1af
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js
@@ -0,0 +1,41 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+/////////////////////////////////////////////////////////////
+
+import * as pgBrowser from 'pgbrowser/browser';
+
+export function childCreateMenuEnabled(itemData, item, data) {
+ // If check is false then , we will allow create menu
+ if (data && data.check === false) {
+ return true;
+ }
+
+ let node = pgBrowser.treeMenu.findNodeByDomElement(item);
+
+ if (node)
+ return node.anyFamilyMember(
+ (node) => (node.getData()._type === 'schema')
+ );
+
+ return false;
+}
+
+export function isTreeItemOfChildOfSchema(itemData, item) {
+ let node = pgBrowser.treeMenu.findNodeByDomElement(item);
+
+ if (node)
+ return isTreeNodeOfSchemaChild(node);
+
+ return false;
+}
+
+export function isTreeNodeOfSchemaChild(node) {
+ return node.anyParent(
+ (parentNode) => (parentNode.getData()._type === 'schema')
+ );
+}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js
index 3eac530d..582167a4 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/column/static/js/column.js
@@ -88,7 +88,6 @@ define('pgadmin.node.column', [
if (!pgBrowser.Nodes['column']) {
pgBrowser.Nodes['column'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table', 'view', 'mview'],
collection_type: ['coll-table', 'coll-view', 'coll-mview'],
type: 'column',
@@ -97,27 +96,24 @@ define('pgadmin.node.column', [
sqlAlterHelp: 'sql-altertable.html',
sqlCreateHelp: 'sql-altertable.html',
dialogHelp: url_for('help.static', {'filename': 'column_dialog.html'}),
- canDrop: function(itemData, item, data){
- if (pgBrowser.Nodes['schema'].canChildDrop.apply(this, [itemData, item, data])) {
- var t = pgBrowser.tree, i = item, d = itemData, parents = [];
- // To iterate over tree to check parent node
- while (i) {
- parents.push(d._type);
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
+ canDrop: function(itemData, item){
+ let node = pgBrowser.treeMenu.findNodeByDomElement(item);
- // Check if menu is allowed ?
- if(_.indexOf(parents, 'catalog') > -1 ||
- _.indexOf(parents, 'view') > -1 ||
- _.indexOf(parents, 'mview') > -1) {
- return false;
- } else if(_.indexOf(parents, 'table') > -1) {
- return true;
- }
- } else {
+ if (!node)
return false;
- }
+
+ // Only a column of a table can be droped, and only when it is not of
+ // catalog.
+ return node.anyParent(
+ (parentNode) => (
+ parentNode.getData()._type === 'table' &&
+ !parentNode.anyParent(
+ (grandParentNode) => (
+ grandParentNode.getData()._type === 'catalog'
+ )
+ )
+ )
+ );
},
hasDepends: true,
hasStatistics: true,
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js
index 857cf4c4..ab28a86b 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js
@@ -8,7 +8,6 @@ define('pgadmin.node.check_constraint', [
// Check Constraint Node
if (!pgBrowser.Nodes['check_constraint']) {
pgAdmin.Browser.Nodes['check_constraint'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'check_constraint',
label: gettext('Check'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js
index 0bbf66a1..adccf2e9 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js
@@ -605,7 +605,6 @@ define('pgadmin.node.exclusion_constraint', [
// Extend the browser's node class for exclusion constraint node
if (!pgBrowser.Nodes['exclusion_constraint']) {
pgAdmin.Browser.Nodes['exclusion_constraint'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'exclusion_constraint',
label: gettext('Exclusion constraint'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js
index 3c4b89f3..9899df92 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js
@@ -603,7 +603,6 @@ define('pgadmin.node.foreign_key', [
// Extend the browser's node class for foreign key node
if (!pgBrowser.Nodes['foreign_key']) {
pgAdmin.Browser.Nodes['foreign_key'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'foreign_key',
label: gettext('Foreign key'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js
index d3a6cff4..0ad0f054 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js
@@ -20,7 +20,6 @@ define('pgadmin.node.primary_key', [
parent_type: ['table','partition'],
canDrop: true,
canDropCascade: true,
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
Init: function() {
/* Avoid multiple registration of menus */
if (this.initialized)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js
index 769185d6..18d3ca33 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js
@@ -20,7 +20,6 @@ define('pgadmin.node.unique_constraint', [
parent_type: ['table','partition'],
canDrop: true,
canDropCascade: true,
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
Init: function() {
/* Avoid multiple registration of menus */
if (this.initialized)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js
index cb242cd2..9c0e24fe 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/static/js/constraints.js
@@ -12,14 +12,12 @@ define('pgadmin.node.constraints', [
node: 'constraints',
label: gettext('Constraints'),
type: 'coll-constraints',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
columns: ['name', 'comment'],
});
}
if (!pgBrowser.Nodes['constraints']) {
pgAdmin.Browser.Nodes['constraints'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'constraints',
label: gettext('Constraints'),
collection_type: 'coll-constraints',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js
index ec2b4da1..e58bb463 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.js
@@ -1,10 +1,12 @@
define('pgadmin.node.index', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'backbone', 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.alertifyjs',
- 'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser.collection',
+ 'pgadmin.backform', 'pgadmin.backgrid',
+ 'pgadmin.node.schema.dir/schema_child_tree_node',
+ 'pgadmin.browser.collection',
], function(
gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Alertify, Backform,
- Backgrid
+ Backgrid, SchemaChildTreeNode
) {
if (!pgBrowser.Nodes['coll-index']) {
@@ -13,7 +15,6 @@ define('pgadmin.node.index', [
node: 'index',
label: gettext('Indexes'),
type: 'coll-index',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
sqlAlterHelp: 'sql-alterindex.html',
sqlCreateHelp: 'sql-createindex.html',
dialogHelp: url_for('help.static', {'filename': 'index_dialog.html'}),
@@ -215,7 +216,6 @@ define('pgadmin.node.index', [
if (!pgBrowser.Nodes['index']) {
pgAdmin.Browser.Nodes['index'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table', 'view', 'mview', 'partition'],
collection_type: ['coll-table', 'coll-view'],
sqlAlterHelp: 'sql-alterindex.html',
@@ -266,8 +266,8 @@ define('pgadmin.node.index', [
},
]);
},
- canDrop: pgBrowser.Nodes['schema'].canChildDrop,
- canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
+ canDrop: SchemaChildTreeNode.isTreeItemOfChildOfSchema,
+ canDropCascade: SchemaChildTreeNode.isTreeItemOfChildOfSchema,
model: pgAdmin.Browser.Node.Model.extend({
idAttribute: 'oid',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js
index d807304e..74649721 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/partitions/static/js/partition.js
@@ -2,10 +2,12 @@ define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.backgrid',
+ 'pgadmin.node.schema.dir/schema_child_tree_node',
'pgadmin.browser.collection', 'pgadmin.browser.table.partition.utils',
],
function(
- gettext, url_for, $, _, S, pgAdmin, pgBrowser, Alertify, Backform, Backgrid
+ gettext, url_for, $, _, S, pgAdmin, pgBrowser, Alertify, Backform, Backgrid,
+ SchemaChildTreeNode
) {
if (!pgBrowser.Nodes['coll-partition']) {
@@ -13,7 +15,6 @@ function(
pgAdmin.Browser.Collection.extend({
node: 'partition',
label: gettext('Partitions'),
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
type: 'coll-partition',
columns: [
'name', 'schema', 'partition_value', 'is_partitioned', 'description',
@@ -80,36 +81,6 @@ function(
},
]);
},
- getTreeNodeHierarchy: function(i) {
- var idx = 0,
- res = {},
- t = pgBrowser.tree;
-
- do {
- var d = t.itemData(i);
- if (
- d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId
- ) {
- if (d._type == 'partition' && 'partition' in res) {
- if (!('table' in res)) {
- res['table'] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- } else if (d._type == 'table') {
- if (!('table' in res)) {
- res['table'] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- } else {
- res[d._type] = _.extend({}, d, {'priority': idx});
- idx -= 1;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- } while (i);
-
- return res;
- },
generate_url: function(item, type, d, with_id, info) {
if (_.indexOf([
'stats', 'statistics', 'dependency', 'dependent', 'reset',
@@ -133,8 +104,8 @@ function(
encodeURIComponent(info['partition']._id)
).value();
},
- canDrop: pgBrowser.Nodes['schema'].canChildDrop,
- canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
+ canDrop: SchemaChildTreeNode.isTreeItemOfChildOfSchema,
+ canDropCascade: SchemaChildTreeNode.isTreeItemOfChildOfSchema,
callbacks: {
/* Enable trigger(s) on table */
enable_triggers_on_table: function(args) {
@@ -1189,34 +1160,7 @@ function(
return data;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-table' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null;
- var prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
+ canCreate: SchemaChildTreeNode.isTreeItemOfChildOfSchema,
// Check to whether table has disable trigger(s)
canCreate_with_trigger_enable: function(itemData, item, data) {
if(this.canCreate.apply(this, [itemData, item, data])) {
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js
index 3af61754..354909f3 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/rules/static/js/rule.js
@@ -16,7 +16,6 @@ define('pgadmin.node.rule', [
node: 'rule',
label: gettext('Rules'),
type: 'coll-rule',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
columns: ['name', 'owner', 'comment'],
});
}
@@ -35,7 +34,6 @@ define('pgadmin.node.rule', [
*/
if (!pgBrowser.Nodes['rule']) {
pgAdmin.Browser.Nodes['rule'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table','view', 'partition'],
type: 'rule',
sqlAlterHelp: 'sql-alterrule.html',
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers.js
new file mode 100644
index 00000000..2d792043
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers.js
@@ -0,0 +1,52 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import axios from 'axios';
+
+export function disableTriggers(tree, alertify, generateUrl, args) {
+ return setTriggers(tree, alertify, generateUrl, args, {enable: 'false' });
+}
+export function enableTriggers(tree, alertify, generateUrl, args) {
+ return setTriggers(tree, alertify, generateUrl, args, {enable: 'true' });
+}
+
+function setTriggers(tree, alertify, generateUrl, args, params) {
+ const treeNode = retrieveTreeNode(args, tree);
+
+ if (!treeNode || treeNode.getData() === null || treeNode.getData() === undefined)
+ return false;
+
+ axios.put(
+ generateUrl(treeNode.getHtmlIdentifier(), 'set_trigger', treeNode.getData(), true),
+ params
+ )
+ .then((res) => {
+ if (res.data.success === 1) {
+ alertify.success(res.data.info);
+ treeNode.reload(tree);
+ }
+ })
+ .catch((xhr) => {
+ try {
+ const err = xhr.response.data;
+ if (err.success === 0) {
+ alertify.error(err.errormsg);
+ }
+ } catch (e) {
+ console.warn(e.stack || e);
+ }
+ treeNode.unload(tree);
+ });
+}
+
+function retrieveTreeNode(args, tree) {
+ const input = args || {};
+ const domElementIdentifier = input.item || tree.selected();
+ return tree.findNodeByDomElement(domElementIdentifier);
+}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
index d440bf04..1f9bcf97 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js
@@ -1,13 +1,16 @@
define('pgadmin.node.table', [
+ 'pgadmin.tables.js/enable_disable_triggers',
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.backgrid',
'pgadmin.tables.js/show_advanced_tab',
- 'pgadmin.browser.collection', 'pgadmin.node.column',
- 'pgadmin.node.constraints', 'pgadmin.browser.table.partition.utils',
+ 'pgadmin.node.schema.dir/child', 'pgadmin.browser.collection',
+ 'pgadmin.node.column', 'pgadmin.node.constraints',
+ 'pgadmin.browser.table.partition.utils',
], function(
+ tableFunctions,
gettext, url_for, $, _, S, pgAdmin, pgBrowser, Alertify, Backform, Backgrid,
- ShowAdvancedTab
+ ShowAdvancedTab, SchemaChild
) {
if (!pgBrowser.Nodes['coll-table']) {
@@ -25,8 +28,7 @@ define('pgadmin.node.table', [
}
if (!pgBrowser.Nodes['table']) {
- pgBrowser.Nodes['table'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
+ pgBrowser.Nodes['table'] = SchemaChild.SchemaChildNode.extend({
type: 'table',
label: gettext('Table'),
collection_type: 'coll-table',
@@ -39,7 +41,6 @@ define('pgadmin.node.table', [
sqlAlterHelp: 'sql-altertable.html',
sqlCreateHelp: 'sql-createtable.html',
dialogHelp: url_for('help.static', {'filename': 'table_dialog.html'}),
- parent_type: ['schema', 'catalog'],
hasScriptTypes: ['create', 'select', 'insert', 'update', 'delete'],
height: '95%',
width: '85%',
@@ -113,51 +114,24 @@ define('pgadmin.node.table', [
this.handle_cache, this
);
},
- canDrop: pgBrowser.Nodes['schema'].canChildDrop,
- canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
callbacks: {
/* Enable trigger(s) on table */
enable_triggers_on_table: function(args) {
- var params = {'enable': true };
- this.callbacks.set_triggers.apply(this, [args, params]);
+ tableFunctions.enableTriggers(
+ pgBrowser.treeMenu,
+ Alertify,
+ this.generate_url.bind(this),
+ args
+ );
},
/* Disable trigger(s) on table */
disable_triggers_on_table: function(args) {
- var params = {'enable': false };
- this.callbacks.set_triggers.apply(this, [args, params]);
- },
- set_triggers: function(args, params) {
- // This function will send request to enable or
- // disable triggers on table level
- var input = args || {},
- obj = this,
- t = pgBrowser.tree,
- i = input.item || t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined;
- if (!d)
- return false;
-
- $.ajax({
- url: obj.generate_url(i, 'set_trigger' , d, true),
- type:'PUT',
- data: params,
- dataType: 'json',
- success: function(res) {
- if (res.success == 1) {
- Alertify.success(res.info);
- t.unload(i);
- t.setInode(i);
- t.deselect(i);
- setTimeout(function() {
- t.select(i);
- }, 10);
- }
- },
- error: function(xhr, status, error) {
- Alertify.pgRespErrorNotify(xhr, error);
- t.unload(i);
- },
- });
+ tableFunctions.disableTriggers(
+ pgBrowser.treeMenu,
+ Alertify,
+ this.generate_url.bind(this),
+ args
+ );
},
/* Truncate table */
truncate_table: function(args) {
@@ -1299,55 +1273,15 @@ define('pgadmin.node.table', [
return data;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-table' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
// Check to whether table has disable trigger(s)
canCreate_with_trigger_enable: function(itemData, item, data) {
- if(this.canCreate.apply(this, [itemData, item, data])) {
- // We are here means we can create menu, now let's check condition
- if(itemData.tigger_count > 0) {
- return true;
- } else {
- return false;
- }
- }
+ return itemData.tigger_count > 0 &&
+ this.canCreate.apply(this, [itemData, item, data]);
},
// Check to whether table has enable trigger(s)
canCreate_with_trigger_disable: function(itemData, item, data) {
- if(this.canCreate.apply(this, [itemData, item, data])) {
- // We are here means we can create menu, now let's check condition
- if(itemData.tigger_count > 0 && itemData.has_enable_triggers > 0) {
- return true;
- } else {
- return false;
- }
- }
+ return itemData.tigger_count > 0 && itemData.has_enable_triggers > 0 &&
+ this.canCreate.apply(this, [itemData, item, data]);
},
onTableUpdated: function(_node, _oldNodeData, _newNodeData) {
var key, childIDs;
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js
index a2c27188..a6e79ce2 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/triggers/static/js/trigger.js
@@ -1,8 +1,13 @@
define('pgadmin.node.trigger', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
- 'underscore.string', 'sources/pgadmin', 'pgadmin.browser', 'backform', 'pgadmin.alertifyjs',
+ 'underscore.string', 'sources/pgadmin', 'pgadmin.browser',
+ 'pgadmin.backform', 'pgadmin.alertifyjs',
+ 'pgadmin.node.schema.dir/schema_child_tree_node',
'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform, alertify) {
+], function(
+ gettext, url_for, $, _, S, pgAdmin, pgBrowser, Backform, alertify,
+ SchemaChildTreeNode
+) {
Backform.CustomSwitchControl = Backform.SwitchControl.extend({
template: _.template([
@@ -29,14 +34,12 @@ define('pgadmin.node.trigger', [
node: 'trigger',
label: gettext('Triggers'),
type: 'coll-trigger',
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
columns: ['name', 'description'],
});
}
if (!pgBrowser.Nodes['trigger']) {
pgAdmin.Browser.Nodes['trigger'] = pgBrowser.Node.extend({
- getTreeNodeHierarchy: pgBrowser.tableChildTreeNodeHierarchy,
parent_type: ['table', 'view', 'partition'],
collection_type: ['coll-table', 'coll-view'],
type: 'trigger',
@@ -175,8 +178,8 @@ define('pgadmin.node.trigger', [
});
},
},
- canDrop: pgBrowser.Nodes['schema'].canChildDrop,
- canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
+ canDrop: SchemaChildTreeNode.isTreeItemOfChildOfSchema,
+ canDropCascade: SchemaChildTreeNode.isTreeItemOfChildOfSchema,
model: pgAdmin.Browser.Node.Model.extend({
defaults: {
name: undefined,
@@ -618,50 +621,16 @@ define('pgadmin.node.trigger', [
return flag;
},
}),
- // Below function will enable right click menu for creating column
- canCreate: function(itemData, item, data) {
- // If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData, parents = [];
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to c reate table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
- parents.push(d._type);
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // If node is under catalog then do not allow 'create' menu
- if (_.indexOf(parents, 'catalog') > -1) {
- return false;
- } else {
- return true;
- }
- },
+ canCreate: SchemaChildTreeNode.isTreeItemOfChildOfSchema,
// Check to whether trigger is disable ?
canCreate_with_trigger_enable: function(itemData, item, data) {
- if(this.canCreate.apply(this, [itemData, item, data])) {
- // We are here means we can create menu, now let's check condition
- if(itemData.icon === 'icon-trigger-bad') {
- return true;
- } else {
- return false;
- }
- }
+ return itemData.icon === 'icon-trigger-bad' &&
+ this.canCreate.apply(this, [itemData, item, data]);
},
// Check to whether trigger is enable ?
canCreate_with_trigger_disable: function(itemData, item, data) {
- if(this.canCreate.apply(this, [itemData, item, data])) {
- // We are here means we can create menu, now let's check condition
- if(itemData.icon === 'icon-trigger') {
- return true;
- } else {
- return false;
- }
- }
+ return itemData.icon === 'icon-trigger' &&
+ this.canCreate.apply(this, [itemData, item, data]);
},
});
}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.js
index c1c24861..5860a752 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.js
@@ -1,8 +1,12 @@
define('pgadmin.node.type', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
- 'pgadmin.backgrid', 'pgadmin.browser.collection',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, Backgrid) {
+ 'pgadmin.backgrid', 'pgadmin.node.schema.dir/child',
+ 'pgadmin.browser.collection',
+], function(
+ gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, Backgrid,
+ schemaChild
+) {
if (!pgBrowser.Nodes['coll-type']) {
pgBrowser.Nodes['coll-type'] =
@@ -245,7 +249,7 @@ define('pgadmin.node.type', [
});
if (!pgBrowser.Nodes['type']) {
- pgBrowser.Nodes['type'] = pgBrowser.Node.extend({
+ pgBrowser.Nodes['type'] = schemaChild.SchemaChildNode.extend({
type: 'type',
sqlAlterHelp: 'sql-altertype.html',
sqlCreateHelp: 'sql-createtype.html',
@@ -254,7 +258,6 @@ define('pgadmin.node.type', [
collection_type: 'coll-type',
hasSQL: true,
hasDepends: true,
- parent_type: ['schema', 'catalog'],
Init: function() {
/* Avoid multiple registration of menus */
if (this.initialized)
@@ -284,8 +287,6 @@ define('pgadmin.node.type', [
]);
},
- canDrop: pgBrowser.Nodes['schema'].canChildDrop,
- canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
ext_funcs: undefined,
model: pgBrowser.Node.Model.extend({
defaults: {
@@ -911,34 +912,6 @@ define('pgadmin.node.type', [
return result;
},
}),
- canCreate: function(itemData, item, data) {
- //If check is false then , we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to create table
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-type' == d._type) {
- //Check if we are not child of catalog
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- },
});
}
return pgBrowser.Nodes['type'];
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.js
index 073ef5cb..dcfdd54b 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.js
@@ -1,8 +1,12 @@
define('pgadmin.node.mview', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.alertifyjs', 'pgadmin.browser',
- 'pgadmin.backform', 'pgadmin.browser.server.privilege',
-], function(gettext, url_for, $, _, pgAdmin, Alertify, pgBrowser, Backform) {
+ 'pgadmin.backform', 'pgadmin.node.schema.dir/child',
+ 'pgadmin.browser.server.privilege',
+], function(
+ gettext, url_for, $, _, pgAdmin, Alertify, pgBrowser, Backform,
+ schemaChild
+) {
/**
Create and add a view collection into nodes
@@ -33,19 +37,16 @@ define('pgadmin.node.mview', [
view option in the context menu
*/
if (!pgBrowser.Nodes['mview']) {
- pgBrowser.Nodes['mview'] = pgBrowser.Node.extend({
- parent_type: ['schema', 'catalog'],
+ pgBrowser.Nodes['mview'] = schemaChild.SchemaChildNode.extend({
type: 'mview',
sqlAlterHelp: 'sql-altermaterializedview.html',
sqlCreateHelp: 'sql-creatematerializedview.html',
dialogHelp: url_for('help.static', {'filename': 'materialized_view_dialog.html'}),
label: gettext('Materialized View'),
- hasSQL: true,
+ hasSQL: true,
hasDepends: true,
hasScriptTypes: ['create', 'select'],
collection_type: 'coll-mview',
- canDrop: pgBrowser.Nodes['schema'].canChildDrop,
- canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
Init: function() {
// Avoid mulitple registration of menus
@@ -236,43 +237,6 @@ define('pgadmin.node.mview', [
}),
- /**
- Show or hide create view menu option on parent node
- and hide for system view in catalogs.
- */
- canCreate: function(itemData, item, data) {
-
- // If check is false then, we will allow create menu
- if (data && data.check === false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
-
- // To iterate over tree to check parent node
- while (i) {
-
- // If it is schema then allow user to create view
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-mview' == d._type) {
-
- // Check if we are not child of view
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
-
- // by default we do not want to allow create menu
- return true;
- },
refresh_mview: function(args) {
var input = args || {},
obj = this,
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js
index 5755a509..cd61ef21 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js
@@ -1,9 +1,12 @@
define('pgadmin.node.view', [
- 'sources/gettext',
- 'sources/url_for', 'jquery', 'underscore', 'sources/pgadmin',
- 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.browser.server.privilege',
+ 'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
+ 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
+ 'pgadmin.node.schema.dir/child', 'pgadmin.browser.server.privilege',
'pgadmin.node.rule',
-], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform) {
+], function(
+ gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, schemaChild
+) {
+
/**
Create and add a view collection into nodes
@@ -28,14 +31,9 @@ define('pgadmin.node.view', [
under which this node to display
@param {variable} type - Type of Node
@param {variable} hasSQL - To show SQL tab
- @param {variable} canDrop - Adds drop view option
- in the context menu
- @param {variable} canDropCascade - Adds drop Cascade
- view option in the context menu
*/
if (!pgBrowser.Nodes['view']) {
- pgBrowser.Nodes['view'] = pgBrowser.Node.extend({
- parent_type: ['schema', 'catalog'],
+ pgBrowser.Nodes['view'] = schemaChild.SchemaChildNode.extend({
type: 'view',
sqlAlterHelp: 'sql-alterview.html',
sqlCreateHelp: 'sql-createview.html',
@@ -45,8 +43,6 @@ define('pgadmin.node.view', [
hasDepends: true,
hasScriptTypes: ['create', 'select', 'insert'],
collection_type: 'coll-view',
- canDrop: pgBrowser.Nodes['schema'].canChildDrop,
- canDropCascade: pgBrowser.Nodes['schema'].canChildDrop,
Init: function() {
// Avoid mulitple registration of menus
@@ -197,45 +193,6 @@ define('pgadmin.node.view', [
return false;
},
}),
-
- /**
- Show or hide create view menu option on parent node
- and hide for system view in catalogs.
- */
- canCreate: function(itemData, item, data) {
-
- // If check is false then, we will allow create menu
- if (data && data.check == false)
- return true;
-
- var t = pgBrowser.tree, i = item, d = itemData;
-
- // To iterate over tree to check parent node
- while (i) {
-
- // If it is schema then allow user to create view
- if (_.indexOf(['schema'], d._type) > -1)
- return true;
-
- if ('coll-view' == d._type) {
-
- // Check if we are not child of view
- var prev_i = t.hasParent(i) ? t.parent(i) : null,
- prev_d = prev_i ? t.itemData(prev_i) : null;
- if( prev_d._type == 'catalog') {
- return false;
- } else {
- return true;
- }
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
-
- // by default we do not want to allow create menu
- return true;
-
- },
});
}
diff --git a/web/pgadmin/browser/static/js/collection.js b/web/pgadmin/browser/static/js/collection.js
index 05f0edd5..67e44214 100644
--- a/web/pgadmin/browser/static/js/collection.js
+++ b/web/pgadmin/browser/static/js/collection.js
@@ -115,17 +115,17 @@ define([
// Fetch Data
collection.fetch({
reset: true,
- error: function(xhr, error, message) {
+ error: function(model, error, xhr) {
pgBrowser.Events.trigger(
'pgadmin:collection:retrieval:error', 'properties', xhr, error,
- message, item, that
+ error.message, item, that
);
if (!Alertify.pgHandleItemError(
- xhr, error, message, {item: item, info: info}
+ xhr, error, error.message, {item: item, info: info}
)) {
Alertify.pgNotifier(error, xhr, S(
gettext('Error retrieving properties - %s.')
- ).sprintf(message || that.label).value(), function() {
+ ).sprintf(error.message || that.label).value(), function() {
console.warn(arguments);
});
}
diff --git a/web/pgadmin/browser/static/js/node.js b/web/pgadmin/browser/static/js/node.js
index 460ab03d..35eca24b 100644
--- a/web/pgadmin/browser/static/js/node.js
+++ b/web/pgadmin/browser/static/js/node.js
@@ -1,9 +1,13 @@
define('pgadmin.browser.node', [
+ 'sources/tree/pgadmin_tree_node',
'sources/gettext', 'jquery', 'underscore', 'underscore.string', 'sources/pgadmin',
'pgadmin.browser.menu', 'backbone', 'pgadmin.alertifyjs', 'pgadmin.browser.datamodel',
'backform', 'sources/browser/generate_url', 'sources/utils', 'pgadmin.browser.utils',
'pgadmin.backform',
-], function(gettext, $, _, S, pgAdmin, Menu, Backbone, Alertify, pgBrowser, Backform, generateUrl, commonUtils) {
+], function(
+ pgadminTreeNode,
+ gettext, $, _, S, pgAdmin, Menu, Backbone, Alertify, pgBrowser, Backform, generateUrl, commonUtils
+) {
var wcDocker = window.wcDocker,
keyCode = {
@@ -28,7 +32,7 @@ define('pgadmin.browser.node', [
//
// It is unlikely - we will instantiate an object for this class.
// (Inspired by Backbone.extend function)
- pgBrowser.Node.extend = function(props) {
+ pgBrowser.Node.extend = function(props, initialize) {
var parent = this;
var child;
@@ -44,6 +48,10 @@ define('pgadmin.browser.node', [
// Make sure - a child have all the callbacks of the parent.
child.callbacks = _.extend({}, parent.callbacks, props.callbacks);
+ // Let's not bind the callbacks, or initialize the child.
+ if (initialize === false)
+ return child;
+
var bindToChild = function(cb) {
if (typeof(child.callbacks[cb]) == 'function') {
child.callbacks[cb] = child.callbacks[cb].bind(child);
@@ -1566,7 +1574,6 @@ define('pgadmin.browser.node', [
* depends, statistics
*/
generate_url: function(item, type, d, with_id, info) {
-
var opURL = {
'create': 'obj',
'drop': 'obj',
@@ -1608,24 +1615,7 @@ define('pgadmin.browser.node', [
Collection: pgBrowser.DataCollection,
// Base class for Node Data Model
Model: pgBrowser.DataModel,
- getTreeNodeHierarchy: function(i) {
- var idx = 0,
- res = {},
- t = pgBrowser.tree,
- d;
- do {
- d = t.itemData(i);
- if (d._type in pgBrowser.Nodes && pgBrowser.Nodes[d._type].hasId) {
- res[d._type] = _.extend({}, d, {
- 'priority': idx,
- });
- idx -= 1;
- }
- i = t.hasParent(i) ? t.parent(i) : null;
- } while (i);
-
- return res;
- },
+ getTreeNodeHierarchy: pgadminTreeNode.getTreeNodeHierarchyFromIdentifier.bind(pgBrowser),
cache: function(url, node_info, level, data) {
var cached = this.cached = this.cached || {},
hash = url,
diff --git a/web/pgadmin/static/js/alertify/dialog.js b/web/pgadmin/static/js/alertify/dialog.js
new file mode 100644
index 00000000..4a0a1b89
--- /dev/null
+++ b/web/pgadmin/static/js/alertify/dialog.js
@@ -0,0 +1,150 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../gettext';
+import {sprintf} from 'sprintf-js';
+import {DialogFactory} from './dialog_factory';
+import Backform from '../backform.pgadmin';
+import {getTreeNodeHierarchyFromIdentifier} from '../tree/pgadmin_tree_node';
+
+/**
+ * This class can be extended to create new dialog boxes.
+ * Examples of this can be found in:
+ * `web/pgadmin/static/js/backup/backup_dialog.js`
+ *
+ * Do not forget to add the new Dialog type to the `DialogFactory`
+ */
+export class Dialog {
+ constructor(errorAlertTitle,
+ dialogContainerSelector,
+ pgBrowser, $, alertify, DialogModel,
+ backform = Backform) {
+ this.errorAlertTitle = errorAlertTitle;
+ this.alertify = alertify;
+ this.pgBrowser = pgBrowser;
+ this.jquery = $;
+ this.dialogModel = DialogModel;
+ this.backform = backform;
+ this.dialogContainerSelector = dialogContainerSelector;
+ }
+
+ retrieveAncestorOfTypeServer(item) {
+ let serverInformation = null;
+ let aciTreeItem = item || this.pgBrowser.treeMenu.selected();
+ let treeNode = this.pgBrowser.treeMenu.findNodeByDomElement(aciTreeItem);
+
+ if (treeNode) {
+ let nodeData;
+ let databaseNode = treeNode.ancestorNode(
+ (node) => {
+ nodeData = node.getData();
+ return (nodeData._type === 'database');
+ }
+ );
+ let isServerNode = (node) => {
+ nodeData = node.getData();
+ return nodeData._type === 'server';
+ };
+
+ if (databaseNode !== null) {
+ if (nodeData._label.indexOf('=') >= 0) {
+ this.alertify.alert(
+ gettext(this.errorAlertTitle),
+ gettext(
+ 'Databases with = symbols in the name cannot be backed up or restored using this utility.'
+ )
+ );
+ } else {
+ if (databaseNode.hasParent(isServerNode))
+ serverInformation = nodeData;
+ }
+ } else {
+ if (treeNode.anyFamilyMember(isServerNode))
+ serverInformation = nodeData;
+ }
+ }
+
+ if (serverInformation === null) {
+ this.alertify.alert(
+ gettext(this.errorAlertTitle),
+ gettext('Please select server or child node from the browser tree.')
+ );
+ }
+
+ return serverInformation;
+ }
+
+ hasBinariesConfiguration(serverInformation) {
+ const module = 'paths';
+ let preference_name = 'pg_bin_dir';
+ let msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
+
+ if ((serverInformation.type && serverInformation.type === 'ppas') ||
+ serverInformation.server_type === 'ppas') {
+ preference_name = 'ppas_bin_dir';
+ msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
+ }
+ const preference = this.pgBrowser.get_preference(module, preference_name);
+
+ if (preference) {
+ if (!preference.value) {
+ this.alertify.alert(gettext('Configuration required'), msg);
+ return false;
+ }
+ } else {
+ this.alertify.alert(
+ gettext(this.errorAlertTitle),
+ sprintf(gettext('Failed to load preference %s of module %s'), preference_name, module)
+ );
+ return false;
+ }
+ return true;
+ }
+
+ dialogName() {
+ return undefined;
+ }
+
+ createOrGetDialog(dialogTitle, typeOfDialog) {
+ const dialogName = this.dialogName(typeOfDialog);
+
+ if (!this.alertify[dialogName]) {
+ const self = this;
+ this.alertify.dialog(dialogName, function factory() {
+ return self.dialogFactory(dialogTitle, typeOfDialog);
+ });
+ }
+ return this.alertify[dialogName];
+ }
+
+ dialogFactory(dialogTitle, typeOfDialog) {
+ const factory = new DialogFactory(
+ this.pgBrowser,
+ this.jquery,
+ this.alertify,
+ this.dialogModel,
+ this.backform,
+ this.dialogContainerSelector);
+ return factory.create(dialogTitle, typeOfDialog);
+ }
+
+ canExecuteOnCurrentDatabase(aciTreeItem) {
+ const treeInfo = getTreeNodeHierarchyFromIdentifier.apply(this.pgBrowser, [aciTreeItem]);
+
+ if (treeInfo.database && treeInfo.database._label.indexOf('=') >= 0) {
+ this.alertify.alert(
+ gettext(this.errorAlertTitle),
+ gettext('Databases with = symbols in the name cannot be backed up or restored using this utility.')
+ );
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/web/pgadmin/static/js/alertify/dialog_factory.js b/web/pgadmin/static/js/alertify/dialog_factory.js
new file mode 100644
index 00000000..500140b8
--- /dev/null
+++ b/web/pgadmin/static/js/alertify/dialog_factory.js
@@ -0,0 +1,52 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import * as BackupDialog from '../../../tools/backup/static/js/backup_dialog_wrapper';
+import {RestoreDialogWrapper} from '../../../tools/restore/static/js/restore_dialog_wrapper';
+
+export class DialogFactory {
+ constructor(pgBrowser, $,
+ alertify, DialogModel,
+ backform, dialogContainerSelector) {
+ this.pgBrowser = pgBrowser;
+ this.jquery = $;
+ this.alertify = alertify;
+ this.dialogModel = DialogModel;
+ this.backform = backform;
+ this.dialogContainerSelector = dialogContainerSelector;
+ }
+
+ create(dialogTitle, typeOfDialog) {
+ if (typeOfDialog === 'restore') {
+ return this.createRestoreDialog(dialogTitle, typeOfDialog);
+ } else {
+ return this.createBackupDialog(dialogTitle, typeOfDialog);
+ }
+ }
+
+ createRestoreDialog(dialogTitle, typeOfDialog) {
+ return new RestoreDialogWrapper(
+ this.dialogContainerSelector, dialogTitle, typeOfDialog,
+ this.jquery,
+ this.pgBrowser,
+ this.alertify,
+ this.dialogModel,
+ this.backform);
+ }
+
+ createBackupDialog(dialogTitle, typeOfDialog) {
+ return new BackupDialog.BackupDialogWrapper(
+ this.dialogContainerSelector, dialogTitle, typeOfDialog,
+ this.jquery,
+ this.pgBrowser,
+ this.alertify,
+ this.dialogModel,
+ this.backform);
+ }
+}
diff --git a/web/pgadmin/static/js/alertify/dialog_wrapper.js b/web/pgadmin/static/js/alertify/dialog_wrapper.js
new file mode 100644
index 00000000..b5ff8204
--- /dev/null
+++ b/web/pgadmin/static/js/alertify/dialog_wrapper.js
@@ -0,0 +1,57 @@
+import * as commonUtils from '../utils';
+
+export class DialogWrapper {
+ constructor(
+ dialogContainerSelector, dialogTitle, jquery, pgBrowser,
+ alertify, dialogModel, backform) {
+ this.hooks = {
+ onclose: function () {
+ if (this.view) {
+ this.view.remove({
+ data: true,
+ internal: true,
+ silent: true,
+ });
+ }
+ },
+ };
+ this.dialogContainerSelector = dialogContainerSelector;
+ this.dialogTitle = dialogTitle;
+ this.jquery = jquery;
+ this.pgBrowser = pgBrowser;
+ this.alertify = alertify;
+ this.dialogModel = dialogModel;
+ this.backform = backform;
+ }
+
+ build() {
+ this.alertify.pgDialogBuild.apply(this);
+ }
+
+ wasHelpButtonPressed(e) {
+ return e.button.element.name === 'dialog_help'
+ || e.button.element.name === 'object_help';
+ }
+
+ getSelectedNodeData(selectedTreeNode) {
+ if (!this.isNodeSelected(selectedTreeNode)) {
+ return undefined;
+ }
+ const treeNodeData = selectedTreeNode.getData();
+ if (treeNodeData) {
+ return treeNodeData;
+ }
+ return undefined;
+ }
+
+ focusOnDialog(dialog) {
+ dialog.$el.attr('tabindex', -1);
+ this.pgBrowser.keyboardNavigation.getDialogTabNavigator(dialog);
+ const container = dialog.$el.find('.tab-content:first > .tab-pane.active:first');
+ commonUtils.findAndSetFocus(container);
+ }
+
+ isNodeSelected(selectedTreeNode) {
+ return selectedTreeNode;
+ }
+}
diff --git a/web/pgadmin/static/js/nodes/supported_database_node.js b/web/pgadmin/static/js/nodes/supported_database_node.js
new file mode 100644
index 00000000..fde1cf98
--- /dev/null
+++ b/web/pgadmin/static/js/nodes/supported_database_node.js
@@ -0,0 +1,37 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {isValidTreeNodeData} from 'sources/tree/tree';
+
+function checkAllowConnIfDatabaseNode(treeNodeData) {
+ return (treeNodeData._type === 'database' && treeNodeData.allowConn)
+ || treeNodeData._type !== 'database';
+}
+
+function ancestorWithTypeCatalog(treeNode) {
+ return treeNode.anyFamilyMember((node) => {
+ return node.getData()._type === 'catalog';
+ });
+}
+
+export function enabled(tree, supportedNodes, treeNodeData, domTreeNode) {
+ if (!isValidTreeNodeData(treeNodeData))
+ return false;
+
+ let treeNode = tree.findNodeByDomElement(domTreeNode);
+ if (!treeNode) {
+ return false;
+ }
+
+ return checkAllowConnIfDatabaseNode(treeNodeData) &&
+ _.indexOf(supportedNodes, treeNodeData._type) !== -1 &&
+ !ancestorWithTypeCatalog(treeNode);
+}
+
+
diff --git a/web/pgadmin/static/js/tree/pgadmin_tree_node.js b/web/pgadmin/static/js/tree/pgadmin_tree_node.js
new file mode 100644
index 00000000..00f10d24
--- /dev/null
+++ b/web/pgadmin/static/js/tree/pgadmin_tree_node.js
@@ -0,0 +1,72 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+/**
+ * This method received pgBrowser and new TreeNode object
+ *
+ * This method retrieves all the data that exists in the tree node and in
+ * `pgBrowser.Nodes` for all the parent node of the provided node.
+ *
+ * The 2 condition to get the information from pgBrowser.Nodes are:
+ * 1 - the variable _type of the tree node
+ * 2 - the presence of hasId in the pgBrowser.Nodes for the specific node
+ *
+ * Number 2 is used to ignore coll-* nodes as they do not add any useful
+ * information
+ */
+export function getTreeNodeHierarchyFromElement(pgBrowser, treeNode) {
+ return getTreeNodeHierarchy.call(pgBrowser, treeNode);
+}
+
+/**
+ * This method received an ACI Tree JQuery node
+ *
+ * NOTE: this function need to be called on pgBrowser instance.
+ * getTreeNodeHierarchyFromIdentifier.apply(pgBrowser, [aciTreeNodeIdentifier])
+ *
+ * This method retrieves all the data that exists in the tree node and in
+ * `pgBrowser.Nodes` for all the parent node of the provided node.
+ *
+ * The 2 condition to get the information from pgBrowser.Nodes are:
+ * 1 - the variable _type of the tree node
+ * 2 - the presence of hasId in the pgBrowser.Nodes for the specific node
+ *
+ * Number 2 is used to ignore coll-* nodes as they do not add any useful
+ * information
+ */
+export function getTreeNodeHierarchyFromIdentifier(aciTreeNodeIdentifier) {
+ let identifier = this.treeMenu.translateTreeNodeIdFromACITree(aciTreeNodeIdentifier);
+ let currentNode = this.treeMenu.findNode(identifier);
+ return getTreeNodeHierarchy.call(this, currentNode);
+}
+
+export function getTreeNodeHierarchy(currentNode) {
+ let idx = 0;
+ let result = {};
+
+ do {
+ const currentNodeData = currentNode.getData();
+ if (currentNodeData._type in this.Nodes && this.Nodes[currentNodeData._type].hasId) {
+ const nodeType = mapType(currentNodeData._type);
+ if (result[nodeType] === undefined) {
+ result[nodeType] = _.extend({}, currentNodeData, {
+ 'priority': idx,
+ });
+ idx -= 1;
+ }
+ }
+ currentNode = currentNode.hasParent() ? currentNode.parent() : null;
+ } while (currentNode);
+
+ return result;
+}
+
+function mapType(type) {
+ return type === 'partition' ? 'table' : type;
+}
diff --git a/web/pgadmin/static/js/tree/tree.js b/web/pgadmin/static/js/tree/tree.js
index 01edb6c3..b5655a70 100644
--- a/web/pgadmin/static/js/tree/tree.js
+++ b/web/pgadmin/static/js/tree/tree.js
@@ -59,17 +59,20 @@ export class TreeNode {
tree.aciTreeApi.unload(this.domNode);
}
- anyParent(condition) {
+ /*
+ * Find the ancestor with matches this condition
+ */
+ ancestorNode(condition) {
let node = this;
while (node.hasParent()) {
node = node.parent();
if (condition(node)) {
- return true;
+ return node;
}
}
- return false;
+ return null;
}
/**
@@ -81,7 +84,10 @@ export class TreeNode {
return true;
}
- return this.anyParent(condition);
+ return this.ancestorNode(condition) !== null;
+ }
+ anyParent(condition) {
+ return this.ancestorNode(condition) !== null;
}
}
@@ -210,3 +216,7 @@ function findInTree(rootNode, path) {
}
})(rootNode);
}
+
+export function isValidTreeNodeData(treeNodeData) {
+ return !_.isUndefined(treeNodeData) && !_.isNull(treeNodeData);
+}
diff --git a/web/pgadmin/tools/backup/static/js/backup.js b/web/pgadmin/tools/backup/static/js/backup.js
index 94ab8b76..d6fd48c5 100644
--- a/web/pgadmin/tools/backup/static/js/backup.js
+++ b/web/pgadmin/tools/backup/static/js/backup.js
@@ -3,9 +3,12 @@ define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'pgadmin.alertifyjs', 'backbone', 'pgadmin.backgrid',
'pgadmin.backform', 'pgadmin.browser', 'sources/utils',
+ 'tools/backup/static/js/menu_utils',
+ 'tools/backup/static/js/backup_dialog',
+ 'sources/nodes/supported_database_node',
], function(
gettext, url_for, $, _, S, alertify, Backbone, Backgrid, Backform, pgBrowser,
-commonUtils
+ commonUtils, menuUtils, globalBackupDialog, supportedNodes
) {
// if module is already initialized, refer to that.
@@ -394,48 +397,6 @@ commonUtils
this.initialized = true;
- // Define list of nodes on which backup context menu option appears
- var backup_supported_nodes = [
- 'database', 'schema', 'table', 'partition',
- ];
-
- /**
- Enable/disable backup menu in tools based
- on node selected
- if selected node is present in supported_nodes,
- menu will be enabled otherwise disabled.
- Also, hide it for system view in catalogs
- */
- var menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData,
- parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
-
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(backup_supported_nodes, d._type) !== -1 &&
- parent_data._type != 'catalog') {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
- var menu_enabled_server = function(itemData) {
- // If server node selected && connected
- if (!_.isUndefined(itemData) && !_.isNull(itemData))
- return (('server' === itemData._type) && itemData.connected);
- else
- return false;
- };
-
// Define the nodes on which the menus to be appear
var menus = [{
name: 'backup_global',
@@ -445,7 +406,7 @@ commonUtils
priority: 12,
label: gettext('Backup Globals...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_server',
module: this,
@@ -454,7 +415,7 @@ commonUtils
priority: 12,
label: gettext('Backup Server...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_global_ctx',
module: this,
@@ -464,7 +425,7 @@ commonUtils
priority: 12,
label: gettext('Backup Globals...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_server_ctx',
module: this,
@@ -474,7 +435,7 @@ commonUtils
priority: 12,
label: gettext('Backup Server...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled_server,
+ enable: menuUtils.menuEnabledServer,
}, {
name: 'backup_object',
module: this,
@@ -483,20 +444,24 @@ commonUtils
priority: 11,
label: gettext('Backup...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled,
+ enable: supportedNodes.enabled.bind(
+ null, pgBrowser.treeMenu, menuUtils.backupSupportedNodes
+ ),
}];
- for (var idx = 0; idx < backup_supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.backupSupportedNodes.length; idx++) {
menus.push({
- name: 'backup_' + backup_supported_nodes[idx],
- node: backup_supported_nodes[idx],
+ name: 'backup_' + menuUtils.backupSupportedNodes[idx],
+ node: menuUtils.backupSupportedNodes[idx],
module: this,
applies: ['context'],
callback: 'backup_objects',
priority: 11,
label: gettext('Backup...'),
icon: 'fa fa-floppy-o',
- enable: menu_enabled,
+ enable: supportedNodes.enabled.bind(
+ null, pgBrowser.treeMenu, menuUtils.backupSupportedNodes
+ ),
});
}
@@ -521,542 +486,25 @@ commonUtils
},
// Callback to draw Backup Dialog for globals/server
- start_backup_global_server: function(action, item, params) {
- var i = item || pgBrowser.tree.selected(),
- server_data = null;
-
- while (i) {
- var node_data = pgBrowser.tree.itemData(i);
- if (node_data._type == 'server') {
- server_data = node_data;
- break;
- }
-
- if (pgBrowser.tree.hasParent(i)) {
- i = $(pgBrowser.tree.parent(i));
- } else {
- alertify.alert(
- gettext('Backup Error'),
- gettext('Please select server or child node from the browser tree.')
- );
- break;
- }
- }
-
- if (!server_data) {
- return;
- }
-
- var module = 'paths',
- preference_name = 'pg_bin_dir',
- msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
-
- if ((server_data.type && server_data.type == 'ppas') ||
- server_data.server_type == 'ppas') {
- preference_name = 'ppas_bin_dir';
- msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
- }
-
- var preference = pgBrowser.get_preference(module, preference_name);
-
- if (preference) {
- if (!preference.value) {
- alertify.alert(gettext('Configuration required'), msg);
- return;
- }
- } else {
- alertify.alert(
- gettext('Backup Error'),
- S(gettext('Failed to load preference %s of module %s')).sprintf(preference_name, module).value()
- );
- return;
- }
-
- var of_type = undefined;
-
- // Set Notes according to type of backup
- if (!_.isUndefined(params['globals']) && params['globals']) {
- of_type = 'globals';
- } else {
- of_type = 'server';
- }
-
- var DialogName = 'BackupDialog_' + of_type,
- DialogTitle = ((of_type == 'globals') ?
- gettext('Backup Globals...') :
- gettext('Backup Server...'));
-
- if (!alertify[DialogName]) {
- alertify.dialog(DialogName, function factory() {
- return {
- main: function(title) {
- this.set('title', title);
- },
- build: function() {
- alertify.pgDialogBuild.apply(this);
- },
- setup: function() {
- return {
- buttons: [{
- text: '',
- className: 'btn btn-default pull-left fa fa-lg fa-info',
- attrs: {
- name: 'object_help',
- type: 'button',
- url: 'backup.html',
- label: gettext('Backup'),
- },
- }, {
- text: '',
- key: 112,
- className: 'btn btn-default pull-left fa fa-lg fa-question',
- attrs: {
- name: 'dialog_help',
- type: 'button',
- label: gettext('Backup'),
- url: url_for('help.static', {
- 'filename': 'backup_dialog.html',
- }),
- },
- }, {
- text: gettext('Backup'),
- key: 13,
- className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
- 'data-btn-name': 'backup',
- }, {
- text: gettext('Cancel'),
- key: 27,
- className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
- 'data-btn-name': 'cancel',
- }],
- // Set options for dialog
- options: {
- title: DialogTitle,
- //disable both padding and overflow control.
- padding: !1,
- overflow: !1,
- model: 0,
- resizable: true,
- maximizable: true,
- pinnable: false,
- closableByDimmer: false,
- modal: false,
- },
- };
- },
- hooks: {
- // Triggered when the dialog is closed
- onclose: function() {
- if (this.view) {
- // clear our backform model/view
- this.view.remove({
- data: true,
- internal: true,
- silent: true,
- });
- }
- },
- },
- prepare: function() {
- var self = this;
- // Disable Backup button until user provides Filename
- this.__internal.buttons[2].element.disabled = true;
-
- var $container = $('<div class=\'backup_dialog\'></div>');
- // Find current/selected node
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (!d)
- return;
- // Create treeInfo
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
- // Instance of backbone model
- var newModel = new BackupModel({
- type: of_type,
- }, {
- node_info: treeInfo,
- }),
- fields = Backform.generateViewSchema(
- treeInfo, newModel, 'create', node, treeInfo.server, true
- );
-
- var view = this.view = new Backform.Dialog({
- el: $container,
- model: newModel,
- schema: fields,
- });
- // Add our class to alertify
- $(this.elements.body.childNodes[0]).addClass(
- 'alertify_tools_dialog_properties obj_properties'
- );
- // Render dialog
- view.render();
-
- this.elements.content.appendChild($container.get(0));
-
- var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
- commonUtils.findAndSetFocus(container);
-
- // Listen to model & if filename is provided then enable Backup button
- this.view.model.on('change', function() {
- if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
- this.errorModel.clear();
- self.__internal.buttons[2].element.disabled = false;
- } else {
- self.__internal.buttons[2].element.disabled = true;
- this.errorModel.set('file', gettext('Please provide a filename'));
- }
- });
- },
- // Callback functions when click on the buttons of the Alertify dialogs
- callback: function(e) {
- // Fetch current server id
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (e.button.element.name == 'dialog_help' || e.button.element.name == 'object_help') {
- e.cancel = true;
- pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
- node, i, e.button.element.getAttribute('label'));
- return;
- }
-
- if (e.button['data-btn-name'] === 'backup') {
-
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- var self = this,
- baseUrl = url_for('backup.create_server_job', {
- 'sid': treeInfo.server._id,
- }),
- args = this.view.model.toJSON();
-
- $.ajax({
- url: baseUrl,
- method: 'POST',
- data: {
- 'data': JSON.stringify(args),
- },
- success: function(res) {
- if (res.success) {
- alertify.success(gettext('Backup job created.'), 5);
- pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
- } else {
- console.warn(res);
- }
- },
- error: function(xhr) {
- try {
- var err = JSON.parse(xhr.responseText);
- alertify.alert(
- gettext('Backup job failed.'),
- err.errormsg
- );
- } catch (e) {
- console.warn(e.stack || e);
- }
- },
- });
- }
- },
- };
- });
- }
- alertify[DialogName](true).resizeTo('60%', '50%');
+ start_backup_global_server: function(action, treeItem, params) {
+ let dialog = new globalBackupDialog.BackupDialog(
+ pgBrowser,
+ $,
+ alertify,
+ BackupModel
+ );
+ dialog.draw(action, treeItem, params);
},
// Callback to draw Backup Dialog for objects
backup_objects: function(action, treeItem) {
-
- var i = treeItem || pgBrowser.tree.selected(),
- server_data = null;
-
- while (i) {
- var node_data = pgBrowser.tree.itemData(i);
- if (node_data._type == 'server') {
- server_data = node_data;
- break;
- }
-
- if (pgBrowser.tree.hasParent(i)) {
- i = $(pgBrowser.tree.parent(i));
- } else {
- alertify.alert(
- gettext('Backup Error'),
- gettext('Please select server or child node from tree.')
- );
- break;
- }
- }
-
- if (!server_data) {
- return;
- }
-
- var module = 'paths',
- preference_name = 'pg_bin_dir',
- msg = gettext('Please set binary path for PostgreSQL Server from preferences.');
-
- if ((server_data.type && server_data.type == 'ppas') ||
- server_data.server_type == 'ppas') {
- preference_name = 'ppas_bin_dir';
- msg = gettext('Please set binary path for EDB Postgres Advanced Server from preferences.');
- }
-
- var preference = pgBrowser.get_preference(module, preference_name);
-
- if (preference) {
- if (!preference.value) {
- alertify.alert(gettext('Configuration required'), msg);
- return;
- }
- } else {
- alertify.alert(
- gettext('Backup Error'),
- S(gettext('Failed to load preference %s of module %s')).sprintf(preference_name, module).value()
- );
- return;
- }
-
- var title = S(gettext('Backup (%s: %s)')),
- tree = pgBrowser.tree,
- item = treeItem || tree.selected(),
- data = item && item.length == 1 && tree.itemData(item),
- node = data && data._type && pgBrowser.Nodes[data._type];
-
- if (!node)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [item]);
-
- if (treeInfo.database._label.indexOf('=') >= 0) {
- alertify.alert(
- gettext('Backup error'),
- gettext('Backup job creation failed. '+
- 'Databases with = symbols in the name cannot be backed up using this utility.')
- );
- return;
- }
-
- title = title.sprintf(node.label, data.label).value();
-
- if (!alertify.backup_objects) {
- // Create Dialog title on the fly with node details
- alertify.dialog('backup_objects', function factory() {
- return {
- main: function(title) {
- this.set('title', title);
- },
- build: function() {
- alertify.pgDialogBuild.apply(this);
- },
- setup: function() {
- return {
- buttons: [{
- text: '',
- className: 'btn btn-default pull-left fa fa-lg fa-info',
- attrs: {
- name: 'object_help',
- type: 'button',
- url: 'backup.html',
- label: gettext('Backup'),
- },
- }, {
- text: '',
- key: 112,
- className: 'btn btn-default pull-left fa fa-lg fa-question',
- attrs: {
- name: 'dialog_help',
- type: 'button',
- label: gettext('Backup'),
- url: url_for('help.static', {
- 'filename': 'backup_dialog.html',
- }),
- },
- }, {
- text: gettext('Backup'),
- key: 13,
- className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
- 'data-btn-name': 'backup',
- }, {
- text: gettext('Cancel'),
- key: 27,
- className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
- 'data-btn-name': 'cancel',
- }],
- // Set options for dialog
- options: {
- title: title,
- //disable both padding and overflow control.
- padding: !1,
- overflow: !1,
- model: 0,
- resizable: true,
- maximizable: true,
- pinnable: false,
- closableByDimmer: false,
- modal: false,
- },
- };
- },
- hooks: {
- // triggered when the dialog is closed
- onclose: function() {
- if (this.view) {
- this.view.remove({
- data: true,
- internal: true,
- silent: true,
- });
- }
- },
- },
- prepare: function() {
- var self = this;
- // Disable Backup button until user provides Filename
- this.__internal.buttons[2].element.disabled = true;
- var $container = $('<div class=\'backup_dialog\'></div>');
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- var newModel = new BackupObjectModel({}, {
- node_info: treeInfo,
- }),
- fields = Backform.generateViewSchema(
- treeInfo, newModel, 'create', node, treeInfo.server, true
- );
-
- var view = this.view = new Backform.Dialog({
- el: $container,
- model: newModel,
- schema: fields,
- });
-
- $(this.elements.body.childNodes[0]).addClass(
- 'alertify_tools_dialog_properties obj_properties'
- );
-
- view.render();
-
- this.elements.content.appendChild($container.get(0));
-
- if(view) {
- view.$el.attr('tabindex', -1);
- // var dialogTabNavigator = pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
- commonUtils.findAndSetFocus(container);
- }
- // Listen to model & if filename is provided then enable Backup button
- this.view.model.on('change', function() {
- if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
- this.errorModel.clear();
- self.__internal.buttons[2].element.disabled = false;
- } else {
- self.__internal.buttons[2].element.disabled = true;
- this.errorModel.set('file', gettext('Please provide filename'));
- }
- });
-
- },
- // Callback functions when click on the buttons of the Alertify dialogs
- callback: function(e) {
- // Fetch current server id
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (e.button.element.name == 'dialog_help' || e.button.element.name == 'object_help') {
- e.cancel = true;
- pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
- node, i, e.button.element.getAttribute('label'));
- return;
- }
-
- if (e.button['data-btn-name'] === 'backup') {
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- // Set current database into model
- this.view.model.set('database', treeInfo.database._label);
-
- // We will remove once object tree is implemented
- // If selected node is Schema then add it in model
- if (d._type == 'schema') {
- var schemas = [];
- schemas.push(d._label);
- this.view.model.set('schemas', schemas);
- }
- // If selected node is Table then add it in model along with
- // its schema
- if (d._type == 'table') {
- this.view.model.set(
- 'tables', [
- [treeInfo.schema._label, d._label],
- ]
- );
- }
-
- // Remove ratio attribute from model if it has empty string.
- // The valid value can be between 0 to 9.
- if (_.isEmpty(this.view.model.get('ratio'))) {
- this.view.model.unset('ratio');
- }
-
- var self = this,
- baseUrl = url_for('backup.create_object_job', {
- 'sid': treeInfo.server._id,
- }),
- args = this.view.model.toJSON();
-
- $.ajax({
- url: baseUrl,
- method: 'POST',
- data: {
- 'data': JSON.stringify(args),
- },
- success: function(res) {
- if (res.success) {
- alertify.success(gettext('Backup job created.'), 5);
- pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
- }
- },
- error: function(xhr) {
- try {
- var err = JSON.parse(xhr.responseText);
- alertify.alert(
- gettext('Backup job failed.'),
- err.errormsg
- );
- } catch (e) {
- console.warn(e.stack || e);
- }
- },
- });
- }
- },
- };
- });
- }
- alertify.backup_objects(title).resizeTo('65%', '60%');
+ let dialog = new globalBackupDialog.BackupDialog(
+ pgBrowser,
+ $,
+ alertify,
+ BackupObjectModel
+ );
+ dialog.draw(action, treeItem, null);
},
};
return pgBrowser.Backup;
diff --git a/web/pgadmin/tools/backup/static/js/backup_dialog.js b/web/pgadmin/tools/backup/static/js/backup_dialog.js
new file mode 100644
index 00000000..2f530d80
--- /dev/null
+++ b/web/pgadmin/tools/backup/static/js/backup_dialog.js
@@ -0,0 +1,72 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../../../../static/js/gettext';
+import Backform from '../../../../static/js/backform.pgadmin';
+import {Dialog} from '../../../../static/js/alertify/dialog';
+
+export class BackupDialog extends Dialog {
+ constructor(pgBrowser, $, alertify, BackupModel, backform = Backform) {
+ super('Backup Error',
+ '<div class=\'backup_dialog\'></div>',
+ pgBrowser, $, alertify, BackupModel, backform
+ );
+ }
+
+ draw(action, aciTreeItem, params) {
+ const serverInformation = this.retrieveAncestorOfTypeServer(aciTreeItem);
+
+ if (!serverInformation) {
+ return;
+ }
+
+ if (!this.hasBinariesConfiguration(serverInformation)) {
+ return;
+ }
+
+ const typeOfDialog = BackupDialog.typeOfDialog(params);
+
+ if (!this.canExecuteOnCurrentDatabase(aciTreeItem)) {
+ return;
+ }
+
+ const dialog = this.createOrGetDialog(
+ BackupDialog.dialogTitle(typeOfDialog),
+ typeOfDialog
+ );
+ dialog(true).resizeTo('60%', '50%');
+ }
+
+ static typeOfDialog(params) {
+ if (params === null) {
+ return 'backup_objects';
+ }
+ let typeOfDialog = 'server';
+ if (!_.isUndefined(params['globals']) && params['globals']) {
+ typeOfDialog = 'globals';
+ }
+ return typeOfDialog;
+ }
+
+ static dialogTitle(typeOfDialog) {
+ if (typeOfDialog === 'backup_objects') {
+ return null;
+ }
+ return ((typeOfDialog === 'globals') ?
+ gettext('Backup Globals...') :
+ gettext('Backup Server...'));
+ }
+
+ dialogName(typeOfDialog) {
+ if (typeOfDialog === 'backup_objects') {
+ return typeOfDialog;
+ }
+ return 'BackupDialog_' + typeOfDialog;
+ }
+}
diff --git a/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js b/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js
new file mode 100644
index 00000000..2cebe3d1
--- /dev/null
+++ b/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js
@@ -0,0 +1,258 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {getTreeNodeHierarchyFromElement} from '../../../../static/js/tree/pgadmin_tree_node';
+import axios from 'axios/index';
+import gettext from '../../../../static/js/gettext';
+import url_for from '../../../../static/js/url_for';
+import _ from 'underscore';
+import {DialogWrapper} from '../../../../static/js/alertify/dialog_wrapper';
+
+export class BackupDialogWrapper extends DialogWrapper {
+ constructor(dialogContainerSelector, dialogTitle, typeOfDialog,
+ jquery, pgBrowser, alertify, dialogModel, backform) {
+ super(dialogContainerSelector, dialogTitle, jquery,
+ pgBrowser, alertify, dialogModel, backform);
+ this.typeOfDialog = typeOfDialog;
+ }
+
+ main(title) {
+ this.set('title', title);
+ }
+
+ setup() {
+ return {
+ buttons: [{
+ text: '',
+ className: 'btn btn-default pull-left fa fa-lg fa-info',
+ attrs: {
+ name: 'object_help',
+ type: 'button',
+ url: 'backup.html',
+ label: gettext('Backup'),
+ },
+ }, {
+ text: '',
+ key: 112,
+ className: 'btn btn-default pull-left fa fa-lg fa-question',
+ attrs: {
+ name: 'dialog_help',
+ type: 'button',
+ label: gettext('Backup'),
+ url: url_for('help.static', {
+ 'filename': 'backup_dialog.html',
+ }),
+ },
+ }, {
+ text: gettext('Backup'),
+ key: 13,
+ className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
+ 'data-btn-name': 'backup',
+ }, {
+ text: gettext('Cancel'),
+ key: 27,
+ className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
+ 'data-btn-name': 'cancel',
+ }],
+ // Set options for dialog
+ options: {
+ title: this.dialogTitle,
+ //disable both padding and overflow control.
+ padding: !1,
+ overflow: !1,
+ model: 0,
+ resizable: true,
+ maximizable: true,
+ pinnable: false,
+ closableByDimmer: false,
+ modal: false,
+ },
+ };
+ }
+
+ prepare() {
+ this.disableBackupButton();
+
+ const $container = this.jquery(this.dialogContainerSelector);
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = this.getSelectedNodeData(selectedTreeNode);
+ if (!selectedTreeNodeData) {
+ return;
+ }
+
+ const node = this.pgBrowser.Nodes[selectedTreeNodeData._type];
+ if (this.dialogTitle === null) {
+ const title = `Backup (${node.label}: ${selectedTreeNodeData.label})`;
+ this.main(title);
+ }
+
+ const treeInfo = getTreeNodeHierarchyFromElement(this.pgBrowser, selectedTreeNode);
+ const dialog = this.createDialog(node, treeInfo, this.typeOfDialog, $container);
+ this.addAlertifyClassToBackupNodeChildNodes();
+ dialog.render();
+
+ this.elements.content.appendChild($container.get(0));
+
+ this.focusOnDialog(dialog);
+ this.setListenersForFilenameChanges();
+ }
+
+ callback(event) {
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = this.getSelectedNodeData(selectedTreeNode);
+ const node = selectedTreeNodeData && this.pgBrowser.Nodes[selectedTreeNodeData._type];
+
+ if (this.wasHelpButtonPressed(event)) {
+ event.cancel = true;
+ this.pgBrowser.showHelp(
+ event.button.element.name,
+ event.button.element.getAttribute('url'),
+ node,
+ selectedTreeNode,
+ event.button.element.getAttribute('label')
+ );
+ return;
+ }
+
+ if (this.wasBackupButtonPressed(event)) {
+
+ if (!selectedTreeNodeData)
+ return;
+
+ const serverIdentifier = this.retrieveServerIdentifier(node, selectedTreeNode);
+
+ const dialog = this;
+ let urlShortcut = 'backup.create_server_job';
+ if (this.typeOfDialog === 'backup_objects') {
+ urlShortcut = 'backup.create_object_job';
+ }
+ const baseUrl = url_for(urlShortcut, {
+ 'sid': serverIdentifier,
+ });
+
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.pgBrowser,
+ selectedTreeNode
+ );
+
+ this.setExtraParameters(selectedTreeNode, treeInfo);
+
+ let service = axios.create({});
+ service.post(
+ baseUrl,
+ this.view.model.toJSON()
+ ).then(function () {
+ dialog.alertify.success(gettext('Backup job created.'), 5);
+ dialog.pgBrowser.Events.trigger('pgadmin-bgprocess:created', dialog);
+ }).catch(function (error) {
+ try {
+ const err = error.response.data;
+ dialog.alertify.alert(
+ gettext('Backup job failed.'),
+ err.errormsg
+ );
+ } catch (e) {
+ console.warn(e.stack || e);
+ }
+ });
+ }
+ }
+
+ addAlertifyClassToBackupNodeChildNodes() {
+ this.jquery(this.elements.body.childNodes[0]).addClass(
+ 'alertify_tools_dialog_properties obj_properties'
+ );
+ }
+
+ getSelectedNode() {
+ const tree = this.pgBrowser.treeMenu;
+ const selectedNode = tree.selected();
+ if (selectedNode) {
+ return tree.findNodeByDomElement(selectedNode);
+ } else {
+ return undefined;
+ }
+ }
+
+ disableBackupButton() {
+ this.__internal.buttons[2].element.disabled = true;
+ }
+
+ enableBackupButton() {
+ this.__internal.buttons[2].element.disabled = false;
+ }
+
+ createDialog(node, treeInfo, typeOfDialog, $container) {
+ let attributes = {};
+ if (typeOfDialog !== 'backup_objects') {
+ attributes['type'] = typeOfDialog;
+ }
+ // Instance of backbone model
+ const newModel = new this.dialogModel(attributes, {
+ node_info: treeInfo,
+ });
+ const fields = this.backform.generateViewSchema(
+ treeInfo, newModel, 'create', node, treeInfo.server, true
+ );
+
+ return this.view = new this.backform.Dialog({
+ el: $container,
+ model: newModel,
+ schema: fields,
+ });
+ }
+
+ retrieveServerIdentifier(node, selectedTreeNode) {
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.pgBrowser,
+ selectedTreeNode
+ );
+ return treeInfo.server._id;
+ }
+
+ setListenersForFilenameChanges() {
+ const self = this;
+
+ this.view.model.on('change', function () {
+ if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
+ this.errorModel.clear();
+ self.enableBackupButton();
+ } else {
+ self.disableBackupButton();
+ this.errorModel.set('file', gettext('Please provide a filename'));
+ }
+ });
+ }
+
+ setExtraParameters(selectedTreeNode, treeInfo) {
+ if (this.typeOfDialog === 'backup_objects') {
+
+ this.view.model.set('database', treeInfo.database._label);
+
+ const nodeData = selectedTreeNode.getData();
+ if (nodeData._type === 'schema') {
+ this.view.model.set('schemas', [nodeData._label]);
+ }
+
+ if (nodeData._type === 'table') {
+ this.view.model.set('tables', [
+ [treeInfo.schema._label, nodeData._label],
+ ]);
+ }
+
+ if (_.isEmpty(this.view.model.get('ratio'))) {
+ this.view.model.unset('ratio');
+ }
+ }
+ }
+
+ wasBackupButtonPressed(event) {
+ return event.button['data-btn-name'] === 'backup';
+ }
+}
diff --git a/web/pgadmin/tools/backup/static/js/menu_utils.js b/web/pgadmin/tools/backup/static/js/menu_utils.js
new file mode 100644
index 00000000..47a9f0d3
--- /dev/null
+++ b/web/pgadmin/tools/backup/static/js/menu_utils.js
@@ -0,0 +1,23 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {isValidTreeNodeData} from '../../../../static/js/tree/tree';
+
+export const backupSupportedNodes = [
+ 'database', 'schema', 'table', 'partition',
+];
+
+function isNodeAServerAndConnected(treeNodeData) {
+ return (('server' === treeNodeData._type) && treeNodeData.connected);
+}
+
+export function menuEnabledServer(treeNodeData) {
+ return isValidTreeNodeData(treeNodeData)
+ && isNodeAServerAndConnected(treeNodeData);
+}
diff --git a/web/pgadmin/tools/datagrid/static/js/datagrid.js b/web/pgadmin/tools/datagrid/static/js/datagrid.js
index 42122ed8..c730b180 100644
--- a/web/pgadmin/tools/datagrid/static/js/datagrid.js
+++ b/web/pgadmin/tools/datagrid/static/js/datagrid.js
@@ -1,10 +1,14 @@
define('pgadmin.datagrid', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'pgadmin.alertifyjs', 'sources/pgadmin', 'bundled_codemirror',
- 'sources/sqleditor_utils', 'backbone', 'wcdocker',
+ 'sources/sqleditor_utils', 'backbone',
+ 'tools/datagrid/static/js/show_data',
+ 'tools/datagrid/static/js/get_panel_title',
+ 'tools/datagrid/static/js/show_query_tool',
+ 'wcdocker',
], function(
gettext, url_for, $, _, alertify, pgAdmin, codemirror, sqlEditorUtils,
- Backbone
+ Backbone, showData, panelTitle, showQueryTool
) {
// Some scripts do export their object in the window only.
// Generally the one, which do no have AMD support.
@@ -161,55 +165,7 @@ define('pgadmin.datagrid', [
// This is a callback function to show data when user click on menu item.
show_data_grid: function(data, i) {
- var self = this,
- d = pgAdmin.Browser.tree.itemData(i);
- if (d === undefined) {
- alertify.alert(
- gettext('Data Grid Error'),
- gettext('No object selected.')
- );
- return;
- }
-
- // Get the parent data from the tree node hierarchy.
- var node = pgBrowser.Nodes[d._type],
- parentData = node.getTreeNodeHierarchy(i);
-
- // If server, database or schema is undefined then return from the function.
- if (parentData.server === undefined || parentData.database === undefined) {
- return;
- }
- // If schema, view, catalog object all are undefined then return from the function.
- if (parentData.schema === undefined && parentData.view === undefined &&
- parentData.catalog === undefined) {
- return;
- }
-
- var nsp_name = '';
-
- if (parentData.schema != undefined) {
- nsp_name = parentData.schema.label;
- }
- else if (parentData.view != undefined) {
- nsp_name = parentData.view.label;
- }
- else if (parentData.catalog != undefined) {
- nsp_name = parentData.catalog.label;
- }
- var url_params = {
- 'cmd_type': data.mnuid,
- 'obj_type': d._type,
- 'sgid': parentData.server_group._id,
- 'sid': parentData.server._id,
- 'did': parentData.database._id,
- 'obj_id': d._id,
- };
-
- var baseUrl = url_for('datagrid.initialize_datagrid', url_params);
- var grid_title = parentData.server.label + ' - ' + parentData.database.label + ' - '
- + nsp_name + '.' + d.label;
-
- self.create_transaction(baseUrl, null, 'false', parentData.server.server_type, '', grid_title, '');
+ showData.showDataGrid(this, pgBrowser, alertify, data, i);
},
// This is a callback function to show filtered data when user click on menu item.
@@ -384,63 +340,12 @@ define('pgadmin.datagrid', [
},
get_panel_title: function() {
- // Get the parent data from the tree node hierarchy.
- var tree = pgAdmin.Browser.tree,
- selected_item = tree.selected(),
- item_data = tree.itemData(selected_item);
-
- var node = pgBrowser.Nodes[item_data._type],
- parentData = node.getTreeNodeHierarchy(selected_item);
-
- // If server, database is undefined then return from the function.
- if (parentData.server === undefined) {
- return;
- }
- // If Database is not available then use default db
- var db_label = parentData.database ? parentData.database.label
- : parentData.server.db;
-
- var grid_title = db_label + ' on ' + parentData.server.user.name + '@' +
- parentData.server.label;
- return grid_title;
+ return panelTitle.getPanelTitle(pgBrowser);
},
// This is a callback function to show query tool when user click on menu item.
- show_query_tool: function(url, i, panel_title) {
- var sURL = url || '',
- d = pgAdmin.Browser.tree.itemData(i);
-
- panel_title = panel_title || '';
- if (d === undefined) {
- alertify.alert(
- gettext('Query Tool Error'),
- gettext('No object selected.')
- );
- return;
- }
-
- // Get the parent data from the tree node hierarchy.
- var node = pgBrowser.Nodes[d._type],
- parentData = node.getTreeNodeHierarchy(i);
-
- // If server, database is undefined then return from the function.
- if (parentData.server === undefined) {
- return;
- }
-
- var url_params = {
- 'sgid': parentData.server_group._id,
- 'sid': parentData.server._id,
- },
- url_endpoint = 'datagrid.initialize_query_tool';
- // If database not present then use Maintenance database
- // We will handle this at server side
- if (parentData.database) {
- url_params['did'] = parentData.database._id;
- url_endpoint = 'datagrid.initialize_query_tool_with_did';
- }
- var baseUrl = url_for(url_endpoint, url_params);
-
- this.create_transaction(baseUrl, null, 'true', parentData.server.server_type, sURL, panel_title, '', false);
+ show_query_tool: function(url, aciTreeIdentifier, panelTitle) {
+ showQueryTool.showQueryTool(this, pgBrowser, alertify, url,
+ aciTreeIdentifier, panelTitle);
},
create_transaction: function(baseUrl, target, is_query_tool, server_type, sURL, panel_title, sql_filter, recreate) {
var self = this;
diff --git a/web/pgadmin/tools/datagrid/static/js/get_panel_title.js b/web/pgadmin/tools/datagrid/static/js/get_panel_title.js
new file mode 100644
index 00000000..64b3a3d2
--- /dev/null
+++ b/web/pgadmin/tools/datagrid/static/js/get_panel_title.js
@@ -0,0 +1,33 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {getTreeNodeHierarchyFromIdentifier} from '../../../../static/js/tree/pgadmin_tree_node';
+
+function getDatabaseLabel(parentData) {
+ return parentData.database ? parentData.database.label
+ : parentData.server.db;
+}
+
+function isServerInformationAvailable(parentData) {
+ return parentData.server === undefined;
+}
+
+export function getPanelTitle(pgBrowser) {
+ const selected_item = pgBrowser.treeMenu.selected();
+
+ const parentData = getTreeNodeHierarchyFromIdentifier
+ .call(pgBrowser, selected_item);
+ if (isServerInformationAvailable(parentData)) {
+ return;
+ }
+
+ const db_label = getDatabaseLabel(parentData);
+
+ return `${db_label} on ${parentData.server.user.name}@${parentData.server.label}`;
+}
diff --git a/web/pgadmin/tools/datagrid/static/js/show_data.js b/web/pgadmin/tools/datagrid/static/js/show_data.js
new file mode 100644
index 00000000..373c97cd
--- /dev/null
+++ b/web/pgadmin/tools/datagrid/static/js/show_data.js
@@ -0,0 +1,92 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import gettext from '../../../../static/js/gettext';
+import url_for from '../../../../static/js/url_for';
+import {getTreeNodeHierarchyFromIdentifier} from '../../../../static/js/tree/pgadmin_tree_node';
+
+export function showDataGrid(
+ datagrid,
+ pgBrowser,
+ alertify,
+ connectionData,
+ aciTreeIdentifier
+) {
+ const node = pgBrowser.treeMenu.findNodeByDomElement(aciTreeIdentifier);
+ if (node === undefined || !node.getData()) {
+ alertify.alert(
+ gettext('Data Grid Error'),
+ gettext('No object selected.')
+ );
+ return;
+ }
+
+ const parentData = getTreeNodeHierarchyFromIdentifier.call(
+ pgBrowser,
+ aciTreeIdentifier
+ );
+
+ if (hasServerOrDatabaseConfiguration(parentData)
+ || !hasSchemaOrCatalogOrViewInformation(parentData)) {
+ return;
+ }
+
+ let namespaceName = retrieveNameSpaceName(parentData);
+ const baseUrl = generateUrl(connectionData, node.getData(), parentData);
+ const grid_title = generateDatagridTitle(parentData, namespaceName, node.getData());
+
+ datagrid.create_transaction(
+ baseUrl,
+ null,
+ 'false',
+ parentData.server.server_type,
+ '',
+ grid_title,
+ ''
+ );
+}
+
+
+function retrieveNameSpaceName(parentData) {
+ if (parentData.schema !== undefined) {
+ return parentData.schema.label;
+ }
+ else if (parentData.view !== undefined) {
+ return parentData.view.label;
+ }
+ else if (parentData.catalog !== undefined) {
+ return parentData.catalog.label;
+ }
+ return '';
+}
+
+function generateUrl(connectionData, nodeData, parentData) {
+ const url_params = {
+ 'cmd_type': connectionData.mnuid,
+ 'obj_type': nodeData._type,
+ 'sgid': parentData.server_group._id,
+ 'sid': parentData.server._id,
+ 'did': parentData.database._id,
+ 'obj_id': nodeData._id,
+ };
+
+ return url_for('datagrid.initialize_datagrid', url_params);
+}
+
+function hasServerOrDatabaseConfiguration(parentData) {
+ return parentData.server === undefined || parentData.database === undefined;
+}
+
+function hasSchemaOrCatalogOrViewInformation(parentData) {
+ return parentData.schema !== undefined || parentData.view !== undefined ||
+ parentData.catalog !== undefined;
+}
+
+function generateDatagridTitle(parentData, namespaceName, nodeData) {
+ return `${parentData.server.label} - ${parentData.database.label} - ${namespaceName}.${nodeData.label}`;
+}
diff --git a/web/pgadmin/tools/datagrid/static/js/show_query_tool.js b/web/pgadmin/tools/datagrid/static/js/show_query_tool.js
new file mode 100644
index 00000000..0eb12b8b
--- /dev/null
+++ b/web/pgadmin/tools/datagrid/static/js/show_query_tool.js
@@ -0,0 +1,63 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../../../../static/js/gettext';
+import url_for from '../../../../static/js/url_for';
+import {getTreeNodeHierarchyFromIdentifier} from '../../../../static/js/tree/pgadmin_tree_node';
+
+function hasDatabaseInformation(parentData) {
+ return parentData.database;
+}
+
+function generateUrl(parentData) {
+ let url_endpoint = 'datagrid.initialize_query_tool';
+ let url_params = {
+ 'sgid': parentData.server_group._id,
+ 'sid': parentData.server._id,
+ };
+
+ if (hasDatabaseInformation(parentData)) {
+ url_params['did'] = parentData.database._id;
+ url_endpoint = 'datagrid.initialize_query_tool_with_did';
+ }
+
+ return url_for(url_endpoint, url_params);
+}
+
+function hasServerInformations(parentData) {
+ return parentData.server === undefined;
+}
+
+export function showQueryTool(datagrid, pgBrowser, alertify, url,
+ aciTreeIdentifier, panelTitle) {
+ const sURL = url || '';
+ const queryToolTitle = panelTitle || '';
+
+ const currentNode = pgBrowser.treeMenu.findNodeByDomElement(aciTreeIdentifier);
+ if (currentNode === undefined) {
+ alertify.alert(
+ gettext('Query Tool Error'),
+ gettext('No object selected.')
+ );
+ return;
+ }
+
+ const parentData = getTreeNodeHierarchyFromIdentifier.call(
+ pgBrowser, aciTreeIdentifier);
+
+ if (hasServerInformations(parentData)) {
+ return;
+ }
+
+ const baseUrl = generateUrl(parentData);
+
+ datagrid.create_transaction(
+ baseUrl, null, 'true',
+ parentData.server.server_type, sURL, queryToolTitle, '', false);
+}
diff --git a/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js b/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js
index 5ce0e2fb..2ace07e5 100644
--- a/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js
+++ b/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js
@@ -2,12 +2,15 @@
define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'pgadmin.alertifyjs', 'pgadmin.backgrid', 'pgadmin.backform',
- 'pgadmin.browser', 'pgadmin.browser.node', 'backgrid.select.all',
+ 'pgadmin.browser', 'pgadmin.browser.node',
+ 'tools/grant_wizard/static/js/menu_utils',
+ 'sources/nodes/supported_database_node',
+ 'backgrid.select.all',
'backgrid.filter', 'pgadmin.browser.server.privilege',
'pgadmin.browser.wizard',
], function(
gettext, url_for, $, _, Backbone, Alertify, Backgrid, Backform, pgBrowser,
- pgNode
+ pgNode, menuUtils, supportedNodes
) {
// if module is already initialized, refer to that.
@@ -143,41 +146,6 @@ define([
this.initialized = true;
- // Define list of nodes on which grant wizard context menu option appears
- var supported_nodes = [
- 'schema', 'coll-function', 'coll-sequence',
- 'coll-table', 'coll-view', 'coll-procedure',
- 'coll-mview', 'database', 'coll-trigger_function',
- ],
-
- /**
- Enable/disable grantwizard menu in tools based
- on node selected
- if selected node is present in supported_nodes,
- menu will be enabled otherwise disabled.
- Also, hide it for system view in catalogs
- */
- menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
- var parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(supported_nodes, d._type) !== -1 &&
- parent_data._type != 'catalog') {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
// Define the nodes on which the menus to be appear
var menus = [{
name: 'grant_wizard_schema',
@@ -187,21 +155,25 @@ define([
priority: 14,
label: gettext('Grant Wizard...'),
icon: 'fa fa-unlock-alt',
- enable: menu_enabled,
+ enable: supportedNodes.enabled.bind(
+ null, pgBrowser.treeMenu, menuUtils.supportedNodes
+ ),
}];
// Add supported menus into the menus list
- for (var idx = 0; idx < supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.supportedNodes.length; idx++) {
menus.push({
- name: 'grant_wizard_schema_context_' + supported_nodes[idx],
- node: supported_nodes[idx],
+ name: 'grant_wizard_schema_context_' + menuUtils.supportedNodes[idx],
+ node: menuUtils.supportedNodes[idx],
module: this,
applies: ['context'],
callback: 'start_grant_wizard',
priority: 14,
label: gettext('Grant Wizard...'),
icon: 'fa fa-unlock-alt',
- enable: menu_enabled,
+ enable: supportedNodes.enabled.bind(
+ null, pgBrowser.treeMenu, menuUtils.supportedNodes
+ ),
});
}
pgBrowser.add_menus(menus);
diff --git a/web/pgadmin/tools/grant_wizard/static/js/menu_utils.js b/web/pgadmin/tools/grant_wizard/static/js/menu_utils.js
new file mode 100644
index 00000000..b56ce3cd
--- /dev/null
+++ b/web/pgadmin/tools/grant_wizard/static/js/menu_utils.js
@@ -0,0 +1,16 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export const supportedNodes = [
+ 'schema', 'coll-function', 'coll-sequence',
+ 'coll-table', 'coll-view', 'coll-procedure',
+ 'coll-mview', 'database', 'coll-trigger_function',
+];
+
+
diff --git a/web/pgadmin/tools/import_export/static/js/import_export.js b/web/pgadmin/tools/import_export/static/js/import_export.js
index e64d3dee..5c73537e 100644
--- a/web/pgadmin/tools/import_export/static/js/import_export.js
+++ b/web/pgadmin/tools/import_export/static/js/import_export.js
@@ -1,10 +1,12 @@
define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'underscore.string', 'pgadmin.alertifyjs',
'sources/pgadmin', 'pgadmin.browser', 'backbone', 'backgrid', 'backform',
- 'sources/utils', 'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser.node.ui',
+ 'sources/utils',
+ 'sources/nodes/supported_database_node',
+ 'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser.node.ui',
], function(
gettext, url_for, $, _, S, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid,
-Backform, commonUtils
+Backform, commonUtils, supportedNodes
) {
pgAdmin = pgAdmin || window.pgAdmin || {};
@@ -383,25 +385,6 @@ Backform, commonUtils
this.initialized = true;
- /*
- * Enable/disable import menu in tools based on node selected. Import
- * menu will be enabled only when user select table node.
- */
- var menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
- var parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data))
- return (
- (_.indexOf(['table'], d._type) !== -1 &&
- parent_data._type != 'catalog') ? true : false
- );
- else
- return false;
- };
-
// Initialize the context menu to display the import options when user open the context menu for table
pgBrowser.add_menus([{
name: 'import',
@@ -413,7 +396,9 @@ Backform, commonUtils
priority: 10,
label: gettext('Import/Export...'),
icon: 'fa fa-shopping-cart',
- enable: menu_enabled,
+ enable: supportedNodes.enabled.bind(
+ null, pgBrowser.treeMenu, ['table']
+ ),
}]);
},
diff --git a/web/pgadmin/tools/maintenance/static/js/maintenance.js b/web/pgadmin/tools/maintenance/static/js/maintenance.js
index f2102602..df05c3d5 100644
--- a/web/pgadmin/tools/maintenance/static/js/maintenance.js
+++ b/web/pgadmin/tools/maintenance/static/js/maintenance.js
@@ -2,11 +2,14 @@ define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'underscore.string', 'pgadmin.alertifyjs', 'sources/pgadmin', 'pgadmin.browser', 'backbone',
'backgrid', 'backform', 'sources/utils',
+ 'tools/maintenance/static/js/menu_utils',
+ 'sources/nodes/supported_database_node',
'pgadmin.backform', 'pgadmin.backgrid',
'pgadmin.browser.node.ui',
], function(
gettext, url_for, $, _, S, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid,
- Backform, commonUtils
+ Backform, commonUtils,
+ menuUtils, supportedNodes
) {
pgAdmin = pgAdmin || window.pgAdmin || {};
@@ -168,36 +171,6 @@ define([
this.initialized = true;
- var maintenance_supported_nodes = [
- 'database', 'table', 'primary_key',
- 'unique_constraint', 'index', 'partition',
- ];
-
- /**
- Enable/disable Maintenance menu in tools based on node selected.
- Maintenance menu will be enabled only when user select table and database node.
- */
- var menu_enabled = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
- var parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(maintenance_supported_nodes, d._type) !== -1 &&
- parent_data._type != 'catalog') {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
var menus = [{
name: 'maintenance',
module: this,
@@ -206,21 +179,25 @@ define([
priority: 10,
label: gettext('Maintenance...'),
icon: 'fa fa-wrench',
- enable: menu_enabled,
+ enable: supportedNodes.enabled.bind(
+ null, pgBrowser.treeMenu, menuUtils.maintenanceSupportedNodes
+ ),
}];
// Add supported menus into the menus list
- for (var idx = 0; idx < maintenance_supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.maintenanceSupportedNodes.length; idx++) {
menus.push({
- name: 'maintenance_context_' + maintenance_supported_nodes[idx],
- node: maintenance_supported_nodes[idx],
+ name: 'maintenance_context_' + menuUtils.maintenanceSupportedNodes[idx],
+ node: menuUtils.maintenanceSupportedNodes[idx],
module: this,
applies: ['context'],
callback: 'callback_maintenance',
priority: 10,
label: gettext('Maintenance...'),
icon: 'fa fa-wrench',
- enable: menu_enabled,
+ enable: supportedNodes.enabled.bind(
+ null, pgBrowser.treeMenu, menuUtils.maintenanceSupportedNodes
+ ),
});
}
pgBrowser.add_menus(menus);
diff --git a/web/pgadmin/tools/maintenance/static/js/menu_utils.js b/web/pgadmin/tools/maintenance/static/js/menu_utils.js
new file mode 100644
index 00000000..8cde1baa
--- /dev/null
+++ b/web/pgadmin/tools/maintenance/static/js/menu_utils.js
@@ -0,0 +1,13 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export const maintenanceSupportedNodes = [
+ 'database', 'table', 'primary_key',
+ 'unique_constraint', 'index', 'partition',
+];
diff --git a/web/pgadmin/tools/restore/static/js/menu_utils.js b/web/pgadmin/tools/restore/static/js/menu_utils.js
new file mode 100644
index 00000000..2d35c951
--- /dev/null
+++ b/web/pgadmin/tools/restore/static/js/menu_utils.js
@@ -0,0 +1,18 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+export const restoreSupportedNodes = [
+ 'database',
+ 'schema',
+ 'table',
+ 'function',
+ 'trigger',
+ 'index',
+ 'partition',
+];
diff --git a/web/pgadmin/tools/restore/static/js/restore.js b/web/pgadmin/tools/restore/static/js/restore.js
index 7daa7645..aeff6d3d 100644
--- a/web/pgadmin/tools/restore/static/js/restore.js
+++ b/web/pgadmin/tools/restore/static/js/restore.js
@@ -1,11 +1,13 @@
-// Restore dialog
define('tools.restore', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
'underscore.string', 'pgadmin.alertifyjs', 'pgadmin.browser',
'pgadmin.backgrid', 'pgadmin.backform', 'sources/utils',
+ 'tools/restore/static/js/menu_utils',
+ 'sources/nodes/supported_database_node',
+ 'tools/restore/static/js/restore_dialog',
], function(
gettext, url_for, $, _, Backbone, S, alertify, pgBrowser, Backgrid, Backform,
-commonUtils
+commonUtils, menuUtils, supportedNodes, restoreDialog
) {
// if module is already initialized, refer to that.
@@ -307,59 +309,6 @@ commonUtils
this.initialized = true;
- // Define list of nodes on which restore context menu option appears
- var restore_supported_nodes = [
- 'database', 'schema',
- 'table', 'function',
- 'trigger', 'index',
- 'partition',
- ];
-
- /**
- Enable/disable restore menu in tools based
- on node selected
- if selected node is present in supported_nodes,
- menu will be enabled otherwise disabled.
- Also, hide it for system view in catalogs
- */
- var menu_enabled = function(itemData, item, data) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
- var parent_item = t.hasParent(i) ? t.parent(i) : null,
- parent_data = parent_item ? t.itemData(parent_item) : null;
- if (!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data)) {
- if (_.indexOf(restore_supported_nodes, d._type) !== -1 &&
- is_parent_catalog(itemData, item, data)) {
- if (d._type == 'database' && d.allowConn)
- return true;
- else if (d._type != 'database')
- return true;
- else
- return false;
- } else
- return false;
- } else
- return false;
- };
-
- var is_parent_catalog = function(itemData, item) {
- var t = pgBrowser.tree,
- i = item,
- d = itemData;
-
- // To iterate over tree to check parent node
- while (i) {
- // If it is schema then allow user to restore
- if (_.indexOf(['catalog'], d._type) > -1)
- return false;
- i = t.hasParent(i) ? t.parent(i) : null;
- d = i ? t.itemData(i) : null;
- }
- // by default we do not want to allow create menu
- return true;
- };
-
// Define the nodes on which the menus to be appear
var menus = [{
name: 'restore_object',
@@ -369,20 +318,24 @@ commonUtils
priority: 13,
label: gettext('Restore...'),
icon: 'fa fa-upload',
- enable: menu_enabled,
+ enable: supportedNodes.enabled.bind(
+ null, pgBrowser.treeMenu, menuUtils.restoreSupportedNodes
+ ),
}];
- for (var idx = 0; idx < restore_supported_nodes.length; idx++) {
+ for (var idx = 0; idx < menuUtils.restoreSupportedNodes.length; idx++) {
menus.push({
- name: 'restore_' + restore_supported_nodes[idx],
- node: restore_supported_nodes[idx],
+ name: 'restore_' + menuUtils.restoreSupportedNodes[idx],
+ node: menuUtils.restoreSupportedNodes[idx],
module: this,
applies: ['context'],
callback: 'restore_objects',
priority: 13,
label: gettext('Restore...'),
icon: 'fa fa-upload',
- enable: menu_enabled,
+ enable: supportedNodes.enabled.bind(
+ null, pgBrowser.treeMenu, menuUtils.restoreSupportedNodes
+ ),
});
}
@@ -391,318 +344,10 @@ commonUtils
},
// Callback to draw Backup Dialog for objects
restore_objects: function(action, treeItem) {
-
- var i = treeItem || pgBrowser.tree.selected(),
- server_data = null;
-
- while (i) {
- var node_data = pgBrowser.tree.itemData(i);
- if (node_data._type == 'server') {
- server_data = node_data;
- break;
- }
-
- if (pgBrowser.tree.hasParent(i)) {
- i = $(pgBrowser.tree.parent(i));
- } else {
- alertify.alert(
- gettext('Restore Error'),
- gettext('Please select server or child node from tree.')
- );
- break;
- }
- }
-
- if (!server_data) {
- return;
- }
-
- var module = 'paths',
- preference_name = 'pg_bin_dir',
- msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
-
- if ((server_data.type && server_data.type == 'ppas') ||
- server_data.server_type == 'ppas') {
- preference_name = 'ppas_bin_dir';
- msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
- }
-
- var preference = pgBrowser.get_preference(module, preference_name);
-
- if (preference) {
- if (!preference.value) {
- alertify.alert(gettext('Configuration required'), msg);
- return;
- }
- } else {
- alertify.alert(
- gettext('Restore Error'),
- S(gettext('Failed to load preference %s of module %s')).sprintf(preference_name, module).value()
- );
- return;
- }
-
- var title = S(gettext('Restore (%s: %s)')),
- tree = pgBrowser.tree,
- item = treeItem || tree.selected(),
- data = item && item.length == 1 && tree.itemData(item),
- node = data && data._type && pgBrowser.Nodes[data._type];
-
- if (!node)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [item]);
-
- if (treeInfo.database._label.indexOf('=') >= 0) {
- alertify.alert(
- gettext('Restore error'),
- gettext('Restore job creation failed. '+
- 'Databases with = symbols in the name cannot be restored using this utility.')
- );
- return;
- }
-
- title = title.sprintf(node.label, data.label).value();
-
- if (!alertify.pg_restore) {
- // Create Dialog title on the fly with node details
- alertify.dialog('pg_restore', function factory() {
- return {
- main: function(title, item, data, node) {
- this.set('title', title);
- this.setting('pg_node', node);
- this.setting('pg_item', item);
- this.setting('pg_item_data', data);
- },
- build: function() {
- alertify.pgDialogBuild.apply(this);
- },
- setup: function() {
- return {
- buttons: [{
- text: '',
- className: 'btn btn-default pull-left fa fa-lg fa-info',
- attrs: {
- name: 'object_help',
- type: 'button',
- url: 'backup.html',
- label: gettext('Restore'),
- },
- }, {
- text: '',
- key: 112,
- className: 'btn btn-default pull-left fa fa-lg fa-question',
- attrs: {
- name: 'dialog_help',
- type: 'button',
- label: gettext('Restore'),
- url: url_for('help.static', {
- 'filename': 'restore_dialog.html',
- }),
- },
- }, {
- text: gettext('Restore'),
- key: 13,
- className: 'btn btn-primary fa fa-upload pg-alertify-button',
- restore: true,
- 'data-btn-name': 'restore',
- }, {
- text: gettext('Cancel'),
- key: 27,
- className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
- restore: false,
- 'data-btn-name': 'cancel',
- }],
- // Set options for dialog
- options: {
- title: title,
- //disable both padding and overflow control.
- padding: !1,
- overflow: !1,
- model: 0,
- resizable: true,
- maximizable: true,
- pinnable: false,
- closableByDimmer: false,
- modal: false,
- },
- };
- },
- hooks: {
- // triggered when the dialog is closed
- onclose: function() {
- if (this.view) {
- this.view.remove({
- data: true,
- internal: true,
- silent: true,
- });
- }
- },
- },
- settings: {
- pg_node: null,
- pg_item: null,
- pg_item_data: null,
- },
- prepare: function() {
-
- var self = this;
- // Disable Backup button until user provides Filename
- this.__internal.buttons[2].element.disabled = true;
- var $container = $('<div class=\'restore_dialog\'></div>');
- var t = pgBrowser.tree,
- i = t.selected(),
- d = i && i.length == 1 ? t.itemData(i) : undefined,
- node = d && pgBrowser.Nodes[d._type];
-
- if (!d)
- return;
-
- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
-
- var newModel = new RestoreObjectModel({
- node_data: node,
- }, {
- node_info: treeInfo,
- }),
- fields = Backform.generateViewSchema(
- treeInfo, newModel, 'create', node, treeInfo.server, true
- );
-
- var view = this.view = new Backform.Dialog({
- el: $container,
- model: newModel,
- schema: fields,
- });
-
- $(this.elements.body.childNodes[0]).addClass(
- 'alertify_tools_dialog_properties obj_properties'
- );
-
- view.render();
-
- this.elements.content.appendChild($container.get(0));
-
- view.$el.attr('tabindex', -1);
- // var dialogTabNavigator = pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
- var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
- commonUtils.findAndSetFocus(container);
-
- // Listen to model & if filename is provided then enable Backup button
- this.view.model.on('change', function() {
- if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
- this.errorModel.clear();
- self.__internal.buttons[2].element.disabled = false;
- } else {
- self.__internal.buttons[2].element.disabled = true;
- this.errorModel.set('file', gettext('Please provide filename'));
- }
- });
-
- },
- // Callback functions when click on the buttons of the Alertify dialogs
- callback: function(e) {
- // Fetch current server id
- var t = pgBrowser.tree,
- i = this.settings['pg_item'] || t.selected(),
- d = this.settings['pg_item_data'] || (
- i && i.length == 1 ? t.itemData(i) : undefined
- ),
- node = this.settings['pg_node'] || (
- d && pgBrowser.Nodes[d._type]
- );
-
- if (e.button.element.name == 'dialog_help' || e.button.element.name == 'object_help') {
- e.cancel = true;
- pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
- node, i, e.button.element.getAttribute('label'));
- return;
- }
-
- if (e.button['data-btn-name'] === 'restore') {
- if (!d)
- return;
-
- var info = node.getTreeNodeHierarchy.apply(node, [i]),
- m = this.view.model;
- // Set current node info into model
- m.set('database', info.database._label);
- if (!m.get('custom')) {
- switch (d._type) {
- case 'schema':
- m.set('schemas', [d._label]);
- break;
- case 'table':
- m.set('schemas', [info.schema._label]);
- m.set('tables', [d._label]);
- break;
- case 'function':
- m.set('schemas', [info.schema._label]);
- m.set('functions', [d._label]);
- break;
- case 'index':
- m.set('schemas', [info.schema._label]);
- m.set('indexes', [d._label]);
- break;
- case 'trigger':
- m.set('schemas', [info.schema._label]);
- m.set('triggers', [d._label]);
- break;
- case 'trigger_func':
- m.set('schemas', [info.schema._label]);
- m.set('trigger_funcs', [d._label]);
- break;
- }
- } else {
- // TODO::
- // When we will implement the object selection in the
- // import dialog, we will need to select the objects from
- // the tree selection tab.
- }
-
- var self = this,
- baseUrl = url_for('restore.create_job', {
- 'sid': info.server._id,
- }),
- args = this.view.model.toJSON();
-
- $.ajax({
- url: baseUrl,
- method: 'POST',
- data: {
- 'data': JSON.stringify(args),
- },
- success: function(res) {
- if (res.success) {
- alertify.success(
- gettext('Restore job created.'), 5
- );
- pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
- } else {
- console.warn(res);
- }
- },
- error: function(xhr) {
- try {
- var err = JSON.parse(xhr.responseText);
- alertify.alert(
- gettext('Restore failed.'),
- err.errormsg
- );
- } catch (e) {
- console.warn(e.stack || e);
- }
- },
- });
- }
- },
- };
- });
- }
-
- alertify.pg_restore(title, item, data, node).resizeTo('65%', '60%');
+ let dialog = new restoreDialog.RestoreDialog(
+ pgBrowser, $, alertify, RestoreObjectModel
+ );
+ dialog.draw(action, treeItem);
},
};
return pgBrowser.Restore;
diff --git a/web/pgadmin/tools/restore/static/js/restore.js.rej b/web/pgadmin/tools/restore/static/js/restore.js.rej
new file mode 100644
index 00000000..43577f42
--- /dev/null
+++ b/web/pgadmin/tools/restore/static/js/restore.js.rej
@@ -0,0 +1,322 @@
+diff a/web/pgadmin/tools/restore/static/js/restore.js b/web/pgadmin/tools/restore/static/js/restore.js (rejected hunks)
+@@ -391,318 +344,8 @@ commonUtils
+ },
+ // Callback to draw Backup Dialog for objects
+ restore_objects: function(action, treeItem) {
+-
+- var i = treeItem || pgBrowser.tree.selected(),
+- server_data = null;
+-
+- while (i) {
+- var node_data = pgBrowser.tree.itemData(i);
+- if (node_data._type == 'server') {
+- server_data = node_data;
+- break;
+- }
+-
+- if (pgBrowser.tree.hasParent(i)) {
+- i = $(pgBrowser.tree.parent(i));
+- } else {
+- alertify.alert(
+- gettext('Restore Error'),
+- gettext('Please select server or child node from tree.')
+- );
+- break;
+- }
+- }
+-
+- if (!server_data) {
+- return;
+- }
+-
+- var module = 'paths',
+- preference_name = 'pg_bin_dir',
+- msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
+-
+- if ((server_data.type && server_data.type == 'ppas') ||
+- server_data.server_type == 'ppas') {
+- preference_name = 'ppas_bin_dir';
+- msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
+- }
+-
+- var preference = pgBrowser.get_preference(module, preference_name);
+-
+- if (preference) {
+- if (!preference.value) {
+- alertify.alert(gettext('Configuration required'), msg);
+- return;
+- }
+- } else {
+- alertify.alert(
+- gettext('Restore Error'),
+- S(gettext('Failed to load preference %s of module %s')).sprintf(preference_name, module).value()
+- );
+- return;
+- }
+-
+- var title = S(gettext('Restore (%s: %s)')),
+- tree = pgBrowser.tree,
+- item = treeItem || tree.selected(),
+- data = item && item.length == 1 && tree.itemData(item),
+- node = data && data._type && pgBrowser.Nodes[data._type];
+-
+- if (!node)
+- return;
+-
+- var treeInfo = node.getTreeNodeHierarchy.apply(node, [item]);
+-
+- if (treeInfo.database._label.indexOf('=') >= 0) {
+- alertify.alert(
+- gettext('Restore error'),
+- gettext('Restore job creation failed. '+
+- 'Databases with = symbols in the name cannot be restored using this utility.')
+- );
+- return;
+- }
+-
+- title = title.sprintf(node.label, data.label).value();
+-
+- if (!alertify.pg_restore) {
+- // Create Dialog title on the fly with node details
+- alertify.dialog('pg_restore', function factory() {
+- return {
+- main: function(title, item, data, node) {
+- this.set('title', title);
+- this.setting('pg_node', node);
+- this.setting('pg_item', item);
+- this.setting('pg_item_data', data);
+- },
+- build: function() {
+- alertify.pgDialogBuild.apply(this);
+- },
+- setup: function() {
+- return {
+- buttons: [{
+- text: '',
+- className: 'btn btn-default pull-left fa fa-lg fa-info',
+- attrs: {
+- name: 'object_help',
+- type: 'button',
+- url: 'backup.html',
+- label: gettext('Restore'),
+- },
+- }, {
+- text: '',
+- key: 112,
+- className: 'btn btn-default pull-left fa fa-lg fa-question',
+- attrs: {
+- name: 'dialog_help',
+- type: 'button',
+- label: gettext('Restore'),
+- url: url_for('help.static', {
+- 'filename': 'restore_dialog.html',
+- }),
+- },
+- }, {
+- text: gettext('Restore'),
+- key: 13,
+- className: 'btn btn-primary fa fa-upload pg-alertify-button',
+- restore: true,
+- 'data-btn-name': 'restore',
+- }, {
+- text: gettext('Cancel'),
+- key: 27,
+- className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
+- restore: false,
+- 'data-btn-name': 'cancel',
+- }],
+- // Set options for dialog
+- options: {
+- title: title,
+- //disable both padding and overflow control.
+- padding: !1,
+- overflow: !1,
+- model: 0,
+- resizable: true,
+- maximizable: true,
+- pinnable: false,
+- closableByDimmer: false,
+- modal: false,
+- },
+- };
+- },
+- hooks: {
+- // triggered when the dialog is closed
+- onclose: function() {
+- if (this.view) {
+- this.view.remove({
+- data: true,
+- internal: true,
+- silent: true,
+- });
+- }
+- },
+- },
+- settings: {
+- pg_node: null,
+- pg_item: null,
+- pg_item_data: null,
+- },
+- prepare: function() {
+-
+- var self = this;
+- // Disable Backup button until user provides Filename
+- this.__internal.buttons[2].element.disabled = true;
+- var $container = $('<div class=\'restore_dialog\'></div>');
+- var t = pgBrowser.tree,
+- i = t.selected(),
+- d = i && i.length == 1 ? t.itemData(i) : undefined,
+- node = d && pgBrowser.Nodes[d._type];
+-
+- if (!d)
+- return;
+-
+- var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
+-
+- var newModel = new RestoreObjectModel({
+- node_data: node,
+- }, {
+- node_info: treeInfo,
+- }),
+- fields = Backform.generateViewSchema(
+- treeInfo, newModel, 'create', node, treeInfo.server, true
+- );
+-
+- var view = this.view = new Backform.Dialog({
+- el: $container,
+- model: newModel,
+- schema: fields,
+- });
+-
+- $(this.elements.body.childNodes[0]).addClass(
+- 'alertify_tools_dialog_properties obj_properties'
+- );
+-
+- view.render();
+-
+- this.elements.content.appendChild($container.get(0));
+-
+- view.$el.attr('tabindex', -1);
+- // var dialogTabNavigator = pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
+- pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
+- var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
+- commonUtils.findAndSetFocus(container);
+-
+- // Listen to model & if filename is provided then enable Backup button
+- this.view.model.on('change', function() {
+- if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
+- this.errorModel.clear();
+- self.__internal.buttons[2].element.disabled = false;
+- } else {
+- self.__internal.buttons[2].element.disabled = true;
+- this.errorModel.set('file', gettext('Please provide filename'));
+- }
+- });
+-
+- },
+- // Callback functions when click on the buttons of the Alertify dialogs
+- callback: function(e) {
+- // Fetch current server id
+- var t = pgBrowser.tree,
+- i = this.settings['pg_item'] || t.selected(),
+- d = this.settings['pg_item_data'] || (
+- i && i.length == 1 ? t.itemData(i) : undefined
+- ),
+- node = this.settings['pg_node'] || (
+- d && pgBrowser.Nodes[d._type]
+- );
+-
+- if (e.button.element.name == 'dialog_help' || e.button.element.name == 'object_help') {
+- e.cancel = true;
+- pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
+- node, i, e.button.element.getAttribute('label'));
+- return;
+- }
+-
+- if (e.button['data-btn-name'] === 'restore') {
+- if (!d)
+- return;
+-
+- var info = node.getTreeNodeHierarchy.apply(node, [i]),
+- m = this.view.model;
+- // Set current node info into model
+- m.set('database', info.database._label);
+- if (!m.get('custom')) {
+- switch (d._type) {
+- case 'schema':
+- m.set('schemas', [d._label]);
+- break;
+- case 'table':
+- m.set('schemas', [info.schema._label]);
+- m.set('tables', [d._label]);
+- break;
+- case 'function':
+- m.set('schemas', [info.schema._label]);
+- m.set('functions', [d._label]);
+- break;
+- case 'index':
+- m.set('schemas', [info.schema._label]);
+- m.set('indexes', [d._label]);
+- break;
+- case 'trigger':
+- m.set('schemas', [info.schema._label]);
+- m.set('triggers', [d._label]);
+- break;
+- case 'trigger_func':
+- m.set('schemas', [info.schema._label]);
+- m.set('trigger_funcs', [d._label]);
+- break;
+- }
+- } else {
+- // TODO::
+- // When we will implement the object selection in the
+- // import dialog, we will need to select the objects from
+- // the tree selection tab.
+- }
+-
+- var self = this,
+- baseUrl = url_for('restore.create_job', {
+- 'sid': info.server._id,
+- }),
+- args = this.view.model.toJSON();
+-
+- $.ajax({
+- url: baseUrl,
+- method: 'POST',
+- data: {
+- 'data': JSON.stringify(args),
+- },
+- success: function(res) {
+- if (res.success) {
+- alertify.success(
+- gettext('Restore job created.'), 5
+- );
+- pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
+- } else {
+- console.warn(res);
+- }
+- },
+- error: function(xhr) {
+- try {
+- var err = $.parseJSON(xhr.responseText);
+- alertify.alert(
+- gettext('Restore failed.'),
+- err.errormsg
+- );
+- } catch (e) {
+- console.warn(e.stack || e);
+- }
+- },
+- });
+- }
+- },
+- };
+- });
+- }
+-
+- alertify.pg_restore(title, item, data, node).resizeTo('65%', '60%');
++ let dialog = new restoreDialog.RestoreDialog(pgBrowser, $, alertify, RestoreObjectModel);
++ dialog.draw(action, treeItem);
+ },
+ };
+ return pgBrowser.Restore;
diff --git a/web/pgadmin/tools/restore/static/js/restore_dialog.js b/web/pgadmin/tools/restore/static/js/restore_dialog.js
new file mode 100644
index 00000000..4884d901
--- /dev/null
+++ b/web/pgadmin/tools/restore/static/js/restore_dialog.js
@@ -0,0 +1,57 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from '../../../../static/js/gettext';
+import {sprintf} from 'sprintf-js';
+import Backform from '../../../../static/js/backform.pgadmin';
+import {Dialog} from '../../../../static/js/alertify/dialog';
+
+export class RestoreDialog extends Dialog {
+ constructor(pgBrowser, $, alertify, RestoreModel, backform = Backform) {
+ super('Restore Error',
+ '<div class=\'restore_dialog\'></div>',
+ pgBrowser, $, alertify, RestoreModel, backform);
+ }
+
+ draw(action, aciTreeItem) {
+
+ const serverInformation = this.retrieveAncestorOfTypeServer(aciTreeItem);
+
+ if (!serverInformation) {
+ return;
+ }
+
+ if (!this.hasBinariesConfiguration(serverInformation)) {
+ return;
+ }
+
+ if (!this.canExecuteOnCurrentDatabase(aciTreeItem)) {
+ return;
+ }
+
+ let aciTreeItem1 = aciTreeItem || this.pgBrowser.treeMenu.selected();
+ let item = this.pgBrowser.treeMenu.findNodeByDomElement(aciTreeItem1);
+ const data = item.getData();
+ const node = this.pgBrowser.Nodes[data._type];
+
+ if (!node)
+ return;
+
+ let title = sprintf(gettext('Restore (%s: %s)'), node.label, data.label);
+
+ this.createOrGetDialog(title, 'restore');
+
+ this.alertify.pg_restore(title, aciTreeItem1, data, node).resizeTo('65%', '60%');
+ }
+
+ dialogName() {
+ return 'pg_restore';
+ }
+}
+
diff --git a/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js b/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js
new file mode 100644
index 00000000..845da7a3
--- /dev/null
+++ b/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js
@@ -0,0 +1,255 @@
+import {getTreeNodeHierarchyFromElement} from '../../../../static/js/tree/pgadmin_tree_node';
+import axios from 'axios/index';
+import _ from 'underscore';
+import gettext from '../../../../static/js/gettext';
+import url_for from '../../../../static/js/url_for';
+import {DialogWrapper} from '../../../../static/js/alertify/dialog_wrapper';
+
+export class RestoreDialogWrapper extends DialogWrapper {
+ constructor(dialogContainerSelector, dialogTitle, typeOfDialog,
+ jquery, pgBrowser, alertify, dialogModel, backform) {
+ super(dialogContainerSelector, dialogTitle, jquery,
+ pgBrowser, alertify, dialogModel, backform);
+ }
+
+ main(title, item, data, node) {
+ this.set('title', title);
+ this.setting('pg_node', node);
+ this.setting('pg_item', item);
+ this.setting('pg_item_data', data);
+ }
+
+ setup() {
+ return {
+ buttons: [{
+ text: '',
+ className: 'btn btn-default pull-left fa fa-lg fa-info',
+ attrs: {
+ name: 'object_help',
+ type: 'button',
+ url: 'backup.html',
+ label: gettext('Restore'),
+ },
+ }, {
+ text: '',
+ key: 112,
+ className: 'btn btn-default pull-left fa fa-lg fa-question',
+ attrs: {
+ name: 'dialog_help',
+ type: 'button',
+ label: gettext('Restore'),
+ url: url_for('help.static', {
+ 'filename': 'restore_dialog.html',
+ }),
+ },
+ }, {
+ text: gettext('Restore'),
+ key: 13,
+ className: 'btn btn-primary fa fa-upload pg-alertify-button',
+ restore: true,
+ 'data-btn-name': 'restore',
+ }, {
+ text: gettext('Cancel'),
+ key: 27,
+ className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button',
+ restore: false,
+ 'data-btn-name': 'cancel',
+ }],
+ // Set options for dialog
+ options: {
+ title: this.dialogTitle,
+ //disable both padding and overflow control.
+ padding: !1,
+ overflow: !1,
+ model: 0,
+ resizable: true,
+ maximizable: true,
+ pinnable: false,
+ closableByDimmer: false,
+ modal: false,
+ },
+ };
+ }
+
+ prepare() {
+ this.disableRestoreButton();
+
+ const $container = this.jquery(this.dialogContainerSelector);
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = this.getSelectedNodeData(selectedTreeNode);
+ if (!selectedTreeNodeData) {
+ return;
+ }
+
+ const node = this.pgBrowser.Nodes[selectedTreeNodeData._type];
+
+ const treeInfo = getTreeNodeHierarchyFromElement(this.pgBrowser, selectedTreeNode);
+ const dialog = this.createDialog(node, treeInfo, $container);
+ this.addAlertifyClassToRestoreNodeChildNodes();
+ dialog.render();
+
+ this.elements.content.appendChild($container.get(0));
+
+ this.focusOnDialog(dialog);
+ this.setListenersForFilenameChanges();
+ }
+
+ callback(event) {
+ const selectedTreeNode = this.getSelectedNode();
+ const selectedTreeNodeData = this.getSelectedNodeData(selectedTreeNode);
+ const node = selectedTreeNodeData && this.pgBrowser.Nodes[selectedTreeNodeData._type];
+
+ if (this.wasHelpButtonPressed(event)) {
+ event.cancel = true;
+ this.pgBrowser.showHelp(
+ event.button.element.name,
+ event.button.element.getAttribute('url'),
+ node,
+ selectedTreeNode,
+ event.button.element.getAttribute('label')
+ );
+ return;
+ }
+
+ if (this.wasRestoreButtonPressed(event)) {
+
+ if (!selectedTreeNodeData)
+ return;
+
+ const serverIdentifier = this.retrieveServerIdentifier(node, selectedTreeNode);
+
+ const dialogWrapper = this;
+ let urlShortcut = 'restore.create_job';
+
+ const baseUrl = url_for(urlShortcut, {
+ 'sid': serverIdentifier,
+ });
+
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.pgBrowser,
+ selectedTreeNode
+ );
+
+ this.setExtraParameters(selectedTreeNode, treeInfo);
+
+ let service = axios.create({});
+ service.post(
+ baseUrl,
+ this.view.model.toJSON()
+ ).then(function () {
+ dialogWrapper.alertify.success(gettext('Restore job created.'), 5);
+ dialogWrapper.pgBrowser.Events.trigger('pgadmin-bgprocess:created', dialogWrapper);
+ }).catch(function (error) {
+ try {
+ const err = error.response.data;
+ dialogWrapper.alertify.alert(
+ gettext('Restore job failed.'),
+ err.errormsg
+ );
+ } catch (e) {
+ console.warn(e.stack || e);
+ }
+ });
+ }
+ }
+
+ addAlertifyClassToRestoreNodeChildNodes() {
+ this.jquery(this.elements.body.childNodes[0]).addClass(
+ 'alertify_tools_dialog_properties obj_properties'
+ );
+ }
+
+ getSelectedNode() {
+ const tree = this.pgBrowser.treeMenu;
+ const selectedNode = tree.selected();
+ if (selectedNode) {
+ return tree.findNodeByDomElement(selectedNode);
+ } else {
+ return undefined;
+ }
+ }
+
+ disableRestoreButton() {
+ this.__internal.buttons[2].element.disabled = true;
+ }
+
+ enableRestoreButton() {
+ this.__internal.buttons[2].element.disabled = false;
+ }
+
+ createDialog(node, treeInfo, $container) {
+ const newModel = new this.dialogModel({
+ node_data: node,
+ }, {
+ node_info: treeInfo,
+ });
+ const fields = this.backform.generateViewSchema(
+ treeInfo, newModel, 'create', node, treeInfo.server, true
+ );
+
+ return this.view = new this.backform.Dialog({
+ el: $container,
+ model: newModel,
+ schema: fields,
+ });
+ }
+
+ retrieveServerIdentifier(node, selectedTreeNode) {
+ const treeInfo = getTreeNodeHierarchyFromElement(
+ this.pgBrowser,
+ selectedTreeNode
+ );
+ return treeInfo.server._id;
+ }
+
+ setListenersForFilenameChanges() {
+ const self = this;
+
+ this.view.model.on('change', function () {
+ if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
+ this.errorModel.clear();
+ self.enableRestoreButton();
+ } else {
+ self.disableRestoreButton();
+ this.errorModel.set('file', gettext('Please provide a filename'));
+ }
+ });
+ }
+
+ setExtraParameters(selectedTreeNode, treeInfo) {
+ this.view.model.set('database', treeInfo.database._label);
+ if (!this.view.model.get('custom')) {
+ const nodeData = selectedTreeNode.getData();
+
+ switch (nodeData._type) {
+ case 'schema':
+ this.view.model.set('schemas', [nodeData._label]);
+ break;
+ case 'table':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('tables', [nodeData._label]);
+ break;
+ case 'function':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('functions', [nodeData._label]);
+ break;
+ case 'index':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('indexes', [nodeData._label]);
+ break;
+ case 'trigger':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('triggers', [nodeData._label]);
+ break;
+ case 'trigger_func':
+ this.view.model.set('schemas', [treeInfo.schema._label]);
+ this.view.model.set('trigger_funcs', [nodeData._label]);
+ break;
+ }
+ }
+ }
+
+ wasRestoreButtonPressed(event) {
+ return event.button['data-btn-name'] === 'restore';
+ }
+}
diff --git a/web/regression/javascript/backup/backup_dialog_spec.js b/web/regression/javascript/backup/backup_dialog_spec.js
new file mode 100644
index 00000000..2655059d
--- /dev/null
+++ b/web/regression/javascript/backup/backup_dialog_spec.js
@@ -0,0 +1,205 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {BackupDialog} from '../../../pgadmin/tools/backup/static/js/backup_dialog';
+import {TreeFake} from '../tree/tree_fake';
+
+const context = describe;
+
+describe('BackupDialog', () => {
+ let backupDialog;
+ let pgBrowser;
+ let jquerySpy;
+ let alertifySpy;
+ let backupModelSpy;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: {
+ hasId: true,
+ label: 'server',
+ getTreeNodeHierarchy: jasmine.createSpy('server.getTreeNodeHierarchy'),
+ },
+ database: {
+ hasId: true,
+ label: 'database',
+ getTreeNodeHierarchy: jasmine.createSpy('db.getTreeNodeHierarchy'),
+ },
+ schema: {
+ hasId: true,
+ label: 'schema',
+ getTreeNodeHierarchy: jasmine.createSpy('db.getTreeNodeHierarchy'),
+ },
+ },
+ };
+ pgBrowser.Nodes.server.hasId = true;
+ pgBrowser.Nodes.database.hasId = true;
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ backupModelSpy = jasmine.createSpy('backupModelSpy');
+
+ const hierarchy = {
+ children: [
+ {
+ id: 'root',
+ children: [
+ {
+ id: 'serverTreeNode',
+ data: {
+ _id: 10,
+ _type: 'server',
+ },
+ children: [
+ {
+ id: 'some_database',
+ data: {
+ _type: 'database',
+ _id: 11,
+ label: 'some_database',
+ _label: 'some_database_label',
+ },
+ }, {
+ id: 'database_with_equal_in_name',
+ data: {
+ _type: 'database',
+ label: 'some_database',
+ _label: '=some_database_label',
+ },
+ },
+ ],
+ },
+ {
+ id: 'ppasServer',
+ data: {
+ _type: 'server',
+ server_type: 'ppas',
+ children: [
+ {id: 'someNodeUnderneathPPASServer'},
+ ],
+ },
+ },
+ ],
+ },
+ ],
+ };
+
+ pgBrowser.treeMenu = TreeFake.build(hierarchy);
+ });
+
+ describe('#draw', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ alertifySpy['backup_objects'] = jasmine.createSpy('backup_objects');
+ backupDialog = new BackupDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ backupModelSpy
+ );
+
+ pgBrowser.get_preference = jasmine.createSpy('get_preferences');
+ });
+
+ context('there are no ancestors of the type server', () => {
+ it('does not create a dialog', () => {
+ pgBrowser.treeMenu.selectNode([{id: 'root'}]);
+ backupDialog.draw(null, null, null);
+ expect(alertifySpy['backup_objects']).not.toHaveBeenCalled();
+ });
+
+ it('display an alert with a Backup Error', () => {
+ backupDialog.draw(null, [{id: 'root'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Please select server or child node from the browser tree.'
+ );
+ });
+ });
+
+ context('there is an ancestor of the type server', () => {
+ context('no preference can be found', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue(undefined);
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [{id: 'some_database'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference pg_bin_dir of module paths'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [{id: 'ppasServer'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference ppas_bin_dir of module paths'
+ );
+ });
+ });
+ });
+
+ context('preference can be found', () => {
+ context('binary folder is not configured', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue({});
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [{id: 'serverTreeNode'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the PostgreSQL Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [{id: 'ppasServer'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the EDB Advanced Server Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+ });
+
+ context('binary folder is configured', () => {
+ let backupDialogResizeToSpy;
+ beforeEach(() => {
+ backupDialogResizeToSpy = jasmine.createSpyObj('backupDialogResizeToSpy', ['resizeTo']);
+ alertifySpy['backup_objects'].and
+ .returnValue(backupDialogResizeToSpy);
+ pgBrowser.get_preference.and.returnValue({value: '/some/path'});
+ });
+
+ it('displays the dialog', () => {
+ backupDialog.draw(null, [{id: 'serverTreeNode'}], null);
+ expect(alertifySpy['backup_objects']).toHaveBeenCalledWith(true);
+ expect(backupDialogResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
+ });
+
+ context('database label contain "="', () => {
+ it('should create alert dialog with backup error', () => {
+ backupDialog.draw(null, [{id: 'database_with_equal_in_name'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith('Backup Error',
+ 'Databases with = symbols in the name cannot be backed up or restored using this utility.');
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/backup/backup_dialog_wrapper_spec.js b/web/regression/javascript/backup/backup_dialog_wrapper_spec.js
new file mode 100644
index 00000000..58705318
--- /dev/null
+++ b/web/regression/javascript/backup/backup_dialog_wrapper_spec.js
@@ -0,0 +1,675 @@
+import {TreeFake} from '../tree/tree_fake';
+import {BackupDialogWrapper} from '../../../pgadmin/tools/backup/static/js/backup_dialog_wrapper';
+import axios from 'axios/index';
+import MockAdapter from 'axios-mock-adapter';
+import {FakeModel} from '../fake_model';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+let context = describe;
+
+describe('BackupDialogWrapper', () => {
+ let jquerySpy;
+ let pgBrowser;
+ let alertifySpy;
+ let dialogModelKlassSpy;
+ let backform;
+ let generatedBackupModel;
+ let backupDialogWrapper;
+ let noDataNode;
+ let serverTreeNode;
+ let databaseTreeNode;
+ let viewSchema;
+ let backupJQueryContainerSpy;
+ let backupNodeChildNodeSpy;
+ let backupNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: {
+ hasId: true,
+ getTreeNodeHierarchy: jasmine.createSpy('getTreeNodeHierarchy'),
+ },
+ database: {
+ hasId: true,
+ },
+ },
+ keyboardNavigation: jasmine.createSpyObj('keyboardNavigation', ['getDialogTabNavigator']),
+ };
+ noDataNode = pgBrowser.treeMenu.addNewNode('level1.1', undefined, [{id: 'level1'}]);
+ serverTreeNode = pgBrowser.treeMenu.addNewNode('level2.1', {
+ _type: 'server',
+ _id: 10,
+ label: 'some-tree-label',
+ }, [{id: 'level2.1'}]);
+ databaseTreeNode = new TreeNode('database-tree-node', {
+ _type: 'database',
+ _label: 'some-database-label',
+ }, [{id: 'database-tree-node'}]);
+ pgBrowser.treeMenu.addChild(serverTreeNode, databaseTreeNode);
+
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ backupNode = {
+ __internal: {
+ buttons: [{}, {}, {
+ element: {
+ disabled: false,
+ },
+ }],
+ },
+ elements: {
+ body: {
+ childNodes: [
+ {},
+ ],
+ },
+ content: jasmine.createSpyObj('content', ['appendChild', 'attr']),
+ },
+ };
+
+ backupJQueryContainerSpy = jasmine.createSpyObj('backupJQueryContainer', ['get', 'attr']);
+ backupJQueryContainerSpy.get.and.returnValue(backupJQueryContainerSpy);
+
+ generatedBackupModel = {};
+ dialogModelKlassSpy = jasmine.createSpy('dialogModelKlass');
+ dialogModelKlassSpy.and.returnValue(generatedBackupModel);
+
+ viewSchema = {};
+ backform = jasmine.createSpyObj('backform', ['generateViewSchema', 'Dialog']);
+ backform.generateViewSchema.and.returnValue(viewSchema);
+
+ backupNodeChildNodeSpy = jasmine.createSpyObj('something', ['addClass']);
+ jquerySpy.and.callFake((selector) => {
+ if (selector === '<div class=\'backup_dialog\'></div>') {
+ return backupJQueryContainerSpy;
+ } else if (selector === backupNode.elements.body.childNodes[0]) {
+ return backupNodeChildNodeSpy;
+ }
+ });
+
+ });
+
+ describe('#prepare', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+ backupDialogWrapper = Object.assign(backupDialogWrapper, backupNode);
+ });
+
+ context('no tree element is selected', () => {
+ it('does not create a backform dialog', () => {
+ backupDialogWrapper.prepare();
+ expect(backform.Dialog).not.toHaveBeenCalled();
+ });
+
+ it('disables the button "submit button" until a filename is selected', () => {
+ backupDialogWrapper.prepare();
+ expect(backupDialogWrapper.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('selected tree node has no data', () => {
+ beforeEach(() => {
+ pgBrowser.treeMenu.selectNode(noDataNode.domNode);
+ });
+
+ it('does not create a backform dialog', () => {
+ backupDialogWrapper.prepare();
+ expect(backform.Dialog).not.toHaveBeenCalled();
+ });
+
+ it('disables the button "submit button" until a filename is selected', () => {
+ backupDialogWrapper.prepare();
+ expect(backupDialogWrapper.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('tree element is selected', () => {
+ let treeHierarchyInformation;
+ let dialogSpy;
+
+ beforeEach(() => {
+ treeHierarchyInformation = {
+ server: {
+ _type: 'server',
+ _id: 10,
+ priority: 0,
+ label: 'some-tree-label',
+ },
+ };
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ pgBrowser.Nodes['server'].getTreeNodeHierarchy.and
+ .returnValue(treeHierarchyInformation);
+ dialogSpy = jasmine.createSpyObj('newView', ['render']);
+ dialogSpy.$el = jasmine.createSpyObj('$el', ['find', 'attr']);
+ dialogSpy.model = jasmine.createSpyObj('model', ['on']);
+ dialogSpy.$el.find.and.returnValue([]);
+
+ backform.Dialog.and.returnValue(dialogSpy);
+ });
+
+ it('creates a backform dialog and displays it', () => {
+ backupDialogWrapper.prepare();
+ expect(backform.Dialog).toHaveBeenCalledWith({
+ el: backupJQueryContainerSpy,
+ model: generatedBackupModel,
+ schema: viewSchema,
+ });
+
+ expect(dialogSpy.render).toHaveBeenCalled();
+ });
+
+
+ it('add alertify classes to restore node childnode', () => {
+ backupDialogWrapper.prepare();
+ expect(backupNodeChildNodeSpy.addClass)
+ .toHaveBeenCalledWith('alertify_tools_dialog_properties obj_properties');
+ });
+
+ it('disables the button submit button until a filename is selected', () => {
+ backupDialogWrapper.prepare();
+ expect(backupNode.__internal.buttons[2].element.disabled).toBe(true);
+ });
+
+ it('generates a new backup model', () => {
+ backupDialogWrapper.prepare();
+ expect(dialogModelKlassSpy).toHaveBeenCalledWith(
+ {type: 'backup'},
+ {node_info: treeHierarchyInformation}
+ );
+ });
+
+ it('add the new dialog to the backup node HTML', () => {
+ backupDialogWrapper.prepare();
+ expect(backupNode.elements.content.appendChild).toHaveBeenCalledWith(backupJQueryContainerSpy);
+ });
+ });
+ });
+
+ describe('onButtonClicked', () => {
+ let networkMock;
+ beforeEach(() => {
+ networkMock = new MockAdapter(axios);
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ backupDialogWrapper = Object.assign(backupDialogWrapper, backupNode);
+ });
+
+ afterEach(() => {
+ networkMock.restore();
+ });
+
+ context('dialog help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.*/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ pgBrowser.showHelp = jasmine.createSpy('showHelp');
+
+ const event = {
+ button: {
+ element: {
+ name: 'dialog_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ },
+ },
+ },
+ };
+ backupDialogWrapper.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'dialog_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ );
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('object help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.*/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ pgBrowser.showHelp = jasmine.createSpy('showHelp');
+
+ const event = {
+ button: {
+ element: {
+ name: 'object_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ },
+ },
+ },
+ };
+ backupDialogWrapper.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'object_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ );
+ });
+
+ it('does not start the backup', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('backup button was pressed', () => {
+ context('no tree node is selected', () => {
+ it('does not start the backup', () => {
+ let networkCalled = false;
+ networkMock.onAny(/.*/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ let event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+
+ backupDialogWrapper.callback(event);
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('tree node has no data', () => {
+ it('does not start the backup', () => {
+ pgBrowser.treeMenu.selectNode(noDataNode.domNode);
+
+ let networkCalled = false;
+ networkMock.onAny(/.*/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ let event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+
+ backupDialogWrapper.callback(event);
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('tree node has data', () => {
+ context('when dialog type is global', () => {
+ let event;
+ beforeEach(() => {
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+
+ backupDialogWrapper.view = {
+ model: new FakeModel(),
+ };
+
+ event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ });
+
+ context('when the backup job is created successfully', () => {
+ let dataSentToServer;
+ beforeEach(() => {
+ pgBrowser.Events = jasmine.createSpyObj('Events', ['trigger']);
+ alertifySpy.success = jasmine.createSpy('success');
+
+ networkMock.onPost('/backup/job/10').reply((request) => {
+ dataSentToServer = request.data;
+ return [200, {}];
+ });
+ });
+
+ it('creates a success alert box', (done) => {
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.success).toHaveBeenCalledWith(
+ 'Backup job created.',
+ 5
+ );
+ done();
+ }, 0);
+ });
+
+ it('trigger an event to background process', (done) => {
+ backupDialogWrapper.callback(event);
+
+ setTimeout(() => {
+ expect(pgBrowser.Events.trigger).toHaveBeenCalledWith(
+ 'pgadmin-bgprocess:created',
+ backupDialogWrapper
+ );
+ done();
+ }, 0);
+ });
+
+ it('send the correct paramenters to the backend', (done) => {
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(JSON.parse(dataSentToServer)).toEqual(
+ {}
+ );
+ done();
+ }, 0);
+ });
+ });
+
+ context('when creating backup job fails', () => {
+ it('creates an alert box', (done) => {
+ alertifySpy.alert = jasmine.createSpy('alert');
+ networkMock.onPost('/backup/job/10').reply(() => {
+ return [400, {
+ errormsg: 'some-error-message',
+ }];
+ });
+
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup job failed.',
+ 'some-error-message'
+ );
+ done();
+ }, 0);
+
+ });
+ });
+ });
+
+ context('when dialog type is object', () => {
+ let event;
+ beforeEach(() => {
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup_objects',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ pgBrowser.treeMenu.selectNode(databaseTreeNode.domNode);
+
+ backupDialogWrapper.view = {
+ model: new FakeModel(),
+ };
+
+ event = {
+ button: {
+ 'data-btn-name': 'backup',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ });
+
+ context('when the backup job is created successfully', () => {
+ let dataSentToServer;
+ beforeEach(() => {
+ pgBrowser.Events = jasmine.createSpyObj('Events', ['trigger']);
+ alertifySpy.success = jasmine.createSpy('success');
+
+ networkMock.onPost('/backup/job/10/object').reply((request) => {
+ dataSentToServer = request.data;
+ return [200, {}];
+ });
+ });
+
+ it('creates a success alert box', (done) => {
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.success).toHaveBeenCalledWith(
+ 'Backup job created.',
+ 5
+ );
+ done();
+ }, 0);
+ });
+
+ it('trigger an event to background process', (done) => {
+ backupDialogWrapper.callback(event);
+
+ setTimeout(() => {
+ expect(pgBrowser.Events.trigger).toHaveBeenCalledWith(
+ 'pgadmin-bgprocess:created',
+ backupDialogWrapper
+ );
+ done();
+ }, 0);
+ });
+
+ it('send the correct parameters to the backend', (done) => {
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(JSON.parse(dataSentToServer)).toEqual(
+ {database: 'some-database-label'}
+ );
+ done();
+ }, 0);
+ });
+ });
+
+ context('when creating backup job fails', () => {
+ it('creates an alert box', (done) => {
+ alertifySpy.alert = jasmine.createSpy('alert');
+ networkMock.onPost('/backup/job/10/object').reply(() => {
+ return [400, {
+ errormsg: 'some-error-message',
+ }];
+ });
+
+ backupDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup job failed.',
+ 'some-error-message'
+ );
+ done();
+ }, 0);
+ });
+ });
+ });
+ });
+ });
+ });
+
+ describe('#setExtraParameters', () => {
+ let selectedTreeNode;
+ let treeInfo;
+ let model;
+
+ context('when dialog type is global', () => {
+ beforeEach(() => {
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ treeInfo = {};
+ model = new FakeModel();
+ backupDialogWrapper.view = {
+ model: model,
+ };
+ });
+
+
+ it('sets nothing on the view model', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.toJSON()).toEqual({});
+ });
+ });
+
+ context('when dialog type is object', () => {
+ beforeEach(() => {
+ backupDialogWrapper = new BackupDialogWrapper(
+ '<div class=\'backup_dialog\'></div>',
+ 'backupDialogTitle',
+ 'backup_objects',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ treeInfo = {
+ database: {
+ _label: 'some-database-label',
+ },
+ schema: {
+ _label: 'some-treeinfo-label',
+ },
+ };
+
+ model = new FakeModel();
+ selectedTreeNode = new TreeNode('some-selected-node',
+ {_type: 'some-type', _label: 'some-selected-label'},
+ []);
+ backupDialogWrapper.view = {
+ model: model,
+ };
+ });
+
+ it('sets the database label on the model', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ 'database': 'some-database-label',
+ });
+ });
+
+ context('when the selected is a schema type', () => {
+ beforeEach(() => {
+ selectedTreeNode = new TreeNode('some-selected-node',
+ {_type: 'schema', _label: 'some-schema-label'},
+ []);
+ });
+
+ it('sets the schema label on the model', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ 'database': 'some-database-label',
+ 'schemas': ['some-schema-label'],
+ });
+ });
+ });
+
+ context('when the selected is a table type', () => {
+ beforeEach(() => {
+ selectedTreeNode = new TreeNode('some-selected-node',
+ {_type: 'table', _label: 'some-table-label'},
+ []);
+ });
+
+ it('sets the schema label on the model', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ 'database': 'some-database-label',
+ 'tables': [['some-treeinfo-label', 'some-table-label']],
+ });
+ });
+ });
+
+ context('when the model has no ratio value', () => {
+ beforeEach(() => {
+ model.set('ratio', '');
+ });
+
+ it('sets clears the ratio value', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.get('ratio')).toBeUndefined();
+ });
+ });
+
+ context('when the model has a valid ratio value', () => {
+ beforeEach(() => {
+ model.set('ratio', '0.25');
+ });
+
+ it('sets clears the ratio value', () => {
+ backupDialogWrapper.setExtraParameters(selectedTreeNode, treeInfo);
+ expect(model.get('ratio')).toEqual('0.25');
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/backup/global_server_backup_dialog_spec.js b/web/regression/javascript/backup/global_server_backup_dialog_spec.js
new file mode 100644
index 00000000..86df672e
--- /dev/null
+++ b/web/regression/javascript/backup/global_server_backup_dialog_spec.js
@@ -0,0 +1,168 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {BackupDialog} from '../../../pgadmin/tools/backup/static/js/backup_dialog';
+import {TreeFake} from '../tree/tree_fake';
+
+const context = describe;
+
+describe('GlobalServerBackupDialog', () => {
+ let backupDialog;
+ let pgBrowser;
+ let jquerySpy;
+ let alertifySpy;
+ let backupModelSpy;
+
+
+ let rootNode;
+ let serverTreeNode;
+ let ppasServerTreeNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: jasmine.createSpyObj('Node[server]', ['getTreeNodeHierarchy']),
+ },
+ };
+ pgBrowser.Nodes.server.hasId = true;
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ backupModelSpy = jasmine.createSpy('backupModelSpy');
+
+ rootNode = pgBrowser.treeMenu.addNewNode('level1', {}, undefined, []);
+ serverTreeNode = pgBrowser.treeMenu.addNewNode('level1.1', {
+ _type: 'server',
+ _id: 10,
+ }, undefined, ['level1']);
+ ppasServerTreeNode = pgBrowser.treeMenu.addNewNode('level1.2', {
+ _type: 'server',
+ server_type: 'ppas',
+ }, undefined, ['level1']);
+ pgBrowser.treeMenu.addNewNode('level3', {}, undefined, ['level1', 'level1.2']);
+ pgBrowser.treeMenu.addNewNode('level3.1', undefined, undefined, ['level1', 'level1.2', 'level3']);
+ });
+
+ describe('#draw', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ alertifySpy['BackupDialog_globals'] = jasmine.createSpy('BackupDialog_globals');
+ alertifySpy['BackupDialog_server'] = jasmine.createSpy('BackupDialog_server');
+ backupDialog = new BackupDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ backupModelSpy
+ );
+
+ pgBrowser.get_preference = jasmine.createSpy('get_preferences');
+ });
+
+ context('there are no ancestors of the type server', () => {
+ it('does not create a dialog', () => {
+ pgBrowser.treeMenu.selectNode([{id: 'level1'}]);
+ backupDialog.draw(null, null, null);
+ expect(alertifySpy['BackupDialog_globals']).not.toHaveBeenCalled();
+ expect(alertifySpy['BackupDialog_server']).not.toHaveBeenCalled();
+ });
+
+ it('display an alert with a Backup Error', () => {
+ backupDialog.draw(null, [rootNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Please select server or child node from the browser tree.'
+ );
+ });
+ });
+
+ context('there is an ancestor of the type server', () => {
+ context('no preference can be found', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue(undefined);
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference pg_bin_dir of module paths'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Backup Error"', () => {
+ backupDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Backup Error',
+ 'Failed to load preference ppas_bin_dir of module paths'
+ );
+ });
+ });
+ });
+
+ context('preference can be found', () => {
+ context('binary folder is not configured', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue({});
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [serverTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the PostgreSQL Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ backupDialog.draw(null, [ppasServerTreeNode], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the EDB Advanced Server Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+ });
+
+ context('binary folder is configured', () => {
+ let globalResizeToSpy;
+ let serverResizeToSpy;
+ beforeEach(() => {
+ globalResizeToSpy = jasmine.createSpyObj('globals', ['resizeTo']);
+ alertifySpy['BackupDialog_globals'].and
+ .returnValue(globalResizeToSpy);
+ serverResizeToSpy = jasmine.createSpyObj('server', ['resizeTo']);
+ alertifySpy['BackupDialog_server'].and
+ .returnValue(serverResizeToSpy);
+ pgBrowser.get_preference.and.returnValue({value: '/some/path'});
+ });
+
+ context('dialog for global backup', () => {
+ it('displays the dialog', () => {
+ backupDialog.draw(null, [serverTreeNode], {globals: true});
+ expect(alertifySpy['BackupDialog_globals']).toHaveBeenCalledWith(true);
+ expect(globalResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
+ });
+ });
+
+ context('dialog for server backup', () => {
+ it('displays the dialog', () => {
+ backupDialog.draw(null, [serverTreeNode], {server: true});
+ expect(alertifySpy['BackupDialog_server']).toHaveBeenCalledWith(true);
+ expect(serverResizeToSpy.resizeTo).toHaveBeenCalledWith('60%', '50%');
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/backup/menu_utils_spec.js b/web/regression/javascript/backup/menu_utils_spec.js
new file mode 100644
index 00000000..9435d699
--- /dev/null
+++ b/web/regression/javascript/backup/menu_utils_spec.js
@@ -0,0 +1,55 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+
+import {menuEnabledServer} from '../../../pgadmin/tools/backup/static/js/menu_utils';
+
+const context = describe;
+
+describe('backup.menuUtils', () => {
+ describe('#menuEnabledServer', () => {
+ context('provided node data is undefined', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer(undefined)).toBe(false);
+ });
+ });
+
+ context('provided node data is null', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer(null)).toBe(false);
+ });
+ });
+
+ context('current node type is not of the type server', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer({_type: 'schema'})).toBe(false);
+ });
+ });
+
+ context('current node type is of the type server', () => {
+ context('is connected', () => {
+ it('returns true', () => {
+ expect(menuEnabledServer({
+ _type: 'server',
+ connected: true,
+ })).toBe(true);
+ });
+ });
+ context('is not connected', () => {
+ it('returns false', () => {
+ expect(menuEnabledServer({
+ _type: 'server',
+ connected: false,
+ })).toBe(false);
+ });
+ });
+ });
+ });
+});
+
diff --git a/web/regression/javascript/common_keyboard_shortcuts_spec.js b/web/regression/javascript/common_keyboard_shortcuts_spec.js
index 9ea31efd..e27929bf 100644
--- a/web/regression/javascript/common_keyboard_shortcuts_spec.js
+++ b/web/regression/javascript/common_keyboard_shortcuts_spec.js
@@ -11,10 +11,6 @@ import keyboardShortcuts from 'sources/keyboard_shortcuts';
describe('the keyboard shortcuts', () => {
const F1_KEY = 112;
- // const EDIT_KEY = 71; // Key: G -> Grid values
- // const LEFT_ARROW_KEY = 37;
- // const RIGHT_ARROW_KEY = 39;
- // const MOVE_NEXT = 'right';
let debuggerElementSpy, event, debuggerUserShortcutSpy;
debuggerUserShortcutSpy = jasmine.createSpyObj(
diff --git a/web/regression/javascript/datagrid/get_panel_title_spec.js b/web/regression/javascript/datagrid/get_panel_title_spec.js
new file mode 100644
index 00000000..8a344a84
--- /dev/null
+++ b/web/regression/javascript/datagrid/get_panel_title_spec.js
@@ -0,0 +1,82 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {getPanelTitle} from '../../../pgadmin/tools/datagrid/static/js/get_panel_title';
+import {TreeFake} from '../tree/tree_fake';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+const context = describe;
+
+describe('#getPanelTitle', () => {
+ let pgBrowser;
+ let tree;
+ beforeEach(() => {
+ tree = new TreeFake();
+ pgBrowser = {
+ treeMenu: tree,
+ Nodes: {
+ server: {
+ hasId: true,
+ _type: 'server',
+ },
+ database: {
+ hasId: true,
+ _type: 'database',
+ },
+ },
+ };
+ });
+
+ context('selected node does not belong to a server', () => {
+ it('returns undefined', () => {
+ const root = tree.addNewNode('level1', {_type: 'server_groups'});
+ tree.addChild(root, new TreeNode('level1.1', {_type: 'other'}));
+ tree.selectNode([{id: 'level1'}]);
+ expect(getPanelTitle(pgBrowser)).toBeUndefined();
+ });
+ });
+
+ context('selected node belong to a server', () => {
+ context('selected node does not belong to a database', () => {
+ it('returns the server label and the username', () => {
+ tree.addNewNode('level1', {
+ _type: 'server',
+ db: 'other db label',
+ user: {name: 'some user name'},
+ label: 'server label',
+ }, []);
+
+ tree.selectNode([{id: 'level1'}]);
+ expect(getPanelTitle(pgBrowser))
+ .toBe('other db label on some user name@server label');
+ });
+ });
+
+ context('selected node belongs to a database', () => {
+ it('returns the database label and the username', () => {
+ const root = tree.addNewNode('level1', {
+ _type: 'server',
+ db: 'other db label',
+ user: {name: 'some user name'},
+ label: 'server label',
+ });
+ const level1 = new TreeNode('level1.1', {
+ _type: 'database',
+ label: 'db label',
+ });
+ tree.addChild(root, level1);
+ tree.addChild(level1,
+ new TreeNode('level1.1.1', {_type: 'table'}));
+ tree.selectNode([{id: 'level1.1.1'}]);
+ expect(getPanelTitle(pgBrowser))
+ .toBe('db label on some user name@server label');
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/datagrid/show_data_spec.js b/web/regression/javascript/datagrid/show_data_spec.js
new file mode 100644
index 00000000..80d25eb3
--- /dev/null
+++ b/web/regression/javascript/datagrid/show_data_spec.js
@@ -0,0 +1,171 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {showDataGrid} from '../../../pgadmin/tools/datagrid/static/js/show_data';
+import {TreeFake} from '../tree/tree_fake';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+const context = describe;
+
+describe('#show_data', () => {
+ let datagrid;
+ let pgBrowser;
+ let alertify;
+ beforeEach(() => {
+ alertify = jasmine.createSpyObj('alertify', ['alert']);
+ datagrid = {
+ create_transaction: jasmine.createSpy('create_transaction'),
+ };
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server_group: {
+ _type: 'server_group',
+ hasId: true,
+ },
+ server: {
+ _type: 'server',
+ hasId: true,
+ },
+ database: {
+ _type: 'database',
+ hasId: true,
+ },
+ schema: {
+ _type: 'schema',
+ hasId: true,
+ },
+ view: {
+ _type: 'view',
+ hasId: true,
+ },
+ catalog: {
+ _type: 'catalog',
+ hasId: true,
+ },
+ },
+ };
+ const parent = pgBrowser.treeMenu.addNewNode('parent', {_type: 'parent'}, []);
+ const serverGroup1 = new TreeNode('server_group1', {
+ _type: 'server_group',
+ _id: 1,
+ });
+ pgBrowser.treeMenu.addChild(parent, serverGroup1);
+
+ const server1 = new TreeNode('server1', {
+ _type: 'server',
+ label: 'server1',
+ server_type: 'pg',
+ _id: 2,
+ }, ['parent', 'server_group1']);
+ pgBrowser.treeMenu.addChild(serverGroup1, server1);
+
+ const database1 = new TreeNode('database1', {
+ _type: 'database',
+ label: 'database1',
+ _id: 3,
+ }, ['parent', 'server_group1', 'server1']);
+ pgBrowser.treeMenu.addChild(server1, database1);
+
+ const schema1 = new TreeNode('schema1', {
+ _type: 'schema',
+ label: 'schema1',
+ _id: 4,
+ });
+ pgBrowser.treeMenu.addChild(database1, schema1);
+
+ const view1 = new TreeNode('view1', {
+ _type: 'view',
+ label: 'view1',
+ _id: 5,
+ }, ['parent', 'server_group1', 'server1', 'database1']);
+ pgBrowser.treeMenu.addChild(database1, view1);
+
+ const catalog1 = new TreeNode('catalog1', {
+ _type: 'catalog',
+ label: 'catalog1',
+ _id: 6,
+ }, ['parent', 'server_group1', 'server1', 'database1']);
+ pgBrowser.treeMenu.addChild(database1, catalog1);
+ });
+
+ context('cannot find the tree node', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: '10'}]);
+ expect(datagrid.create_transaction).not.toHaveBeenCalled();
+ });
+
+ it('display alert', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: '10'}]);
+ expect(alertify.alert).toHaveBeenCalledWith(
+ 'Data Grid Error',
+ 'No object selected.'
+ );
+ });
+ });
+
+ context('current node is not underneath a server', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: 'parent'}]);
+ expect(datagrid.create_transaction).not.toHaveBeenCalled();
+ });
+ });
+
+ context('current node is not underneath a schema or view or catalog', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {}, [{id: 'database1'}]);
+ expect(datagrid.create_transaction).not.toHaveBeenCalled();
+ });
+ });
+
+ context('current node is underneath a schema', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {mnuid: 11}, [{id: 'schema1'}]);
+ expect(datagrid.create_transaction).toHaveBeenCalledWith(
+ '/initialize/datagrid/11/schema/1/2/3/4',
+ null,
+ 'false',
+ 'pg',
+ '',
+ 'server1 - database1 - schema1.schema1',
+ ''
+ );
+ });
+ });
+
+ context('current node is underneath a view', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {mnuid: 11}, [{id: 'view1'}]);
+ expect(datagrid.create_transaction).toHaveBeenCalledWith(
+ '/initialize/datagrid/11/view/1/2/3/5',
+ null,
+ 'false',
+ 'pg',
+ '',
+ 'server1 - database1 - view1.view1',
+ ''
+ );
+ });
+ });
+
+ context('current node is underneath a catalog', () => {
+ it('does not create a transaction', () => {
+ showDataGrid(datagrid, pgBrowser, alertify, {mnuid: 11}, [{id: 'catalog1'}]);
+ expect(datagrid.create_transaction).toHaveBeenCalledWith(
+ '/initialize/datagrid/11/catalog/1/2/3/6',
+ null,
+ 'false',
+ 'pg',
+ '',
+ 'server1 - database1 - catalog1.catalog1',
+ ''
+ );
+ });
+ });
+});
diff --git a/web/regression/javascript/datagrid/show_query_tool_spec.js b/web/regression/javascript/datagrid/show_query_tool_spec.js
new file mode 100644
index 00000000..66bd37ce
--- /dev/null
+++ b/web/regression/javascript/datagrid/show_query_tool_spec.js
@@ -0,0 +1,125 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import {TreeFake} from '../tree/tree_fake';
+import {showQueryTool} from '../../../pgadmin/tools/datagrid/static/js/show_query_tool';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+const context = describe;
+
+describe('#showQueryTool', () => {
+ let queryTool;
+ let pgBrowser;
+ let alertify;
+ beforeEach(() => {
+ alertify = jasmine.createSpyObj('alertify', ['alert']);
+ queryTool = {
+ create_transaction: jasmine.createSpy('create_transaction'),
+ };
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server_group: {
+ _type: 'server_group',
+ hasId: true,
+ },
+ server: {
+ _type: 'server',
+ hasId: true,
+ },
+ database: {
+ _type: 'database',
+ hasId: true,
+ },
+ },
+ };
+ const parent = pgBrowser.treeMenu.addNewNode('parent', {_type: 'parent'});
+ const serverGroup1 = new TreeNode('server_group1', {
+ _type: 'server_group',
+ _id: 1,
+ }, ['parent']);
+ pgBrowser.treeMenu.addChild(parent, serverGroup1);
+
+ const server1 = new TreeNode('server1', {
+ _type: 'server',
+ label: 'server1',
+ server_type: 'pg',
+ _id: 2,
+ });
+ pgBrowser.treeMenu.addChild(serverGroup1, server1);
+
+ const database1 = new TreeNode('database1', {
+ _type: 'database',
+ label: 'database1',
+ _id: 3,
+ });
+ pgBrowser.treeMenu.addChild(server1, database1);
+ });
+
+ context('cannot find the tree node', () => {
+ beforeEach(() => {
+ showQueryTool(queryTool, pgBrowser, alertify, '', [{id: '10'}], 'title');
+ });
+ it('does not create a transaction', () => {
+ expect(queryTool.create_transaction).not.toHaveBeenCalled();
+ });
+
+ it('display alert', () => {
+ expect(alertify.alert).toHaveBeenCalledWith(
+ 'Query Tool Error',
+ 'No object selected.'
+ );
+ });
+ });
+
+ context('current node is not underneath a server', () => {
+ it('does not create a transaction', () => {
+ showQueryTool(queryTool, pgBrowser, alertify, '', [{id: 'parent'}], 'title');
+ expect(queryTool.create_transaction).not.toHaveBeenCalled();
+ });
+
+ it('no alert is displayed', () => {
+ expect(alertify.alert).not.toHaveBeenCalled();
+ });
+ });
+
+ context('current node is underneath a server', () => {
+ context('current node is not underneath a database', () => {
+ it('creates a transaction', () => {
+ showQueryTool(queryTool, pgBrowser, alertify, 'http://someurl', [{id: 'server1'}], 'title');
+ expect(queryTool.create_transaction).toHaveBeenCalledWith(
+ '/initialize/query_tool/1/2',
+ null,
+ 'true',
+ 'pg',
+ 'http://someurl',
+ 'title',
+ '',
+ false
+ );
+ });
+ });
+
+ context('current node is underneath a database', () => {
+ it('creates a transaction', () => {
+ showQueryTool(queryTool, pgBrowser, alertify, 'http://someurl', [{id: 'database1'}], 'title');
+ expect(queryTool.create_transaction).toHaveBeenCalledWith(
+ '/initialize/query_tool/1/2/3',
+ null,
+ 'true',
+ 'pg',
+ 'http://someurl',
+ 'title',
+ '',
+ false
+ );
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/fake_browser/browser.js b/web/regression/javascript/fake_browser/browser.js
new file mode 100644
index 00000000..195e5c51
--- /dev/null
+++ b/web/regression/javascript/fake_browser/browser.js
@@ -0,0 +1,12 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+let treeMenu = null;
+
+export {treeMenu};
diff --git a/web/regression/javascript/fake_endpoints.js b/web/regression/javascript/fake_endpoints.js
index 54b86a94..c060ba78 100644
--- a/web/regression/javascript/fake_endpoints.js
+++ b/web/regression/javascript/fake_endpoints.js
@@ -12,5 +12,11 @@ define(function () {
'static': '/base/pgadmin/static/<path:filename>',
'sqleditor.poll': '/sqleditor/query_tool/poll/<path:trans_id>',
'sqleditor.query_tool_start': '/sqleditor/query_tool/start/<path:trans_id>',
+ 'backup.create_server_job': '/backup/job/<int:sid>',
+ 'backup.create_object_job': '/backup/job/<int:sid>/object',
+ 'datagrid.initialize_datagrid': '/initialize/datagrid/<int:cmd_type>/<obj_type>/<int:sgid>/<int:sid>/<int:did>/<int:obj_id>',
+ 'datagrid.initialize_query_tool': '/initialize/query_tool/<int:sgid>/<int:sid>',
+ 'datagrid.initialize_query_tool_with_did': '/initialize/query_tool/<int:sgid>/<int:sid>/<int:did>',
+ 'restore.create_job': '/restore/job/<int:sid>',
};
});
diff --git a/web/regression/javascript/fake_model.js b/web/regression/javascript/fake_model.js
new file mode 100644
index 00000000..acfaa532
--- /dev/null
+++ b/web/regression/javascript/fake_model.js
@@ -0,0 +1,21 @@
+export class FakeModel {
+ constructor() {
+ this.values = {};
+ }
+
+ set(key, value) {
+ this.values[key] = value;
+ }
+
+ get(key) {
+ return this.values[key];
+ }
+
+ unset(key) {
+ delete this.values[key];
+ }
+
+ toJSON() {
+ return Object.assign({}, this.values);
+ }
+}
diff --git a/web/regression/javascript/nodes/schema/child_menu_spec.js b/web/regression/javascript/nodes/schema/child_menu_spec.js
new file mode 100644
index 00000000..3d3dc55e
--- /dev/null
+++ b/web/regression/javascript/nodes/schema/child_menu_spec.js
@@ -0,0 +1,253 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+/////////////////////////////////////////////////////////////
+
+import {
+ isTreeItemOfChildOfSchema, childCreateMenuEnabled,
+} from 'pgadmin.schema.dir/schema_child_tree_node';
+
+import * as pgBrowser from 'pgbrowser/browser';
+import {TreeFake} from '../../tree/tree_fake';
+
+describe('#childCreateMenuEnabled', () => {
+ let data;
+ let tree;
+
+ describe(' - when data is not null', () => {
+ beforeEach(() => {
+ data = {};
+ });
+ describe(' and check is false', () => {
+ beforeEach(() => {
+ data = {check: false};
+ });
+ it(', then it returns true', () => {
+ expect(childCreateMenuEnabled({}, {}, data)).toBe(true);
+ });
+ });
+
+ describe(' and check', () => {
+ describe(' is true', () => {
+ beforeEach(() => {
+ data = {check: true};
+ });
+
+ describe(', on schema node', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [{
+ id: 'level2',
+ data: {_type: 'schema'},
+ }],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ pgBrowser.treeMenu = tree;
+ });
+ it(' it is true', () => {
+ expect(childCreateMenuEnabled(
+ {}, [{id: 'level2'}], data
+ )).toBe(true);
+
+ });
+ });
+
+ describe(', on child collection node under schema node ', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [{
+ id: 'level2',
+ data: {_type: 'schema'},
+ children: [{
+ id: 'coll-table',
+ data: {_type: 'coll-table'},
+ }],
+ }],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ pgBrowser.treeMenu = tree;
+ });
+
+ it(' it is true', () => {
+ expect(childCreateMenuEnabled(
+ {}, [{id: 'coll-table'}], data
+ )).toBe(true);
+ });
+ });
+
+ describe(', on one of the child node under schema node ', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [{
+ id: 'level2',
+ data: {_type: 'schema'},
+ children: [{
+ id: 'coll-table',
+ data: {_type: 'coll-table'},
+ children: [{
+ id: 'table/1',
+ data: {_type: 'table'},
+ }],
+ }],
+ }],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ pgBrowser.treeMenu = tree;
+ });
+
+ it(' it is true', () => {
+ expect(childCreateMenuEnabled(
+ {}, [{id: 'table/1'}], data
+ )).toBe(true);
+ });
+ });
+
+ describe(', on catalog node', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [{
+ id: 'level2',
+ data: {_type: 'catalog'},
+ }],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ pgBrowser.treeMenu = tree;
+ });
+ it(' it is false', () => {
+ expect(
+ childCreateMenuEnabled({}, [{id: 'level2'}], data)
+ ).toBe(false);
+ });
+ });
+
+ describe(', on child collection node under catalog node ', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [{
+ id: 'level2',
+ data: {_type: 'catalog'},
+ children: [{
+ id: 'coll-table',
+ data: {_type: 'coll-table'},
+ }],
+ }],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ pgBrowser.treeMenu = tree;
+ });
+
+ it(' it is false', () => {
+ expect(childCreateMenuEnabled(
+ {}, [{id: 'coll-table'}], data
+ )).toBe(false);
+ });
+ });
+
+ describe(', on one of the child node under catalog node ', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [{
+ id: 'level2',
+ data: {_type: 'catalog'},
+ children: [{
+ id: 'coll-table',
+ data: {_type: 'coll-table'},
+ children: [{
+ id: 'table/1',
+ data: {_type: 'table'},
+ }],
+ }],
+ }],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ pgBrowser.treeMenu = tree;
+ });
+
+ it(' it is false', () => {
+ expect(childCreateMenuEnabled(
+ {}, [{id: 'table/1'}], data
+ )).toBe(false);
+ });
+ });
+ });
+ });
+ });
+});
+
+describe('#childDropMenuEnabled', () => {
+ let tree;
+
+ describe(' - the child node under schema node ', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [{
+ id: 'level2',
+ data: {_type: 'schema'},
+ children: [{
+ id: 'coll-table',
+ data: {_type: 'coll-table'},
+ children: [{
+ id: 'table/1',
+ data: {_type: 'table'},
+ }],
+ }],
+ }],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ pgBrowser.treeMenu = tree;
+ });
+
+ it(' it is true', () => {
+ expect(isTreeItemOfChildOfSchema(
+ {}, [{id: 'table/1'}]
+ )).toBe(true);
+ });
+ });
+
+ describe('- the child node under the catalog node ', () => {
+ beforeEach(() => {
+ let hierarchy = {
+ id: 'root',
+ children: [{
+ id: 'level2',
+ data: {_type: 'catalog'},
+ children: [{
+ id: 'coll-table',
+ data: {_type: 'coll-table'},
+ children: [{
+ id: 'table/1',
+ data: {_type: 'table'},
+ }],
+ }],
+ }],
+ };
+
+ tree = TreeFake.build(hierarchy);
+ pgBrowser.treeMenu = tree;
+ });
+
+ it(' it is false', () => {
+ expect(isTreeItemOfChildOfSchema(
+ {}, [{id: 'table/1'}]
+ )).toBe(false);
+ });
+ });
+});
diff --git a/web/regression/javascript/restore/restore_dialog_spec.js b/web/regression/javascript/restore/restore_dialog_spec.js
new file mode 100644
index 00000000..156f56bb
--- /dev/null
+++ b/web/regression/javascript/restore/restore_dialog_spec.js
@@ -0,0 +1,203 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {TreeFake} from '../tree/tree_fake';
+import {RestoreDialog} from '../../../pgadmin/tools/restore/static/js/restore_dialog';
+
+const context = describe;
+
+describe('RestoreDialog', () => {
+ let restoreDialog;
+ let pgBrowser;
+ let jquerySpy;
+ let alertifySpy;
+ let restoreModelSpy;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: jasmine.createSpyObj('Node[server]', ['getTreeNodeHierarchy']),
+ database: jasmine.createSpyObj('Node[database]', ['getTreeNodeHierarchy']),
+ },
+ };
+ pgBrowser.Nodes.server.hasId = true;
+ pgBrowser.Nodes.database.hasId = true;
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ restoreModelSpy = jasmine.createSpy('restoreModelSpy');
+
+ const hierarchy = {
+ children: [
+ {
+ id: 'root',
+ children: [
+ {
+ id: 'serverTreeNode',
+ data: {
+ _id: 10,
+ _type: 'server',
+ label: 'some-tree-label',
+ },
+ children: [
+ {
+ id: 'some_database',
+ data: {
+ _type: 'database',
+ _id: 11,
+ label: 'some_database',
+ _label: 'some_database_label',
+ },
+ }, {
+ id: 'database_with_equal_in_name',
+ data: {
+ _type: 'database',
+ label: 'some_database',
+ _label: '=some_database_label',
+ },
+ },
+ ],
+ },
+ {
+ id: 'ppasServer',
+ data: {
+ _type: 'server',
+ server_type: 'ppas',
+ children: [
+ {id: 'someNodeUnderneathPPASServer'},
+ ],
+ },
+ },
+ ],
+ },
+ ],
+ };
+
+ pgBrowser.treeMenu = TreeFake.build(hierarchy);
+ });
+
+ describe('#draw', () => {
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ alertifySpy['pg_restore'] = jasmine.createSpy('pg_restore');
+ restoreDialog = new RestoreDialog(
+ pgBrowser,
+ jquerySpy,
+ alertifySpy,
+ restoreModelSpy
+ );
+
+ pgBrowser.get_preference = jasmine.createSpy('get_preferences');
+ });
+
+ context('there are no ancestors of the type server', () => {
+ it('does not create a dialog', () => {
+ pgBrowser.treeMenu.selectNode([{id: 'root'}]);
+ restoreDialog.draw(null, null, null);
+ expect(alertifySpy['pg_restore']).not.toHaveBeenCalled();
+ });
+
+ it('display an alert with a Restore Error', () => {
+ restoreDialog.draw(null, [{id: 'root'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Restore Error',
+ 'Please select server or child node from the browser tree.'
+ );
+ });
+ });
+
+ context('there is an ancestor of the type server', () => {
+ context('no preference can be found', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue(undefined);
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Restore Error"', () => {
+ restoreDialog.draw(null, [{id: 'serverTreeNode'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Restore Error',
+ 'Failed to load preference pg_bin_dir of module paths'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Restore Error"', () => {
+ restoreDialog.draw(null, [{id: 'ppasServer'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Restore Error',
+ 'Failed to load preference ppas_bin_dir of module paths'
+ );
+ });
+ });
+ });
+
+ context('preference can be found', () => {
+ context('binary folder is not configured', () => {
+ beforeEach(() => {
+ pgBrowser.get_preference.and.returnValue({});
+ });
+
+ context('server is a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ restoreDialog.draw(null, [{id: 'serverTreeNode'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the PostgreSQL Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+
+ context('server is not a ppas server', () => {
+ it('display an alert with "Configuration required"', () => {
+ restoreDialog.draw(null, [{id: 'ppasServer'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Configuration required',
+ 'Please configure the EDB Advanced Server Binary Path in the Preferences dialog.'
+ );
+ });
+ });
+ });
+
+ context('binary folder is configured', () => {
+ let spy;
+ beforeEach(() => {
+ spy = jasmine.createSpyObj('globals', ['resizeTo']);
+ alertifySpy['pg_restore'].and
+ .returnValue(spy);
+ pgBrowser.get_preference.and.returnValue({value: '/some/path'});
+ pgBrowser.Nodes.server.label = 'some-server-label';
+ });
+
+ it('displays the dialog', () => {
+ restoreDialog.draw(null, [{id: 'serverTreeNode'}], {server: true});
+ expect(alertifySpy['pg_restore']).toHaveBeenCalledWith(
+ 'Restore (some-server-label: some-tree-label)',
+ [{id: 'serverTreeNode'}],
+ {
+ _id: 10,
+ _type: 'server',
+ label: 'some-tree-label',
+ },
+ pgBrowser.Nodes.server
+ );
+ expect(spy.resizeTo).toHaveBeenCalledWith('65%', '60%');
+ });
+
+ context('database label contain "="', () => {
+ it('should create alert dialog with restore error', () => {
+ restoreDialog.draw(null, [{id: 'database_with_equal_in_name'}], null);
+ expect(alertifySpy.alert).toHaveBeenCalledWith('Restore Error',
+ 'Databases with = symbols in the name cannot be backed up or restored using this utility.');
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/restore/restore_dialog_wrapper_spec.js b/web/regression/javascript/restore/restore_dialog_wrapper_spec.js
new file mode 100644
index 00000000..c2a31d55
--- /dev/null
+++ b/web/regression/javascript/restore/restore_dialog_wrapper_spec.js
@@ -0,0 +1,593 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import {TreeFake} from '../tree/tree_fake';
+import {RestoreDialogWrapper} from '../../../pgadmin/tools/restore/static/js/restore_dialog_wrapper';
+import MockAdapter from 'axios-mock-adapter';
+import axios from 'axios/index';
+import {FakeModel} from '../fake_model';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+let context = describe;
+
+describe('RestoreDialogWrapper', () => {
+ let jquerySpy;
+ let pgBrowser;
+ let alertifySpy;
+ let dialogModelKlassSpy;
+ let backform;
+ let generatedRestoreModel;
+ let restoreDialogWrapper;
+ let noDataNode;
+ let serverTreeNode;
+ let viewSchema;
+ let restoreJQueryContainerSpy;
+ let restoreNodeChildNodeSpy;
+ let restoreNode;
+
+ beforeEach(() => {
+ pgBrowser = {
+ treeMenu: new TreeFake(),
+ Nodes: {
+ server: {
+ hasId: true,
+ getTreeNodeHierarchy: jasmine.createSpy('getTreeNodeHierarchy'),
+ },
+ },
+ keyboardNavigation: jasmine.createSpyObj('keyboardNavigation', ['getDialogTabNavigator']),
+ };
+
+ noDataNode = pgBrowser.treeMenu.addNewNode('level1.1', undefined, [{id: 'level1'}]);
+ serverTreeNode = pgBrowser.treeMenu.addNewNode('level2.1', {
+ _type: 'server',
+ _id: 10,
+ label: 'some-tree-label',
+ }, [{id: 'level2.1'}]);
+ jquerySpy = jasmine.createSpy('jquerySpy');
+ dialogModelKlassSpy = jasmine.createSpy('dialogModelKlass');
+ generatedRestoreModel = {};
+ viewSchema = {};
+ backform = jasmine.createSpyObj('backform', ['generateViewSchema', 'Dialog']);
+ backform.generateViewSchema.and.returnValue(viewSchema);
+ dialogModelKlassSpy.and.returnValue(generatedRestoreModel);
+ restoreJQueryContainerSpy = jasmine.createSpyObj('restoreJQueryContainer', ['get', 'attr']);
+ restoreJQueryContainerSpy.get.and.returnValue(restoreJQueryContainerSpy);
+
+ restoreNode = {
+ __internal: {
+ buttons: [
+ {}, {},
+ {
+ element: {
+ disabled: false,
+ },
+ },
+ ],
+ },
+ elements: {
+ body: {
+ childNodes: [
+ {},
+ ],
+ },
+ content: jasmine.createSpyObj('content', ['appendChild', 'attr']),
+ },
+ };
+
+
+ restoreNodeChildNodeSpy = jasmine.createSpyObj('something', ['addClass']);
+
+ jquerySpy.and.callFake((selector) => {
+ if (selector === '<div class=\'restore_dialog\'></div>') {
+ return restoreJQueryContainerSpy;
+ } else if (selector === restoreNode.elements.body.childNodes[0]) {
+ return restoreNodeChildNodeSpy;
+ }
+ });
+ });
+
+ describe('#prepare', () => {
+
+ beforeEach(() => {
+ alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
+ restoreDialogWrapper = new RestoreDialogWrapper(
+ '<div class=\'restore_dialog\'></div>',
+ 'restoreDialogTitle',
+ 'restore',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+ restoreDialogWrapper = Object.assign(restoreDialogWrapper, restoreNode);
+ });
+ context('no tree element is selected', () => {
+ it('does not create a backform dialog', () => {
+ restoreDialogWrapper.prepare();
+ expect(backform.Dialog).not.toHaveBeenCalled();
+ });
+
+ it('disables the button "submit button" until a filename is selected', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreDialogWrapper.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('selected tree node has no data', () => {
+ beforeEach(() => {
+ pgBrowser.treeMenu.selectNode(noDataNode.domNode);
+ });
+
+ it('does not create a backform dialog', () => {
+ restoreDialogWrapper.prepare();
+ expect(backform.Dialog).not.toHaveBeenCalled();
+ });
+
+ it('disables the button "submit button" until a filename is selected', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreDialogWrapper.__internal.buttons[2].element.disabled).toBe(true);
+ });
+ });
+
+ context('tree element is selected', () => {
+ let treeHierarchyInformation;
+ let dialogSpy;
+ beforeEach(() => {
+ treeHierarchyInformation = {
+ server: {
+ _type: 'server',
+ _id: 10,
+ priority: 0,
+ label: 'some-tree-label',
+ },
+ };
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ pgBrowser.Nodes['server'].getTreeNodeHierarchy.and
+ .returnValue(treeHierarchyInformation);
+ dialogSpy = jasmine.createSpyObj('newView', ['render']);
+ dialogSpy.$el = jasmine.createSpyObj('$el', ['find', 'attr']);
+ dialogSpy.model = jasmine.createSpyObj('model', ['on']);
+ dialogSpy.$el.find.and.returnValue([]);
+
+ backform.Dialog.and.returnValue(dialogSpy);
+ });
+
+ it('creates a backform dialog and displays it', () => {
+ restoreDialogWrapper.prepare();
+ expect(backform.Dialog).toHaveBeenCalledWith({
+ el: restoreJQueryContainerSpy,
+ model: generatedRestoreModel,
+ schema: viewSchema,
+ });
+
+ expect(dialogSpy.render).toHaveBeenCalled();
+ });
+
+ it('add alertify classes to restore node childnode', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreNodeChildNodeSpy.addClass)
+ .toHaveBeenCalledWith('alertify_tools_dialog_properties obj_properties');
+ });
+
+ it('disables the button submit button until a filename is selected', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreNode.__internal.buttons[2].element.disabled).toBe(true);
+ });
+
+ it('generates a new restore model', () => {
+ restoreDialogWrapper.prepare();
+ expect(dialogModelKlassSpy).toHaveBeenCalledWith(
+ {node_data: pgBrowser.Nodes['server']},
+ {node_info: treeHierarchyInformation}
+ );
+ });
+
+ it('add the new dialog to the restore node HTML', () => {
+ restoreDialogWrapper.prepare();
+ expect(restoreNode.elements.content.appendChild).toHaveBeenCalledWith(restoreJQueryContainerSpy);
+ });
+ });
+ });
+
+ describe('onButtonClicked', () => {
+ let networkMock;
+
+ beforeEach(() => {
+ pgBrowser.showHelp = jasmine.createSpy('showHelp');
+ networkMock = new MockAdapter(axios);
+ alertifySpy = jasmine.createSpyObj('alertify', ['success', 'alert']);
+ restoreDialogWrapper = new RestoreDialogWrapper(
+ '<div class=\'restore_dialog\'></div>',
+ 'restoreDialogTitle',
+ 'restore',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+ restoreDialogWrapper = Object.assign(restoreDialogWrapper, restoreNode);
+
+ });
+
+ afterEach(function () {
+ networkMock.restore();
+ });
+
+ context('dialog help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ element: {
+ name: 'dialog_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ },
+ },
+ },
+ };
+ restoreDialogWrapper.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'dialog_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ );
+ });
+
+ it('does not start the restore', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('object help button was pressed', () => {
+ let networkCalled;
+ beforeEach(() => {
+ networkCalled = false;
+ pgBrowser.treeMenu.selectNode(serverTreeNode.domNode);
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+
+ const event = {
+ button: {
+ element: {
+ name: 'object_help',
+ getAttribute: (attributeName) => {
+ if (attributeName === 'url') {
+ return 'http://someurl';
+ } else if (attributeName === 'label') {
+ return 'some label';
+ }
+ },
+ },
+ },
+ };
+ restoreDialogWrapper.callback(event);
+ });
+
+ it('displays help for dialog', () => {
+ expect(pgBrowser.showHelp).toHaveBeenCalledWith(
+ 'object_help',
+ 'http://someurl',
+ pgBrowser.Nodes['server'],
+ serverTreeNode,
+ 'some label'
+ );
+ });
+
+ it('does not start the restore', () => {
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('restore button was pressed', () => {
+ let networkCalled;
+ let event;
+
+ context('no tree node is selected', () => {
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+ event = {
+ button: {
+ 'data-btn-name': 'restore',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ });
+
+ it('does not start the restore', () => {
+ restoreDialogWrapper.callback(event);
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('tree node selected has no data', () => {
+ beforeEach(() => {
+ networkCalled = false;
+ networkMock.onAny(/.+/).reply(() => {
+ networkCalled = true;
+ return [200, {}];
+ });
+ event = {
+ button: {
+ 'data-btn-name': 'restore',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ pgBrowser.treeMenu.selectNode(noDataNode.domNode);
+ });
+
+ it('does not start the restore', () => {
+ restoreDialogWrapper.callback(event);
+ expect(networkCalled).toBe(false);
+ });
+ });
+
+ context('tree node select has data', () => {
+
+ let databaseTreeNode;
+
+ beforeEach(() => {
+ databaseTreeNode = pgBrowser.treeMenu.addNewNode('level3.1', {
+ _type: 'database',
+ _id: 10,
+ _label: 'some-database-label',
+ }, [{id: 'level3.1'}]);
+ pgBrowser.treeMenu.addChild(serverTreeNode, databaseTreeNode);
+ pgBrowser.Nodes.database = {
+ hasId: true,
+ _label: 'some-database-label',
+ };
+ let fakeModel = new FakeModel();
+ fakeModel.set('some-key', 'some-value');
+ restoreDialogWrapper.view = {
+ model: fakeModel,
+ };
+ pgBrowser.treeMenu.selectNode(databaseTreeNode.domNode);
+ pgBrowser.Events = jasmine.createSpyObj('pgBrowserEventsSpy', ['trigger']);
+ event = {
+ button: {
+ 'data-btn-name': 'restore',
+ element: {
+ getAttribute: () => {
+ return 'http://someurl';
+ },
+ },
+ },
+ };
+ });
+ context('restore job created successfully', () => {
+ let dataSentToServer;
+ beforeEach(() => {
+ networkMock.onPost('/restore/job/10').reply((request) => {
+ dataSentToServer = request.data;
+ return [200, {}];
+ });
+ });
+
+ it('create an success alert box', (done) => {
+ restoreDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.success).toHaveBeenCalledWith(
+ 'Restore job created.',
+ 5
+ );
+ done();
+ }, 0);
+ });
+
+ it('trigger background process', (done) => {
+ restoreDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(pgBrowser.Events.trigger).toHaveBeenCalledWith(
+ 'pgadmin-bgprocess:created',
+ restoreDialogWrapper
+ );
+ done();
+ }, 0);
+ });
+
+ it('send correct data to server', (done) => {
+ restoreDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(JSON.parse(dataSentToServer)).toEqual({
+ 'some-key': 'some-value',
+ 'database': 'some-database-label',
+ });
+ done();
+ }, 0);
+ });
+ });
+
+ context('error creating restore job', () => {
+ beforeEach(() => {
+ networkMock.onPost('/restore/job/10').reply(() => {
+ return [400, {}];
+ });
+ });
+
+ it('creates an alert box', (done) => {
+ restoreDialogWrapper.callback(event);
+ setTimeout(() => {
+ expect(alertifySpy.alert).toHaveBeenCalledWith(
+ 'Restore job failed.',
+ undefined
+ );
+ done();
+ }, 0);
+ });
+ });
+ });
+ });
+ });
+
+ describe('setExtraParameters', () => {
+ let selectedNode;
+ let treeInfo;
+ let model;
+
+ beforeEach(() => {
+ restoreDialogWrapper = new RestoreDialogWrapper(
+ '<div class=\'restore_dialog\'></div>',
+ 'restoreDialogTitle',
+ 'restore',
+ jquerySpy,
+ pgBrowser,
+ alertifySpy,
+ dialogModelKlassSpy,
+ backform
+ );
+
+ model = new FakeModel();
+ restoreDialogWrapper.view = {
+ model: model,
+ };
+ });
+
+ context('when it is a custom model', () => {
+ beforeEach(() => {
+ model.set('custom', true);
+ treeInfo = {
+ 'database': {
+ '_label': 'some-database-label',
+ },
+ };
+ });
+
+ it('only sets the database', () => {
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ 'custom': true,
+ 'database': 'some-database-label',
+ });
+ });
+ });
+
+ context('when it is not a custom model', () => {
+ beforeEach(() => {
+ model.set('custom', false);
+ treeInfo = {
+ 'database': {
+ '_label': 'some-database-label',
+ },
+ 'schema': {
+ '_label': 'some-schema-label',
+ },
+ };
+ });
+
+ context('when selected node is a schema', () => {
+ it('sets schemas on the model', () => {
+ selectedNode = new TreeNode('schema', {_type: 'schema', _label: 'some-schema-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ });
+ });
+ });
+
+ context('when selected node is a table', () => {
+ it('sets schemas and table on the model', () => {
+ selectedNode = new TreeNode('table', {_type: 'table', _label: 'some-table-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ tables: ['some-table-label'],
+ });
+ });
+ });
+
+ context('when selected node is a function', () => {
+ it('sets schemas and function on the model', () => {
+ selectedNode = new TreeNode('function', {_type: 'function', _label: 'some-function-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ functions: ['some-function-label'],
+ });
+ });
+ });
+
+ context('when selected node is an index', () => {
+ it('sets schemas and index on the model', () => {
+ selectedNode = new TreeNode('index', {_type: 'index', _label: 'some-index-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ indexes: ['some-index-label'],
+ });
+ });
+ });
+
+ context('when selected node is a trigger', () => {
+ it('sets schemas and trigger on the model', () => {
+ selectedNode = new TreeNode('trigger', {_type: 'trigger', _label: 'some-trigger-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ triggers: ['some-trigger-label'],
+ });
+ });
+ });
+
+ context('when selected node is a trigger_func', () => {
+ it('sets schemas and trigger_func on the model', () => {
+ selectedNode = new TreeNode('trigger_func', {_type: 'trigger_func', _label: 'some-trigger_func-label'}, '');
+ restoreDialogWrapper.setExtraParameters(selectedNode, treeInfo);
+ expect(model.toJSON()).toEqual({
+ custom: false,
+ database: 'some-database-label',
+ schemas: ['some-schema-label'],
+ trigger_funcs: ['some-trigger_func-label'],
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/sqleditor/filter_dialog_specs.js b/web/regression/javascript/sqleditor/filter_dialog_specs.js
index ed77dff5..cea75e6b 100644
--- a/web/regression/javascript/sqleditor/filter_dialog_specs.js
+++ b/web/regression/javascript/sqleditor/filter_dialog_specs.js
@@ -7,10 +7,8 @@
//
//////////////////////////////////////////////////////////////////////////
import filterDialog from 'sources/sqleditor/filter_dialog';
-// import filterDialogModel from 'sources/sqleditor/filter_dialog_model';
describe('filterDialog', () => {
- jasmine.createSpy('sqlEditorController');
describe('filterDialog', () => {
describe('when using filter dialog', () => {
beforeEach(() => {
diff --git a/web/regression/javascript/table/enable_disable_triggers_spec.js b/web/regression/javascript/table/enable_disable_triggers_spec.js
new file mode 100644
index 00000000..7bdd284e
--- /dev/null
+++ b/web/regression/javascript/table/enable_disable_triggers_spec.js
@@ -0,0 +1,271 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import MockAdapter from 'axios-mock-adapter';
+import axios from 'axios/index';
+import {
+ enableTriggers,
+ disableTriggers,
+} from '../../../pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/enable_disable_triggers';
+import {TreeFake} from '../tree/tree_fake';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+
+describe('#enableTriggers', () => {
+ let networkMock;
+ let tree;
+ let alertify;
+ let generateUrlSpy;
+ beforeEach(() => {
+ networkMock = new MockAdapter(axios);
+ tree = new TreeFake();
+ const server1 = tree.addNewNode('server1', {_id: 1}, ['<li>server1</li>']);
+ const database1 = tree.addNewNode('database1', {_type: 'database'}, ['<li>database1</li>']);
+ tree.addChild(server1, database1);
+
+ const schema1 = tree.addNewNode('schema1', {_type: 'schema'}, ['<li>schema1</li>']);
+ tree.addChild(database1, schema1);
+
+ const table1 = tree.addNewNode('table1', {_type: 'table'}, ['<li>table1</li>']);
+ tree.addChild(schema1, table1);
+
+ const column1 = tree.addNewNode('column1', {_type: 'column'}, ['<li>column1</li>']);
+ tree.addChild(table1, column1);
+
+ const tableNoData = tree.addNewNode('table-no-data', undefined, ['<li>table-no-data</li>']);
+ tree.addChild(schema1, tableNoData);
+
+ alertify = jasmine.createSpyObj('alertify', ['success', 'error']);
+ generateUrlSpy = jasmine.createSpy('generateUrl');
+ generateUrlSpy.and.returnValue('/some/place');
+ });
+
+ describe('no node is selected', () => {
+ it('does not send the request to the backend', (done) => {
+ networkMock.onAny('.*').reply(200, () => {
+ });
+
+ setTimeout(() => {
+ expect(enableTriggers(tree, alertify, generateUrlSpy, {})).toBe(false);
+ done();
+ }, 0);
+ });
+ });
+
+ describe('a node is selected', () => {
+ describe('node as no data', () => {
+ it('does not send the request to the backend', () => {
+ tree.selectNode([{id: 'table-no-data'}]);
+
+ networkMock.onAny('.*').reply(200, () => {
+ });
+
+ setTimeout(() => {
+ expect(enableTriggers(tree, alertify, generateUrlSpy, {})).toBe(false);
+ }, 0);
+ });
+ });
+
+ describe('node as data', () => {
+ describe('backend responds with success', () => {
+ let networkMockCalledWith;
+ beforeEach(() => {
+ networkMockCalledWith = false;
+ networkMock.onPut(/.*/).reply((configuration) => {
+ networkMockCalledWith = configuration;
+ return [200, {
+ success: 1,
+ info: 'some information',
+ }];
+ });
+ });
+
+ it('displays an alert box with success', (done) => {
+ tree.selectNode([{id: 'table1'}]);
+ enableTriggers(tree, alertify, generateUrlSpy, {});
+ setTimeout(() => {
+ expect(alertify.success).toHaveBeenCalledWith('some information');
+ done();
+ }, 0);
+ });
+
+ it('reloads the node', (done) => {
+ enableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+ setTimeout(() => {
+ expect(tree.selected()).toEqual(['<li>table1</li>']);
+ done();
+ }, 20);
+ });
+
+ it('call backend with the correct parameters', (done) => {
+ enableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+ setTimeout(() => {
+ expect(networkMockCalledWith.data).toEqual(JSON.stringify({enable: 'true'}));
+ done();
+ }, 0);
+ });
+ });
+
+ describe('backend responds with error', () => {
+ beforeEach(() => {
+ networkMock.onPut(/.*/).reply(() => {
+ return [500, {
+ success: 0,
+ errormsg: 'some error message',
+ }];
+ });
+ });
+
+ it('displays an error alert', (done) => {
+ tree.selectNode([{id: 'table1'}]);
+ enableTriggers(tree, alertify, generateUrlSpy, {});
+ setTimeout(() => {
+ expect(alertify.error).toHaveBeenCalledWith('some error message');
+ done();
+ }, 0);
+ });
+
+ it('unload the node', (done) => {
+ enableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+
+ setTimeout(() => {
+ expect(tree.findNodeByDomElement([{id: 'table1'}]).children.length).toBe(0);
+ done();
+ }, 20);
+ });
+ });
+ });
+ });
+});
+
+describe('#disableTriggers', () => {
+ let networkMock;
+ let tree;
+ let alertify;
+ let generateUrlSpy;
+ beforeEach(() => {
+ networkMock = new MockAdapter(axios);
+ tree = new TreeFake();
+ const server1 = tree.addNewNode('server1', {_id: 1}, ['<li>server1</li>']);
+ const database1 = new TreeNode('database1', {_type: 'database'}, ['<li>database1</li>']);
+ tree.addChild(server1, database1);
+
+ const schema1 = new TreeNode('schema1', {_type: 'schema'}, ['<li>schema1</li>']);
+ tree.addChild(database1, schema1);
+
+ const table1 = new TreeNode('table1', {_type: 'table'}, ['<li>table1</li>']);
+ tree.addChild(schema1, table1);
+
+ const column1 = new TreeNode('column1', {_type: 'column'}, ['<li>column1</li>']);
+ tree.addChild(table1, column1);
+
+ const tableNoData = new TreeNode('table-no-data', undefined, ['<li>table-no-data</li>']);
+ tree.addChild(schema1, tableNoData);
+
+ alertify = jasmine.createSpyObj('alertify', ['success', 'error']);
+ generateUrlSpy = jasmine.createSpy('generateUrl');
+ generateUrlSpy.and.returnValue('/some/place');
+ });
+
+ describe('no node is selected', () => {
+ it('does not send the request to the backend', (done) => {
+ networkMock.onAny('.*').reply(200, () => {
+ });
+
+ setTimeout(() => {
+ expect(disableTriggers(tree, alertify, generateUrlSpy, {})).toBe(false);
+ done();
+ }, 0);
+ });
+ });
+
+ describe('a node is selected', () => {
+ describe('node as no data', () => {
+ it('does not send the request to the backend', () => {
+ tree.selectNode([{id: 'table-no-data'}]);
+
+ networkMock.onAny('.*').reply(200, () => {
+ });
+
+ setTimeout(() => {
+ expect(disableTriggers(tree, alertify, generateUrlSpy, {})).toBe(false);
+ }, 0);
+ });
+ });
+
+ describe('node as data', () => {
+ describe('backend responds with success', () => {
+ let networkMockCalledWith;
+ beforeEach(() => {
+ networkMockCalledWith = false;
+ networkMock.onPut(/.*/).reply((configuration) => {
+ networkMockCalledWith = configuration;
+ return [200, {
+ success: 1,
+ info: 'some information',
+ }];
+ });
+ });
+
+ it('displays an alert box with success', (done) => {
+ tree.selectNode([{id: 'table1'}]);
+ disableTriggers(tree, alertify, generateUrlSpy, {});
+ setTimeout(() => {
+ expect(alertify.success).toHaveBeenCalledWith('some information');
+ done();
+ }, 0);
+ });
+
+ it('reloads the node', (done) => {
+ disableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+ setTimeout(() => {
+ expect(tree.selected()).toEqual(['<li>table1</li>']);
+ done();
+ }, 20);
+ });
+
+ it('call backend with the correct parameters', (done) => {
+ disableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+ setTimeout(() => {
+ expect(networkMockCalledWith.data).toEqual(JSON.stringify({enable: 'false'}));
+ done();
+ }, 0);
+ });
+ });
+
+ describe('backend responds with error', () => {
+ beforeEach(() => {
+ networkMock.onPut(/.*/).reply(() => {
+ return [500, {
+ success: 0,
+ errormsg: 'some error message',
+ }];
+ });
+ });
+
+ it('displays an error alert', (done) => {
+ tree.selectNode([{id: 'table1'}]);
+ disableTriggers(tree, alertify, generateUrlSpy, {});
+ setTimeout(() => {
+ expect(alertify.error).toHaveBeenCalledWith('some error message');
+ done();
+ }, 0);
+ });
+
+ it('unload the node', (done) => {
+ disableTriggers(tree, alertify, generateUrlSpy, {item: [{id: 'table1'}]});
+
+ setTimeout(() => {
+ expect(tree.findNodeByDomElement([{id: 'table1'}]).children.length).toBe(0);
+ done();
+ }, 20);
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/tree/pgadmin_tree_node_spec.js b/web/regression/javascript/tree/pgadmin_tree_node_spec.js
new file mode 100644
index 00000000..479e515c
--- /dev/null
+++ b/web/regression/javascript/tree/pgadmin_tree_node_spec.js
@@ -0,0 +1,353 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+import {
+ getTreeNodeHierarchyFromElement,
+ getTreeNodeHierarchyFromIdentifier,
+} from '../../../pgadmin/static/js/tree/pgadmin_tree_node';
+import {TreeNode} from '../../../pgadmin/static/js/tree/tree';
+import {TreeFake} from './tree_fake';
+
+const context = describe;
+
+describe('tree#node#getTreeNodeHierarchy', () => {
+ let browser;
+ let newTree;
+ beforeEach(() => {
+ newTree = new TreeFake();
+ browser = {
+ Nodes: {
+ 'special one': {hasId: true},
+ 'child special': {hasId: true},
+ 'other type': {hasId: true},
+ 'table': {hasId: true},
+ 'partition': {hasId: true},
+ 'no id': {hasId: false},
+ },
+ };
+ browser.treeMenu = newTree;
+ });
+
+ context('getTreeNodeHierarchy is called with aciTreeNode object', () => {
+ describe('When the current node is root element', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ });
+ });
+
+ it('returns a object with the element type passed data and priority == 0', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)([{id: 'root'}]);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+
+ describe('When the current node is not of a known type', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'do not exist',
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)('root');
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node type has no id', () => {
+ beforeEach(() => {
+ newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'no id',
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)('root');
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node is at the second level', () => {
+ beforeEach(() => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ });
+ const firstChild = new TreeNode('first child', {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ }, ['root']);
+ newTree.addChild(root, firstChild);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)([{id: 'first child'}]);
+ expect(result).toEqual({
+ 'child special': {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ 'priority': 0,
+ },
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+
+ context('When tree as "special type"', () => {
+ context('When "special type" have "other type"', () => {
+ context('When "other type" have "special type"', () => {
+ describe('When retrieving lead node', () => {
+ it('does not override previous node type data', () => {
+ const rootNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+
+ const level1 = new TreeNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'other type',
+ });
+ newTree.addChild(rootNode, level1);
+
+ newTree.addChild(level1, new TreeNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ }));
+
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)([{id: 'level 2'}]);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ 'priority': 0,
+ },
+ 'other type': {
+ 'some key': 'some value',
+ '_type': 'other type',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+ });
+ });
+ });
+
+ context('When tree has table', () => {
+ context('when table has partition', () => {
+ it('returns table with partition parameters', () => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+ const level1 = new TreeNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'table',
+ });
+ newTree.addChild(root, level1);
+ newTree.addChild(level1, new TreeNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'partition',
+ 'some other key': 'some other value',
+ }));
+
+ const result = getTreeNodeHierarchyFromIdentifier.bind(browser)([{id:'level 2'}]);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ 'table': {
+ 'some key': 'expected value',
+ 'some other key': 'some other value',
+ '_type': 'partition',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+ });
+ });
+
+ context('getTreeNodeHierarchy is called with TreeNode object', () => {
+ let treeNode;
+ describe('When the current node is root element', () => {
+ beforeEach(() => {
+ treeNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+ });
+
+ it('returns a object with the element type passed data and priority == 0', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+
+ describe('When the current node is not of a known type', () => {
+ beforeEach(() => {
+ treeNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'do not exist',
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node type has no id', () => {
+ beforeEach(() => {
+ treeNode = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'no id',
+ }, []);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('When the current node is at the second level', () => {
+ beforeEach(() => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+ treeNode = new TreeNode('first child', {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ });
+ newTree.addChild(root, treeNode);
+ });
+
+ it('returns a empty object', () => {
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'child special': {
+ 'some key': 'some other value',
+ '_type': 'child special',
+ 'priority': 0,
+ },
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+
+ context('When tree as "special type"', () => {
+ context('When "special type" have "other type"', () => {
+ context('When "other type" have "special type"', () => {
+ describe('When retrieving lead node', () => {
+ it('does not override previous node type data', () => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ }, []);
+ const level1 = new TreeNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'other type',
+ });
+ newTree.addChild(root, level1);
+ treeNode = new TreeNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ });
+ newTree.addChild(level1, treeNode);
+
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'expected value',
+ '_type': 'special one',
+ 'some other key': 'some other value',
+ 'priority': 0,
+ },
+ 'other type': {
+ 'some key': 'some value',
+ '_type': 'other type',
+ 'priority': -1,
+ },
+ });
+ });
+ });
+ });
+ });
+ });
+
+ context('When tree has table', () => {
+ context('when table has partition', () => {
+ it('returns table with partition parameters', () => {
+ const root = newTree.addNewNode('root', {
+ 'some key': 'some value',
+ '_type': 'special one',
+ });
+ const level1 = newTree.addNewNode('level 1', {
+ 'some key': 'some value',
+ '_type': 'table',
+ });
+ newTree.addChild(root, level1);
+ treeNode = new TreeNode('level 2', {
+ 'some key': 'expected value',
+ '_type': 'partition',
+ 'some other key': 'some other value',
+ });
+ newTree.addChild(level1, treeNode);
+
+ const result = getTreeNodeHierarchyFromElement(browser, treeNode);
+ expect(result).toEqual({
+ 'special one': {
+ 'some key': 'some value',
+ '_type': 'special one',
+ 'priority': -1,
+ },
+ 'table': {
+ 'some key': 'expected value',
+ 'some other key': 'some other value',
+ '_type': 'partition',
+ 'priority': 0,
+ },
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/web/regression/javascript/tree/tree_fake.js b/web/regression/javascript/tree/tree_fake.js
index b285a45f..e03d71fb 100644
--- a/web/regression/javascript/tree/tree_fake.js
+++ b/web/regression/javascript/tree/tree_fake.js
@@ -10,6 +10,32 @@
import {Tree} from '../../../pgadmin/static/js/tree/tree';
export class TreeFake extends Tree {
+ static build(structure) {
+ let tree = new TreeFake();
+ let rootNode = tree.rootNode;
+
+ if (structure.children !== undefined) {
+ structure.children.forEach((child) => {
+ TreeFake.recursivelyAddNodes(tree, child, rootNode);
+ });
+ }
+
+ return tree;
+ }
+
+ static recursivelyAddNodes(tree, newNode, parent) {
+ let id = newNode.id;
+ let data = newNode.data ? newNode.data : {};
+ let domNode = newNode.domNode ? newNode.domNode : [{id: id}];
+ tree.addNewNode(id, data, domNode, tree.translateTreeNodeIdFromACITree([parent]));
+
+ if (newNode.children !== undefined) {
+ newNode.children.forEach((child) => {
+ TreeFake.recursivelyAddNodes(tree, child, newNode);
+ });
+ }
+ }
+
constructor() {
super();
this.aciTreeToOurTreeTranslator = {};
@@ -45,7 +71,7 @@ export class TreeFake extends Tree {
}
translateTreeNodeIdFromACITree(aciTreeNode) {
- if(aciTreeNode === undefined || aciTreeNode[0] === undefined) {
+ if (aciTreeNode === undefined || aciTreeNode[0] === undefined) {
return null;
}
return this.aciTreeToOurTreeTranslator[aciTreeNode[0].id];
diff --git a/web/webpack.shim.js b/web/webpack.shim.js
index 64f24336..c12e7f3b 100644
--- a/web/webpack.shim.js
+++ b/web/webpack.shim.js
@@ -130,6 +130,7 @@ var webpackShimConfig = {
'sources/utils': path.join(__dirname, './pgadmin/static/js/utils'),
'babel-polyfill': path.join(__dirname, './node_modules/babel-polyfill/dist/polyfill'),
'tools': path.join(__dirname, './pgadmin/tools/'),
+ 'pgbrowser': path.join(__dirname, './pgadmin/browser/static/js/'),
// Vendor JS
'jquery': path.join(__dirname, './node_modules/jquery/dist/jquery'),
diff --git a/web/webpack.test.config.js b/web/webpack.test.config.js
index a1fc6824..ef893e0b 100644
--- a/web/webpack.test.config.js
+++ b/web/webpack.test.config.js
@@ -81,6 +81,8 @@ module.exports = {
'pgadmin.alertifyjs': sourcesDir + '/js/alertify.pgadmin.defaults',
'pgadmin.backgrid': sourcesDir + '/js/backgrid.pgadmin',
'pgadmin.backform': sourcesDir + '/js/backform.pgadmin',
+ 'pgbrowser': path.resolve(__dirname, 'regression/javascript/fake_browser'),
+ 'pgadmin.schema.dir': path.resolve(__dirname, 'pgadmin/browser/server_groups/servers/databases/schemas/static/js'),
},
},
};
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-06-01 19:17 Victoria Henry <[email protected]>
parent: Ashesh Vashi <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Victoria Henry @ 2018-06-01 19:17 UTC (permalink / raw)
To: Ashesh Vashi <[email protected]>; +Cc: Anthony Emengo <[email protected]>; Joao De Almeida Pereira <[email protected]>; Dave Page <[email protected]>; Akshay Joshi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
Hey Ashesh,
LGTM! The some of the CI tests failed but it looks like a flake. But you
can go ahead and merge this.
Sincerely,
Victoria
On Fri, Jun 1, 2018 at 2:36 PM Ashesh Vashi <[email protected]>
wrote:
> On Fri, Jun 1, 2018 at 10:09 PM, Victoria Henry <[email protected]> wrote:
>
>> Hi Ashesh,
>>
>> We just attempted to apply your patch over master but it did not work.
>> We don't want to introduce any bugs or break any functionality. Please
>> update the patch to make sure it is synced up with the master branch.
>>
> Please find the updated patch.
>
>>
>> Sincerely,
>>
>> Victoria
>>
>> On Fri, Jun 1, 2018 at 11:18 AM Anthony Emengo <[email protected]>
>> wrote:
>>
>>> Hey Ashesh,
>>>
>>> Thanks for the explanation. It was great and it really helped!
>>>
>>> C pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js
>>> C pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js
>>>
>>> It makes sense to remove duplication by extracting these attributes out
>>> and setting the canDrop and canCreate functions here. But is it
>>> possible to combine these two files into one since they are related so we
>>> don’t need to import schema_child_tree_node?
>>>
>> That was the original plan, but 'pgadmin/browser/static/js//node.js'
> script has too many dependecies, which are not easily portable.
> And - that may lead to change the scope of the patch.
>
> Hence - I decided to use the separate file to make sure we have enough
> test coverage (which is more imprortant than changing the scope).
>
>> M pgadmin/static/js/tree/tree.js
>>>
>>> The creation of the ancestorNode function feels like a
>>> pre-optimization. That function is not used any where outside of the
>>> tree.js file, so it’s more confusing to have it defined.
>>>
>> It is being used in the latest changes. :-)
>
>
>> On a lighter note, could we avoid the !! syntax when possible? For
>>> example, instead of return !!obj, we could do something like return obj
>>> === undefined or return _.isUndefined(obj) as this is more intuitive.
>>>
>>> https://softwareengineering.stackexchange.com/a/80092
>>>
>> I am kind of disagree here. But - I have changed it anyway.
>
>> In addition, please update this patch as it is out of sync with the
>>> latest commit on the master branch. Otherwise, everything looks good!
>>>
>> Here - you go!
>
> -- Thanks, Ashesh
>
>>
>>>
>>> Thanks
>>> Anthony && Victoria
>>>
>>> On Fri, Jun 1, 2018 at 7:52 AM Ashesh Vashi <
>>> [email protected]> wrote:
>>>
>>>> On Thu, May 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
>>>> [email protected]> wrote:
>>>>
>>>>> Hey, Thanks so much for the reply.
>>>>>
>>>>> We've noticed that you've made several modifications on top of our
>>>>> original patch. Unfortunately, we've found it very hard to follow. Could we
>>>>> please get a brief synopsis of the changes you have made - just so we can
>>>>> better understand the rationale behind them? Just like we've done for you
>>>>> previously.
>>>>>
>>>> Please find the changes from your original patch:
>>>>
>>>> M webpack.shim.js
>>>> M webpack.test.config.js
>>>> - In order to specify the fake_browser in regression tests, we need to use 'pgbrowser/browser' in the 'schema_child_tree_node.js' script.D pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child.js
>>>> - We don't need this with the new implementation.C pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js
>>>> - All the children of schema node have common properties as 'parent_type', 'canDrop', 'canDropCascase', 'canCreate'.
>>>> Hence - instead of defining them in each node, we have created a base node, which will have all these properties.
>>>> And, modified all schema children node to inherit from it.C pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js
>>>> - In this script, we're defining three functions 'childCreateMenuEnabled', 'isTreeItemOfChildOfSchema', & 'isTreeNodeOfSchemaChild', which are used by the 'SchemaChildNode' objects.M pgadmin/browser/static/js/collection.js
>>>> - Fixed an issue related to wrongly defined 'error' function for the Collection object.D pgadmin/static/js/menu/can_create.js
>>>> - It defined the function, which was defining a check for creation of a schema child node, or not by looking at the parent node (i.e. a schema/catalog node).
>>>> The file was not defintely placed under the wrong directory, because - the similar logic was under 'can_drop_child.js', and it was defined under 'pgadmin/browser/server_groups/servers/databases/schemas/static/js' directory.D pgadmin/static/js/menu/menu_enabled.jsC pgadmin/static/js/nodes/supported_database_node.js
>>>> - Used by the external tools for checking whether the 'selected' tree-node is:
>>>> + 'database' node, and it is allowed to connect it.
>>>> + Or, it is one of the schema child (and, not 'catalog' child).
>>>> - Finding the correct location was difficult for this, as there is no defined pattern, also it can be used by other functions too. Hence - moved it out of 'pgadmin/static/js/menu' directory.M pgadmin/static/js/tree/tree.js
>>>> - Introduced a function, which returns the ancestor node object, fow which the condition is true.D regression/javascript/menu/can_create_spec.js
>>>> D regression/javascript/menu/menu_enabled_spec.js
>>>> D regression/javascript/schema/can_drop_child_spec.jsC regression/javascript/fake_browser/browser.js
>>>> C regression/javascript/nodes/schema/child_menu_spec.js
>>>> - Modified the regression to test the new functionalies.M pgadmin/browser/server_groups/servers/databases/schemas/**/*.js
>>>> - Extending the schema child nodes from the 'SchemaChildNode' class defined in 'pgadmin/.../schemas/static/js/child.js' script.
>>>>
>>>> Let me know if you need more information.
>>>>
>>>>
>>>>> Let's keep in mind that the original intent was simply to introduce
>>>>> this abstraction into the code base, which is a big enough task. I'd hate
>>>>> for the scope of the changes we're making to expand beyond that.
>>>>>
>>>>
>>>> I have the mutual feeling.
>>>>
>>>> -- Thanks, Ashesh
>>>>
>>>>>
>>>>> Thanks
>>>>> Joao && Anthony
>>>>>
>>>>>
>>>>> On Thu, May 24, 2018 at 2:59 AM Ashesh Vashi <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Sorry for the late reply.
>>>>>> On Wed, May 16, 2018 at 8:55 PM, Anthony Emengo <[email protected]>
>>>>>> wrote:
>>>>>>
>>>>>>> export function canCreate(pgBrowser, childOfCatalogType) {
>>>>>>> return canCreateObject.bind({
>>>>>>> browser: pgBrowser,
>>>>>>> childOfCatalogType: childOfCatalogType,
>>>>>>> });
>>>>>>> }
>>>>>>>
>>>>>>> With respect to the above code: this bind pattern looks good and
>>>>>>> seems like the idiomatic way to handle this in JavaScript. On a lighter
>>>>>>> node, I don’t even see the need for an additional method to wrap it. The
>>>>>>> invocation could have easily been like canCreate:
>>>>>>> canCreateObject.bind({ browser: pgBrowser, childOfCatalogType:
>>>>>>> childOfCatalogType }), I don’t feel too strongly here.
>>>>>>>
>>>>>> I do agree - we can handle the same problem many ways.
>>>>>> I prefer object oriented pardigm more in general.
>>>>>> Any way - I have modified the code with some other changes.
>>>>>>
>>>>>>> I renamed it as isValidTreeNodeData, because - we were using it in
>>>>>>> for testing the tree data. Please suggest me the right place, and name.
>>>>>>>
>>>>>>> We’re not sure; maybe after continued refactoring, we will come
>>>>>>> across more generic functions. At that point we can revisit this and create
>>>>>>> a utils.js file.
>>>>>>>
>>>>>> Sure.
>>>>>>
>>>>>>> The original patch was separating them in different places, but -
>>>>>>> still uses some of the functionalities directly from the tree, which was
>>>>>>> happening because we have contextual menu.
>>>>>>> To give a better solution, I can think of putting the menus related
>>>>>>> code understand ‘sources/tree/menu’ directory.
>>>>>>>
>>>>>>> We’re particularly worried because we’re trying to avoid the
>>>>>>> coupling that we see in the code base today. We want to decouple *application
>>>>>>> state* from *business domain* logic as much as we can - because
>>>>>>> this makes the code much easier to understand. We achieve lower coupling by
>>>>>>> have more suitable interfaces to retrieve *application state* like:
>>>>>>> anyParent (the menu doesn’t care how this happens). This is the
>>>>>>> direction that we’re trying to move towards, we just don’t want the package
>>>>>>> structure to undermine that developer intent.
>>>>>>>
>>>>>> I realized after revisiting the code, menu/can_create.js was only
>>>>>> applicable to the children of the schema/catalog nodes, same as
>>>>>> 'can_drop_child'.
>>>>>> We should have put both scripts in the same directory.
>>>>>>
>>>>>> Please find the updated patch for the same.
>>>>>>
>>>>>> Please review it, and let me know your concerns.
>>>>>>
>>>>>> -- Thanks, Ashesh
>>>>>>
>>>>>>> How about nodeMenu.isSupportedNode(…)?
>>>>>>>
>>>>>>> Naming is one of the hardest problems in programming. I don’t feel
>>>>>>> too strongly about this one. For now, let’s keep it as is
>>>>>>>
>>>>>>> Thanks
>>>>>>> Anthony && Victoria
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>
>>>>
>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-06-06 11:07 Aditya Toshniwal <[email protected]>
parent: Victoria Henry <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Aditya Toshniwal @ 2018-06-06 11:07 UTC (permalink / raw)
To: Victoria Henry <[email protected]>; +Cc: Ashesh Vashi <[email protected]>; Anthony Emengo <[email protected]>; Joao De Almeida Pereira <[email protected]>; Dave Page <[email protected]>; Akshay Joshi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
Hi Anthony/Victoria/Joao,
I know I am very late to review on patch 004. The idea of moving js files
from tools to static folder looks good, but I have a few suggestions:
1) Why don't we start using webpack alias's instead of using absolute path.
For eg,
import {RestoreDialogWrapper} from
'../../../pgadmin/static/js/restore/restore_dialog_wrapper';
can be used as import {RestoreDialogWrapper} from
'pgadmin_static/js/restore/restore_dialog_wrapper';
by adding pgadmin_static alias to webpack config.
2) Few of the js are left behind, the ones which are used in python
__init__.py. Can't we move them too ? It would be nicer to not to leave
behind a single js.
Kindly let me know your views on this.
Thanks and Regards,
Aditya Toshniwal
Software Engineer | EnterpriseDB Software Solutions | Pune
"Don't Complain about Heat, Plant a tree"
On Sat, Jun 2, 2018 at 12:47 AM, Victoria Henry <[email protected]> wrote:
> Hey Ashesh,
>
> LGTM! The some of the CI tests failed but it looks like a flake. But you
> can go ahead and merge this.
>
> Sincerely,
>
> Victoria
>
> On Fri, Jun 1, 2018 at 2:36 PM Ashesh Vashi <[email protected]>
> wrote:
>
>> On Fri, Jun 1, 2018 at 10:09 PM, Victoria Henry <[email protected]>
>> wrote:
>>
>>> Hi Ashesh,
>>>
>>> We just attempted to apply your patch over master but it did not work.
>>> We don't want to introduce any bugs or break any functionality. Please
>>> update the patch to make sure it is synced up with the master branch.
>>>
>> Please find the updated patch.
>>
>>>
>>> Sincerely,
>>>
>>> Victoria
>>>
>>> On Fri, Jun 1, 2018 at 11:18 AM Anthony Emengo <[email protected]>
>>> wrote:
>>>
>>>> Hey Ashesh,
>>>>
>>>> Thanks for the explanation. It was great and it really helped!
>>>>
>>>> C pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js
>>>> C pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js
>>>>
>>>> It makes sense to remove duplication by extracting these attributes out
>>>> and setting the canDrop and canCreate functions here. But is it
>>>> possible to combine these two files into one since they are related so we
>>>> don’t need to import schema_child_tree_node?
>>>>
>>> That was the original plan, but 'pgadmin/browser/static/js//node.js'
>> script has too many dependecies, which are not easily portable.
>> And - that may lead to change the scope of the patch.
>>
>> Hence - I decided to use the separate file to make sure we have enough
>> test coverage (which is more imprortant than changing the scope).
>>
>>> M pgadmin/static/js/tree/tree.js
>>>>
>>>> The creation of the ancestorNode function feels like a
>>>> pre-optimization. That function is not used any where outside of the
>>>> tree.js file, so it’s more confusing to have it defined.
>>>>
>>> It is being used in the latest changes. :-)
>>
>>
>>> On a lighter note, could we avoid the !! syntax when possible? For
>>>> example, instead of return !!obj, we could do something like return
>>>> obj === undefined or return _.isUndefined(obj) as this is more
>>>> intuitive.
>>>>
>>>> https://softwareengineering.stackexchange.com/a/80092
>>>>
>>> I am kind of disagree here. But - I have changed it anyway.
>>
>>> In addition, please update this patch as it is out of sync with the
>>>> latest commit on the master branch. Otherwise, everything looks good!
>>>>
>>> Here - you go!
>>
>> -- Thanks, Ashesh
>>
>>>
>>>>
>>>> Thanks
>>>> Anthony && Victoria
>>>>
>>>> On Fri, Jun 1, 2018 at 7:52 AM Ashesh Vashi <
>>>> [email protected]> wrote:
>>>>
>>>>> On Thu, May 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Hey, Thanks so much for the reply.
>>>>>>
>>>>>> We've noticed that you've made several modifications on top of our
>>>>>> original patch. Unfortunately, we've found it very hard to follow. Could we
>>>>>> please get a brief synopsis of the changes you have made - just so we can
>>>>>> better understand the rationale behind them? Just like we've done for you
>>>>>> previously.
>>>>>>
>>>>> Please find the changes from your original patch:
>>>>>
>>>>> M webpack.shim.js
>>>>> M webpack.test.config.js
>>>>> - In order to specify the fake_browser in regression tests, we need to use 'pgbrowser/browser' in the 'schema_child_tree_node.js' script.D pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child.js
>>>>> - We don't need this with the new implementation.C pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js
>>>>> - All the children of schema node have common properties as 'parent_type', 'canDrop', 'canDropCascase', 'canCreate'.
>>>>> Hence - instead of defining them in each node, we have created a base node, which will have all these properties.
>>>>> And, modified all schema children node to inherit from it.C pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js
>>>>> - In this script, we're defining three functions 'childCreateMenuEnabled', 'isTreeItemOfChildOfSchema', & 'isTreeNodeOfSchemaChild', which are used by the 'SchemaChildNode' objects.M pgadmin/browser/static/js/collection.js
>>>>> - Fixed an issue related to wrongly defined 'error' function for the Collection object.D pgadmin/static/js/menu/can_create.js
>>>>> - It defined the function, which was defining a check for creation of a schema child node, or not by looking at the parent node (i.e. a schema/catalog node).
>>>>> The file was not defintely placed under the wrong directory, because - the similar logic was under 'can_drop_child.js', and it was defined under 'pgadmin/browser/server_groups/servers/databases/schemas/static/js' directory.D pgadmin/static/js/menu/menu_enabled.jsC pgadmin/static/js/nodes/supported_database_node.js
>>>>> - Used by the external tools for checking whether the 'selected' tree-node is:
>>>>> + 'database' node, and it is allowed to connect it.
>>>>> + Or, it is one of the schema child (and, not 'catalog' child).
>>>>> - Finding the correct location was difficult for this, as there is no defined pattern, also it can be used by other functions too. Hence - moved it out of 'pgadmin/static/js/menu' directory.M pgadmin/static/js/tree/tree.js
>>>>> - Introduced a function, which returns the ancestor node object, fow which the condition is true.D regression/javascript/menu/can_create_spec.js
>>>>> D regression/javascript/menu/menu_enabled_spec.js
>>>>> D regression/javascript/schema/can_drop_child_spec.jsC regression/javascript/fake_browser/browser.js
>>>>> C regression/javascript/nodes/schema/child_menu_spec.js
>>>>> - Modified the regression to test the new functionalies.M pgadmin/browser/server_groups/servers/databases/schemas/**/*.js
>>>>> - Extending the schema child nodes from the 'SchemaChildNode' class defined in 'pgadmin/.../schemas/static/js/child.js' script.
>>>>>
>>>>> Let me know if you need more information.
>>>>>
>>>>>
>>>>>> Let's keep in mind that the original intent was simply to introduce
>>>>>> this abstraction into the code base, which is a big enough task. I'd hate
>>>>>> for the scope of the changes we're making to expand beyond that.
>>>>>>
>>>>>
>>>>> I have the mutual feeling.
>>>>>
>>>>> -- Thanks, Ashesh
>>>>>
>>>>>>
>>>>>> Thanks
>>>>>> Joao && Anthony
>>>>>>
>>>>>>
>>>>>> On Thu, May 24, 2018 at 2:59 AM Ashesh Vashi <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Sorry for the late reply.
>>>>>>> On Wed, May 16, 2018 at 8:55 PM, Anthony Emengo <[email protected]>
>>>>>>> wrote:
>>>>>>>
>>>>>>>> export function canCreate(pgBrowser, childOfCatalogType) {
>>>>>>>> return canCreateObject.bind({
>>>>>>>> browser: pgBrowser,
>>>>>>>> childOfCatalogType: childOfCatalogType,
>>>>>>>> });
>>>>>>>> }
>>>>>>>>
>>>>>>>> With respect to the above code: this bind pattern looks good and
>>>>>>>> seems like the idiomatic way to handle this in JavaScript. On a lighter
>>>>>>>> node, I don’t even see the need for an additional method to wrap it. The
>>>>>>>> invocation could have easily been like canCreate:
>>>>>>>> canCreateObject.bind({ browser: pgBrowser, childOfCatalogType:
>>>>>>>> childOfCatalogType }), I don’t feel too strongly here.
>>>>>>>>
>>>>>>> I do agree - we can handle the same problem many ways.
>>>>>>> I prefer object oriented pardigm more in general.
>>>>>>> Any way - I have modified the code with some other changes.
>>>>>>>
>>>>>>>> I renamed it as isValidTreeNodeData, because - we were using it in
>>>>>>>> for testing the tree data. Please suggest me the right place, and name.
>>>>>>>>
>>>>>>>> We’re not sure; maybe after continued refactoring, we will come
>>>>>>>> across more generic functions. At that point we can revisit this and create
>>>>>>>> a utils.js file.
>>>>>>>>
>>>>>>> Sure.
>>>>>>>
>>>>>>>> The original patch was separating them in different places, but -
>>>>>>>> still uses some of the functionalities directly from the tree, which was
>>>>>>>> happening because we have contextual menu.
>>>>>>>> To give a better solution, I can think of putting the menus related
>>>>>>>> code understand ‘sources/tree/menu’ directory.
>>>>>>>>
>>>>>>>> We’re particularly worried because we’re trying to avoid the
>>>>>>>> coupling that we see in the code base today. We want to decouple *application
>>>>>>>> state* from *business domain* logic as much as we can - because
>>>>>>>> this makes the code much easier to understand. We achieve lower coupling by
>>>>>>>> have more suitable interfaces to retrieve *application state*
>>>>>>>> like: anyParent (the menu doesn’t care how this happens). This is
>>>>>>>> the direction that we’re trying to move towards, we just don’t want the
>>>>>>>> package structure to undermine that developer intent.
>>>>>>>>
>>>>>>> I realized after revisiting the code, menu/can_create.js was only
>>>>>>> applicable to the children of the schema/catalog nodes, same as
>>>>>>> 'can_drop_child'.
>>>>>>> We should have put both scripts in the same directory.
>>>>>>>
>>>>>>> Please find the updated patch for the same.
>>>>>>>
>>>>>>> Please review it, and let me know your concerns.
>>>>>>>
>>>>>>> -- Thanks, Ashesh
>>>>>>>
>>>>>>>> How about nodeMenu.isSupportedNode(…)?
>>>>>>>>
>>>>>>>> Naming is one of the hardest problems in programming. I don’t feel
>>>>>>>> too strongly about this one. For now, let’s keep it as is
>>>>>>>>
>>>>>>>> Thanks
>>>>>>>> Anthony && Victoria
>>>>>>>>
>>>>>>>>
>>>>>>>>
>>>>>>>>
>>>>>>>>
>>>>>>>
>>>>>
>>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-06-06 15:25 Victoria Henry <[email protected]>
parent: Aditya Toshniwal <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Victoria Henry @ 2018-06-06 15:25 UTC (permalink / raw)
To: Aditya Toshniwal <[email protected]>; +Cc: Ashesh Vashi <[email protected]>; Anthony Emengo <[email protected]>; Joao De Almeida Pereira <[email protected]>; Dave Page <[email protected]>; Akshay Joshi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
Hi Aditya,
1) Why don't we start using webpack alias's instead of using absolute path.
> For eg,
> import {RestoreDialogWrapper} from
> '../../../pgadmin/static/js/restore/restore_dialog_wrapper';
> can be used as import {RestoreDialogWrapper} from
> 'pgadmin_static/js/restore/restore_dialog_wrapper';
> by adding pgadmin_static alias to webpack config.
Great point. In some areas of the code, we began making this change. There
is already an alias in webpack shims for `../../../pgadmin/static/js`
called `sources`. You can find an example of this in import statements for
`supported_database_node.js`
2) Few of the js are left behind, the ones which are used in python
> __init__.py. Can't we move them too ? It would be nicer to not to leave
> behind a single js.
I'm not sure what you mean. Could you point to an example of a single js
file?
Sincerely,
Victoria
On Wed, Jun 6, 2018 at 7:07 AM Aditya Toshniwal <
[email protected]> wrote:
> Hi Anthony/Victoria/Joao,
>
> I know I am very late to review on patch 004. The idea of moving js files
> from tools to static folder looks good, but I have a few suggestions:
>
> 1) Why don't we start using webpack alias's instead of using absolute
> path. For eg,
> import {RestoreDialogWrapper} from
> '../../../pgadmin/static/js/restore/restore_dialog_wrapper';
> can be used as import {RestoreDialogWrapper} from
> 'pgadmin_static/js/restore/restore_dialog_wrapper';
> by adding pgadmin_static alias to webpack config.
>
> 2) Few of the js are left behind, the ones which are used in python
> __init__.py. Can't we move them too ? It would be nicer to not to leave
> behind a single js.
>
> Kindly let me know your views on this.
>
>
> Thanks and Regards,
> Aditya Toshniwal
> Software Engineer | EnterpriseDB Software Solutions | Pune
> "Don't Complain about Heat, Plant a tree"
>
> On Sat, Jun 2, 2018 at 12:47 AM, Victoria Henry <[email protected]> wrote:
>
>> Hey Ashesh,
>>
>> LGTM! The some of the CI tests failed but it looks like a flake. But
>> you can go ahead and merge this.
>>
>> Sincerely,
>>
>> Victoria
>>
>> On Fri, Jun 1, 2018 at 2:36 PM Ashesh Vashi <
>> [email protected]> wrote:
>>
>>> On Fri, Jun 1, 2018 at 10:09 PM, Victoria Henry <[email protected]>
>>> wrote:
>>>
>>>> Hi Ashesh,
>>>>
>>>> We just attempted to apply your patch over master but it did not work.
>>>> We don't want to introduce any bugs or break any functionality. Please
>>>> update the patch to make sure it is synced up with the master branch.
>>>>
>>> Please find the updated patch.
>>>
>>>>
>>>> Sincerely,
>>>>
>>>> Victoria
>>>>
>>>> On Fri, Jun 1, 2018 at 11:18 AM Anthony Emengo <[email protected]>
>>>> wrote:
>>>>
>>>>> Hey Ashesh,
>>>>>
>>>>> Thanks for the explanation. It was great and it really helped!
>>>>>
>>>>> C pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js
>>>>> C pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js
>>>>>
>>>>> It makes sense to remove duplication by extracting these attributes
>>>>> out and setting the canDrop and canCreate functions here. But is it
>>>>> possible to combine these two files into one since they are related so we
>>>>> don’t need to import schema_child_tree_node?
>>>>>
>>>> That was the original plan, but 'pgadmin/browser/static/js//node.js'
>>> script has too many dependecies, which are not easily portable.
>>> And - that may lead to change the scope of the patch.
>>>
>>> Hence - I decided to use the separate file to make sure we have enough
>>> test coverage (which is more imprortant than changing the scope).
>>>
>>>> M pgadmin/static/js/tree/tree.js
>>>>>
>>>>> The creation of the ancestorNode function feels like a
>>>>> pre-optimization. That function is not used any where outside of the
>>>>> tree.js file, so it’s more confusing to have it defined.
>>>>>
>>>> It is being used in the latest changes. :-)
>>>
>>>
>>>> On a lighter note, could we avoid the !! syntax when possible? For
>>>>> example, instead of return !!obj, we could do something like return
>>>>> obj === undefined or return _.isUndefined(obj) as this is more
>>>>> intuitive.
>>>>>
>>>>> https://softwareengineering.stackexchange.com/a/80092
>>>>>
>>>> I am kind of disagree here. But - I have changed it anyway.
>>>
>>>> In addition, please update this patch as it is out of sync with the
>>>>> latest commit on the master branch. Otherwise, everything looks good!
>>>>>
>>>> Here - you go!
>>>
>>> -- Thanks, Ashesh
>>>
>>>>
>>>>>
>>>>> Thanks
>>>>> Anthony && Victoria
>>>>>
>>>>> On Fri, Jun 1, 2018 at 7:52 AM Ashesh Vashi <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> On Thu, May 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hey, Thanks so much for the reply.
>>>>>>>
>>>>>>> We've noticed that you've made several modifications on top of our
>>>>>>> original patch. Unfortunately, we've found it very hard to follow. Could we
>>>>>>> please get a brief synopsis of the changes you have made - just so we can
>>>>>>> better understand the rationale behind them? Just like we've done for you
>>>>>>> previously.
>>>>>>>
>>>>>> Please find the changes from your original patch:
>>>>>>
>>>>>> M webpack.shim.js
>>>>>> M webpack.test.config.js
>>>>>> - In order to specify the fake_browser in regression tests, we need to use 'pgbrowser/browser' in the 'schema_child_tree_node.js' script.D pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child.js
>>>>>> - We don't need this with the new implementation.C pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js
>>>>>> - All the children of schema node have common properties as 'parent_type', 'canDrop', 'canDropCascase', 'canCreate'.
>>>>>> Hence - instead of defining them in each node, we have created a base node, which will have all these properties.
>>>>>> And, modified all schema children node to inherit from it.C pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js
>>>>>> - In this script, we're defining three functions 'childCreateMenuEnabled', 'isTreeItemOfChildOfSchema', & 'isTreeNodeOfSchemaChild', which are used by the 'SchemaChildNode' objects.M pgadmin/browser/static/js/collection.js
>>>>>> - Fixed an issue related to wrongly defined 'error' function for the Collection object.D pgadmin/static/js/menu/can_create.js
>>>>>> - It defined the function, which was defining a check for creation of a schema child node, or not by looking at the parent node (i.e. a schema/catalog node).
>>>>>> The file was not defintely placed under the wrong directory, because - the similar logic was under 'can_drop_child.js', and it was defined under 'pgadmin/browser/server_groups/servers/databases/schemas/static/js' directory.D pgadmin/static/js/menu/menu_enabled.jsC pgadmin/static/js/nodes/supported_database_node.js
>>>>>> - Used by the external tools for checking whether the 'selected' tree-node is:
>>>>>> + 'database' node, and it is allowed to connect it.
>>>>>> + Or, it is one of the schema child (and, not 'catalog' child).
>>>>>> - Finding the correct location was difficult for this, as there is no defined pattern, also it can be used by other functions too. Hence - moved it out of 'pgadmin/static/js/menu' directory.M pgadmin/static/js/tree/tree.js
>>>>>> - Introduced a function, which returns the ancestor node object, fow which the condition is true.D regression/javascript/menu/can_create_spec.js
>>>>>> D regression/javascript/menu/menu_enabled_spec.js
>>>>>> D regression/javascript/schema/can_drop_child_spec.jsC regression/javascript/fake_browser/browser.js
>>>>>> C regression/javascript/nodes/schema/child_menu_spec.js
>>>>>> - Modified the regression to test the new functionalies.M pgadmin/browser/server_groups/servers/databases/schemas/**/*.js
>>>>>> - Extending the schema child nodes from the 'SchemaChildNode' class defined in 'pgadmin/.../schemas/static/js/child.js' script.
>>>>>>
>>>>>> Let me know if you need more information.
>>>>>>
>>>>>>
>>>>>>> Let's keep in mind that the original intent was simply to introduce
>>>>>>> this abstraction into the code base, which is a big enough task. I'd hate
>>>>>>> for the scope of the changes we're making to expand beyond that.
>>>>>>>
>>>>>>
>>>>>> I have the mutual feeling.
>>>>>>
>>>>>> -- Thanks, Ashesh
>>>>>>
>>>>>>>
>>>>>>> Thanks
>>>>>>> Joao && Anthony
>>>>>>>
>>>>>>>
>>>>>>> On Thu, May 24, 2018 at 2:59 AM Ashesh Vashi <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Sorry for the late reply.
>>>>>>>> On Wed, May 16, 2018 at 8:55 PM, Anthony Emengo <[email protected]
>>>>>>>> > wrote:
>>>>>>>>
>>>>>>>>> export function canCreate(pgBrowser, childOfCatalogType) {
>>>>>>>>> return canCreateObject.bind({
>>>>>>>>> browser: pgBrowser,
>>>>>>>>> childOfCatalogType: childOfCatalogType,
>>>>>>>>> });
>>>>>>>>> }
>>>>>>>>>
>>>>>>>>> With respect to the above code: this bind pattern looks good and
>>>>>>>>> seems like the idiomatic way to handle this in JavaScript. On a lighter
>>>>>>>>> node, I don’t even see the need for an additional method to wrap it. The
>>>>>>>>> invocation could have easily been like canCreate:
>>>>>>>>> canCreateObject.bind({ browser: pgBrowser, childOfCatalogType:
>>>>>>>>> childOfCatalogType }), I don’t feel too strongly here.
>>>>>>>>>
>>>>>>>> I do agree - we can handle the same problem many ways.
>>>>>>>> I prefer object oriented pardigm more in general.
>>>>>>>> Any way - I have modified the code with some other changes.
>>>>>>>>
>>>>>>>>> I renamed it as isValidTreeNodeData, because - we were using it in
>>>>>>>>> for testing the tree data. Please suggest me the right place, and name.
>>>>>>>>>
>>>>>>>>> We’re not sure; maybe after continued refactoring, we will come
>>>>>>>>> across more generic functions. At that point we can revisit this and create
>>>>>>>>> a utils.js file.
>>>>>>>>>
>>>>>>>> Sure.
>>>>>>>>
>>>>>>>>> The original patch was separating them in different places, but -
>>>>>>>>> still uses some of the functionalities directly from the tree, which was
>>>>>>>>> happening because we have contextual menu.
>>>>>>>>> To give a better solution, I can think of putting the menus
>>>>>>>>> related code understand ‘sources/tree/menu’ directory.
>>>>>>>>>
>>>>>>>>> We’re particularly worried because we’re trying to avoid the
>>>>>>>>> coupling that we see in the code base today. We want to decouple *application
>>>>>>>>> state* from *business domain* logic as much as we can - because
>>>>>>>>> this makes the code much easier to understand. We achieve lower coupling by
>>>>>>>>> have more suitable interfaces to retrieve *application state*
>>>>>>>>> like: anyParent (the menu doesn’t care how this happens). This is
>>>>>>>>> the direction that we’re trying to move towards, we just don’t want the
>>>>>>>>> package structure to undermine that developer intent.
>>>>>>>>>
>>>>>>>> I realized after revisiting the code, menu/can_create.js was only
>>>>>>>> applicable to the children of the schema/catalog nodes, same as
>>>>>>>> 'can_drop_child'.
>>>>>>>> We should have put both scripts in the same directory.
>>>>>>>>
>>>>>>>> Please find the updated patch for the same.
>>>>>>>>
>>>>>>>> Please review it, and let me know your concerns.
>>>>>>>>
>>>>>>>> -- Thanks, Ashesh
>>>>>>>>
>>>>>>>>> How about nodeMenu.isSupportedNode(…)?
>>>>>>>>>
>>>>>>>>> Naming is one of the hardest problems in programming. I don’t feel
>>>>>>>>> too strongly about this one. For now, let’s keep it as is
>>>>>>>>>
>>>>>>>>> Thanks
>>>>>>>>> Anthony && Victoria
>>>>>>>>>
>>>>>>>>>
>>>>>>>>>
>>>>>>>>>
>>>>>>>>>
>>>>>>>>
>>>>>>
>>>
>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-06-07 04:35 Aditya Toshniwal <[email protected]>
parent: Victoria Henry <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Aditya Toshniwal @ 2018-06-07 04:35 UTC (permalink / raw)
To: Victoria Henry <[email protected]>; +Cc: Ashesh Vashi <[email protected]>; Anthony Emengo <[email protected]>; Joao De Almeida Pereira <[email protected]>; Dave Page <[email protected]>; Akshay Joshi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
Hi Victoria,
On Wed, Jun 6, 2018 at 8:55 PM, Victoria Henry <[email protected]> wrote:
> Hi Aditya,
>
> 1) Why don't we start using webpack alias's instead of using absolute
>> path. For eg,
>> import {RestoreDialogWrapper} from '../../../pgadmin/static/js/
>> restore/restore_dialog_wrapper';
>> can be used as import {RestoreDialogWrapper} from
>> 'pgadmin_static/js/restore/restore_dialog_wrapper';
>> by adding pgadmin_static alias to webpack config.
>
>
> Great point. In some areas of the code, we began making this change.
> There is already an alias in webpack shims for `../../../pgadmin/static/js`
> called `sources`. You can find an example of this in import statements for
> `supported_database_node.js`
>
> 2) Few of the js are left behind, the ones which are used in python
>> __init__.py. Can't we move them too ? It would be nicer to not to leave
>> behind a single js.
>
> I'm not sure what you mean. Could you point to an example of a single js
> file?
>
Sure. I did not find moving
web/pgadmin/tools/datagrid/static/js/datagrid.js. Please correct me if I am
missing anything.
>
> Sincerely,
>
> Victoria
>
> On Wed, Jun 6, 2018 at 7:07 AM Aditya Toshniwal <aditya.toshniwal@
> enterprisedb.com> wrote:
>
>> Hi Anthony/Victoria/Joao,
>>
>> I know I am very late to review on patch 004. The idea of moving js files
>> from tools to static folder looks good, but I have a few suggestions:
>>
>> 1) Why don't we start using webpack alias's instead of using absolute
>> path. For eg,
>> import {RestoreDialogWrapper} from '../../../pgadmin/static/js/
>> restore/restore_dialog_wrapper';
>> can be used as import {RestoreDialogWrapper} from
>> 'pgadmin_static/js/restore/restore_dialog_wrapper';
>> by adding pgadmin_static alias to webpack config.
>>
>> 2) Few of the js are left behind, the ones which are used in python
>> __init__.py. Can't we move them too ? It would be nicer to not to leave
>> behind a single js.
>>
>> Kindly let me know your views on this.
>>
>>
>> Thanks and Regards,
>> Aditya Toshniwal
>> Software Engineer | EnterpriseDB Software Solutions | Pune
>> "Don't Complain about Heat, Plant a tree"
>>
>> On Sat, Jun 2, 2018 at 12:47 AM, Victoria Henry <[email protected]>
>> wrote:
>>
>>> Hey Ashesh,
>>>
>>> LGTM! The some of the CI tests failed but it looks like a flake. But
>>> you can go ahead and merge this.
>>>
>>> Sincerely,
>>>
>>> Victoria
>>>
>>> On Fri, Jun 1, 2018 at 2:36 PM Ashesh Vashi <
>>> [email protected]> wrote:
>>>
>>>> On Fri, Jun 1, 2018 at 10:09 PM, Victoria Henry <[email protected]>
>>>> wrote:
>>>>
>>>>> Hi Ashesh,
>>>>>
>>>>> We just attempted to apply your patch over master but it did not
>>>>> work. We don't want to introduce any bugs or break any functionality.
>>>>> Please update the patch to make sure it is synced up with the master branch.
>>>>>
>>>> Please find the updated patch.
>>>>
>>>>>
>>>>> Sincerely,
>>>>>
>>>>> Victoria
>>>>>
>>>>> On Fri, Jun 1, 2018 at 11:18 AM Anthony Emengo <[email protected]>
>>>>> wrote:
>>>>>
>>>>>> Hey Ashesh,
>>>>>>
>>>>>> Thanks for the explanation. It was great and it really helped!
>>>>>>
>>>>>> C pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js
>>>>>> C pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js
>>>>>>
>>>>>> It makes sense to remove duplication by extracting these attributes
>>>>>> out and setting the canDrop and canCreate functions here. But is it
>>>>>> possible to combine these two files into one since they are related so we
>>>>>> don’t need to import schema_child_tree_node?
>>>>>>
>>>>> That was the original plan, but 'pgadmin/browser/static/js//node.js'
>>>> script has too many dependecies, which are not easily portable.
>>>> And - that may lead to change the scope of the patch.
>>>>
>>>> Hence - I decided to use the separate file to make sure we have enough
>>>> test coverage (which is more imprortant than changing the scope).
>>>>
>>>>> M pgadmin/static/js/tree/tree.js
>>>>>>
>>>>>> The creation of the ancestorNode function feels like a
>>>>>> pre-optimization. That function is not used any where outside of the
>>>>>> tree.js file, so it’s more confusing to have it defined.
>>>>>>
>>>>> It is being used in the latest changes. :-)
>>>>
>>>>
>>>>> On a lighter note, could we avoid the !! syntax when possible? For
>>>>>> example, instead of return !!obj, we could do something like return
>>>>>> obj === undefined or return _.isUndefined(obj) as this is more
>>>>>> intuitive.
>>>>>>
>>>>>> https://softwareengineering.stackexchange.com/a/80092
>>>>>>
>>>>> I am kind of disagree here. But - I have changed it anyway.
>>>>
>>>>> In addition, please update this patch as it is out of sync with the
>>>>>> latest commit on the master branch. Otherwise, everything looks good!
>>>>>>
>>>>> Here - you go!
>>>>
>>>> -- Thanks, Ashesh
>>>>
>>>>>
>>>>>>
>>>>>> Thanks
>>>>>> Anthony && Victoria
>>>>>>
>>>>>> On Fri, Jun 1, 2018 at 7:52 AM Ashesh Vashi <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> On Thu, May 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hey, Thanks so much for the reply.
>>>>>>>>
>>>>>>>> We've noticed that you've made several modifications on top of our
>>>>>>>> original patch. Unfortunately, we've found it very hard to follow. Could we
>>>>>>>> please get a brief synopsis of the changes you have made - just so we can
>>>>>>>> better understand the rationale behind them? Just like we've done for you
>>>>>>>> previously.
>>>>>>>>
>>>>>>> Please find the changes from your original patch:
>>>>>>>
>>>>>>> M webpack.shim.js
>>>>>>> M webpack.test.config.js
>>>>>>> - In order to specify the fake_browser in regression tests, we need to use 'pgbrowser/browser' in the 'schema_child_tree_node.js' script.D pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child.js
>>>>>>> - We don't need this with the new implementation.C pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js
>>>>>>> - All the children of schema node have common properties as 'parent_type', 'canDrop', 'canDropCascase', 'canCreate'.
>>>>>>> Hence - instead of defining them in each node, we have created a base node, which will have all these properties.
>>>>>>> And, modified all schema children node to inherit from it.C pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js
>>>>>>> - In this script, we're defining three functions 'childCreateMenuEnabled', 'isTreeItemOfChildOfSchema', & 'isTreeNodeOfSchemaChild', which are used by the 'SchemaChildNode' objects.M pgadmin/browser/static/js/collection.js
>>>>>>> - Fixed an issue related to wrongly defined 'error' function for the Collection object.D pgadmin/static/js/menu/can_create.js
>>>>>>> - It defined the function, which was defining a check for creation of a schema child node, or not by looking at the parent node (i.e. a schema/catalog node).
>>>>>>> The file was not defintely placed under the wrong directory, because - the similar logic was under 'can_drop_child.js', and it was defined under 'pgadmin/browser/server_groups/servers/databases/schemas/static/js' directory.D pgadmin/static/js/menu/menu_enabled.jsC pgadmin/static/js/nodes/supported_database_node.js
>>>>>>> - Used by the external tools for checking whether the 'selected' tree-node is:
>>>>>>> + 'database' node, and it is allowed to connect it.
>>>>>>> + Or, it is one of the schema child (and, not 'catalog' child).
>>>>>>> - Finding the correct location was difficult for this, as there is no defined pattern, also it can be used by other functions too. Hence - moved it out of 'pgadmin/static/js/menu' directory.M pgadmin/static/js/tree/tree.js
>>>>>>> - Introduced a function, which returns the ancestor node object, fow which the condition is true.D regression/javascript/menu/can_create_spec.js
>>>>>>> D regression/javascript/menu/menu_enabled_spec.js
>>>>>>> D regression/javascript/schema/can_drop_child_spec.jsC regression/javascript/fake_browser/browser.js
>>>>>>> C regression/javascript/nodes/schema/child_menu_spec.js
>>>>>>> - Modified the regression to test the new functionalies.M pgadmin/browser/server_groups/servers/databases/schemas/**/*.js
>>>>>>> - Extending the schema child nodes from the 'SchemaChildNode' class defined in 'pgadmin/.../schemas/static/js/child.js' script.
>>>>>>>
>>>>>>> Let me know if you need more information.
>>>>>>>
>>>>>>>
>>>>>>>> Let's keep in mind that the original intent was simply to introduce
>>>>>>>> this abstraction into the code base, which is a big enough task. I'd hate
>>>>>>>> for the scope of the changes we're making to expand beyond that.
>>>>>>>>
>>>>>>>
>>>>>>> I have the mutual feeling.
>>>>>>>
>>>>>>> -- Thanks, Ashesh
>>>>>>>
>>>>>>>>
>>>>>>>> Thanks
>>>>>>>> Joao && Anthony
>>>>>>>>
>>>>>>>>
>>>>>>>> On Thu, May 24, 2018 at 2:59 AM Ashesh Vashi <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Sorry for the late reply.
>>>>>>>>> On Wed, May 16, 2018 at 8:55 PM, Anthony Emengo <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> export function canCreate(pgBrowser, childOfCatalogType) {
>>>>>>>>>> return canCreateObject.bind({
>>>>>>>>>> browser: pgBrowser,
>>>>>>>>>> childOfCatalogType: childOfCatalogType,
>>>>>>>>>> });
>>>>>>>>>> }
>>>>>>>>>>
>>>>>>>>>> With respect to the above code: this bind pattern looks good and
>>>>>>>>>> seems like the idiomatic way to handle this in JavaScript. On a lighter
>>>>>>>>>> node, I don’t even see the need for an additional method to wrap it. The
>>>>>>>>>> invocation could have easily been like canCreate:
>>>>>>>>>> canCreateObject.bind({ browser: pgBrowser, childOfCatalogType:
>>>>>>>>>> childOfCatalogType }), I don’t feel too strongly here.
>>>>>>>>>>
>>>>>>>>> I do agree - we can handle the same problem many ways.
>>>>>>>>> I prefer object oriented pardigm more in general.
>>>>>>>>> Any way - I have modified the code with some other changes.
>>>>>>>>>
>>>>>>>>>> I renamed it as isValidTreeNodeData, because - we were using it
>>>>>>>>>> in for testing the tree data. Please suggest me the right place, and name.
>>>>>>>>>>
>>>>>>>>>> We’re not sure; maybe after continued refactoring, we will come
>>>>>>>>>> across more generic functions. At that point we can revisit this and create
>>>>>>>>>> a utils.js file.
>>>>>>>>>>
>>>>>>>>> Sure.
>>>>>>>>>
>>>>>>>>>> The original patch was separating them in different places, but -
>>>>>>>>>> still uses some of the functionalities directly from the tree, which was
>>>>>>>>>> happening because we have contextual menu.
>>>>>>>>>> To give a better solution, I can think of putting the menus
>>>>>>>>>> related code understand ‘sources/tree/menu’ directory.
>>>>>>>>>>
>>>>>>>>>> We’re particularly worried because we’re trying to avoid the
>>>>>>>>>> coupling that we see in the code base today. We want to decouple *application
>>>>>>>>>> state* from *business domain* logic as much as we can - because
>>>>>>>>>> this makes the code much easier to understand. We achieve lower coupling by
>>>>>>>>>> have more suitable interfaces to retrieve *application state*
>>>>>>>>>> like: anyParent (the menu doesn’t care how this happens). This
>>>>>>>>>> is the direction that we’re trying to move towards, we just don’t want the
>>>>>>>>>> package structure to undermine that developer intent.
>>>>>>>>>>
>>>>>>>>> I realized after revisiting the code, menu/can_create.js was only
>>>>>>>>> applicable to the children of the schema/catalog nodes, same as
>>>>>>>>> 'can_drop_child'.
>>>>>>>>> We should have put both scripts in the same directory.
>>>>>>>>>
>>>>>>>>> Please find the updated patch for the same.
>>>>>>>>>
>>>>>>>>> Please review it, and let me know your concerns.
>>>>>>>>>
>>>>>>>>> -- Thanks, Ashesh
>>>>>>>>>
>>>>>>>>>> How about nodeMenu.isSupportedNode(…)?
>>>>>>>>>>
>>>>>>>>>> Naming is one of the hardest problems in programming. I don’t
>>>>>>>>>> feel too strongly about this one. For now, let’s keep it as is
>>>>>>>>>>
>>>>>>>>>> Thanks
>>>>>>>>>> Anthony && Victoria
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>
>>>>>>>
>>>>
>>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-06-07 07:41 Khushboo Vashi <[email protected]>
parent: Joao De Almeida Pereira <[email protected]>
2 siblings, 0 replies; 69+ messages in thread
From: Khushboo Vashi @ 2018-06-07 07:41 UTC (permalink / raw)
To: Joao De Almeida Pereira <[email protected]>; +Cc: Dave Page <[email protected]>; Ashesh Vashi <[email protected]>; Anthony Emengo <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
Hi All,
On Sat, May 5, 2018 at 3:31 AM, Joao De Almeida Pereira <
[email protected]> wrote:
> Hi Hackers,
>
> *Be prepared this is going to be a big email.*
> Objectives of this patch:
>
> 1. Start the work to split ACI from pgAdmin code
> 2. Add more tests around the front end components
> 3. Start the discussion on application architecture
>
> 1. Start the work to split ACI from pgAdmin code Why
>
> This journey started on our first attempt to store state of the current
> tree, so that when a user accessed again pgAdmin
> there would be no need to reopen all the nodes and make the flow better.
> Another problem users brought to us was related
> to the UI behavior when the number of objects was too big.
>
> In order to do implement this features we would have to play around with
> aciTree or some functions from our code that
> called and leveraged the aciTree functionalities. To do this we needed to
> have some confidence that our changes would
> implement correctly the new feature and it would not break the current
> functionality.
> The process that we used was first extract the functions that interacted
> with the tree, wrap them with
> tests and then refactor them in some way. While doing this work we
> realized that the tree started spreading roots
> throughout the majority of the application.
> How
>
> Options:
>
> 1. Rewrite the front end
> 2. One bang replace ACI Tree without change on the application
> 3. Create an abstraction layer between ACI Tree and the application
>
> 1. Rewrite the front end
> Pros Const
> Achieve decoupling by recreating the frontend Cost is too high
> Do not rely on deprecated or unmaintained libraries Turn around is too
> long
> One shot change 2. One bang replace ACI Tree
> Pros Const
> Achieve decoupling by recreating the frontend Cost is too high
> Does not achieve the decoupling unless we change the application
> Need to create an adaptor
> No garantee of success
> One shot change 3. Create an abstraction layer between ACI Tree and the
> application
> Pros Const
> Achieve decoupling of the application and the tree 90% of the time on
> Code archaeology
> Lower cost
> Ability to change or keep ACI Tree, decision TBD
> Can be done in a iterative way
>
> We decided that options 3 looked more attractive specially because we
> could do it in a iterative way.
>
> The next image depicts the current state of the interactions between the
> application and the ACI and the place we want
> to be in.
>
> [image: Screen Shot 2018-05-04 at 4.06.17 PM.png]
>
> for retrieving of data from the ACI Tree that then is used in different
> ways.
>
> Because we are doing this in a iterative process we need to keep the
> current tree working and create our adaptor next
> to it. In order to do that we created a new property on PGBrowser class
> called treeMenu and when ACI Tree receives
> the node information we also populate the new Tree with all the needed
> information.
>
> This approach allow us not to worry, for now, with data retrieval, URL
> system and other issues that should be addressed
> in the future when the adaptor is done.
>
> This first patch tries to deal with the low hanging fruit, functions that
> are triggered by events from ACI Tree.
> Cases like registering to events and triggering events need to be handled
> in the future as well.
> 2. Add more tests around the front end components Why
>
> Go Fast Forever: https://builttoadapt.io/why-tdd-489fdcdda05e
> I think this is a very good summary of why do we need tests in our
> applications.
> 3. Start the discussion on application architecture
>
> Why should we care about location of files inside a our application?
>
> Why is this way better the another?
>
> These are 2 good questions that have very lengthy answers. Trying to more
> or less summarize the answers we care about
> the location of the files, because we want our application to communicate
> intent and there are always pros and cons
> on all the decisions that we make.
>
> At this point the application structure follows our menu, this approach
> eventually make is easier to follow the code
> but at the same time if the menu changes down the line, will we change the
> structure of our folders?
>
Right, currently it is very easy to follow the code as it is menu driven.
We can easily find the particular database object (node) and do whatever
required within that folder only.
We can also find the parent and its child nodes very easily, for this we
don't need to look into the code to find out who is parent and who is
child. So, it is a huge advantage in terms of the application which is
specially designed for the database GUI. The downside of this is to
remember the path as you have mentioned. But after working for so long I
don't consider this as a downside :) but yes definitely for others who are
new to the project.
> The proposal that we do with the last diff of this patch is to change to a
> structure that slices vertically the
> application. This way we can understand intent behind the code and more
> easily find what we are looking for.
>
> In the current structure if you want to see the tables code you need to go
> to
> pgAdmin/browser/server_groups/servers/databases/schemas/tables/ this is a
> huge path to remember and to get to. What
> do we win with this? If we open pgAdmin we know which nodes to click in
> order to get to tables. But for development
> every time that you are looking for a specific functionality you need to
> run the application, navigate the menu so that
> you know where you can find the code. This doesn’t sound very appealing.
>
> What if our structure would look like this:
>
> - web
> - tables
> - controller
> - get_nodes.py
> - get_sql.py
> - __init__.py
> - frontend
> - component
> - ddl_component.js
> - services
> - table-service.js
> - schemas
> - servers
> - ....
>
> The intent driven architecture is always good and you have also mentioned
the pros and cons for the same.
Let me understand the proposed structure correctly.
-- We would have all the nodes (browser, server_groups, servers, database
etc.) at same level i.e. in the web folder
-- The __init__.py file would have the main business logic and also the
routs same as the current approach.
-- The get_nodes.py, get_sql.py etc. would contain the functionality which
will be extracted from __init__.py
- Please correct me if i am wrong
-- Frontend part would reside in the different folder same as the current
one.
-- All the static files will be in static folder (in their respected
folder).
- Right now the main frontend logic is in the static/js file, for
example, tables/static/js/tables.js file. So, as per me this should be here
only. Main advantage of this is, it is easy to locate
and also good for pluggability. For example, If I want to
change/add/understand the code, it is always good to have in its respected
module and I don't see any downside for this.
> This would saves us time because all the information that we need is what
> are we working on and everything is right there.
> Menu driven structure Intent Driven Structure
> *Pros:* *Pros:*
> Already in place Explicitly shows features
> Self contained features Self contained features
> Support for drop in features Support for drop in features
> *Cons:* *Cons:*
> Follows the menu, and it could change Need to change current code
> Hard to find features Some additional plumbing might be needed
> Drop in features need to be placed in a specific location according to the
> menu location
>
> What are your thought about this architecture?
>
> Around minute 7 of this video
> <https://www.youtube.com/watch?v=hALFGQNeEnU; Uncle Bob shows an
> application written
> in rails to talk about architecture. It is a long KeyNote if you are
> curious I would advise you to see the full video.
> His approach to architecture of the application is pretty interesting.
>
So, at the end, I can see the main disadvantage of the menu driven
architecture is, missing intent (I realised after watching the video) and
hard to find the module.
And the main advantage right now is, find out the tree structure, easy to
plug-in / plug-out the feature and easy to understand to code.
So, both the architectures have pros and cons. I like the proposed
architecture with some changes as I mentioned and now as a community we
need to decide, should we go ahead with this proposed architecture (with
some cost i.e. additional plumbing) or not.
Thanks,
Khushboo
> ------------------------------
> Patches 0001 Change the order of the shims on the shim file
>
> Simple change that orders the shims inside the shim file
> 0002 New tree implementation
>
> This is the first version of our Tree implementation. At this point is a
> very simple tree without no abstractions and
> with code that eventually is not very performant, but this is only the
> first iteration and we are trying to follow the
> Last Responsible Moment Principle
> <https://medium.com/@aidanjcasey/guiding-principles-for-an-evolutionary-software-architecture-b6dc2cb...;
> .
>
> What can you find in this patch:
>
> - Creation of PGBrowser.treeMenu
> - Initial version of the Tree Adaptor web/pgadmin/static/js/tree/
> tree.js
> - TreeFake test double <https://martinfowler.com/bliki/TestDouble.html;
> that can replace the Tree for testing purposes
> - Tests. As an interesting asside because Fake’s need to behave like
> the real object you will noticed that there are
> tests for this type of double and they the same as of the real object.
>
> 0003 Extract, Test and Refactor Methods
>
> This is the patch that contains the change that we talked a in the
> objectives. It touches in a subset of the places
> where itemData function is called. To have a sense of progress the
> following image depicts, on the left all places
> where this functions can be found in the code and on the right the places
> where it still remains after this patch.
>
> But this patch is not only related to the ACI Tree, it also creates some
> abstractions from code that we found repeated.
> Some examples of this are the dialogs for Backup and Restore, where the
> majority of the logic was common, so we created
> in web/pgadmin/static/js/alertify/ 3 different objects:
> A Factory DialogFactory that is responsible for creating new Dialog and
> these dialogs will use the DialogWrapper.
>
> This is also a good example of the Last Responsible Moment Principle
> <https://medium.com/@aidanjcasey/guiding-principles-for-an-evolutionary-software-architecture-b6dc2cb...;
> ,
> in Dialog there are still some functions that are used in a Recover and
> Backup scenario. When we have more examples
> we will be able to, if we want, to extract that into another function.
>
> While doing some code refactoring we found out that the Right Click Menu
> as some checks that can be centralized. Examples
> of these are checks for Enable/Disable of an option of the display of the
> Create/Edit/Drop option in some menus.
> Check web/pgadmin/static/js/menu/ for more information. This simplified
> code like:
>
> canCreate: function(itemData, item, data) {
> - //If check is false then , we will allow create menu
> - if (data && data.check == false)
> - return true;
> -
> - var t = pgBrowser.tree, i = item, d = itemData;
> - // To iterate over tree to check parent node
> - while (i) {
> - // If it is schema then allow user to create domain
> - if (_.indexOf(['schema'], d._type) > -1)
> - return true;
> -
> - if ('coll-domain' == d._type) {
> - //Check if we are not child of catalog
> - var prev_i = t.hasParent(i) ? t.parent(i) : null,
> - prev_d = prev_i ? t.itemData(prev_i) : null;
> - if( prev_d._type == 'catalog') {
> - return false;
> - } else {
> - return true;
> - }
> - }
> - i = t.hasParent(i) ? t.parent(i) : null;
> - d = i ? t.itemData(i) : null;
> - }
> - // by default we do not want to allow create menu
> - return true;
> + return canCreate.canCreate(pgBrowser, 'coll-domain', item, data);
> }
>
> This refactor was made in a couple of places throughout the code.
>
> Another refactor that had to be done was the creation of the functions
> getTreeNodeHierarchyFromElement and
> getTreeNodeHierarchyFromIdentifier that replace the old
> getTreeNodeHierarchy. This implementation was done
> because for the time being we have 2 different types of trees and calls to
> them.
>
> The rest of the changes resulted from our process of refactoring that we
> are following:
>
> 1. Find a location where itemData is used
> 2. Extract that into a function outside of the current code
> 3. Wrap it with tests
> 4. When we have enough confidence on the tests, we refactor the
> function to use the new Tree
> 5. We refactor the function to become more readable
> 6. Start over
>
> 0004 Architecture
>
> The proposed structure is our vision for the application, this is not the
> state we will be in when this patch lands.
> For now the only change we are doing is move the javascript into
> static/js/FEATURE_NAME. This is a first step in the
> direction that we would like to see the product heading.
>
> Even if we decide to keep using the same architecture the move of
> Javascript to a centralized place will
> not break any plugability as webpack can retrieve all the Javascript from
> any place in the code.
>
> Have a nice weekend
> Joao
>
>
>
Attachments:
[image/png] Screen Shot 2018-05-04 at 4.06.17 PM.png (104.4K, 3-Screen%20Shot%202018-05-04%20at%204.06.17%20PM.png)
download | view image
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-06-07 15:21 Victoria Henry <[email protected]>
parent: Aditya Toshniwal <[email protected]>
0 siblings, 1 reply; 69+ messages in thread
From: Victoria Henry @ 2018-06-07 15:21 UTC (permalink / raw)
To: Aditya Toshniwal <[email protected]>; +Cc: Ashesh Vashi <[email protected]>; Anthony Emengo <[email protected]>; Joao De Almeida Pereira <[email protected]>; Dave Page <[email protected]>; Akshay Joshi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
Hi Aditya
Sure. I did not find moving
> web/pgadmin/tools/datagrid/static/js/datagrid.js. Please correct me if I am
> missing anything.
Generally speaking, I agree with moving/deleting files if it makes sense.
But in regards to web/pgadmin/tools/datagrid/static/js/datagrid.js, it
looks like this is still being used in
web/pgadmin/tools/sqleditor/static/js/sqleditor.js with
Datagrid.create_transaction
Sincerely,
Victoria
On Thu, Jun 7, 2018 at 12:35 AM Aditya Toshniwal <
[email protected]> wrote:
> Hi Victoria,
>
> On Wed, Jun 6, 2018 at 8:55 PM, Victoria Henry <[email protected]> wrote:
>
>> Hi Aditya,
>>
>> 1) Why don't we start using webpack alias's instead of using absolute
>>> path. For eg,
>>> import {RestoreDialogWrapper} from
>>> '../../../pgadmin/static/js/restore/restore_dialog_wrapper';
>>> can be used as import {RestoreDialogWrapper} from
>>> 'pgadmin_static/js/restore/restore_dialog_wrapper';
>>> by adding pgadmin_static alias to webpack config.
>>
>>
>> Great point. In some areas of the code, we began making this change.
>> There is already an alias in webpack shims for `
>> ../../../pgadmin/static/js` called `sources`. You can find an example
>> of this in import statements for `supported_database_node.js`
>>
>> 2) Few of the js are left behind, the ones which are used in python
>>> __init__.py. Can't we move them too ? It would be nicer to not to leave
>>> behind a single js.
>>
>> I'm not sure what you mean. Could you point to an example of a single js
>> file?
>>
>
> Sure. I did not find moving
> web/pgadmin/tools/datagrid/static/js/datagrid.js. Please correct me if I am
> missing anything.
>
>>
>> Sincerely,
>>
>> Victoria
>>
>> On Wed, Jun 6, 2018 at 7:07 AM Aditya Toshniwal <
>> [email protected]> wrote:
>>
>>> Hi Anthony/Victoria/Joao,
>>>
>>> I know I am very late to review on patch 004. The idea of moving js
>>> files from tools to static folder looks good, but I have a few suggestions:
>>>
>>> 1) Why don't we start using webpack alias's instead of using absolute
>>> path. For eg,
>>> import {RestoreDialogWrapper} from
>>> '../../../pgadmin/static/js/restore/restore_dialog_wrapper';
>>> can be used as import {RestoreDialogWrapper} from
>>> 'pgadmin_static/js/restore/restore_dialog_wrapper';
>>> by adding pgadmin_static alias to webpack config.
>>>
>>> 2) Few of the js are left behind, the ones which are used in python
>>> __init__.py. Can't we move them too ? It would be nicer to not to leave
>>> behind a single js.
>>>
>>> Kindly let me know your views on this.
>>>
>>>
>>> Thanks and Regards,
>>> Aditya Toshniwal
>>> Software Engineer | EnterpriseDB Software Solutions | Pune
>>> "Don't Complain about Heat, Plant a tree"
>>>
>>> On Sat, Jun 2, 2018 at 12:47 AM, Victoria Henry <[email protected]>
>>> wrote:
>>>
>>>> Hey Ashesh,
>>>>
>>>> LGTM! The some of the CI tests failed but it looks like a flake. But
>>>> you can go ahead and merge this.
>>>>
>>>> Sincerely,
>>>>
>>>> Victoria
>>>>
>>>> On Fri, Jun 1, 2018 at 2:36 PM Ashesh Vashi <
>>>> [email protected]> wrote:
>>>>
>>>>> On Fri, Jun 1, 2018 at 10:09 PM, Victoria Henry <[email protected]>
>>>>> wrote:
>>>>>
>>>>>> Hi Ashesh,
>>>>>>
>>>>>> We just attempted to apply your patch over master but it did not
>>>>>> work. We don't want to introduce any bugs or break any functionality.
>>>>>> Please update the patch to make sure it is synced up with the master branch.
>>>>>>
>>>>> Please find the updated patch.
>>>>>
>>>>>>
>>>>>> Sincerely,
>>>>>>
>>>>>> Victoria
>>>>>>
>>>>>> On Fri, Jun 1, 2018 at 11:18 AM Anthony Emengo <[email protected]>
>>>>>> wrote:
>>>>>>
>>>>>>> Hey Ashesh,
>>>>>>>
>>>>>>> Thanks for the explanation. It was great and it really helped!
>>>>>>>
>>>>>>> C pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js
>>>>>>> C pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js
>>>>>>>
>>>>>>> It makes sense to remove duplication by extracting these attributes
>>>>>>> out and setting the canDrop and canCreate functions here. But is it
>>>>>>> possible to combine these two files into one since they are related so we
>>>>>>> don’t need to import schema_child_tree_node?
>>>>>>>
>>>>>> That was the original plan, but 'pgadmin/browser/static/js//node.js'
>>>>> script has too many dependecies, which are not easily portable.
>>>>> And - that may lead to change the scope of the patch.
>>>>>
>>>>> Hence - I decided to use the separate file to make sure we have enough
>>>>> test coverage (which is more imprortant than changing the scope).
>>>>>
>>>>>> M pgadmin/static/js/tree/tree.js
>>>>>>>
>>>>>>> The creation of the ancestorNode function feels like a
>>>>>>> pre-optimization. That function is not used any where outside of the
>>>>>>> tree.js file, so it’s more confusing to have it defined.
>>>>>>>
>>>>>> It is being used in the latest changes. :-)
>>>>>
>>>>>
>>>>>> On a lighter note, could we avoid the !! syntax when possible? For
>>>>>>> example, instead of return !!obj, we could do something like return
>>>>>>> obj === undefined or return _.isUndefined(obj) as this is more
>>>>>>> intuitive.
>>>>>>>
>>>>>>> https://softwareengineering.stackexchange.com/a/80092
>>>>>>>
>>>>>> I am kind of disagree here. But - I have changed it anyway.
>>>>>
>>>>>> In addition, please update this patch as it is out of sync with the
>>>>>>> latest commit on the master branch. Otherwise, everything looks good!
>>>>>>>
>>>>>> Here - you go!
>>>>>
>>>>> -- Thanks, Ashesh
>>>>>
>>>>>>
>>>>>>>
>>>>>>> Thanks
>>>>>>> Anthony && Victoria
>>>>>>>
>>>>>>> On Fri, Jun 1, 2018 at 7:52 AM Ashesh Vashi <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> On Thu, May 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> Hey, Thanks so much for the reply.
>>>>>>>>>
>>>>>>>>> We've noticed that you've made several modifications on top of our
>>>>>>>>> original patch. Unfortunately, we've found it very hard to follow. Could we
>>>>>>>>> please get a brief synopsis of the changes you have made - just so we can
>>>>>>>>> better understand the rationale behind them? Just like we've done for you
>>>>>>>>> previously.
>>>>>>>>>
>>>>>>>> Please find the changes from your original patch:
>>>>>>>>
>>>>>>>> M webpack.shim.js
>>>>>>>> M webpack.test.config.js
>>>>>>>> - In order to specify the fake_browser in regression tests, we need to use 'pgbrowser/browser' in the 'schema_child_tree_node.js' script.D pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child.js
>>>>>>>> - We don't need this with the new implementation.C pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js
>>>>>>>> - All the children of schema node have common properties as 'parent_type', 'canDrop', 'canDropCascase', 'canCreate'.
>>>>>>>> Hence - instead of defining them in each node, we have created a base node, which will have all these properties.
>>>>>>>> And, modified all schema children node to inherit from it.C pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js
>>>>>>>> - In this script, we're defining three functions 'childCreateMenuEnabled', 'isTreeItemOfChildOfSchema', & 'isTreeNodeOfSchemaChild', which are used by the 'SchemaChildNode' objects.M pgadmin/browser/static/js/collection.js
>>>>>>>> - Fixed an issue related to wrongly defined 'error' function for the Collection object.D pgadmin/static/js/menu/can_create.js
>>>>>>>> - It defined the function, which was defining a check for creation of a schema child node, or not by looking at the parent node (i.e. a schema/catalog node).
>>>>>>>> The file was not defintely placed under the wrong directory, because - the similar logic was under 'can_drop_child.js', and it was defined under 'pgadmin/browser/server_groups/servers/databases/schemas/static/js' directory.D pgadmin/static/js/menu/menu_enabled.jsC pgadmin/static/js/nodes/supported_database_node.js
>>>>>>>> - Used by the external tools for checking whether the 'selected' tree-node is:
>>>>>>>> + 'database' node, and it is allowed to connect it.
>>>>>>>> + Or, it is one of the schema child (and, not 'catalog' child).
>>>>>>>> - Finding the correct location was difficult for this, as there is no defined pattern, also it can be used by other functions too. Hence - moved it out of 'pgadmin/static/js/menu' directory.M pgadmin/static/js/tree/tree.js
>>>>>>>> - Introduced a function, which returns the ancestor node object, fow which the condition is true.D regression/javascript/menu/can_create_spec.js
>>>>>>>> D regression/javascript/menu/menu_enabled_spec.js
>>>>>>>> D regression/javascript/schema/can_drop_child_spec.jsC regression/javascript/fake_browser/browser.js
>>>>>>>> C regression/javascript/nodes/schema/child_menu_spec.js
>>>>>>>> - Modified the regression to test the new functionalies.M pgadmin/browser/server_groups/servers/databases/schemas/**/*.js
>>>>>>>> - Extending the schema child nodes from the 'SchemaChildNode' class defined in 'pgadmin/.../schemas/static/js/child.js' script.
>>>>>>>>
>>>>>>>> Let me know if you need more information.
>>>>>>>>
>>>>>>>>
>>>>>>>>> Let's keep in mind that the original intent was simply to
>>>>>>>>> introduce this abstraction into the code base, which is a big enough task.
>>>>>>>>> I'd hate for the scope of the changes we're making to expand beyond that.
>>>>>>>>>
>>>>>>>>
>>>>>>>> I have the mutual feeling.
>>>>>>>>
>>>>>>>> -- Thanks, Ashesh
>>>>>>>>
>>>>>>>>>
>>>>>>>>> Thanks
>>>>>>>>> Joao && Anthony
>>>>>>>>>
>>>>>>>>>
>>>>>>>>> On Thu, May 24, 2018 at 2:59 AM Ashesh Vashi <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Sorry for the late reply.
>>>>>>>>>> On Wed, May 16, 2018 at 8:55 PM, Anthony Emengo <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> export function canCreate(pgBrowser, childOfCatalogType) {
>>>>>>>>>>> return canCreateObject.bind({
>>>>>>>>>>> browser: pgBrowser,
>>>>>>>>>>> childOfCatalogType: childOfCatalogType,
>>>>>>>>>>> });
>>>>>>>>>>> }
>>>>>>>>>>>
>>>>>>>>>>> With respect to the above code: this bind pattern looks good and
>>>>>>>>>>> seems like the idiomatic way to handle this in JavaScript. On a lighter
>>>>>>>>>>> node, I don’t even see the need for an additional method to wrap it. The
>>>>>>>>>>> invocation could have easily been like canCreate:
>>>>>>>>>>> canCreateObject.bind({ browser: pgBrowser, childOfCatalogType:
>>>>>>>>>>> childOfCatalogType }), I don’t feel too strongly here.
>>>>>>>>>>>
>>>>>>>>>> I do agree - we can handle the same problem many ways.
>>>>>>>>>> I prefer object oriented pardigm more in general.
>>>>>>>>>> Any way - I have modified the code with some other changes.
>>>>>>>>>>
>>>>>>>>>>> I renamed it as isValidTreeNodeData, because - we were using it
>>>>>>>>>>> in for testing the tree data. Please suggest me the right place, and name.
>>>>>>>>>>>
>>>>>>>>>>> We’re not sure; maybe after continued refactoring, we will come
>>>>>>>>>>> across more generic functions. At that point we can revisit this and create
>>>>>>>>>>> a utils.js file.
>>>>>>>>>>>
>>>>>>>>>> Sure.
>>>>>>>>>>
>>>>>>>>>>> The original patch was separating them in different places, but
>>>>>>>>>>> - still uses some of the functionalities directly from the tree, which was
>>>>>>>>>>> happening because we have contextual menu.
>>>>>>>>>>> To give a better solution, I can think of putting the menus
>>>>>>>>>>> related code understand ‘sources/tree/menu’ directory.
>>>>>>>>>>>
>>>>>>>>>>> We’re particularly worried because we’re trying to avoid the
>>>>>>>>>>> coupling that we see in the code base today. We want to decouple *application
>>>>>>>>>>> state* from *business domain* logic as much as we can - because
>>>>>>>>>>> this makes the code much easier to understand. We achieve lower coupling by
>>>>>>>>>>> have more suitable interfaces to retrieve *application state*
>>>>>>>>>>> like: anyParent (the menu doesn’t care how this happens). This
>>>>>>>>>>> is the direction that we’re trying to move towards, we just don’t want the
>>>>>>>>>>> package structure to undermine that developer intent.
>>>>>>>>>>>
>>>>>>>>>> I realized after revisiting the code, menu/can_create.js was only
>>>>>>>>>> applicable to the children of the schema/catalog nodes, same as
>>>>>>>>>> 'can_drop_child'.
>>>>>>>>>> We should have put both scripts in the same directory.
>>>>>>>>>>
>>>>>>>>>> Please find the updated patch for the same.
>>>>>>>>>>
>>>>>>>>>> Please review it, and let me know your concerns.
>>>>>>>>>>
>>>>>>>>>> -- Thanks, Ashesh
>>>>>>>>>>
>>>>>>>>>>> How about nodeMenu.isSupportedNode(…)?
>>>>>>>>>>>
>>>>>>>>>>> Naming is one of the hardest problems in programming. I don’t
>>>>>>>>>>> feel too strongly about this one. For now, let’s keep it as is
>>>>>>>>>>>
>>>>>>>>>>> Thanks
>>>>>>>>>>> Anthony && Victoria
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>
>>>>>
>>>
>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-06-11 05:44 Aditya Toshniwal <[email protected]>
parent: Victoria Henry <[email protected]>
0 siblings, 0 replies; 69+ messages in thread
From: Aditya Toshniwal @ 2018-06-11 05:44 UTC (permalink / raw)
To: Victoria Henry <[email protected]>; +Cc: Ashesh Vashi <[email protected]>; Anthony Emengo <[email protected]>; Joao De Almeida Pereira <[email protected]>; Dave Page <[email protected]>; Akshay Joshi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers; Khushboo Vashi <[email protected]>
Hi Victoria,
On Thu, Jun 7, 2018 at 8:51 PM, Victoria Henry <[email protected]> wrote:
> Hi Aditya
>
> Sure. I did not find moving web/pgadmin/tools/datagrid/static/js/datagrid.js.
>> Please correct me if I am missing anything.
>
> Generally speaking, I agree with moving/deleting files if it makes sense.
> But in regards to web/pgadmin/tools/datagrid/static/js/datagrid.js, it
> looks like this is still being used in web/pgadmin/tools/sqleditor/
> static/js/sqleditor.js with Datagrid.create_transaction
>
And this is another js which is not moved.(sqleditor.js). If we are moving
js files, why not this too.
> Sincerely,
>
> Victoria
>
>
>
>
> On Thu, Jun 7, 2018 at 12:35 AM Aditya Toshniwal <aditya.toshniwal@
> enterprisedb.com> wrote:
>
>> Hi Victoria,
>>
>> On Wed, Jun 6, 2018 at 8:55 PM, Victoria Henry <[email protected]> wrote:
>>
>>> Hi Aditya,
>>>
>>> 1) Why don't we start using webpack alias's instead of using absolute
>>>> path. For eg,
>>>> import {RestoreDialogWrapper} from '../../../pgadmin/static/js/
>>>> restore/restore_dialog_wrapper';
>>>> can be used as import {RestoreDialogWrapper} from
>>>> 'pgadmin_static/js/restore/restore_dialog_wrapper';
>>>> by adding pgadmin_static alias to webpack config.
>>>
>>>
>>> Great point. In some areas of the code, we began making this change.
>>> There is already an alias in webpack shims for `
>>> ../../../pgadmin/static/js` called `sources`. You can find an example
>>> of this in import statements for `supported_database_node.js`
>>>
>>> 2) Few of the js are left behind, the ones which are used in python
>>>> __init__.py. Can't we move them too ? It would be nicer to not to leave
>>>> behind a single js.
>>>
>>> I'm not sure what you mean. Could you point to an example of a single
>>> js file?
>>>
>>
>> Sure. I did not find moving web/pgadmin/tools/datagrid/static/js/datagrid.js.
>> Please correct me if I am missing anything.
>>
>>>
>>> Sincerely,
>>>
>>> Victoria
>>>
>>> On Wed, Jun 6, 2018 at 7:07 AM Aditya Toshniwal <aditya.toshniwal@
>>> enterprisedb.com> wrote:
>>>
>>>> Hi Anthony/Victoria/Joao,
>>>>
>>>> I know I am very late to review on patch 004. The idea of moving js
>>>> files from tools to static folder looks good, but I have a few suggestions:
>>>>
>>>> 1) Why don't we start using webpack alias's instead of using absolute
>>>> path. For eg,
>>>> import {RestoreDialogWrapper} from '../../../pgadmin/static/js/
>>>> restore/restore_dialog_wrapper';
>>>> can be used as import {RestoreDialogWrapper} from
>>>> 'pgadmin_static/js/restore/restore_dialog_wrapper';
>>>> by adding pgadmin_static alias to webpack config.
>>>>
>>>> 2) Few of the js are left behind, the ones which are used in python
>>>> __init__.py. Can't we move them too ? It would be nicer to not to leave
>>>> behind a single js.
>>>>
>>>> Kindly let me know your views on this.
>>>>
>>>>
>>>> Thanks and Regards,
>>>> Aditya Toshniwal
>>>> Software Engineer | EnterpriseDB Software Solutions | Pune
>>>> "Don't Complain about Heat, Plant a tree"
>>>>
>>>> On Sat, Jun 2, 2018 at 12:47 AM, Victoria Henry <[email protected]>
>>>> wrote:
>>>>
>>>>> Hey Ashesh,
>>>>>
>>>>> LGTM! The some of the CI tests failed but it looks like a flake. But
>>>>> you can go ahead and merge this.
>>>>>
>>>>> Sincerely,
>>>>>
>>>>> Victoria
>>>>>
>>>>> On Fri, Jun 1, 2018 at 2:36 PM Ashesh Vashi <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> On Fri, Jun 1, 2018 at 10:09 PM, Victoria Henry <[email protected]>
>>>>>> wrote:
>>>>>>
>>>>>>> Hi Ashesh,
>>>>>>>
>>>>>>> We just attempted to apply your patch over master but it did not
>>>>>>> work. We don't want to introduce any bugs or break any functionality.
>>>>>>> Please update the patch to make sure it is synced up with the master branch.
>>>>>>>
>>>>>> Please find the updated patch.
>>>>>>
>>>>>>>
>>>>>>> Sincerely,
>>>>>>>
>>>>>>> Victoria
>>>>>>>
>>>>>>> On Fri, Jun 1, 2018 at 11:18 AM Anthony Emengo <[email protected]>
>>>>>>> wrote:
>>>>>>>
>>>>>>>> Hey Ashesh,
>>>>>>>>
>>>>>>>> Thanks for the explanation. It was great and it really helped!
>>>>>>>>
>>>>>>>> C pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js
>>>>>>>> C pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js
>>>>>>>>
>>>>>>>> It makes sense to remove duplication by extracting these attributes
>>>>>>>> out and setting the canDrop and canCreate functions here. But is
>>>>>>>> it possible to combine these two files into one since they are related so
>>>>>>>> we don’t need to import schema_child_tree_node?
>>>>>>>>
>>>>>>> That was the original plan, but 'pgadmin/browser/static/js//node.js'
>>>>>> script has too many dependecies, which are not easily portable.
>>>>>> And - that may lead to change the scope of the patch.
>>>>>>
>>>>>> Hence - I decided to use the separate file to make sure we have
>>>>>> enough test coverage (which is more imprortant than changing the scope).
>>>>>>
>>>>>>> M pgadmin/static/js/tree/tree.js
>>>>>>>>
>>>>>>>> The creation of the ancestorNode function feels like a
>>>>>>>> pre-optimization. That function is not used any where outside of the
>>>>>>>> tree.js file, so it’s more confusing to have it defined.
>>>>>>>>
>>>>>>> It is being used in the latest changes. :-)
>>>>>>
>>>>>>
>>>>>>> On a lighter note, could we avoid the !! syntax when possible? For
>>>>>>>> example, instead of return !!obj, we could do something like return
>>>>>>>> obj === undefined or return _.isUndefined(obj) as this is more
>>>>>>>> intuitive.
>>>>>>>>
>>>>>>>> https://softwareengineering.stackexchange.com/a/80092
>>>>>>>>
>>>>>>> I am kind of disagree here. But - I have changed it anyway.
>>>>>>
>>>>>>> In addition, please update this patch as it is out of sync with the
>>>>>>>> latest commit on the master branch. Otherwise, everything looks good!
>>>>>>>>
>>>>>>> Here - you go!
>>>>>>
>>>>>> -- Thanks, Ashesh
>>>>>>
>>>>>>>
>>>>>>>>
>>>>>>>> Thanks
>>>>>>>> Anthony && Victoria
>>>>>>>>
>>>>>>>> On Fri, Jun 1, 2018 at 7:52 AM Ashesh Vashi <
>>>>>>>> [email protected]> wrote:
>>>>>>>>
>>>>>>>>> On Thu, May 24, 2018 at 8:13 PM, Joao De Almeida Pereira <
>>>>>>>>> [email protected]> wrote:
>>>>>>>>>
>>>>>>>>>> Hey, Thanks so much for the reply.
>>>>>>>>>>
>>>>>>>>>> We've noticed that you've made several modifications on top of
>>>>>>>>>> our original patch. Unfortunately, we've found it very hard to follow.
>>>>>>>>>> Could we please get a brief synopsis of the changes you have made - just so
>>>>>>>>>> we can better understand the rationale behind them? Just like we've done
>>>>>>>>>> for you previously.
>>>>>>>>>>
>>>>>>>>> Please find the changes from your original patch:
>>>>>>>>>
>>>>>>>>> M webpack.shim.js
>>>>>>>>> M webpack.test.config.js
>>>>>>>>> - In order to specify the fake_browser in regression tests, we need to use 'pgbrowser/browser' in the 'schema_child_tree_node.js' script.D pgadmin/browser/server_groups/servers/databases/schemas/static/js/can_drop_child.js
>>>>>>>>> - We don't need this with the new implementation.C pgadmin/browser/server_groups/servers/databases/schemas/static/js/child.js
>>>>>>>>> - All the children of schema node have common properties as 'parent_type', 'canDrop', 'canDropCascase', 'canCreate'.
>>>>>>>>> Hence - instead of defining them in each node, we have created a base node, which will have all these properties.
>>>>>>>>> And, modified all schema children node to inherit from it.C pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema_child_tree_node.js
>>>>>>>>> - In this script, we're defining three functions 'childCreateMenuEnabled', 'isTreeItemOfChildOfSchema', & 'isTreeNodeOfSchemaChild', which are used by the 'SchemaChildNode' objects.M pgadmin/browser/static/js/collection.js
>>>>>>>>> - Fixed an issue related to wrongly defined 'error' function for the Collection object.D pgadmin/static/js/menu/can_create.js
>>>>>>>>> - It defined the function, which was defining a check for creation of a schema child node, or not by looking at the parent node (i.e. a schema/catalog node).
>>>>>>>>> The file was not defintely placed under the wrong directory, because - the similar logic was under 'can_drop_child.js', and it was defined under 'pgadmin/browser/server_groups/servers/databases/schemas/static/js' directory.D pgadmin/static/js/menu/menu_enabled.jsC pgadmin/static/js/nodes/supported_database_node.js
>>>>>>>>> - Used by the external tools for checking whether the 'selected' tree-node is:
>>>>>>>>> + 'database' node, and it is allowed to connect it.
>>>>>>>>> + Or, it is one of the schema child (and, not 'catalog' child).
>>>>>>>>> - Finding the correct location was difficult for this, as there is no defined pattern, also it can be used by other functions too. Hence - moved it out of 'pgadmin/static/js/menu' directory.M pgadmin/static/js/tree/tree.js
>>>>>>>>> - Introduced a function, which returns the ancestor node object, fow which the condition is true.D regression/javascript/menu/can_create_spec.js
>>>>>>>>> D regression/javascript/menu/menu_enabled_spec.js
>>>>>>>>> D regression/javascript/schema/can_drop_child_spec.jsC regression/javascript/fake_browser/browser.js
>>>>>>>>> C regression/javascript/nodes/schema/child_menu_spec.js
>>>>>>>>> - Modified the regression to test the new functionalies.M pgadmin/browser/server_groups/servers/databases/schemas/**/*.js
>>>>>>>>> - Extending the schema child nodes from the 'SchemaChildNode' class defined in 'pgadmin/.../schemas/static/js/child.js' script.
>>>>>>>>>
>>>>>>>>> Let me know if you need more information.
>>>>>>>>>
>>>>>>>>>
>>>>>>>>>> Let's keep in mind that the original intent was simply to
>>>>>>>>>> introduce this abstraction into the code base, which is a big enough task.
>>>>>>>>>> I'd hate for the scope of the changes we're making to expand beyond that.
>>>>>>>>>>
>>>>>>>>>
>>>>>>>>> I have the mutual feeling.
>>>>>>>>>
>>>>>>>>> -- Thanks, Ashesh
>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> Thanks
>>>>>>>>>> Joao && Anthony
>>>>>>>>>>
>>>>>>>>>>
>>>>>>>>>> On Thu, May 24, 2018 at 2:59 AM Ashesh Vashi <
>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>
>>>>>>>>>>> Sorry for the late reply.
>>>>>>>>>>> On Wed, May 16, 2018 at 8:55 PM, Anthony Emengo <
>>>>>>>>>>> [email protected]> wrote:
>>>>>>>>>>>
>>>>>>>>>>>> export function canCreate(pgBrowser, childOfCatalogType) {
>>>>>>>>>>>> return canCreateObject.bind({
>>>>>>>>>>>> browser: pgBrowser,
>>>>>>>>>>>> childOfCatalogType: childOfCatalogType,
>>>>>>>>>>>> });
>>>>>>>>>>>> }
>>>>>>>>>>>>
>>>>>>>>>>>> With respect to the above code: this bind pattern looks good
>>>>>>>>>>>> and seems like the idiomatic way to handle this in JavaScript. On a lighter
>>>>>>>>>>>> node, I don’t even see the need for an additional method to wrap it. The
>>>>>>>>>>>> invocation could have easily been like canCreate:
>>>>>>>>>>>> canCreateObject.bind({ browser: pgBrowser, childOfCatalogType:
>>>>>>>>>>>> childOfCatalogType }), I don’t feel too strongly here.
>>>>>>>>>>>>
>>>>>>>>>>> I do agree - we can handle the same problem many ways.
>>>>>>>>>>> I prefer object oriented pardigm more in general.
>>>>>>>>>>> Any way - I have modified the code with some other changes.
>>>>>>>>>>>
>>>>>>>>>>>> I renamed it as isValidTreeNodeData, because - we were using it
>>>>>>>>>>>> in for testing the tree data. Please suggest me the right place, and name.
>>>>>>>>>>>>
>>>>>>>>>>>> We’re not sure; maybe after continued refactoring, we will come
>>>>>>>>>>>> across more generic functions. At that point we can revisit this and create
>>>>>>>>>>>> a utils.js file.
>>>>>>>>>>>>
>>>>>>>>>>> Sure.
>>>>>>>>>>>
>>>>>>>>>>>> The original patch was separating them in different places, but
>>>>>>>>>>>> - still uses some of the functionalities directly from the tree, which was
>>>>>>>>>>>> happening because we have contextual menu.
>>>>>>>>>>>> To give a better solution, I can think of putting the menus
>>>>>>>>>>>> related code understand ‘sources/tree/menu’ directory.
>>>>>>>>>>>>
>>>>>>>>>>>> We’re particularly worried because we’re trying to avoid the
>>>>>>>>>>>> coupling that we see in the code base today. We want to decouple *application
>>>>>>>>>>>> state* from *business domain* logic as much as we can -
>>>>>>>>>>>> because this makes the code much easier to understand. We achieve lower
>>>>>>>>>>>> coupling by have more suitable interfaces to retrieve *application
>>>>>>>>>>>> state* like: anyParent (the menu doesn’t care how this
>>>>>>>>>>>> happens). This is the direction that we’re trying to move towards, we just
>>>>>>>>>>>> don’t want the package structure to undermine that developer intent.
>>>>>>>>>>>>
>>>>>>>>>>> I realized after revisiting the code, menu/can_create.js was
>>>>>>>>>>> only applicable to the children of the schema/catalog nodes, same as
>>>>>>>>>>> 'can_drop_child'.
>>>>>>>>>>> We should have put both scripts in the same directory.
>>>>>>>>>>>
>>>>>>>>>>> Please find the updated patch for the same.
>>>>>>>>>>>
>>>>>>>>>>> Please review it, and let me know your concerns.
>>>>>>>>>>>
>>>>>>>>>>> -- Thanks, Ashesh
>>>>>>>>>>>
>>>>>>>>>>>> How about nodeMenu.isSupportedNode(…)?
>>>>>>>>>>>>
>>>>>>>>>>>> Naming is one of the hardest problems in programming. I don’t
>>>>>>>>>>>> feel too strongly about this one. For now, let’s keep it as is
>>>>>>>>>>>>
>>>>>>>>>>>> Thanks
>>>>>>>>>>>> Anthony && Victoria
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>>
>>>>>>>>>>>
>>>>>>>>>
>>>>>>
>>>>
>>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-06-11 10:29 Ashesh Vashi <[email protected]>
parent: Joao De Almeida Pereira <[email protected]>
2 siblings, 1 reply; 69+ messages in thread
From: Ashesh Vashi @ 2018-06-11 10:29 UTC (permalink / raw)
To: Joao De Almeida Pereira <[email protected]>; +Cc: Dave Page <[email protected]>; Anthony Emengo <[email protected]>; Khushboo Vashi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
Hi Team,
On Sat, May 5, 2018 at 3:31 AM, Joao De Almeida Pereira <
[email protected]> wrote:
> *...*
> 3. Start the discussion on application architecture
>
> Why should we care about location of files inside a our application?
>
> Why is this way better the another?
>
> These are 2 good questions that have very lengthy answers. Trying to more
> or less summarize the answers we care about
> the location of the files, because we want our application to communicate
> intent and there are always pros and cons
> on all the decisions that we make.
>
To be honest, it depends on how do you see the application, and its
expectations.
That question leads me to another question "What do we want from pgAdmin 4?"
At this point the application structure follows our menu, this approach
> eventually make is easier to follow the code
> but at the same time if the menu changes down the line, will we change the
> structure of our folders?
>
To be honest not menu driven. (Probably a wrong choice of word 'Menu'.)
But - only under 'browser' (You can also call it object browser/tree), it
is driven by the database object relation model.
And, each module maintains the parent-child relationship.
> The proposal that we do with the last diff of this patch is to change to a
> structure that slices vertically the
> application. This way we can understand intent behind the code and more
> easily find what we are looking for.
>
In the current structure if you want to see the tables code you need to go
> to
> pgAdmin/browser/server_groups/servers/databases/schemas/tables/ this is a
> huge path to remember and to get to. What
> do we win with this?
>
I agree - it is too deep structure.
But - it gives me the idea what's the structure of the database objection
relationship.
Does it hurt, I would say no?
Because - as a database developer, I know whats the relationship of
objects, and where I can find them.
(Until I heard Dave saying it is about to touch the limit of maximum file
system path.)
So - there is a scope for improvement there, we can leave without the
object relations (but - limited to the object browser I think).
> If we open pgAdmin we know which nodes to click in order to get to tables.
> But for development
> every time that you are looking for a specific functionality you need to
> run the application, navigate the menu so that
> you know where you can find the code. This doesn’t sound very appealing.
>
What if our structure would look like this:
>
> - web
> - tables
> - controller
> - get_nodes.py
> - get_sql.py
> - __init__.py
> - frontend
> - component
> - ddl_component.js
> - services
> - table-service.js
> - schemas
> - servers
> - ....
>
> I think - there is nothing wrong with the current module structure. It is
more appealing to me than the above one.
The python package contains the backend code, and static contains all your
frontend.
I am not saying, we should not change anything.
We can definitely divide them in smaller chunks for both backend, and
frontend side.
This would saves us time because all the information that we need is what
> are we working on and everything is right there.
> Menu driven structure Intent Driven Structure
> *Pros:* *Pros:*
> Already in place Explicitly shows features
> Self contained features Self contained features
> Support for drop in features Support for drop in features
> *Cons:* *Cons:*
> Follows the menu, and it could change Need to change current code
> Hard to find features Some additional plumbing might be needed
> Drop in features need to be placed in a specific location according to the
> menu location
>
> What are your thought about this architecture?
>
I am not a fan of flat file structure in application.
There are many reasons - why we need namespace in C++, same applies here.
Let me start from the question "What do we want from pgAdmin 4?"
* A object browser
* Miscallenous operations associated with the object shown in the object
browser
+ Reversed Enginering query (if any)
+ Properties Viewer
+ Edit/Create Viewer
+ Staticis
+ List of dependencies
+ List of dependents
* Query tool
* Data Viewer (table/view)
* Function debugger
* Extendability/Pluggability
According to me - these as the intent of the application, and not objects
assiciated with them.
I do agree, there is no boundry (or, very thin) between data & presentation
at the moment.
We can start from there.
I can think of the following directory structure ( which is not too
different from the current, but - still will give the developers a lot more
comfort as per your complain :-) ).
> pgadmin/
> - database_objects/
> - server/
> - __init__.py
> - create.py
> - get.py
> - sql.py
> - update.py
> - static/
> - img/
> - server.svg
> - server_bad.svg
> - javascripts/
> - server.js
> - ui_schema.js
> - database/
> - templates
> ...
> ...
> + schema/
> ...
> - tests/
> - api/
> ...
> - gui/
> ...
> - karma/
> ...
> - browser/
> + pgagents/
> - server_groups/
> - servers/
> - databases/
> - __init__.py
> - node.py
> - schemas
> ...
> ...
> ...
> ...
> + tests/
> - tools/
> + sqleditor/
> + dataviewer/
> + debugger/
> - misc/
> + sql/
> + statistics/
> ...
>
> NOTE:
We also need to think about the facts that.
These changes may lead to a lot of night mare packagers to make changes in
the installers in upgrade mode.
Also - when I say pluggability/extendability, we should be able to create
plugins on top of pgAdmin 4.
Just take an example of this feature (Geospatial Query Viewer)
<https://www.postgresql.org/message-id/[email protected]...;
.
I think - it is best implemented as an plugin, as not every pgAdmin user
will benificiary.
We need ability to load the modules as pluggable module same as we used to
have before we introduced the webpack to bundle everything as modules.
We also need to start thinking about it.
-- Thanks, Ashesh
Around minute 7 of this video <https://www.youtube.com/watch?v=hALFGQNeEnU;
> Uncle Bob shows an application written
> in rails to talk about architecture. It is a long KeyNote if you are
> curious I would advise you to see the full video.
> His approach to architecture of the application is pretty interesting.
>
^ permalink raw reply [nested|flat] 69+ messages in thread
* Re: [pgadmin4][patch] Initial patch to decouple from ACI Tree
@ 2018-06-11 10:35 Ashesh Vashi <[email protected]>
parent: Ashesh Vashi <[email protected]>
0 siblings, 0 replies; 69+ messages in thread
From: Ashesh Vashi @ 2018-06-11 10:35 UTC (permalink / raw)
To: Joao De Almeida Pereira <[email protected]>; +Cc: Dave Page <[email protected]>; Anthony Emengo <[email protected]>; Khushboo Vashi <[email protected]>; Murtuza Zabuawala <[email protected]>; pgadmin-hackers
On Mon, Jun 11, 2018 at 3:59 PM, Ashesh Vashi <[email protected]
> wrote:
> Hi Team,
>
> On Sat, May 5, 2018 at 3:31 AM, Joao De Almeida Pereira <
> [email protected]> wrote:
>
>> *...*
>> 3. Start the discussion on application architecture
>>
>> Why should we care about location of files inside a our application?
>>
>> Why is this way better the another?
>>
>> These are 2 good questions that have very lengthy answers. Trying to more
>> or less summarize the answers we care about
>> the location of the files, because we want our application to communicate
>> intent and there are always pros and cons
>> on all the decisions that we make.
>>
> To be honest, it depends on how do you see the application, and its
> expectations.
> That question leads me to another question "What do we want from pgAdmin
> 4?"
>
> At this point the application structure follows our menu, this approach
>> eventually make is easier to follow the code
>> but at the same time if the menu changes down the line, will we change
>> the structure of our folders?
>>
> To be honest not menu driven. (Probably a wrong choice of word 'Menu'.)
> But - only under 'browser' (You can also call it object browser/tree), it
> is driven by the database object relation model.
> And, each module maintains the parent-child relationship.
>
>
>> The proposal that we do with the last diff of this patch is to change to
>> a structure that slices vertically the
>> application. This way we can understand intent behind the code and more
>> easily find what we are looking for.
>>
> In the current structure if you want to see the tables code you need to go
>> to
>> pgAdmin/browser/server_groups/servers/databases/schemas/tables/ this is
>> a huge path to remember and to get to. What
>> do we win with this?
>>
> I agree - it is too deep structure.
> But - it gives me the idea what's the structure of the database objection
> relationship.
>
> Does it hurt, I would say no?
> Because - as a database developer, I know whats the relationship of
> objects, and where I can find them.
> (Until I heard Dave saying it is about to touch the limit of maximum file
> system path.)
>
>
So - there is a scope for improvement there, we can leave *live* without
the object relations (but - limited to the object browser I think).
>
>
>> If we open pgAdmin we know which nodes to click in order to get to
>> tables. But for development
>> every time that you are looking for a specific functionality you need to
>> run the application, navigate the menu so that
>> you know where you can find the code. This doesn’t sound very appealing.
>>
> What if our structure would look like this:
>>
>> - web
>> - tables
>> - controller
>> - get_nodes.py
>> - get_sql.py
>> - __init__.py
>> - frontend
>> - component
>> - ddl_component.js
>> - services
>> - table-service.js
>> - schemas
>> - servers
>> - ....
>>
>> I think - there is nothing wrong with the current module structure. It is
> more appealing to me than the above one.
> The python package contains the backend code, and static contains all your
> frontend.
>
> I am not saying, we should not change anything.
> We can definitely divide them in smaller chunks for both backend, and
> frontend side.
>
> This would saves us time because all the information that we need is what
>> are we working on and everything is right there.
>> Menu driven structure Intent Driven Structure
>> *Pros:* *Pros:*
>> Already in place Explicitly shows features
>> Self contained features Self contained features
>> Support for drop in features Support for drop in features
>> *Cons:* *Cons:*
>> Follows the menu, and it could change Need to change current code
>> Hard to find features Some additional plumbing might be needed
>> Drop in features need to be placed in a specific location according to
>> the menu location
>>
>> What are your thought about this architecture?
>>
> I am not a fan of flat file structure in application.
> There are many reasons - why we need namespace in C++, same applies here.
>
> Let me start from the question "What do we want from pgAdmin 4?"
> * A object browser
> * Miscallenous operations associated with the object shown in the object
> browser
> + Reversed Enginering query (if any)
> + Properties Viewer
> + Edit/Create Viewer
> + Staticis
> + List of dependencies
> + List of dependents
> * Query tool
> * Data Viewer (table/view)
> * Function debugger
> * Extendability/Pluggability
>
> According to me - these as the intent of the application, and not objects
> assiciated with them.
>
> I do agree, there is no boundry (or, very thin) between data &
> presentation at the moment.
> We can start from there.
>
> I can think of the following directory structure ( which is not too
> different from the current, but - still will give the developers a lot more
> comfort as per your complain :-) ).
>
>> pgadmin/
>> - database_objects/
>> - server/
>> - __init__.py
>> - create.py
>> - get.py
>> - sql.py
>> - update.py
>> - static/
>> - img/
>> - server.svg
>> - server_bad.svg
>> - javascripts/
>> - server.js
>> - ui_schema.js
>> - database/
>> - templates
>> ...
>> ...
>> + schema/
>> ...
>> - tests/
>> - api/
>> ...
>> - gui/
>> ...
>> - karma/
>> ...
>> - browser/
>> + pgagents/
>> - server_groups/
>> - servers/
>> - databases/
>> - __init__.py
>> - node.py
>> - schemas
>> ...
>> ...
>> ...
>> ...
>> + tests/
>> - tools/
>> + sqleditor/
>> + dataviewer/
>> + debugger/
>> - misc/
>> + sql/
>> + statistics/
>> ...
>>
>> NOTE:
> We also need to think about the facts that.
> These changes may lead to a lot of night mare packagers to make changes in
> the installers in upgrade mode.
>
> Also - when I say pluggability/extendability, we should be able to create
> plugins on top of pgAdmin 4.
> Just take an example of this feature (Geospatial Query Viewer)
> <https://www.postgresql.org/message-id/[email protected]...;
> .
>
> I think - it is best implemented as an plugin, as not every pgAdmin user
> will benificiary.
> We need ability to load the modules as pluggable module same as we used to
> have before we introduced the webpack to bundle everything as modules.
> We also need to start thinking about it.
>
> -- Thanks, Ashesh
>
> Around minute 7 of this video
>> <https://www.youtube.com/watch?v=hALFGQNeEnU; Uncle Bob shows an
>> application written
>> in rails to talk about architecture. It is a long KeyNote if you are
>> curious I would advise you to see the full video.
>> His approach to architecture of the application is pretty interesting.
>>
>
>
>
^ permalink raw reply [nested|flat] 69+ messages in thread
end of thread, other threads:[~2018-06-11 10:35 UTC | newest]
Thread overview: 69+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2018-04-02 18:45 [pgadmin4][patch] Initial patch to decouple from ACI Tree Joao De Almeida Pereira <[email protected]>
2018-04-04 16:17 ` Dave Page <[email protected]>
2018-04-05 03:35 ` Khushboo Vashi <[email protected]>
2018-04-05 10:31 ` Khushboo Vashi <[email protected]>
2018-04-05 13:38 ` Joao De Almeida Pereira <[email protected]>
2018-04-06 08:48 ` Murtuza Zabuawala <[email protected]>
2018-04-06 19:13 ` Joao De Almeida Pereira <[email protected]>
2018-04-09 05:59 ` Khushboo Vashi <[email protected]>
2018-04-10 16:00 ` Joao De Almeida Pereira <[email protected]>
2018-04-18 14:22 ` Joao De Almeida Pereira <[email protected]>
2018-04-24 19:13 ` Joao De Almeida Pereira <[email protected]>
2018-04-25 07:52 ` Dave Page <[email protected]>
2018-04-27 09:15 ` Dave Page <[email protected]>
2018-04-27 09:34 ` Ashesh Vashi <[email protected]>
2018-04-27 22:25 ` Joao De Almeida Pereira <[email protected]>
2018-04-30 06:16 ` Ashesh Vashi <[email protected]>
2018-04-30 10:21 ` Anthony Emengo <[email protected]>
2018-04-30 10:24 ` Ashesh Vashi <[email protected]>
2018-04-30 14:57 ` Joao De Almeida Pereira <[email protected]>
2018-04-30 15:00 ` Ashesh Vashi <[email protected]>
2018-04-30 15:03 ` Ashesh Vashi <[email protected]>
2018-04-30 15:28 ` Anthony Emengo <[email protected]>
2018-04-30 15:33 ` Ashesh Vashi <[email protected]>
2018-04-30 15:37 ` Dave Page <[email protected]>
2018-04-30 15:45 ` Ashesh Vashi <[email protected]>
2018-04-30 17:57 ` Joao De Almeida Pereira <[email protected]>
2018-05-02 05:30 ` Ashesh Vashi <[email protected]>
2018-05-02 08:08 ` Dave Page <[email protected]>
2018-05-02 14:54 ` Joao De Almeida Pereira <[email protected]>
2018-05-04 22:01 ` Joao De Almeida Pereira <[email protected]>
2018-05-04 22:03 ` Joao De Almeida Pereira <[email protected]>
2018-05-07 19:01 ` Joao De Almeida Pereira <[email protected]>
2018-05-09 11:02 ` Khushboo Vashi <[email protected]>
2018-05-09 12:38 ` Akshay Joshi <[email protected]>
2018-05-09 15:00 ` Joao De Almeida Pereira <[email protected]>
2018-05-10 06:52 ` Ashesh Vashi <[email protected]>
2018-05-10 14:38 ` Anthony Emengo <[email protected]>
2018-05-11 06:31 ` Ashesh Vashi <[email protected]>
2018-05-11 21:27 ` Joao De Almeida Pereira <[email protected]>
2018-05-11 23:10 ` Ashesh Vashi <[email protected]>
2018-05-14 09:29 ` Dave Page <[email protected]>
2018-05-14 12:38 ` Ashesh Vashi <[email protected]>
2018-05-14 12:40 ` Dave Page <[email protected]>
2018-05-14 12:41 ` Ashesh Vashi <[email protected]>
2018-05-15 10:37 ` Ashesh Vashi <[email protected]>
2018-05-15 15:03 ` Joao De Almeida Pereira <[email protected]>
2018-05-16 07:20 ` Ashesh Vashi <[email protected]>
2018-05-16 15:25 ` Anthony Emengo <[email protected]>
2018-05-21 13:46 ` Anthony Emengo <[email protected]>
2018-05-21 13:56 ` Dave Page <[email protected]>
2018-05-23 14:14 ` Joao De Almeida Pereira <[email protected]>
2018-05-23 14:16 ` Dave Page <[email protected]>
2018-05-24 06:59 ` Ashesh Vashi <[email protected]>
2018-05-24 14:43 ` Joao De Almeida Pereira <[email protected]>
2018-05-31 14:39 ` Robert Eckhardt <[email protected]>
2018-05-31 15:13 ` Dave Page <[email protected]>
2018-06-01 11:52 ` Ashesh Vashi <[email protected]>
2018-06-01 15:17 ` Anthony Emengo <[email protected]>
2018-06-01 16:39 ` Victoria Henry <[email protected]>
2018-06-01 18:36 ` Ashesh Vashi <[email protected]>
2018-06-01 19:17 ` Victoria Henry <[email protected]>
2018-06-06 11:07 ` Aditya Toshniwal <[email protected]>
2018-06-06 15:25 ` Victoria Henry <[email protected]>
2018-06-07 04:35 ` Aditya Toshniwal <[email protected]>
2018-06-07 15:21 ` Victoria Henry <[email protected]>
2018-06-11 05:44 ` Aditya Toshniwal <[email protected]>
2018-06-07 07:41 ` Khushboo Vashi <[email protected]>
2018-06-11 10:29 ` Ashesh Vashi <[email protected]>
2018-06-11 10:35 ` Ashesh Vashi <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox