ignite-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From a.@apache.org
Subject [11/28] ignite git commit: IGNITE-843 Implemented Web Console.
Date Wed, 18 May 2016 09:21:14 GMT
http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/app/services/AgentMonitor.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/app/services/AgentMonitor.service.js b/modules/web-console/src/main/js/app/services/AgentMonitor.service.js
new file mode 100644
index 0000000..88995d5
--- /dev/null
+++ b/modules/web-console/src/main/js/app/services/AgentMonitor.service.js
@@ -0,0 +1,337 @@
+/*
+ * 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.
+ */
+
+import io from 'socket.io-client'; // eslint-disable-line no-unused-vars
+
+class IgniteAgentMonitor {
+    constructor(socketFactory, $root, $q, $state, $modal, $common) {
+        this._scope = $root.$new();
+
+        $root.$on('$stateChangeStart', () => {
+            this.stopWatch();
+        });
+
+        // Pre-fetch modal dialogs.
+        this._downloadAgentModal = $modal({
+            scope: this._scope,
+            templateUrl: '/templates/agent-download.html',
+            show: false,
+            backdrop: 'static'
+        });
+
+        const _modalHide = this._downloadAgentModal.hide;
+
+        /**
+         * Special dialog hide function.
+         */
+        this._downloadAgentModal.hide = () => {
+            $common.hideAlert();
+
+            _modalHide();
+        };
+
+        /**
+         * Close dialog and go by specified link.
+         */
+        this._scope.back = () => {
+            this.stopWatch();
+
+            if (this._scope.backState)
+                this._scope.$$postDigest(() => $state.go(this._scope.backState));
+        };
+
+        this._scope.downloadAgent = () => {
+            const lnk = document.createElement('a');
+
+            lnk.setAttribute('href', '/api/v1/agent/download/zip');
+            lnk.setAttribute('target', '_self');
+            lnk.setAttribute('download', null);
+            lnk.style.display = 'none';
+
+            document.body.appendChild(lnk);
+
+            lnk.click();
+
+            document.body.removeChild(lnk);
+        };
+
+        this._scope.hasAgents = null;
+        this._scope.showModal = false;
+
+        this._evtOrderKey = $common.randomString(20);
+        this._evtThrottleCntrKey = $common.randomString(20);
+
+        /**
+         * @type {Socket}
+         */
+        this._socket = null;
+
+        this._socketFactory = socketFactory;
+
+        this._$q = $q;
+
+        this._$common = $common;
+    }
+
+    /**
+     * @private
+     */
+    checkModal() {
+        if (this._scope.showModal && !this._scope.hasAgents)
+            this._downloadAgentModal.$promise.then(this._downloadAgentModal.show);
+        else if ((this._scope.hasAgents || !this._scope.showModal) && this._downloadAgentModal.$isShown)
+            this._downloadAgentModal.hide();
+    }
+
+    /**
+     * @returns {Promise}
+     */
+    awaitAgent() {
+        if (this._scope.hasAgents)
+            return this._$q.when();
+
+        if (this._scope.hasAgents !== null)
+            this.checkModal();
+
+        const latch = this._$q.defer();
+
+        const offConnected = this._scope.$on('agent:connected', (event, success) => {
+            offConnected();
+
+            if (success)
+                return latch.resolve();
+
+            latch.reject();
+        });
+
+        return latch.promise;
+    }
+
+    init() {
+        this._socket = this._socketFactory();
+
+        this._socket.on('connect_error', () => {
+            this._scope.hasAgents = false;
+        });
+
+        this._socket.on('agent:count', ({count}) => {
+            this._scope.hasAgents = count > 0;
+
+            this.checkModal();
+
+            if (this._scope.hasAgents)
+                this._scope.$broadcast('agent:connected', true);
+        });
+
+        this._socket.on('disconnect', () => {
+            this._scope.hasAgents = false;
+
+            this.checkModal();
+        });
+    }
+
+    /**
+     * @param {Object} back
+     * @returns {Promise}
+     */
+    startWatch(back) {
+        this._scope.backState = back.state;
+        this._scope.backText = back.text;
+
+        this._scope.agentGoal = back.goal;
+
+        this._scope.showModal = true;
+
+        return this.awaitAgent();
+    }
+
+    /**
+     *
+     * @param {String} event
+     * @param {Object} [args]
+     * @returns {Promise}
+     * @private
+     */
+    _emit(event, ...args) {
+        if (!this._socket)
+            return this._$q.reject('Failed to connect to agent');
+
+        const latch = this._$q.defer();
+
+        const onDisconnect = () => {
+            this._socket.removeListener('disconnect', onDisconnect);
+
+            latch.reject('Connection to server was closed');
+        };
+
+        this._socket.on('disconnect', onDisconnect);
+
+        args.push((err, res) => {
+            this._socket.removeListener('disconnect', onDisconnect);
+
+            if (err)
+                latch.reject(err);
+
+            latch.resolve(res);
+        });
+
+        this._socket.emit(event, ...args);
+
+        return latch.promise;
+    }
+
+    drivers() {
+        return this._emit('schemaImport:drivers');
+    }
+
+    /**
+     *
+     * @param {Object} preset
+     * @returns {Promise}
+     */
+    schemas(preset) {
+        return this._emit('schemaImport:schemas', preset);
+    }
+
+    /**
+     *
+     * @param {Object} preset
+     * @returns {Promise}
+     */
+    tables(preset) {
+        return this._emit('schemaImport:tables', preset);
+    }
+
+    /**
+     * @param {String} errMsg
+     */
+    showNodeError(errMsg) {
+        this._downloadAgentModal.show();
+
+        this._$common.showError(errMsg);
+    }
+
+    /**
+     *
+     * @param {String} event
+     * @param {Object} [args]
+     * @returns {Promise}
+     * @private
+     */
+    _rest(event, ...args) {
+        return this._downloadAgentModal.$promise
+            .then(() => this._emit(event, ...args));
+    }
+
+    /**
+     * @param {Boolean} [attr]
+     * @param {Boolean} [mtr]
+     * @returns {Promise}
+     */
+    topology(attr, mtr) {
+        return this._rest('node:topology', !!attr, !!mtr);
+    }
+
+    /**
+     * @param {int} [queryId]
+     * @returns {Promise}
+     */
+    queryClose(queryId) {
+        return this._rest('node:query:close', queryId);
+    }
+
+    /**
+     * @param {String} cacheName Cache name.
+     * @param {int} pageSize
+     * @param {String} [query] Query if null then scan query.
+     * @returns {Promise}
+     */
+    query(cacheName, pageSize, query) {
+        return this._rest('node:query', _.isEmpty(cacheName) ? null : cacheName, pageSize, query);
+    }
+
+    /**
+     * @param {String} cacheName Cache name.
+     * @param {String} [query] Query if null then scan query.
+     * @returns {Promise}
+     */
+    queryGetAll(cacheName, query) {
+        return this._rest('node:query:getAll', _.isEmpty(cacheName) ? null : cacheName, query);
+    }
+
+    /**
+     * @param {String} [cacheName] Cache name.
+     * @returns {Promise}
+     */
+    metadata(cacheName) {
+        return this._rest('node:cache:metadata', _.isEmpty(cacheName) ? null : cacheName);
+    }
+
+    /**
+     * @param {int} queryId
+     * @param {int} pageSize
+     * @returns {Promise}
+     */
+    next(queryId, pageSize) {
+        return this._rest('node:query:fetch', queryId, pageSize);
+    }
+
+    collect() {
+        return this._rest('node:visor:collect', this._evtOrderKey, this._evtThrottleCntrKey);
+    }
+
+    /**
+     * Clear specified cache on specified node.
+     * @param {String} nid Node id.
+     * @param {String} cacheName Cache name.
+     * @returns {Promise}
+     */
+    cacheClear(nid, cacheName) {
+        return this._rest('node:cache:clear', nid, cacheName);
+    }
+
+    /**
+     * Stop specified cache on specified node.
+     * @param {String} nid Node id.
+     * @param {String} cacheName Cache name.
+     * @returns {Promise}
+     */
+    cacheStop(nid, cacheName) {
+        return this._rest('node:cache:stop', nid, cacheName);
+    }
+
+    /**
+     * Ping node.
+     * @param {String} nid Node id.
+     * @returns {Promise}
+     */
+    ping(nid) {
+        return this._rest('node:ping', nid);
+    }
+
+    stopWatch() {
+        this._scope.showModal = false;
+
+        this.checkModal();
+
+        this._scope.$broadcast('agent:connected', false);
+    }
+}
+
+IgniteAgentMonitor.$inject = ['igniteSocketFactory', '$rootScope', '$q', '$state', '$modal', '$common'];
+
+export default ['IgniteAgentMonitor', IgniteAgentMonitor];

http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/app/services/ChartColors.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/app/services/ChartColors.service.js b/modules/web-console/src/main/js/app/services/ChartColors.service.js
new file mode 100644
index 0000000..ec3f365
--- /dev/null
+++ b/modules/web-console/src/main/js/app/services/ChartColors.service.js
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+import COLORS from 'app/data/colors.json!';
+
+export default ['IgniteChartColors', function() {
+    return COLORS;
+}];

http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/app/services/Countries.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/app/services/Countries.service.js b/modules/web-console/src/main/js/app/services/Countries.service.js
new file mode 100644
index 0000000..87d11fd
--- /dev/null
+++ b/modules/web-console/src/main/js/app/services/Countries.service.js
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+import COUNTRIES from 'app/data/countries.json!';
+
+export default ['IgniteCountries', function() {
+    const indexByName = _.keyBy(COUNTRIES, 'name');
+    const UNDEFINED_COUNTRY = {name: '', code: ''};
+
+    const getByName = (name) => (indexByName[name] || UNDEFINED_COUNTRY);
+    const getAll = () => (COUNTRIES);
+
+    return {
+        getByName,
+        getAll
+    };
+}];

