IGNITE-843 Web console initial commit.
Project: http://git-wip-us.apache.org/repos/asf/ignite/repo Commit: http://git-wip-us.apache.org/repos/asf/ignite/commit/bce0deb7 Tree: http://git-wip-us.apache.org/repos/asf/ignite/tree/bce0deb7 Diff: http://git-wip-us.apache.org/repos/asf/ignite/diff/bce0deb7 Branch: refs/heads/ignite-843-rc1 Commit: bce0deb71f5a70868725ea696644f9b1f96cf2b1 Parents: 81962c4 Author: Andrey <anovi...@gridgain.com> Authored: Tue Oct 13 10:06:27 2015 +0700 Committer: Andrey <anovi...@gridgain.com> Committed: Tue Oct 13 10:06:27 2015 +0700 ---------------------------------------------------------------------- .../control-center-web/licenses/apache-2.0.txt | 202 ++ modules/control-center-web/pom.xml | 71 + .../control-center-web/src/main/js/.gitignore | 4 + .../control-center-web/src/main/js/DEVNOTES.txt | 21 + .../src/main/js/agents/agent-manager.js | 312 +++ .../src/main/js/agents/agent-server.js | 98 + modules/control-center-web/src/main/js/app.js | 190 ++ modules/control-center-web/src/main/js/bin/www | 126 ++ .../src/main/js/config/default.json | 25 + .../src/main/js/controllers/admin-controller.js | 81 + .../main/js/controllers/caches-controller.js | 594 ++++++ .../main/js/controllers/clusters-controller.js | 560 +++++ .../src/main/js/controllers/common-module.js | 2017 ++++++++++++++++++ .../main/js/controllers/metadata-controller.js | 1252 +++++++++++ .../src/main/js/controllers/models/caches.json | 1027 +++++++++ .../main/js/controllers/models/clusters.json | 1333 ++++++++++++ .../main/js/controllers/models/metadata.json | 279 +++ .../src/main/js/controllers/models/summary.json | 172 ++ .../main/js/controllers/profile-controller.js | 94 + .../src/main/js/controllers/sql-controller.js | 1097 ++++++++++ .../main/js/controllers/summary-controller.js | 233 ++ modules/control-center-web/src/main/js/db.js | 431 ++++ .../src/main/js/helpers/common-utils.js | 104 + .../src/main/js/helpers/configuration-loader.js | 43 + .../src/main/js/helpers/data-structures.js | 113 + .../src/main/js/keys/test.crt | 13 + .../src/main/js/keys/test.key | 18 + .../control-center-web/src/main/js/package.json | 53 + .../public/stylesheets/_bootstrap-custom.scss | 67 + .../stylesheets/_bootstrap-variables.scss | 890 ++++++++ .../src/main/js/public/stylesheets/style.scss | 1838 ++++++++++++++++ .../src/main/js/routes/admin.js | 127 ++ .../src/main/js/routes/agent.js | 261 +++ .../src/main/js/routes/caches.js | 171 ++ .../src/main/js/routes/clusters.js | 145 ++ .../js/routes/generator/generator-common.js | 353 +++ .../js/routes/generator/generator-docker.js | 60 + .../main/js/routes/generator/generator-java.js | 1581 ++++++++++++++ .../js/routes/generator/generator-properties.js | 112 + .../main/js/routes/generator/generator-xml.js | 1202 +++++++++++ .../src/main/js/routes/metadata.js | 192 ++ .../src/main/js/routes/notebooks.js | 157 ++ .../src/main/js/routes/presets.js | 70 + .../src/main/js/routes/profile.js | 105 + .../src/main/js/routes/public.js | 266 +++ .../src/main/js/routes/sql.js | 39 + .../src/main/js/routes/summary.js | 104 + .../src/main/js/views/configuration/caches.jade | 46 + .../main/js/views/configuration/clusters.jade | 46 + .../js/views/configuration/metadata-load.jade | 89 + .../main/js/views/configuration/metadata.jade | 65 + .../main/js/views/configuration/sidebar.jade | 48 + .../js/views/configuration/summary-tabs.jade | 24 + .../main/js/views/configuration/summary.jade | 124 ++ .../src/main/js/views/error.jade | 22 + .../src/main/js/views/includes/controls.jade | 515 +++++ .../src/main/js/views/includes/footer.jade | 22 + .../src/main/js/views/includes/header.jade | 40 + .../src/main/js/views/index.jade | 141 ++ .../src/main/js/views/reset.jade | 38 + .../src/main/js/views/settings/admin.jade | 57 + .../src/main/js/views/settings/profile.jade | 68 + .../src/main/js/views/sql/cache-metadata.jade | 26 + .../src/main/js/views/sql/chart-settings.jade | 38 + .../src/main/js/views/sql/notebook-new.jade | 31 + .../src/main/js/views/sql/paragraph-rate.jade | 31 + .../src/main/js/views/sql/sql.jade | 173 ++ .../main/js/views/templates/agent-download.jade | 49 + .../main/js/views/templates/batch-confirm.jade | 32 + .../src/main/js/views/templates/clone.jade | 32 + .../src/main/js/views/templates/confirm.jade | 27 + .../src/main/js/views/templates/layout.jade | 82 + .../src/main/js/views/templates/message.jade | 26 + .../src/main/js/views/templates/select.jade | 26 + .../js/views/templates/validation-error.jade | 25 + .../src/test/js/routes/agent.js | 96 + 76 files changed, 20342 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/licenses/apache-2.0.txt ---------------------------------------------------------------------- diff --git a/modules/control-center-web/licenses/apache-2.0.txt b/modules/control-center-web/licenses/apache-2.0.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/modules/control-center-web/licenses/apache-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/pom.xml ---------------------------------------------------------------------- diff --git a/modules/control-center-web/pom.xml b/modules/control-center-web/pom.xml new file mode 100644 index 0000000..fcd9b91 --- /dev/null +++ b/modules/control-center-web/pom.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- + ~ /* + ~ * Licensed to the Apache Software Foundation (ASF) under one or more + ~ * contributor license agreements. See the NOTICE file distributed with + ~ * this work for additional information regarding copyright ownership. + ~ * The ASF licenses this file to You under the Apache License, Version 2.0 + ~ * (the "License"); you may not use this file except in compliance with + ~ * the License. You may obtain a copy of the License at + ~ * + ~ * http://www.apache.org/licenses/LICENSE-2.0 + ~ * + ~ * Unless required by applicable law or agreed to in writing, software + ~ * distributed under the License is distributed on an "AS IS" BASIS, + ~ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ * See the License for the specific language governing permissions and + ~ * limitations under the License. + ~ */ + --> + +<!-- + POM file. +--> +<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.apache.ignite</groupId> + <artifactId>ignite-parent</artifactId> + <version>1</version> + <relativePath>../../parent</relativePath> + </parent> + + <artifactId>ignite-control-center-web</artifactId> + <version>1.5.0-SNAPSHOT</version> + + <build> + <plugins> + <plugin> + <groupId>com.github.eirslett</groupId> + <artifactId>frontend-maven-plugin</artifactId> + <version>0.0.23</version> + + <configuration> + <workingDirectory>src/main/js</workingDirectory> + </configuration> + + <executions> + <execution> + <id>install node and npm</id> + <goals> + <goal>install-node-and-npm</goal> + </goals> + <configuration> + <nodeVersion>v0.12.7</nodeVersion> + <npmVersion>2.13.2</npmVersion> + </configuration> + </execution> + + <execution> + <id>npm install</id> + <goals> + <goal>npm</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/.gitignore ---------------------------------------------------------------------- diff --git a/modules/control-center-web/src/main/js/.gitignore b/modules/control-center-web/src/main/js/.gitignore new file mode 100644 index 0000000..96db808 --- /dev/null +++ b/modules/control-center-web/src/main/js/.gitignore @@ -0,0 +1,4 @@ +node_modules +*.idea +*.log +*.css http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/DEVNOTES.txt ---------------------------------------------------------------------- diff --git a/modules/control-center-web/src/main/js/DEVNOTES.txt b/modules/control-center-web/src/main/js/DEVNOTES.txt new file mode 100644 index 0000000..4859cf1 --- /dev/null +++ b/modules/control-center-web/src/main/js/DEVNOTES.txt @@ -0,0 +1,21 @@ +Ignite Web Console Instructions +====================================== + +How to deploy: + +1. Install locally NodeJS using installer from site https://nodejs.org for your OS. +2. Install locally MongoDB follow instructions from site http://docs.mongodb.org/manual/installation +3. Checkout ignite-843 branch. +4. Change directory '$IGNITE_HOME/modules/control-center-web/src/main/js'. +5. Run "npm install" in terminal for download all dependencies. + +Steps 1 - 5 should be executed once. + +How to run: + +1. Run MongoDB. + 1.1 In terminal change dir to $MONGO_INSTALL_DIR/server/3.0/bin. + 1.2 Run "mongod". +2. In new terminal change directory '$IGNITE_HOME/modules/control-center-web/src/main/js'. +3. Start application by executing "npm start". +4. In browser open: http://localhost:3000 http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/agents/agent-manager.js ---------------------------------------------------------------------- diff --git a/modules/control-center-web/src/main/js/agents/agent-manager.js b/modules/control-center-web/src/main/js/agents/agent-manager.js new file mode 100644 index 0000000..582cb11 --- /dev/null +++ b/modules/control-center-web/src/main/js/agents/agent-manager.js @@ -0,0 +1,312 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +var WebSocketServer = require('ws').Server; + +var apacheIgnite = require('apache-ignite'); + +var db = require('../db'); + +var AgentServer = require('./agent-server').AgentServer; + +/** + * @constructor + */ +function AgentManager(srv) { + this._clients = {}; + + this._server = srv; + + this._wss = new WebSocketServer({ server: this._server }); + + var self = this; + + this._wss.on('connection', function(ws) { + new Client(ws, self); + }); +} + +/** + * @param userId + * @param {Client} client + */ +AgentManager.prototype._removeClient = function(userId, client) { + var connections = this._clients[userId]; + + if (connections) { + removeFromArray(connections, client); + + if (connections.length == 0) + delete this._clients[userId]; + } +}; + +/** + * @param userId + * @param {Client} client + */ +AgentManager.prototype._addClient = function(userId, client) { + var existingConnections = this._clients[userId]; + + if (!existingConnections) { + existingConnections = []; + + this._clients[userId] = existingConnections; + } + + existingConnections.push(client); +}; + +/** + * @param userId + * @return {Client} + */ +AgentManager.prototype.findClient = function(userId) { + var clientsList = this._clients[userId]; + + if (!clientsList || clientsList.length == 0) + return null; + + return clientsList[0]; +}; + +/** + * @constructor + * @param {AgentManager} manager + * @param {WebSocket} ws + */ +function Client(ws, manager) { + var self = this; + + this._manager = manager; + this._ws = ws; + + ws.on('close', function() { + if (self._user) { + self._manager._removeClient(self._user._id, self); + } + }); + + ws.on('message', function (msgStr) { + var msg = JSON.parse(msgStr); + + self['_rmt' + msg.type](msg); + }); + + this._reqCounter = 0; + + this._cbMap = {}; +} + +/** + * @param {String} path + * @param {Object} params + * @param {String} [method] + * @param {Object} [headers] + * @param {String} [body] + * @param {Function} [cb] Callback. Take 3 arguments: {String} error, {number} httpCode, {string} response. + */ +Client.prototype.executeRest = function(path, params, method, headers, body, cb) { + if (typeof(params) != 'object') + throw '"params" argument must be an object'; + + if (typeof(cb) != 'function') + throw 'callback must be a function'; + + if (body && typeof(body) != 'string') + throw 'body must be a string'; + + if (headers && typeof(headers) != 'object') + throw 'headers must be an object'; + + if (!method) + method = 'GET'; + else + method = method.toUpperCase(); + + if (method != 'GET' && method != 'POST') + throw 'Unknown HTTP method: ' + method; + + var newArgs = argsToArray(arguments); + + newArgs[5] = function(ex, res) { + if (ex) + cb(ex.message); + else + cb(null, res.code, res.message) + }; + + this._invokeRmtMethod('executeRest', newArgs); +}; + +/** + * @param {string} error + */ +Client.prototype.authResult = function(error) { + this._invokeRmtMethod('authResult', arguments) +}; + +/** + * @param {String} jdbcDriverJarPath + * @param {String} jdbcDriverClass + * @param {String} jdbcUrl + * @param {Object} jdbcInfo + * @param {Function} cb Callback. Take two arguments: {Object} exception, {Object} result. + * @return {Array} List of tables (see org.apache.ignite.schema.parser.DbTable java class) + */ +Client.prototype.metadataSchemas = function(jdbcDriverJarPath, jdbcDriverClass, jdbcUrl, jdbcInfo, cb) { + this._invokeRmtMethod('schemas', arguments) +}; + +/** + * @param {String} jdbcDriverJarPath + * @param {String} jdbcDriverClass + * @param {String} jdbcUrl + * @param {Object} jdbcInfo + * @param {Array} schemas + * @param {Boolean} tablesOnly + * @param {Function} cb Callback. Take two arguments: {Object} exception, {Object} result. + * @return {Array} List of tables (see org.apache.ignite.schema.parser.DbTable java class) + */ +Client.prototype.metadataTables = function(jdbcDriverJarPath, jdbcDriverClass, jdbcUrl, jdbcInfo, schemas, tablesOnly, cb) { + this._invokeRmtMethod('metadata', arguments) +}; + +/** + * @param {Function} cb Callback. Take two arguments: {Object} exception, {Object} result. + * @return {Array} List of jars from driver folder. + */ +Client.prototype.availableDrivers = function(cb) { + this._invokeRmtMethod('availableDrivers', arguments) +}; + +Client.prototype._invokeRmtMethod = function(methodName, args) { + var cb = null; + + var m = argsToArray(args); + + if (m.length > 0 && typeof m[m.length - 1] == 'function') + cb = m.pop(); + + if (this._ws.readyState != 1) { + if (cb) + cb({type: 'org.apache.ignite.agent.AgentException', message: 'Connection is closed'}); + + return + } + + var msg = { + mtdName: methodName, + args: m + }; + + if (cb) { + var reqId = this._reqCounter++; + + this._cbMap[reqId] = cb; + + msg.reqId = reqId; + } + + this._ws.send(JSON.stringify(msg)) +}; + +Client.prototype._rmtAuthMessage = function(msg) { + var self = this; + + db.Account.findOne({ token: msg.token }, function (err, account) { + if (err) { + self.authResult('Failed to authorize user'); + // TODO IGNITE-1379 send error to web master. + } + else if (!account) + self.authResult('Invalid token, user not found'); + else { + self.authResult(null); + + self._user = account; + + self._manager._addClient(account._id, self); + + self._ignite = new apacheIgnite.Ignite(new AgentServer(self)); + } + }); +}; + +Client.prototype._rmtCallRes = function(msg) { + var cb = this._cbMap[msg.reqId]; + + if (!cb) return; + + delete this._cbMap[msg.reqId]; + + if (msg.ex) + cb(msg.ex); + else + cb(null, msg.res); +}; + +/** + * @return {Ignite} + */ +Client.prototype.ignite = function() { + return this._ignite; +}; + +function removeFromArray(arr, val) { + var idx; + + while ((idx = arr.indexOf(val)) !== -1) { + arr.splice(idx, 1); + } +} + +/** + * @param args + * @returns {Array} + */ +function argsToArray(args) { + var res = []; + + for (var i = 0; i < args.length; i++) + res.push(args[i]) + + return res; +} + +exports.AgentManager = AgentManager; + +/** + * @type {AgentManager} + */ +var manager = null; + +exports.createManager = function(srv) { + if (manager) + throw 'Agent manager already cleared!'; + + manager = new AgentManager(srv); +}; + +/** + * @return {AgentManager} + */ +exports.getAgentManager = function() { + return manager; +}; http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/agents/agent-server.js ---------------------------------------------------------------------- diff --git a/modules/control-center-web/src/main/js/agents/agent-server.js b/modules/control-center-web/src/main/js/agents/agent-server.js new file mode 100644 index 0000000..bd7efbf --- /dev/null +++ b/modules/control-center-web/src/main/js/agents/agent-server.js @@ -0,0 +1,98 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +var _ = require('lodash'); + +/** + * Creates an instance of server for Ignite + * + * @constructor + * @this {AgentServer} + * @param {Client} client connected client + */ +function AgentServer(client) { + this._client = client; +} + +/** + * Run http request + * + * @this {AgentServer} + * @param {Command} cmd Command + * @param {callback} callback on finish + */ +AgentServer.prototype.runCommand = function(cmd, callback) { + var params = {cmd: cmd.name()}; + + _.forEach(cmd._params, function (p) { + params[p.key] = p.value; + }); + + var body = undefined; + + var headers = undefined; + + var method = 'GET'; + + if (cmd._isPost()) { + body = cmd.postData(); + + method = 'POST'; + + headers = {'JSONObject': 'application/json'}; + } + + this._client.executeRest("ignite", params, method, headers, body, function(error, code, message) { + if (error) { + callback(error); + return + } + + if (code !== 200) { + if (code === 401) { + callback.call(null, "Authentication failed. Status code 401."); + } + else { + callback.call(null, "Request failed. Status code " + code); + } + + return; + } + + var igniteResponse; + + try { + igniteResponse = JSON.parse(message); + } + catch (e) { + callback.call(null, e, null); + + return; + } + + if (igniteResponse.successStatus) { + callback.call(null, igniteResponse.error, null) + } + else { + callback.call(null, null, igniteResponse.response); + } + }); +}; + +exports.AgentServer = AgentServer; http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/app.js ---------------------------------------------------------------------- diff --git a/modules/control-center-web/src/main/js/app.js b/modules/control-center-web/src/main/js/app.js new file mode 100644 index 0000000..69f6663 --- /dev/null +++ b/modules/control-center-web/src/main/js/app.js @@ -0,0 +1,190 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +var express = require('express'); +var compress = require('compression'); +var path = require('path'); +var favicon = require('serve-favicon'); +var logger = require('morgan'); +var cookieParser = require('cookie-parser'); +var bodyParser = require('body-parser'); +var session = require('express-session'); +var mongoStore = require('connect-mongo')(session); +var forceSSL = require('express-force-ssl'); +var config = require('./helpers/configuration-loader.js'); + +var publicRoutes = require('./routes/public'); +var notebooksRoutes = require('./routes/notebooks'); +var clustersRouter = require('./routes/clusters'); +var cachesRouter = require('./routes/caches'); +var metadataRouter = require('./routes/metadata'); +var presetsRouter = require('./routes/presets'); +var summary = require('./routes/summary'); +var adminRouter = require('./routes/admin'); +var profileRouter = require('./routes/profile'); +var sqlRouter = require('./routes/sql'); +var agentRouter = require('./routes/agent'); + +var passport = require('passport'); + +var db = require('./db'); + +var app = express(); + +app.use(compress()); + +app.use(bodyParser.json({limit: '50mb'})); +app.use(bodyParser.urlencoded({limit: '50mb', extended: true})); + +// Views engine setup. +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'jade'); + +// Site favicon. +app.use(favicon(__dirname + '/public/favicon.ico')); + +app.use(logger('dev')); + +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({extended: false})); + +app.use(require('node-sass-middleware')({ + /* Options */ + src: path.join(__dirname, 'public'), + dest: path.join(__dirname, 'public'), + debug: true, + outputStyle: 'nested' +})); + +app.use(express.static(path.join(__dirname, 'public'))); +app.use(express.static(path.join(__dirname, 'controllers'))); +app.use(express.static(path.join(__dirname, 'helpers'))); +app.use(express.static(path.join(__dirname, 'routes/generator'))); + +app.use(cookieParser('keyboard cat')); + +app.use(session({ + secret: 'keyboard cat', + resave: false, + saveUninitialized: true, + store: new mongoStore({ + mongooseConnection: db.mongoose.connection + }) +})); + +app.use(passport.initialize()); +app.use(passport.session()); + +passport.serializeUser(db.Account.serializeUser()); +passport.deserializeUser(db.Account.deserializeUser()); + +passport.use(db.Account.createStrategy()); + +if (config.get('server:ssl')) { + var httpsPort = config.normalizePort(config.get('server:https-port') || 443); + + app.set('forceSSLOptions', { + enable301Redirects: true, + trustXFPHeader: true, + httpsPort: httpsPort + }); + + app.use(forceSSL); +} + +var mustAuthenticated = function (req, res, next) { + req.isAuthenticated() ? next() : res.redirect('/'); +}; + +var adminOnly = function(req, res, next) { + req.isAuthenticated() && req.user.admin ? next() : res.sendStatus(403); +}; + +app.all('/configuration/*', mustAuthenticated); + +app.all('*', function(req, res, next) { + var becomeUsed = req.session.viewedUser && req.user.admin; + + if (req.url.lastIndexOf('/reset', 0) === 0) { + res.locals.user = null; + res.locals.becomeUsed = false; + } + else { + res.locals.user = becomeUsed ? req.session.viewedUser : req.user; + res.locals.becomeUsed = becomeUsed; + } + + req.currentUserId = function() { + if (!req.user) + return null; + + if (req.session.viewedUser && req.user.admin) + return req.session.viewedUser._id; + + return req.user._id; + }; + + next(); +}); + +app.use('/', publicRoutes); +app.use('/admin', mustAuthenticated, adminOnly, adminRouter); +app.use('/profile', mustAuthenticated, profileRouter); + +app.use('/configuration/clusters', clustersRouter); +app.use('/configuration/caches', cachesRouter); +app.use('/configuration/metadata', metadataRouter); +app.use('/configuration/presets', presetsRouter); +app.use('/configuration/summary', summary); + +app.use('/notebooks', mustAuthenticated, notebooksRoutes); +app.use('/sql', mustAuthenticated, sqlRouter); + +app.use('/agent', mustAuthenticated, agentRouter); + +// Catch 404 and forward to error handler. +app.use(function (req, res, next) { + var err = new Error('Not Found: ' + req.originalUrl); + err.status = 404; + next(err); +}); + +// Error handlers. + +// Development error handler: will print stacktrace. +if (app.get('env') === 'development') { + app.use(function (err, req, res) { + res.status(err.status || 500); + res.render('error', { + message: err.message, + error: err + }); + }); +} + +// Production error handler: no stacktraces leaked to user. +app.use(function (err, req, res) { + res.status(err.status || 500); + res.render('error', { + message: err.message, + error: {} + }); +}); + +module.exports = app; http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/bin/www ---------------------------------------------------------------------- diff --git a/modules/control-center-web/src/main/js/bin/www b/modules/control-center-web/src/main/js/bin/www new file mode 100644 index 0000000..69e73e3 --- /dev/null +++ b/modules/control-center-web/src/main/js/bin/www @@ -0,0 +1,126 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +#!/usr/bin/env node + +/** + * Module dependencies. + */ +var http = require('http'); +var https = require('https'); +var config = require('../helpers/configuration-loader.js'); +var app = require('../app'); +var agentManager = require('../agents/agent-manager'); + +var fs = require('fs'); + +var debug = require('debug')('ignite-web-console:server'); + +/** + * Get port from environment and store in Express. + */ +var port = config.normalizePort(config.get('server:port') || process.env.PORT || 80); + +// Create HTTP server. +var server = http.createServer(app); + +app.set('port', port); + +/** + * Listen on provided port, on all network interfaces. + */ +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); + +if (config.get('server:ssl')) { + httpsServer = https.createServer({ + key: fs.readFileSync(config.get('server:key')), + cert: fs.readFileSync(config.get('server:cert')), + passphrase: config.get('server:keyPassphrase') + }, app); + + var httpsPort = config.normalizePort(config.get('server:https-port') || 443); + + /** + * Listen on provided port, on all network interfaces. + */ + httpsServer.listen(httpsPort); + httpsServer.on('error', onError); + httpsServer.on('listening', onListening); +} + +/** + * Start agent server. + */ +var agentServer; + +if (config.get('agent-server:ssl')) { + agentServer = https.createServer({ + key: fs.readFileSync(config.get('agent-server:key')), + cert: fs.readFileSync(config.get('agent-server:cert')), + passphrase: config.get('agent-server:keyPassphrase') + }); +} +else { + agentServer = http.createServer(); +} + +agentServer.listen(config.get('agent-server:port')); + +agentManager.createManager(agentServer); + +/** + * Event listener for HTTP server "error" event. + */ +function onError(error) { + if (error.syscall !== 'listen') { + throw error; + } + + var bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ +function onListening() { + var addr = server.address(); + var bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + + debug('Listening on ' + bind); +} http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/config/default.json ---------------------------------------------------------------------- diff --git a/modules/control-center-web/src/main/js/config/default.json b/modules/control-center-web/src/main/js/config/default.json new file mode 100644 index 0000000..bf1e88b --- /dev/null +++ b/modules/control-center-web/src/main/js/config/default.json @@ -0,0 +1,25 @@ +{ + "server": { + "port": 3000, + "https-port": 8443, + "ssl": false, + "key": "keys/test.key", + "cert": "keys/test.crt", + "keyPassphrase": "password" + }, + "mongoDB": { + "url": "mongodb://localhost/web-control-center" + }, + "agent-server": { + "port": 3001, + "ssl": true, + "key": "keys/test.key", + "cert": "keys/test.crt", + "keyPassphrase": "password" + }, + "smtp": { + "service": "", + "username": "", + "password": "" + } +} http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/controllers/admin-controller.js ---------------------------------------------------------------------- diff --git a/modules/control-center-web/src/main/js/controllers/admin-controller.js b/modules/control-center-web/src/main/js/controllers/admin-controller.js new file mode 100644 index 0000000..0b57da5 --- /dev/null +++ b/modules/control-center-web/src/main/js/controllers/admin-controller.js @@ -0,0 +1,81 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +// Controller for Admin screen. +consoleModule.controller('adminController', + ['$scope', '$window', '$http', '$common', '$confirm', + function ($scope, $window, $http, $common, $confirm) { + $scope.users = null; + + function reload() { + $http.post('admin/list') + .success(function (data) { + $scope.users = data; + }) + .error(function (errMsg) { + $common.showError($common.errorMessage(errMsg)); + }); + } + + reload(); + + $scope.becomeUser = function (user) { + $window.location = '/admin/become?viewedUserId=' + user._id; + }; + + $scope.removeUser = function (user) { + $confirm.confirm('Are you sure you want to remove user: "' + user.username + '"?') + .then(function () { + $http.post('admin/remove', {userId: user._id}).success( + function () { + var i = _.findIndex($scope.users, function (u) { + return u._id == user._id; + }); + + if (i >= 0) + $scope.users.splice(i, 1); + + $common.showInfo('User has been removed: "' + user.username + '"'); + }).error(function (errMsg, status) { + if (status == 503) + $common.showInfo(errMsg); + else + $common.showError('Failed to remove user: "' + $common.errorMessage(errMsg) + '"'); + }); + }); + }; + + $scope.toggleAdmin = function (user) { + if (user.adminChanging) + return; + + user.adminChanging = true; + + $http.post('admin/save', {userId: user._id, adminFlag: user.admin}).success( + function () { + $common.showInfo('Admin right was successfully toggled for user: "' + user.username + '"'); + + user.adminChanging = false; + }).error(function (errMsg) { + $common.showError('Failed to toggle admin right for user: "' + $common.errorMessage(errMsg) + '"'); + + user.adminChanging = false; + }); + } +}]); http://git-wip-us.apache.org/repos/asf/ignite/blob/bce0deb7/modules/control-center-web/src/main/js/controllers/caches-controller.js ---------------------------------------------------------------------- diff --git a/modules/control-center-web/src/main/js/controllers/caches-controller.js b/modules/control-center-web/src/main/js/controllers/caches-controller.js new file mode 100644 index 0000000..28cedf0 --- /dev/null +++ b/modules/control-center-web/src/main/js/controllers/caches-controller.js @@ -0,0 +1,594 @@ +/* + * + * * Licensed to the Apache Software Foundation (ASF) under one or more + * * contributor license agreements. See the NOTICE file distributed with + * * this work for additional information regarding copyright ownership. + * * The ASF licenses this file to You under the Apache License, Version 2.0 + * * (the "License"); you may not use this file except in compliance with + * * the License. You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +// Controller for Caches screen. +consoleModule.controller('cachesController', [ + '$scope', '$controller', '$filter', '$http', '$timeout', '$common', '$focus', '$confirm', '$message', '$clone', '$table', '$preview', '$loading', '$unsavedChangesGuard', + function ($scope, $controller, $filter, $http, $timeout, $common, $focus, $confirm, $message, $clone, $table, $preview, $loading, $unsavedChangesGuard) { + $unsavedChangesGuard.install($scope); + + // Initialize the super class and extend it. + angular.extend(this, $controller('save-remove', {$scope: $scope})); + + $scope.ui = $common.formUI(); + + $scope.showMoreInfo = $message.message; + + $scope.joinTip = $common.joinTip; + $scope.getModel = $common.getModel; + $scope.javaBuildInClasses = $common.javaBuildInClasses; + $scope.compactJavaName = $common.compactJavaName; + $scope.saveBtnTipText = $common.saveBtnTipText; + + $scope.tableReset = $table.tableReset; + $scope.tableNewItem = $table.tableNewItem; + $scope.tableNewItemActive = $table.tableNewItemActive; + $scope.tableEditing = $table.tableEditing; + $scope.tableStartEdit = $table.tableStartEdit; + $scope.tableRemove = function (item, field, index) { + $table.tableRemove(item, field, index); + }; + + $scope.tableSimpleSave = $table.tableSimpleSave; + $scope.tableSimpleSaveVisible = $table.tableSimpleSaveVisible; + $scope.tableSimpleUp = $table.tableSimpleUp; + $scope.tableSimpleDown = $table.tableSimpleDown; + $scope.tableSimpleDownVisible = $table.tableSimpleDownVisible; + + $scope.tablePairSave = $table.tablePairSave; + $scope.tablePairSaveVisible = $table.tablePairSaveVisible; + + var previews = []; + + $scope.previewInit = function (preview) { + previews.push(preview); + + $preview.previewInit(preview); + }; + + $scope.previewChanged = $preview.previewChanged; + + $scope.hidePopover = $common.hidePopover; + + var showPopoverMessage = $common.showPopoverMessage; + + $scope.atomicities = $common.mkOptions(['ATOMIC', 'TRANSACTIONAL']); + + $scope.cacheModes = $common.mkOptions(['PARTITIONED', 'REPLICATED', 'LOCAL']); + + $scope.atomicWriteOrderModes = $common.mkOptions(['CLOCK', 'PRIMARY']); + + $scope.memoryModes = $common.mkOptions(['ONHEAP_TIERED', 'OFFHEAP_TIERED', 'OFFHEAP_VALUES']); + + $scope.evictionPolicies = [ + {value: 'LRU', label: 'LRU'}, + {value: 'RND', label: 'Random'}, + {value: 'FIFO', label: 'FIFO'}, + {value: 'SORTED', label: 'Sorted'}, + {value: undefined, label: 'Not set'} + ]; + + $scope.rebalanceModes = $common.mkOptions(['SYNC', 'ASYNC', 'NONE']); + + $scope.cacheStoreFactories = [ + {value: 'CacheJdbcPojoStoreFactory', label: 'JDBC POJO store factory'}, + {value: 'CacheJdbcBlobStoreFactory', label: 'JDBC BLOB store factory'}, + {value: 'CacheHibernateBlobStoreFactory', label: 'Hibernate BLOB store factory'}, + {value: undefined, label: 'Not set'} + ]; + + $scope.cacheStoreJdbcDialects = [ + {value: 'Oracle', label: 'Oracle'}, + {value: 'DB2', label: 'IBM DB2'}, + {value: 'SQLServer', label: 'Microsoft SQL Server'}, + {value: 'MySQL', label: 'My SQL'}, + {value: 'PostgreSQL', label: 'Postgre SQL'}, + {value: 'H2', label: 'H2 database'} + ]; + + $scope.toggleExpanded = function () { + $scope.ui.expanded = !$scope.ui.expanded; + + $common.hidePopover(); + }; + + $scope.panels = {activePanels: [0]}; + + $scope.general = []; + $scope.advanced = []; + $scope.caches = []; + $scope.metadatas = []; + + $scope.preview = { + general: {xml: '', java: '', allDefaults: true}, + memory: {xml: '', java: '', allDefaults: true}, + query: {xml: '', java: '', allDefaults: true}, + store: {xml: '', java: '', allDefaults: true}, + concurrency: {xml: '', java: '', allDefaults: true}, + rebalance: {xml: '', java: '', allDefaults: true}, + serverNearCache: {xml: '', java: '', allDefaults: true}, + statistics: {xml: '', java: '', allDefaults: true} + }; + + $scope.required = function (field) { + var model = $common.isDefined(field.path) ? field.path + '.' + field.model : field.model; + + var backupItem = $scope.backupItem; + + var memoryMode = backupItem.memoryMode; + + var onHeapTired = memoryMode == 'ONHEAP_TIERED'; + var offHeapTired = memoryMode == 'OFFHEAP_TIERED'; + + var offHeapMaxMemory = backupItem.offHeapMaxMemory; + + if (model == 'offHeapMaxMemory' && offHeapTired) + return true; + + if (model == 'evictionPolicy.kind' && onHeapTired) + return backupItem.swapEnabled || ($common.isDefined(offHeapMaxMemory) && offHeapMaxMemory >= 0); + + return false; + }; + + $scope.tableSimpleValid = function (item, field, fx, index) { + var model; + + switch (field.model) { + case 'hibernateProperties': + if (fx.indexOf('=') < 0) + return showPopoverMessage(null, null, $table.tableFieldId(index, 'HibProp'), 'Property should be present in format key=value!'); + + model = item.cacheStoreFactory.CacheHibernateBlobStoreFactory[field.model]; + + var key = fx.split('=')[0]; + + var exist = false; + + if ($common.isDefined(model)) { + model.forEach(function (val) { + if (val.split('=')[0] == key) + exist = true; + }) + } + + if (exist) + return showPopoverMessage(null, null, $table.tableFieldId(index, 'HibProp'), 'Property with such name already exists!'); + + break; + + case 'sqlFunctionClasses': + if (!$common.isValidJavaClass('SQL function', fx, false, $table.tableFieldId(index, 'SqlFx'))) + return $table.tableFocusInvalidField(index, 'SqlFx'); + + model = item[field.model]; + + if ($common.isDefined(model)) { + var idx = _.indexOf(model, fx); + + // Found duplicate. + if (idx >= 0 && idx != index) + return showPopoverMessage(null, null, $table.tableFieldId(index, 'SqlFx'), 'SQL function with such class name already exists!'); + } + } + + return true; + }; + + $scope.tablePairValid = function (item, field, index) { + var pairValue = $table.tablePairValue(field, index); + + if (!$common.isValidJavaClass('Indexed type key', pairValue.key, true, $table.tableFieldId(index, 'KeyIndexedType'))) + return $table.tableFocusInvalidField(index, 'KeyIndexedType'); + + if (!$common.isValidJavaClass('Indexed type value', pairValue.value, true, $table.tableFieldId(index, 'ValueIndexedType'))) + return $table.tableFocusInvalidField(index, 'ValueIndexedType'); + + var model = item[field.model]; + + if ($common.isDefined(model)) { + var idx = _.findIndex(model, function (pair) { + return pair.keyClass == pairValue.key && pair.valueClass == pairValue.value; + }); + + // Found duplicate. + if (idx >= 0 && idx != index) + return showPopoverMessage(null, null, $table.tableFieldId(index, 'ValueIndexedType'), 'Indexed type with such key and value classes already exists!'); + } + + return true; + }; + + function selectFirstItem() { + if ($scope.caches.length > 0) + $scope.selectItem($scope.caches[0]); + } + + function cacheMetadatas(item) { + return _.reduce($scope.metadatas, function (memo, meta) { + if (item && _.contains(item.metadatas, meta.value)) { + memo.push(meta.meta); + } + + return memo; + }, []); + } + + $loading.start('loadingCachesScreen'); + + // When landing on the page, get caches and show them. + $http.post('caches/list') + .success(function (data) { + var validFilter = $filter('metadatasValidation'); + + $scope.spaces = data.spaces; + $scope.caches = data.caches; + $scope.clusters = data.clusters; + $scope.metadatas = _.sortBy(_.map(validFilter(data.metadatas, true, false), function (meta) { + return {value: meta._id, label: meta.valueType, kind: meta.kind, meta: meta} + }), 'label'); + + // Load page descriptor. + $http.get('/models/caches.json') + .success(function (data) { + $scope.screenTip = data.screenTip; + $scope.moreInfo = data.moreInfo; + $scope.general = data.general; + $scope.advanced = data.advanced; + + $scope.ui.addGroups(data.general, data.advanced); + + if ($common.getQueryVariable('new')) + $scope.createItem(); + else { + var lastSelectedCache = angular.fromJson(sessionStorage.lastSelectedCache); + + if (lastSelectedCache) { + var idx = _.findIndex($scope.caches, function (cache) { + return cache._id == lastSelectedCache; + }); + + if (idx >= 0) + $scope.selectItem($scope.caches[idx]); + else { + sessionStorage.removeItem('lastSelectedCache'); + + selectFirstItem(); + } + + } + else + selectFirstItem(); + } + + $scope.$watch('backupItem', function (val) { + if (val) { + var srcItem = $scope.selectedItem ? $scope.selectedItem : prepareNewItem(); + + $scope.ui.checkDirty(val, srcItem); + + var metas = cacheMetadatas(val); + var varName = $commonUtils.toJavaName('cache', val.name); + + $scope.preview.general.xml = $generatorXml.cacheMetadatas(metas, $generatorXml.cacheGeneral(val)).asString(); + $scope.preview.general.java = $generatorJava.cacheMetadatas(metas, varName, $generatorJava.cacheGeneral(val, varName)).asString(); + $scope.preview.general.allDefaults = $common.isEmptyString($scope.preview.general.xml); + + $scope.preview.memory.xml = $generatorXml.cacheMemory(val).asString(); + $scope.preview.memory.java = $generatorJava.cacheMemory(val, varName).asString(); + $scope.preview.memory.allDefaults = $common.isEmptyString($scope.preview.memory.xml); + + $scope.preview.query.xml = $generatorXml.cacheQuery(val).asString(); + $scope.preview.query.java = $generatorJava.cacheQuery(val, varName).asString(); + $scope.preview.query.allDefaults = $common.isEmptyString($scope.preview.query.xml); + + $scope.preview.store.xml = $generatorXml.cacheStore(val).asString(); + $scope.preview.store.java = $generatorJava.cacheStore(val, varName).asString(); + $scope.preview.store.allDefaults = $common.isEmptyString($scope.preview.store.xml); + + $scope.preview.concurrency.xml = $generatorXml.cacheConcurrency(val).asString(); + $scope.preview.concurrency.java = $generatorJava.cacheConcurrency(val, varName).asString(); + $scope.preview.concurrency.allDefaults = $common.isEmptyString($scope.preview.concurrency.xml); + + $scope.preview.rebalance.xml = $generatorXml.cacheRebalance(val).asString(); + $scope.preview.rebalance.java = $generatorJava.cacheRebalance(val, varName).asString(); + $scope.preview.rebalance.allDefaults = $common.isEmptyString($scope.preview.rebalance.xml); + + $scope.preview.serverNearCache.xml = $generatorXml.cacheServerNearCache(val).asString(); + $scope.preview.serverNearCache.java = $generatorJava.cacheServerNearCache(val, varName).asString(); + $scope.preview.serverNearCache.allDefaults = $common.isEmptyString($scope.preview.serverNearCache.xml); + + $scope.preview.statistics.xml = $generatorXml.cacheStatistics(val).asString(); + $scope.preview.statistics.java = $generatorJava.cacheStatistics(val, varName).asString(); + $scope.preview.statistics.allDefaults = $common.isEmptyString($scope.preview.statistics.xml); + } + }, true); + + $scope.$watch('backupItem.metadatas', function (val) { + var item = $scope.backupItem; + + var cacheStoreFactory = $common.isDefined(item) && + $common.isDefined(item.cacheStoreFactory) && + $common.isDefined(item.cacheStoreFactory.kind); + + if (val && !cacheStoreFactory) { + if (_.findIndex(cacheMetadatas(item), $common.metadataForStoreConfigured) >= 0) { + item.cacheStoreFactory.kind = 'CacheJdbcPojoStoreFactory'; + + if (!item.readThrough && !item.writeThrough) { + item.readThrough = true; + item.writeThrough = true; + } + + $timeout(function () { + $common.ensureActivePanel($scope.panels, 'store'); + }); + } + } + }, true); + }) + .error(function (errMsg) { + $common.showError(errMsg); + }); + }) + .error(function (errMsg) { + $common.showError(errMsg); + }) + .finally(function () { + $scope.ui.ready = true; + $loading.finish('loadingCachesScreen'); + }); + + $scope.selectItem = function (item, backup) { + function selectItem() { + $table.tableReset(); + + $scope.selectedItem = angular.copy(item); + + try { + if (item) + sessionStorage.lastSelectedCache = angular.toJson(item._id); + else + sessionStorage.removeItem('lastSelectedCache'); + } + catch (error) { } + + _.forEach(previews, function(preview) { + preview.attractAttention = false; + }); + + if (backup) + $scope.backupItem = backup; + else if (item) + $scope.backupItem = angular.copy(item); + else + $scope.backupItem = undefined; + } + + $common.confirmUnsavedChanges($scope.ui.isDirty(), selectItem); + + $scope.ui.formTitle = $common.isDefined($scope.backupItem) && $scope.backupItem._id ? + 'Selected cache: ' + $scope.backupItem.name : 'New cache'; + }; + + function prepareNewItem() { + return { + space: $scope.spaces[0]._id, + cacheMode: 'PARTITIONED', + atomicityMode: 'ATOMIC', + readFromBackup: true, + copyOnRead: true, + clusters: [], + metadatas: [] + } + } + + // Add new cache. + $scope.createItem = function () { + $table.tableReset(); + + $timeout(function () { + $common.ensureActivePanel($scope.panels, 'general', 'cacheName'); + }); + + $scope.selectItem(undefined, prepareNewItem()); + }; + + // Check cache logical consistency. + function validate(item) { + if ($common.isEmptyString(item.name)) + return showPopoverMessage($scope.panels, 'general', 'cacheName', 'Name should not be empty'); + + if (item.memoryMode == 'OFFHEAP_TIERED' && item.offHeapMaxMemory == null) + return showPopoverMessage($scope.panels, 'memory', 'offHeapMaxMemory', + 'Off-heap max memory should be specified'); + + if (item.memoryMode == 'ONHEAP_TIERED' && item.offHeapMaxMemory > 0 && + !$common.isDefined(item.evictionPolicy.kind)) { + return showPopoverMessage($scope.panels, 'memory', 'evictionPolicy', 'Eviction policy should not be configured'); + } + + var cacheStoreFactorySelected = item.cacheStoreFactory && item.cacheStoreFactory.kind; + + if (cacheStoreFactorySelected) { + if (item.cacheStoreFactory.kind == 'CacheJdbcPojoStoreFactory') { + if ($common.isEmptyString(item.cacheStoreFactory.CacheJdbcPojoStoreFactory.dataSourceBean)) + return showPopoverMessage($scope.panels, 'store', 'dataSourceBean', + 'Data source bean should not be empty'); + + if (!item.cacheStoreFactory.CacheJdbcPojoStoreFactory.dialect) + return showPopoverMessage($scope.panels, 'store', 'dialect', + 'Dialect should not be empty'); + } + + if (item.cacheStoreFactory.kind == 'CacheJdbcBlobStoreFactory') { + if ($common.isEmptyString(item.cacheStoreFactory.CacheJdbcBlobStoreFactory.user)) + return showPopoverMessage($scope.panels, 'store', 'user', + 'User should not be empty'); + + if ($common.isEmptyString(item.cacheStoreFactory.CacheJdbcBlobStoreFactory.dataSourceBean)) + return showPopoverMessage($scope.panels, 'store', 'dataSourceBean', + 'Data source bean should not be empty'); + } + } + + if ((item.readThrough || item.writeThrough) && !cacheStoreFactorySelected) + return showPopoverMessage($scope.panels, 'store', 'cacheStoreFactory', + (item.readThrough ? 'Read' : 'Write') + ' through are enabled but store is not configured!'); + + if (item.writeBehindEnabled && !cacheStoreFactorySelected) + return showPopoverMessage($scope.panels, 'store', 'cacheStoreFactory', + 'Write behind enabled but store is not configured!'); + + if (cacheStoreFactorySelected) { + if (!item.readThrough && !item.writeThrough) + return showPopoverMessage($scope.panels, 'store', 'readThrough', + 'Store is configured but read/write through are not enabled!'); + + if (item.cacheStoreFactory.kind == 'CacheJdbcPojoStoreFactory') { + if ($common.isDefined(item.metadatas)) { + var metadatas = cacheMetadatas($scope.backupItem); + + if (_.findIndex(metadatas, $common.metadataForStoreConfigured) < 0) + return showPopoverMessage($scope.panels, 'general', 'metadata', + 'Cache with configured JDBC POJO store factory should contain at least one metadata with store configuration'); + } + } + } + + return true; + } + + // Save cache into database. + function save(item) { + $http.post('caches/save', item) + .success(function (_id) { + $scope.ui.markPristine(); + + var idx = _.findIndex($scope.caches, function (cache) { + return cache._id == _id; + }); + + if (idx >= 0) + angular.extend($scope.caches[idx], item); + else { + item._id = _id; + $scope.caches.push(item); + } + + $scope.selectItem(item); + + $common.showInfo('Cache "' + item.name + '" saved.'); + }) + .error(function (errMsg) { + $common.showError(errMsg); + }); + } + + // Save cache. + $scope.saveItem = function () { + $table.tableReset(); + + var item = $scope.backupItem; + + if (validate(item)) + save(item); + }; + + // Save cache with new name. + $scope.cloneItem = function () { + $table.tableReset(); + + if (validate($scope.backupItem)) + $clone.confirm($scope.backupItem.name).then(function (newName) { + var item = angular.copy($scope.backupItem); + + item._id = undefined; + item.name = newName; + + save(item); + }); + }; + + // Remove cache from db. + $scope.removeItem = function () { + $table.tableReset(); + + var selectedItem = $scope.selectedItem; + + $confirm.confirm('Are you sure you want to remove cache: "' + selectedItem.name + '"?') + .then(function () { + var _id = selectedItem._id; + + $http.post('caches/remove', {_id: _id}) + .success(function () { + $common.showInfo('Cache has been removed: ' + selectedItem.name); + + var caches = $scope.caches; + + var idx = _.findIndex(caches, function (cache) { + return cache._id == _id; + }); + + if (idx >= 0) { + caches.splice(idx, 1); + + if (caches.length > 0) + $scope.selectItem(caches[0]); + else + $scope.selectItem(undefined, undefined); + } + }) + .error(function (errMsg) { + $common.showError(errMsg); + }); + }); + }; + + // Remove all caches from db. + $scope.removeAllItems = function () { + $table.tableReset(); + + $confirm.confirm('Are you sure you want to remove all caches?') + .then(function () { + $http.post('caches/remove/all') + .success(function () { + $common.showInfo('All caches have been removed'); + + $scope.caches = []; + + $scope.selectItem(undefined, undefined); + }) + .error(function (errMsg) { + $common.showError(errMsg); + }); + }); + }; + + $scope.resetItem = function (group) { + var resetTo = $scope.selectedItem; + + if (!$common.isDefined(resetTo)) + resetTo = prepareNewItem(); + + $common.resetItem($scope.backupItem, resetTo, $scope.general, group); + $common.resetItem($scope.backupItem, resetTo, $scope.advanced, group); + } + }] +);