http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/app/services/InetAddress.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/app/services/InetAddress.service.js b/modules/web-console/src/main/js/app/services/InetAddress.service.js
new file mode 100644
index 0000000..abdd8a3
--- /dev/null
+++ b/modules/web-console/src/main/js/app/services/InetAddress.service.js
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+
+export default ['IgniteInetAddress', function() {
+    return {
+        /**
+         * @param {String} ip IP address to check.
+         * @returns {boolean} 'true' if given ip address is valid.
+         */
+        validIp(ip) {
+            const regexp = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/;
+
+            return regexp.test(ip);
+        },
+        /**
+         * @param {String} hostNameOrIp host name or ip address to check.
+         * @returns {boolean} 'true' if given is host name or ip.
+         */
+        validHost(hostNameOrIp) {
+            const regexp = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/;
+
+            return regexp.test(hostNameOrIp) || this.validIp(hostNameOrIp);
+        },
+        /**
+         * @param {int} port Port value to check.
+         * @returns boolean 'true' if given port is valid tcp/udp port range.
+         */
+        validPort(port) {
+            return _.isInteger(port) && port > 0 && port <= 65535;
+        },
+        /**
+         * @param {int} port Port value to check.
+         * @returns {boolean} 'true' if given port in non system port range(user+dynamic).
+         */
+        validNonSystemPort(port) {
+            return _.isInteger(port) && port >= 1024 && port <= 65535;
+        }
+    };
+}];

http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/app/services/JavaTypes.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/app/services/JavaTypes.service.js b/modules/web-console/src/main/js/app/services/JavaTypes.service.js
new file mode 100644
index 0000000..a755e13
--- /dev/null
+++ b/modules/web-console/src/main/js/app/services/JavaTypes.service.js
@@ -0,0 +1,84 @@
+/*
+ * 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.
+ */
+
+// Java built-in class names.
+import JAVA_CLASSES from 'app/data/java-classes.json!';
+
+// Java build-in primitive.
+import JAVA_PRIMITIVES from 'app/data/java-primitives.json!';
+
+import JAVA_KEYWORDS from 'app/data/java-keywords.json!';
+
+export default ['JavaTypes', function() {
+    return {
+        /**
+         * @param {String} clsName Class name to check.
+         * @returns boolean 'true' if given class name non a Java built-in type.
+         */
+        nonBuiltInClass(clsName) {
+            return _.isNil(_.find(JAVA_CLASSES, (clazz) => clsName === clazz.short || clsName === clazz.full));
+        },
+        /**
+         * @param clsName Class name to check.
+         * @returns Full class name for java build-in types or source class otherwise.
+         */
+        fullClassName(clsName) {
+            const type = _.find(JAVA_CLASSES, (clazz) => clsName === clazz.short);
+
+            return type ? type.full : clsName;
+        },
+        /**
+         * @param {String} value text to check.
+         * @returns boolean 'true' if given text is valid Java identifier.
+         */
+        validIdentifier(value) {
+            const regexp = /^(([a-zA-Z_$][a-zA-Z0-9_$]*)\.)*([a-zA-Z_$][a-zA-Z0-9_$]*)$/igm;
+
+            return value === '' || regexp.test(value);
+        },
+        /**
+         * @param {String} value text to check.
+         * @returns boolean 'true' if given text is valid Java package.
+         */
+        validPackage(value) {
+            const regexp = /^(([a-zA-Z_$][a-zA-Z0-9_$]*)\.)*([a-zA-Z_$][a-zA-Z0-9_$]*(\.?\*)?)$/igm;
+
+            return value === '' || regexp.test(value);
+        },
+        /**
+         * @param {String} value text to check.
+         * @returns boolean 'true' if given text is a Java type with package.
+         */
+        packageSpecified(value) {
+            return value.split('.').length >= 2;
+        },
+        /**
+         * @param {String} value text to check.
+         * @returns boolean 'true' if given text non Java keyword.
+         */
+        isKeywords(value) {
+            return _.includes(JAVA_KEYWORDS, value);
+        },
+        /**
+         * @param {String} clsName Class name to check.
+         * @returns {boolean} 'true' if givent class name is java primitive.
+         */
+        isJavaPrimitive(clsName) {
+            return _.includes(JAVA_PRIMITIVES, clsName);
+        }
+    };
+}];

http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/app/services/cleanup.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/app/services/cleanup.service.js b/modules/web-console/src/main/js/app/services/cleanup.service.js
new file mode 100644
index 0000000..380beda
--- /dev/null
+++ b/modules/web-console/src/main/js/app/services/cleanup.service.js
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+
+export default ['$cleanup', () => {
+    const cleanup = (original, dist) => {
+        if (_.isUndefined(original))
+            return dist;
+
+        if (_.isObject(original)) {
+            _.forOwn(original, (value, key) => {
+                if (/\$\$hashKey/.test(key))
+                    return;
+
+                const attr = cleanup(value);
+
+                if (!_.isNil(attr)) {
+                    dist = dist || {};
+                    dist[key] = attr;
+                }
+            });
+        } else if ((_.isString(original) && original.length) || _.isNumber(original) || _.isBoolean(original))
+            dist = original;
+        else if (_.isArray(original) && original.length)
+            dist = _.map(original, (value) => cleanup(value, {}));
+
+        return dist;
+    };
+
+    return cleanup;
+}];

http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/app/services/confirm.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/app/services/confirm.service.js b/modules/web-console/src/main/js/app/services/confirm.service.js
new file mode 100644
index 0000000..bb07cfd
--- /dev/null
+++ b/modules/web-console/src/main/js/app/services/confirm.service.js
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+
+// Confirm popup service.
+export default ['$confirm', ['$modal', '$rootScope', '$q', '$animate', ($modal, $root, $q, $animate) => {
+    const scope = $root.$new();
+
+    const modal = $modal({templateUrl: '/templates/confirm.html', scope, placement: 'center', show: false});
+
+    let deferred;
+
+    const _hide = (animate) => {
+        $animate.enabled(modal.$element, animate);
+
+        modal.hide();
+    };
+
+    scope.confirmYes = () => {
+        _hide(scope.animate);
+
+        deferred.resolve(true);
+    };
+
+    scope.confirmNo = () => {
+        _hide(scope.animate);
+
+        deferred.resolve(false);
+    };
+
+    scope.confirmCancel = () => {
+        _hide(true);
+
+        deferred.reject('cancelled');
+    };
+
+    /**
+     *
+     * @param {String } content
+     * @param {Boolean} [yesNo]
+     * @param {Boolean} [animate]
+     * @returns {Promise}
+     */
+    modal.confirm = (content, yesNo, animate) => {
+        scope.animate = !!animate;
+        scope.content = content || 'Confirm?';
+        scope.yesNo = !!yesNo;
+
+        deferred = $q.defer();
+
+        modal.$promise.then(modal.show);
+
+        return deferred.promise;
+    };
+
+    return modal;
+}]];

http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/build/system.config.js
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/build/system.config.js b/modules/web-console/src/main/js/build/system.config.js
new file mode 100644
index 0000000..edd9e9e
--- /dev/null
+++ b/modules/web-console/src/main/js/build/system.config.js
@@ -0,0 +1,415 @@
+System.config({
+  defaultJSExtensions: true,
+  transpiler: "babel",
+  babelOptions: {
+    "optional": [
+      "runtime",
+      "optimisation.modules.system"
+    ]
+  },
+  paths: {
+    "github:*": "build/jspm_packages/github/*",
+    "npm:*": "build/jspm_packages/npm/*"
+  },
+  separateCSS: true,
+
+  map: {
+    "ace": "github:ajaxorg/ace-builds@1.2.3",
+    "angular": "github:angular/bower-angular@1.5.5",
+    "angular-animate": "github:angular/bower-angular-animate@1.5.5",
+    "angular-drag-and-drop-lists": "github:marceljuenemann/angular-drag-and-drop-lists@1.4.0",
+    "angular-gridster": "github:ManifestWebDesign/angular-gridster@0.13.5",
+    "angular-motion": "github:mgcrea/angular-motion@0.4.4",
+    "angular-nvd3": "github:krispo/angular-nvd3@1.0.6",
+    "angular-retina": "github:jrief/angular-retina@0.3.8",
+    "angular-sanitize": "github:angular/bower-angular-sanitize@1.5.5",
+    "angular-smart-table": "github:lorenzofox3/Smart-Table@2.1.8",
+    "angular-socket-io": "github:btford/angular-socket-io@0.7.0",
+    "angular-strap": "github:mgcrea/angular-strap@2.3.8",
+    "angular-tree-control": "github:wix/angular-tree-control@0.2.25",
+    "angular-ui-grid": "github:angular-ui/bower-ui-grid@3.1.1",
+    "angular-ui-router": "github:angular-ui/ui-router@0.2.18",
+    "angular-ui-router-metatags": "github:tinusn/ui-router-metatags@1.0.3",
+    "babel": "npm:babel-core@5.8.38",
+    "babel-runtime": "npm:babel-runtime@5.8.38",
+    "blob": "github:eligrey/Blob.js@master",
+    "bootstrap-carousel": "github:twbs/bootstrap@3.3.6",
+    "clean-css": "npm:clean-css@3.4.12",
+    "core-js": "npm:core-js@1.2.6",
+    "css": "github:systemjs/plugin-css@0.1.21",
+    "file-saver": "github:eligrey/FileSaver.js@master",
+    "font-awesome": "npm:font-awesome@4.5.0",
+    "jade": "github:johnsoftek/plugin-jade@0.6.0",
+    "jquery": "github:components/jquery@2.2.1",
+    "json": "github:systemjs/plugin-json@0.1.0",
+    "jszip": "github:Stuk/jszip@2.6.0",
+    "lodash": "github:lodash/lodash@4.11.1",
+    "pdfmake": "github:bpampuch/pdfmake@0.1.20",
+    "query-command-supported": "github:zenorocha/document.queryCommandSupported@1.0.0",
+    "socket.io-client": "github:socketio/socket.io-client@1.4.5",
+    "text": "github:systemjs/plugin-text@0.0.7",
+    "github:angular-ui/bower-ui-grid@3.1.1": {
+      "pdfmake": "github:bpampuch/pdfmake@0.1.20"
+    },
+    "github:angular-ui/ui-router@0.2.18": {
+      "angular": "github:angular/bower-angular@1.5.5"
+    },
+    "github:angular/bower-angular-animate@1.5.5": {
+      "angular": "github:angular/bower-angular@1.5.5"
+    },
+    "github:angular/bower-angular-sanitize@1.5.5": {
+      "angular": "github:angular/bower-angular@1.5.5"
+    },
+    "github:angular/bower-angular@1.5.5": {
+      "jquery": "github:components/jquery@2.2.1"
+    },
+    "github:btford/angular-socket-io@0.7.0": {
+      "socket.io-client": "github:socketio/socket.io-client@1.4.5"
+    },
+    "github:eligrey/FileSaver.js@master": {
+      "blob": "github:eligrey/Blob.js@master"
+    },
+    "github:johnsoftek/plugin-jade@0.6.0": {
+      "jade-compiler": "npm:jade@1.11.0",
+      "text": "github:systemjs/plugin-text@0.0.4"
+    },
+    "github:jspm/nodelibs-assert@0.1.0": {
+      "assert": "npm:assert@1.3.0"
+    },
+    "github:jspm/nodelibs-buffer@0.1.0": {
+      "buffer": "npm:buffer@3.6.0"
+    },
+    "github:jspm/nodelibs-events@0.1.1": {
+      "events": "npm:events@1.0.2"
+    },
+    "github:jspm/nodelibs-http@1.7.1": {
+      "Base64": "npm:Base64@0.2.1",
+      "events": "github:jspm/nodelibs-events@0.1.1",
+      "inherits": "npm:inherits@2.0.1",
+      "stream": "github:jspm/nodelibs-stream@0.1.0",
+      "url": "github:jspm/nodelibs-url@0.1.0",
+      "util": "github:jspm/nodelibs-util@0.1.0"
+    },
+    "github:jspm/nodelibs-https@0.1.0": {
+      "https-browserify": "npm:https-browserify@0.0.0"
+    },
+    "github:jspm/nodelibs-os@0.1.0": {
+      "os-browserify": "npm:os-browserify@0.1.2"
+    },
+    "github:jspm/nodelibs-path@0.1.0": {
+      "path-browserify": "npm:path-browserify@0.0.0"
+    },
+    "github:jspm/nodelibs-process@0.1.2": {
+      "process": "npm:process@0.11.2"
+    },
+    "github:jspm/nodelibs-stream@0.1.0": {
+      "stream-browserify": "npm:stream-browserify@1.0.0"
+    },
+    "github:jspm/nodelibs-tty@0.1.0": {
+      "tty-browserify": "npm:tty-browserify@0.0.0"
+    },
+    "github:jspm/nodelibs-url@0.1.0": {
+      "url": "npm:url@0.10.3"
+    },
+    "github:jspm/nodelibs-util@0.1.0": {
+      "util": "npm:util@0.10.3"
+    },
+    "github:jspm/nodelibs-vm@0.1.0": {
+      "vm-browserify": "npm:vm-browserify@0.0.4"
+    },
+    "github:krispo/angular-nvd3@1.0.6": {
+      "d3": "npm:d3@3.5.14",
+      "nvd3": "npm:nvd3@1.8.1"
+    },
+    "github:mgcrea/angular-motion@0.4.4": {
+      "angular": "github:angular/bower-angular@1.5.5",
+      "css": "github:systemjs/plugin-css@0.1.21"
+    },
+    "github:mgcrea/angular-strap@2.3.8": {
+      "angular": "github:angular/bower-angular@1.5.5",
+      "angular-animate": "github:angular/bower-angular-animate@1.5.5",
+      "angular-motion": "github:mgcrea/angular-motion@0.4.4",
+      "angular-sanitize": "github:angular/bower-angular-sanitize@1.5.5"
+    },
+    "github:twbs/bootstrap@3.3.6": {
+      "jquery": "npm:jquery@2.2.3"
+    },
+    "npm:acorn-globals@1.0.9": {
+      "acorn": "npm:acorn@2.7.0"
+    },
+    "npm:acorn@1.2.2": {
+      "fs": "github:jspm/nodelibs-fs@0.1.2",
+      "path": "github:jspm/nodelibs-path@0.1.0",
+      "process": "github:jspm/nodelibs-process@0.1.2",
+      "stream": "github:jspm/nodelibs-stream@0.1.0"
+    },
+    "npm:acorn@2.7.0": {
+      "fs": "github:jspm/nodelibs-fs@0.1.2",
+      "path": "github:jspm/nodelibs-path@0.1.0",
+      "process": "github:jspm/nodelibs-process@0.1.2",
+      "stream": "github:jspm/nodelibs-stream@0.1.0"
+    },
+    "npm:align-text@0.1.4": {
+      "kind-of": "npm:kind-of@3.0.2",
+      "longest": "npm:longest@1.0.1",
+      "repeat-string": "npm:repeat-string@1.5.4"
+    },
+    "npm:amdefine@1.0.0": {
+      "fs": "github:jspm/nodelibs-fs@0.1.2",
+      "module": "github:jspm/nodelibs-module@0.1.0",
+      "path": "github:jspm/nodelibs-path@0.1.0",
+      "process": "github:jspm/nodelibs-process@0.1.2"
+    },
+    "npm:asap@1.0.0": {
+      "process": "github:jspm/nodelibs-process@0.1.2"
+    },
+    "npm:assert@1.3.0": {
+      "util": "npm:util@0.10.3"
+    },
+    "npm:async@0.2.10": {
+      "process": "github:jspm/nodelibs-process@0.1.2"
+    },
+    "npm:babel-runtime@5.8.38": {
+      "process": "github:jspm/nodelibs-process@0.1.2"
+    },
+    "npm:buffer@3.6.0": {
+      "base64-js": "npm:base64-js@0.0.8",
+      "child_process": "github:jspm/nodelibs-child_process@0.1.0",
+      "fs": "github:jspm/nodelibs-fs@0.1.2",
+      "ieee754": "npm:ieee754@1.1.6",
+      "isarray": "npm:isarray@1.0.0",
+      "process": "github:jspm/nodelibs-process@0.1.2"
+    },
+    "npm:center-align@0.1.3": {
+      "align-text": "npm:align-text@0.1.4",
+      "lazy-cache": "npm:lazy-cache@1.0.3"
+    },
+    "npm:clean-css@3.4.12": {
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+      "commander": "npm:commander@2.8.1",
+      "fs": "github:jspm/nodelibs-fs@0.1.2",
+      "http": "github:jspm/nodelibs-http@1.7.1",
+      "https": "github:jspm/nodelibs-https@0.1.0",
+      "os": "github:jspm/nodelibs-os@0.1.0",
+      "path": "github:jspm/nodelibs-path@0.1.0",
+      "process": "github:jspm/nodelibs-process@0.1.2",
+      "source-map": "npm:source-map@0.4.4",
+      "url": "github:jspm/nodelibs-url@0.1.0",
+      "util": "github:jspm/nodelibs-util@0.1.0"
+    },
+    "npm:cliui@2.1.0": {
+      "center-align": "npm:center-align@0.1.3",
+      "right-align": "npm:right-align@0.1.3",
+      "wordwrap": "npm:wordwrap@0.0.2"
+    },
+    "npm:commander@2.6.0": {
+      "child_process": "github:jspm/nodelibs-child_process@0.1.0",
+      "events": "github:jspm/nodelibs-events@0.1.1",
+      "path": "github:jspm/nodelibs-path@0.1.0",
+      "process": "github:jspm/nodelibs-process@0.1.2"
+    },
+    "npm:commander@2.8.1": {
+      "child_process": "github:jspm/nodelibs-child_process@0.1.0",
+      "events": "github:jspm/nodelibs-events@0.1.1",
+      "fs": "github:jspm/nodelibs-fs@0.1.2",
+      "graceful-readlink": "npm:graceful-readlink@1.0.1",
+      "path": "github:jspm/nodelibs-path@0.1.0",
+      "process": "github:jspm/nodelibs-process@0.1.2"
+    },
+    "npm:constantinople@3.0.2": {
+      "acorn": "npm:acorn@2.7.0"
+    },
+    "npm:core-js@1.2.6": {
+      "fs": "github:jspm/nodelibs-fs@0.1.2",
+      "path": "github:jspm/nodelibs-path@0.1.0",
+      "process": "github:jspm/nodelibs-process@0.1.2",
+      "systemjs-json": "github:systemjs/plugin-json@0.1.0"
+    },
+    "npm:core-util-is@1.0.2": {
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0"
+    },
+    "npm:css@1.0.8": {
+      "assert": "github:jspm/nodelibs-assert@0.1.0",
+      "css-parse": "npm:css-parse@1.0.4",
+      "css-stringify": "npm:css-stringify@1.0.5",
+      "fs": "github:jspm/nodelibs-fs@0.1.2",
+      "process": "github:jspm/nodelibs-process@0.1.2"
+    },
+    "npm:font-awesome@4.5.0": {
+      "css": "github:systemjs/plugin-css@0.1.21"
+    },
+    "npm:graceful-readlink@1.0.1": {
+      "fs": "github:jspm/nodelibs-fs@0.1.2"
+    },
+    "npm:https-browserify@0.0.0": {
+      "http": "github:jspm/nodelibs-http@1.7.1"
+    },
+    "npm:inherits@2.0.1": {
+      "util": "github:jspm/nodelibs-util@0.1.0"
+    },
+    "npm:is-buffer@1.1.3": {
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0"
+    },
+    "npm:jade@1.11.0": {
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+      "character-parser": "npm:character-parser@1.2.1",
+      "clean-css": "npm:clean-css@3.4.12",
+      "commander": "npm:commander@2.6.0",
+      "constantinople": "npm:constantinople@3.0.2",
+      "jstransformer": "npm:jstransformer@0.0.2",
+      "mkdirp": "npm:mkdirp@0.5.1",
+      "path": "github:jspm/nodelibs-path@0.1.0",
+      "process": "github:jspm/nodelibs-process@0.1.2",
+      "systemjs-json": "github:systemjs/plugin-json@0.1.0",
+      "transformers": "npm:transformers@2.1.0",
+      "uglify-js": "npm:uglify-js@2.6.2",
+      "void-elements": "npm:void-elements@2.0.1",
+      "with": "npm:with@4.0.3"
+    },
+    "npm:jstransformer@0.0.2": {
+      "assert": "github:jspm/nodelibs-assert@0.1.0",
+      "fs": "github:jspm/nodelibs-fs@0.1.2",
+      "is-promise": "npm:is-promise@2.1.0",
+      "promise": "npm:promise@6.1.0"
+    },
+    "npm:kind-of@3.0.2": {
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+      "is-buffer": "npm:is-buffer@1.1.3"
+    },
+    "npm:lazy-cache@1.0.3": {
+      "process": "github:jspm/nodelibs-process@0.1.2"
+    },
+    "npm:mkdirp@0.5.1": {
+      "fs": "github:jspm/nodelibs-fs@0.1.2",
+      "minimist": "npm:minimist@0.0.8",
+      "path": "github:jspm/nodelibs-path@0.1.0",
+      "process": "github:jspm/nodelibs-process@0.1.2"
+    },
+    "npm:nvd3@1.8.1": {
+      "d3": "npm:d3@3.5.14"
+    },
+    "npm:optimist@0.3.7": {
+      "path": "github:jspm/nodelibs-path@0.1.0",
+      "process": "github:jspm/nodelibs-process@0.1.2",
+      "wordwrap": "npm:wordwrap@0.0.2"
+    },
+    "npm:os-browserify@0.1.2": {
+      "os": "github:jspm/nodelibs-os@0.1.0"
+    },
+    "npm:path-browserify@0.0.0": {
+      "process": "github:jspm/nodelibs-process@0.1.2"
+    },
+    "npm:process@0.11.2": {
+      "assert": "github:jspm/nodelibs-assert@0.1.0"
+    },
+    "npm:promise@2.0.0": {
+      "is-promise": "npm:is-promise@1.0.1",
+      "process": "github:jspm/nodelibs-process@0.1.2"
+    },
+    "npm:promise@6.1.0": {
+      "asap": "npm:asap@1.0.0"
+    },
+    "npm:punycode@1.3.2": {
+      "process": "github:jspm/nodelibs-process@0.1.2"
+    },
+    "npm:readable-stream@1.1.14": {
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+      "core-util-is": "npm:core-util-is@1.0.2",
+      "events": "github:jspm/nodelibs-events@0.1.1",
+      "inherits": "npm:inherits@2.0.1",
+      "isarray": "npm:isarray@0.0.1",
+      "process": "github:jspm/nodelibs-process@0.1.2",
+      "stream-browserify": "npm:stream-browserify@1.0.0",
+      "string_decoder": "npm:string_decoder@0.10.31"
+    },
+    "npm:right-align@0.1.3": {
+      "align-text": "npm:align-text@0.1.4"
+    },
+    "npm:source-map@0.1.43": {
+      "amdefine": "npm:amdefine@1.0.0",
+      "fs": "github:jspm/nodelibs-fs@0.1.2",
+      "path": "github:jspm/nodelibs-path@0.1.0",
+      "process": "github:jspm/nodelibs-process@0.1.2"
+    },
+    "npm:source-map@0.4.4": {
+      "amdefine": "npm:amdefine@1.0.0",
+      "process": "github:jspm/nodelibs-process@0.1.2"
+    },
+    "npm:source-map@0.5.3": {
+      "process": "github:jspm/nodelibs-process@0.1.2"
+    },
+    "npm:stream-browserify@1.0.0": {
+      "events": "github:jspm/nodelibs-events@0.1.1",
+      "inherits": "npm:inherits@2.0.1",
+      "readable-stream": "npm:readable-stream@1.1.14"
+    },
+    "npm:string_decoder@0.10.31": {
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0"
+    },
+    "npm:transformers@2.1.0": {
+      "css": "npm:css@1.0.8",
+      "fs": "github:jspm/nodelibs-fs@0.1.2",
+      "path": "github:jspm/nodelibs-path@0.1.0",
+      "process": "github:jspm/nodelibs-process@0.1.2",
+      "promise": "npm:promise@2.0.0",
+      "uglify-js": "npm:uglify-js@2.2.5",
+      "vm": "github:jspm/nodelibs-vm@0.1.0"
+    },
+    "npm:uglify-js@2.2.5": {
+      "fs": "github:jspm/nodelibs-fs@0.1.2",
+      "optimist": "npm:optimist@0.3.7",
+      "path": "github:jspm/nodelibs-path@0.1.0",
+      "process": "github:jspm/nodelibs-process@0.1.2",
+      "source-map": "npm:source-map@0.1.43",
+      "util": "github:jspm/nodelibs-util@0.1.0",
+      "vm": "github:jspm/nodelibs-vm@0.1.0"
+    },
+    "npm:uglify-js@2.6.2": {
+      "async": "npm:async@0.2.10",
+      "fs": "github:jspm/nodelibs-fs@0.1.2",
+      "path": "github:jspm/nodelibs-path@0.1.0",
+      "process": "github:jspm/nodelibs-process@0.1.2",
+      "source-map": "npm:source-map@0.5.3",
+      "uglify-to-browserify": "npm:uglify-to-browserify@1.0.2",
+      "yargs": "npm:yargs@3.10.0"
+    },
+    "npm:uglify-to-browserify@1.0.2": {
+      "fs": "github:jspm/nodelibs-fs@0.1.2",
+      "stream": "github:jspm/nodelibs-stream@0.1.0"
+    },
+    "npm:url@0.10.3": {
+      "assert": "github:jspm/nodelibs-assert@0.1.0",
+      "punycode": "npm:punycode@1.3.2",
+      "querystring": "npm:querystring@0.2.0",
+      "util": "github:jspm/nodelibs-util@0.1.0"
+    },
+    "npm:util@0.10.3": {
+      "inherits": "npm:inherits@2.0.1",
+      "process": "github:jspm/nodelibs-process@0.1.2"
+    },
+    "npm:vm-browserify@0.0.4": {
+      "indexof": "npm:indexof@0.0.1"
+    },
+    "npm:void-elements@2.0.1": {
+      "http": "github:jspm/nodelibs-http@1.7.1"
+    },
+    "npm:window-size@0.1.0": {
+      "process": "github:jspm/nodelibs-process@0.1.2",
+      "tty": "github:jspm/nodelibs-tty@0.1.0"
+    },
+    "npm:with@4.0.3": {
+      "acorn": "npm:acorn@1.2.2",
+      "acorn-globals": "npm:acorn-globals@1.0.9"
+    },
+    "npm:yargs@3.10.0": {
+      "assert": "github:jspm/nodelibs-assert@0.1.0",
+      "camelcase": "npm:camelcase@1.2.1",
+      "cliui": "npm:cliui@2.1.0",
+      "decamelize": "npm:decamelize@1.2.0",
+      "fs": "github:jspm/nodelibs-fs@0.1.2",
+      "path": "github:jspm/nodelibs-path@0.1.0",
+      "process": "github:jspm/nodelibs-process@0.1.2",
+      "window-size": "npm:window-size@0.1.0"
+    }
+  }
+});

http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/controllers/admin-controller.js
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/controllers/admin-controller.js b/modules/web-console/src/main/js/controllers/admin-controller.js
new file mode 100644
index 0000000..5abee04
--- /dev/null
+++ b/modules/web-console/src/main/js/controllers/admin-controller.js
@@ -0,0 +1,92 @@
+/*
+ * 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.
+import consoleModule from 'controllers/common-module';
+
+consoleModule.controller('adminController', [
+    '$rootScope', '$scope', '$http', '$q', '$common', '$confirm', '$state', 'User', 'IgniteCountries',
+    ($rootScope, $scope, $http, $q, $common, $confirm, $state, User, Countries) => {
+        $scope.users = null;
+
+        const _reloadUsers = () => {
+            $http.post('/api/v1/admin/list')
+                .then(({data}) => {
+                    $scope.users = data;
+
+                    _.forEach($scope.users, (user) => {
+                        user.userName = user.firstName + ' ' + user.lastName;
+                        user.countryCode = Countries.getByName(user.country).code;
+                        user.label = user.userName + ' ' + user.email + ' ' +
+                            (user.company || '') + ' ' + (user.country || '');
+                    })
+                })
+                .catch((err) => $common.showError(err));
+        };
+
+        _reloadUsers();
+
+        $scope.becomeUser = function (user) {
+            $http.get('/api/v1/admin/become', { params: {viewedUserId: user._id}})
+                .then(User.read)
+                .then((user) => {
+                    $rootScope.$broadcast('user', user);
+
+                    $state.go('base.configuration.clusters')
+                })
+                .catch((errMsg) => $common.showError($common.errorMessage(errMsg)));
+        };
+
+        $scope.removeUser = (user) => {
+            $confirm.confirm('Are you sure you want to remove user: "' + user.userName + '"?')
+                .then(() => {
+                    $http.post('/api/v1/admin/remove', {userId: user._id})
+                        .success(() => {
+                            const i = _.findIndex($scope.users, (u) => u._id === user._id);
+
+                            if (i >= 0)
+                                $scope.users.splice(i, 1);
+
+                            $common.showInfo('User has been removed: "' + user.userName + '"');
+                        })
+                        .error((errMsg, status) => {
+                            if (status == 503)
+                                $common.showInfo(errMsg);
+                            else
+                                $common.showError('Failed to remove user: "' + $common.errorMessage(errMsg) + '"');
+                        });
+                });
+        };
+
+        $scope.toggleAdmin = (user) => {
+            if (user.adminChanging)
+                return;
+
+            user.adminChanging = true;
+
+            $http.post('/api/v1/admin/save', {userId: user._id, adminFlag: !user.admin})
+                .success(() => {
+                    user.admin = !user.admin;
+
+                    $common.showInfo('Admin right was successfully toggled for user: "' + user.userName + '"');
+                }).error((err) => {
+                    $common.showError('Failed to toggle admin right for user: "' + $common.errorMessage(err) + '"');
+                })
+                .finally(() => user.adminChanging = false);
+        };
+    }]
+);

http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/controllers/caches-controller.js
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/controllers/caches-controller.js b/modules/web-console/src/main/js/controllers/caches-controller.js
new file mode 100644
index 0000000..8f4bedd
--- /dev/null
+++ b/modules/web-console/src/main/js/controllers/caches-controller.js
@@ -0,0 +1,493 @@
+/*
+ * 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.
+import consoleModule from 'controllers/common-module';
+
+consoleModule.controller('cachesController', [
+    '$scope', '$http', '$state', '$filter', '$timeout', '$common', '$confirm', '$clone', '$loading', '$cleanup', '$unsavedChangesGuard',
+    function ($scope, $http, $state, $filter, $timeout, $common, $confirm, $clone, $loading, $cleanup, $unsavedChangesGuard) {
+        $unsavedChangesGuard.install($scope);
+
+        var emptyCache = {empty: true};
+
+        var __original_value;
+
+        var blank = {
+            evictionPolicy: {},
+            cacheStoreFactory: {},
+            nearConfiguration: {}
+        };
+
+        // We need to initialize backupItem with empty object in order to properly used from angular directives.
+        $scope.backupItem = emptyCache;
+
+        $scope.ui = $common.formUI();
+        $scope.ui.activePanels = [0];
+        $scope.ui.topPanels = [0, 1, 2, 3];
+
+        $scope.hidePopover = $common.hidePopover;
+        $scope.saveBtnTipText = $common.saveBtnTipText;
+        $scope.widthIsSufficient = $common.widthIsSufficient;
+
+        var showPopoverMessage = $common.showPopoverMessage;
+
+        $scope.contentVisible = function () {
+            var item = $scope.backupItem;
+
+            return !item.empty && (!item._id || _.find($scope.displayedRows, {_id: item._id}));
+        };
+
+        $scope.toggleExpanded = function () {
+            $scope.ui.expanded = !$scope.ui.expanded;
+
+            $common.hidePopover();
+        };
+
+        $scope.caches = [];
+        $scope.domains = [];
+
+        function _cacheLbl(cache) {
+            return cache.name + ', ' + cache.cacheMode + ', ' + cache.atomicityMode;
+        }
+
+        function selectFirstItem() {
+            if ($scope.caches.length > 0)
+                $scope.selectItem($scope.caches[0]);
+        }
+
+        function cacheDomains(item) {
+            return _.reduce($scope.domains, function (memo, domain) {
+                if (item && _.includes(item.domains, domain.value)) {
+                    memo.push(domain.meta);
+                }
+
+                return memo;
+            }, []);
+        }
+
+        $loading.start('loadingCachesScreen');
+
+        // When landing on the page, get caches and show them.
+        $http.post('/api/v1/configuration/caches/list')
+            .success(function (data) {
+                var validFilter = $filter('domainsValidation');
+
+                $scope.spaces = data.spaces;
+
+                _.forEach(data.caches, function (cache) {
+                    cache.label = _cacheLbl(cache);
+                });
+
+                $scope.caches = data.caches;
+
+                $scope.clusters = _.map(data.clusters, function (cluster) {
+                    return {
+                        value: cluster._id,
+                        label: cluster.name,
+                        caches: cluster.caches
+                    };
+                });
+
+                $scope.domains = _.sortBy(_.map(validFilter(data.domains, true, false), function (domain) {
+                    return {
+                        value: domain._id,
+                        label: domain.valueType,
+                        kind: domain.kind,
+                        meta: domain
+                    };
+                }), 'label');
+
+                if ($state.params.id)
+                    $scope.createItem($state.params.id);
+                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('ui.inputForm.$valid', function(valid) {
+                    if (valid && __original_value === JSON.stringify($cleanup($scope.backupItem))) {
+                        $scope.ui.inputForm.$dirty = false;
+                    }
+                });
+
+                $scope.$watch('backupItem', function (val) {
+                    var form = $scope.ui.inputForm;
+
+                    if (form.$pristine || (form.$valid && __original_value === JSON.stringify($cleanup(val))))
+                        form.$setPristine();
+                    else
+                        form.$setDirty();
+                }, true);
+            })
+            .catch(function (errMsg) {
+                $common.showError(errMsg);
+            })
+            .finally(function () {
+                $scope.ui.ready = true;
+                $scope.ui.inputForm.$setPristine();
+                $loading.finish('loadingCachesScreen');
+            });
+
+        $scope.selectItem = function (item, backup) {
+            function selectItem() {
+                $scope.selectedItem = item;
+
+                try {
+                    if (item && item._id)
+                        sessionStorage.lastSelectedCache = angular.toJson(item._id);
+                    else
+                        sessionStorage.removeItem('lastSelectedCache');
+                }
+                catch (ignored) {
+                    // No-op.
+                }
+
+                if (backup)
+                    $scope.backupItem = backup;
+                else if (item)
+                    $scope.backupItem = angular.copy(item);
+                else
+                    $scope.backupItem = emptyCache;
+
+                $scope.backupItem = angular.merge({}, blank, $scope.backupItem);
+
+                __original_value = JSON.stringify($cleanup($scope.backupItem));
+
+                if ($common.getQueryVariable('new'))
+                    $state.go('base.configuration.caches');
+            }
+
+            $common.confirmUnsavedChanges($scope.backupItem && $scope.ui.inputForm.$dirty, selectItem);
+        };
+
+        function prepareNewItem(id) {
+            return {
+                space: $scope.spaces[0]._id,
+                cacheMode: 'PARTITIONED',
+                atomicityMode: 'ATOMIC',
+                readFromBackup: true,
+                copyOnRead: true,
+                clusters: id && _.find($scope.clusters, {value: id})
+                    ? [id] : _.map($scope.clusters, function (cluster) { return cluster.value; }),
+                domains: id && _.find($scope.domains, { value: id }) ? [id] : [],
+                cacheStoreFactory: {CacheJdbcBlobStoreFactory: {connectVia: 'DataSource'}}
+            };
+        }
+
+        // Add new cache.
+        $scope.createItem = function (id) {
+            $timeout(function () {
+                $common.ensureActivePanel($scope.ui, 'general', 'cacheName');
+            });
+
+            $scope.selectItem(undefined, prepareNewItem(id));
+        };
+
+        function checkDataSources() {
+            var clusters = _.filter($scope.clusters, function (cluster) {
+                return _.includes($scope.backupItem.clusters, cluster.value);
+            });
+
+            var checkRes = { checked: true };
+
+            var failCluster = _.find(clusters, function (cluster) {
+                var caches = _.filter($scope.caches, function (cache) {
+                    return cache._id !== $scope.backupItem._id && _.find(cluster.caches, function (clusterCache) {
+                        return clusterCache === cache._id;
+                    });
+                });
+
+                caches.push($scope.backupItem);
+
+                checkRes = $common.checkCachesDataSources(caches, $scope.backupItem);
+
+                return !checkRes.checked;
+            });
+
+            if (!checkRes.checked) {
+                return showPopoverMessage($scope.ui, 'store', checkRes.firstCache.cacheStoreFactory.kind === 'CacheJdbcPojoStoreFactory' ? 'pojoDialect' : 'blobDialect',
+                    'Found cache "' + checkRes.secondCache.name + '" in cluster "' + failCluster.label + '" ' +
+                    'with the same data source bean name "' + checkRes.firstCache.cacheStoreFactory[checkRes.firstCache.cacheStoreFactory.kind].dataSourceBean +
+                    '" and different database: "' + $common.cacheStoreJdbcDialectsLabel(checkRes.firstDB) + '" in current cache and "' +
+                    $common.cacheStoreJdbcDialectsLabel(checkRes.secondDB) + '" in "' + checkRes.secondCache.name + '"', 10000);
+            }
+
+            return true;
+        }
+
+        // Check cache logical consistency.
+        function validate(item) {
+            $common.hidePopover();
+
+            if ($common.isEmptyString(item.name))
+                return showPopoverMessage($scope.ui, 'general', 'cacheName', 'Cache name should not be empty!');
+
+            if (item.memoryMode === 'ONHEAP_TIERED' && item.offHeapMaxMemory > 0 && !$common.isDefined(item.evictionPolicy.kind))
+                return showPopoverMessage($scope.ui, 'memory', 'evictionPolicyKind', 'Eviction policy should not be configured!');
+
+            var form = $scope.ui.inputForm;
+            var errors = form.$error;
+            var errKeys = Object.keys(errors);
+
+            if (errKeys && errKeys.length > 0) {
+                var firstErrorKey = errKeys[0];
+
+                var firstError = errors[firstErrorKey][0];
+                var actualError = firstError.$error[firstErrorKey][0];
+
+                var errNameFull = actualError.$name;
+                var errNameShort = errNameFull;
+
+                if (errNameShort.endsWith('TextInput'))
+                    errNameShort = errNameShort.substring(0, errNameShort.length - 9);
+
+                var extractErrorMessage = function (errName) {
+                    try {
+                        return errors[firstErrorKey][0].$errorMessages[errName][firstErrorKey];
+                    }
+                    catch(ignored) {
+                        try {
+                            msg = form[firstError.$name].$errorMessages[errName][firstErrorKey];
+                        }
+                        catch(ignited) {
+                            return false;
+                        }
+                    }
+                };
+
+                var msg = extractErrorMessage(errNameFull) || extractErrorMessage(errNameShort) || 'Invalid value!';
+
+                return showPopoverMessage($scope.ui, firstError.$name, errNameFull, msg);
+            }
+
+            if (item.memoryMode === 'OFFHEAP_VALUES' && !_.isEmpty(item.domains))
+                return showPopoverMessage($scope.ui, 'memory', 'memoryMode',
+                    'Query indexing could not be enabled while values are stored off-heap!');
+
+            if (item.memoryMode === 'OFFHEAP_TIERED' && !$common.isDefined(item.offHeapMaxMemory))
+                return showPopoverMessage($scope.ui, 'memory', 'offHeapMaxMemory',
+                    'Off-heap max memory should be specified!');
+
+            var cacheStoreFactorySelected = item.cacheStoreFactory && item.cacheStoreFactory.kind;
+
+            if (cacheStoreFactorySelected) {
+                var storeFactory = item.cacheStoreFactory[item.cacheStoreFactory.kind];
+
+                if (item.cacheStoreFactory.kind === 'CacheJdbcPojoStoreFactory') {
+                    if ($common.isEmptyString(storeFactory.dataSourceBean))
+                        return showPopoverMessage($scope.ui, 'store', 'dataSourceBean',
+                            'Data source bean name should not be empty!');
+
+                    if (!$common.isValidJavaIdentifier('Data source bean', storeFactory.dataSourceBean, 'dataSourceBean', $scope.ui, 'store'))
+                        return false;
+
+                    if (!storeFactory.dialect)
+                        return showPopoverMessage($scope.ui, 'store', 'pojoDialect',
+                            'Dialect should not be empty!');
+
+                    if (!checkDataSources())
+                        return false;
+                }
+
+                if (item.cacheStoreFactory.kind === 'CacheJdbcBlobStoreFactory') {
+                    if (storeFactory.connectVia === 'URL') {
+                        if ($common.isEmptyString(storeFactory.connectionUrl))
+                            return showPopoverMessage($scope.ui, 'store', 'connectionUrl',
+                                'Connection URL should not be empty!');
+
+                        if ($common.isEmptyString(storeFactory.user))
+                            return showPopoverMessage($scope.ui, 'store', 'user',
+                                'User should not be empty!');
+                    }
+                    else {
+                        if ($common.isEmptyString(storeFactory.dataSourceBean))
+                            return showPopoverMessage($scope.ui, 'store', 'dataSourceBean',
+                                'Data source bean name should not be empty!');
+
+                        if (!$common.isValidJavaIdentifier('Data source bean', storeFactory.dataSourceBean, 'dataSourceBean', $scope.ui, 'store'))
+                            return false;
+
+                        if (!storeFactory.dialect)
+                            return showPopoverMessage($scope.ui, 'store', 'blobDialect',
+                                'Database should not be empty!');
+
+                        if (!checkDataSources())
+                            return false;
+                    }
+                }
+            }
+
+            if ((item.readThrough || item.writeThrough) && !cacheStoreFactorySelected)
+                return showPopoverMessage($scope.ui, 'store', 'cacheStoreFactory',
+                    (item.readThrough ? 'Read' : 'Write') + ' through are enabled but store is not configured!');
+
+            if (item.writeBehindEnabled && !cacheStoreFactorySelected)
+                return showPopoverMessage($scope.ui, 'store', 'cacheStoreFactory',
+                    'Write behind enabled but store is not configured!');
+
+            if (cacheStoreFactorySelected) {
+                if (!item.readThrough && !item.writeThrough)
+                    return showPopoverMessage($scope.ui, 'store', 'readThroughTooltip',
+                        'Store is configured but read/write through are not enabled!');
+            }
+
+            if (item.writeBehindFlushSize === 0 && item.writeBehindFlushFrequency === 0)
+                return showPopoverMessage($scope.ui, 'store', 'writeBehindFlushSize',
+                    'Both "Flush frequency" and "Flush size" are not allowed as 0!');
+
+            if (item.cacheMode !== 'LOCAL' && item.rebalanceMode !== 'NONE' && item.rebalanceBatchSize === 0)
+                return showPopoverMessage($scope.ui, 'rebalance', 'rebalanceBatchSize',
+                    'Batch size should be more than 0 if rebalance mode is "SYNC" or "ASYNC" !', 10000);
+
+            return true;
+        }
+
+        // Save cache in database.
+        function save(item) {
+            $http.post('/api/v1/configuration/caches/save', item)
+                .success(function (_id) {
+                    item.label = _cacheLbl(item);
+
+                    $scope.ui.inputForm.$setPristine();
+
+                    var idx = _.findIndex($scope.caches, function (cache) {
+                        return cache._id === _id;
+                    });
+
+                    if (idx >= 0)
+                        angular.merge($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 () {
+            var item = $scope.backupItem;
+
+            angular.extend(item, $common.autoCacheStoreConfiguration(item, cacheDomains(item)));
+
+            if (validate(item))
+                save(item);
+        };
+
+        function _cacheNames() {
+            return _.map($scope.caches, function (cache) {
+                return cache.name;
+            });
+        }
+
+        // Clone cache with new name.
+        $scope.cloneItem = function () {
+            if (validate($scope.backupItem)) {
+                $clone.confirm($scope.backupItem.name, _cacheNames()).then(function (newName) {
+                    var item = angular.copy($scope.backupItem);
+
+                    delete item._id;
+
+                    item.name = newName;
+
+                    save(item);
+                });
+            }
+        };
+
+        // Remove cache from db.
+        $scope.removeItem = function () {
+            var selectedItem = $scope.selectedItem;
+
+            $confirm.confirm('Are you sure you want to remove cache: "' + selectedItem.name + '"?')
+                .then(function () {
+                    var _id = selectedItem._id;
+
+                    $http.post('/api/v1/configuration/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.backupItem = emptyCache;
+                            }
+                        })
+                        .error(function (errMsg) {
+                            $common.showError(errMsg);
+                        });
+                });
+        };
+
+        // Remove all caches from db.
+        $scope.removeAllItems = function () {
+            $confirm.confirm('Are you sure you want to remove all caches?')
+                .then(function () {
+                    $http.post('/api/v1/configuration/caches/remove/all')
+                        .success(function () {
+                            $common.showInfo('All caches have been removed');
+
+                            $scope.caches = [];
+                            $scope.backupItem = emptyCache;
+                            $scope.ui.inputForm.$setPristine();
+                        })
+                        .error(function (errMsg) {
+                            $common.showError(errMsg);
+                        });
+                });
+        };
+
+        $scope.resetAll = function () {
+            $confirm.confirm('Are you sure you want to undo all changes for current cache?')
+                .then(function () {
+                    $scope.backupItem = $scope.selectedItem ? angular.copy($scope.selectedItem) : prepareNewItem();
+                    $scope.ui.inputForm.$setPristine();
+                });
+        };
+    }]
+);

http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/controllers/clusters-controller.js
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/controllers/clusters-controller.js b/modules/web-console/src/main/js/controllers/clusters-controller.js
new file mode 100644
index 0000000..8aecc9f
--- /dev/null
+++ b/modules/web-console/src/main/js/controllers/clusters-controller.js
@@ -0,0 +1,555 @@
+/*
+ * 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 Clusters screen.
+import consoleModule from 'controllers/common-module';
+
+consoleModule.controller('clustersController', [
+    '$rootScope', '$scope', '$http', '$state', '$timeout', '$common', '$confirm', '$clone', '$loading', '$cleanup', '$unsavedChangesGuard', 'igniteEventGroups', 'DemoInfo',
+    function ($root, $scope, $http, $state, $timeout, $common, $confirm, $clone, $loading, $cleanup, $unsavedChangesGuard, igniteEventGroups, DemoInfo) {
+        $unsavedChangesGuard.install($scope);
+
+        var emptyCluster = {empty: true};
+
+        var __original_value;
+
+        var blank = {
+            atomicConfiguration: {},
+            binaryConfiguration: {},
+            communication: {},
+            connector: {},
+            discovery: {},
+            marshaller: {},
+            sslContextFactory: {},
+            swapSpaceSpi: {},
+            transactionConfiguration: {}
+        };
+
+        // We need to initialize backupItem with empty object in order to properly used from angular directives.
+        $scope.backupItem = emptyCluster;
+
+        $scope.ui = $common.formUI();
+        $scope.ui.activePanels = [0];
+        $scope.ui.topPanels = [0];
+
+        $scope.hidePopover = $common.hidePopover;
+        $scope.saveBtnTipText = $common.saveBtnTipText;
+        $scope.widthIsSufficient = $common.widthIsSufficient;
+
+        var showPopoverMessage = $common.showPopoverMessage;
+
+        $scope.contentVisible = function () {
+            var item = $scope.backupItem;
+
+            return !item.empty && (!item._id || _.find($scope.displayedRows, {_id: item._id}));
+        };
+
+        $scope.toggleExpanded = function () {
+            $scope.ui.expanded = !$scope.ui.expanded;
+
+            $common.hidePopover();
+        };
+
+        $scope.discoveries = [
+            {value: 'Vm', label: 'Static IPs'},
+            {value: 'Multicast', label: 'Multicast'},
+            {value: 'S3', label: 'AWS S3'},
+            {value: 'Cloud', label: 'Apache jclouds'},
+            {value: 'GoogleStorage', label: 'Google cloud storage'},
+            {value: 'Jdbc', label: 'JDBC'},
+            {value: 'SharedFs', label: 'Shared filesystem'},
+            {value: 'ZooKeeper', label: 'Apache ZooKeeper'}
+        ];
+
+        $scope.swapSpaceSpis = [
+            {value: 'FileSwapSpaceSpi', label: 'File-based swap'},
+            {value: undefined, label: 'Not set'}
+        ];
+
+        $scope.eventGroups = igniteEventGroups;
+
+        $scope.clusters = [];
+
+        function _clusterLbl (cluster) {
+            return cluster.name + ', ' + _.find($scope.discoveries, {value: cluster.discovery.kind}).label;
+        }
+
+        function selectFirstItem() {
+            if ($scope.clusters.length > 0)
+                $scope.selectItem($scope.clusters[0]);
+        }
+
+        function clusterCaches(item) {
+            return _.reduce($scope.caches, function (memo, cache) {
+                if (item && _.includes(item.caches, cache.value)) {
+                    memo.push(cache.cache);
+                }
+
+                return memo;
+            }, []);
+        }
+
+        $loading.start('loadingClustersScreen');
+
+        // When landing on the page, get clusters and show them.
+        $http.post('/api/v1/configuration/clusters/list')
+            .success(function (data) {
+                $scope.spaces = data.spaces;
+
+                _.forEach(data.clusters, function (cluster) {
+                    cluster.label = _clusterLbl(cluster);
+                });
+
+                $scope.clusters = data.clusters;
+
+                $scope.caches = _.map(data.caches, function (cache) {
+                    return {value: cache._id, label: cache.name, cache: cache};
+                });
+
+                $scope.igfss = _.map(data.igfss, function (igfs) {
+                    return {value: igfs._id, label: igfs.name, igfs: igfs};
+                });
+
+                if ($state.params.id)
+                    $scope.createItem($state.params.id);
+                else {
+                    var lastSelectedCluster = angular.fromJson(sessionStorage.lastSelectedCluster);
+
+                    if (lastSelectedCluster) {
+                        var idx = _.findIndex($scope.clusters, function (cluster) {
+                            return cluster._id === lastSelectedCluster;
+                        });
+
+                        if (idx >= 0)
+                            $scope.selectItem($scope.clusters[idx]);
+                        else {
+                            sessionStorage.removeItem('lastSelectedCluster');
+
+                            selectFirstItem();
+                        }
+                    }
+                    else
+                        selectFirstItem();
+                }
+
+                $scope.$watch('ui.inputForm.$valid', function(valid) {
+                    if (valid && __original_value === JSON.stringify($cleanup($scope.backupItem))) {
+                        $scope.ui.inputForm.$dirty = false;
+                    }
+                });
+
+                $scope.$watch('backupItem', function (val) {
+                    var form = $scope.ui.inputForm;
+
+                    if (form.$pristine || (form.$valid && __original_value === JSON.stringify($cleanup(val))))
+                        form.$setPristine();
+                    else
+                        form.$setDirty();
+                }, true);
+
+                if ($root.IgniteDemoMode) {
+                    if (sessionStorage.showDemoInfo !== 'true') {
+                        sessionStorage.showDemoInfo = 'true';
+
+                        DemoInfo.show();
+                    }
+                }
+
+            })
+            .catch(function (errMsg) {
+                $common.showError(errMsg);
+            })
+            .finally(function () {
+                $scope.ui.ready = true;
+                $scope.ui.inputForm.$setPristine();
+                $loading.finish('loadingClustersScreen');
+            });
+
+        $scope.selectItem = function (item, backup) {
+            function selectItem() {
+                $scope.selectedItem = item;
+
+                try {
+                    if (item && item._id)
+                        sessionStorage.lastSelectedCluster = angular.toJson(item._id);
+                    else
+                        sessionStorage.removeItem('lastSelectedCluster');
+                }
+                catch (ignored) {
+                    // No-op.
+                }
+
+                if (backup)
+                    $scope.backupItem = backup;
+                else if (item)
+                    $scope.backupItem = angular.copy(item);
+                else
+                    $scope.backupItem = emptyCluster ;
+
+                $scope.backupItem = angular.merge({}, blank, $scope.backupItem);
+
+                __original_value = JSON.stringify($cleanup($scope.backupItem));
+
+                if ($common.getQueryVariable('new'))
+                    $state.go('base.configuration.clusters');
+            }
+
+            $common.confirmUnsavedChanges($scope.backupItem && $scope.ui.inputForm.$dirty, selectItem);
+        };
+
+        function prepareNewItem(id) {
+            var newItem = {
+                discovery: {
+                    kind: 'Multicast',
+                    Vm: {addresses: ['127.0.0.1:47500..47510']},
+                    Multicast: {addresses: ['127.0.0.1:47500..47510']}
+                },
+                binaryConfiguration: {
+                    typeConfigurations: [],
+                    compactFooter: true
+                },
+                communication: {
+                    tcpNoDelay: true
+                },
+                connector: {
+                    noDelay: true
+                }
+            };
+
+            newItem = angular.merge({}, blank, newItem);
+
+            newItem.caches = id && _.find($scope.caches, {value: id}) ? [id] : [];
+            newItem.igfss = id && _.find($scope.igfss, {value: id}) ? [id] : [];
+            newItem.space = $scope.spaces[0]._id;
+
+            return newItem;
+        }
+
+        // Add new cluster.
+        $scope.createItem = function(id) {
+            $timeout(function () {
+                $common.ensureActivePanel($scope.ui, "general", 'clusterName');
+            });
+
+            $scope.selectItem(undefined, prepareNewItem(id));
+        };
+
+        $scope.indexOfCache = function (cacheId) {
+            return _.findIndex($scope.caches, function (cache) {
+                return cache.value === cacheId;
+            });
+        };
+
+        // Check cluster logical consistency.
+        function validate(item) {
+            $common.hidePopover();
+
+            if ($common.isEmptyString(item.name))
+                return showPopoverMessage($scope.ui, 'general', 'clusterName', 'Cluster name should not be empty!');
+
+            var form = $scope.ui.inputForm;
+            var errors = form.$error;
+            var errKeys = Object.keys(errors);
+
+            if (errKeys && errKeys.length > 0) {
+                var firstErrorKey = errKeys[0];
+
+                var firstError = errors[firstErrorKey][0];
+                var actualError = firstError.$error[firstErrorKey][0];
+
+                var errNameFull = actualError.$name;
+                var errNameShort = errNameFull;
+
+                if (errNameShort.endsWith('TextInput'))
+                    errNameShort = errNameShort.substring(0, errNameShort.length - 9);
+
+                var extractErrorMessage = function (errName) {
+                    try {
+                        return errors[firstErrorKey][0].$errorMessages[errName][firstErrorKey];
+                    }
+                    catch(ignored) {
+                        try {
+                            msg = form[firstError.$name].$errorMessages[errName][firstErrorKey];
+                        }
+                        catch(ignited) {
+                            return false;
+                        }
+                    }
+                };
+
+                var msg = extractErrorMessage(errNameFull) || extractErrorMessage(errNameShort) || 'Invalid value!';
+
+                return showPopoverMessage($scope.ui, firstError.$name, errNameFull, msg);
+            }
+
+            var caches = _.filter(_.map($scope.caches, function (scopeCache) {
+                return scopeCache.cache;
+            }), function (cache) {
+                return _.includes($scope.backupItem.caches, cache._id);
+            });
+
+            var checkRes = $common.checkCachesDataSources(caches);
+
+            if (!checkRes.checked) {
+                return showPopoverMessage($scope.ui, 'general', 'caches',
+                    'Found caches "' + checkRes.firstCache.name + '" and "' + checkRes.secondCache.name + '" ' +
+                    'with the same data source bean name "' + checkRes.firstCache.cacheStoreFactory[checkRes.firstCache.cacheStoreFactory.kind].dataSourceBean +
+                    '" and different databases: "' + $common.cacheStoreJdbcDialectsLabel(checkRes.firstDB) + '" in "' + checkRes.firstCache.name + '" and "' +
+                    $common.cacheStoreJdbcDialectsLabel(checkRes.secondDB) + '" in "' + checkRes.secondCache.name + '"', 10000);
+            }
+
+            var b = item.binaryConfiguration;
+
+            if ($common.isDefined(b)) {
+                if (!_.isEmpty(b.typeConfigurations)) {
+                    var sameName = function (t, ix) {
+                        return ix < typeIx && t.typeName === type.typeName;
+                    };
+
+                    for (var typeIx = 0; typeIx < b.typeConfigurations.length; typeIx++) {
+                        var type = b.typeConfigurations[typeIx];
+
+                        if ($common.isEmptyString(type.typeName))
+                            return showPopoverMessage($scope.ui, 'binary', 'typeName' + typeIx, 'Type name should be specified!');
+
+                        if (_.find(b.typeConfigurations, sameName))
+                            return showPopoverMessage($scope.ui, 'binary', 'typeName' + typeIx, 'Type with such name is already specified!');
+                    }
+                }
+            }
+
+            var c = item.communication;
+
+            if ($common.isDefined(c)) {
+                if ($common.isDefined(c.unacknowledgedMessagesBufferSize)) {
+                    if ($common.isDefined(c.messageQueueLimit))
+                        if (c.unacknowledgedMessagesBufferSize < 5 * c.messageQueueLimit)
+                            return showPopoverMessage($scope.ui, 'communication', 'unacknowledgedMessagesBufferSize', 'Maximum number of stored unacknowledged messages should be at least 5 * message queue limit!');
+
+                    if ($common.isDefined(c.ackSendThreshold))
+                        if (c.unacknowledgedMessagesBufferSize < 5 * c.ackSendThreshold)
+                            return showPopoverMessage($scope.ui, 'communication', 'unacknowledgedMessagesBufferSize', 'Maximum number of stored unacknowledged messages should be at least 5 * ack send threshold!');
+                }
+
+                if (c.sharedMemoryPort === 0)
+                    return showPopoverMessage($scope.ui, 'communication', 'sharedMemoryPort', 'Shared memory port should be more than "0" or equals to "-1"!');
+            }
+
+            var r = item.connector;
+
+            if ($common.isDefined(r)) {
+                if (r.sslEnabled && $common.isEmptyString(r.sslFactory))
+                    return showPopoverMessage($scope.ui, 'connector', 'connectorSslFactory', 'SSL factory should not be empty!');
+            }
+
+            var d = item.discovery;
+
+            if (d) {
+                if ((d.maxAckTimeout != undefined ? d.maxAckTimeout : 600000) < (d.ackTimeout || 5000))
+                    return showPopoverMessage($scope.ui, 'discovery', 'ackTimeout', 'Acknowledgement timeout should be less than max acknowledgement timeout!');
+
+                if (d.kind === 'Vm' && d.Vm && d.Vm.addresses.length === 0)
+                    return showPopoverMessage($scope.ui, 'general', 'addresses', 'Addresses are not specified!');
+
+                if (d.kind === 'S3' && d.S3 && $common.isEmptyString(d.S3.bucketName))
+                    return showPopoverMessage($scope.ui, 'general', 'bucketName', 'Bucket name should not be empty!');
+
+                if (d.kind === 'Cloud' && d.Cloud) {
+                    if ($common.isEmptyString(d.Cloud.identity))
+                        return showPopoverMessage($scope.ui, 'general', 'identity', 'Identity should not be empty!');
+
+                    if ($common.isEmptyString(d.Cloud.provider))
+                        return showPopoverMessage($scope.ui, 'general', 'provider', 'Provider should not be empty!');
+                }
+
+                if (d.kind === 'GoogleStorage' && d.GoogleStorage) {
+                    if ($common.isEmptyString(d.GoogleStorage.projectName))
+                        return showPopoverMessage($scope.ui, 'general', 'projectName', 'Project name should not be empty!');
+
+                    if ($common.isEmptyString(d.GoogleStorage.bucketName))
+                        return showPopoverMessage($scope.ui, 'general', 'bucketName', 'Bucket name should not be empty!');
+
+                    if ($common.isEmptyString(d.GoogleStorage.serviceAccountP12FilePath))
+                        return showPopoverMessage($scope.ui, 'general', 'serviceAccountP12FilePath', 'Private key path should not be empty!');
+
+                    if ($common.isEmptyString(d.GoogleStorage.serviceAccountId))
+                        return showPopoverMessage($scope.ui, 'general', 'serviceAccountId', 'Account ID should not be empty!');
+                }
+            }
+
+            var swapKind = item.swapSpaceSpi && item.swapSpaceSpi.kind;
+
+            if (swapKind && item.swapSpaceSpi[swapKind]) {
+                var swap = item.swapSpaceSpi[swapKind];
+
+                var sparsity = swap.maximumSparsity;
+
+                if ($common.isDefined(sparsity) && (sparsity < 0 || sparsity >= 1))
+                    return showPopoverMessage($scope.ui, 'swap', 'maximumSparsity', 'Maximum sparsity should be more or equal 0 and less than 1!');
+
+                var readStripesNumber = swap.readStripesNumber;
+
+                if (readStripesNumber && !(readStripesNumber == -1 || (readStripesNumber & (readStripesNumber - 1)) == 0))
+                    return showPopoverMessage($scope.ui, 'swap', 'readStripesNumber', 'Read stripe size must be positive and power of two!');
+            }
+
+            if (item.sslEnabled) {
+                if (!$common.isDefined(item.sslContextFactory) || $common.isEmptyString(item.sslContextFactory.keyStoreFilePath))
+                    return showPopoverMessage($scope.ui, 'sslConfiguration', 'keyStoreFilePath', 'Key store file should not be empty!');
+
+                if ($common.isEmptyString(item.sslContextFactory.trustStoreFilePath) && _.isEmpty(item.sslContextFactory.trustManagers))
+                    return showPopoverMessage($scope.ui, 'sslConfiguration', 'sslConfiguration-title', 'Trust storage file or managers should be configured!');
+            }
+
+            if (!swapKind && item.caches) {
+                for (var i = 0; i < item.caches.length; i++) {
+                    var idx = $scope.indexOfCache(item.caches[i]);
+
+                    if (idx >= 0) {
+                        var cache = $scope.caches[idx];
+
+                        if (cache.cache.swapEnabled)
+                            return showPopoverMessage($scope.ui, 'swap', 'swapSpaceSpi',
+                                'Swap space SPI is not configured, but cache "' + cache.label + '" configured to use swap!');
+                    }
+                }
+            }
+
+            if (item.rebalanceThreadPoolSize && item.systemThreadPoolSize && item.systemThreadPoolSize <= item.rebalanceThreadPoolSize)
+                return showPopoverMessage($scope.ui, 'pools', 'rebalanceThreadPoolSize',
+                    'Rebalance thread pool size exceed or equals System thread pool size!');
+
+            return true;
+        }
+
+        // Save cluster in database.
+        function save(item) {
+            $http.post('/api/v1/configuration/clusters/save', item)
+                .success(function (_id) {
+                    item.label = _clusterLbl(item);
+
+                    $scope.ui.inputForm.$setPristine();
+
+                    var idx = _.findIndex($scope.clusters, (cluster) => cluster._id === _id);
+
+                    if (idx >= 0)
+                        angular.merge($scope.clusters[idx], item);
+                    else {
+                        item._id = _id;
+                        $scope.clusters.push(item);
+                    }
+
+                    $scope.selectItem(item);
+
+                    $common.showInfo('Cluster "' + item.name + '" saved.');
+                })
+                .error((err) => $common.showError(err));
+        }
+
+        // Save cluster.
+        $scope.saveItem = function () {
+            var item = $scope.backupItem;
+
+            var swapSpi = $common.autoClusterSwapSpiConfiguration(item, clusterCaches(item));
+
+            if (swapSpi)
+                angular.extend(item, swapSpi);
+
+            if (validate(item))
+                save(item);
+        };
+
+        function _clusterNames() {
+            return _.map($scope.clusters, function (cluster) {
+                return cluster.name;
+            });
+        }
+
+        // Clone cluster with new name.
+        $scope.cloneItem = function () {
+            if (validate($scope.backupItem)) {
+                $clone.confirm($scope.backupItem.name, _clusterNames()).then(function (newName) {
+                    var item = angular.copy($scope.backupItem);
+
+                    delete item._id;
+                    item.name = newName;
+
+                    save(item);
+                });
+            }
+        };
+
+        // Remove cluster from db.
+        $scope.removeItem = function () {
+            var selectedItem = $scope.selectedItem;
+
+            $confirm.confirm('Are you sure you want to remove cluster: "' + selectedItem.name + '"?')
+                .then(function () {
+                    var _id = selectedItem._id;
+
+                    $http.post('/api/v1/configuration/clusters/remove', {_id: _id})
+                        .success(function () {
+                            $common.showInfo('Cluster has been removed: ' + selectedItem.name);
+
+                            var clusters = $scope.clusters;
+
+                            var idx = _.findIndex(clusters, function (cluster) {
+                                return cluster._id === _id;
+                            });
+
+                            if (idx >= 0) {
+                                clusters.splice(idx, 1);
+
+                                if (clusters.length > 0)
+                                    $scope.selectItem(clusters[0]);
+                                else
+                                    $scope.backupItem = emptyCluster;
+                            }
+                        })
+                        .error(function (errMsg) {
+                            $common.showError(errMsg);
+                        });
+                });
+        };
+
+        // Remove all clusters from db.
+        $scope.removeAllItems = function () {
+            $confirm.confirm('Are you sure you want to remove all clusters?')
+                .then(function () {
+                    $http.post('/api/v1/configuration/clusters/remove/all')
+                        .success(function () {
+                            $common.showInfo('All clusters have been removed');
+
+                            $scope.clusters = [];
+                            $scope.backupItem = emptyCluster;
+                            $scope.ui.inputForm.$setPristine();
+                        })
+                        .error(function (errMsg) {
+                            $common.showError(errMsg);
+                        });
+                });
+        };
+
+        $scope.resetAll = function() {
+            $confirm.confirm('Are you sure you want to undo all changes for current cluster?')
+                .then(function() {
+                    $scope.backupItem = $scope.selectedItem ? angular.copy($scope.selectedItem) : prepareNewItem();
+                    $scope.ui.inputForm.$setPristine();
+                });
+        };
+    }]
+);


Mime
View raw message